luoyuxia commented on code in PR #199:
URL: https://github.com/apache/paimon-cpp/pull/199#discussion_r3795419714


##########
src/paimon/core/table/source/realtime_split.h:
##########
@@ -59,25 +74,27 @@ class RealtimeSplit : public Split {
         return disk_splits_;
     }
 
-    const std::shared_ptr<MemIndexer>& Indexer() const {
-        return indexer_;
+    int64_t CommittedOffset() const {
+        return committed_offset_;
     }
 
-    const std::shared_ptr<MemReadView>& ReadView() const {
-        return read_view_;
+    int64_t MemoryUpperOffset() const {
+        return memory_upper_offset_;
     }
 
-    int64_t CommittedOffset() const {
-        return committed_offset_;
+    const std::string& OpaqueTicket() const {
+        return opaque_ticket_;
     }
 
  private:
+    int32_t version_;
+    std::optional<int64_t> snapshot_id_;
     std::map<std::string, std::string> partition_;
     int32_t bucket_;
     std::vector<std::shared_ptr<Split>> disk_splits_;
-    std::shared_ptr<MemIndexer> indexer_;
-    std::shared_ptr<MemReadView> read_view_;
     int64_t committed_offset_;

Review Comment:
   Would it be more intuitive to represent committed progress as an exclusive 
end offset? The committed snapshot covers offsets < committed_end_offset, and 
the reader starts from committed_end_offset inclusively. This avoids the extra 
+1 conversion, represents an empty committed range naturally with 0, and makes 
adjacent ranges compose cleanly as half-open intervals.
   
   Correspondingly, could memory_upper_offset be represented as an exclusive 
memory_end_offset? At the RealtimeSplit level, the in-memory range could then 
be expressed directly as [start_offset, end_offset): start_offset equals 
committed_end_offset, and end_offset equals memory_end_offset. The reader 
consumes exactly that range, which makes both boundaries explicit and avoids 
mixing inclusive committed_offset and memory_upper_offset semantics.



##########
include/paimon/realtime/realtime_context.h:
##########
@@ -66,29 +65,12 @@ struct PAIMON_EXPORT RealtimePartitionBucket {
 /// Largest committed offset for each partition-bucket.
 using RealtimeOffsetMap = std::map<RealtimePartitionBucket, int64_t>;
 
-/// Memory indexer and its initial offset resolved from committed and retained 
memory progress.
-struct PAIMON_EXPORT RealtimeMemIndexerState {
-    /// Plugin instance associated with the requested partition-bucket.
-    std::shared_ptr<MemIndexer> indexer;
-    /// First offset after both committed rows and rows currently retained by 
the indexer.
-    int64_t initial_offset;
-};
-
-/// One partition-bucket and the immutable plugin view captured for a table 
scan.
-struct PAIMON_EXPORT RealtimePartitionBucketView {
-    /// Partition-bucket associated with this view.
-    RealtimePartitionBucket partition_bucket;
-    /// Plugin instance that creates readers from `read_view`.
-    std::shared_ptr<MemIndexer> indexer;
-    /// Immutable rows pinned for one query plan.
-    std::shared_ptr<MemReadView> read_view;
-};
-
 /// Shared context that owns the `MemIndexer` instances used by a real-time 
writer.
 ///
-/// Applications share one context between `WriteContext` and `ScanContext`. 
The context uses
-/// either the default Arrow implementation or an application-provided factory 
and keeps each
-/// created indexer available across writes, prepare-commit operations, and 
process-local reads.
+/// Applications share one context between `WriteContext`, `ScanContext`, and 
`ReadContext`. The
+/// context uses either the default Arrow implementation or an 
application-provided factory and
+/// keeps each created indexer available across writes, prepare-commit 
operations, and
+/// process-local reads.
 class PAIMON_EXPORT RealtimeContext {

Review Comment:
   The current RealtimeContext hierarchy seems unclear. 
RealtimeContext::Create() constructs a RealtimeContextImpl and upcasts it to 
the public base type, while  the write, scan, and read paths immediately 
downcast it through RealtimeContextImpl::Cast() before performing any operation.
   As a result, the effective dependency is still RealtimeContextImpl; 
RealtimeContext only transports the concrete instance and does not define a 
behavioral contract. If only one implementation is intended, a concrete 
RealtimeContext with Pimpl would make the call hierarchy clearer. Otherwise, 
the operations required by the write, scan, and read paths should be expressed 
on the shared abstraction and invoked without downcasting.
   
   The name RealtimeContext also seems misleading for its current role. Unlike 
WriteContext, ScanContext, or ReadContext, this object is not an 
operation-scoped collection of configuration and dependencies. It is a 
long-lived, stateful object shared by the writer, scanner, and reader, and it 
owns the mem-indexer  registry, committed progress, pinned read views, and 
their lifecycle. Would RealtimeStore better describe this ownership model and 
make the relationship with writer, scan, and read more explicit?
   
   This structure also makes it difficult to support another realtime-store 
implementation. The current extension point is MemIndexerFactory, which can 
replace the per-partition-bucket memory indexer, but it cannot replace the 
writer, scan/planning, split, and reader behavior of the realtime store as a 
whole. A different  RealtimeContext implementation cannot be used through the 
existing APIs because all write, scan, and read paths eventually require 
RealtimeContextImpl::Cast(). Supporting another store would therefore require 
modifying these core call sites and adding implementation-specific branches.
   
   Would it be clearer to make RealtimeStore the behavioral boundary and 
introduce a RealtimeStoreFactory that creates the complete store 
implementation? The  existing mem-indexer-based implementation could remain one 
RealtimeStore, while another store could provide its own writer, planner, 
split, and reader behavior  without changing the core dispatch path.



##########
include/paimon/realtime/realtime_context.h:
##########
@@ -66,29 +65,12 @@ struct PAIMON_EXPORT RealtimePartitionBucket {
 /// Largest committed offset for each partition-bucket.
 using RealtimeOffsetMap = std::map<RealtimePartitionBucket, int64_t>;
 
-/// Memory indexer and its initial offset resolved from committed and retained 
memory progress.
-struct PAIMON_EXPORT RealtimeMemIndexerState {
-    /// Plugin instance associated with the requested partition-bucket.
-    std::shared_ptr<MemIndexer> indexer;
-    /// First offset after both committed rows and rows currently retained by 
the indexer.
-    int64_t initial_offset;
-};
-
-/// One partition-bucket and the immutable plugin view captured for a table 
scan.
-struct PAIMON_EXPORT RealtimePartitionBucketView {
-    /// Partition-bucket associated with this view.
-    RealtimePartitionBucket partition_bucket;
-    /// Plugin instance that creates readers from `read_view`.
-    std::shared_ptr<MemIndexer> indexer;
-    /// Immutable rows pinned for one query plan.
-    std::shared_ptr<MemReadView> read_view;
-};
-
 /// Shared context that owns the `MemIndexer` instances used by a real-time 
writer.
 ///
-/// Applications share one context between `WriteContext` and `ScanContext`. 
The context uses
-/// either the default Arrow implementation or an application-provided factory 
and keeps each
-/// created indexer available across writes, prepare-commit operations, and 
process-local reads.
+/// Applications share one context between `WriteContext`, `ScanContext`, and 
`ReadContext`. The
+/// context uses either the default Arrow implementation or an 
application-provided factory and
+/// keeps each created indexer available across writes, prepare-commit 
operations, and
+/// process-local reads.
 class PAIMON_EXPORT RealtimeContext {

Review Comment:
   I am not fully sure whether the following API shape covers all of your 
intended use cases, so please treat it as a design sketch for discussion.
   
   The overall object model is inspired by Paimon’s Java CatalogFactory → 
Catalog → Table creation model. This is an object-creation and ownership 
hierarchy rather than inheritance between these three types: a factory creates 
a backend-scoped store, the store loads a table-specific handle, and the table 
handle creates or wraps the write, scan, and read operations.
   
   Please note the follow api proposal is is roughly the API shape I have in 
mind. I have not examined every related code path in detail, so the concrete 
class names and method signatures may not be completely accurate. The main 
point is the overall creation and composition hierarchy:
   ```
   RealtimeStoreFactory
         └── creates RealtimeStore
                 └── loads RealtimeTable
                         ├── creates write operations
                         ├── creates scan operations
                         └── creates read operations
   ```
   Concrete backends implement the corresponding factory, store, and table 
interfaces. The exact API may differ, but this is roughly the abstraction 
boundary and dependency direction I have in mind.
   
   ## Propose API
   ```
   /// Store-level configuration shared by all tables opened from one realtime 
store.
   struct PAIMON_EXPORT RealtimeStoreOptions {
         std::map<std::string, std::string> options;
         xxx
     };
   
   /// Identifies and configures one table loaded from a RealtimeStore.
     struct PAIMON_EXPORT RealtimeTableDescriptor {
         Identifier identifier;
         std::string table_path;
         std::string branch;
         std::shared_ptr<Schema> schema;
         std::map<std::string, std::string> options;
     };
   
   /// Factory for creating a backend-level RealtimeStore.
     ///
     /// Implementations may be registered through the existing Paimon Factory
     /// mechanism using a unique identifier.
     class PAIMON_EXPORT RealtimeStoreFactory : public Factory {
      public:
         ~RealtimeStoreFactory() override = default;
   
         /// Creates a RealtimeStore using this factory.
         virtual Result<std::shared_ptr<RealtimeStore>> Create(
             const RealtimeStoreOptions& options) const = 0;
     };
   
   /// A long-lived realtime backend that may serve multiple tables.
     ///
     /// Implementations may own backend-wide resources. Table-specific state
     /// should be represented by RealtimeTable.
     class PAIMON_EXPORT RealtimeStore {
      public:
         virtual ~RealtimeStore() = default;
   
         /// Loads or opens a table from this realtime store.
         ///
         /// The returned table may retain shared backend resources owned by 
this
         /// store. Implementations may cache table state internally.
         virtual Result<std::shared_ptr<RealtimeTable>> LoadTable(
             const RealtimeTableDescriptor& table) = 0;
     };
   
   /// A table-specific realtime handle.
     ///
     /// The same table handle may be shared by write, scan, and read 
operations.
     /// An in-process implementation may keep table-level indexers, committed
     /// progress, and query views here. A remote implementation may keep only
     /// table metadata and backend clients.
     class PAIMON_EXPORT RealtimeTable {
      public:
         virtual ~RealtimeTable() = default;
   
         /// Creates the write path for this realtime table.
         ///
         /// The returned object follows the existing FileStoreWrite contract,
         /// including prepare-commit and committed-snapshot notification.
         virtual Result<std::unique_ptr<FileStoreWrite>> CreateWrite(
             std::unique_ptr<WriteContext> context) = 0;
   
         /// Creates the scan/planning path for this realtime table.
         ///
         /// CreatePlan() may combine the committed disk snapshot with the
         /// realtime portion and return implementation-specific Split objects.
         virtual Result<std::unique_ptr<TableScan>> CreateScan(
             std::unique_ptr<ScanContext> context) = 0;
   
         /// Creates the read path for splits planned by CreateScan().
         ///
         /// The implementation is responsible for recognizing its own realtime
         /// splits and composing disk and realtime readers as required.
         virtual Result<std::unique_ptr<TableRead>> CreateRead(
             std::unique_ptr<ReadContext> context) = 0;
     };
   ```
   ## Example usage
   ```
   RealtimeStoreOptions store_options;
   store_options.options = options;
   
   PAIMON_ASSIGN_OR_RAISE(
         std::shared_ptr<RealtimeStore> realtime_store,
         RealtimeStoreFactory::Create(realtime_store_type, store_options));
   
     RealtimeTableDescriptor table_descriptor{
         identifier,
         table_path,
         branch,
         schema,
         table_options};
   
     PAIMON_ASSIGN_OR_RAISE(
         std::shared_ptr<RealtimeTable> realtime_table,
         realtime_store->LoadTable(table_descriptor));
   
     Create a writer:
   
     WriteContextBuilder write_builder(table_path, commit_user);
     write_builder.SetOptions(write_options);
   
     PAIMON_ASSIGN_OR_RAISE(
         std::unique_ptr<WriteContext> write_context,
         write_builder.Finish());
   
     PAIMON_ASSIGN_OR_RAISE(
         std::unique_ptr<FileStoreWrite> writer,
         realtime_table->CreateWrite(std::move(write_context)));
   
     Create a two-stage scan and read:
   
     ScanContextBuilder scan_builder(table_path);
     scan_builder.SetOptions(scan_options);
   
     PAIMON_ASSIGN_OR_RAISE(
         std::unique_ptr<ScanContext> scan_context,
         scan_builder.Finish());
   
     PAIMON_ASSIGN_OR_RAISE(
         std::unique_ptr<TableScan> scan,
         realtime_table->CreateScan(std::move(scan_context)));
   
     PAIMON_ASSIGN_OR_RAISE(
         std::shared_ptr<Plan> plan,
         scan->CreatePlan());
   
     ReadContextBuilder read_builder(table_path);
     read_builder.SetOptions(read_options);
   
     PAIMON_ASSIGN_OR_RAISE(
         std::unique_ptr<ReadContext> read_context,
         read_builder.Finish());
   
     PAIMON_ASSIGN_OR_RAISE(
         std::unique_ptr<TableRead> table_read,
         realtime_table->CreateRead(std::move(read_context)));
   
     PAIMON_ASSIGN_OR_RAISE(
         std::unique_ptr<BatchReader> reader,
         table_read->CreateReader(plan->Splits()));
   
     Possible implementation hierarchy:
   
     RealtimeStoreFactory
     ├── MemoryRealtimeStoreFactory
     └── OtherRealtimeStoreFactory
   
     RealtimeStore
     ├── MemoryRealtimeStore
     │     └── LoadTable() → MemoryRealtimeTable
     └── OtherRealtimeStore
           └── LoadTable() → OtherRealtimeTable
   
     RealtimeTable
     ├── MemoryRealtimeTable
     │     ├── CreateWrite() → mem-indexer-backed FileStoreWrite
     │     ├── CreateScan()  → memory union TableScan
     │     └── CreateRead()  → memory union TableRead
     └── OtherRealtimeTable
           ├── CreateWrite() → backend-specific FileStoreWrite
           ├── CreateScan()  → backend-specific TableScan
           └── CreateRead()  → backend-specific TableRead
   ```
   
   With this structure, the current mem-indexer implementation could become 
MemoryRealtimeStore and MemoryRealtimeTable. MemIndexerFactory would remain an 
implementation detail of that backend.
   
   The write, scan, and read paths would invoke the RealtimeTable interface 
directly. They would no longer upcast a concrete implementation to 
RealtimeContext and  later recover it through RealtimeContextImpl::Cast(). This 
proposal assumes that every realtime-table implementation exposed through 
paimon-cpp can conform to the existing FileStoreWrite, TableScan, and TableRead 
contracts. If that assumption does not match the intended writer or commit 
lifecycle, the operation interfaces may need to be separated further.
   
   
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to