This is an automated email from the ASF dual-hosted git repository.
yuxia pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fluss-rust.git
The following commit(s) were added to refs/heads/main by this push:
new 4dff1cb chore: make RowView shared_ptr (#353)
4dff1cb is described below
commit 4dff1cbfbf1ff92e9496cbf644404020ea7b2a2b
Author: Anton Borisov <[email protected]>
AuthorDate: Thu Feb 19 09:01:59 2026 +0000
chore: make RowView shared_ptr (#353)
---
bindings/cpp/include/fluss.hpp | 29 +++++++++----------
bindings/cpp/src/table.cpp | 35 ++++-------------------
website/docs/user-guide/cpp/api-reference.md | 10 +++----
website/docs/user-guide/cpp/data-types.md | 29 ++++++++++---------
website/docs/user-guide/cpp/example/log-tables.md | 28 ++++++++++++++++++
5 files changed, 68 insertions(+), 63 deletions(-)
diff --git a/bindings/cpp/include/fluss.hpp b/bindings/cpp/include/fluss.hpp
index a1a6b1f..27d1fcb 100644
--- a/bindings/cpp/include/fluss.hpp
+++ b/bindings/cpp/include/fluss.hpp
@@ -617,15 +617,15 @@ class GenericRow {
/// Read-only row view for scan results. Zero-copy access to string and bytes
data.
///
-/// WARNING: RowView borrows from ScanRecords. It must not outlive the
ScanRecords
-/// that produced it (similar to std::string_view borrowing from std::string).
+/// RowView shares ownership of the underlying scan data via reference
counting,
+/// so it can safely outlive the ScanRecords that produced it.
class RowView : public detail::NamedGetters<RowView> {
friend struct detail::NamedGetters<RowView>;
public:
- RowView(const ffi::ScanResultInner* inner, size_t record_idx,
- const detail::ColumnMap* column_map)
- : inner_(inner), record_idx_(record_idx), column_map_(column_map) {}
+ RowView(std::shared_ptr<const ffi::ScanResultInner> inner, size_t
record_idx,
+ std::shared_ptr<const detail::ColumnMap> column_map)
+ : inner_(std::move(inner)), record_idx_(record_idx),
column_map_(std::move(column_map)) {}
// ── Index-based getters ──────────────────────────────────────────
size_t FieldCount() const;
@@ -665,15 +665,15 @@ class RowView : public detail::NamedGetters<RowView> {
}
return detail::ResolveColumn(*column_map_, name);
}
- const ffi::ScanResultInner* inner_;
+ std::shared_ptr<const ffi::ScanResultInner> inner_;
size_t record_idx_;
- const detail::ColumnMap* column_map_; // borrowed from ScanRecords (same
lifetime as inner_)
+ std::shared_ptr<const detail::ColumnMap> column_map_;
};
/// A single scan record. Contains metadata and a RowView for field access.
///
-/// WARNING: ScanRecord contains a RowView that borrows from ScanRecords.
-/// It must not outlive the ScanRecords that produced it.
+/// ScanRecord is a value type that can be freely copied, stored, and
+/// accumulated across multiple Poll() calls.
struct ScanRecord {
int32_t bucket_id;
std::optional<int64_t> partition_id;
@@ -685,13 +685,13 @@ struct ScanRecord {
class ScanRecords {
public:
- ScanRecords() noexcept;
- ~ScanRecords() noexcept;
+ ScanRecords() noexcept = default;
+ ~ScanRecords() noexcept = default;
ScanRecords(const ScanRecords&) = delete;
ScanRecords& operator=(const ScanRecords&) = delete;
- ScanRecords(ScanRecords&& other) noexcept;
- ScanRecords& operator=(ScanRecords&& other) noexcept;
+ ScanRecords(ScanRecords&&) noexcept = default;
+ ScanRecords& operator=(ScanRecords&&) noexcept = default;
size_t Size() const;
bool Empty() const;
@@ -720,9 +720,8 @@ class ScanRecords {
/// Returns the column name-to-index map (lazy-built, cached).
const std::shared_ptr<detail::ColumnMap>& GetColumnMap() const;
friend class LogScanner;
- void Destroy() noexcept;
void BuildColumnMap() const;
- ffi::ScanResultInner* inner_{nullptr};
+ std::shared_ptr<ffi::ScanResultInner> inner_;
mutable std::shared_ptr<detail::ColumnMap> column_map_;
};
diff --git a/bindings/cpp/src/table.cpp b/bindings/cpp/src/table.cpp
index 76f1806..a697cea 100644
--- a/bindings/cpp/src/table.cpp
+++ b/bindings/cpp/src/table.cpp
@@ -266,32 +266,7 @@ std::string RowView::GetDecimalString(size_t idx) const {
// ScanRecords — backed by opaque Rust ScanResultInner
// ============================================================================
-ScanRecords::ScanRecords() noexcept = default;
-
-ScanRecords::~ScanRecords() noexcept { Destroy(); }
-
-void ScanRecords::Destroy() noexcept {
- if (inner_) {
- rust::Box<ffi::ScanResultInner>::from_raw(inner_);
- inner_ = nullptr;
- column_map_.reset();
- }
-}
-
-ScanRecords::ScanRecords(ScanRecords&& other) noexcept
- : inner_(other.inner_), column_map_(std::move(other.column_map_)) {
- other.inner_ = nullptr;
-}
-
-ScanRecords& ScanRecords::operator=(ScanRecords&& other) noexcept {
- if (this != &other) {
- Destroy();
- inner_ = other.inner_;
- column_map_ = std::move(other.column_map_);
- other.inner_ = nullptr;
- }
- return *this;
-}
+// ScanRecords constructor, destructor, move operations are all defaulted in
the header.
size_t ScanRecords::Size() const { return inner_ ? inner_->sv_record_count() :
0; }
@@ -331,7 +306,7 @@ ScanRecord ScanRecords::operator[](size_t idx) const {
inner_->sv_offset(idx),
inner_->sv_timestamp(idx),
static_cast<ChangeType>(inner_->sv_change_type(idx)),
- RowView(inner_, idx, GetColumnMap().get())};
+ RowView(inner_, idx, GetColumnMap())};
}
ScanRecord ScanRecords::Iterator::operator*() const { return
owner_->operator[](idx_); }
@@ -1107,8 +1082,10 @@ Result LogScanner::Poll(int64_t timeout_ms, ScanRecords&
out) {
std::string(result_box->sv_error_message()));
}
- out.Destroy();
- out.inner_ = result_box.into_raw();
+ out.column_map_.reset();
+ out.inner_ = std::shared_ptr<ffi::ScanResultInner>(
+ result_box.into_raw(),
+ [](ffi::ScanResultInner* p) {
rust::Box<ffi::ScanResultInner>::from_raw(p); });
return utils::make_ok();
}
diff --git a/website/docs/user-guide/cpp/api-reference.md
b/website/docs/user-guide/cpp/api-reference.md
index a20739b..a07dd6c 100644
--- a/website/docs/user-guide/cpp/api-reference.md
+++ b/website/docs/user-guide/cpp/api-reference.md
@@ -203,10 +203,10 @@ When using `table.NewRow()`, the `Set()` method
auto-routes to the correct type
## `RowView`
-Read-only row view for scan results. Provides zero-copy access to string and
bytes data.
+Read-only row view for scan results. Provides zero-copy access to string and
bytes data. `RowView` shares ownership of the underlying scan data via
reference counting, so it can safely outlive the `ScanRecords` that produced it.
-:::warning Lifetime
-`RowView` borrows from `ScanRecords`. It must not outlive the `ScanRecords`
that produced it (similar to `std::string_view` borrowing from `std::string`).
+:::note string_view Lifetime
+`GetString()` returns `std::string_view` that borrows from the underlying
data. The `string_view` is valid as long as any `RowView` (or `ScanRecord`)
referencing the same poll result is alive. Copy to `std::string` if you need
the value after all references are gone.
:::
### Index-Based Getters
@@ -248,9 +248,7 @@ Read-only row view for scan results. Provides zero-copy
access to string and byt
## `ScanRecord`
-:::warning Lifetime
-`ScanRecord` contains a `RowView` that borrows from `ScanRecords`. It must not
outlive the `ScanRecords` that produced it.
-:::
+`ScanRecord` is a value type that can be freely copied, stored, and
accumulated across multiple `Poll()` calls. It shares ownership of the
underlying scan data via reference counting.
| Field | Type | Description |
|----------------|-------------------------|----------------------------------|
diff --git a/website/docs/user-guide/cpp/data-types.md
b/website/docs/user-guide/cpp/data-types.md
index 18121d3..bfb296f 100644
--- a/website/docs/user-guide/cpp/data-types.md
+++ b/website/docs/user-guide/cpp/data-types.md
@@ -58,28 +58,31 @@ row.Set("nickname", nullptr); // set to null
Field values are read through `RowView` (from scan results) and `LookupResult`
(from lookups), not through `GenericRow`. Both provide the same getter
interface with zero-copy access to string and bytes data.
-:::warning Lifetime
-`RowView` borrows from `ScanRecords`. It must not outlive the `ScanRecords`
that produced it (similar to `std::string_view` borrowing from `std::string`).
+`ScanRecord` is a value type — it can be freely copied, stored, and
accumulated across multiple `Poll()` calls via reference counting.
+
+:::note string_view Lifetime
+`GetString()` returns `std::string_view` that borrows from the underlying
data. The `string_view` is valid as long as any `ScanRecord` referencing the
same poll result is alive. Copy to `std::string` if you need the value after
all records are gone.
:::
```cpp
-// DON'T — string_view dangles after ScanRecords is destroyed:
-std::string_view dangling;
-{
- fluss::ScanRecords records;
- scanner.Poll(5000, records);
- dangling = records[0].row.GetString(0); // points into ScanRecords memory
-}
-// dangling is undefined behavior here — ScanRecords is gone!
-
-// DO — use values within ScanRecords lifetime, or copy when you need
ownership:
+// ScanRecord is a value type — safe to store and accumulate:
+std::vector<fluss::ScanRecord> all_records;
fluss::ScanRecords records;
scanner.Poll(5000, records);
for (const auto& rec : records) {
+ all_records.push_back(rec); // safe! ref-counted
auto name = rec.row.GetString(0); // zero-copy string_view
auto owned = std::string(rec.row.GetString(0)); // explicit copy when
needed
- process(owned);
}
+
+// DON'T — string_view dangles after all records referencing the data are
destroyed:
+std::string_view dangling;
+{
+ fluss::ScanRecords records;
+ scanner.Poll(5000, records);
+ dangling = records[0].row.GetString(0);
+}
+// dangling is undefined behavior here — no ScanRecord keeps the data alive!
```
### From Scan Results (RowView)
diff --git a/website/docs/user-guide/cpp/example/log-tables.md
b/website/docs/user-guide/cpp/example/log-tables.md
index c94bb84..3a862c1 100644
--- a/website/docs/user-guide/cpp/example/log-tables.md
+++ b/website/docs/user-guide/cpp/example/log-tables.md
@@ -62,6 +62,34 @@ for (const auto& rec : records) {
}
```
+**Continuous polling:**
+
+```cpp
+while (running) {
+ fluss::ScanRecords records;
+ scanner.Poll(1000, records);
+ for (const auto& rec : records) {
+ process(rec);
+ }
+}
+```
+
+**Accumulating records across polls:**
+
+`ScanRecord` is a value type — it can be freely copied, stored, and
accumulated. The underlying data stays alive via reference counting (zero-copy).
+
+```cpp
+std::vector<fluss::ScanRecord> all_records;
+while (all_records.size() < 1000) {
+ fluss::ScanRecords records;
+ scanner.Poll(1000, records);
+ for (const auto& rec : records) {
+ all_records.push_back(rec); // ref-counted, no data copy
+ }
+}
+// all_records is valid — each record keeps its data alive
+```
+
**Batch subscribe:**
```cpp