Skip to content

Commit b8631b8

Browse files
Parameterized query plans
1 parent 29b21cc commit b8631b8

20 files changed

Lines changed: 706 additions & 180 deletions

File tree

crates/core/src/host/module_host.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ use spacetimedb_datastore::traits::{IsolationLevel, Program, TxData};
5353
pub use spacetimedb_durability::{DurabilityExited, DurableOffset};
5454
use spacetimedb_execution::pipelined::{PipelinedProject, ViewProject};
5555
use spacetimedb_execution::RelValue;
56-
use spacetimedb_expr::expr::CollectViews;
56+
use spacetimedb_expr::expr::{BindEnv, CollectViews};
5757
use spacetimedb_lib::db::raw_def::v9::Lifecycle;
5858
use spacetimedb_lib::http::{Request as HttpRequest, Response as HttpResponse};
5959
use spacetimedb_lib::identity::{AuthCtx, RequestId};
@@ -3272,13 +3272,14 @@ impl ModuleHost {
32723272
plans,
32733273
_,
32743274
table_name,
3275-
_,
3275+
requires_sender_binding,
32763276
) = compile_subscription(query, &schema_tx, auth)?;
3277+
let bind_env = BindEnv::for_sender_binding(requires_sender_binding, auth.caller());
32773278

32783279
// Optimize each fragment.
32793280
let optimized = plans
32803281
.into_iter()
3281-
.map(|plan| plan.optimize(auth))
3282+
.map(|plan| plan.optimize(auth).map(|plan| plan.bind_params(&bind_env)))
32823283
.collect::<Result<Vec<_>, _>>()?;
32833284

32843285
check_row_limit(

crates/core/src/subscription/delta.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::host::module_host::UpdatesRelValue;
22
use anyhow::Result;
33
use spacetimedb_data_structures::map::{HashCollectionExt as _, HashMap};
44
use spacetimedb_execution::{Datastore, DeltaStore, RelValue, Row};
5+
use spacetimedb_expr::expr::BindEnv;
56
use spacetimedb_lib::metrics::ExecutionMetrics;
67
use spacetimedb_primitives::ColList;
78
use spacetimedb_sats::product_value::InvalidFieldError;
@@ -20,6 +21,7 @@ pub fn eval_delta<'a, Tx: Datastore + DeltaStore>(
2021
tx: &'a Tx,
2122
metrics: &mut ExecutionMetrics,
2223
plan: &SubscriptionPlan,
24+
bind_env: &BindEnv,
2325
) -> Result<Option<UpdatesRelValue<'a>>> {
2426
metrics.delta_queries_evaluated += 1;
2527

@@ -42,12 +44,12 @@ pub fn eval_delta<'a, Tx: Datastore + DeltaStore>(
4244
if !plan.is_join() {
4345
// Single table plans will never return redundant rows,
4446
// so there's no need to track row counts.
45-
plan.for_each_insert(tx, metrics, &mut |row| {
47+
plan.for_each_insert(bind_env, tx, metrics, &mut |row| {
4648
inserts.push(maybe_project(row)?);
4749
Ok(())
4850
})?;
4951

50-
plan.for_each_delete(tx, metrics, &mut |row| {
52+
plan.for_each_delete(bind_env, tx, metrics, &mut |row| {
5153
deletes.push(maybe_project(row)?);
5254
Ok(())
5355
})?;
@@ -57,7 +59,7 @@ pub fn eval_delta<'a, Tx: Datastore + DeltaStore>(
5759
let mut insert_counts = HashMap::new();
5860
let mut delete_counts = HashMap::new();
5961

60-
plan.for_each_insert(tx, metrics, &mut |row| {
62+
plan.for_each_insert(bind_env, tx, metrics, &mut |row| {
6163
let row = maybe_project(row)?;
6264
let n = insert_counts.entry(row).or_default();
6365
if *n > 0 {
@@ -67,7 +69,7 @@ pub fn eval_delta<'a, Tx: Datastore + DeltaStore>(
6769
Ok(())
6870
})?;
6971

70-
plan.for_each_delete(tx, metrics, &mut |row| {
72+
plan.for_each_delete(bind_env, tx, metrics, &mut |row| {
7173
let row = maybe_project(row)?;
7274
match insert_counts.get_mut(&row) {
7375
// We have not seen an insert for this row.

crates/core/src/subscription/metrics.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ fn extract_columns(
6464
extract_columns(expr, schema, columns);
6565
}
6666
}
67-
PhysicalExpr::Value(_) => {}
67+
PhysicalExpr::Value(_) | PhysicalExpr::Param(..) => {}
6868
}
6969
}
7070

crates/core/src/subscription/mod.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -256,14 +256,27 @@ pub fn execute_plans<F: BuildableWebsocketFormat>(
256256
) -> Result<(ws_v1::DatabaseUpdate<F>, ExecutionMetrics, Vec<QueryMetrics>), DBError> {
257257
plans
258258
.par_iter()
259-
.flat_map_iter(|plan| plan.plans_fragments().map(|fragment| (plan.sql(), fragment)))
260-
.filter(|(_, plan)| {
259+
.flat_map_iter(|plan| {
260+
plan.plans_fragments()
261+
.map(|fragment| (plan.sql(), plan.bind_env(), fragment))
262+
})
263+
.filter(|(_, _, plan)| {
261264
// Since subscriptions only support selects and inner joins,
262265
// we filter out any plans that read from an empty table.
263266
plan.table_ids().all(|table_id| tx.row_count(table_id) > 0)
264267
})
265-
.map(|(sql, plan)| (sql, plan, plan.subscribed_table_id(), plan.subscribed_table_name()))
266-
.map(|(sql, plan, table_id, table_name)| (sql, plan.optimized_physical_plan().clone(), table_id, table_name))
268+
.map(|(sql, bind_env, plan)| {
269+
(
270+
sql,
271+
bind_env,
272+
plan,
273+
plan.subscribed_table_id(),
274+
plan.subscribed_table_name(),
275+
)
276+
})
277+
.map(|(sql, bind_env, plan, table_id, table_name)| {
278+
(sql, plan.bound_optimized_physical_plan(bind_env), table_id, table_name)
279+
})
267280
.map(|(sql, plan, table_id, table_name)| (sql, plan.optimize(auth), table_id, table_name))
268281
.map(|(sql, plan, table_id, table_name)| {
269282
plan.and_then(|plan| {

crates/core/src/subscription/module_subscription_actor.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,9 @@ impl ModuleSubscriptions {
427427
tx,
428428
|plan, tx| {
429429
plan.plans_fragments()
430-
.map(|plan_fragment| estimate_rows_scanned(tx, plan_fragment.optimized_physical_plan()))
430+
.map(|plan_fragment| {
431+
estimate_rows_scanned(tx, &plan_fragment.bound_optimized_physical_plan(plan.bind_env()))
432+
})
431433
.fold(0, |acc, rows_scanned| acc.saturating_add(rows_scanned))
432434
},
433435
auth,
@@ -438,8 +440,7 @@ impl ModuleSubscriptions {
438440

439441
let plans = query
440442
.plans_fragments()
441-
.map(|fragment| fragment.optimized_physical_plan())
442-
.cloned()
443+
.map(|fragment| fragment.bound_optimized_physical_plan(query.bind_env()))
443444
.map(|plan| plan.optimize(auth))
444445
.collect::<Result<Vec<_>, _>>()?;
445446

@@ -533,7 +534,9 @@ impl ModuleSubscriptions {
533534
tx,
534535
|plan, tx| {
535536
plan.plans_fragments()
536-
.map(|plan_fragment| estimate_rows_scanned(tx, plan_fragment.optimized_physical_plan()))
537+
.map(|plan_fragment| {
538+
estimate_rows_scanned(tx, &plan_fragment.bound_optimized_physical_plan(plan.bind_env()))
539+
})
537540
.fold(0, |acc, rows_scanned| acc.saturating_add(rows_scanned))
538541
},
539542
auth,
@@ -1538,7 +1541,9 @@ impl ModuleSubscriptions {
15381541
&tx,
15391542
|plan, tx| {
15401543
plan.plans_fragments()
1541-
.map(|plan_fragment| estimate_rows_scanned(tx, plan_fragment.optimized_physical_plan()))
1544+
.map(|plan_fragment| {
1545+
estimate_rows_scanned(tx, &plan_fragment.bound_optimized_physical_plan(plan.bind_env()))
1546+
})
15421547
.fold(0, |acc, rows_scanned| acc.saturating_add(rows_scanned))
15431548
},
15441549
&auth,

crates/core/src/subscription/module_subscription_manager.rs

Lines changed: 76 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use spacetimedb_data_structures::map::{
2727
};
2828
use spacetimedb_datastore::locking_tx_datastore::state_view::StateView;
2929
use spacetimedb_durability::TxOffset;
30-
use spacetimedb_expr::expr::CollectViews;
30+
use spacetimedb_expr::expr::{BindEnv, CollectViews};
3131
use spacetimedb_lib::metrics::ExecutionMetrics;
3232
use spacetimedb_lib::{AlgebraicValue, ConnectionId, Identity, ProductValue};
3333
use 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.
4747
type ClientId = (Identity, ConnectionId);
48-
type Query = Arc<Plan>;
48+
type Query = Arc<PlanInstance>;
4949
type Client = Arc<ClientConnectionSender>;
5050
type 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;
6060
type SubscriptionId = (ClientId, ClientQueryId);
6161
type 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

Comments
 (0)