nodedb_lite/query/document_ops/sets/
materialize.rs1use super::super::is_strict;
3use super::super::reads::loro_value_to_ndb_value;
4use crate::error::LiteError;
5use crate::query::engine::LiteQueryEngine;
6use crate::query::msgpack_helpers::{write_array_header, write_bin, write_str, write_u32};
7use crate::query::value_utils::value_to_string;
8use crate::storage::engine::StorageEngine;
9use nodedb_types::result::QueryResult;
10use nodedb_types::value::Value;
11use std::collections::HashMap;
12pub async fn materialize_scan<S: StorageEngine>(
26 engine: &LiteQueryEngine<S>,
27 collection: &str,
28 cursor: &[u8],
29 count: usize,
30) -> Result<QueryResult, LiteError> {
31 let cursor_str = if cursor.is_empty() {
32 None
33 } else {
34 Some(String::from_utf8_lossy(cursor).into_owned())
35 };
36
37 let pairs: Vec<(String, Vec<u8>)> = if is_strict(engine, collection) {
39 let schema = engine
40 .strict
41 .schema(collection)
42 .ok_or_else(|| LiteError::collection_not_found("strict", collection))?;
43 let pk_idx = schema
44 .columns
45 .iter()
46 .position(|c| c.primary_key)
47 .ok_or_else(|| LiteError::BadRequest {
48 detail: format!("strict collection '{collection}' has no primary key"),
49 })?;
50 let columns = schema.columns;
51 let all_rows = engine.strict.list_rows(collection).await?;
52 let mut out = Vec::new();
53 let mut past_cursor = cursor_str.is_none();
54 for row in all_rows {
55 let pk = row.get(pk_idx).ok_or_else(|| LiteError::Storage {
56 detail: format!(
57 "strict collection '{collection}' row omits primary key at column {pk_idx}"
58 ),
59 })?;
60 let pk = value_to_string(pk);
61 if !past_cursor {
62 if let Some(ref c) = cursor_str
63 && &pk == c
64 {
65 past_cursor = true;
66 }
67 continue;
68 }
69 if out.len() >= count {
70 break;
71 }
72 let map: HashMap<String, Value> = columns
73 .iter()
74 .enumerate()
75 .filter_map(|(i, col)| row.get(i).map(|value| (col.name.clone(), value.clone())))
76 .collect();
77 let bytes = zerompk::to_msgpack_vec(&Value::Object(map)).map_err(|e| {
78 LiteError::Serialization {
79 detail: format!("materialize_scan serialize row: {e}"),
80 }
81 })?;
82 out.push((pk, bytes));
83 }
84 out
85 } else {
86 let crdt = engine.crdt.lock().map_err(|_| LiteError::LockPoisoned)?;
87 let ids = crdt.list_ids(collection);
88 let mut out = Vec::new();
89 let mut past_cursor = cursor_str.is_none();
90 for id in ids {
91 if !past_cursor {
92 if let Some(ref c) = cursor_str
93 && &id == c
94 {
95 past_cursor = true;
96 }
97 continue;
98 }
99 if out.len() >= count {
100 break;
101 }
102 if let Some(val) = crdt.read(collection, &id) {
103 let ndb_val = loro_value_to_ndb_value(&val);
104 let bytes =
105 zerompk::to_msgpack_vec(&ndb_val).map_err(|e| LiteError::Serialization {
106 detail: format!("materialize_scan serialize crdt row: {e}"),
107 })?;
108 out.push((id, bytes));
109 }
110 }
111 drop(crdt);
112 out
113 };
114
115 let next_cursor: Vec<u8> = if pairs.len() < count {
118 Vec::new()
119 } else {
120 pairs
121 .last()
122 .map(|(id, _)| id.as_bytes().to_vec())
123 .unwrap_or_default()
124 };
125
126 let payload = encode_materialize_payload(&next_cursor, &pairs);
127
128 Ok(QueryResult {
129 columns: vec!["payload".into()],
130 rows: vec![vec![Value::Bytes(payload)]],
131 rows_affected: 0,
132 command: None,
133 })
134}
135
136fn encode_materialize_payload(next_cursor: &[u8], pairs: &[(String, Vec<u8>)]) -> Vec<u8> {
138 let mut out = Vec::new();
139 write_array_header(&mut out, 2);
140 write_bin(&mut out, next_cursor);
141 write_array_header(&mut out, pairs.len());
142 for (doc_id, value_bytes) in pairs {
143 write_array_header(&mut out, 3);
144 write_str(&mut out, doc_id.as_bytes());
145 write_u32(&mut out, 0u32);
147 write_bin(&mut out, value_bytes);
148 }
149 out
150}
151
152#[cfg(test)]
153mod tests {
154 use crate::NodeDbLite;
155 use crate::PagedbStorageMem;
156
157 async fn make_db() -> std::sync::Arc<NodeDbLite<PagedbStorageMem>> {
158 let storage = PagedbStorageMem::open_in_memory().await.unwrap();
159 NodeDbLite::open(storage).await.unwrap()
160 }
161
162 #[test]
164 fn materialize_scan_payload_envelope_shape() {
165 let payload = super::encode_materialize_payload(&[], &[]);
166 assert_eq!(payload[0], 0x92, "outer envelope must be fixarray(2)");
168 }
169
170 #[test]
173 fn materialize_scan_payload_one_entry() {
174 let doc_id = "abc".to_string();
175 let val_bytes = b"data".to_vec();
176 let payload =
177 super::encode_materialize_payload(&[], &[(doc_id.clone(), val_bytes.clone())]);
178 assert!(payload.len() > 10, "payload must have content");
181 let needle = doc_id.as_bytes();
183 let found = payload.windows(needle.len()).any(|w| w == needle);
184 assert!(found, "doc_id must appear in payload");
185 }
186
187 #[tokio::test]
189 async fn materialize_scan_empty_collection() {
190 let db = make_db().await;
191 let result = super::materialize_scan(&db.query_engine, "nonexistent_coll", &[], 10)
192 .await
193 .unwrap();
194 assert_eq!(result.columns, vec!["payload"]);
195 assert_eq!(result.rows.len(), 1);
196 if let nodedb_types::value::Value::Bytes(payload) = &result.rows[0][0] {
198 assert!(!payload.is_empty());
199 } else {
200 panic!("expected Value::Bytes for MaterializeScan result");
201 }
202 }
203}