1use std::collections::VecDeque;
11use std::pin::Pin;
12use std::sync::atomic::{AtomicU64, Ordering};
13use std::sync::{Arc, Mutex};
14
15use futures_channel::mpsc;
16use futures_util::{Stream, StreamExt};
17
18use crate::error::Error;
19use crate::render::{DataPlane, Page, Query, Revision, Row, RowId, RowPatch};
20use crate::table::{filter_indices, sort_indices};
21
22#[derive(Debug, Clone, Default)]
25pub struct Schema {
26 pub column_ids: Vec<String>,
27}
28
29#[derive(Debug, Clone)]
33struct HistoryEntry {
34 revision: Revision,
35 patch: RowPatch,
36}
37
38#[derive(Debug)]
39struct State {
40 rows: Vec<Row>,
43 index: std::collections::HashMap<RowId, usize>,
45 history: VecDeque<HistoryEntry>,
49}
50
51#[derive(Clone)]
53pub struct MemPlane {
54 state: Arc<Mutex<State>>,
55 revision: Arc<AtomicU64>,
56 schema: Schema,
57 live: Arc<Mutex<Vec<mpsc::UnboundedSender<RowPatch>>>>,
60 history_cap: usize,
61}
62
63impl MemPlane {
64 pub fn new(schema: Schema) -> Self {
66 Self {
67 state: Arc::new(Mutex::new(State {
68 rows: Vec::new(),
69 index: std::collections::HashMap::new(),
70 history: VecDeque::new(),
71 })),
72 revision: Arc::new(AtomicU64::new(0)),
73 schema,
74 live: Arc::new(Mutex::new(Vec::new())),
75 history_cap: 1024,
76 }
77 }
78
79 pub fn revision(&self) -> Revision {
81 Revision(self.revision.load(Ordering::SeqCst))
82 }
83
84 pub fn upsert(&self, row: Row) -> Revision {
86 let id = row.id.clone();
87 let (revision, patch) = {
88 let mut state = self.state.lock().expect("mem plane poisoned");
89 let revision = self.revision.fetch_add(1, Ordering::SeqCst) + 1;
90 match state.index.get(&id) {
91 Some(&i) => {
92 state.rows[i] = row.clone();
93 }
94 None => {
95 let len = state.rows.len();
96 state.index.insert(id.clone(), len);
97 state.rows.push(row.clone());
98 }
99 }
100 let patch = RowPatch::Upsert { id, row };
101 state.history.push_back(HistoryEntry {
102 revision: Revision(revision),
103 patch: patch.clone(),
104 });
105 if state.history.len() > self.history_cap {
106 state.history.pop_front();
107 }
108 (Revision(revision), patch)
109 };
110 self.broadcast(patch);
111 revision
112 }
113
114 pub fn remove(&self, id: &RowId) -> Revision {
116 let (revision, patch) = {
117 let mut state = self.state.lock().expect("mem plane poisoned");
118 let Some(i) = state.index.remove(id) else {
119 return self.revision();
120 };
121 state.rows.swap_remove(i);
122 if let Some(moved) = state.rows.get(i) {
123 let moved_id = moved.id.clone();
124 state.index.insert(moved_id, i);
125 }
126 let revision = self.revision.fetch_add(1, Ordering::SeqCst) + 1;
127 let patch = RowPatch::Remove { id: id.clone() };
128 state.history.push_back(HistoryEntry {
129 revision: Revision(revision),
130 patch: patch.clone(),
131 });
132 if state.history.len() > self.history_cap {
133 state.history.pop_front();
134 }
135 (Revision(revision), patch)
136 };
137 self.broadcast(patch);
138 revision
139 }
140
141 fn broadcast(&self, patch: RowPatch) {
142 let mut senders = self.live.lock().expect("mem plane live poisoned");
143 senders.retain(|tx| !tx.is_closed());
146 for tx in senders.iter() {
147 let _ = tx.unbounded_send(patch.clone());
148 }
149 }
150
151 pub fn rows(&self) -> Vec<Row> {
153 self.state.lock().expect("mem plane poisoned").rows.clone()
154 }
155
156 pub fn schema_column_ids(&self) -> Vec<String> {
158 self.schema.column_ids.clone()
159 }
160}
161
162#[async_trait::async_trait]
163impl DataPlane for MemPlane {
164 async fn query(&self, query: &Query) -> Result<Page, Error> {
165 let revision = self.revision();
166 let indices = {
170 let state = self.state.lock().expect("mem plane poisoned");
171 let mut indices: Vec<usize> = (0..state.rows.len()).collect();
172 sort_indices(
173 &mut indices,
174 &state.rows,
175 &self.schema.column_ids,
176 query.sort.as_ref(),
177 );
178 filter_indices(indices, &state.rows, query.filter.as_ref())
179 };
180
181 let total = indices.len();
183 let start = query.start.min(total);
184 let end = query.end.min(total).max(start);
185 let page_rows: Vec<Row> = {
186 let state = self.state.lock().expect("mem plane poisoned");
187 indices[start..end]
188 .iter()
189 .map(|&i| state.rows[i].clone())
190 .collect()
191 };
192 Ok(Page {
193 rows: page_rows,
194 total,
195 revision,
196 })
197 }
198
199 fn subscribe(&self, from: Revision) -> Pin<Box<dyn Stream<Item = RowPatch> + Send>> {
200 let history: Vec<RowPatch> = {
205 let state = self.state.lock().expect("mem plane poisoned");
206 state
207 .history
208 .iter()
209 .filter(|e| e.revision > from)
210 .map(|e| e.patch.clone())
211 .collect()
212 };
213 let (tx, rx) = mpsc::unbounded();
214 self.live.lock().expect("mem plane live poisoned").push(tx);
215 let stream = futures_util::stream::iter(history).chain(rx);
216 Box::pin(stream)
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use crate::render::{Cell, Filter, SortSpec};
224
225 fn text(v: &str) -> Cell {
226 Cell::Text { value: v.into() }
227 }
228 fn row(id: &str, name: &str, n: i64) -> Row {
229 Row {
230 id: RowId(id.into()),
231 cells: vec![text(name), Cell::Number { value: n }],
232 }
233 }
234
235 fn plane() -> MemPlane {
236 MemPlane::new(Schema {
237 column_ids: vec!["name".into(), "count".into()],
238 })
239 }
240
241 #[test]
242 fn query_filters_sorts_and_windows() {
243 let p = plane();
244 p.upsert(row("a", "zebra", 10));
245 p.upsert(row("b", "apple", 2));
246 p.upsert(row("c", "banana", 9));
247
248 let page = tokio_test_block(p.query(&Query {
249 start: 0,
250 end: 100,
251 sort: Some(SortSpec {
252 column: "count".into(),
253 descending: false,
254 }),
255 filter: None,
256 }))
257 .expect("query");
258 let names: Vec<String> = page
259 .rows
260 .iter()
261 .map(|r| crate::table::cell_text(&r.cells[0]))
262 .collect();
263 assert_eq!(names, vec!["apple", "banana", "zebra"]);
264 assert_eq!(page.total, 3);
265
266 let page2 = tokio_test_block(p.query(&Query {
268 start: 1,
269 end: 2,
270 sort: Some(SortSpec {
271 column: "count".into(),
272 descending: false,
273 }),
274 filter: None,
275 }))
276 .expect("query");
277 assert_eq!(page2.rows.len(), 1);
278 assert_eq!(page2.total, 3);
279 assert_eq!(crate::table::cell_text(&page2.rows[0].cells[0]), "banana");
280 }
281
282 #[test]
283 fn filter_by_expression() {
284 let p = plane();
285 p.upsert(row("a", "zebra", 10));
286 p.upsert(row("b", "apple", 2));
287 let page = tokio_test_block(p.query(&Query {
288 start: 0,
289 end: 100,
290 sort: None,
291 filter: Some(Filter {
292 expression: "app".into(),
293 }),
294 }))
295 .expect("query");
296 assert_eq!(page.rows.len(), 1);
297 assert_eq!(page.rows[0].id, RowId("b".into()));
298 }
299
300 #[test]
301 fn subscribe_replays_history_then_live() {
302 let p = plane();
303 p.upsert(row("a", "one", 1));
304 let from = p.revision(); p.upsert(row("b", "two", 2));
306 p.upsert(row("a", "one-v2", 1)); let mut stream = p.subscribe(from);
310 let mut got = Vec::new();
311 let waker = futures_util::task::noop_waker();
313 let mut cx = std::task::Context::from_waker(&waker);
314 let mut pin = Pin::new(&mut stream);
315 while let std::task::Poll::Ready(Some(item)) = pin.as_mut().poll_next(&mut cx) {
316 got.push(item);
317 if got.len() == 2 {
318 break;
319 }
320 }
321 assert_eq!(got.len(), 2);
322 }
323
324 #[test]
325 fn remove_deletes_row() {
326 let p = plane();
327 p.upsert(row("a", "one", 1));
328 p.upsert(row("b", "two", 2));
329 p.remove(&RowId("a".into()));
330 let rows = p.rows();
331 assert_eq!(rows.len(), 1);
332 assert_eq!(rows[0].id, RowId("b".into()));
333 }
334
335 #[test]
336 fn history_replay_includes_removals() {
337 let p = plane();
338 p.upsert(row("a", "one", 1));
339 let from = p.revision(); p.upsert(row("b", "two", 2));
341 p.remove(&RowId("a".into())); let mut stream = p.subscribe(from);
344 let waker = futures_util::task::noop_waker();
345 let mut cx = std::task::Context::from_waker(&waker);
346 let mut pin = Pin::new(&mut stream);
347 let mut got = Vec::new();
348 while let std::task::Poll::Ready(Some(item)) = pin.as_mut().poll_next(&mut cx) {
349 got.push(item);
350 if got.len() == 2 {
351 break;
352 }
353 }
354 assert_eq!(got.len(), 2);
356 assert!(matches!(&got[0], RowPatch::Upsert { id, .. } if id.0 == "b"));
357 assert!(matches!(&got[1], RowPatch::Remove { id } if id.0 == "a"));
358 }
359
360 #[test]
361 fn broadcast_drops_closed_senders() {
362 let p = plane();
363 {
365 let _stream = p.subscribe(Revision(0));
366 }
367 p.upsert(row("a", "one", 1));
368 let live_len = p.live.lock().expect("live").len();
371 assert!(
372 live_len <= 1,
373 "closed sender should be dropped, got {live_len}"
374 );
375 }
376
377 #[test]
383 fn query_50k_rows_is_not_quadratic() {
384 let p = plane();
385 for i in 0..50_000 {
386 p.upsert(row(&format!("row-{i}"), &format!("name-{i}"), i));
387 }
388 let start = std::time::Instant::now();
389 for _ in 0..10 {
390 let page = tokio_test_block(p.query(&Query {
391 start: 0,
392 end: 50,
393 sort: Some(SortSpec {
394 column: "count".into(),
395 descending: true,
396 }),
397 filter: Some(Filter {
398 expression: "name-4".into(),
399 }),
400 }))
401 .expect("query");
402 assert_eq!(
403 page.total, 11_111,
404 "filter should match name-4*, name-40*.."
405 );
406 }
407 let elapsed = start.elapsed();
408 assert!(
411 elapsed.as_secs() < 5,
412 "50k-row query regressed: {elapsed:?} for 10 queries"
413 );
414 }
415
416 fn tokio_test_block<F: std::future::Future>(f: F) -> F::Output {
418 futures_util::pin_mut!(f);
419 let waker = futures_util::task::noop_waker();
420 let mut cx = std::task::Context::from_waker(&waker);
421 loop {
422 match f.as_mut().poll(&mut cx) {
423 std::task::Poll::Ready(v) => return v,
424 std::task::Poll::Pending => std::thread::yield_now(),
425 }
426 }
427 }
428}