Skip to content

Commit c2db64d

Browse files
SQL smoketests for views (#3616)
# Description of Changes This patch tests calling, updating, and materialization of views through the SQL api. # API and ABI breaking changes None # Expected complexity level and risk 1.5 # Testing Smoketests --------- Signed-off-by: joshua-spacetime <[email protected]> Co-authored-by: joshua-spacetime <[email protected]>
1 parent e0b8e6f commit c2db64d

File tree

4 files changed

+162
-30
lines changed

4 files changed

+162
-30
lines changed

crates/core/src/db/relational_db.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ use spacetimedb_durability as durability;
3636
use spacetimedb_lib::bsatn::ToBsatn;
3737
use spacetimedb_lib::db::auth::StAccess;
3838
use spacetimedb_lib::db::raw_def::v9::{btree, RawModuleDefV9Builder, RawSql};
39-
use spacetimedb_lib::de::DeserializeSeed as _;
39+
use spacetimedb_lib::de::DeserializeSeed;
4040
use spacetimedb_lib::st_var::StVarValue;
4141
use spacetimedb_lib::Identity;
4242
use spacetimedb_lib::{bsatn, ConnectionId};

crates/core/src/host/module_host.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1543,9 +1543,11 @@ impl ModuleHost {
15431543
let view_name = st_view_row.view_name;
15441544
let view_id = st_view_row.view_id;
15451545
let table_id = st_view_row.table_id.ok_or(ViewCallError::TableDoesNotExist(view_id))?;
1546+
let is_anonymous = st_view_row.is_anonymous;
1547+
let sender = if is_anonymous { None } else { Some(caller) };
15461548
if !tx.is_view_materialized(view_id, ArgId::SENTINEL, caller)? {
15471549
tx = self
1548-
.call_view(tx, &view_name, view_id, table_id, Nullary, caller, Some(caller))
1550+
.call_view(tx, &view_name, view_id, table_id, Nullary, caller, sender)
15491551
.await?
15501552
.tx;
15511553
}

crates/datastore/src/locking_tx_datastore/mut_tx.rs

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@ use crate::{
1313
error::ViewError,
1414
system_tables::{
1515
system_tables, ConnectionIdViaU128, IdentityViaU256, StConnectionCredentialsFields, StConnectionCredentialsRow,
16-
StViewArgFields, StViewArgRow, StViewColumnFields, StViewFields, StViewParamFields, StViewParamRow,
17-
StViewSubFields, StViewSubRow, ST_CONNECTION_CREDENTIALS_ID, ST_VIEW_ARG_ID, ST_VIEW_COLUMN_ID, ST_VIEW_ID,
18-
ST_VIEW_PARAM_ID, ST_VIEW_SUB_ID,
16+
StViewColumnFields, StViewFields, StViewParamFields, StViewParamRow, StViewSubFields, StViewSubRow,
17+
ST_CONNECTION_CREDENTIALS_ID, ST_VIEW_COLUMN_ID, ST_VIEW_ID, ST_VIEW_PARAM_ID, ST_VIEW_SUB_ID,
1918
},
2019
};
2120
use crate::{
@@ -30,15 +29,14 @@ use crate::{
3029
};
3130
use crate::{execution_context::ExecutionContext, system_tables::StViewColumnRow};
3231
use crate::{execution_context::Workload, system_tables::StViewRow};
33-
use bytes::Bytes;
3432
use core::ops::RangeBounds;
3533
use core::{cell::RefCell, mem};
3634
use core::{iter, ops::Bound};
3735
use smallvec::SmallVec;
3836
use spacetimedb_data_structures::map::{HashMap, HashSet, IntMap};
3937
use spacetimedb_durability::TxOffset;
4038
use spacetimedb_execution::{dml::MutDatastore, Datastore, DeltaStore, Row};
41-
use spacetimedb_lib::{bsatn::ToBsatn as _, db::raw_def::v9::RawSql, metrics::ExecutionMetrics, Timestamp};
39+
use spacetimedb_lib::{db::raw_def::v9::RawSql, metrics::ExecutionMetrics, Timestamp};
4240
use spacetimedb_lib::{
4341
db::{auth::StAccess, raw_def::SEQUENCE_ALLOCATION_STEP},
4442
ConnectionId, Identity,
@@ -50,7 +48,6 @@ use spacetimedb_sats::{
5048
bsatn::{self, to_writer, DecodeError, Deserializer},
5149
de::{DeserializeSeed, WithBound},
5250
memory_usage::MemoryUsage,
53-
product,
5451
ser::Serialize,
5552
AlgebraicType, AlgebraicValue, ProductType, ProductValue, WithTypespace,
5653
};
@@ -2049,27 +2046,6 @@ impl MutTxId {
20492046
Ok(())
20502047
}
20512048

2052-
/// Get or insert view argument into `ST_VIEW_ARG_ID`.
2053-
pub fn get_or_insert_st_view_arg(&mut self, args: &Bytes) -> Result<u64> {
2054-
let bytes_av = AlgebraicValue::Bytes(args.to_vec().into());
2055-
let mut rows = self.iter_by_col_eq(ST_VIEW_ARG_ID, [StViewArgFields::Bytes], &bytes_av)?;
2056-
2057-
// Extract the first matching `arg_id`, if any.
2058-
if let Some(res) = rows.next() {
2059-
let row = StViewArgRow::try_from(res).expect("valid StViewArgRow");
2060-
return Ok(row.id);
2061-
}
2062-
2063-
let view_arg_bytes = product![0u64, bytes_av]
2064-
.to_bsatn_vec()
2065-
.expect("StViewArgRow serialization to never fail");
2066-
2067-
let (_, view_arg_row, _) = self.insert_via_serialize_bsatn(ST_VIEW_ARG_ID, &view_arg_bytes)?;
2068-
let StViewArgRow { id: arg_id, .. } = view_arg_row.collapse().try_into().expect("valid StViewArgRow");
2069-
2070-
Ok(arg_id)
2071-
}
2072-
20732049
/// Lookup a row in `st_view` by its primary key
20742050
fn st_view_row(&self, view_id: ViewId) -> Result<Option<StViewRow>> {
20752051
self.iter_by_col_eq(ST_VIEW_ID, col_list![StViewFields::ViewId], &view_id.into())?

smoketests/tests/views.py

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from .. import Smoketest, random_string
22

3-
43
class Views(Smoketest):
54
MODULE_CODE = """
65
use spacetimedb::ViewContext;
@@ -95,3 +94,158 @@ def test_fail_publish_wrong_return_type(self):
9594

9695
with self.assertRaises(Exception):
9796
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

Comments
 (0)