@@ -103,13 +103,13 @@ type FullSubscriptionUpdate = FormatSwitch<ws::DatabaseUpdate<BsatnFormat>, ws::
103103
104104/// A utility for sending an error message to a client and returning early
105105macro_rules! return_on_err {
106- ( $expr: expr, $handler: expr) => {
106+ ( $expr: expr, $handler: expr, $metrics : expr ) => {
107107 match $expr {
108108 Ok ( val) => val,
109109 Err ( e) => {
110110 // TODO: Handle errors sending messages.
111111 let _ = $handler( e. to_string( ) . into( ) ) ;
112- return Ok ( ( ) ) ;
112+ return Ok ( $metrics ) ;
113113 }
114114 }
115115 } ;
@@ -398,7 +398,7 @@ impl ModuleSubscriptions {
398398 sender : Arc < ClientConnectionSender > ,
399399 request : UnsubscribeMulti ,
400400 timer : Instant ,
401- ) -> Result < ( ) , DBError > {
401+ ) -> Result < Option < ExecutionMetrics > , DBError > {
402402 // Send an error message to the client
403403 let send_err_msg = |message| {
404404 sender. send_message ( SubscriptionMessage {
@@ -420,38 +420,23 @@ impl ModuleSubscriptions {
420420 let removed_queries = {
421421 let mut subscriptions = self . subscriptions . write ( ) ;
422422
423- match subscriptions. remove_subscription ( ( sender. id . identity , sender. id . connection_id ) , request. query_id ) {
424- Ok ( queries) => queries,
425- Err ( error) => {
426- // Apparently we ignore errors sending messages.
427- let _ = send_err_msg ( error. to_string ( ) . into ( ) ) ;
428- return Ok ( ( ) ) ;
429- }
430- }
431- } ;
432-
433- let auth = AuthCtx :: new ( self . owner_identity , sender. id . identity ) ;
434- let eval_result = self . evaluate_queries (
435- sender. clone ( ) ,
436- & removed_queries,
437- & tx,
438- & auth,
439- TableUpdateType :: Unsubscribe ,
440- ) ;
441- // If execution error, send to client
442- let ( update, metrics) = match eval_result {
443- Ok ( ok) => ok,
444- Err ( e) => {
445- // Apparently we ignore errors sending messages.
446- let _ = send_err_msg ( e. to_string ( ) . into ( ) ) ;
447- return Ok ( ( ) ) ;
448- }
423+ return_on_err ! (
424+ subscriptions. remove_subscription( ( sender. id. identity, sender. id. connection_id) , request. query_id) ,
425+ send_err_msg,
426+ None
427+ )
449428 } ;
450429
451- record_exec_metrics (
452- & WorkloadType :: Unsubscribe ,
453- & self . relational_db . database_identity ( ) ,
454- metrics,
430+ let ( update, metrics) = return_on_err ! (
431+ self . evaluate_queries(
432+ sender. clone( ) ,
433+ & removed_queries,
434+ & tx,
435+ & AuthCtx :: new( self . owner_identity, sender. id. identity) ,
436+ TableUpdateType :: Unsubscribe ,
437+ ) ,
438+ send_err_msg,
439+ None
455440 ) ;
456441
457442 let _ = sender. send_message ( SubscriptionMessage {
@@ -460,7 +445,8 @@ impl ModuleSubscriptions {
460445 timer : Some ( timer) ,
461446 result : SubscriptionResult :: UnsubscribeMulti ( SubscriptionData { data : update } ) ,
462447 } ) ;
463- Ok ( ( ) )
448+
449+ Ok ( Some ( metrics) )
464450 }
465451
466452 /// Compiles the queries in a [Subscribe] or [SubscribeMulti] message.
@@ -538,7 +524,7 @@ impl ModuleSubscriptions {
538524 request : SubscribeMulti ,
539525 timer : Instant ,
540526 _assert : Option < AssertTxFn > ,
541- ) -> Result < ( ) , DBError > {
527+ ) -> Result < Option < ExecutionMetrics > , DBError > {
542528 // Send an error message to the client
543529 let send_err_msg = |message| {
544530 let _ = sender. send_message ( SubscriptionMessage {
@@ -555,7 +541,8 @@ impl ModuleSubscriptions {
555541 let num_queries = request. query_strings . len ( ) ;
556542 let ( queries, auth, tx) = return_on_err ! (
557543 self . compile_queries( sender. id. identity, request. query_strings, num_queries) ,
558- send_err_msg
544+ send_err_msg,
545+ None
559546 ) ;
560547 let tx = scopeguard:: guard ( tx, |tx| {
561548 self . relational_db . release_tx ( tx) ;
@@ -578,15 +565,9 @@ impl ModuleSubscriptions {
578565 let mut subscriptions = self . subscriptions . write ( ) ;
579566 subscriptions. remove_subscription ( ( sender. id . identity , sender. id . connection_id ) , request. query_id ) ?;
580567 send_err_msg ( "Internal error evaluating queries" . into ( ) ) ;
581- return Ok ( ( ) ) ;
568+ return Ok ( None ) ;
582569 } ;
583570
584- record_exec_metrics (
585- & WorkloadType :: Subscribe ,
586- & self . relational_db . database_identity ( ) ,
587- metrics,
588- ) ;
589-
590571 #[ cfg( test) ]
591572 if let Some ( assert) = _assert {
592573 assert ( & tx) ;
@@ -602,7 +583,8 @@ impl ModuleSubscriptions {
602583 timer : Some ( timer) ,
603584 result : SubscriptionResult :: SubscribeMulti ( SubscriptionData { data : update } ) ,
604585 } ) ;
605- Ok ( ( ) )
586+
587+ Ok ( Some ( metrics) )
606588 }
607589
608590 /// Add a subscriber to the module. NOTE: this function is blocking.
@@ -614,7 +596,7 @@ impl ModuleSubscriptions {
614596 subscription : Subscribe ,
615597 timer : Instant ,
616598 _assert : Option < AssertTxFn > ,
617- ) -> Result < ( ) , DBError > {
599+ ) -> Result < ExecutionMetrics , DBError > {
618600 let num_queries = subscription. query_strings . len ( ) ;
619601 let ( queries, auth, tx) = self . compile_queries ( sender. id . identity , subscription. query_strings , num_queries) ?;
620602 let tx = scopeguard:: guard ( tx, |tx| {
@@ -641,12 +623,6 @@ impl ModuleSubscriptions {
641623 . map ( |( table_update, metrics) | ( FormatSwitch :: Json ( table_update) , metrics) ) ?,
642624 } ;
643625
644- record_exec_metrics (
645- & WorkloadType :: Subscribe ,
646- & self . relational_db . database_identity ( ) ,
647- metrics,
648- ) ;
649-
650626 // It acquires the subscription lock after `eval`, allowing `add_subscription` to run concurrently.
651627 // This also makes it possible for `broadcast_event` to get scheduled before the subsequent part here
652628 // but that should not pose an issue.
@@ -667,7 +643,8 @@ impl ModuleSubscriptions {
667643 request_id : Some ( subscription. request_id ) ,
668644 timer : Some ( timer) ,
669645 } ) ;
670- Ok ( ( ) )
646+
647+ Ok ( metrics)
671648 }
672649
673650 pub fn remove_subscriber ( & self , client_id : ClientActorId ) {
@@ -764,6 +741,7 @@ mod tests {
764741 use hashbrown:: HashMap ;
765742 use itertools:: Itertools ;
766743 use parking_lot:: RwLock ;
744+ use pretty_assertions:: assert_matches;
767745 use spacetimedb_client_api_messages:: energy:: EnergyQuanta ;
768746 use spacetimedb_client_api_messages:: websocket:: {
769747 CompressableQueryUpdate , Compression , FormatSwitch , QueryId , Subscribe , SubscribeMulti , SubscribeSingle ,
@@ -794,7 +772,8 @@ mod tests {
794772 query_strings : [ sql. into ( ) ] . into ( ) ,
795773 request_id : 0 ,
796774 } ;
797- module_subscriptions. add_legacy_subscriber ( sender, subscribe, Instant :: now ( ) , assert)
775+ module_subscriptions. add_legacy_subscriber ( sender, subscribe, Instant :: now ( ) , assert) ?;
776+ Ok ( ( ) )
798777 }
799778
800779 /// An in-memory `RelationalDB` for testing
@@ -945,10 +924,12 @@ mod tests {
945924 queries : & [ & ' static str ] ,
946925 sender : Arc < ClientConnectionSender > ,
947926 counter : & mut u32 ,
948- ) -> anyhow:: Result < ( ) > {
927+ ) -> anyhow:: Result < ExecutionMetrics > {
949928 * counter += 1 ;
950- subs. add_multi_subscription ( sender, multi_subscribe ( queries, * counter) , Instant :: now ( ) , None ) ?;
951- Ok ( ( ) )
929+ let metrics = subs
930+ . add_multi_subscription ( sender, multi_subscribe ( queries, * counter) , Instant :: now ( ) , None )
931+ . map ( |metrics| metrics. unwrap_or_default ( ) ) ?;
932+ Ok ( metrics)
952933 }
953934
954935 /// Unsubscribe from a single query
@@ -1237,6 +1218,8 @@ mod tests {
12371218 } )
12381219 } ) ?;
12391220
1221+ commit_tx ( & db, & subs, [ ] , [ ( table_id, product ! [ 0_u8 ] ) ] ) ?;
1222+
12401223 let mut query_id = 0 ;
12411224
12421225 // Subscribe to `t`
@@ -1858,6 +1841,50 @@ mod tests {
18581841 Ok ( ( ) )
18591842 }
18601843
1844+ /// Test that we do not evaluate queries that return trivially empty results
1845+ #[ tokio:: test]
1846+ async fn test_query_pruning_for_empty_tables ( ) -> anyhow:: Result < ( ) > {
1847+ // Establish a client connection
1848+ let ( tx, mut rx) = client_connection ( client_id_from_u8 ( 1 ) ) ;
1849+
1850+ let db = relational_db ( ) ?;
1851+ let subs = module_subscriptions ( db. clone ( ) ) ;
1852+
1853+ let schema = & [ ( "id" , AlgebraicType :: U64 ) , ( "a" , AlgebraicType :: U64 ) ] ;
1854+ let indices = & [ 0 . into ( ) ] ;
1855+ // Create tables `t` and `s` with `(i: u64, a: u64)`.
1856+ db. create_table_for_test ( "t" , schema, indices) ?;
1857+ let s_id = db. create_table_for_test ( "s" , schema, indices) ?;
1858+
1859+ // Insert one row into `s`, but leave `t` empty.
1860+ commit_tx ( & db, & subs, [ ] , [ ( s_id, product ! [ 0u64 , 0u64 ] ) ] ) ?;
1861+
1862+ // Subscribe to queries that return empty results
1863+ let metrics = subscribe_multi (
1864+ & subs,
1865+ & [
1866+ "select t.* from t where a = 0" ,
1867+ "select t.* from t join s on t.id = s.id where s.a = 0" ,
1868+ "select s.* from t join s on t.id = s.id where t.a = 0" ,
1869+ ] ,
1870+ tx,
1871+ & mut 0 ,
1872+ ) ?;
1873+
1874+ assert_matches ! (
1875+ rx. recv( ) . await ,
1876+ Some ( SerializableMessage :: Subscription ( SubscriptionMessage {
1877+ result: SubscriptionResult :: SubscribeMulti ( _) ,
1878+ ..
1879+ } ) )
1880+ ) ;
1881+
1882+ assert_eq ! ( metrics. rows_scanned, 0 ) ;
1883+ assert_eq ! ( metrics. index_seeks, 0 ) ;
1884+
1885+ Ok ( ( ) )
1886+ }
1887+
18611888 /// Asserts that a subscription holds a tx handle for the entire length of its evaluation.
18621889 #[ test]
18631890 fn test_tx_subscription_ordering ( ) -> ResultTest < ( ) > {
0 commit comments