diff --git a/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/person_2_table.ts b/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/person_2_table.ts index 5a94f4ea501..9359fb15439 100644 --- a/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/person_2_table.ts +++ b/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/person_2_table.ts @@ -12,10 +12,10 @@ import { import { Person3Info } from './types'; export default __t.row({ - person2Id: __t.u32().primaryKey().name('Person2Id'), - firstName: __t.string().name('FirstName'), - playerRef: __t.u32(), + person2Id: __t.u32().primaryKey().name('person_2_id'), + firstName: __t.string().name('first_name'), + playerRef: __t.u32().name('player_ref'), get personInfo() { - return Person3Info; + return Person3Info.name('person_info'); }, }); diff --git a/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/person_at_level_2_table.ts b/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/person_at_level_2_table.ts index 5a94f4ea501..9359fb15439 100644 --- a/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/person_at_level_2_table.ts +++ b/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/person_at_level_2_table.ts @@ -12,10 +12,10 @@ import { import { Person3Info } from './types'; export default __t.row({ - person2Id: __t.u32().primaryKey().name('Person2Id'), - firstName: __t.string().name('FirstName'), - playerRef: __t.u32(), + person2Id: __t.u32().primaryKey().name('person_2_id'), + firstName: __t.string().name('first_name'), + playerRef: __t.u32().name('player_ref'), get personInfo() { - return Person3Info; + return Person3Info.name('person_info'); }, }); diff --git a/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/player_1_table.ts b/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/player_1_table.ts index ac5c3648fea..fba1e5f12f3 100644 --- a/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/player_1_table.ts +++ b/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/player_1_table.ts @@ -12,10 +12,10 @@ import { import { Player2Status } from './types'; export default __t.row({ - player1Id: __t.u32().primaryKey().name('Player1Id'), + player1Id: __t.u32().primaryKey().name('player_1_id'), playerName: __t.string().name('player_name'), - currentLevel2: __t.u32(), + currentLevel2: __t.u32().name('current_level_2'), get status3Field() { - return Player2Status; + return Player2Status.name('status_3_field'); }, }); diff --git a/crates/bindings-typescript/src/lib/type_builders.ts b/crates/bindings-typescript/src/lib/type_builders.ts index 136e040a2b6..41d2da11015 100644 --- a/crates/bindings-typescript/src/lib/type_builders.ts +++ b/crates/bindings-typescript/src/lib/type_builders.ts @@ -1568,7 +1568,9 @@ class SumBuilderImpl extends TypeBuilder, VariantsToSumType> implements Defaultable, VariantsToSumType>, - Nameable, VariantsToSumType> + Nameable, VariantsToSumType>, + Indexable, VariantsToSumType>, + PrimaryKeyable, VariantsToSumType> { readonly variants: Variants; readonly typeName: string | undefined; @@ -1673,6 +1675,33 @@ class SumBuilderImpl ): SumColumnBuilder> { return new SumColumnBuilder(this, set(defaultMetadata, { name })); } + index(): SumColumnBuilder< + Variants, + SetField + >; + index>( + algorithm: N + ): SumColumnBuilder>; + index( + algorithm: IndexTypes = 'btree' + ): SumColumnBuilder< + Variants, + SetField + > { + return new SumColumnBuilder( + this, + set(defaultMetadata, { indexType: algorithm }) + ); + } + primaryKey(): SumColumnBuilder< + Variants, + SetField + > { + return new SumColumnBuilder( + this, + set(defaultMetadata, { isPrimaryKey: true }) + ); + } } export const SumBuilder: { @@ -3273,7 +3302,9 @@ export class SumColumnBuilder< extends ColumnBuilder, VariantsToSumType, M> implements Defaultable, VariantsToSumType>, - Nameable, VariantsToSumType> + Nameable, VariantsToSumType>, + Indexable, VariantsToSumType>, + PrimaryKeyable, VariantsToSumType> { default( value: EnumType @@ -3294,6 +3325,33 @@ export class SumColumnBuilder< set(this.columnMetadata, { name }) ); } + index(): SumColumnBuilder< + Variants, + SetField + >; + index>( + algorithm: N + ): SumColumnBuilder>; + index( + algorithm: IndexTypes = 'btree' + ): SumColumnBuilder< + Variants, + SetField + > { + return new SumColumnBuilder( + this.typeBuilder, + set(this.columnMetadata, { indexType: algorithm }) + ); + } + primaryKey(): SumColumnBuilder< + Variants, + SetField + > { + return new SumColumnBuilder( + this.typeBuilder, + set(this.columnMetadata, { isPrimaryKey: true }) + ); + } } export class SimpleSumColumnBuilder< diff --git a/crates/bindings/src/http.rs b/crates/bindings/src/http.rs index 0fb8a63d53b..4c8ea487c47 100644 --- a/crates/bindings/src/http.rs +++ b/crates/bindings/src/http.rs @@ -13,7 +13,7 @@ use crate::StdbRng; #[cfg(feature = "unstable")] use crate::{try_with_tx, with_tx, Timestamp, TxContext}; use bytes::Bytes; -#[cfg(all(feature = "rand", feature = "unstable"))] +#[cfg(all(feature = "rand08", feature = "unstable"))] use rand08::RngCore; #[cfg(feature = "unstable")] use spacetimedb_lib::db::raw_def::v10::MethodOrAny; @@ -22,10 +22,10 @@ use spacetimedb_lib::http as st_http; use spacetimedb_lib::http::{character_is_acceptable_for_route_path, ACCEPTABLE_ROUTE_PATH_CHARS_HUMAN_DESCRIPTION}; #[cfg(feature = "unstable")] use spacetimedb_lib::Identity; -#[cfg(all(feature = "unstable", feature = "rand"))] +#[cfg(all(feature = "unstable", feature = "rand08"))] use spacetimedb_lib::Uuid; use spacetimedb_lib::{bsatn, TimeDuration}; -#[cfg(all(feature = "unstable", feature = "rand"))] +#[cfg(all(feature = "unstable", feature = "rand08"))] use std::cell::Cell; #[cfg(all(feature = "unstable", feature = "rand08"))] use std::cell::OnceCell; @@ -72,7 +72,7 @@ pub use spacetimedb_bindings_macro::http_handler as handler; /// Register a [`Router`](struct@Router) to route HTTP requests to handlers. /// -/// This should annotate a function of no arguments which returns a [`Router`](struct@router). +/// This should annotate a function of no arguments which returns a [`Router`](struct@Router). /// /// ```no_run /// # use spacetimedb::http::{handler, router, Request, Response, Body, HandlerContext, Router}; @@ -109,7 +109,7 @@ pub struct HandlerContext { /// A counter used for generating UUIDv7 values. /// **Note:** must be 0..=u32::MAX - #[cfg(feature = "rand")] + #[cfg(feature = "rand08")] pub(crate) counter_uuid: Cell, } @@ -121,7 +121,7 @@ impl HandlerContext { http: HttpClient {}, #[cfg(feature = "rand08")] rng: OnceCell::new(), - #[cfg(feature = "rand")] + #[cfg(feature = "rand08")] counter_uuid: Cell::new(0), } } @@ -148,7 +148,7 @@ impl HandlerContext { } /// Create a new random [`Uuid`] `v4` using the built-in RNG. - #[cfg(feature = "rand")] + #[cfg(feature = "rand08")] pub fn new_uuid_v4(&self) -> anyhow::Result { let mut bytes = [0u8; 16]; self.rng().try_fill_bytes(&mut bytes)?; @@ -156,7 +156,7 @@ impl HandlerContext { } /// Create a new sortable [`Uuid`] `v7` using the built-in RNG, counter and timestamp. - #[cfg(feature = "rand")] + #[cfg(feature = "rand08")] pub fn new_uuid_v7(&self) -> anyhow::Result { let mut random_bytes = [0u8; 4]; self.rng().try_fill_bytes(&mut random_bytes)?; diff --git a/crates/bindings/src/lib.rs b/crates/bindings/src/lib.rs index 0d375719829..2c7faa78c6b 100644 --- a/crates/bindings/src/lib.rs +++ b/crates/bindings/src/lib.rs @@ -1,9 +1,11 @@ #![doc = include_str!("../README.md")] // ^ if you are working on docs, go read the top comment of README.md please. -use core::cell::{Cell, LazyCell, OnceCell, RefCell}; +use core::cell::{LazyCell, OnceCell, RefCell}; use core::ops::Deref; use spacetimedb_lib::bsatn; +#[cfg(feature = "rand08")] +use std::cell::Cell; use std::rc::Rc; #[cfg(feature = "unstable")] @@ -24,9 +26,11 @@ pub use spacetimedb_query_builder as query_builder; #[cfg(feature = "unstable")] pub use client_visibility_filter::Filter; pub use log; -#[cfg(feature = "rand")] +#[cfg(feature = "rand08")] +use rand::distributions::{Distribution, Standard}; +#[cfg(feature = "rand08")] pub use rand08 as rand; -#[cfg(feature = "rand")] +#[cfg(feature = "rand08")] use rand08::RngCore; #[cfg(feature = "rand08")] pub use rng::StdbRng; @@ -42,6 +46,9 @@ pub use spacetimedb_lib::ser::Serialize; pub use spacetimedb_lib::AlgebraicValue; pub use spacetimedb_lib::ConnectionId; // `FilterableValue` re-exported purely for rustdoc. +#[cfg(feature = "unstable")] +use crate::http::HandlerContext; +use crate::http::HttpClient; pub use spacetimedb_lib::FilterableValue; pub use spacetimedb_lib::Identity; pub use spacetimedb_lib::ScheduleAt; @@ -1027,7 +1034,7 @@ pub struct ReducerContext { rng: std::cell::OnceCell, /// A counter used for generating UUIDv7 values. /// **Note:** must be 0..=u32::MAX - #[cfg(feature = "rand")] + #[cfg(feature = "rand08")] counter_uuid: Cell, } @@ -1042,7 +1049,7 @@ impl ReducerContext { sender_auth: AuthCtx::internal(), #[cfg(feature = "rand08")] rng: std::cell::OnceCell::new(), - #[cfg(feature = "rand")] + #[cfg(feature = "rand08")] counter_uuid: Cell::new(0), } } @@ -1057,7 +1064,7 @@ impl ReducerContext { sender_auth: AuthCtx::from_connection_id_opt(connection_id), #[cfg(feature = "rand08")] rng: std::cell::OnceCell::new(), - #[cfg(feature = "rand")] + #[cfg(feature = "rand08")] counter_uuid: Cell::new(0), } } @@ -1123,7 +1130,7 @@ impl ReducerContext { /// } /// # } /// ``` - #[cfg(feature = "rand")] + #[cfg(feature = "rand08")] pub fn new_uuid_v4(&self) -> anyhow::Result { let mut bytes = [0u8; 16]; self.rng().try_fill_bytes(&mut bytes)?; @@ -1145,7 +1152,7 @@ impl ReducerContext { /// } /// # } /// ``` - #[cfg(feature = "rand")] + #[cfg(feature = "rand08")] pub fn new_uuid_v7(&self) -> anyhow::Result { let mut random_bytes = [0u8; 4]; self.rng().try_fill_bytes(&mut random_bytes)?; @@ -1268,7 +1275,7 @@ pub struct ProcedureContext { /// A counter used for generating UUIDv7 values. /// **Note:** must be 0..=u32::MAX // Disabled when compiling without `rand`, as both v4 and v7 UUIDs have random components. - #[cfg(feature = "rand")] + #[cfg(feature = "rand08")] counter_uuid: Cell, } @@ -1281,7 +1288,7 @@ impl ProcedureContext { http: http::HttpClient {}, #[cfg(feature = "rand08")] rng: std::cell::OnceCell::new(), - #[cfg(feature = "rand")] + #[cfg(feature = "rand08")] counter_uuid: Cell::new(0), } } @@ -1421,7 +1428,7 @@ impl ProcedureContext { /// } /// # } /// ``` - #[cfg(feature = "rand")] + #[cfg(feature = "rand08")] pub fn new_uuid_v4(&self) -> anyhow::Result { let mut bytes = [0u8; 16]; self.rng().try_fill_bytes(&mut bytes)?; @@ -1443,7 +1450,7 @@ impl ProcedureContext { /// } /// # } /// ``` - #[cfg(feature = "rand")] + #[cfg(feature = "rand08")] pub fn new_uuid_v7(&self) -> anyhow::Result { let mut random_bytes = [0u8; 4]; self.rng().try_fill_bytes(&mut random_bytes)?; @@ -1452,6 +1459,7 @@ impl ProcedureContext { } /// A handle on a database with a particular table schema. +#[deprecated(note = "Use the capability based traits (CtxDbRead, CtxDbWrite) instead!")] pub trait DbContext { /// A view into the tables of a database. /// @@ -1473,6 +1481,7 @@ pub trait DbContext { fn db_read_only(&self) -> &LocalReadOnly; } +#[allow(deprecated)] impl DbContext for AnonymousViewContext { type DbView = LocalReadOnly; @@ -1485,6 +1494,7 @@ impl DbContext for AnonymousViewContext { } } +#[allow(deprecated)] impl DbContext for ReducerContext { type DbView = Local; @@ -1497,6 +1507,7 @@ impl DbContext for ReducerContext { } } +#[allow(deprecated)] impl DbContext for TxContext { type DbView = Local; @@ -1509,6 +1520,7 @@ impl DbContext for TxContext { } } +#[allow(deprecated)] impl DbContext for ViewContext { type DbView = LocalReadOnly; @@ -1538,6 +1550,295 @@ impl Local { } } +/// Contexts which provide read access to the database. +/// +/// This trait is useful for writing reusable logic which is generic over the context type, +/// allowing it to be used from views, reducers, and transactions started by procedures and HTTP handlers. +/// +/// When operating on a concrete-typed [`ViewContext`], [`ReducerContext`] or [`TxContext`], +/// this trait is not necessary, as the context's `db` field provides the same (or greater, read-write) access. +pub trait CtxDbRead { + fn db_read_only(&self) -> &LocalReadOnly; +} + +impl CtxDbRead for TxContext { + fn db_read_only(&self) -> &LocalReadOnly { + &LocalReadOnly {} + } +} + +impl CtxDbRead for ReducerContext { + fn db_read_only(&self) -> &LocalReadOnly { + &LocalReadOnly {} + } +} + +impl CtxDbRead for ViewContext { + fn db_read_only(&self) -> &LocalReadOnly { + &LocalReadOnly {} + } +} + +impl CtxDbRead for AnonymousViewContext { + fn db_read_only(&self) -> &LocalReadOnly { + &LocalReadOnly {} + } +} + +/// Contexts which provide read-write access to the database. +/// +/// This trait is useful for writing reusable logic which is generic over the context type, +/// allowing it to be used from reducers and from transactions started by procedures and HTTP handlers. +/// +/// When operating on a concrete-typed [`ReducerContext`] or [`TxContext`], this trait is not necessary, +/// as the context's `db` field provides the same access. +pub trait CtxDbWrite: CtxDbRead { + fn db(&self) -> &Local; +} + +impl CtxDbWrite for TxContext { + fn db(&self) -> &Local { + &Local {} + } +} + +impl CtxDbWrite for ReducerContext { + fn db(&self) -> &Local { + &Local {} + } +} + +/// Contexts which can retrieve the sender [`Identity`]. +/// +/// This trait is useful for writing reusable logic which is generic over the context type, +/// allowing it to be used from views, reducers, and transactions started by procedures and HTTP handlers. +/// +/// When operating on a concrete-typed [`ViewContext`], [`ReducerContext`], [`ProcedureContext`] or [`TxContext`], +/// this trait is not necessary, as the context's inherent `sender` method provides the same access. +pub trait CtxWithSender { + fn sender(&self) -> Identity; +} + +impl CtxWithSender for ViewContext { + fn sender(&self) -> Identity { + self.sender + } +} + +impl CtxWithSender for ReducerContext { + fn sender(&self) -> Identity { + self.sender + } +} + +impl CtxWithSender for TxContext { + fn sender(&self) -> Identity { + self.0.sender + } +} + +impl CtxWithSender for ProcedureContext { + fn sender(&self) -> Identity { + self.sender + } +} + +/// Contexts which can retrieve the current [`Timestamp`]. +/// +/// This trait is useful for writing reusable logic which is generic over the context type, +/// allowing it to be used from reducers, procedures, HTTP handlers, +/// and transactions started by procedures and HTTP handlers. +/// +/// When operating on a concrete-typed [`ReducerContext`], [`ProcedureContext`] +#[cfg_attr(feature = "unstable", doc = ", [`HandlerContext`]")] +/// or [`TxContext`], +/// this trait is not necessary, as the context's `timestamp` field provides the same access. +pub trait CtxWithTimestamp { + fn timestamp(&self) -> Timestamp; +} + +impl CtxWithTimestamp for ReducerContext { + fn timestamp(&self) -> Timestamp { + self.timestamp + } +} + +impl CtxWithTimestamp for TxContext { + fn timestamp(&self) -> Timestamp { + self.timestamp + } +} + +impl CtxWithTimestamp for ProcedureContext { + fn timestamp(&self) -> Timestamp { + self.timestamp + } +} + +#[cfg(feature = "unstable")] +impl CtxWithTimestamp for HandlerContext { + fn timestamp(&self) -> Timestamp { + self.timestamp + } +} + +/// Contexts which can retrieve the current [`AuthCtx`]. +/// +/// This trait is useful for writing reusable logic which is generic over the context type, +/// allowing it to be used from reducers and procedures. +/// +/// When operating on a concrete-typed [`ReducerContext`], [`ProcedureContext`], [`TxContext`], +/// this trait is not necessary, as the context's sender_auth method provides the same access. +pub trait CtxWithSenderAuth { + fn sender_auth(&self) -> &AuthCtx; +} + +impl CtxWithSenderAuth for ReducerContext { + fn sender_auth(&self) -> &AuthCtx { + self.sender_auth() + } +} + +impl CtxWithSenderAuth for TxContext { + fn sender_auth(&self) -> &AuthCtx { + self.0.sender_auth() + } +} + +/// Contexts which can enter a start a transaction with a [`TxContext`] inside the function. +/// +/// This trait is useful for writing reusable logic which is generic over the context type, +/// allowing it to be used from reducers and procedures. +/// +/// When operating on a concrete-typed +#[cfg_attr(feature = "unstable", doc = "[`HandlerContext`] or")] +/// [`ProcedureContext`], +/// this trait is not necessary, as the context's methods provide the same access. +pub trait CtxWithTxManagement { + fn with_tx(&mut self, body: impl Fn(&TxContext) -> T) -> T; + fn try_with_tx(&mut self, body: impl Fn(&TxContext) -> Result) -> Result; +} + +impl CtxWithTxManagement for ProcedureContext { + fn with_tx(&mut self, body: impl Fn(&TxContext) -> T) -> T { + self.with_tx(body) + } + + fn try_with_tx(&mut self, body: impl Fn(&TxContext) -> Result) -> Result { + self.try_with_tx(body) + } +} + +#[cfg(feature = "unstable")] +impl CtxWithTxManagement for HandlerContext { + fn with_tx(&mut self, body: impl Fn(&TxContext) -> T) -> T { + self.with_tx(body) + } + + fn try_with_tx(&mut self, body: impl Fn(&TxContext) -> Result) -> Result { + self.try_with_tx(body) + } +} + +/// Contexts which can retrieve the current [`StdbRng`] state. +/// +/// This trait is useful for writing reusable logic which is generic over the context type, +/// allowing it to be used from reducers and procedures. +/// +/// When operating on a concrete-typed +#[cfg_attr(feature = "unstable", doc = "[`HandlerContext`],")] +/// [`ProcedureContext`], [`ReducerContext`], [`TxContext`] +/// this trait is not necessary, as the context's methods provide the same access. +#[cfg(feature = "rand08")] +pub trait CtxWithRng { + fn rng(&self) -> &StdbRng; + fn random(&self) -> T + where + Standard: Distribution; +} + +#[cfg(feature = "rand08")] +impl CtxWithRng for ProcedureContext { + fn rng(&self) -> &StdbRng { + self.rng() + } + + fn random(&self) -> T + where + Standard: Distribution, + { + self.random() + } +} + +#[cfg(all(feature = "unstable", feature = "rand08"))] +impl CtxWithRng for HandlerContext { + fn rng(&self) -> &StdbRng { + self.rng() + } + + fn random(&self) -> T + where + Standard: Distribution, + { + self.random() + } +} + +#[cfg(feature = "rand08")] +impl CtxWithRng for ReducerContext { + fn rng(&self) -> &StdbRng { + self.rng() + } + + fn random(&self) -> T + where + Standard: Distribution, + { + self.random() + } +} + +#[cfg(feature = "rand08")] +impl CtxWithRng for TxContext { + fn rng(&self) -> &StdbRng { + self.0.rng() + } + + fn random(&self) -> T + where + Standard: Distribution, + { + self.0.random() + } +} + +/// Contexts which can perform outgoing HTTP requests. +/// +/// This type is useful for writing reusable logic which is generic over the context type, +/// allowing it to be used from procedures and HTTP handlers. +/// +/// When operating on a concrete-typed [`ProcedureContext`] +#[cfg_attr(feature = "unstable", doc = "or [`HandlerContext`],")] +/// this trait is not necessary, +/// as the context's `http` field provides the same access. +pub trait CtxWithHttp { + fn http(&self) -> &HttpClient; +} + +#[cfg(feature = "unstable")] +impl CtxWithHttp for HandlerContext { + fn http(&self) -> &HttpClient { + &self.http + } +} + +impl CtxWithHttp for ProcedureContext { + fn http(&self) -> &HttpClient { + &self.http + } +} + /// The [JWT] of an [`AuthCtx`]. /// /// [JWT]: https://en.wikipedia.org/wiki/JSON_Web_Token diff --git a/crates/client-api/src/util.rs b/crates/client-api/src/util.rs index adc1f632bc4..0cc87a82bfd 100644 --- a/crates/client-api/src/util.rs +++ b/crates/client-api/src/util.rs @@ -49,8 +49,10 @@ impl headers::Header for XForwardedFor { fn decode<'i, I: Iterator>(values: &mut I) -> Result { let val = values.next().ok_or_else(headers::Error::invalid)?; let val = val.to_str().map_err(|_| headers::Error::invalid())?; - let (first, _) = val.split_once(',').ok_or_else(headers::Error::invalid)?; - let ip = first.trim().parse().map_err(|_| headers::Error::invalid())?; + // X-Forwarded-For is a comma-separated chain. For a single-hop + // proxy there is no comma; take the first IP either way. + let first = val.split(',').next().unwrap_or(val).trim(); + let ip = first.parse().map_err(|_| headers::Error::invalid())?; Ok(XForwardedFor(ip)) } @@ -185,3 +187,41 @@ impl FromRequest for EmptyBody { Ok(Self) } } + +#[cfg(test)] +mod tests { + use super::*; + use headers::Header; + + fn decode_one(raw: &str) -> Result { + let val = HeaderValue::from_str(raw).unwrap(); + let values = [val]; + XForwardedFor::decode(&mut values.iter()) + } + + #[test] + fn decodes_single_ip() { + // Single-hop proxies (e.g. nginx inserting the client IP) emit + // a value with no comma. This must succeed. + let got = decode_one("10.0.0.1").expect("single IP should decode"); + assert_eq!(got.0, "10.0.0.1".parse::().unwrap()); + } + + #[test] + fn decodes_chain_takes_first() { + let got = decode_one("10.0.0.1, 192.168.1.1, 172.16.0.1").expect("chain should decode"); + assert_eq!(got.0, "10.0.0.1".parse::().unwrap()); + } + + #[test] + fn decodes_chain_trims_whitespace() { + let got = decode_one(" 10.0.0.1 , 192.168.1.1").expect("chain should decode"); + assert_eq!(got.0, "10.0.0.1".parse::().unwrap()); + } + + #[test] + fn rejects_non_ip() { + assert!(decode_one("not-an-ip").is_err()); + assert!(decode_one("not-an-ip, 10.0.0.1").is_err()); + } +} diff --git a/crates/codegen/src/typescript.rs b/crates/codegen/src/typescript.rs index 457d8960d1a..a2d1cbf8e6c 100644 --- a/crates/codegen/src/typescript.rs +++ b/crates/codegen/src/typescript.rs @@ -15,7 +15,7 @@ use convert_case::{Case, Casing}; use spacetimedb_lib::sats::layout::PrimitiveType; use spacetimedb_lib::sats::AlgebraicTypeRef; use spacetimedb_primitives::ColId; -use spacetimedb_schema::def::{ConstraintDef, IndexDef, ModuleDef, ReducerDef, TableDef, TypeDef}; +use spacetimedb_schema::def::{ColumnDef, ConstraintDef, IndexDef, ModuleDef, ReducerDef, TableDef, TypeDef}; use spacetimedb_schema::identifier::Identifier; use spacetimedb_schema::reducer_name::ReducerName; use spacetimedb_schema::schema::TableSchema; @@ -85,7 +85,15 @@ impl Lang for TypeScript { writeln!(out, "export default __t.row({{"); out.indent(1); - write_object_type_builder_fields(module, out, &product_def.elements, table.primary_key, true, true).unwrap(); + write_object_type_builder_fields( + module, + out, + &product_def.elements, + table.primary_key, + true, + Some(&table.columns), + ) + .unwrap(); out.dedent(1); writeln!(out, "}});"); OutputFile { @@ -143,7 +151,7 @@ impl Lang for TypeScript { writeln!(out, "export const params = {{"); out.with_indent(|out| { - write_object_type_builder_fields(module, out, &procedure.params_for_generate.elements, None, true, false) + write_object_type_builder_fields(module, out, &procedure.params_for_generate.elements, None, true, None) .unwrap() }); writeln!(out, "}};"); @@ -815,7 +823,7 @@ fn define_body_for_reducer(module: &ModuleDef, out: &mut Indenter, params: &[(Id writeln!(out, "}};"); } else { writeln!(out); - out.with_indent(|out| write_object_type_builder_fields(module, out, params, None, true, false).unwrap()); + out.with_indent(|out| write_object_type_builder_fields(module, out, params, None, true, None).unwrap()); writeln!(out, "}};"); } } @@ -840,7 +848,7 @@ fn define_body_for_product( writeln!(out, "}});"); } else { writeln!(out); - out.with_indent(|out| write_object_type_builder_fields(module, out, elements, None, true, false).unwrap()); + out.with_indent(|out| write_object_type_builder_fields(module, out, elements, None, true, None).unwrap()); writeln!(out, "}});"); } writeln!(out, "export type {name} = __Infer;"); @@ -932,7 +940,7 @@ fn write_object_type_builder_fields( elements: &[(Identifier, AlgebraicTypeUse)], primary_key: Option, convert_case: bool, - write_original_name: bool, + columns: Option<&[ColumnDef]>, ) -> anyhow::Result<()> { for (i, (ident, ty)) in elements.iter().enumerate() { let name = if convert_case { @@ -945,7 +953,14 @@ fn write_object_type_builder_fields( Some(pk) => pk.idx() == i, None => false, }; - let original_name = (write_original_name && convert_case && *name != **ident).then_some(&**ident); + // The `.name(..)` value is the in-database (canonical) column name, which may + // differ from the generated camelCase accessor key. Emit it only when the + // canonical name differs, so the client maps to the correct wire/column name + // regardless of the source identifier's casing. + let original_name = columns + .and_then(|columns| columns.get(i)) + .map(|column| column.name.deref()) + .filter(|canonical| convert_case && *canonical != name.as_str()); write_type_builder_field(module, out, &name, original_name, ty, is_primary_key)?; } @@ -1083,7 +1098,7 @@ fn define_body_for_sum( (Identifier::for_test(pascal), ty.clone()) }) .collect(); - out.with_indent(|out| write_object_type_builder_fields(module, out, &pascal_variants, None, false, false).unwrap()); + out.with_indent(|out| write_object_type_builder_fields(module, out, &pascal_variants, None, false, None).unwrap()); writeln!(out, "}});"); writeln!(out, "export type {name} = __Infer;"); out.newline(); diff --git a/crates/execution/src/lib.rs b/crates/execution/src/lib.rs index 34d02317d1f..7eac07f7c29 100644 --- a/crates/execution/src/lib.rs +++ b/crates/execution/src/lib.rs @@ -1,9 +1,12 @@ use anyhow::Result; use core::hash::{Hash, Hasher}; use core::ops::RangeBounds; -use spacetimedb_lib::{hash_sender_view_args, identity::AuthCtx, query::Delta, AlgebraicType, Identity}; +use spacetimedb_lib::{ + hash_empty_view_args, hash_sender_view_args, identity::AuthCtx, query::Delta, AlgebraicType, Identity, +}; use spacetimedb_physical_plan::plan::{ - ParamResolver, ParamSlot, ProjectField, TupleField, PARAM_SENDER, PARAM_VIEW_ARG_HASH, + ParamResolver, ParamSlot, ProjectField, TupleField, PARAM_SENDER, PARAM_VIEW_ARG_HASH_EMPTY, + PARAM_VIEW_ARG_HASH_SENDER, }; use spacetimedb_primitives::{ColList, IndexId, TableId}; use spacetimedb_sats::bsatn::{BufReservedFill, EncodeError, ToBsatn}; @@ -18,14 +21,16 @@ pub mod pipelined; #[derive(Debug, Clone, Copy)] pub struct ExecutionParams { sender: Identity, - view_arg_hash: u256, + empty_view_arg_hash: u256, + sender_view_arg_hash: u256, } impl ExecutionParams { pub fn from_sender(sender: Identity) -> Self { Self { sender, - view_arg_hash: hash_sender_view_args(sender).to_u256(), + empty_view_arg_hash: hash_empty_view_args().to_u256(), + sender_view_arg_hash: hash_sender_view_args(sender).to_u256(), } } @@ -40,8 +45,14 @@ impl ParamResolver for ExecutionParams { PARAM_SENDER if ty.is_identity() => self.sender.into(), PARAM_SENDER if ty.is_bytes() => AlgebraicValue::Bytes(self.sender.to_be_byte_array().into()), PARAM_SENDER => panic!("unsupported type for :sender: {ty:?}"), - PARAM_VIEW_ARG_HASH if matches!(ty, AlgebraicType::U256) => AlgebraicValue::U256(self.view_arg_hash.into()), - PARAM_VIEW_ARG_HASH => panic!("unsupported type for view arg hash: {ty:?}"), + PARAM_VIEW_ARG_HASH_EMPTY if matches!(ty, AlgebraicType::U256) => { + AlgebraicValue::U256(self.empty_view_arg_hash.into()) + } + PARAM_VIEW_ARG_HASH_EMPTY => panic!("unsupported type for empty view arg hash: {ty:?}"), + PARAM_VIEW_ARG_HASH_SENDER if matches!(ty, AlgebraicType::U256) => { + AlgebraicValue::U256(self.sender_view_arg_hash.into()) + } + PARAM_VIEW_ARG_HASH_SENDER => panic!("unsupported type for sender view arg hash: {ty:?}"), ParamSlot(slot) => panic!("unknown physical plan parameter slot: {slot}"), } } diff --git a/crates/physical-plan/src/plan.rs b/crates/physical-plan/src/plan.rs index 3dfbd82d80c..aba742cce6b 100644 --- a/crates/physical-plan/src/plan.rs +++ b/crates/physical-plan/src/plan.rs @@ -6,9 +6,7 @@ use spacetimedb_expr::{ expr::{AggType, CollectViews}, StatementSource, }; -use spacetimedb_lib::{ - empty_view_arg_hash_value, query::Delta, sats::size_of::SizeOf, AlgebraicType, AlgebraicValue, ProductValue, -}; +use spacetimedb_lib::{query::Delta, sats::size_of::SizeOf, AlgebraicType, AlgebraicValue, ProductValue}; use spacetimedb_primitives::{ColId, ColOrCols, ColSet, IndexId, TableId, ViewId}; use spacetimedb_schema::schema::{IndexSchema, TableSchema, VIEW_ARG_HASH_COL}; use spacetimedb_sql_parser::ast::{BinOp, LogOp}; @@ -42,7 +40,8 @@ pub trait ParamResolver { pub struct ParamSlot(pub u16); pub const PARAM_SENDER: ParamSlot = ParamSlot(0); -pub const PARAM_VIEW_ARG_HASH: ParamSlot = ParamSlot(1); +pub const PARAM_VIEW_ARG_HASH_EMPTY: ParamSlot = ParamSlot(1); +pub const PARAM_VIEW_ARG_HASH_SENDER: ParamSlot = ParamSlot(2); /// Physical plans always terminate with a projection. /// This type of projection returns row ids. @@ -553,9 +552,10 @@ impl PhysicalPlan { /// If a view has private arguments, its backing table has an `arg_hash` column. /// This column tracks which rows belong to which argument tuple. /// - /// As a result, queries over such views cannot read the entire backing table. - /// They must only select the rows corresponding to the caller of the query. - /// Hence we must add an implicit selection over these types of views. + /// As a result, queries over views cannot read the entire backing table. + /// They must only select the rows corresponding to the view arguments used + /// by each view reference. Hence we must add an implicit selection over + /// these types of views. /// /// Ex. /// ```sql @@ -569,11 +569,12 @@ impl PhysicalPlan { fn expand_views(self) -> Self { match self { Self::TableScan(scan, label) if scan.schema.is_view() => { - let arg_hash = if scan.schema.is_anonymous_view() { - PhysicalExpr::Value(empty_view_arg_hash_value()) + let param = if scan.schema.is_anonymous_view() { + PARAM_VIEW_ARG_HASH_EMPTY } else { - PhysicalExpr::Param(PARAM_VIEW_ARG_HASH, AlgebraicType::U256) + PARAM_VIEW_ARG_HASH_SENDER }; + let arg_hash = PhysicalExpr::Param(param, AlgebraicType::U256); Self::Filter( Box::new(Self::TableScan(scan, label)), PhysicalExpr::BinOp( diff --git a/modules/keynote-benchmarks/src/lib.rs b/modules/keynote-benchmarks/src/lib.rs index 3ba8394089c..70d68bc0152 100644 --- a/modules/keynote-benchmarks/src/lib.rs +++ b/modules/keynote-benchmarks/src/lib.rs @@ -1,5 +1,5 @@ use core::ops::AddAssign; -use spacetimedb::{log_stopwatch::LogStopwatch, rand::Rng, reducer, table, DbContext, ReducerContext, Table}; +use spacetimedb::{log_stopwatch::LogStopwatch, rand::Rng, reducer, table, ReducerContext, Table}; #[derive(Clone, Copy, Debug)] #[table(accessor = position, public)] @@ -39,7 +39,7 @@ fn init(ctx: &ReducerContext) { // Insert 10^6 randomized positions and velocities, // but with incrementing and corresponding ids. - let db = ctx.db(); + let db = &ctx.db; let mut rng = ctx.rng(); for id in 0..1_000_000 { let (x, y, z) = rng.r#gen(); @@ -71,7 +71,7 @@ fn update_positions_by_collect(ctx: &ReducerContext) { #[reducer] fn roundtrip(ctx: &ReducerContext) { // Warmup the index. - let id = ctx.db().velocity().id(); + let id = ctx.db.velocity().id(); for x in 0..10_000 { id.find(x); } diff --git a/modules/sdk-test-procedure-concurrency/src/lib.rs b/modules/sdk-test-procedure-concurrency/src/lib.rs index 4f99c3a422e..e8874309f8e 100644 --- a/modules/sdk-test-procedure-concurrency/src/lib.rs +++ b/modules/sdk-test-procedure-concurrency/src/lib.rs @@ -1,6 +1,4 @@ -use spacetimedb::{ - procedure, reducer, table, DbContext, ProcedureContext, ReducerContext, ScheduleAt, Table, TxContext, -}; +use spacetimedb::{procedure, reducer, table, ProcedureContext, ReducerContext, ScheduleAt, Table, TxContext}; use std::time::Duration; #[table(public, accessor = procedure_concurrency_row)] @@ -19,7 +17,7 @@ fn insert_procedure_concurrency_row(ctx: &TxContext, insertion_context: &str) { #[reducer] fn insert_reducer_row(ctx: &ReducerContext) { - ctx.db().procedure_concurrency_row().insert(ProcedureConcurrencyRow { + ctx.db.procedure_concurrency_row().insert(ProcedureConcurrencyRow { insertion_order: 0, insertion_context: "reducer".into(), }); @@ -82,7 +80,7 @@ struct ScheduledReducerRow { #[reducer] fn insert_scheduled_reducer(ctx: &ReducerContext, _schedule: ScheduledReducerRow) { - ctx.db().procedure_concurrency_row().insert(ProcedureConcurrencyRow { + ctx.db.procedure_concurrency_row().insert(ProcedureConcurrencyRow { insertion_order: 0, insertion_context: "scheduled_reducer".into(), }); @@ -130,11 +128,11 @@ fn scheduled_procedure_sleep_between_inserts(ctx: &mut ProcedureContext, _schedu #[reducer] fn schedule_procedure_then_reducer(ctx: &ReducerContext) { - ctx.db().scheduled_procedure_row().insert(ScheduledProcedureRow { + ctx.db.scheduled_procedure_row().insert(ScheduledProcedureRow { scheduled_id: 0, scheduled_at: ctx.timestamp.into(), }); - ctx.db().scheduled_reducer_row().insert(ScheduledReducerRow { + ctx.db.scheduled_reducer_row().insert(ScheduledReducerRow { scheduled_id: 0, scheduled_at: (ctx.timestamp + Duration::from_secs(2)).into(), }); diff --git a/modules/sdk-test-procedure/src/lib.rs b/modules/sdk-test-procedure/src/lib.rs index a8befa92e3b..5eb2f848ad5 100644 --- a/modules/sdk-test-procedure/src/lib.rs +++ b/modules/sdk-test-procedure/src/lib.rs @@ -1,6 +1,6 @@ use spacetimedb::{ - duration, procedure, reducer, table, DbContext, ProcedureContext, ReducerContext, ScheduleAt, SpacetimeType, Table, - Timestamp, TxContext, Uuid, + duration, procedure, reducer, table, ProcedureContext, ReducerContext, ScheduleAt, SpacetimeType, Table, Timestamp, + TxContext, Uuid, }; #[derive(SpacetimeType)] @@ -108,7 +108,7 @@ fn insert_with_tx_rollback(ctx: &mut ProcedureContext) { #[reducer] fn schedule_proc(ctx: &ReducerContext) { // Schedule the procedure to run in 1s. - ctx.db().scheduled_proc_table().insert(ScheduledProcTable { + ctx.db.scheduled_proc_table().insert(ScheduledProcTable { scheduled_id: 0, scheduled_at: duration!("1000ms").into(), // Store the timestamp at which this reducer was called. @@ -136,7 +136,7 @@ fn scheduled_proc(ctx: &mut ProcedureContext, data: ScheduledProcTable) { let ScheduledProcTable { reducer_ts, x, y, .. } = data; let procedure_ts = ctx.timestamp; ctx.with_tx(|ctx| { - ctx.db().proc_inserts_into().insert(ProcInsertsInto { + ctx.db.proc_inserts_into().insert(ProcInsertsInto { reducer_ts, procedure_ts, x,