@@ -27,7 +27,7 @@ use spacetimedb_data_structures::map::{
2727} ;
2828use spacetimedb_datastore:: locking_tx_datastore:: state_view:: StateView ;
2929use spacetimedb_durability:: TxOffset ;
30- use spacetimedb_expr:: expr:: CollectViews ;
30+ use spacetimedb_expr:: expr:: { BindEnv , CollectViews } ;
3131use spacetimedb_lib:: metrics:: ExecutionMetrics ;
3232use spacetimedb_lib:: { AlgebraicValue , ConnectionId , Identity , ProductValue } ;
3333use spacetimedb_primitives:: { ColId , IndexId , TableId , ViewId } ;
@@ -45,7 +45,7 @@ use tokio::sync::{mpsc, oneshot};
4545/// Identity is insufficient because different ConnectionIds can use the same Identity.
4646/// TODO: Determine if ConnectionId is sufficient for uniquely identifying a client.
4747type ClientId = ( Identity , ConnectionId ) ;
48- type Query = Arc < Plan > ;
48+ type Query = Arc < PlanInstance > ;
4949type Client = Arc < ClientConnectionSender > ;
5050type SwitchedTableUpdate =
5151 ws_v1:: FormatSwitch < ws_v1:: TableUpdate < ws_v1:: BsatnFormat > , ws_v1:: TableUpdate < ws_v1:: JsonFormat > > ;
@@ -60,25 +60,61 @@ type ClientQueryId = ws_v1::QueryId;
6060type SubscriptionId = ( ClientId , ClientQueryId ) ;
6161type SubscriptionIdV2 = ( ClientId , ClientQuerySetId ) ;
6262
63+ /// The unbound, reusable subscription query plan.
64+ ///
65+ /// Runtime values such as `:sender` are represented as formal parameters inside these plans.
66+ /// TODO: Intern and share `PlanTemplate`s across bound [`PlanInstance`]s with the same template hash.
67+ /// Today, [`PlanInstance::new`] still allocates a fresh template for each cached subscription instance.
6368#[ derive( Debug ) ]
64- pub struct Plan {
65- hash : QueryHash ,
69+ pub struct PlanTemplate {
6670 sql : String ,
6771 plans : Vec < SubscriptionPlan > ,
6872}
6973
70- impl CollectViews for Plan {
74+ impl CollectViews for PlanTemplate {
7175 fn collect_views ( & self , views : & mut HashSet < ViewId > ) {
7276 for plan in & self . plans {
7377 plan. collect_views ( views) ;
7478 }
7579 }
7680}
7781
78- impl Plan {
79- /// Create a new subscription plan to be cached
80- pub fn new ( plans : Vec < SubscriptionPlan > , hash : QueryHash , text : String ) -> Self {
81- Self { plans, hash, sql : text }
82+ impl PlanTemplate {
83+ pub fn new ( plans : Vec < SubscriptionPlan > , text : String ) -> Self {
84+ Self { sql : text, plans }
85+ }
86+ }
87+
88+ /// A concrete subscription query instance with runtime parameter bindings.
89+ #[ derive( Debug ) ]
90+ pub struct PlanInstance {
91+ hash : QueryHash ,
92+ template : Arc < PlanTemplate > ,
93+ bind_env : BindEnv ,
94+ }
95+
96+ /// A subscription plan tracked by the subscription manager.
97+ pub type Plan = PlanInstance ;
98+
99+ impl CollectViews for PlanInstance {
100+ fn collect_views ( & self , views : & mut HashSet < ViewId > ) {
101+ self . template . collect_views ( views) ;
102+ }
103+ }
104+
105+ impl PlanInstance {
106+ /// Create a new subscription plan instance to be cached.
107+ pub fn new ( plans : Vec < SubscriptionPlan > , hash : QueryHash , text : String , bind_env : BindEnv ) -> Self {
108+ Self :: from_template ( Arc :: new ( PlanTemplate :: new ( plans, text) ) , hash, bind_env)
109+ }
110+
111+ /// Create a new subscription plan instance from an existing plan template.
112+ pub fn from_template ( template : Arc < PlanTemplate > , hash : QueryHash , bind_env : BindEnv ) -> Self {
113+ Self {
114+ hash,
115+ template,
116+ bind_env,
117+ }
82118 }
83119
84120 /// Returns the query hash for this subscription
@@ -89,18 +125,19 @@ impl Plan {
89125 /// A subscription query return rows from a single table.
90126 /// This method returns the id of that table.
91127 pub fn subscribed_table_id ( & self ) -> TableId {
92- self . plans [ 0 ] . subscribed_table_id ( )
128+ self . template . plans [ 0 ] . subscribed_table_id ( )
93129 }
94130
95131 /// A subscription query return rows from a single table.
96132 /// This method returns the name of that table.
97133 pub fn subscribed_table_name ( & self ) -> & TableName {
98- self . plans [ 0 ] . subscribed_table_name ( )
134+ self . template . plans [ 0 ] . subscribed_table_name ( )
99135 }
100136
101137 /// Returns the index ids from which this subscription reads
102138 pub fn index_ids ( & self ) -> impl Iterator < Item = ( TableId , IndexId ) > + use < > {
103- self . plans
139+ self . template
140+ . plans
104141 . iter ( )
105142 . flat_map ( |plan| plan. index_ids ( ) )
106143 . collect :: < HashSet < _ > > ( )
@@ -109,7 +146,8 @@ impl Plan {
109146
110147 /// Returns the table ids from which this subscription reads
111148 pub fn table_ids ( & self ) -> impl Iterator < Item = TableId > + ' _ {
112- self . plans
149+ self . template
150+ . plans
113151 . iter ( )
114152 . flat_map ( |plan| plan. table_ids ( ) )
115153 . collect :: < HashSet < _ > > ( )
@@ -120,9 +158,10 @@ impl Plan {
120158 fn search_args ( & self ) -> impl Iterator < Item = ( TableId , ColId , AlgebraicValue ) > + use < > {
121159 let mut args = HashSet :: new ( ) ;
122160 for arg in self
161+ . template
123162 . plans
124163 . iter ( )
125- . flat_map ( |subscription| subscription. optimized_physical_plan ( ) . search_args ( ) )
164+ . flat_map ( |subscription| subscription. bound_optimized_physical_plan ( & self . bind_env ) . search_args ( ) )
126165 {
127166 args. insert ( arg) ;
128167 }
@@ -132,22 +171,30 @@ impl Plan {
132171 /// Returns the plan fragments that comprise this subscription.
133172 /// Will only return one element unless there is a table with multiple RLS rules.
134173 pub fn plans_fragments ( & self ) -> impl Iterator < Item = & SubscriptionPlan > + ' _ {
135- self . plans . iter ( )
174+ self . template . plans . iter ( )
136175 }
137176
138177 /// Returns the join edges for this plan, if any.
139178 pub fn join_edges ( & self ) -> impl Iterator < Item = ( JoinEdge , AlgebraicValue ) > + ' _ {
140- self . plans . iter ( ) . filter_map ( |plan| plan. join_edge ( ) )
179+ self . template
180+ . plans
181+ . iter ( )
182+ . filter_map ( |plan| plan. bound_join_edge ( & self . bind_env ) )
141183 }
142184
143185 /// The `SQL` text of this subscription.
144186 pub fn sql ( & self ) -> & str {
145- & self . sql
187+ & self . template . sql
188+ }
189+
190+ /// Runtime parameter bindings for this subscription instance.
191+ pub fn bind_env ( & self ) -> & BindEnv {
192+ & self . bind_env
146193 }
147194
148195 /// Does this plan return rows from an event table?
149196 pub fn returns_event_table ( & self ) -> bool {
150- self . plans . iter ( ) . any ( |p| p. returns_event_table ( ) )
197+ self . template . plans . iter ( ) . any ( |p| p. returns_event_table ( ) )
151198 }
152199}
153200
@@ -1480,15 +1527,15 @@ impl SubscriptionManager {
14801527 } )
14811528 . fold ( FoldState :: default ( ) , |mut acc, ( qstate, plan, _hash) | {
14821529 let table_name = plan. subscribed_table_name ( ) . clone ( ) ;
1483- match eval_delta ( tx, & mut acc. metrics , plan) {
1530+ match eval_delta ( tx, & mut acc. metrics , plan, qstate . query . bind_env ( ) ) {
14841531 Err ( err) => {
14851532 tracing:: error!(
14861533 message = "Query errored during tx update" ,
1487- sql = qstate. query. sql,
1534+ sql = qstate. query. sql( ) ,
14881535 reason = ?err,
14891536 ) ;
14901537 let err = DBError :: WithSql {
1491- sql : qstate. query . sql . as_str ( ) . into ( ) ,
1538+ sql : qstate. query . sql ( ) . into ( ) ,
14921539 error : Box :: new ( err. into ( ) ) ,
14931540 }
14941541 . to_string ( )
@@ -1637,15 +1684,15 @@ impl SubscriptionManager {
16371684
16381685 let clients_for_query = qstate. all_v1_clients ( ) ;
16391686
1640- match eval_delta ( tx, & mut acc. metrics , plan) {
1687+ match eval_delta ( tx, & mut acc. metrics , plan, qstate . query . bind_env ( ) ) {
16411688 Err ( err) => {
16421689 tracing:: error!(
16431690 message = "Query errored during tx update" ,
1644- sql = qstate. query. sql,
1691+ sql = qstate. query. sql( ) ,
16451692 reason = ?err,
16461693 ) ;
16471694 let err = DBError :: WithSql {
1648- sql : qstate. query . sql . as_str ( ) . into ( ) ,
1695+ sql : qstate. query . sql ( ) . into ( ) ,
16491696 error : Box :: new ( err. into ( ) ) ,
16501697 }
16511698 . to_string ( )
@@ -2191,6 +2238,7 @@ mod tests {
21912238 use std:: { sync:: Arc , time:: Duration } ;
21922239
21932240 use spacetimedb_client_api_messages:: websocket:: { v1 as ws_v1, v2 as ws_v2} ;
2241+ use spacetimedb_expr:: expr:: BindEnv ;
21942242 use spacetimedb_lib:: AlgebraicValue ;
21952243 use spacetimedb_lib:: { error:: ResultTest , identity:: AuthCtx , AlgebraicType , ConnectionId , Identity , Timestamp } ;
21962244 use spacetimedb_primitives:: { ColId , TableId } ;
@@ -2231,9 +2279,10 @@ mod tests {
22312279 fn compile_plan_with_auth ( db : & RelationalDB , sql : & str , auth : AuthCtx ) -> ResultTest < Arc < Plan > > {
22322280 with_read_only ( db, |tx| {
22332281 let tx = SchemaViewer :: new ( & * tx, & auth) ;
2234- let ( plans, has_param) = SubscriptionPlan :: compile ( sql, & tx, & auth) . unwrap ( ) ;
2235- let hash = QueryHash :: from_string ( sql, auth. caller ( ) , has_param) ;
2236- Ok ( Arc :: new ( Plan :: new ( plans, hash, sql. into ( ) ) ) )
2282+ let ( plans, requires_sender_binding) = SubscriptionPlan :: compile ( sql, & tx, & auth) . unwrap ( ) ;
2283+ let hash = QueryHash :: from_string ( sql, auth. caller ( ) , requires_sender_binding) ;
2284+ let bind_env = BindEnv :: for_sender_binding ( requires_sender_binding, auth. caller ( ) ) ;
2285+ Ok ( Arc :: new ( Plan :: new ( plans, hash, sql. into ( ) , bind_env) ) )
22372286 } )
22382287 }
22392288
0 commit comments