Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .codex/skills/create-pr/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
name: create-pr
description: Create GitHub pull requests with the gh CLI using the reductstore/.github PR template, then update CHANGELOG.md with the created PR ID and commit the changelog (do not push). Use when a user asks to open a PR and record its ID in the changelog.
---

# Create Pr

## Overview

Create a PR using the standardized template from reductstore/.github, then record the PR number in CHANGELOG.md and commit the changelog without pushing.

## Workflow

### 1) Preconditions
- Ensure `gh auth status` is logged in and the current branch is the intended PR branch.
- Ensure the working tree is clean (or only the intended changes are present).
- Use context from the current conversation to infer intent, scope, and any constraints.

### 2) Understand changes and rationale
- Compare the current branch against `main` to understand what changed and why (use this to derive the PR title, description, and rationale):
- `git fetch origin main`
- `git log --oneline origin/main..HEAD`
- `git diff --stat origin/main...HEAD`
- `git diff origin/main...HEAD`
- If the branch name starts with an issue number (e.g., `123-...`), use that issue in the rationale and fill the `Closes #` line in the PR template.
- If the branch name does not include an issue number, infer the likely issue from changes and conversation context, or leave `Closes #` empty if unsure.

### 3) Fetch the PR template
Use the helper script to download the latest PR template from reductstore/.github:

```bash
./.codex/skills/create-pr/scripts/fetch_pr_template.sh /tmp/pr_template.md
```

Fill in the template file with the relevant summary, testing, and rationale derived from the diff and conversation context.

### 4) Create the PR with gh
Use the filled template as the PR body:

```bash
gh pr create --title "<title>" --body-file /tmp/pr_template.md
```

Capture the PR number after creation:

```bash
gh pr view --json number -q .number
```

### 5) Update CHANGELOG.md
- Find the appropriate section (usually the most recent/unreleased section).
- Add a new entry following the existing style in the file.
- Include the PR ID as `#<number>` exactly as prior entries do.

### 6) Commit (no push)
Stage and commit the changelog update only:

```bash
git add CHANGELOG.md
git commit -m "Update changelog for PR #<number>"
```

Do not push.

## Resources

### scripts/
- `fetch_pr_template.sh`: Download the latest PR template from reductstore/.github.
20 changes: 20 additions & 0 deletions .codex/skills/create-pr/scripts/fetch_pr_template.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail

if [[ $# -ne 1 ]]; then
echo "Usage: $0 <output-file>" >&2
exit 1
fi

output_file="$1"

curl -sS -L \
"https://raw.githubusercontent.com/reductstore/.github/main/.github/pull_request_template.md" \
-o "$output_file"

if [[ ! -s "$output_file" ]]; then
echo "Template download failed or empty: $output_file" >&2
exit 1
fi

echo "Template saved to $output_file"
4 changes: 0 additions & 4 deletions .github/actions/run-tests/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,13 @@ inputs:
description: "Reduct Store version"
required: false
default: "main"
lic_file: # id of input
description: "License file"
required: true
runs:
using: "composite"
steps:
- name: Run Reduct Store
shell: bash
run: docker run -p 8383:8383 -v ${PWD}:/workdir
--env RS_API_TOKEN=${{inputs.api-token}}
--env RS_LICENSE_PATH=/workdir/${{inputs.lic_file}}
--env RS_EXT_PATH=/tmp
--name reduct-store
-d reduct/store:${{inputs.reductstore-version}}
Expand Down
11 changes: 2 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ jobs:
matrix:
token: [ "", "TOKEN" ]
reductstore_version: [ "main", "latest" ]
license_file: [ "", "lic.key" ]
include:
- token: ""
exclude_token_api_tag: "~[token_api]"
Expand All @@ -72,9 +71,7 @@ jobs:
- reductstore_version: "main"
exclude_api_version_tag: ""
- reductstore_version: "latest"
exclude_api_version_tag: "~[1_18]"
- license_file: ""
exclude_license_tag: "~[license]"
exclude_api_version_tag: "~[1_19]"

steps:
- uses: actions/checkout@v4
Expand All @@ -87,15 +84,11 @@ jobs:
- name: Set execute permissions
run: chmod +x build/Release/bin/reduct-tests

- name: Generate license
run: echo '${{secrets.LICENSE_KEY}}' > lic.key

- uses: ./.github/actions/run-tests
with:
api-token: ${{matrix.token}}
tags: "${{matrix.exclude_token_api_tag}} ${{matrix.exclude_api_version_tag}} ${{matrix.exclude_license_tag}}"
tags: "${{matrix.exclude_token_api_tag}} ${{matrix.exclude_api_version_tag}}"
reductstore-version: ${{matrix.reductstore_version}}
lic_file: ${{matrix.license_file}}


check-example-linux:
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added

- Add entry attachments API (`WriteAttachments`, `ReadAttachments`, `RemoveAttachments`) for ReductStore API v1.19, [PR-112](https://github.com/reductstore/reduct-cpp/pull/112)

## 1.18.0 - 2026-02-04

### Added
Expand Down
1 change: 0 additions & 1 deletion _codeql_detected_source_root

This file was deleted.

112 changes: 112 additions & 0 deletions src/reduct/bucket.cc
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,108 @@ class Bucket : public IBucket {
return ProcessBatchV2(std::move(callback), BatchType::kUpdate);
}

Error WriteAttachments(std::string_view entry_name,
const AttachmentMap& attachments) const noexcept override {
if (attachments.empty()) {
return Error::kOk;
}

Batch batch;
const auto meta_entry = fmt::format("{}/$meta", entry_name);
auto timestamp = std::chrono::time_point_cast<std::chrono::microseconds>(Time::clock::now());
for (const auto& [key, payload] : attachments) {
try {
[[maybe_unused]] auto parsed = nlohmann::json::parse(payload);
} catch (const std::exception& ex) {
return Error{.code = -1, .message = ex.what()};
}
batch.AddRecord(meta_entry, timestamp, payload, "application/json", {{"key", key}});
timestamp += std::chrono::microseconds(1);
}

auto [errors, err] = internal::ProcessBatchV2Records(client_.get(), io_path_, std::move(batch), BatchType::kWrite);
if (err) {
return err;
}

return FirstBatchRecordError(errors);
}

Result<AttachmentMap> ReadAttachments(std::string_view entry_name) const noexcept override {
AttachmentMap attachments;
Error callback_err = Error::kOk;
const auto meta_entry = fmt::format("{}/$meta", entry_name);

auto err = QueryV2({meta_entry}, std::nullopt, std::nullopt, {}, [&attachments, &callback_err](const auto& record) {
auto key = record.labels.find("key");
if (key == record.labels.end()) {
return true;
}

auto [payload, read_err] = record.ReadAll();
if (read_err) {
callback_err = std::move(read_err);
return false;
}

try {
[[maybe_unused]] auto parsed = nlohmann::json::parse(payload);
} catch (const std::exception& ex) {
callback_err = Error{.code = -1, .message = ex.what()};
return false;
}

attachments[key->second] = std::move(payload);
return true;
});

if (err) {
return {{}, std::move(err)};
}

if (callback_err) {
return {{}, std::move(callback_err)};
}

return {std::move(attachments), Error::kOk};
}

Error RemoveAttachments(std::string_view entry_name,
const std::set<std::string>& attachment_keys) const noexcept override {
QueryOptions options;
if (!attachment_keys.empty()) {
nlohmann::json when;
when["$in"] = nlohmann::json::array();
when["$in"].push_back("&key");
for (const auto& key : attachment_keys) {
when["$in"].push_back(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),
[&remove_batch](const auto& record) {
auto labels = record.labels;
labels["remove"] = "true";
remove_batch.AddOnlyLabels(record.entry, record.timestamp, std::move(labels));
return true;
});

if (query_err) {
return query_err;
}

auto [errors, err] =
internal::ProcessBatchV2Records(client_.get(), io_path_, std::move(remove_batch), BatchType::kUpdate);
if (err) {
return err;
}

return FirstBatchRecordError(errors);
}

Result<BatchErrors> RemoveBatch(std::string_view entry_name, BatchCallback callback) const noexcept override {
return ProcessBatchV1(entry_name, std::move(callback), BatchType::kRemove);
}
Expand Down Expand Up @@ -655,6 +757,16 @@ class Bucket : public IBucket {
return api_version && internal::IsCompatible("1.18", *api_version);
}

static Error FirstBatchRecordError(const BatchRecordErrors& errors) {
for (const auto& [entry, record_errors] : errors) {
(void)entry;
if (!record_errors.empty()) {
return record_errors.begin()->second;
}
}
return Error::kOk;
}

Result<BatchErrors> ProcessBatchV1(std::string_view entry_name, BatchCallback callback,
BatchType type) const noexcept {
Batch batch;
Expand Down
34 changes: 34 additions & 0 deletions src/reduct/bucket.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <map>
#include <optional>
#include <ostream>
#include <set>
#include <string>
#include <utility>
#include <vector>
Expand Down Expand Up @@ -84,6 +85,7 @@ class IBucket {
};

using LabelMap = std::map<std::string, std::string>;
using AttachmentMap = std::map<std::string, std::string>; // attachment key -> JSON payload

/**
* ReadableRecord
Expand Down Expand Up @@ -369,6 +371,38 @@ class IBucket {
*/
[[nodiscard]] virtual Result<BatchRecordErrors> UpdateBatch(BatchCallback callback) const noexcept = 0;

/**
* @brief Write JSON attachments for an entry
*
* Attachments are stored in a metadata entry `<entry_name>/$meta`.
*
* @param entry_name entry in bucket
* @param attachments map of attachment keys to JSON payloads
* @return HTTP or communication error
*/
virtual Error WriteAttachments(std::string_view entry_name,
const AttachmentMap& attachments) const noexcept = 0;

/**
* @brief Read JSON attachments for an entry
*
* @param entry_name entry in bucket
* @return map of attachment keys to JSON payloads
*/
[[nodiscard]] virtual Result<AttachmentMap> ReadAttachments(std::string_view entry_name) const noexcept = 0;

/**
* @brief Remove JSON attachments for an entry
*
* If attachment_keys is empty, all attachments are removed.
*
* @param entry_name entry in bucket
* @param attachment_keys attachment keys to remove
* @return HTTP or communication error
*/
virtual Error RemoveAttachments(std::string_view entry_name, const std::set<std::string>& attachment_keys) const
noexcept = 0;

/**
* Query options
*/
Expand Down
6 changes: 5 additions & 1 deletion tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ add_executable(reduct-tests ${SRC_FILES})
target_include_directories(reduct-tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(
reduct-tests
PRIVATE ${RCPP_TARGET_NAME} fmt::fmt Catch2::Catch2
PRIVATE
${RCPP_TARGET_NAME}
fmt::fmt
nlohmann_json::nlohmann_json
Catch2::Catch2
)
set_target_properties(
reduct-tests
Expand Down
Loading
Loading