use powdb_query::ast::Statement;
use powdb_query::executor::Engine;
use powdb_query::lexer::lex;
use powdb_query::parser::parse;
use powdb_query::result::QueryResult;
use powdb_storage::types::Value;
fn temp_dir(name: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!(
"powdb_matview_roundtrip_{name}_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
))
}
fn exec(engine: &mut Engine, query: &str) -> QueryResult {
engine
.execute_powql(query)
.unwrap_or_else(|e| panic!("failed to execute `{query}`: {e}"))
}
fn col_i64(engine: &mut Engine, query: &str) -> Vec<i64> {
match exec(engine, query) {
QueryResult::Rows { rows, .. } => {
let mut out: Vec<i64> = rows
.iter()
.map(|r| match r.as_slice() {
[Value::Int(n)] => *n,
other => panic!("expected a single int column, got {other:?}"),
})
.collect();
out.sort_unstable();
out
}
other => panic!("expected rows, got {other:?}"),
}
}
fn stored_source(query: &str) -> String {
match parse(&format!("materialize V_stored as {query}"))
.unwrap_or_else(|e| panic!("failed to parse a materialize over `{query}`: {e}"))
{
Statement::CreateView(v) => v.query_text,
other => panic!("expected CreateView, got {other:?}"),
}
}
fn assert_source_relexes_identically(query: &str) {
let stored = stored_source(query);
let want = lex(query).unwrap_or_else(|e| panic!("failed to lex `{query}`: {}", e.message));
let got = lex(&stored)
.unwrap_or_else(|e| panic!("stored source `{stored}` does not lex: {}", e.message));
assert_eq!(
got, want,
"stored view source `{stored}` re-lexes differently from `{query}`"
);
}
#[test]
fn test_matview_backslash_literal_matches_direct_query() {
let dir = temp_dir("backslash");
std::fs::create_dir_all(&dir).unwrap();
let mut engine = Engine::new(&dir).unwrap();
exec(
&mut engine,
"type U { required id: int, required name: str }",
);
exec(
&mut engine,
r#"insert U { id := 1, name := "back\\slash" }"#,
);
exec(&mut engine, r#"insert U { id := 2, name := "plain" }"#);
let direct = r#"U filter .name = "back\\slash" { .id }"#;
assert_eq!(
col_i64(&mut engine, direct),
vec![1],
"the direct query must match the backslash row"
);
exec(&mut engine, &format!("materialize W as {direct}"));
assert_eq!(
col_i64(&mut engine, "W"),
vec![1],
"the view must return exactly what its defining query returns"
);
exec(&mut engine, "refresh W");
assert_eq!(
col_i64(&mut engine, "W"),
vec![1],
"the view must survive an explicit refresh (source text is re-lexed)"
);
drop(engine);
let mut engine = Engine::new(&dir).unwrap();
assert_eq!(
col_i64(&mut engine, "W"),
vec![1],
"the view must survive a reopen (source text is re-read from the catalog)"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn test_matview_escaped_quote_literal_matches_direct_query() {
let dir = temp_dir("escaped_quote");
std::fs::create_dir_all(&dir).unwrap();
let mut engine = Engine::new(&dir).unwrap();
exec(
&mut engine,
"type U { required id: int, required name: str }",
);
exec(
&mut engine,
r#"insert U { id := 1, name := "he said \"hi\"" }"#,
);
exec(&mut engine, r#"insert U { id := 2, name := "plain" }"#);
let direct = r#"U filter .name = "he said \"hi\"" { .id }"#;
assert_eq!(col_i64(&mut engine, direct), vec![1]);
exec(&mut engine, &format!("materialize V as {direct}"));
assert_eq!(col_i64(&mut engine, "V"), vec![1]);
exec(&mut engine, "refresh V");
assert_eq!(col_i64(&mut engine, "V"), vec![1]);
drop(engine);
let mut engine = Engine::new(&dir).unwrap();
assert_eq!(col_i64(&mut engine, "V"), vec![1]);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn test_matview_over_reserved_word_table_name() {
let dir = temp_dir("reserved_word");
std::fs::create_dir_all(&dir).unwrap();
let mut engine = Engine::new(&dir).unwrap();
exec(
&mut engine,
"type `order` { required id: int, required qty: int }",
);
exec(&mut engine, "insert `order` { id := 1, qty := 5 }");
exec(&mut engine, "insert `order` { id := 2, qty := 1 }");
let direct = "`order` filter .qty > 3 { .id }";
assert_eq!(col_i64(&mut engine, direct), vec![1]);
exec(&mut engine, &format!("materialize BigOrders as {direct}"));
assert_eq!(col_i64(&mut engine, "BigOrders"), vec![1]);
exec(&mut engine, "refresh BigOrders");
assert_eq!(col_i64(&mut engine, "BigOrders"), vec![1]);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn test_stored_source_relexes_identically_for_awkward_literals() {
let cases = [
r#"U filter .name = "back\\slash" { .id }"#,
r#"U filter .name = "he said \"hi\"" { .id }"#,
r#"U filter .name = "line\nbreak" { .id }"#,
r#"U filter .name = "tab\there" { .id }"#,
r#"U filter .name = "\\\"" { .id }"#,
r#"U filter .name = "" { .id }"#,
r#"U filter .name = "} filter .x = 1 #" { .id }"#,
"`order` filter .qty > 3 { .id }",
"`true` { .id }",
"`column name` { .id }",
"U filter .`order` = 1 { .id }",
"U filter .`field name` = 1 { .id }",
"U { .`select` }",
"U filter .score = 3.0 { .id }",
"U filter .score = -0.0 { .id }",
"U filter .score = 1.5 { .id }",
"U filter .score = -2.0 { .id }",
"U filter .n = -9223372036854775808 { .id }",
"U filter .n = 9223372036854775807 { .id }",
"U filter .n - 1 = -5 { .id }",
];
for case in cases {
assert_source_relexes_identically(case);
}
}
mod prop {
use super::assert_source_relexes_identically;
use proptest::prelude::*;
fn literal_payload() -> impl Strategy<Value = String> {
proptest::collection::vec(
prop_oneof![
Just('\\'),
Just('"'),
Just('\n'),
Just('\t'),
Just('\r'),
Just('#'),
Just('`'),
Just('$'),
Just('}'),
Just(' '),
Just('a'),
Just('9'),
Just('é'),
],
0..12usize,
)
.prop_map(|chars| {
let mut out = String::new();
for c in chars {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
other => out.push(other),
}
}
out
})
}
fn quoted_ident() -> impl Strategy<Value = String> {
prop_oneof![
Just("order".to_string()),
Just("filter".to_string()),
Just("true".to_string()),
Just("null".to_string()),
Just("count".to_string()),
Just("column name".to_string()),
Just("1st".to_string()),
Just("a-b".to_string()),
Just("with#hash".to_string()),
Just("with.dot".to_string()),
Just("plain".to_string()),
]
}
proptest! {
#[test]
fn prop_string_literal_source_relexes_identically(payload in literal_payload()) {
assert_source_relexes_identically(&format!(
"U filter .name = \"{payload}\" {{ .id }}"
));
}
#[test]
fn prop_quoted_identifier_source_relexes_identically(
table in quoted_ident(),
field in quoted_ident(),
) {
assert_source_relexes_identically(&format!(
"`{table}` filter .`{field}` = 1 {{ .`{field}` }}"
));
}
#[test]
fn prop_numeric_literal_source_relexes_identically(
int in any::<i64>(),
float in prop_oneof![
any::<i32>().prop_map(f64::from),
(-1e18f64..1e18f64),
Just(0.0f64),
Just(-0.0f64),
],
) {
assert_source_relexes_identically(&format!("U filter .n = {int} {{ .id }}"));
let rendered = if float.fract() == 0.0 {
format!("{float:.1}")
} else {
format!("{float}")
};
assert_source_relexes_identically(&format!("U filter .s = {rendered} {{ .id }}"));
}
}
}
mod stored_source_is_not_retroactively_fixed {
use super::{col_i64, exec, temp_dir};
use powdb_query::executor::Engine;
use powdb_query::result::QueryError;
use powdb_storage::view::{ViewDef, ViewRegistry};
const LEGACY_BROKEN_SOURCE: &str = r#"U filter .name = "he said "hi"" { .id }"#;
fn plant_broken_source(dir: &std::path::Path, view: &str) {
let mut registry = ViewRegistry::open(dir).expect("the view registry is readable");
registry.unregister(view).expect("the view is registered");
registry
.register(ViewDef {
name: view.to_string(),
query: LEGACY_BROKEN_SOURCE.to_string(),
depends_on: Vec::new(),
dirty: false,
})
.expect("the view can be re-registered");
}
fn fixture(tag: &str) -> std::path::PathBuf {
let dir = temp_dir(tag);
std::fs::create_dir_all(&dir).expect("the fixture directory is creatable");
let mut engine = Engine::new(&dir).expect("engine opens over a fresh temp dir");
exec(
&mut engine,
"type U { required id: int, required name: str }",
);
exec(&mut engine, r#"insert U { id := 1, name := "a" }"#);
exec(&mut engine, r#"insert U { id := 2, name := "b" }"#);
exec(&mut engine, "materialize V as U filter .id > 0 { .id }");
assert_eq!(
col_i64(&mut engine, "V"),
vec![1, 2],
"the fixture view must be correct before its source is broken"
);
drop(engine);
plant_broken_source(&dir, "V");
dir
}
fn assert_names_the_view(error: &QueryError) {
let text = error.to_string();
assert!(
matches!(error, QueryError::ViewError(_)),
"a view whose source cannot be read must report a view error, got: {error:?}"
);
assert!(
text.contains('V') && text.contains("materialize"),
"the error must name the view and say how to repair it, got: {text}"
);
}
#[test]
fn reading_a_view_with_an_unreadable_source_errors_instead_of_serving_stale_rows() {
let dir = fixture("stale_read");
let mut engine = Engine::new(&dir).expect("engine reopens the planted database");
exec(&mut engine, r#"insert U { id := 3, name := "c" }"#);
let error = engine
.execute_powql("V")
.expect_err("a view whose stored source cannot be parsed must not answer");
assert_names_the_view(&error);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn filtering_a_view_with_an_unreadable_source_errors() {
let dir = fixture("stale_filter");
let mut engine = Engine::new(&dir).expect("engine reopens the planted database");
exec(&mut engine, r#"insert U { id := 3, name := "c" }"#);
let error = engine
.execute_powql("V filter .id > 0 { .id }")
.expect_err("a view whose stored source cannot be parsed must not answer");
assert_names_the_view(&error);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn refreshing_a_view_with_an_unreadable_source_errors() {
let dir = fixture("stale_refresh");
let mut engine = Engine::new(&dir).expect("engine reopens the planted database");
let error = engine
.execute_powql("refresh V")
.expect_err("refreshing an unreadable source must not silently succeed");
assert_names_the_view(&error);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_view_with_an_unreadable_source_can_be_dropped_and_recreated() {
let dir = fixture("stale_repair");
let mut engine = Engine::new(&dir).expect("engine reopens the planted database");
exec(&mut engine, r#"insert U { id := 3, name := "c" }"#);
exec(&mut engine, "drop view V");
exec(&mut engine, "materialize V as U filter .id > 0 { .id }");
assert_eq!(
col_i64(&mut engine, "V"),
vec![1, 2, 3],
"the re-created view must answer its own query over the current rows"
);
exec(&mut engine, r#"insert U { id := 4, name := "d" }"#);
assert_eq!(
col_i64(&mut engine, "V"),
vec![1, 2, 3, 4],
"the re-created view must be dirtied by writes to its base table again"
);
std::fs::remove_dir_all(&dir).ok();
}
}