diff --git a/crates/cli/src/api.rs b/crates/cli/src/api.rs index d40b03ee87e..2ed4d37dc77 100644 --- a/crates/cli/src/api.rs +++ b/crates/cli/src/api.rs @@ -73,9 +73,10 @@ impl ClientApi { } pub async fn call(&self, reducer_name: &str, arg_json: String) -> anyhow::Result { + let encoded = reducer_name.replace('/', "%2F"); Ok(self .client - .post(self.con.db_uri("call") + "/" + reducer_name) + .post(self.con.db_uri("call") + "/" + &encoded) .header(http::header::CONTENT_TYPE, "application/json") .body(arg_json) .send() diff --git a/crates/cli/src/subcommands/call.rs b/crates/cli/src/subcommands/call.rs index 777fa044890..aea38c7b35a 100644 --- a/crates/cli/src/subcommands/call.rs +++ b/crates/cli/src/subcommands/call.rs @@ -10,9 +10,9 @@ use clap::{Arg, ArgMatches}; use convert_case::{Case, Casing}; use core::ops::Deref; use itertools::Itertools; +use spacetimedb_lib::db::raw_def::v9::{RawMiscModuleExportV9, RawModuleDefV9, RawProcedureDefV9, RawReducerDefV9}; use spacetimedb_lib::sats::{self, AlgebraicType, Typespace}; use spacetimedb_lib::{Identity, ProductTypeElement}; -use spacetimedb_schema::def::{ModuleDef, ProcedureDef, ReducerDef}; use std::fmt::Write; pub fn cli() -> clap::Command { @@ -38,21 +38,21 @@ pub fn cli() -> clap::Command { } enum CallDef<'a> { - Reducer(&'a ReducerDef), - Procedure(&'a ProcedureDef), + Reducer(&'a RawReducerDefV9), + Procedure(&'a RawProcedureDefV9), } impl<'a> CallDef<'a> { fn params(&self) -> &'a sats::ProductType { match self { - CallDef::Reducer(reducer_def) => &reducer_def.params, - CallDef::Procedure(procedure_def) => &procedure_def.params, + CallDef::Reducer(r) => &r.params, + CallDef::Procedure(p) => &p.params, } } fn name(&self) -> &str { match self { - CallDef::Reducer(reducer_def) => &reducer_def.name, - CallDef::Procedure(procedure_def) => &procedure_def.name, + CallDef::Reducer(r) => r.name.as_ref(), + CallDef::Procedure(p) => p.name.as_ref(), } } fn kind(&self) -> &str { @@ -101,21 +101,31 @@ pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), Error> { let database_identity = api.con.database_identity; let database = &api.con.database; - let module_def: ModuleDef = api.module_def().await?.try_into()?; + let raw = api.module_def().await?; - let call_def = match module_def.reducer(&**reducer_procedure_name) { + let call_def = match raw.reducers.iter().find(|r| r.name.as_ref() == reducer_procedure_name.as_str()) { Some(reducer_def) => CallDef::Reducer(reducer_def), - None => match module_def.procedure(&**reducer_procedure_name) { - Some(procedure_def) => CallDef::Procedure(procedure_def), - None => { - return Err(anyhow::Error::msg(no_such_reducer_or_procedure( - &database_identity, - database, - reducer_procedure_name, - &module_def, - ))); + None => { + let procedure = raw.misc_exports.iter().find_map(|e| match e { + RawMiscModuleExportV9::Procedure(p) + if p.name.as_ref() == reducer_procedure_name.as_str() => + { + Some(p) + } + _ => None, + }); + match procedure { + Some(procedure_def) => CallDef::Procedure(procedure_def), + None => { + return Err(anyhow::Error::msg(no_such_reducer_or_procedure( + &database_identity, + database, + reducer_procedure_name, + &raw, + ))); + } } - }, + } }; // String quote any arguments that should be quoted @@ -141,9 +151,9 @@ pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), Error> { let error_msg = if response_text.starts_with("no such reducer") || response_text.starts_with("no such procedure") { - no_such_reducer_or_procedure(&database_identity, database, reducer_procedure_name, &module_def) + no_such_reducer_or_procedure(&database_identity, database, reducer_procedure_name, &raw) } else if response_text.starts_with("invalid arguments") { - invalid_arguments(&database_identity, database, &response_text, &module_def, call_def) + invalid_arguments(&database_identity, database, &response_text, &raw.typespace, call_def) } else { return error; }; @@ -160,7 +170,7 @@ pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), Error> { } /// Returns an error message for when `reducer` is called with wrong arguments. -fn invalid_arguments(identity: &Identity, db: &str, text: &str, module_def: &ModuleDef, call_def: CallDef) -> String { +fn invalid_arguments(identity: &Identity, db: &str, text: &str, typespace: &Typespace, call_def: CallDef) -> String { let mut error = format!( "Invalid arguments provided for {} `{}` for database `{}` resolving to identity `{}`.", call_def.kind(), @@ -181,7 +191,7 @@ fn invalid_arguments(identity: &Identity, db: &str, text: &str, module_def: &Mod error, "\n\nThe {} has the following signature:\n\t{}", call_def.kind(), - CallSignature(module_def.typespace().with_type(&call_def)) + CallSignature(typespace.with_type(&call_def)) ) .unwrap(); @@ -235,12 +245,12 @@ impl std::fmt::Display for CallSignature<'_> { } /// Returns an error message for when `reducer` or `procedure` does not exist in `db`. -fn no_such_reducer_or_procedure(database_identity: &Identity, db: &str, name: &str, module_def: &ModuleDef) -> String { +fn no_such_reducer_or_procedure(database_identity: &Identity, db: &str, name: &str, raw: &RawModuleDefV9) -> String { let mut error = format!( "No such reducer OR procedure `{name}` for database `{db}` resolving to identity `{database_identity}`." ); - add_reducer_procedure_ctx_to_err(&mut error, module_def, name); + add_reducer_procedure_ctx_to_err(&mut error, raw, name); error } @@ -249,16 +259,21 @@ const CALL_PRINT_LIMIT: usize = 10; /// Provided the schema for the database, /// decorate `error` with more helpful info about reducers and procedures. -fn add_reducer_procedure_ctx_to_err(error: &mut String, module_def: &ModuleDef, reducer_name: &str) { - let reducers = module_def - .reducers() - .filter(|reducer| reducer.lifecycle.is_none()) - .map(|reducer| &*reducer.name) +fn add_reducer_procedure_ctx_to_err(error: &mut String, raw: &RawModuleDefV9, reducer_name: &str) { + let reducers = raw + .reducers + .iter() + .filter(|r| r.lifecycle.is_none()) + .map(|r| r.name.as_ref()) .collect::>(); - let procedures = module_def - .procedures() - .map(|reducer| &*reducer.name) + let procedures = raw + .misc_exports + .iter() + .filter_map(|e| match e { + RawMiscModuleExportV9::Procedure(p) => Some(p.name.as_ref()), + _ => None, + }) .collect::>(); if let Some(best) = find_best_match_for_name(&reducers, reducer_name, None) { diff --git a/crates/sql-parser/src/parser/mod.rs b/crates/sql-parser/src/parser/mod.rs index b6fbe15c0dd..fb930abde6f 100644 --- a/crates/sql-parser/src/parser/mod.rs +++ b/crates/sql-parser/src/parser/mod.rs @@ -5,6 +5,8 @@ use sqlparser::ast::{ WildcardAdditionalOptions, }; +use spacetimedb_lib::sats::raw_identifier::RawIdentifier; + use crate::ast::{ BinOp, LogOp, Parameter, Project, ProjectElem, ProjectExpr, SqlExpr, SqlFrom, SqlIdent, SqlJoin, SqlLiteral, }; @@ -348,5 +350,8 @@ pub(crate) fn parse_parts(mut parts: Vec) -> SqlParseResult { if parts.len() == 1 { return Ok(parts.swap_remove(0).into()); } - Err(SqlUnsupported::MultiPartName(ObjectName(parts)).into()) + // Join multi-part names (e.g. `lib.library_table`) with dots to match + // namespace-prefixed table names stored in the catalog. + let joined = parts.iter().map(|p| p.value.as_str()).collect::>().join("."); + Ok(SqlIdent(RawIdentifier::new(joined))) } diff --git a/crates/sql-parser/src/parser/sql.rs b/crates/sql-parser/src/parser/sql.rs index e689817cc4c..27cf43337b5 100644 --- a/crates/sql-parser/src/parser/sql.rs +++ b/crates/sql-parser/src/parser/sql.rs @@ -403,8 +403,6 @@ mod tests { for sql in [ // FROM is required "select 1", - // Multi-part table names - "select a from s.t", // Bit-string literals "select * from t where a = B'1010'", // Wildcard with non-wildcard projections @@ -430,6 +428,8 @@ mod tests { fn supported() { for sql in [ "select a from t", + // Multi-part names are joined with dots for namespace-qualified tables + "select a from s.t", "select a from t where x = :sender", "select count(*) as n from t", "select count(*) as n from t join s on t.id = s.id where s.x = 1",