Skip to content

Commit c4bf7b1

Browse files
Add primary key support for procedural views to rust and ts modules
1 parent 0305a24 commit c4bf7b1

50 files changed

Lines changed: 3113 additions & 122 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ members = [
5050
"modules/sdk-test-view",
5151
"modules/sdk-test-case-conversion",
5252
"modules/sdk-test-view-pk",
53+
"modules/sdk-test-procedural-view-pk",
5354
"modules/sdk-test-event-table",
5455
"sdks/rust/tests/test-client",
5556
"sdks/rust/tests/test-counter",
@@ -58,6 +59,7 @@ members = [
5859
"sdks/rust/tests/view-client",
5960
"sdks/rust/tests/case-conversion-client",
6061
"sdks/rust/tests/view-pk-client",
62+
"sdks/rust/tests/procedural-view-pk-client",
6163
"sdks/rust/tests/event-table-client",
6264
"tools/ci",
6365
"tools/upgrade-version",

crates/bindings-macro/src/view.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,49 @@ use crate::util::{check_duplicate_msg, match_meta};
1212
pub(crate) struct ViewArgs {
1313
name: Option<LitStr>,
1414
accessor: Ident,
15+
primary_key: Option<ViewPrimaryKeyArg>,
1516
#[allow(unused)]
1617
public: bool,
1718
}
1819

20+
/// Argument accepted by `#[view(primary_key = ...)]`.
21+
///
22+
/// Both identifier and string literal syntax is supported.
23+
enum ViewPrimaryKeyArg {
24+
Ident(Ident),
25+
Literal(LitStr),
26+
}
27+
28+
impl ViewPrimaryKeyArg {
29+
fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
30+
if input.peek(LitStr) {
31+
input.parse().map(Self::Literal)
32+
} else {
33+
input.parse().map(Self::Ident)
34+
}
35+
}
36+
37+
fn name(&self) -> String {
38+
match self {
39+
Self::Ident(ident) => ident.unraw().to_string(),
40+
Self::Literal(lit) => lit.value(),
41+
}
42+
}
43+
44+
fn ident(&self) -> Option<&Ident> {
45+
match self {
46+
Self::Ident(ident) => Some(ident),
47+
Self::Literal(_) => None,
48+
}
49+
}
50+
}
51+
1952
impl ViewArgs {
2053
/// Parse `#[view(accessor = ..., public)]` where both `name` and `public` are required.
2154
pub(crate) fn parse(input: TokenStream, func_ident: &Ident) -> syn::Result<Self> {
2255
let mut name = None;
2356
let mut accessor = None;
57+
let mut primary_key = None;
2458
let mut public = None;
2559
syn::meta::parser(|meta| {
2660
match_meta!(match meta {
@@ -36,6 +70,10 @@ impl ViewArgs {
3670
check_duplicate_msg(&accessor, &meta, "`accessor` already specified")?;
3771
accessor = Some(meta.value()?.parse()?);
3872
}
73+
sym::primary_key => {
74+
check_duplicate_msg(&primary_key, &meta, "`primary_key` already specified")?;
75+
primary_key = Some(ViewPrimaryKeyArg::parse(meta.value()?)?);
76+
}
3977
});
4078
Ok(())
4179
})
@@ -51,6 +89,7 @@ impl ViewArgs {
5189
.ok_or_else(|| syn::Error::new(Span::call_site(), "views must be `public`, e.g. `#[view(public)]`"))?;
5290
Ok(Self {
5391
name,
92+
primary_key,
5493
public: true,
5594
accessor,
5695
})
@@ -74,6 +113,29 @@ fn extract_impl_query_inner(ty: &syn::Type) -> Option<&syn::Type> {
74113
None
75114
}
76115

116+
/// If `ty` is a supported view return type, returns the row type `T`.
117+
fn extract_view_return_row_type(ty: &syn::Type) -> Option<&syn::Type> {
118+
if let Some(inner) = extract_impl_query_inner(ty) {
119+
return Some(inner);
120+
}
121+
122+
let syn::Type::Path(path) = ty else {
123+
return None;
124+
};
125+
126+
let seg = path.path.segments.last()?;
127+
if !matches!(seg.ident.to_string().as_str(), "Vec" | "Option" | "RawQuery") {
128+
return None;
129+
}
130+
let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
131+
return None;
132+
};
133+
let Some(syn::GenericArgument::Type(inner)) = args.args.first() else {
134+
return None;
135+
};
136+
Some(inner)
137+
}
138+
77139
pub(crate) fn view_impl(args: ViewArgs, original_function: &ItemFn) -> syn::Result<TokenStream> {
78140
let vis = &original_function.vis;
79141
let func_name = &original_function.sig.ident;
@@ -221,6 +283,24 @@ pub(crate) fn view_impl(args: ViewArgs, original_function: &ItemFn) -> syn::Resu
221283
};
222284

223285
let eff_ret_ty = &effective_ret_ty;
286+
let primary_key_column_name = args.primary_key.as_ref().map(ViewPrimaryKeyArg::name);
287+
let primary_key_field_check = args
288+
.primary_key
289+
.as_ref()
290+
.and_then(ViewPrimaryKeyArg::ident)
291+
.zip(extract_view_return_row_type(ret_ty))
292+
.map(|(primary_key, row_ty)| {
293+
quote! {
294+
const _: () = {
295+
fn _assert_view_primary_key_column #lt_params (__row: &#row_ty) #lt_where_clause {
296+
let _ = &__row.#primary_key;
297+
}
298+
};
299+
}
300+
});
301+
let primary_key_column_const = primary_key_column_name
302+
.as_ref()
303+
.map(|primary_key| quote! { const VIEW_PRIMARY_KEY_COLUMNS: &'static [&'static str] = &[#primary_key]; });
224304

225305
Ok(quote! {
226306
#emitted_fn
@@ -243,6 +323,8 @@ pub(crate) fn view_impl(args: ViewArgs, original_function: &ItemFn) -> syn::Resu
243323
}
244324
};
245325

326+
#primary_key_field_check
327+
246328
impl #func_name {
247329
fn invoke(__ctx: #ctx_ty, __args: &[u8]) -> Vec<u8> {
248330
spacetimedb::rt::ViewDispatcher::<#ctx_ty>::invoke::<_, _, _>(#func_name, __ctx, __args)
@@ -266,6 +348,8 @@ pub(crate) fn view_impl(args: ViewArgs, original_function: &ItemFn) -> syn::Resu
266348
/// The pointer for invoking this function
267349
const INVOKE: Self::Invoke = #func_name::invoke;
268350

351+
#primary_key_column_const
352+
269353
/// The return type of this function
270354
fn return_type(
271355
ts: &mut impl spacetimedb::sats::typespace::TypespaceBuilder

crates/bindings-typescript/src/lib/autogen/types.ts

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bindings-typescript/src/lib/schema.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ export class ModuleContext {
194194
schedules: [],
195195
procedures: [],
196196
views: [],
197+
viewPrimaryKeys: [],
197198
lifeCycleReducers: [],
198199
caseConversionPolicy: { tag: 'SnakeCase' },
199200
explicitNames: {
@@ -220,6 +221,12 @@ export class ModuleContext {
220221
push(module.reducers && { tag: 'Reducers', value: module.reducers });
221222
push(module.procedures && { tag: 'Procedures', value: module.procedures });
222223
push(module.views && { tag: 'Views', value: module.views });
224+
push(
225+
module.viewPrimaryKeys && {
226+
tag: 'ViewPrimaryKeys',
227+
value: module.viewPrimaryKeys,
228+
}
229+
);
223230
push(module.schedules && { tag: 'Schedules', value: module.schedules });
224231
push(
225232
module.lifeCycleReducers && {

crates/bindings-typescript/src/server/schema.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
type ViewFn,
3939
type ViewOpts,
4040
type ViewReturnTypeBuilder,
41+
type ValidateViewPrimaryKey,
4142
type Views,
4243
} from './views';
4344
import type { UntypedTableDef } from '../lib/table';
@@ -347,7 +348,11 @@ export class Schema<S extends UntypedSchemaDef> implements ModuleDefaultExport {
347348
view<Ret extends ViewReturnTypeBuilder, F extends ViewFn<S, {}, Ret>>(
348349
opts: ViewOpts,
349350
ret: Ret,
350-
fn: F
351+
fn: F,
352+
// Compile-time-only guard: this rest parameter is `[]` for valid return
353+
// builders, but becomes a required error tuple when a returned row builder
354+
// marks more than one column with `.primaryKey()`.
355+
..._: ValidateViewPrimaryKey<Ret>
351356
): ViewExport<F> {
352357
return makeViewExport<S, {}, Ret, F>(this.#ctx, opts, {}, ret, fn);
353358
}
@@ -380,7 +385,15 @@ export class Schema<S extends UntypedSchemaDef> implements ModuleDefaultExport {
380385
anonymousView<
381386
Ret extends ViewReturnTypeBuilder,
382387
F extends AnonymousViewFn<S, {}, Ret>,
383-
>(opts: ViewOpts, ret: Ret, fn: F): ViewExport<F> {
388+
>(
389+
opts: ViewOpts,
390+
ret: Ret,
391+
fn: F,
392+
// Compile-time-only guard: this rest parameter is `[]` for valid return
393+
// builders, but becomes a required error tuple when a returned row builder
394+
// marks more than one column with `.primaryKey()`.
395+
..._: ValidateViewPrimaryKey<Ret>
396+
): ViewExport<F> {
384397
return makeAnonViewExport<S, {}, Ret, F>(this.#ctx, opts, {}, ret, fn);
385398
}
386399

crates/bindings-typescript/src/server/view.test-d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,24 @@ const spacetime = schema({
7373

7474
const arrayRetValue = t.array(person.rowType);
7575
const optionalPerson = t.option(person.rowType);
76+
const multiplePrimaryKeyRows = t.array(
77+
t.row('MultiplePrimaryKeyRows', {
78+
id: t.u32().primaryKey(),
79+
name: t.string().primaryKey(),
80+
})
81+
);
7682

7783
spacetime.anonymousView({ name: 'v1', public: true }, arrayRetValue, ctx => {
7884
return ctx.from.person.build();
7985
});
8086

87+
// @ts-expect-error views can have at most one primaryKey column on the returned row type.
88+
spacetime.anonymousView(
89+
{ name: 'multiplePrimaryRows', public: true },
90+
multiplePrimaryKeyRows,
91+
() => []
92+
);
93+
8194
spacetime.anonymousView(
8295
{ name: 'optionalPerson', public: true },
8396
optionalPerson,

0 commit comments

Comments
 (0)