1
- use super :: query:: { self , find_op_type_col_pos , Supported , OP_TYPE_FIELD_NAME } ;
1
+ use super :: query:: { self , Supported } ;
2
2
use super :: subscription:: { IncrementalJoin , SupportedQuery } ;
3
3
use crate :: db:: relational_db:: { RelationalDB , Tx } ;
4
4
use crate :: error:: DBError ;
@@ -9,11 +9,11 @@ use crate::vm::{build_query, TxMode};
9
9
use spacetimedb_client_api_messages:: client_api:: { TableRowOperation , TableUpdate } ;
10
10
use spacetimedb_lib:: bsatn:: to_writer;
11
11
use spacetimedb_primitives:: TableId ;
12
- use spacetimedb_sats:: relation:: { DbTable , Header } ;
12
+ use spacetimedb_sats:: relation:: DbTable ;
13
13
use spacetimedb_vm:: eval:: IterRows ;
14
14
use spacetimedb_vm:: expr:: { Query , QueryExpr , SourceExpr , SourceSet } ;
15
15
use spacetimedb_vm:: rel_ops:: RelOps ;
16
- use spacetimedb_vm:: relation:: RelValue ;
16
+ use spacetimedb_vm:: relation:: { MemTable , RelValue } ;
17
17
use std:: hash:: Hash ;
18
18
19
19
/// A hash for uniquely identifying query execution units,
@@ -58,7 +58,7 @@ enum EvalIncrPlan {
58
58
Semijoin ( IncrementalJoin ) ,
59
59
60
60
/// For single-table selects, store only one version of the plan,
61
- /// which has a single source, a [`MemTable`] produced by [`query::to_mem_table_with_op_type `].
61
+ /// which has a single source, a [`MemTable`], produced by [`query::query_to_mem_table `].
62
62
Select ( QueryExpr ) ,
63
63
}
64
64
@@ -100,7 +100,6 @@ impl From<SupportedQuery> for ExecutionUnit {
100
100
101
101
impl ExecutionUnit {
102
102
/// Pre-compute a plan for `eval_incr` which reads from a `MemTable`
103
- /// whose rows are augmented with an `__op_type` column,
104
103
/// rather than re-planning on every incremental update.
105
104
fn compile_select_eval_incr ( expr : & QueryExpr ) -> QueryExpr {
106
105
let source = expr
@@ -126,7 +125,7 @@ impl ExecutionUnit {
126
125
// Some day down the line, when we have a real query planner,
127
126
// we may need to provide a row count estimation that is, if not accurate,
128
127
// at least less specifically inaccurate.
129
- let ( eval_incr_plan, _source_set) = query:: to_mem_table ( expr. clone ( ) , & table_update) ;
128
+ let ( eval_incr_plan, _source_set) = query:: query_to_mem_table ( expr. clone ( ) , & table_update) ;
130
129
debug_assert_eq ! ( _source_set. len( ) , 1 ) ;
131
130
132
131
eval_incr_plan
@@ -278,6 +277,21 @@ impl ExecutionUnit {
278
277
} ) )
279
278
}
280
279
280
+ fn eval_query_expr_against_memtable < ' a > (
281
+ ctx : & ' a ExecutionContext ,
282
+ db : & ' a RelationalDB ,
283
+ tx : & ' a TxMode ,
284
+ mem_table : MemTable ,
285
+ eval_incr_plan : & ' a QueryExpr ,
286
+ ) -> Result < Box < IterRows < ' a > > , DBError > {
287
+ // Build a `SourceSet` containing the updates from `table`.
288
+ let mut sources = SourceSet :: default ( ) ;
289
+ sources. add_mem_table ( mem_table) ;
290
+ // Evaluate the saved plan against the new `SourceSet`,
291
+ // returning an iterator over the selected rows.
292
+ build_query ( ctx, db, tx, eval_incr_plan, & mut sources) . map_err ( Into :: into)
293
+ }
294
+
281
295
fn eval_incr_query_expr < ' a > (
282
296
db : & RelationalDB ,
283
297
tx : & Tx ,
@@ -289,51 +303,64 @@ impl ExecutionUnit {
289
303
let tx: TxMode = tx. into ( ) ;
290
304
291
305
let SourceExpr :: MemTable {
292
- source_id : _source_id,
293
306
ref header,
294
307
table_access,
295
308
..
296
309
} = eval_incr_plan. source
297
310
else {
298
311
panic ! ( "Expected MemTable in `eval_incr_plan`, but found `DbTable`" ) ;
299
312
} ;
313
+
314
+ // Partition the `update` into two `MemTable`s, `(inserts, deletes)`,
315
+ // so that we can remember which are which without adding a column to each row.
316
+ // Previously, we used to add such a column `"__op_type: AlgebraicType::U8"`.
317
+ let partition_updates = |update : & DatabaseTableUpdate | -> ( Option < MemTable > , Option < MemTable > ) {
318
+ // Pre-allocate with capacity given by an upper bound,
319
+ // because realloc is worse than over-allocing.
320
+ let mut inserts = Vec :: with_capacity ( update. ops . len ( ) ) ;
321
+ let mut deletes = Vec :: with_capacity ( update. ops . len ( ) ) ;
322
+ for op in update. ops . iter ( ) {
323
+ // 0 = delete, 1 = insert
324
+ if op. op_type == 0 { & mut deletes } else { & mut inserts } . push ( op. row . clone ( ) ) ;
325
+ }
326
+ (
327
+ ( !inserts. is_empty ( ) ) . then ( || MemTable :: new ( header. clone ( ) , table_access, inserts) ) ,
328
+ ( !deletes. is_empty ( ) ) . then ( || MemTable :: new ( header. clone ( ) , table_access, deletes) ) ,
329
+ )
330
+ } ;
331
+
300
332
let mut ops = Vec :: new ( ) ;
301
333
302
334
for table in tables. filter ( |table| table. table_id == return_table) {
303
- // Build a `SourceSet` containing the updates from `table`.
304
- let mem_table = query:: to_mem_table_with_op_type ( header. clone ( ) , table_access, table) ;
305
- let mut sources = SourceSet :: default ( ) ;
306
- let _source_expr = sources. add_mem_table ( mem_table) ;
307
- debug_assert_eq ! ( _source_expr. source_id( ) , Some ( _source_id) ) ;
308
- // Evaluate the saved plan against the new `SourceSet`
309
- // and capture the new row operations.
310
- let query = build_query ( & ctx, db, & tx, eval_incr_plan, & mut sources) ?;
311
- Self :: collect_rows_remove_table_ops ( & mut ops, query, header) ?;
335
+ // Evaluate the query separately against inserts and deletes,
336
+ // so that we can pass each row to the query engine unaltered,
337
+ // without forgetting which are inserts and which are deletes.
338
+ // Then, collect the rows into the single `ops` vec,
339
+ // restoring the appropriate `op_type`.
340
+ let ( inserts, deletes) = partition_updates ( table) ;
341
+ if let Some ( inserts) = inserts {
342
+ let query = Self :: eval_query_expr_against_memtable ( & ctx, db, & tx, inserts, eval_incr_plan) ?;
343
+ // op_type 1: insert
344
+ Self :: collect_rows_with_table_op ( & mut ops, query, 1 ) ?;
345
+ }
346
+ if let Some ( deletes) = deletes {
347
+ let query = Self :: eval_query_expr_against_memtable ( & ctx, db, & tx, deletes, eval_incr_plan) ?;
348
+ // op_type 0: delete
349
+ Self :: collect_rows_with_table_op ( & mut ops, query, 0 ) ?;
350
+ }
312
351
}
313
352
Ok ( ops)
314
353
}
315
354
316
- /// Convert a set of rows annotated with the `__op_type` fields into a set of [`TableOp`]s ,
317
- /// and collect them into a vec `into `.
318
- fn collect_rows_remove_table_ops (
355
+ /// Collect the results of `query` into a vec `into` ,
356
+ /// annotating each as a `TableOp` with the `op_type `.
357
+ fn collect_rows_with_table_op (
319
358
into : & mut Vec < TableOp > ,
320
359
mut query : Box < IterRows < ' _ > > ,
321
- header : & Header ,
360
+ op_type : u8 ,
322
361
) -> Result < ( ) , DBError > {
323
- let pos_op_type = find_op_type_col_pos ( header) . unwrap_or_else ( || {
324
- panic ! (
325
- "Failed to locate `{OP_TYPE_FIELD_NAME}` in `{}`, fields: {:?}" ,
326
- header. table_name,
327
- header. fields. iter( ) . map( |x| & x. field) . collect:: <Vec <_>>( )
328
- )
329
- } ) ;
330
- let pos_op_type = pos_op_type. idx ( ) ;
331
362
while let Some ( row_ref) = query. next ( ) ? {
332
- let mut row = row_ref. into_product_value ( ) ;
333
- let op_type =
334
- row. elements . remove ( pos_op_type) . into_u8 ( ) . unwrap_or_else ( |_| {
335
- panic ! ( "Failed to extract `{OP_TYPE_FIELD_NAME}` from `{}`" , header. table_name)
336
- } ) ;
363
+ let row = row_ref. into_product_value ( ) ;
337
364
into. push ( TableOp :: new ( op_type, row) ) ;
338
365
}
339
366
Ok ( ( ) )
0 commit comments