|
1 | 1 | from .. import Smoketest, random_string |
2 | 2 |
|
3 | | - |
4 | 3 | class Views(Smoketest): |
5 | 4 | MODULE_CODE = """ |
6 | 5 | use spacetimedb::ViewContext; |
@@ -95,3 +94,158 @@ def test_fail_publish_wrong_return_type(self): |
95 | 94 |
|
96 | 95 | with self.assertRaises(Exception): |
97 | 96 | self.publish_module(name) |
| 97 | + |
| 98 | +class SqlViews(Smoketest): |
| 99 | + MODULE_CODE = """ |
| 100 | +use spacetimedb::{AnonymousViewContext, ReducerContext, Table, ViewContext}; |
| 101 | +
|
| 102 | +#[derive(Copy, Clone)] |
| 103 | +#[spacetimedb::table(name = player_state)] |
| 104 | +#[spacetimedb::table(name = player_level)] |
| 105 | +pub struct PlayerState { |
| 106 | + #[primary_key] |
| 107 | + id: u64, |
| 108 | + #[index(btree)] |
| 109 | + level: u64, |
| 110 | +} |
| 111 | +
|
| 112 | +#[spacetimedb::reducer] |
| 113 | +pub fn add_player_level(ctx: &ReducerContext, id: u64, level: u64) { |
| 114 | + ctx.db.player_level().insert(PlayerState { id, level }); |
| 115 | +} |
| 116 | +
|
| 117 | +#[spacetimedb::view(name = my_player_and_level, public)] |
| 118 | +pub fn my_player_and_level(ctx: &AnonymousViewContext) -> Option<PlayerState> { |
| 119 | + ctx.db.player_level().id().find(0) |
| 120 | +} |
| 121 | +
|
| 122 | +#[spacetimedb::view(name = player_and_level, public)] |
| 123 | +pub fn player_and_level(ctx: &AnonymousViewContext) -> Vec<PlayerState> { |
| 124 | + ctx.db.player_level().level().filter(2u64).collect() |
| 125 | +} |
| 126 | +
|
| 127 | +#[spacetimedb::view(name = player, public)] |
| 128 | +pub fn player(ctx: &ViewContext) -> Option<PlayerState> { |
| 129 | + log::info!("player view called"); |
| 130 | + ctx.db.player_state().id().find(42) |
| 131 | +} |
| 132 | +
|
| 133 | +#[spacetimedb::view(name = player_none, public)] |
| 134 | +pub fn player_none(_ctx: &ViewContext) -> Option<PlayerState> { |
| 135 | + None |
| 136 | +} |
| 137 | +
|
| 138 | +#[spacetimedb::view(name = player_vec, public)] |
| 139 | +pub fn player_vec(ctx: &ViewContext) -> Vec<PlayerState> { |
| 140 | + let first = ctx.db.player_state().id().find(42).unwrap(); |
| 141 | + let second = PlayerState { id: 7, level: 3 }; |
| 142 | + vec![first, second] |
| 143 | +} |
| 144 | +""" |
| 145 | + |
| 146 | + def assertSql(self, sql, expected): |
| 147 | + self.maxDiff = None |
| 148 | + sql_out = self.spacetime("sql", self.database_identity, sql) |
| 149 | + sql_out = "\n".join([line.rstrip() for line in sql_out.splitlines()]) |
| 150 | + expected = "\n".join([line.rstrip() for line in expected.splitlines()]) |
| 151 | + |
| 152 | + self.assertMultiLineEqual(sql_out, expected) |
| 153 | + |
| 154 | + def insert_initial_data(self): |
| 155 | + self.spacetime( |
| 156 | + "sql", |
| 157 | + self.database_identity, |
| 158 | + """\ |
| 159 | +INSERT INTO player_state (id, level) VALUES (42, 7); |
| 160 | +""", |
| 161 | + ) |
| 162 | + |
| 163 | + def call_player_view(self): |
| 164 | + |
| 165 | + self.assertSql("SELECT * FROM player", """\ |
| 166 | + id | level |
| 167 | +----+------- |
| 168 | + 42 | 7 |
| 169 | +""") |
| 170 | + |
| 171 | + def test_http_sql(self): |
| 172 | + """This test asserts that views can be queried over HTTP SQL""" |
| 173 | + self.insert_initial_data() |
| 174 | + |
| 175 | + self.call_player_view() |
| 176 | + |
| 177 | + self.assertSql("SELECT * FROM player_none", """\ |
| 178 | + id | level |
| 179 | +----+------- |
| 180 | +""") |
| 181 | + |
| 182 | + self.assertSql("SELECT * FROM player_vec", """\ |
| 183 | + id | level |
| 184 | +----+------- |
| 185 | + 42 | 7 |
| 186 | + 7 | 3 |
| 187 | +""") |
| 188 | + |
| 189 | + # test is prefixed with 'a' to ensure it runs before any other tests, |
| 190 | + # since it relies on log capturing starting from an empty log. |
| 191 | + def test_a_view_materialization(self): |
| 192 | + """This test asserts whether views are materialized correctly""" |
| 193 | + self.insert_initial_data() |
| 194 | + player_called_log = "player view called" |
| 195 | + |
| 196 | + self.assertNotIn(player_called_log, self.logs(100)) |
| 197 | + |
| 198 | + self.call_player_view() |
| 199 | + #On first call, the view is evaluated |
| 200 | + self.assertIn(player_called_log, self.logs(100)) |
| 201 | + |
| 202 | + self.call_player_view() |
| 203 | + #On second call, the view is cached |
| 204 | + logs = self.logs(100) |
| 205 | + self.assertEqual(logs.count(player_called_log), 1) |
| 206 | + |
| 207 | + # insert to cause cache invalidation |
| 208 | + self.spacetime( |
| 209 | + "sql", |
| 210 | + self.database_identity, |
| 211 | + """\ |
| 212 | +INSERT INTO player_state (id, level) VALUES (22, 8); |
| 213 | +""", |
| 214 | + ) |
| 215 | + |
| 216 | + self.call_player_view() |
| 217 | + #On third call, after invalidation, the view is evaluated again |
| 218 | + logs = self.logs(100) |
| 219 | + self.assertEqual(logs.count(player_called_log), 2) |
| 220 | + |
| 221 | + def test_query_anonymous_view_reducer(self): |
| 222 | + """Tests that anonymous views are updated for reducers""" |
| 223 | + self.call("add_player_level", 0, 1) |
| 224 | + self.call("add_player_level", 1, 2) |
| 225 | + |
| 226 | + self.assertSql("SELECT * FROM my_player_and_level", """\ |
| 227 | + id | level |
| 228 | +----+------- |
| 229 | + 0 | 1 |
| 230 | +""") |
| 231 | + |
| 232 | + self.assertSql("SELECT * FROM player_and_level", """\ |
| 233 | + id | level |
| 234 | +----+------- |
| 235 | + 1 | 2 |
| 236 | +""") |
| 237 | + |
| 238 | + self.call("add_player_level", 2, 2) |
| 239 | + |
| 240 | + self.assertSql("SELECT * FROM player_and_level", """\ |
| 241 | + id | level |
| 242 | +----+------- |
| 243 | + 1 | 2 |
| 244 | + 2 | 2 |
| 245 | +""") |
| 246 | + |
| 247 | + self.assertSql("SELECT * FROM player_and_level WHERE id = 2", """\ |
| 248 | + id | level |
| 249 | +----+------- |
| 250 | + 2 | 2 |
| 251 | +""") |
0 commit comments