@@ -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+
80183fn 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]
120247fn 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]
557712fn test_disconnect_does_not_break_sender_view ( ) {
558713 let test = Smoketest :: builder ( ) . precompiled_module ( "views-sql" ) . build ( ) ;
0 commit comments