Primary key tables (KV tables) support upsert, delete, and lookup operations.
auto schema = fluss::Schema::NewBuilder() .AddColumn("id", fluss::DataType::Int()) .AddColumn("name", fluss::DataType::String()) .AddColumn("age", fluss::DataType::BigInt()) .SetPrimaryKeys({"id"}) .Build(); auto descriptor = fluss::TableDescriptor::NewBuilder() .SetSchema(schema) .SetBucketCount(3) .Build(); fluss::TablePath table_path("fluss", "users"); admin.CreateTable(table_path, descriptor, true);
fluss::Table table; conn.GetTable(table_path, table); fluss::UpsertWriter upsert_writer; table.NewUpsert().CreateWriter(upsert_writer); // Fire-and-forget upserts { auto row = table.NewRow(); row.Set("id", 1); row.Set("name", "Alice"); row.Set("age", static_cast<int64_t>(25)); upsert_writer.Upsert(row); } { auto row = table.NewRow(); row.Set("id", 2); row.Set("name", "Bob"); row.Set("age", static_cast<int64_t>(30)); upsert_writer.Upsert(row); } upsert_writer.Flush(); // Per-record acknowledgment { auto row = table.NewRow(); row.Set("id", 3); row.Set("name", "Charlie"); row.Set("age", static_cast<int64_t>(35)); fluss::WriteResult wr; upsert_writer.Upsert(row, wr); wr.Wait(); }
Upsert with the same primary key to update an existing record.
auto row = table.NewRow(); row.Set("id", 1); row.Set("name", "Alice Updated"); row.Set("age", static_cast<int64_t>(26)); fluss::WriteResult wr; upsert_writer.Upsert(row, wr); wr.Wait();
auto pk_row = table.NewRow(); pk_row.Set("id", 2); fluss::WriteResult wr; upsert_writer.Delete(pk_row, wr); wr.Wait();
Update only specific columns while preserving others.
// By column names fluss::UpsertWriter partial_writer; table.NewUpsert() .PartialUpdateByName({"id", "age"}) .CreateWriter(partial_writer); auto row = table.NewRow(); row.Set("id", 1); row.Set("age", static_cast<int64_t>(27)); fluss::WriteResult wr; partial_writer.Upsert(row, wr); wr.Wait(); // By column indices fluss::UpsertWriter partial_writer_idx; table.NewUpsert() .PartialUpdateByIndex({0, 2}) .CreateWriter(partial_writer_idx);
fluss::Lookuper lookuper; table.NewLookup().CreateLookuper(lookuper); auto pk_row = table.NewRow(); pk_row.Set("id", 1); fluss::LookupResult result; lookuper.Lookup(pk_row, result); if (result.Found()) { std::cout << "Found: name=" << result.GetString(1) << ", age=" << result.GetInt64(2) << std::endl; } else { std::cout << "Not found" << std::endl; }
Every primary key table maintains a changelog of its row-level changes. Read it with a record-mode LogScanner — the same API used for log tables — to stream CDC events. Each ScanRecord carries a change_type: +I (insert), -U / +U (the old and new images of an update), and -D (delete, carrying the old row).
fluss::LogScanner log_scanner; table.NewScan().CreateLogScanner(log_scanner); // Subscribe to every bucket from the start of the changelog. int32_t num_buckets = table.GetTableInfo().num_buckets; for (int32_t bucket_id = 0; bucket_id < num_buckets; ++bucket_id) { log_scanner.Subscribe(bucket_id, fluss::EARLIEST_OFFSET); } while (true) { fluss::ScanRecords records; log_scanner.Poll(3000, records); if (records.IsEmpty()) { break; // caught up to the end of the changelog } for (auto rec : records) { // rec.change_type is +I / -U / +U / -D (see ChangeTypeShortString in the API reference) std::cout << ChangeTypeShortString(rec.change_type) << " " << rec.row.GetInt32(0) << " " << rec.row.GetString(1) << std::endl; } }
With the default changelog mode ('table.changelog.image' = 'FULL'), updating a key emits a -U/+U pair and deleting it emits -D; the WAL mode emits only +U on update. To resume from a committed position instead of the start, Subscribe at a specific offset.
To read up to n rows of a bucket's current state without supplying keys, use a batch scanner. The server returns the deduplicated current rows as Arrow batches, convenient for previews or analytics.
int64_t table_id = table.GetTableInfo().table_id; fluss::TableBucket bucket{table_id, 0}; fluss::BatchScanner scanner; table.NewScan().Limit(10).CreateBucketBatchScanner(bucket, scanner); fluss::ArrowRecordBatches batches; scanner.CollectAllBatches(batches);
The limit applies per bucket; scan each bucket to cover a multi-bucket table.