Skip to content

Commit 02954c6

Browse files
committed
flatten mounted module IDs into host ID spaces
1 parent c8cac1d commit 02954c6

6 files changed

Lines changed: 213 additions & 52 deletions

File tree

crates/core/src/client/message_handlers_v1.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ pub async fn handle(client: &ClientConnection, message: DataMessage, timer: Inst
4949
res.map_err(|e| {
5050
(
5151
Some(reducer),
52-
mod_info.module_def.reducer_full(&**reducer).map(|(id, _)| id),
52+
mod_info.module_def.reducer_by_name(&**reducer).map(|(id, _)| id),
5353
e.into(),
5454
)
5555
})

crates/core/src/db/relational_db.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,16 @@ impl RelationalDB {
11481148
Ok(tx.create_view(module_def, view_def)?)
11491149
}
11501150

1151+
pub fn create_view_with_prefix(
1152+
&self,
1153+
tx: &mut MutTx,
1154+
owning_def: &ModuleDef,
1155+
view_def: &ViewDef,
1156+
name_prefix: &str,
1157+
) -> Result<(ViewId, TableId), DBError> {
1158+
Ok(tx.create_view_with_prefix(owning_def, view_def, name_prefix)?)
1159+
}
1160+
11511161
pub fn drop_view(&self, tx: &mut MutTx, view_id: ViewId) -> Result<(), DBError> {
11521162
Ok(tx.drop_view(view_id)?)
11531163
}

crates/core/src/host/module_host.rs

Lines changed: 125 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ use spacetimedb_lib::{bsatn, ConnectionId, TimeDuration, Timestamp};
6363
use spacetimedb_primitives::{ArgId, HttpHandlerId, ProcedureId, TableId, ViewFnPtr, ViewId};
6464
use spacetimedb_query::compile_subscription;
6565
use spacetimedb_sats::raw_identifier::RawIdentifier;
66-
use spacetimedb_sats::{AlgebraicType, AlgebraicTypeRef, ProductValue};
66+
use spacetimedb_sats::{AlgebraicType, AlgebraicTypeRef, ProductValue, Typespace};
6767
use spacetimedb_schema::auto_migrate::{AutoMigrateError, MigrationPolicy};
6868
use spacetimedb_schema::def::{ModuleDef, ProcedureDef, ReducerDef, TableDef, ViewDef};
6969
use spacetimedb_schema::identifier::Identifier;
@@ -560,9 +560,50 @@ pub fn create_table_from_def(
560560
module_def: &ModuleDef,
561561
table_def: &TableDef,
562562
) -> anyhow::Result<()> {
563-
let schema = TableSchema::from_module_def(module_def, table_def, (), TableId::SENTINEL);
563+
create_table_from_def_with_prefix(stdb, tx, module_def, table_def, "")
564+
}
565+
566+
/// Creates a mounted submodule table in `stdb`, applying the namespace to its canonical name.
567+
/// `name_prefix` is the dot-terminated namespace string (e.g. `"alias."`).
568+
pub fn create_table_from_def_with_prefix(
569+
stdb: &RelationalDB,
570+
tx: &mut MutTxId,
571+
owning_def: &ModuleDef,
572+
table_def: &TableDef,
573+
name_prefix: &str,
574+
) -> anyhow::Result<()> {
575+
let mut schema = TableSchema::from_module_def(owning_def, table_def, (), TableId::SENTINEL);
576+
if !name_prefix.is_empty() {
577+
// Use accessor_name so the canonical DB name matches what TypeScript looks up
578+
// as `namespace + table.sourceName`. '.' is not a valid XID char so namespaced
579+
// table names can never collide with user-defined tables.
580+
let prefixed_name = format!("{}{}", name_prefix, &*table_def.accessor_name);
581+
schema.table_name = TableName::new_raw(RawIdentifier::from(prefixed_name));
582+
583+
// No alias needed: the namespaced canonical name is already the unique lookup key.
584+
schema.alias = None;
585+
586+
// Apply the namespace to the scheduled reducer/procedure name so the scheduler can
587+
// resolve it via reducer_by_name / procedure_by_name, both of which use '/' as the
588+
// namespace separator (e.g. "lib." prefix → "lib/reducerName").
589+
// name_prefix is always of the form "ns." or "ns1.ns2." with no internal dots in
590+
// individual segments, so replacing '.' with '/' is unambiguous.
591+
if let Some(schedule) = &mut schema.schedule {
592+
let fn_prefix = name_prefix.replace('.', "/");
593+
let prefixed_fn = format!("{}{}", fn_prefix, &*schedule.function_name);
594+
schedule.function_name = Identifier::new_assume_valid(RawIdentifier::from(prefixed_fn));
595+
}
596+
597+
// Apply the namespace to index canonical names and aliases for global uniqueness.
598+
for index in &mut schema.indexes {
599+
index.index_name = RawIdentifier::from(format!("{}{}", name_prefix, index.index_name));
600+
if let Some(alias) = &index.alias {
601+
index.alias = Some(RawIdentifier::from(format!("{}{}", name_prefix, alias)));
602+
}
603+
}
604+
}
564605
stdb.create_table(tx, schema)
565-
.with_context(|| format!("failed to create table {}", &table_def.name))?;
606+
.with_context(|| format!("failed to create table {}{}", name_prefix, &*table_def.accessor_name))?;
566607
Ok(())
567608
}
568609

@@ -578,6 +619,20 @@ pub fn create_table_from_view_def(
578619
Ok(())
579620
}
580621

622+
/// Creates the table for a mounted `view_def` in `stdb`, applying the namespace prefix.
623+
/// `name_prefix` is the dot-terminated namespace string (e.g. `"lib."`).
624+
pub fn create_table_from_view_def_with_prefix(
625+
stdb: &RelationalDB,
626+
tx: &mut MutTxId,
627+
owning_def: &ModuleDef,
628+
view_def: &ViewDef,
629+
name_prefix: &str,
630+
) -> anyhow::Result<()> {
631+
stdb.create_view_with_prefix(tx, owning_def, view_def, name_prefix)
632+
.with_context(|| format!("failed to create table for view {}{}", name_prefix, &view_def.name))?;
633+
Ok(())
634+
}
635+
581636
/// Moves out the `trapped: bool` from `res`.
582637
fn extract_trapped<T, E>(res: Result<(T, bool), E>) -> (Result<T, E>, bool) {
583638
match res {
@@ -612,21 +667,35 @@ fn init_database_inner(
612667
let auth_ctx = AuthCtx::for_current(owner_identity);
613668
let (tx, ()) = stdb
614669
.with_auto_rollback(tx, |tx| {
615-
// Create all in-memory tables defined by the module,
616-
// with IDs ordered lexicographically by the table names.
617-
let mut table_defs: Vec<_> = module_def.tables().collect();
618-
table_defs.sort_by_key(|x| &x.name);
619-
for def in table_defs {
620-
logger.info(&format!("Creating table `{}`", &def.name));
621-
create_table_from_def(stdb, tx, module_def, def)?;
670+
// Create all in-memory tables defined by the module (including mounted submodules),
671+
// with IDs ordered lexicographically by their full namespaced names.
672+
let mut table_defs = module_def.all_tables_with_prefix();
673+
table_defs.sort_by(|(p1, _, d1), (p2, _, d2)| {
674+
let n1 = format!("{}{}", p1, d1.name);
675+
let n2 = format!("{}{}", p2, d2.name);
676+
n1.cmp(&n2)
677+
});
678+
for (prefix, owning_def, def) in table_defs {
679+
let display_name = format!("{}{}", prefix, def.name);
680+
logger.info(&format!("Creating table `{}`", display_name));
681+
create_table_from_def_with_prefix(stdb, tx, owning_def, def, &prefix)?;
622682
}
623683

624-
// Create all in-memory views defined by the module.
625-
let mut view_defs: Vec<_> = module_def.views().collect();
626-
view_defs.sort_by_key(|x| &x.name);
627-
for def in view_defs {
628-
logger.info(&format!("Creating table for view `{}`", &def.name));
629-
create_table_from_view_def(stdb, tx, module_def, def)?;
684+
// Create all in-memory views defined by the module (root + mounted).
685+
let mut view_defs: Vec<(String, &ModuleDef, &ViewDef)> = module_def.all_views_with_prefix();
686+
view_defs.sort_by(|(p1, _, d1), (p2, _, d2)| {
687+
let n1 = format!("{}{}", p1, d1.name);
688+
let n2 = format!("{}{}", p2, d2.name);
689+
n1.cmp(&n2)
690+
});
691+
for (prefix, owning_def, def) in view_defs {
692+
let display_name = format!("{}{}", prefix, def.name);
693+
logger.info(&format!("Creating table for view `{}`", display_name));
694+
if prefix.is_empty() {
695+
create_table_from_view_def(stdb, tx, owning_def, def)?;
696+
} else {
697+
create_table_from_view_def_with_prefix(stdb, tx, owning_def, def, &prefix)?;
698+
}
630699
}
631700

632701
// Insert the late-bound row-level security expressions.
@@ -706,7 +775,7 @@ pub fn call_identity_connected(
706775
// abort the connection: we can't really recover.
707776
let tx = Some(ScopeGuard::into_inner(mut_tx));
708777
let params = ModuleHost::call_reducer_params(
709-
module,
778+
&module.module_def,
710779
caller_auth.claims.identity,
711780
Some(caller_connection_id),
712781
None,
@@ -1087,6 +1156,10 @@ pub struct CallViewParams {
10871156
pub args: ArgsTuple,
10881157
pub row_type: AlgebraicTypeRef,
10891158
pub timestamp: Timestamp,
1159+
/// The typespace of the module that owns this view.
1160+
/// For root views this equals the top-level typespace;
1161+
/// for mounted views this is the mount's own typespace.
1162+
pub view_typespace: Typespace,
10901163
}
10911164

10921165
pub struct CallProcedureParams {
@@ -2083,7 +2156,7 @@ impl ModuleHost {
20832156
// that `st_client` is updated appropriately.
20842157
let tx = Some(mut_tx);
20852158
let result = Self::call_reducer_params(
2086-
info,
2159+
&info.module_def,
20872160
caller_identity,
20882161
Some(caller_connection_id),
20892162
None,
@@ -2170,7 +2243,7 @@ impl ModuleHost {
21702243
}
21712244

21722245
fn call_reducer_params(
2173-
module: &ModuleInfo,
2246+
owning_def: &ModuleDef,
21742247
caller_identity: Identity,
21752248
caller_connection_id: Option<ConnectionId>,
21762249
client: Option<Arc<ClientConnectionSender>>,
@@ -2181,7 +2254,7 @@ impl ModuleHost {
21812254
args: FunctionArgs,
21822255
) -> Result<CallReducerParams, InvalidReducerArguments> {
21832256
let args = args
2184-
.into_tuple_for_def(&module.module_def, reducer_def)
2257+
.into_tuple_for_def(owning_def, reducer_def)
21852258
.map_err(InvalidReducerArguments)?;
21862259
let caller_connection_id = caller_connection_id.unwrap_or(ConnectionId::ZERO);
21872260
Ok(CallReducerParams {
@@ -2206,10 +2279,10 @@ impl ModuleHost {
22062279
reducer_name: &str,
22072280
args: FunctionArgs,
22082281
) -> Result<(&'a ReducerDef, CallReducerParams), ReducerCallError> {
2209-
let (reducer_id, reducer_def) = self
2282+
let (reducer_id, reducer_def, owning_def) = self
22102283
.info
22112284
.module_def
2212-
.reducer_full(reducer_name)
2285+
.reducer_by_name_with_module(reducer_name)
22132286
.ok_or(ReducerCallError::NoSuchReducer)?;
22142287
if let Some(lifecycle) = reducer_def.lifecycle {
22152288
return Err(ReducerCallError::LifecycleReducer(lifecycle));
@@ -2222,7 +2295,7 @@ impl ModuleHost {
22222295
Ok((
22232296
reducer_def,
22242297
Self::call_reducer_params(
2225-
&self.info,
2298+
owning_def,
22262299
caller_identity,
22272300
caller_connection_id,
22282301
client,
@@ -2248,7 +2321,17 @@ impl ModuleHost {
22482321

22492322
fn log_reducer_submit_error(&self, reducer_name: &str, err: &ReducerCallError) {
22502323
let log_message = match err {
2251-
ReducerCallError::NoSuchReducer => Some(no_such_function_log_message("reducer", reducer_name)),
2324+
// Only log NoSuchReducer when the name is also not a known procedure.
2325+
// The HTTP /call/:reducer endpoint falls back to procedure on NoSuchReducer,
2326+
// so a valid procedure name would otherwise incorrectly produce an error log.
2327+
ReducerCallError::NoSuchReducer => {
2328+
let module_def = &self.info().module_def;
2329+
if module_def.procedure_by_name(reducer_name).is_none() {
2330+
Some(no_such_function_log_message("reducer", reducer_name))
2331+
} else {
2332+
None
2333+
}
2334+
}
22522335
ReducerCallError::Args(_) => Some(args_error_log_message("reducer", reducer_name)),
22532336
_ => None,
22542337
};
@@ -2719,18 +2802,18 @@ impl ModuleHost {
27192802
procedure_name: &str,
27202803
args: FunctionArgs,
27212804
) -> Result<(&'a ProcedureDef, CallProcedureParams), ProcedureCallError> {
2722-
let (procedure_id, procedure_def) = self
2805+
let (procedure_id, procedure_def, owning_def) = self
27232806
.info
27242807
.module_def
2725-
.procedure_full(procedure_name)
2808+
.procedure_by_name_with_module(procedure_name)
27262809
.ok_or(ProcedureCallError::NoSuchProcedure)?;
27272810

27282811
if procedure_def.visibility.is_private() && !self.is_database_owner(caller_identity) {
27292812
return Err(ProcedureCallError::NoSuchProcedure);
27302813
}
27312814

27322815
let args = args
2733-
.into_tuple_for_def(&self.info.module_def, procedure_def)
2816+
.into_tuple_for_def(owning_def, procedure_def)
27342817
.map_err(InvalidProcedureArguments)?;
27352818
let caller_connection_id = caller_connection_id.unwrap_or(ConnectionId::ZERO);
27362819

@@ -2838,7 +2921,7 @@ impl ModuleHost {
28382921
view_collector.collect_views(&mut view_ids);
28392922
for view_id in view_ids {
28402923
let st_view_row = tx.lookup_st_view(view_id)?;
2841-
let view_name = st_view_row.view_name.into();
2924+
let view_name = Identifier::new_assume_valid(st_view_row.view_name.into());
28422925
let view_id = st_view_row.view_id;
28432926
let table_id = st_view_row.table_id.ok_or(ViewCallError::TableDoesNotExist(view_id))?;
28442927
let is_anonymous = st_view_row.is_anonymous;
@@ -2904,11 +2987,13 @@ impl ModuleHost {
29042987
sender,
29052988
} in tx.views_for_refresh().cloned().collect::<Vec<_>>()
29062989
{
2907-
let Some(view_def) = module_def.get_view_by_id(fn_ptr, sender.is_none()) else {
2990+
let Some((view_def, owning_def)) =
2991+
module_def.get_view_by_global_id_with_module(fn_ptr, sender.is_none())
2992+
else {
29082993
outcome = ViewOutcome::Failed(format!("view with fn_ptr `{fn_ptr}` not found"));
29092994
break;
29102995
};
2911-
let args = match FunctionArgs::Nullary.into_tuple_for_def(module_def, view_def) {
2996+
let args = match FunctionArgs::Nullary.into_tuple_for_def(owning_def, view_def) {
29122997
Ok(args) => args,
29132998
Err(err) => {
29142999
outcome = ViewOutcome::Failed(format!("failed to build view args: {err}"));
@@ -2922,12 +3007,13 @@ impl ModuleHost {
29223007
&view_def.name,
29233008
view_id,
29243009
table_id,
2925-
view_def.fn_ptr,
3010+
fn_ptr,
29263011
caller,
29273012
sender,
29283013
args,
29293014
view_def.product_type_ref,
29303015
timestamp,
3016+
owning_def.typespace().clone(),
29313017
);
29323018

29333019
// Increment execution stats
@@ -2990,15 +3076,17 @@ impl ModuleHost {
29903076
timestamp: Timestamp,
29913077
) -> Result<(ViewCallResult, bool), ViewCallError> {
29923078
let module_def = &instance.common.info().module_def;
2993-
let view_def = module_def.view(view_name).ok_or(ViewCallError::NoSuchView)?;
2994-
let fn_ptr = view_def.fn_ptr;
3079+
let (global_fn_ptr, view_def, owning_def) = module_def
3080+
.view_by_name_with_global_fn_ptr(view_name.as_ref())
3081+
.ok_or(ViewCallError::NoSuchView)?;
29953082
let row_type = view_def.product_type_ref;
29963083
let args = args
2997-
.into_tuple_for_def(module_def, view_def)
3084+
.into_tuple_for_def(owning_def, view_def)
29983085
.map_err(InvalidViewArguments)?;
29993086

30003087
Ok(Self::call_view_inner(
3001-
instance, tx, view_name, view_id, table_id, fn_ptr, caller, sender, args, row_type, timestamp,
3088+
instance, tx, view_name, view_id, table_id, global_fn_ptr, caller, sender, args, row_type, timestamp,
3089+
owning_def.typespace().clone(),
30023090
))
30033091
}
30043092

@@ -3014,6 +3102,7 @@ impl ModuleHost {
30143102
args: ArgsTuple,
30153103
row_type: AlgebraicTypeRef,
30163104
timestamp: Timestamp,
3105+
view_typespace: Typespace,
30173106
) -> (ViewCallResult, bool) {
30183107
let view_name = name.clone();
30193108
let params = CallViewParams {
@@ -3026,6 +3115,7 @@ impl ModuleHost {
30263115
sender,
30273116
args,
30283117
row_type,
3118+
view_typespace,
30293119
};
30303120

30313121
instance.common.call_view_with_tx(tx, params, instance.instance)

crates/core/src/host/scheduler.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -306,12 +306,13 @@ impl ScheduledFunctionParams {
306306
}
307307

308308
fn kind(&self, module: &ModuleInfo) -> ScheduledFunctionKind {
309-
if module.module_def.procedure_full(self.function_name()).is_some() {
309+
if module.module_def.procedure_by_name(self.function_name()).is_some() {
310310
ScheduledFunctionKind::Procedure
311311
} else {
312312
ScheduledFunctionKind::Reducer
313313
}
314314
}
315+
315316
}
316317

317318
#[derive(thiserror::Error, Debug)]
@@ -760,11 +761,14 @@ fn function_to_reducer_call_params(
760761
) -> anyhow::Result<(Timestamp, Instant, CallReducerParams)> {
761762
let identity = module.database_identity;
762763

764+
// Find the reducer and deserialize the arguments.
765+
// Use the owning module's typespace (not necessarily the root's) so that type-index
766+
// references inside the def are resolved correctly for mounted submodules.
763767
let module = &module.module_def;
764-
let Some((id, def)) = module.reducer_full(name) else {
768+
let Some((id, def, owning)) = module.reducer_by_name_with_module(name) else {
765769
return Err(anyhow!("Reducer `{name}` not found"));
766770
};
767-
let args = args.into_tuple_for_def(module, def).map_err(InvalidReducerArguments)?;
771+
let args = args.into_tuple_for_def(owning, def).map_err(InvalidReducerArguments)?;
768772

769773
let (ts, instant) = scheduled_call_time(at);
770774
Ok((ts, instant, CallReducerParams::from_system(ts, identity, id, args)))
@@ -779,11 +783,11 @@ fn function_to_procedure_call_params(
779783
let identity = module.database_identity;
780784

781785
let module = &module.module_def;
782-
let Some((id, def)) = module.procedure_full(name) else {
786+
let Some((id, def, owning)) = module.procedure_by_name_with_module(name) else {
783787
return Err(anyhow!("Procedure `{name}` not found"));
784788
};
785789
let args = args
786-
.into_tuple_for_def(module, def)
790+
.into_tuple_for_def(owning, def)
787791
.map_err(InvalidProcedureArguments)?;
788792

789793
let (ts, instant) = scheduled_call_time(at);

0 commit comments

Comments
 (0)