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
64 changes: 36 additions & 28 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,49 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Added

- storage.Prefixed: New wrapper that scopes every operation, predicate,
Range, and Watch call under a given namespace prefix. Nested Prefixed
wrappers are flattened at construction.
### Changed

### Fixed

## [v1.3.0] - 2026-05-05

This release introduces a new transaction-aware integrity API
(`Codec[T]`, `Store[T]`, and `Tx`), a `Prefixed` storage wrapper for
namespace scoping, a `LayeredNamer` with per-category key layout, and
additional typed marshallers. It also bumps dependencies to address
vulnerabilities reported by govulncheck.

### Added

- storage.Prefixed: wrapper that scopes every storage operation,
predicate, Range, and Watch call under a given namespace prefix.
Nested wrappers are flattened automatically (#67).
- namer.LayeredNamer: namespace-agnostic namer that places each key
category under its own top-level location segment
(`/<objectLocation>/<name>`, `/<hashLocation>/<name>`,
`/<sigLocation>/<name>`). The constructor validates segments
(non-empty, slash-free, unique across categories) and `ParseKey`
parses keys back to `(name, KeyType, property)` unambiguously.
- integrity.Codec: schema-only `Codec[T]` (no storage handle) and
`CodecBuilder[T]` with a fluent value-receiver API mirroring
`TypedBuilder[T]`. `Build()` returns `(*Codec[T], error)` and
(`/<objectLocation>/<name>`, `/hash/<hashLocation>/...`,
`/sig/<sigLocation>/...`). Segments are validated at construction
and `ParseKey` parses keys back to `(name, KeyType, property)`
unambiguously (#68).
- integrity: new schema-driven API for integrity-protected storage.
`Codec[T]` describes the value layout independently of any storage
handle and is built via the fluent `CodecBuilder[T]`, which
validates location-override keys eagerly so typos like
`WithHashLocation("sah256", …)` are no longer silently ignored.
- integrity.Tx: transaction primitives `Tx`, `Branch`, `Branchable`,
`GetFuture[T]`, `RangeFuture[T]`, `Response`, and sentinel errors
`ErrBranchNotFired`, `ErrTxNotCommitted`, `ErrTxAlreadyCommitted`.
`Codec[T]` gains `TxGet`, `TxPut`, `TxDelete`, `TxRange`, and
`BindPredicate` for use inside transactions.
- integrity.Store: `Store[T]` (codec bound to a storage handle) with
`Get`/`Put`/`Delete`/`Range`/`Watch` implemented as thin `Tx`
wrappers, and `Codec[T].Bind` for cheap binding. Multi-codec
transactions lower to a single storage call.
`Store[T]` binds a codec to a storage handle and exposes
`Get`/`Put`/`Delete`/`Range`/`Watch`. Conditional multi-key updates
are available through `Tx` with `Branch` and typed futures;
multi-codec transactions are lowered to a single storage call
(#69, #70, #71).
- marshaller: `TypedJSONMarshaller[T]` for `encoding/json`-based
marshalling and `TypedBytesMarshaller` passthrough
`TypedMarshaller[[]byte]` for values that are already serialized or
stored as opaque blobs.

### Changed
marshalling and `TypedBytesMarshaller` passthrough for values
already stored as opaque bytes (#73).

### Fixed

- Bump google.golang.org/grpc to v1.79.3 to fix GO-2026-4762
(authorization bypass via missing leading slash in :path).
- Bump go.opentelemetry.io/otel/sdk to v1.40.0 to fix GO-2026-4394
(arbitrary code execution via PATH hijacking).
- Bumped `google.golang.org/grpc` to `v1.79.3` (GO-2026-4762:
authorization bypass via missing leading slash in `:path`) and
`go.opentelemetry.io/otel/sdk` to `v1.40.0` (GO-2026-4394: arbitrary
code execution via PATH hijacking) (#75).

## [v1.2.0] - 2026-04-29

Expand Down Expand Up @@ -138,6 +145,7 @@ The release introduces the initial version of the library.

### Fixed

[v1.3.0]: https://github.com/tarantool/go-storage/releases/tag/v1.3.0
[v1.2.0]: https://github.com/tarantool/go-storage/releases/tag/v1.2.0
[v1.1.2]: https://github.com/tarantool/go-storage/releases/tag/v1.1.2
[v1.1.1]: https://github.com/tarantool/go-storage/releases/tag/v1.1.1
Expand Down
112 changes: 112 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ safety are critical.
- Real-time Watch: Monitor changes to keys and prefixes
- Conditional Execution: Value and version-based predicates for safe updates
- Data Integrity: Built-in signing and verification of stored data
- Schema-Driven Integrity API: `Codec[T]` / `Store[T]` for type-safe values
plus multi-key atomic transactions via `Tx`
- Namespace Scoping: `Prefixed` wrapper that scopes every operation under a
key prefix
- Key‑Value Operations: Get, Put, Delete with prefix support
- Range Queries: Efficient scanning of keys with filters
- Extensible Drivers: Easy to add new storage backends
Expand Down Expand Up @@ -208,6 +212,23 @@ The core `Storage` interface (`storage.Storage`) provides high‑level methods:
- `Tx(ctx) tx.Tx` – create a transaction builder
- `Range(ctx, opts) ([]kv.KeyValue, error)` – range query with prefix/limit

#### Namespace Scoping with `Prefixed`

`storage.Prefixed(prefix, inner)` returns a `Storage` that transparently
prepends `prefix` to every operation, predicate, Range, and Watch call, and
strips it back from any keys returned to the caller. Nested wrappers are
flattened at construction (`Prefixed("/a", Prefixed("/b", base))` is
equivalent to `Prefixed("/a/b", base)`).

```go
scoped := storage.Prefixed("/ns", storage.NewStorage(driver))

// Caller writes /cfg/version; the driver actually stores /ns/cfg/version.
_, err := scoped.Tx(ctx).Then(
operation.Put([]byte("/cfg/version"), []byte("1.0.0")),
).Commit()
```

#### Transaction Builder
The `tx.Tx` interface enables conditional transactions:

Expand Down Expand Up @@ -346,6 +367,97 @@ The `integrity.Typed` builder also accepts custom marshallers (default is
YAML), custom namers, and separate signer/verifier instances for asymmetric
setups.

#### Schema-Driven Integrity API (`Codec`, `Store`, `Tx`)

Alongside `integrity.Typed`, the package exposes a schema-first API split
into three pieces:

- `integrity.Codec[T]` describes the on-disk layout (object location,
hashers, signers, marshaller) without binding to any storage handle. It is
built via the fluent `CodecBuilder[T]`, which validates location-override
keys eagerly so typos like `WithHashLocation("sah256", …)` fail at
`Build()` instead of being silently ignored.
- `integrity.Store[T]` is a codec bound to a `storage.Storage` and exposes
the familiar `Get` / `Put` / `Delete` / `Range` / `Watch` methods.
- `integrity.Tx` accumulates `TxGet` / `TxPut` / `TxDelete` / `TxRange`
calls from one or more codecs and commits them atomically through a
single storage call. Reads return typed futures (`GetFuture[T]`,
`RangeFuture[T]`) whose `Result()` is populated after `Commit`.

##### Codec and Store

```go
codec, err := integrity.NewCodecBuilder[MyConfig]().
WithObjectLocation("config").
WithHasher(hasher.NewSHA256Hasher()).
Build()
if err != nil {
log.Fatal(err)
}

store := codec.Bind(baseStorage)

if err := store.Put(ctx, "app/settings", MyConfig{...}); err != nil {
log.Fatal(err)
}

res, err := store.Get(ctx, "app/settings")
if err != nil {
log.Fatal(err)
}
cfg := res.Value.Unwrap()
```

##### Multi-Key Transactions

`Tx` batches reads and writes — across multiple codecs if needed — into one
atomic storage call. `If` predicates are routed by `Then` / `Else`; futures
attached to the branch that did not fire return `ErrBranchNotFired`.

```go
txn := integrity.NewTx(baseStorage)

pred, _ := codec.ValueEqual(MyConfig{...})
bound, _ := codec.BindPredicate("app/settings", pred)
txn.If(bound)

newFut := codec.TxGet(txn.Then(), "app/new-settings")
_ = codec.TxPut(txn.Then(), "app/settings", MyConfig{...})

resp, err := txn.Commit(ctx)
if err != nil {
log.Fatal(err)
}
if !resp.Succeeded {
// The Then branch did not fire; newFut.Result() returns
// integrity.ErrBranchNotFired.
}
```

##### Layered Key Layout

The new API uses `namer.LayeredNamer` by default, which places each key
category under its own top-level location segment:

```
/<objectLocation>/<name> (value)
/hash/<hashLocation>/<objectLocation>/<name> (one per hasher)
/sig/<sigLocation>/<objectLocation>/<name> (one per signer)
```

`namer.CompactSingleHash()` and `namer.CompactSingleSig()` drop the
per-hasher / per-signer segment when exactly one is configured. `ParseKey`
parses a raw key back to `(name, KeyType, property)` unambiguously.

##### Marshallers

Beyond the default YAML marshaller, the `marshaller` package now ships:

- `TypedJSONMarshaller[T]` — `encoding/json`-based marshalling for any
Go type.
- `TypedBytesMarshaller` — passthrough `TypedMarshaller[[]byte]` for values
that are already serialized or stored as opaque blobs.

### Examples

Comprehensive examples are available in the driver packages:
Expand Down
Loading