このページ
- 1. テーブルと行
- 2. 1対多、多対多
- 3. シーケンス
- 4. 楽観ロック(行バージョン)
- 5. CHECK制約と複数キー不変条件
- 6. 冪等性キー
- 7. ソフトデリート
- 8. 複合順序付け(ORDER BY a, b)
- 9. JSONB
- 10. カスケード削除/外部キー
- 11. 不要になるアウトボックス
- 12. 監査履歴
- 13. ロールバックウィンドウ(逆方向ミラー)
- 14. 分析エクスポート
- 15. ロード順序(インデックス後回しの規則)
- 16. TTL付きセッションコンテキスト
- 17. エピソード記憶(時間×意味)
- 18. ハイブリッド検索のRAGチャンク
- 19. センサーキャッシュ(最新値+生存リース)
- 20. エッジ集計(書き込み時GROUP BY+アップリンク)
- 21. 行の純関数としての派生状態
- 22. PG/MySQL のスキーマを移植する
- レシピ索引
RDS→kevyモデリング・クックブック
リレーショナルなデータモデルをkevyへ移すためのレシピ集です。以下のレシピはすべて出荷済みのプリミティブだけを使います——ロードマップ上の機能も「近日公開」もありません。各レシピは、置き換える対象のRDS概念と、それを担うkevyのパターンを名指しで示します。
すべてのレシピに共通する設計姿勢は、スキーマではなくアクセスパスをモデリングすることです。RDSではその決定をクエリプランナーに先送りできますが、kevyはあなた自身に宣言させます——その見返りが、サービング時のマイクロ秒単位のページ応答です(実測値はbench/VALIDATION-LEDGER.mdにあります)。
各コマンドブロックは、まっさらなローカルkevy(kevy --port 6004。レシピ11〜14、16、20はさらにkevy.tomlに[feed] enabled = trueが必要です——docs/cdc.md参照)に対してそのまま実行できます。bench/cookbook_smoke.shが下記のすべてのkevy-cli行を使い捨てサーバーに対して実行するので、ブロックの内容は常に正直に保たれます。
1. テーブルと行
SQL相当:CREATE TABLE + SELECT col FROM t WHERE id = ?——マトリクス:テーブル・行・カラム。
行は、型を表すプレフィックスの下のハッシュです。
kevy-cli -p 6004 HSET user:42 name ada email ada@example.com age 36
kevy-cli -p 6004 HGET user:42 name
kevy-cli -p 6004 HGET user:42 phone # NULL = absent field: already answers (nil)- テーブル→キープレフィックス(
user:)。カラム→ハッシュフィールド。主キー→キーそのもの。 - NULL=フィールドの不在。番兵文字列を格納してはいけません。存在しないフィールドへの
HGETはすでにnilを返しますし、インデックス仕様はフィールドの欠けた行を「除外された行」として扱います(IDX.VERIFYのカウントで確認できます)。 - カラムの型はあなたの管轄です。kevyが格納するのはバイト列です。型が意味を持つ場所——インデックス作成時(
TYPE i64|f64|str|vector)——で宣言してください。型強制の失敗はカウントされ、黙ってインデックスされることはありません。
2. 1対多、多対多
SQL相当:外部キーカラム+中間テーブル。SELECT … FROM orders WHERE user_id = ?——マトリクス:JOIN。
リンクキーが関係を担います。片側ごとに1つのセットです。
kevy-cli -p 6004 HSET order:1001 user_id 42 total 1999 status shipped
kevy-cli -p 6004 HSET order:1002 user_id 42 total 550 status pending
kevy-cli -p 6004 SADD user:42:orders 1001 1002 # 1-N: member = order id
kevy-cli -p 6004 RPUSH order:1001:items sku-7 sku-9
kevy-cli -p 6004 SADD tag:urgent:orders 1001 # N-M: one set per side
kevy-cli -p 6004 SADD order:1001:tags urgentあるいはリンクキーを一切使わない手もあります。外部キーを行の中に置き(上のuser_id)、インデックスを宣言する——IDX.QUERY … EQ 42がこの世界のSELECT … WHERE user_id = 42で、1ホップでハイドレートされます。
kevy-cli -p 6004 IDX.CREATE order_user ON PREFIX order: FIELD user_id TYPE i64 KIND range
kevy-cli -p 6004 IDX.QUERY order_user EQ 42 FIELDS total status3. シーケンス
SQL相当:AUTO_INCREMENT / CREATE SEQUENCE + nextval()——マトリクス:主キー・UNIQUE・AUTO_INCREMENT。
kevy-cli -p 6004 INCR seq:order # one id
kevy-cli -p 6004 INCRBY seq:order 100 # block allocation: hand out 100 ids
# from app memory, refill when dryブロック割り当てが高スループット形です。クラッシュ時に欠番が出るのは、PostgreSQLのシーケンスと同じ契約です。
4. 楽観ロック(行バージョン)
SQL相当:UPDATE t SET …, version = v+1 WHERE id = ? AND version = v(バージョンカラムCAS)——マトリクス:トランザクション。
サーバー側はWATCH/MULTI——CASループです。トランザクションはコネクションスコープなので、1つのREPLセッション内で実行します(ここではヒアドキュメントで流し込みます)。
kevy-cli -p 6004 HSET user:42 balance 100 version 7
kevy-cli -p 6004 <<'TXN'
WATCH user:42
HGET user:42 version
MULTI
HSET user:42 balance 90 version 8
EXEC
TXNWATCHの後に誰かがuser:42に触れていた場合、EXECはnilを返します——レースに負けたということなので、読み直してリトライしてください。
組み込み側では、読んで・判断して・書くの一連を1つのatomic()ブロック内で実行します——シャードロックのおかげで、リトライループなしに分岐がレースフリーになります。
5. CHECK制約と複数キー不変条件
SQL相当:CHECK (balance >= 0)+トリガー維持の監査行——マトリクス:制約とトリガー。
RDSはCHECK (balance >= 0)をエンジン内で実行します。kevyでの置き換えはアトミックブロック内の読み取りです。不変条件の評価はアプリが行い、その判断と書き込みコミットが一体であることをエンジンが保証します。
具体的には、クロージャからErrを返すと、それが行ったすべての書き込みが——メモリ上でもAOF上でも——巻き戻されます。拒否されたトランザクションは痕跡を残さないので、検証がすべての書き込みより先に来るようにクロージャを組み立てる必要はありません。(この保証が本物になったのは4.0からです。それ以前のバージョンは、拒否されたクロージャの書き込みをメモリ上には生かしたままAOFフレームだけを捨てていたため、再起動が動作中のプロセスと食い違いました。)
// embedded — debit that must not overdraw, plus an audit row:
store.atomic(b"acct:7", |ctx| {
let bal: i64 = parse(ctx.hget(b"acct:7", b"balance")?);
if bal < amount { return Err(Overdraw); }
ctx.hset(b"acct:7", &[(b"balance", &(bal - amount))])?;
ctx.rpush(b"acct:7:ledger", &[entry])?;
Ok(())
})シャードをまたぐ不変条件にはatomic_all_shards(決定的ロック順序、文書化されたデッドロック免除)があります。使いどころは控えめに——これは直列化可能トランザクションという名のハンマーであり、大半の不変条件は設計上1つのキープレフィックスの下に収まるものです。
6. 冪等性キー
SQL相当:UNIQUE INDEX + INSERT … ON CONFLICT DO NOTHING——マトリクス:主キー・UNIQUE・AUTO_INCREMENT。
kevy-cli -p 6004 HSET req:9001 idem_key pay-2026-07-04-a77 amount 1999
kevy-cli -p 6004 IDX.CREATE req_idem ON PREFIX req: FIELD idem_key TYPE str KIND unique
kevy-cli -p 6004 IDX.QUERY req_idem EQ pay-2026-07-04-a77 # duplicates are visible as multi-hit reads
kevy-cli -p 6004 IDX.VERIFY req_idem # ...and counted here
kevy-cli -p 6004 SET idem:pay-2026-07-04-a77 1 NX PX 86400000行を書いてからクエリします——重複は*可視*です(uniqueは書き込みを拒否する代わりにVERIFYで重複を数える、宣言的なフェンスであって書き込みゲートではありません)。ハードなゲートが必要なら、処理の前にSET … NX PX形を使います。NXがアトミックな占有宣言で、TTLが保持ウィンドウです。
7. ソフトデリート
SQL相当:deletedフラグカラム+部分インデックス/ビューWHERE deleted = 0——マトリクス:VIEW。
消さずにフラグを立てます。
kevy-cli -p 6004 HSET user:42 deleted 0 age 36
kevy-cli -p 6004 HSET user:43 deleted 1 age 51
kevy-cli -p 6004 IDX.CREATE user_live ON PREFIX user: FIELD deleted TYPE i64 KIND range
kevy-cli -p 6004 IDX.QUERY user_live EQ 0 LIMIT 100 # live rows onlyビューを使えばフィルタを恒久的に畳み込めます——呼び出し側が条件を毎回書き直す必要はなくなります。
kevy-cli -p 6004 IDX.CREATE user_age ON PREFIX user: FIELD age TYPE i64 KIND range
kevy-cli -p 6004 VIEW.CREATE live_users QUERY '(' AND user_live EQ 0 user_age RANGE 18 200 ')' ORDER BY user_age
kevy-cli -p 6004 VIEW.QUERY live_users LIMIT 108. 複合順序付け(ORDER BY a, b)
SQL相当:複合インデックスでのORDER BY a, b——マトリクス:ORDER BY / LIMIT / OFFSET。
複合キーを書き込み時に1つのインデックス対象フィールドへエンコードします。有界な整数bならscore = a * 1_000_000 + b、辞書順の複合ならゼロ埋め文字列フィールドです——インデックスは1本、ORDER BYも1つ。書き込みフックが他のフィールドと同様に維持します。
kevy-cli -p 6004 HSET evt:1 ord '2026-07-04|000042'
kevy-cli -p 6004 HSET evt:2 ord '2026-07-04|000007'
kevy-cli -p 6004 HSET evt:3 ord '2026-07-05|000001'
kevy-cli -p 6004 IDX.CREATE evt_ord ON PREFIX evt: FIELD ord TYPE str KIND range
kevy-cli -p 6004 IDX.QUERY evt_ord RANGE '2026-07-04|000000' '2026-07-04|999999' LIMIT 1009. JSONB
SQL相当:生成カラムインデックス付きのJSON/JSONBカラム——マトリクス:型システム。
ハッシュフィールドへ平坦化します。profile.city→フィールドprofile.city。フィールド単位の読み書き、フィールドTTL(HEXPIRE)、インデックス可能性はそのまま残ります——JSONBが与えてくれたもののうちJSONパスクエリだけが恒久的に対象外です(クエリエンジンへの坂道。docs/designing-on-kevy.mdのREFUSEDテーブル参照)。
kevy-cli -p 6004 HSET user:7 profile.city tokyo profile.plan pro
kevy-cli -p 6004 HGET user:7 profile.city
kevy-cli -p 6004 HEXPIRE user:7 3600 FIELDS 1 profile.plan # per-field TTL survives the flattening誰もインデックスしない深いネストのブロブは、シリアライズ済みの1フィールドのままで構いません。パスが意味を持った瞬間に、フィールドへ昇格させてください。
10. カスケード削除/外部キー
SQL相当:FOREIGN KEY … ON DELETE CASCADE——マトリクス:制約とトリガー。
カスケードはアプリのパターンであって、エンジンの魔法では決してありません。
- 同期・小さな影響範囲:1つのアトミックブロック内で削除(
ctx.del(row)、ctx.srem(parent_link, id))。 - 一括・プレフィックス形:
delete-prefix——レート制限つき、再開可能。 - 非同期:CDCコンシューマ(
PREFIX付きFEED.READ)が親の削除に反応して子を掃除——コミット後・疎結合・リプレイ可能な、トリガーの置き換えです。
kevy-cli -p 6004 HSET order:1001 user_id 42
kevy-cli -p 6004 RPUSH order:1001:items sku-7 sku-9
kevy-cli -p 6004 SADD order:1001:tags urgent
kevy-cli delete-prefix -p 6004 --rate 5000 order:1001: # children gone, parent row stays11. 不要になるアウトボックス
SQL相当:トランザクショナル・アウトボックステーブル+リレーワーカー——マトリクス:CDC。
トランザクショナル・アウトボックスというパターンは、RDSのコミットとメッセージバスへのpublishをアトミックにできないから存在します。kevyではフィードがアウトボックスです。コミットされた各書き込みは、すでに(generation, offset)カーソル位置の変更フレームであり、at-least-once配送・プレフィックスフィルタ可能です(docs/cdc.md)。FEED.READを消費してください。二本目のジャーナルを組んではいけません。
# needs [feed] enabled = true in kevy.toml (docs/cdc.md)
kevy-cli -p 6004 HSET order:9001 status paid
kevy-cli -p 6004 FEED.SHARDS
kevy-cli -p 6004 FEED.TAIL 0 # a fresh consumer's starting cursor
kevy-cli -p 6004 FEED.READ 0 $(kevy-cli -p 6004 FEED.TAIL 0 | head -1 | awk '{print $3}') 0 COUNT 10 PREFIX order: # generation は FEED.TAIL から:カウンタではなく識別子12. 監査履歴
SQL相当:トリガー維持の監査/履歴テーブル(またはbinlog考古学)——マトリクス:CDC。
CDCの保持期間こそが監査ログです。フレームはコミット順に、適用された効果のargvを運びます。コンプライアンス上負っているウィンドウに合わせてフィードのバックログをサイズし、カーソルコンシューマでコールドストレージへエクスポートします。特定時点の再構築には、スナップショットをリストアして(gen, offset)リカバリポイントまでリプレイします(docs/persistence.md)。
kevy-cli -p 6004 HSET acct:7 balance 100
kevy-cli -p 6004 HSET acct:7 balance 90
kevy-cli -p 6004 FEED.READ 0 $(kevy-cli -p 6004 FEED.TAIL 0 | head -1 | awk '{print $3}') 0 COUNT 100 PREFIX acct: # 誰が何をいつ書いたか、コミット順に13. ロールバックウィンドウ(逆方向ミラー)
SQL相当:カットオーバー中の旧プライマリへの逆レプリケーション——移行プレイブックのフェーズ5。
カットオーバー中は、kevyへの書き込みを旧RDSへ書き戻すCDCコンシューマ(FEED.READ→UPDATE文)を走らせます。こうしておけばロールバック計画は「アプリの向き先を戻す」であって、「データを逆移行する」ではなくなります。確信が固まったらミラーを退役させます。kevy-cli diff(プレフィックスごとのダイジェスト)が確信の計器です。
kevy-cli -p 6004 HSET user:42 name ada
kevy-cli -p 6004 FEED.READ 0 $(kevy-cli -p 6004 FEED.TAIL 0 | head -1 | awk '{print $3}') 0 COUNT 10 PREFIX user: # ミラー消費者の読み取りループ
kevy-cli diff 127.0.0.1:6004 127.0.0.1:6004 user: # digests match: safe form of the check
kevy-cli diff old-rds-mirror.internal:6379 127.0.0.1:6004 user: # needs-external14. 分析エクスポート
SQL相当:ウェアハウスへ流すETLジョブ/binlogタップ——マトリクス:CDC。
サービングと分析はエンジンを共有しません。エクスポートのパターンは次の通りです。
export——論理エクスポート。再開可能で、RESPが通じる場所ならどこへでもロードできます。- CDC→ウェアハウス:カーソルコンシューマがOLAPストアへinsertをストリーミング。まさにCDC-to-Kafkaの形です。
- 読み取り専用リスナー(
docs/embedded-listener.md):組み込みアプリからのアドホックな取り出しに。
kevy-cli -p 6004 HSET order:1001 user_id 42 total 1999
kevy-cli export -p 6004 --prefix order: /tmp/orders.resp
kevy-cli -p 6004 FEED.READ 0 $(kevy-cli -p 6004 FEED.TAIL 0 | head -1 | awk '{print $3}') 0 COUNT 100 PREFIX order: # CDC からウェアハウスへの読み取りループ15. ロード順序(インデックス後回しの規則)
SQL相当:LOAD DATAが先、CREATE INDEXが後(バルクロードの規律)——マトリクス:セカンダリインデックスDDL。
バルクロードが先、インデックス/ビューの宣言は後です。バックフィルは既存の行から100万行あたり約7秒で構築します——インポートする行ごとに書き込みフックのコストを払うより桁違いに安上がりです(docs/migration.md)。
kevy-cli -p 6004 HSET item:1 price 10
kevy-cli -p 6004 HSET item:2 price 25
kevy-cli -p 6004 HSET item:3 price 7
kevy-cli export -p 6004 --prefix item: /tmp/items.resp
kevy-cli import -p 6004 /tmp/items.resp # bulk load FIRST: no index write hook to pay
kevy-cli -p 6004 IDX.CREATE item_price ON PREFIX item: FIELD price TYPE i64 KIND range # declare AFTER: backfill
kevy-cli -p 6004 IDX.QUERY item_price RANGE 0 100 LIMIT 10続く3つのレシピはワークロードを入れ替えます。置き換える対象はRDSではなく、AIエージェントのメモリスタックです。新しいものは何も要りません——セッション状態も、エピソード記憶も、RAG検索も、キープレフィックスを着替えただけの同じアクセスパスパターンです。
16. TTL付きセッションコンテキスト
SQL相当:セッションテーブル+期限切れ掃除のcronジョブ——マトリクス:運用上の差分。
エージェントの作業コンテキストは、リース付きの1行です。コンパクション済みの会話はハッシュに住み、EXPIREがアイドル退去ポリシーです(ターンごとに更新——スライディングウィンドウ)。そして「ターン7の時点でエージェントは何を知っていたか」と聞かれたときにリプレイする監査証跡が、フィードです。
# needs [feed] enabled = true in kevy.toml (docs/cdc.md)
kevy-cli -p 6004 HSET session:a7 user 42 turns 6 messages 'wants refund for order 1001; tone calm' last_tool order_lookup
kevy-cli -p 6004 EXPIRE session:a7 3600
kevy-cli -p 6004 HSET session:a7 turns 7 messages 'refund approved; awaiting confirmation'
kevy-cli -p 6004 EXPIRE session:a7 3600 # renew the lease on every turn
kevy-cli -p 6004 FEED.TAIL 0 # audit cursor: where the log ends now
kevy-cli -p 6004 FEED.READ 0 $(kevy-cli -p 6004 FEED.TAIL 0 | head -1 | awk '{print $3}') 0 COUNT 100 PREFIX session: # generation は FEED.TAIL から:カウンタではなく識別子messagesフィールドの中身は、あなたのコンパクションステップが生成する要約なら何でも構いません。書き換えはHSET1回で、しかもすべての改訂はすでにコミット順の変更フレームになっています——多くのエージェントフレームワークが後付けする「会話履歴」テーブルは、レシピ12の監査ログとしてタダで手に入ります。
17. エピソード記憶(時間×意味)
SQL相当:WHERE ts BETWEEN …+pgvectorのORDER BY embedding <=> ? LIMIT k——マトリクス:SELECT。
エピソード記憶は、同じ行たちに対して2つの質問に答えます。最近何が起きたか(時間)と、これに似ているものは何か(意味)です。プレフィックスは1つ、質問ごとにインデックスを1本——DIM 8はデモを読みやすくするためで、実際の埋め込みは768次元以上をf32-LEブロブとして送ります。下のcsv:デバッグ形式は、ベクトルを受け付けるすべての場所で使えます(格納フィールドもクエリベクトルも同じパーサーを通ります——docs/vector-search.md)。
kevy-cli -p 6004 HSET mem:1 ts 1783200000 kind obs what 'user prefers dark roast' v csv:0.9,0.1,0,0,0,0,0,0
kevy-cli -p 6004 HSET mem:2 ts 1783203600 kind obs what 'user asked about decaf' v csv:0.8,0.3,0.1,0,0,0,0,0
kevy-cli -p 6004 HSET mem:3 ts 1783207200 kind reflection what 'coffee questions cluster in the morning' v csv:0,0.2,0.9,0.1,0,0,0,0
kevy-cli -p 6004 IDX.CREATE mem_ts ON PREFIX mem: FIELD ts TYPE i64 KIND range
kevy-cli -p 6004 IDX.CREATE mem_kind ON PREFIX mem: FIELD kind TYPE str KIND range
kevy-cli -p 6004 IDX.CREATE mem_ann ON PREFIX mem: FIELD v TYPE vector KIND ann DIM 8
kevy-cli -p 6004 IDX.QUERY mem_ts RANGE 1783203000 1783210000 LIMIT 10 FIELDS what # recent memories
kevy-cli -p 6004 IDX.QUERY mem_ann KNN csv:0.85,0.2,0,0,0,0,0,0 LIMIT 2 FIELDS what ts # similar memories
kevy-cli -p 6004 IDX.QUERY COMPOSE AND mem_ts RANGE 1783203000 1783210000 mem_kind EQ reflection LIMIT 10 FIELDS whatCOMPOSE ANDはスカラーの脚(RANGE/EQ)を連言します——ここでは「この時間窓の中で、かつreflectionであるもの」。*窓の中で似ているもの*については、意図的にKNNの脚がありません(グラフ探索の内側でのフィルタリングはクエリエンジンへの坂道であり、REFUSEDです)。LIMITに余裕を持たせてKNNを実行し、上のようにFIELDSでtsをハイドレートして、窓の外のヒットをクライアント側で捨ててください。
18. ハイブリッド検索のRAGチャンク
SQL相当:tsvector全文検索+pgvector KNNのアプリ側融合——マトリクス:SELECT。
チャンクは2つの検索面——テキストとその埋め込み——を併せ持つ行なので、1回の書き込みが両方のインデックスを維持します。
kevy-cli -p 6004 HSET chunk:1 doc kevy-guide seq 1 body 'rows are hashes under a typed key prefix' v csv:1,0,0,0,0,0,0,0
kevy-cli -p 6004 HSET chunk:2 doc kevy-guide seq 2 body 'indexes are declared once and maintained by the write hook' v csv:0,1,0,0,0,0,0,0
kevy-cli -p 6004 HSET chunk:3 doc kevy-guide seq 3 body 'the feed streams every committed write as a change frame' v csv:0,0,1,0,0,0,0,0
kevy-cli -p 6004 IDX.CREATE chunk_text ON PREFIX chunk: FIELD body TYPE str KIND text
kevy-cli -p 6004 IDX.CREATE chunk_ann ON PREFIX chunk: FIELD v TYPE vector KIND ann DIM 8
kevy-cli -p 6004 IDX.QUERY HYBRID chunk_text MATCH 'typed key prefix' chunk_ann KNN csv:0.9,0.1,0.1,0,0,0,0,0 LIMIT 2 FIELDS body
kevy-cli -p 6004 IDX.QUERY HYBRID chunk_text MATCH 'change frame' chunk_ann KNN csv:0,0.1,0.9,0,0,0,0,0 LIMIT 2 RRFK 20 FIELDS bodyHYBRIDは両方の脚をサーバー側で実行し、Reciprocal Rank Fusionで融合します。各キーはBM25リストとKNNリストにわたってΣ 1/(k + rank)のスコアを得ます——ランクのみを使うので、性質の異なる2つのスコア尺度を正規化する必要がなく、*両方*の脚で上位に来るチャンクが片方だけで首位のチャンクに勝ちます。RRFKがそのk(デフォルト60)です。各脚のトップヒットを信頼していて、そこでの一致を支配的にしたいなら下げ、両リストのより深いところまで見た合意へ融合を平らにしたいなら上げてください。
最後の2つのレシピは、ラックの外へ出ます。エッジノードの上のkevy——同じサーバーバイナリ、あるいはcoreティアまで絞り込んで655 KBにしたkevy-embedded(docs/iot.md)——は同じverbを話すので、パターンはデータセンターからセンサーゲートウェイまでそのまま持ち運べます。
19. センサーキャッシュ(最新値+生存リース)
SQL相当:readings_latestアップサートテーブル+鮮度チェックのcron——マトリクス:運用上の差分。
各センサーの現在値は1つの行で、TTLが生存契約です。報告が止まったセンサーはキャッシュから期限切れで消えます——不在こそがオフラインの信号であり、書くべき掃除ジョブはありません。
kevy-cli -p 6004 HSET sensor:t1 val 21.5 unit C ts 1783200000
kevy-cli -p 6004 EXPIRE sensor:t1 90
kevy-cli -p 6004 HSET sensor:t1 val 21.7 unit C ts 1783200030
kevy-cli -p 6004 EXPIRE sensor:t1 90 # every report renews the lease
kevy-cli -p 6004 EXISTS sensor:t1 # 1 = reporting, 0 = gone darkリースの長さはアラーム許容度に合わせます(ここでは90秒=30秒間隔の報告を3回落としたらオフライン)。ポーリングの代わりにセンサーの沈黙へ*反応*したいなら、x(expired)クラスを含むkeyspace notificationsを有効化して期限切れイベントを購読してください——同じ契約のプッシュ形です(docs/pubsub.md)。
直近ウィンドウは、ハードキャップ付きのストリームです——MAXLEN ~がノードのメモリを稼働時間に関係なく有界に保ちます。何か月も動き続けるエッジ機ではこれこそが大事な不変条件です。
kevy-cli -p 6004 XADD sensor:t1:log MAXLEN '~' 1000 '*' val 21.5
kevy-cli -p 6004 XADD sensor:t1:log MAXLEN '~' 1000 '*' val 21.7
kevy-cli -p 6004 XLEN sensor:t1:log
kevy-cli -p 6004 XRANGE sensor:t1:log - + COUNT 10組み込み形:ゲートウェイプロセスの中から型付きAPIで同じverbを使います——store.hset(…)/store.expire(…)/store.xadd(…)——ソケットは一切ありません。このレシピが使うものはすべてcoreフィーチャーティアに収まっています(docs/iot.md)。
20. エッジ集計(書き込み時GROUP BY+アップリンク)
SQL相当:ダッシュボード更新のたびに再実行されるSELECT zone, COUNT(*), SUM(w) … GROUP BY zone——マトリクス:GROUP BYと集計。
エッジノードはローカルで要約し、要約だけを送ります——生の読み取り値はアップリンクに流すには多すぎます。集計を一度宣言すれば、それは書き込みパスの中で維持されます。つまり「集計ジョブ」はただ存在しなくなるのです。
kevy-cli -p 6004 HSET reading:1 zone floor1 w 120
kevy-cli -p 6004 HSET reading:2 zone floor1 w 180
kevy-cli -p 6004 HSET reading:3 zone floor2 w 95
kevy-cli -p 6004 IDX.CREATE zone_w ON PREFIX reading: FIELD w TYPE i64 KIND agg GROUPBY zone
kevy-cli -p 6004 IDX.QUERY zone_w GROUP floor1 # [count, sum, min, max, avg]
kevy-cli -p 6004 IDX.QUERY zone_w GROUPS BY sum LIMIT 10 # zones ranked by loadアップリンクは、レシピ11のアウトボックスが作業着を着たものです。フィードはすでにコミット済みのすべての書き込みをジャーナルしているので、クラウド同期コンシューマはカーソルループになります。何時間も切れるリンクをまたいで再開可能で、at-least-once・コミット順・クラウドが必要とするものだけにプレフィックスフィルタ済みです。
# needs [feed] enabled = true in kevy.toml (docs/cdc.md)
kevy-cli -p 6004 FEED.TAIL 0
kevy-cli -p 6004 FEED.READ 0 $(kevy-cli -p 6004 FEED.TAIL 0 | head -1 | awk '{print $3}') 0 COUNT 100 PREFIX reading: # アップリンクのループレシピ19のMAXLENキャップとTTLを組み合わせてください。生の読み取り値はノード上で有界に保たれ、集計行は小さいまま、フィードカーソルは再起動を生き延びます——kevyそれ自体の他に可動部品ゼロの、エッジの物語の全部です。
21. 行の純関数としての派生状態
SQL 対応:トリガ層まるごと——ON DELETE CASCADE、UNIQUE 制約、そしてそれらを信じなくなったあとに書く突合ジョブ——マトリクス:制約とトリガ。
これは上のレシピ群がずっと周回していたパターンです:§2 のリンクキー、§5 の不変条件、§10 のカスケード、§12 の監査行は、同じ考えを四度当てはめたものです。一度きちんと述べれば、カスケードも一意性もドリフト検出も同時に片づきます。 これは本番の移行から来ていて、そこへ辿り着くのに一日かかりました——節約する価値のある一日です。
考え方:行から、そこに由来するすべてのキーへの純関数をひとつ書きます。キーを更新する手続きではなく、「何が存在すべきか」を返す関数です。
// Everything user:42 implies, computed from the row alone.
fn derived(id: &[u8], row: &Row) -> Vec<Vec<u8>> {
vec![
key(b"email:", &row.email), // uniqueness claim
key(b"dept:", &row.dept, b":users"), // membership
]
}すると、あらゆる操作が差分になり、そのどれもが設計されるのではなく落ちてきます:
| 操作 | すること | ただで付いてくるもの |
|---|---|---|
| 挿入 | derived(new) を足す | 主張とメンバーシップが一緒に現れる |
| 更新 | derived(new) - derived(old) を足し、derived(old) - derived(new) を消す | 変更された email が古い主張を解放する——誰もが手で書いてしまうバグ |
| 削除 | derived(old) を消す | カスケードが別のコード経路でなくなる |
| 検証 | 全行で derived を再計算し、実在するものと突合 | 設計しなくて済んだドリフト検出器 |
元を取るのは更新の行です。手書きのカスケードはほぼ必ず、新しい主張を足して古いものの解放を忘れます——解放はチケットで誰も実演しないケースだからです。
store.atomic_all_shards(|ctx| {
let old = read_row(ctx, id)?;
let (want, had) = (derived(id, &new), derived(id, &old));
for k in want.iter().filter(|k| !had.contains(k)) {
if ctx.exists(&[k]) > 0 { return Err(Taken); } // uniqueness
ctx.set(k, id);
}
for k in had.iter().filter(|k| !want.contains(k)) {
ctx.del(&[k]); // release
}
write_row(ctx, id, &new)
})Err を返せば全体が巻き戻るので(§5)、拒否された書き込みは、行も、半分だけ適用された主張の集合も残しません。
主張か、インデックスか。 一意性の主張は第二の真実の源であり、行からドリフトしえます。セカンダリインデックスは構造上派生であり、ドリフトしえません。atomic_all_shards の中からは直接引けます:
if ctx.idx_count(b"email_idx", &want, &want)? > 0 { return Err(Taken); }制限が二つ、どちらも意図的です:
atomic_all_shardsの上だけ。 インデックスの項目は、それが指すキーのシャードに住むので、「この email を持つ行があるか」はすべてのシャードについての問いです。単一シャードのatomic()はロックを一つしか持たず、自分の取り分についてしか答えられません——キースペースの 1/N しか見ない一意性検査はほぼ常に「一意」と報告するので、脚注付きで提供するのではなく、提供しません。- インデックスの読みは、そのトランザクション自身の書き込みを見ません。 保守はコミット時に走ります。二行を挿入するクロージャは、その二行同士を自分で比べる必要があります。
検証。 derived が関数である以上、検査器はその関数そのものです——同じものを reconcile に渡してください:
let report = store.snapshot().reconcile(
b"user:", // the rows
&[b"email:", b"dept:"], // where derived keys live
|key, row| derived(key, row),
);
if !report.is_clean() {
warn!("{} missing, {} orphaned", report.missing_count, report.orphaned_count);
}これは両方向に突合します。そこが自分で書かずに済ませる価値のある部分です。欠けたキーは失われた派生状態ですが、孤児——行が消えたのに残っている主張——こそ、半分だけ適用された更新が残すものであり、あとの挿入を黙って塞ぐ故障です。欠落だけを探す検査器は、まさにその故障のあいだ「クリーン」と報告します。
これはスナップショット(store.snapshot())に対して、すべてのシャードロックの下で凍結して走るので、並行する書き込みをドリフトと取り違えません。ただしそれは、書き込み自体が原子的だった場合に限ります:行とその主張はひとつの atomic_all_shards ブロックに入っていなければならず、さもなければ、見つけるべき半適用状態が本当に存在します。突合と原子的書き込みは、同じ保証を両端から見たものです。
起動時に走らせても、定期でも、書き込みを信じきったあとは走らせなくてもかまいません——でも走らせてください。「自分が信じている不変条件が、実際に持っている不変条件だ」と言えるものは、これしかないからです。
22. PG/MySQL のスキーマを移植する
SQL 対応:スキーマファイルそのもの——CREATE TABLE、CREATE INDEX、CREATE VIEW——マトリクス:セカンダリインデックスの DDL。
レシピ 1–8 が手でやることのすべてを、すでに手元にある SQL からコンパイルします。kevy-sql(そして kevy-cli sql という顔)は宣言時のコンパイラです:移行ツールのようにスキーマを一度だけ読み、明示的な TABLE.DECLARE / VIEW.CREATE コマンドと*クエリカード*——$N の枠を残した既製の IDX.QUERY テンプレート——を出します。サーバの中でクエリごとに走るものは何もありません。実行時の場当たり SQL は、エンジン自身が拒み続けます(Law 3)。
対象のスキーマ——docs/examples/shop.sql、実在の users/orders/order_items を削ったもの:
CREATE TABLE users (
id bigserial PRIMARY KEY,
email text,
name text,
plan text
);
CREATE UNIQUE INDEX ON users (email);
CREATE TABLE orders (
id bigserial PRIMARY KEY,
user_id bigint,
status text,
total numeric(10,2),
created_at bigint -- epoch seconds, app-encoded
);
-- INCLUDE = PG covering columns -> kevy stored VALUES (residual FILTER/SORT).
CREATE INDEX ON orders (status) INCLUDE (total, created_at);
-- Multi-column -> a composite ORDERPATH (the (user_id, created_at DESC) walk).
CREATE INDEX ON orders (user_id, created_at DESC);
CREATE TABLE order_items (
id bigserial PRIMARY KEY,
order_id bigint,
sku text,
qty int
);
CREATE INDEX ON order_items (order_id);
CREATE VIEW paid_orders AS
SELECT * FROM orders WHERE status = 'paid';
CREATE VIEW recent_orders_by_user AS
SELECT id, status, total, created_at FROM orders
WHERE user_id = $1
ORDER BY created_at DESC
LIMIT 20;コンパイルし、宣言をサーバに適用します:
kevy-cli sql compile docs/examples/shop.sql
kevy-cli sql compile docs/examples/shop.sql --apply --url 127.0.0.1:6004コンパイル結果のスクリプト(そのまま)。各テーブルは自分のインデックスをひとつの TABLE.DECLARE に畳み込みます。定数のビューはエンジンのビューに、パラメータ付きのビューはクエリカードになります。粗い型の対応づけは、どれも notes で正直に名指しされます(kevy の列は i64|f64|str だけ——タイムスタンプはアプリ側の符号化で、serial は id を割り当ててくれません):
TABLE.DECLARE users PREFIX users: PK id COLUMN id i64 COLUMN email str COLUMN name str COLUMN plan str INDEX email unique
TABLE.DECLARE orders PREFIX orders: PK id COLUMN id i64 COLUMN user_id i64 COLUMN status str COLUMN total f64 COLUMN created_at i64 INDEX status range VALUES total created_at ORDERPATH user_id_created_at ON user_id THEN created_at DESC
TABLE.DECLARE order_items PREFIX order_items: PK id COLUMN id i64 COLUMN order_id i64 COLUMN sku str COLUMN qty i64 INDEX order_id range
VIEW.CREATE paid_orders QUERY orders.status EQ paid ORDER BY orders.status
# ---- query card: recent_orders_by_user ----
# runtime template — substitute the $N slots and send as-is:
# $1 = user_id (i64)
# IDX.QUERY orders.user_id_created_at WHERE user_id EQ $1 LIMIT 20 FIELDS id status total created_at
# notes:
# - users.id: bigserial → i64, but ids do NOT auto-increment — allocate them app-side (INCR block, cookbook §3)
# - orders.total: numeric → f64 — fixed-point precision becomes binary float; keep money as integer cents if exactness matters
# - view paid_orders: read with VIEW.QUERY paid_orders, then hydrate rows with HMGET <key> id user_id status total created_at行はテーブル接頭辞の下の普通のハッシュで(レシピ 1)、コンパイルされた経路はすぐに供せます——$1 の枠に実際の引数を入れれば、カードはそのまま走ります:
kevy-cli -p 6004 HSET users:1 id 1 email ada@example.com name Ada plan pro
kevy-cli -p 6004 HSET orders:1 id 1 user_id 1 status paid total 19.5 created_at 1700000100
kevy-cli -p 6004 HSET orders:2 id 2 user_id 1 status pending total 5 created_at 1700000200
kevy-cli -p 6004 HSET orders:3 id 3 user_id 2 status paid total 8 created_at 1700000300
kevy-cli -p 6004 HSET order_items:1 id 1 order_id 1 sku sku-7 qty 2
kevy-cli -p 6004 IDX.QUERY users.email EQ ada@example.com
kevy-cli -p 6004 IDX.QUERY orders.user_id_created_at WHERE user_id EQ 1 LIMIT 20 FIELDS id status total created_at
kevy-cli -p 6004 VIEW.QUERY paid_orders LIMIT 10
kevy-cli -p 6004 IDX.QUERY orders.status EQ paid FILTER total RANGE 10 inf
kevy-cli -p 6004 IDX.QUERY order_items.order_id EQ 1 FIELDS sku qty
kevy-cli -p 6004 TABLE.LIST- カードのクエリは
SELECT id, status, total, created_at FROM orders WHERE user_id = 1 ORDER BY created_at DESC LIMIT 20——複合の走査が供し、新しい順、一跳で列を補います。 FILTER total RANGE 10 infの行はINCLUDEされた列に対する残余述語です——WHERE status = 'paid' AND total >= 10を、行に触れずに。order_items.order_id EQ 1が JOIN を置き換える FK 参照です(レシピ 2):クエリ二本、クエリ時の join なし。
拒否が教えます。 コンパイラはクエリ時の評価を要するものをすべて拒みます——名指しで、行と列を添えて、それをモデル化するレシピを指して。JOIN なら:
CREATE VIEW order_emails AS
SELECT id, email FROM orders
JOIN users ON users.id = orders.user_id;$ kevy-cli sql compile join.sql
kevy-cli sql: join.sql: line 6, col 3: JOIN is not compilable — kevy
refuses query-time joins (Law 3); model the lookup with an indexed FK
column (IDX.QUERY t.fk EQ …) or app-side assembly (cookbook §2)WHERE が宣言済みのどのアクセス経路にも合わないビューは、追加すべき宣言を名指ししてエラーになります(… matches no declared access path — add: CREATE INDEX ON orders (status, total))。そして実行時の場当たり SQL には、そもそも扉がありませんでした:
$ kevy-cli -p 6004 SQL SELECT * FROM users
(error) ERR unknown command 'SQL'レシピ索引
レシピ↔それが置き換えるSQL構文↔意味論と限界を明記するrds-workloads.mdのマトリクス行、の対応表です。
| # | レシピ | SQL構文 | マトリクス行 |
|---|---|---|---|
| 1 | テーブルと行 | CREATE TABLE、ポイントSELECT | テーブル・行・カラム |
| 2 | 1対多、多対多 | FKカラム、中間テーブル、WHERE fk = ? | JOIN |
| 3 | シーケンス | AUTO_INCREMENT / nextval() | PK・UNIQUE・AUTO_INCREMENT |
| 4 | 楽観ロック | バージョンカラムCASのUPDATE | トランザクション |
| 5 | CHECK制約 | CHECK (…)+監査トリガー | 制約とトリガー |
| 6 | 冪等性キー | UNIQUE INDEX + ON CONFLICT DO NOTHING | PK・UNIQUE・AUTO_INCREMENT |
| 7 | ソフトデリート | フラグカラム+フィルタ付きビュー | VIEW |
| 8 | 複合順序付け | ORDER BY a, b | ORDER BY / LIMIT / OFFSET |
| 9 | JSONB | JSONカラム+生成カラムインデックス | 型システム |
| 10 | カスケード削除/FK | ON DELETE CASCADE | 制約とトリガー |
| 11 | 不要になるアウトボックス | トランザクショナル・アウトボックステーブル | CDC |
| 12 | 監査履歴 | 監査テーブル/binlog考古学 | CDC |
| 13 | ロールバックウィンドウ | カットオーバー時の逆レプリケーション | 移行プレイブック |
| 14 | 分析エクスポート | ウェアハウスへのETL/binlogタップ | CDC |
| 15 | ロード順序 | バルクLOAD DATA、インデックスは後 | セカンダリインデックスDDL |
| 16 | TTL付きセッションコンテキスト | セッションテーブル+期限切れcron | 運用上の差分 |
| 17 | エピソード記憶 | 時間BETWEEN+pgvector KNN | SELECT |
| 18 | RAGハイブリッド検索 | tsvector+pgvector、融合 | SELECT |
| 19 | センサーキャッシュ | アップサートテーブル+鮮度cron | 運用上の差分 |
| 20 | エッジ集計 | 更新ごとのGROUP BY+ETLアップリンク | GROUP BYと集計 |
| 21 | 行の関数としての派生状態 | トリガ層まるごと:カスケード、UNIQUE、突合 | 制約とトリガー |
| 22 | PG/MySQL スキーマの移植 | CREATE TABLE / CREATE INDEX / CREATE VIEW、コンパイル済み | セカンダリインデックスの DDL |