pub mod lexer;
pub mod refs;
pub use lexer::{Token, TokenKind, tokenize};
pub use refs::{RawRef, references, unescape_name};
use crate::identity::{NameKey, ObjectId};
use crate::model::TabularDatabase;
use crate::model::index::{ModelIndex, Resolved};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Binding<'a> {
Bound {
raw: RawRef<'a>,
targets: Vec<ObjectId>,
},
Unresolved {
raw: RawRef<'a>,
},
}
impl Binding<'_> {
#[must_use]
pub fn targets(&self) -> &[ObjectId] {
match self {
Binding::Bound { targets, .. } => targets,
Binding::Unresolved { .. } => &[],
}
}
#[must_use]
pub fn is_unresolved(&self) -> bool {
matches!(self, Binding::Unresolved { .. })
}
}
#[must_use]
pub fn bindings<'a>(db: &TabularDatabase, index: &ModelIndex, text: &'a str) -> Vec<Binding<'a>> {
references(text)
.into_iter()
.map(|raw| bind(db, index, raw))
.collect()
}
#[must_use]
pub fn bind<'a>(db: &TabularDatabase, index: &ModelIndex, raw: RawRef<'a>) -> Binding<'a> {
let mut targets = Vec::new();
match &raw {
RawRef::Field {
table: Some(table),
name,
..
} => {
let table = unescape_name(table);
let name = unescape_name(name);
if let Some(handle) = index.resolve_table(&table)
&& let Some(resolved) = db.table(handle)
{
targets.push(ObjectId::Table {
table: NameKey::new(resolved.name.as_str()),
});
}
if let Some(handle) = index.resolve_column(&table, &name)
&& let Some(id) = db.object_id(Resolved::Column(handle))
{
targets.push(id);
}
}
RawRef::Field {
table: None, name, ..
}
| RawRef::ColumnString { name, .. } => {
let name = unescape_name(name);
for handle in index.resolve_columns(&name) {
if let Some(id) = db.object_id(Resolved::Column(handle)) {
targets.push(id);
}
}
}
RawRef::Name { name, .. } => {
let name = unescape_name(name);
if let Some(handle) = index.resolve_table(&name)
&& let Some(table) = db.table(handle)
{
targets.push(ObjectId::Table {
table: NameKey::new(table.name.as_str()),
});
}
if let Some(handle) = index.resolve_expression(&name)
&& let Some(expression) = db.shared_expression(handle)
{
targets.push(ObjectId::Expression {
name: NameKey::new(expression.name.as_str()),
});
}
}
}
if targets.is_empty() {
Binding::Unresolved { raw }
} else {
Binding::Bound { raw, targets }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{Column, Measure, SharedExpression, Table};
fn model() -> TabularDatabase {
let column = |name: &str| Column {
name: name.to_string(),
..Default::default()
};
let measure = |name: &str| Measure {
name: name.to_string(),
expression: "0".to_string(),
..Default::default()
};
TabularDatabase {
tables: vec![
Table {
name: "Sales".to_string(),
columns: vec![column("Amount"), column("Antal")],
measures: vec![measure("Total Sales"), measure("Antal")],
..Default::default()
},
Table {
name: "Dato".to_string(),
columns: vec![column("Måned"), column("Antal")],
..Default::default()
},
],
expressions: vec![SharedExpression {
name: "ServerName".to_string(),
expression: "\"localhost\"".to_string(),
..Default::default()
}],
..Default::default()
}
}
fn targets_of(expression: &str) -> Vec<ObjectId> {
let db = model();
let index = ModelIndex::build(&db);
bindings(&db, &index, expression)
.iter()
.flat_map(|binding| binding.targets().iter().cloned())
.collect()
}
#[test]
fn a_qualified_reference_names_the_table_and_the_column() {
let targets = targets_of(r#"#"Sales"[Antal]"#);
assert_eq!(
targets,
[
ObjectId::Table {
table: NameKey::new("Sales"),
},
ObjectId::Column {
table: NameKey::new("Sales"),
column: NameKey::new("Antal"),
},
]
);
let targets = targets_of("[Antal]");
assert_eq!(targets.len(), 2);
assert!(
!targets
.iter()
.any(|t| matches!(t, ObjectId::Measure { .. } | ObjectId::Table { .. }))
);
}
#[test]
fn a_column_string_keeps_every_column_of_that_name_alive() {
let targets = targets_of(r#"Table.SelectColumns(Source, {"Antal"})"#);
assert_eq!(targets.len(), 2);
let targets = targets_of(r#"Table.ExpandTableColumn(Source, "Amount")"#);
assert_eq!(
targets,
[ObjectId::Column {
table: NameKey::new("Sales"),
column: NameKey::new("Amount"),
}]
);
}
#[test]
fn a_name_resolves_to_the_table_or_shared_expression_of_that_name() {
let targets = targets_of("let Source = Sales in Source");
assert!(targets.contains(&ObjectId::Table {
table: NameKey::new("Sales"),
}));
let targets = targets_of("Sql.Database(ServerName)");
assert_eq!(
targets,
[ObjectId::Expression {
name: NameKey::new("ServerName"),
}]
);
}
#[test]
fn resolution_is_case_insensitive_end_to_end() {
let targets = targets_of(r#"#"SALES"[amount]"#);
assert_eq!(
targets,
[
ObjectId::Table {
table: NameKey::new("Sales"),
},
ObjectId::Column {
table: NameKey::new("Sales"),
column: NameKey::new("Amount"),
},
]
);
}
#[test]
fn an_escaped_qualifier_resolves_to_the_escaped_name() {
let db = TabularDatabase {
tables: vec![Table {
name: "It\"s".to_string(),
columns: vec![Column {
name: "X".to_string(),
..Default::default()
}],
..Default::default()
}],
..Default::default()
};
let index = ModelIndex::build(&db);
let raw = references(r#"#"It""s"[X]"#).remove(0);
assert_eq!(
bind(&db, &index, raw).targets(),
[
ObjectId::Table {
table: NameKey::new("It\"s"),
},
ObjectId::Column {
table: NameKey::new("It\"s"),
column: NameKey::new("X"),
},
]
);
}
#[test]
fn an_unknown_name_binds_to_nothing_and_stays_data() {
let db = model();
let index = ModelIndex::build(&db);
let bindings_found = bindings(&db, &index, "let Source = Gone in Source[Gone]");
assert_eq!(bindings_found.len(), 3);
assert!(bindings_found.iter().all(Binding::is_unresolved));
assert!(bindings_found.iter().all(|b| b.targets().is_empty()));
}
}