feat(kv): high-throughput bulk-insert path for the KV store (COPY-based)
メンテナーはふだん 2 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 5/5
- 見積もり時間
- 1週間以上
- 初心者へのやさしさ
- 28/100
- issue の種類
- 機能追加
- 明瞭さ
- 説明が足りない
- 活発さ
- 静か
調査の方向性
Start with hyperdb-api/src/kv_store.rs:187-208 and :398-416, then read inserter.rs:135-139 and :192 to understand the existing write and COPY paths. The issue is ready only after a concrete use case selects and scopes one bulk-loading option, including its TCP-only behavior and correctness guarantees.
索引モデルが issue の本文から書いたものです。
説明
Summary
The KV store's write path (set / set_batch) tops out around 1,000–2,000 inserts/sec because every key goes through an UPDATE-then-conditional-INSERT upsert over the extended-query (text SQL) protocol. For workloads that need to load millions of new KV entries quickly, this is the bottleneck. The repo already has a binary-COPY Inserter that hits 22M+ rows/sec, so a bulk-insert path for the KV store is feasible — but it interacts with a correctness constraint that shapes the whole design.
This issue captures the design so it can be prioritized if real use cases surface. Not scheduled work today.
Background: why set_batch is slow
setperforms a manual upsert: it always issuesUPDATE {t} SET value = $3 WHERE store_name = $1 AND key = $2, and only when theUPDATEaffects 0 rows issues a guardedINSERT INTO {t} (store_name, key, value) SELECT $1, $2, $3 WHERE NOT EXISTS (SELECT 1 FROM {t} WHERE store_name = $4 AND key = $5)(hyperdb-api/src/kv_store.rs:187-208). So a new key costs 2 round-trips.set_batchwraps all per-key upserts in oneBEGIN/COMMIT(kv_store.rs:398-416), which helps, but it's still 1–2 text-SQL statements per key. That per-row overhead is the ceiling.
The correctness constraint (this is the crux)
The backing table _hyperdb_kv_store(store_name TEXT NOT NULL, key TEXT NOT NULL, value TEXT) has no PRIMARY KEY, no UNIQUE, no index — Hyper rejects all of them (0A000: Index support is disabled). Uniqueness of (store_name, key) is therefore an application-side invariant, enforced only by the UPDATE-then-conditional-INSERT ... WHERE NOT EXISTS guard (kv_store.rs:62-75 DDL, :187-208 guard).
A raw binary-COPY bypasses that guard entirely (COPY is pure append-only — no UPDATE, no conflict handling). If duplicate (store_name, key) rows ever land in the table, the read side silently returns wrong results:
| Method | Behavior under duplicate keys |
|---|---|
get / get_as |
returns one arbitrary value (no LIMIT/ORDER BY/aggregate) |
size() |
inflated — COUNT(*) counts physical rows, not distinct keys |
keys() |
returns duplicate keys (no DISTINCT) |
pop() |
returns one value, but its DELETE removes all duplicate rows for that key — silently drops the others |
exists() |
still correct (only checks for ≥1 row) |
So the design question is not throughput — it's who guarantees the keys are new and unique. Also note: COPY is TCP-only (inserter.rs:135-139), not available over gRPC.
Proposed options
The two options below are complementary, not mutually exclusive — a real use case can decide which to prioritize (or both).
Option A — load_new: maximally fast, caller guarantees uniqueness
Add an append-only bulk method that writes straight through the Inserter. Inserter::from_table(conn, KV_TABLE) (inserter.rs:192) picks up the existing 3-column schema from the catalog — no new DDL, no schema change — and add_str covers all three TEXT columns.
/// Appends entries via the binary COPY path (orders of magnitude faster than `set_batch`).
///
/// **Caller contract:** every key MUST be new and unique within this store — both
/// absent from the store already AND distinct within `entries`. This path does NO
/// upsert and NO duplicate check; violating the contract inserts duplicate rows,
/// which silently corrupt `get`/`size`/`keys`/`pop`. Use `set_batch` if keys may collide.
pub fn load_new(&self, entries: &[(&str, &str)]) -> Result<u64>
- Internally:
Inserter::from_table→ per entryadd_str(store_name); add_str(key); add_str(value); end_row()→execute(). - Name deliberately signals append-only / new-keys-only semantics at the call site (not
set_batch_fast). - Sync + async twins (async via
AsyncArrowInserter/ the async COPY path). - Trade-off: fastest possible, but a contract violation is silent corruption — squarely the caller's responsibility.
Option B — safe bulk merge: fast, dedup-preserving, no caller contract
Keep the uniqueness invariant intact by staging + merging:
- COPY the batch into a temporary staging table via the
Inserter. INSERT INTO _hyperdb_kv_store (store_name, key, value) SELECT ... FROM stage s WHERE NOT EXISTS (SELECT 1 FROM _hyperdb_kv_store t WHERE t.store_name = s.store_name AND t.key = s.key)— and dedup within the staging batch itself (e.g.DISTINCT ON).
- Trade-off: one extra pass so not raw-COPY speed, but far faster than millions of per-key upserts, and it cannot create duplicates. A good default helper for callers who can't promise clean keys.
- Could be surfaced as a helper API alongside
load_new.
Recommendation / decision guidance
- One-shot bulk load of millions of unique rows you'll query with SQL → arguably skip the KV store entirely: create a normal typed table and use the
Inserterdirectly. Full COPY throughput, real column types, nostore_nameoverhead, and no uniqueness trap (you're not pretending it's a KV store). The KV store's value is the handle semantics (get/set/pop/existsover a named store) — a bulk analytical dump uses none of that. - Data must remain a live KV store you'll
get/popagainst afterward →load_new(Option A) if the caller can guarantee new/unique keys, or the safe merge (Option B) otherwise.
Scope notes
- No schema change and no change to existing methods required for either option.
- TCP-only (COPY constraint) — document it; gRPC connections can't use this path.
- Natural fit for a post-M2 milestone; prioritize when concrete use cases appear.
Filed from an investigation of the KV write path, the COPY Inserter API, and the KV read-side invariants. File:line references are against main at time of filing.
- 主要言語
- Rust
- スター
- 3
- フォーク
- 2
- 平均マージ
- 2日 6時間
- マージ済み PR(30日)
- 34
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
tableau/hyper-api-rust のほかの issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
tableau/hyper-api-rust#294 ·
メンテナーはふだん 2 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 35/100
tableau/hyper-api-rust#311 ·
メンテナーはふだん 2 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 45/100
tableau/hyper-api-rust#305 ·
メンテナーはふだん 2 日以内に返信
-
Windows Named Pipe: verify DACL denies other users, and measure read-path perf for MCP workloadsオープン
難易度 4/5 3〜5日 初心者へのやさしさ 38/100
tableau/hyper-api-rust#302 ·
メンテナーはふだん 2 日以内に返信
-
難易度 3/5 1〜2日 初心者へのやさしさ 72/100
tableau/hyper-api-rust#300 ·
メンテナーはふだん 2 日以内に返信
tableau/hyper-api-rust の issue をすべて見る
似ている issue
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
メンテナーはふだん 1 日以内に返信
-
dependencies effort:low flow:chore rust
難易度 2/5 1〜3時間 初心者へのやさしさ 64/100
jaroslawherod/micold-ai-ide#875 ·
メンテナーはふだん 1 日以内に返信
-
priority: low
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
storytold/photocraft#6286 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
Four problem loader browser tests fail on Windows対応中かも @Patriciaath999 が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 88/100
メンテナーはふだん 1 日以内に返信
-
Kazakh (kk_KZ) and Georgian (ka_GE) system locales get English instead of the shipped translationsオープンbug
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
メンテナーはふだん 1 日以内に返信