Skip to content

Commit d66dacb

Browse files
committed
test(insert-select): cover the kv source route, its refusals, and the key column
- a point-in-time or transactional read from a kv source is refused by name; an in-transaction INSERT ... SELECT reaches that path - a source engine with no materializer (columnar) is refused by name - a kv collection whose key column is the key sentinel now carries the key: the arm uses the one shaping rule every scan path uses, so the copied column reads the row's own key instead of NULL
1 parent 2f7972d commit d66dacb

3 files changed

Lines changed: 96 additions & 1 deletion

File tree

‎nodedb/src/control/maintenance/clone_materializer/auto_source.rs‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,17 @@ pub(crate) async fn scan_source_page(
6565
let (pairs, next) =
6666
kv::scan_source_page(state, tenant_id, database_id, source_qualified, cursor)
6767
.await?;
68+
// The KV key slot sits outside the stored body. The one shaping
69+
// rule injects it (the same converter the scan and RETURNING paths
70+
// use), so a copied `key` column reads the row's own key instead of
71+
// NULL.
6872
let entries = pairs
6973
.into_iter()
70-
.map(|(key, value)| (String::from_utf8_lossy(&key).into_owned(), 0, value))
74+
.map(|(key, value)| {
75+
let key = String::from_utf8_lossy(&key).into_owned();
76+
let body = nodedb_query::msgpack_scan::kv_row_msgpack(&key, &value);
77+
(key, 0, body)
78+
})
7179
.collect();
7280
Ok((entries, next))
7381
}

‎nodedb/tests/wire/cases/insert_select_cross_engine.rs‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,3 +348,60 @@ async fn insert_select_copies_a_kv_source_through_its_own_engine() {
348348
"both rows must copy, with expression cells evaluated: {rows:?}"
349349
);
350350
}
351+
352+
/// A source engine with no `INSERT ... SELECT` materializer is refused by name:
353+
/// scanning it with the document materializer would copy nothing and report
354+
/// `INSERT 0 0`.
355+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
356+
async fn insert_select_refuses_a_source_it_cannot_scan() {
357+
let server = TestServer::start().await;
358+
359+
server
360+
.exec("CREATE COLLECTION isk_nosrc_src (id TEXT PRIMARY KEY) WITH (engine = 'columnar')")
361+
.await
362+
.unwrap();
363+
server
364+
.exec("CREATE COLLECTION isk_nosrc_dst")
365+
.await
366+
.unwrap();
367+
368+
server
369+
.expect_error(
370+
"INSERT INTO isk_nosrc_dst SELECT * FROM isk_nosrc_src",
371+
"Columnar",
372+
)
373+
.await;
374+
}
375+
376+
/// A kv collection whose primary key column is `key` stores the key outside the
377+
/// row body. The copy must still carry it: a NULL key column is silent data
378+
/// loss, and the target's own key would be minted from the wrong column.
379+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
380+
async fn insert_select_copies_a_kv_key_column() {
381+
let server = TestServer::start().await;
382+
383+
server
384+
.exec("CREATE COLLECTION isk_key_src (key TEXT PRIMARY KEY, v TEXT) WITH (engine = 'kv')")
385+
.await
386+
.unwrap();
387+
server
388+
.exec("INSERT INTO isk_key_src (key, v) VALUES ('k1', 'hello')")
389+
.await
390+
.unwrap();
391+
392+
server.exec("CREATE COLLECTION isk_key_dst").await.unwrap();
393+
server
394+
.exec("INSERT INTO isk_key_dst (key, v) SELECT key, v FROM isk_key_src")
395+
.await
396+
.unwrap();
397+
398+
let rows = server
399+
.query_rows("SELECT key, v FROM isk_key_dst")
400+
.await
401+
.unwrap();
402+
assert_eq!(
403+
rows,
404+
vec![vec!["k1".to_string(), "hello".to_string()]],
405+
"the copied key column must carry the source key: {rows:?}"
406+
);
407+
}

‎nodedb/tests/wire/cases/sql_transactions_insert_select_overlay.rs‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,3 +242,33 @@ async fn strict_insert_select_sees_source_rows_staged_earlier_in_txn() {
242242
)
243243
.await;
244244
}
245+
246+
/// In-transaction `INSERT ... SELECT` stages through the per-transaction
247+
/// overlay, so the source read is transactional. The kv materialize-scan
248+
/// carries no snapshot fields; the read is refused by name rather than copying
249+
/// committed-only rows the transaction did not ask for.
250+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
251+
async fn kv_source_refuses_an_in_transaction_read() {
252+
let server = TestServer::start().await;
253+
server
254+
.exec(
255+
"CREATE COLLECTION is_kv_tx_src (id STRING PRIMARY KEY, n INT) \
256+
WITH (engine='kv')",
257+
)
258+
.await
259+
.unwrap();
260+
server
261+
.exec("INSERT INTO is_kv_tx_src (id, n) VALUES ('a', 1)")
262+
.await
263+
.unwrap();
264+
server.exec("CREATE COLLECTION is_kv_tx_tgt").await.unwrap();
265+
266+
server.exec("BEGIN").await.unwrap();
267+
server
268+
.expect_error(
269+
"INSERT INTO is_kv_tx_tgt SELECT * FROM is_kv_tx_src",
270+
"transactional",
271+
)
272+
.await;
273+
server.exec("ROLLBACK").await.unwrap();
274+
}

0 commit comments

Comments
 (0)