Skip to content

Commit bccf462

Browse files
[Rust] Extend view table accessors with count
1 parent 61be6e6 commit bccf462

9 files changed

Lines changed: 341 additions & 143 deletions

File tree

crates/bindings-macro/src/table.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,6 +1302,11 @@ pub(crate) fn table_impl(mut args: TableArgs, item: &syn::DeriveInput) -> syn::R
13021302
}
13031303

13041304
impl #viewhandle_ident {
1305+
#[inline]
1306+
pub fn count(&self) -> u64 {
1307+
spacetimedb::table::count::<#tablehandle_ident>()
1308+
}
1309+
13051310
#(#index_accessors_ro)*
13061311
}
13071312

crates/bindings/src/table.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pub trait Table: TableInternal + ExplicitNames {
2727
/// even though those modifications have not yet been committed or broadcast to clients.
2828
/// This applies generally to insertions, deletions, updates, and iteration as well.
2929
fn count(&self) -> u64 {
30-
sys::datastore_table_row_count(Self::table_id()).expect("datastore_table_row_count() call failed")
30+
count::<Self>()
3131
}
3232

3333
/// Iterate over all rows of the table.
@@ -119,6 +119,12 @@ pub trait Table: TableInternal + ExplicitNames {
119119
fn integrate_generated_columns(row: &mut Self::Row, generated_cols: &[u8]);
120120
}
121121

122+
#[doc(hidden)]
123+
#[inline]
124+
pub fn count<Tbl: Table>() -> u64 {
125+
sys::datastore_table_row_count(Tbl::table_id()).expect("datastore_table_row_count() call failed")
126+
}
127+
122128
#[doc(hidden)]
123129
pub trait TableInternal: Sized {
124130
const TABLE_NAME: &'static str;

crates/bindings/tests/ui/views.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,6 @@ fn view_handle_no_iter(ctx: &ReducerContext) {
1515
for _ in read_only.db.test().iter() {}
1616
}
1717

18-
#[reducer]
19-
fn view_handle_no_count(ctx: &ReducerContext) {
20-
let read_only = ctx.as_read_only();
21-
// Should not compile: ViewHandle does not expose `count()`
22-
let _ = read_only.db.test().count();
23-
}
24-
2518
#[reducer]
2619
fn view_handle_no_insert(ctx: &ReducerContext) {
2720
let read_only = ctx.as_read_only();

crates/bindings/tests/ui/views.stderr

Lines changed: 111 additions & 135 deletions
Large diffs are not rendered by default.

crates/smoketests/modules/Cargo.lock

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

crates/smoketests/modules/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ members = [
2525
"views-subscribe",
2626
"views-query",
2727
"views-callable",
28+
"views-count",
2829

2930
# Security and permissions
3031
"rls",
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "smoketest-module-views-count"
3+
version = "0.1.0"
4+
edition = "2021"
5+
publish = false
6+
7+
[lib]
8+
crate-type = ["cdylib"]
9+
10+
[dependencies]
11+
spacetimedb.workspace = true
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use spacetimedb::{reducer, table, view, AnonymousViewContext};
2+
use spacetimedb::{ReducerContext, SpacetimeType, Table, ViewContext};
3+
4+
#[table(accessor = item)]
5+
pub struct Item {
6+
#[primary_key]
7+
id: u32,
8+
value: u32,
9+
}
10+
11+
#[derive(SpacetimeType)]
12+
pub struct ItemCount {
13+
count: u64,
14+
}
15+
16+
#[view(accessor = sender_table_count, public)]
17+
pub fn sender_table_count(ctx: &ViewContext) -> Option<ItemCount> {
18+
Some(ItemCount {
19+
count: ctx.db.item().count(),
20+
})
21+
}
22+
23+
#[view(accessor = anon_table_count, public)]
24+
pub fn anon_table_count(ctx: &AnonymousViewContext) -> Option<ItemCount> {
25+
Some(ItemCount {
26+
count: ctx.db.item().count(),
27+
})
28+
}
29+
30+
#[reducer]
31+
pub fn insert_item(ctx: &ReducerContext, id: u32, value: u32) {
32+
ctx.db.item().insert(Item { id, value });
33+
}
34+
35+
#[reducer]
36+
pub fn replace_item(ctx: &ReducerContext, id: u32, value: u32) {
37+
ctx.db.item().id().delete(&id);
38+
ctx.db.item().insert(Item { id, value });
39+
}
40+
41+
#[reducer]
42+
pub fn delete_item(ctx: &ReducerContext, id: u32) {
43+
ctx.db.item().id().delete(&id);
44+
}

crates/smoketests/tests/smoketests/views.rs

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,109 @@ public static partial class Module
7777
}
7878
"#;
7979

80+
const CS_COUNT_VIEW_MODULE: &str = r#"using SpacetimeDB;
81+
82+
[SpacetimeDB.Type]
83+
public partial struct ItemCount
84+
{
85+
public ulong count;
86+
}
87+
88+
public static partial class Module
89+
{
90+
[Table(Accessor = "item", Public = true)]
91+
public partial struct Item
92+
{
93+
[PrimaryKey]
94+
public uint id;
95+
public uint value;
96+
}
97+
98+
[View(Accessor = "sender_table_count", Public = true)]
99+
public static ItemCount? sender_table_count(ViewContext ctx)
100+
{
101+
return new ItemCount { count = ctx.Db.item.Count };
102+
}
103+
104+
[View(Accessor = "anon_table_count", Public = true)]
105+
public static ItemCount? anon_table_count(AnonymousViewContext ctx)
106+
{
107+
return new ItemCount { count = ctx.Db.item.Count };
108+
}
109+
110+
[Reducer]
111+
public static void insert_item(ReducerContext ctx, uint id, uint value)
112+
{
113+
ctx.Db.item.Insert(new Item { id = id, value = value });
114+
}
115+
116+
[Reducer]
117+
public static void replace_item(ReducerContext ctx, uint id, uint value)
118+
{
119+
ctx.Db.item.id.Delete(id);
120+
ctx.Db.item.Insert(new Item { id = id, value = value });
121+
}
122+
123+
[Reducer]
124+
public static void delete_item(ReducerContext ctx, uint id)
125+
{
126+
ctx.Db.item.id.Delete(id);
127+
}
128+
}
129+
"#;
130+
131+
const TS_COUNT_VIEW_MODULE: &str = r#"import { schema, t, table } from "spacetimedb/server";
132+
133+
const item = table(
134+
{ name: "item" },
135+
{
136+
id: t.u32().primaryKey(),
137+
value: t.u32(),
138+
}
139+
);
140+
141+
const itemCount = t.object("ItemCountRow", {
142+
count: t.u64(),
143+
});
144+
145+
const spacetimedb = schema({ item });
146+
export default spacetimedb;
147+
148+
export const sender_table_count = spacetimedb.view(
149+
{ public: true },
150+
t.option(itemCount),
151+
ctx => ({ count: ctx.db.item.count() })
152+
);
153+
154+
export const anon_table_count = spacetimedb.anonymousView(
155+
{ public: true },
156+
t.option(itemCount),
157+
ctx => ({ count: ctx.db.item.count() })
158+
);
159+
160+
export const insert_item = spacetimedb.reducer(
161+
{ id: t.u32(), value: t.u32() },
162+
(ctx, { id, value }) => {
163+
ctx.db.item.insert({ id, value });
164+
}
165+
);
166+
167+
export const replace_item = spacetimedb.reducer(
168+
{ id: t.u32(), value: t.u32() },
169+
(ctx, { id, value }) => {
170+
ctx.db.item.id.delete(id);
171+
ctx.db.item.insert({ id, value });
172+
}
173+
);
174+
175+
export const delete_item = spacetimedb.reducer(
176+
{ id: t.u32() },
177+
(ctx, { id }) => {
178+
ctx.db.item.id.delete(id);
179+
}
180+
);
181+
"#;
182+
80183
fn project_fields(events: Vec<Value>, view_name: &str, projected_fields: &[&str]) -> Vec<Value> {
81184
let project_row = |row: &Value| {
82185
if projected_fields.is_empty() {
@@ -115,6 +218,30 @@ fn project_fields(events: Vec<Value>, view_name: &str, projected_fields: &[&str]
115218
.collect()
116219
}
117220

221+
fn assert_count_view_refresh_behavior(test: &Smoketest, view_name: &str, id: &str, value: &str, updated_value: &str) {
222+
let query = format!("select * from {view_name}");
223+
let sub = test.subscribe_background(&[&query], 2).unwrap();
224+
225+
test.call("insert_item", &[id, value]).unwrap();
226+
test.call("replace_item", &[id, updated_value]).unwrap();
227+
test.call("delete_item", &[id]).unwrap();
228+
229+
let events = sub.collect().unwrap();
230+
let projection = project_fields(events, view_name, &["count"]);
231+
assert_eq!(
232+
serde_json::json!(projection),
233+
serde_json::json!([
234+
{view_name: {"deletes": [{"count": 0}], "inserts": [{"count": 1}]}},
235+
{view_name: {"deletes": [{"count": 1}], "inserts": [{"count": 0}]}}
236+
])
237+
);
238+
}
239+
240+
fn assert_all_count_view_refreshes(test: &Smoketest) {
241+
assert_count_view_refresh_behavior(test, "sender_table_count", "1", "10", "11");
242+
assert_count_view_refresh_behavior(test, "anon_table_count", "2", "20", "21");
243+
}
244+
118245
/// Tests that views populate the st_view_* system tables
119246
#[test]
120247
fn test_st_view_tables() {
@@ -553,6 +680,34 @@ fn test_typescript_procedure_triggers_subscription_updates() {
553680
);
554681
}
555682

683+
#[test]
684+
fn test_rust_count_view_subscription_refreshes() {
685+
let test = Smoketest::builder().precompiled_module("views-count").build();
686+
assert_all_count_view_refreshes(&test);
687+
}
688+
689+
#[test]
690+
fn test_csharp_count_view_subscription_refreshes() {
691+
require_dotnet!();
692+
693+
let mut test = Smoketest::builder().autopublish(false).build();
694+
test.publish_csharp_module_source("views-count-csharp", "views-count-csharp", CS_COUNT_VIEW_MODULE)
695+
.unwrap();
696+
697+
assert_all_count_view_refreshes(&test);
698+
}
699+
700+
#[test]
701+
fn test_typescript_count_view_subscription_refreshes() {
702+
require_pnpm!();
703+
704+
let mut test = Smoketest::builder().autopublish(false).build();
705+
test.publish_typescript_module_source("views-count-typescript", "views-count-typescript", TS_COUNT_VIEW_MODULE)
706+
.unwrap();
707+
708+
assert_all_count_view_refreshes(&test);
709+
}
710+
556711
#[test]
557712
fn test_disconnect_does_not_break_sender_view() {
558713
let test = Smoketest::builder().precompiled_module("views-sql").build();

0 commit comments

Comments
 (0)