pub mod index;
use crate::identity::{NameKey, ObjectId};
use crate::model::index::{
ColumnHandle, ExpressionHandle, FunctionHandle, HierarchyHandle, MeasureHandle, Resolved,
TableHandle,
};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TabularDatabase {
pub name: Option<String>,
pub tables: Vec<Table>,
pub relationships: Vec<Relationship>,
pub roles: Vec<Role>,
pub expressions: Vec<SharedExpression>,
pub functions: Vec<Function>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Table {
pub name: String,
pub columns: Vec<Column>,
pub measures: Vec<Measure>,
pub partitions: Vec<Partition>,
pub refresh_policy: Option<RefreshPolicy>,
pub hierarchies: Vec<Hierarchy>,
pub calendars: Vec<Calendar>,
pub calculation_group: Option<CalculationGroup>,
pub detail_rows_expression: Option<String>,
pub is_hidden: bool,
pub is_private: bool,
pub is_local_date_table: bool,
pub is_template_date_table: bool,
}
impl Table {
pub fn is_calculated(&self) -> bool {
self.partitions
.iter()
.any(|partition| matches!(partition.source, PartitionSource::Calculated { .. }))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Column {
pub name: String,
pub kind: ColumnKind,
pub is_hidden: bool,
pub sort_by_column: Option<String>,
pub group_by_columns: Vec<String>,
pub variations: Vec<Variation>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Variation {
pub name: String,
pub is_default: bool,
pub relationship: Option<String>,
pub default_hierarchy: Option<HierarchyRef>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct HierarchyRef {
pub table: String,
pub hierarchy: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum ColumnKind {
#[default]
Data,
Calculated {
expression: String,
},
CalculatedTableColumn,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Measure {
pub name: String,
pub expression: String,
pub is_hidden: bool,
pub format_string_expression: Option<String>,
pub detail_rows_expression: Option<String>,
pub kpi: Option<Kpi>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Kpi {
pub target_expression: Option<String>,
pub status_expression: Option<String>,
pub trend_expression: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Partition {
pub name: String,
pub source: PartitionSource,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PartitionSource {
M {
expression: String,
},
Calculated {
expression: String,
},
Query {
query: String,
},
Other {
kind: Option<String>,
},
}
impl Default for PartitionSource {
fn default() -> Self {
PartitionSource::Other { kind: None }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct RefreshPolicy {
pub policy_type: Option<String>,
pub source_expression: Option<String>,
pub change_detection: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Relationship {
pub name: Option<String>,
pub from_table: String,
pub from_column: String,
pub to_table: String,
pub to_column: String,
pub is_active: bool,
}
impl Default for Relationship {
fn default() -> Self {
Self {
name: None,
from_table: String::new(),
from_column: String::new(),
to_table: String::new(),
to_column: String::new(),
is_active: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Hierarchy {
pub name: String,
pub levels: Vec<HierarchyLevel>,
pub is_hidden: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct HierarchyLevel {
pub name: String,
pub column: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Role {
pub name: String,
pub table_permissions: Vec<TablePermission>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TablePermission {
pub table: String,
pub filter_expression: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CalculationGroup {
pub items: Vec<CalculationItem>,
pub no_selection_expression: Option<String>,
pub no_selection_format_string_expression: Option<String>,
pub multiple_or_empty_selection_expression: Option<String>,
pub multiple_or_empty_selection_format_string_expression: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CalculationItem {
pub name: String,
pub expression: String,
pub format_string_expression: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SharedExpression {
pub name: String,
pub expression: String,
pub parameter_values_column: Option<ParameterValuesColumn>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ParameterValuesColumn {
pub table: String,
pub column: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Function {
pub name: String,
pub expression: String,
pub is_hidden: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Calendar {
pub name: String,
pub columns: Vec<String>,
}
impl TabularDatabase {
pub fn table(&self, h: TableHandle) -> Option<&Table> {
self.tables.get(h.0)
}
pub fn column(&self, h: ColumnHandle) -> Option<&Column> {
self.tables.get(h.table)?.columns.get(h.column)
}
pub fn measure(&self, h: MeasureHandle) -> Option<&Measure> {
self.tables.get(h.table)?.measures.get(h.measure)
}
pub fn hierarchy(&self, h: HierarchyHandle) -> Option<&Hierarchy> {
self.tables.get(h.table)?.hierarchies.get(h.hierarchy)
}
pub fn shared_expression(&self, h: ExpressionHandle) -> Option<&SharedExpression> {
self.expressions.get(h.0)
}
pub fn function(&self, h: FunctionHandle) -> Option<&Function> {
self.functions.get(h.0)
}
pub fn object_id(&self, r: Resolved) -> Option<ObjectId> {
match r {
Resolved::Column(h) => {
let table = self.tables.get(h.table)?;
let column = table.columns.get(h.column)?;
Some(ObjectId::Column {
table: NameKey::new(table.name.as_str()),
column: NameKey::new(column.name.as_str()),
})
}
Resolved::Measure(h) => {
let table = self.tables.get(h.table)?;
let measure = table.measures.get(h.measure)?;
Some(ObjectId::Measure {
table: NameKey::new(table.name.as_str()),
measure: NameKey::new(measure.name.as_str()),
})
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DaxExpressionKind {
Measure,
MeasureFormatString,
MeasureDetailRows,
KpiTarget,
KpiStatus,
KpiTrend,
CalculatedColumn,
CalculatedTable,
ChangeDetection,
TableDetailRows,
RlsFilter,
CalculationItem,
CalculationItemFormatString,
CalculationGroupNoSelection,
CalculationGroupNoSelectionFormatString,
CalculationGroupMultipleOrEmptySelection,
CalculationGroupMultipleOrEmptySelectionFormatString,
Function,
ReportMeasure,
ReportMeasureFormatString,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ExpressionOwner<'a> {
Table {
table: &'a str,
},
Column {
table: &'a str,
column: &'a str,
},
Measure {
table: &'a str,
measure: &'a str,
},
Partition {
table: &'a str,
partition: &'a str,
},
Role {
role: &'a str,
},
CalculationItem {
table: &'a str,
item: &'a str,
},
Expression {
name: &'a str,
},
Function {
name: &'a str,
},
ReportMeasure {
measure: &'a str,
},
}
impl ExpressionOwner<'_> {
#[must_use]
pub fn to_object_id(&self) -> ObjectId {
match *self {
ExpressionOwner::Table { table } => ObjectId::Table {
table: NameKey::new(table),
},
ExpressionOwner::Column { table, column } => ObjectId::Column {
table: NameKey::new(table),
column: NameKey::new(column),
},
ExpressionOwner::Measure { table, measure } => ObjectId::Measure {
table: NameKey::new(table),
measure: NameKey::new(measure),
},
ExpressionOwner::Partition { table, partition } => ObjectId::Partition {
table: NameKey::new(table),
partition: NameKey::new(partition),
},
ExpressionOwner::Role { role } => ObjectId::Role {
role: NameKey::new(role),
},
ExpressionOwner::CalculationItem { table, item } => ObjectId::CalculationItem {
table: NameKey::new(table),
item: NameKey::new(item),
},
ExpressionOwner::Expression { name } => ObjectId::Expression {
name: NameKey::new(name),
},
ExpressionOwner::Function { name } => ObjectId::Function {
name: NameKey::new(name),
},
ExpressionOwner::ReportMeasure { measure } => ObjectId::ReportMeasure {
measure: NameKey::new(measure),
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DaxExpressionRef<'a> {
pub owner: ExpressionOwner<'a>,
pub kind: DaxExpressionKind,
pub home_table: Option<&'a str>,
pub text: &'a str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MExpressionRef<'a> {
pub owner: ExpressionOwner<'a>,
pub text: &'a str,
}
impl TabularDatabase {
#[must_use]
pub fn dax_expressions(&self) -> Vec<DaxExpressionRef<'_>> {
let mut out = Vec::new();
for table in &self.tables {
let home = Some(table.name.as_str());
for measure in &table.measures {
let owner = ExpressionOwner::Measure {
table: &table.name,
measure: &measure.name,
};
let kpi = measure.kpi.as_ref();
let sources = [
(DaxExpressionKind::Measure, Some(&measure.expression)),
(
DaxExpressionKind::MeasureFormatString,
measure.format_string_expression.as_ref(),
),
(
DaxExpressionKind::MeasureDetailRows,
measure.detail_rows_expression.as_ref(),
),
(
DaxExpressionKind::KpiTarget,
kpi.and_then(|kpi| kpi.target_expression.as_ref()),
),
(
DaxExpressionKind::KpiStatus,
kpi.and_then(|kpi| kpi.status_expression.as_ref()),
),
(
DaxExpressionKind::KpiTrend,
kpi.and_then(|kpi| kpi.trend_expression.as_ref()),
),
];
for (kind, text) in sources {
if let Some(text) = text {
out.push(DaxExpressionRef {
owner,
kind,
home_table: home,
text,
});
}
}
}
for column in &table.columns {
if let ColumnKind::Calculated { expression } = &column.kind {
out.push(DaxExpressionRef {
owner: ExpressionOwner::Column {
table: &table.name,
column: &column.name,
},
kind: DaxExpressionKind::CalculatedColumn,
home_table: home,
text: expression,
});
}
}
for partition in &table.partitions {
if let PartitionSource::Calculated { expression } = &partition.source {
out.push(DaxExpressionRef {
owner: ExpressionOwner::Partition {
table: &table.name,
partition: &partition.name,
},
kind: DaxExpressionKind::CalculatedTable,
home_table: home,
text: expression,
});
}
}
if let Some(text) = table
.refresh_policy
.as_ref()
.and_then(|policy| policy.change_detection.as_ref())
{
for partition in &table.partitions {
out.push(DaxExpressionRef {
owner: ExpressionOwner::Partition {
table: &table.name,
partition: &partition.name,
},
kind: DaxExpressionKind::ChangeDetection,
home_table: home,
text,
});
}
}
if let Some(text) = &table.detail_rows_expression {
out.push(DaxExpressionRef {
owner: ExpressionOwner::Table { table: &table.name },
kind: DaxExpressionKind::TableDetailRows,
home_table: home,
text,
});
}
if let Some(group) = &table.calculation_group {
for item in &group.items {
let owner = ExpressionOwner::CalculationItem {
table: &table.name,
item: &item.name,
};
out.push(DaxExpressionRef {
owner,
kind: DaxExpressionKind::CalculationItem,
home_table: home,
text: item.expression.as_str(),
});
if let Some(text) = &item.format_string_expression {
out.push(DaxExpressionRef {
owner,
kind: DaxExpressionKind::CalculationItemFormatString,
home_table: home,
text,
});
}
}
let group_owner = ExpressionOwner::Table { table: &table.name };
let sources = [
(
DaxExpressionKind::CalculationGroupNoSelection,
group.no_selection_expression.as_ref(),
),
(
DaxExpressionKind::CalculationGroupNoSelectionFormatString,
group.no_selection_format_string_expression.as_ref(),
),
(
DaxExpressionKind::CalculationGroupMultipleOrEmptySelection,
group.multiple_or_empty_selection_expression.as_ref(),
),
(
DaxExpressionKind::CalculationGroupMultipleOrEmptySelectionFormatString,
group
.multiple_or_empty_selection_format_string_expression
.as_ref(),
),
];
for (kind, text) in sources {
if let Some(text) = text {
out.push(DaxExpressionRef {
owner: group_owner,
kind,
home_table: home,
text,
});
}
}
}
}
for role in &self.roles {
for permission in &role.table_permissions {
if let Some(text) = &permission.filter_expression {
out.push(DaxExpressionRef {
owner: ExpressionOwner::Role { role: &role.name },
kind: DaxExpressionKind::RlsFilter,
home_table: Some(permission.table.as_str()),
text,
});
}
}
}
for function in &self.functions {
out.push(DaxExpressionRef {
owner: ExpressionOwner::Function {
name: &function.name,
},
kind: DaxExpressionKind::Function,
home_table: None,
text: &function.expression,
});
}
out
}
#[must_use]
pub fn m_expressions(&self) -> Vec<MExpressionRef<'_>> {
let mut out = Vec::new();
for table in &self.tables {
for partition in &table.partitions {
if let PartitionSource::M { expression } = &partition.source {
out.push(MExpressionRef {
owner: ExpressionOwner::Partition {
table: &table.name,
partition: &partition.name,
},
text: expression,
});
}
}
if let Some(policy) = &table.refresh_policy {
let texts = [
policy.source_expression.as_ref(),
policy.change_detection.as_ref(),
];
for text in texts.into_iter().flatten() {
for partition in &table.partitions {
out.push(MExpressionRef {
owner: ExpressionOwner::Partition {
table: &table.name,
partition: &partition.name,
},
text,
});
}
}
}
}
for expression in &self.expressions {
out.push(MExpressionRef {
owner: ExpressionOwner::Expression {
name: &expression.name,
},
text: expression.expression.as_str(),
});
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
fn table_id(table: &str) -> ObjectId {
ObjectId::Table {
table: NameKey::new(table),
}
}
fn column_id(table: &str, column: &str) -> ObjectId {
ObjectId::Column {
table: NameKey::new(table),
column: NameKey::new(column),
}
}
fn measure_id(table: &str, measure: &str) -> ObjectId {
ObjectId::Measure {
table: NameKey::new(table),
measure: NameKey::new(measure),
}
}
fn partition_id(table: &str, partition: &str) -> ObjectId {
ObjectId::Partition {
table: NameKey::new(table),
partition: NameKey::new(partition),
}
}
fn calc_item_id(table: &str, item: &str) -> ObjectId {
ObjectId::CalculationItem {
table: NameKey::new(table),
item: NameKey::new(item),
}
}
fn expression_id(name: &str) -> ObjectId {
ObjectId::Expression {
name: NameKey::new(name),
}
}
fn function_id(name: &str) -> ObjectId {
ObjectId::Function {
name: NameKey::new(name),
}
}
fn role_id(role: &str) -> ObjectId {
ObjectId::Role {
role: NameKey::new(role),
}
}
fn partition(name: &str, source: PartitionSource) -> Partition {
Partition {
name: name.to_string(),
source,
}
}
fn dax_tuples(db: &TabularDatabase) -> Vec<(DaxExpressionKind, ObjectId, Option<&str>, &str)> {
db.dax_expressions()
.into_iter()
.map(|e| (e.kind, e.owner.to_object_id(), e.home_table, e.text))
.collect()
}
fn m_tuples(db: &TabularDatabase) -> Vec<(ObjectId, &str)> {
db.m_expressions()
.into_iter()
.map(|e| (e.owner.to_object_id(), e.text))
.collect()
}
fn owners(db: &TabularDatabase) -> Vec<ObjectId> {
dax_tuples(db)
.into_iter()
.map(|(_, owner, _, _)| owner)
.collect()
}
fn every_kind_fixture() -> TabularDatabase {
TabularDatabase {
name: Some("Contoso".to_string()),
tables: vec![
Table {
name: "Sales".to_string(),
columns: vec![
Column {
name: "Amount".to_string(),
kind: ColumnKind::Data,
..Default::default()
},
Column {
name: "Margin".to_string(),
kind: ColumnKind::Calculated {
expression: "'Sales'[Amount] * 0.2".to_string(),
},
..Default::default()
},
],
measures: vec![Measure {
name: "Total Sales".to_string(),
expression: "SUM('Sales'[Amount])".to_string(),
is_hidden: false,
format_string_expression: Some("\"#,##0\"".to_string()),
detail_rows_expression: Some("SELECTCOLUMNS('Sales')".to_string()),
kpi: Some(Kpi {
target_expression: Some("[Budget]".to_string()),
status_expression: Some("IF([Total Sales] > 0, 1, -1)".to_string()),
trend_expression: Some("[Total Sales] - [Prior]".to_string()),
}),
}],
partitions: vec![partition(
"Sales-Part1",
PartitionSource::M {
expression: "let Source = Sql.Database() in Source".to_string(),
},
)],
refresh_policy: Some(RefreshPolicy {
policy_type: Some("basicRefreshPolicy".to_string()),
source_expression: Some(
"let Source = Sql.Database(Server, DB) in Source".to_string(),
),
change_detection: Some(
"EVALUATE ROW(\"Bookmark\", [Total Sales])".to_string(),
),
}),
detail_rows_expression: Some(
"SELECTCOLUMNS('Sales', \"A\", [Amount])".to_string(),
),
..Default::default()
},
Table {
name: "Top Products".to_string(),
partitions: vec![partition(
"Top Products",
PartitionSource::Calculated {
expression: "TOPN(10, 'Product', [Total Sales])".to_string(),
},
)],
..Default::default()
},
Table {
name: "Time Intelligence".to_string(),
calculation_group: Some(CalculationGroup {
items: vec![CalculationItem {
name: "YTD".to_string(),
expression: "TOTALYTD(SELECTEDMEASURE(), 'Date'[Date])".to_string(),
format_string_expression: Some("\"#,##0;;\"".to_string()),
}],
no_selection_expression: Some("SELECTEDMEASURE()".to_string()),
no_selection_format_string_expression: Some(
"SELECTEDMEASUREFORMATSTRING()".to_string(),
),
multiple_or_empty_selection_expression: Some(
"ERROR(\"Pick one item\")".to_string(),
),
multiple_or_empty_selection_format_string_expression: Some(
"\"General\"".to_string(),
),
}),
..Default::default()
},
],
functions: vec![Function {
name: "Sales.NetPrice".to_string(),
expression: "(price: SCALAR) => price * (1 - [Discount Pct])".to_string(),
is_hidden: false,
}],
roles: vec![Role {
name: "Reader".to_string(),
table_permissions: vec![
TablePermission {
table: "Sales".to_string(),
filter_expression: Some("'Sales'[Amount] > 0".to_string()),
},
TablePermission {
table: "Top Products".to_string(),
filter_expression: None,
},
],
}],
..Default::default()
}
}
fn m_fixture() -> TabularDatabase {
TabularDatabase {
tables: vec![Table {
name: "Sales".to_string(),
partitions: vec![
partition(
"Sales-M",
PartitionSource::M {
expression: "let Source = Sql.Database(Server) in Source".to_string(),
},
),
partition(
"Sales-Native",
PartitionSource::Query {
query: "SELECT * FROM dbo.Sales".to_string(),
},
),
partition(
"Sales-Lake",
PartitionSource::Other {
kind: Some("entity".to_string()),
},
),
],
..Default::default()
}],
expressions: vec![
SharedExpression {
name: "Server".to_string(),
expression: "\"contoso.database.windows.net\"".to_string(),
..Default::default()
},
SharedExpression {
name: "Database".to_string(),
expression: "\"AdventureWorks\"".to_string(),
..Default::default()
},
],
..Default::default()
}
}
mod expression_views {
use super::*;
#[test]
fn are_copy_so_enumeration_borrows_everything() {
fn assert_copy<T: Copy>() {}
assert_copy::<DaxExpressionRef<'_>>();
assert_copy::<MExpressionRef<'_>>();
assert_copy::<ExpressionOwner<'_>>();
}
}
mod is_calculated {
use super::*;
fn table_with(source: Option<PartitionSource>) -> Table {
Table {
name: "Anything".to_string(),
partitions: source.into_iter().map(|s| partition("P", s)).collect(),
..Default::default()
}
}
#[rstest]
#[case::dax_partition(
Some(PartitionSource::Calculated { expression: "TOPN(10, 'Sales')".to_string() }),
true
)]
#[case::m_partition(
Some(PartitionSource::M { expression: "let Source = Sql.Database() in Source".to_string() }),
false
)]
#[case::native_query(
Some(PartitionSource::Query { query: "SELECT * FROM dbo.Sales".to_string() }),
false
)]
#[case::direct_lake_entity(
Some(PartitionSource::Other { kind: Some("entity".to_string()) }),
false
)]
#[case::unknown_future_source(Some(PartitionSource::Other { kind: None }), false)]
#[case::no_partitions(None, false)]
fn follows_the_partition_source(
#[case] source: Option<PartitionSource>,
#[case] expected: bool,
) {
assert_eq!(table_with(source).is_calculated(), expected);
}
#[test]
fn is_true_when_only_one_of_several_partitions_is_dax() {
let mixed = Table {
name: "Sales".to_string(),
partitions: vec![
partition(
"Sales-2023",
PartitionSource::Query {
query: "SELECT * FROM dbo.Sales".to_string(),
},
),
partition(
"Sales-2024",
PartitionSource::Calculated {
expression: "FILTER('Raw', TRUE())".to_string(),
},
),
],
..Default::default()
};
assert!(
mixed.is_calculated(),
"one DAX partition makes the table calculated"
);
}
}
mod defaults {
use super::*;
#[test]
fn a_relationship_is_active() {
assert!(Relationship::default().is_active);
}
#[test]
fn a_relationship_has_no_other_content() {
assert_eq!(
Relationship::default(),
Relationship {
name: None,
from_table: String::new(),
from_column: String::new(),
to_table: String::new(),
to_column: String::new(),
is_active: true,
}
);
}
#[test]
fn a_partition_source_is_other_with_no_kind() {
assert_eq!(
PartitionSource::default(),
PartitionSource::Other { kind: None }
);
}
#[test]
fn a_partition_carries_the_default_source() {
assert_eq!(
Partition::default().source,
PartitionSource::Other { kind: None }
);
}
#[test]
fn a_column_kind_is_data() {
assert_eq!(ColumnKind::default(), ColumnKind::Data);
}
#[test]
fn a_column_carries_the_default_kind() {
assert_eq!(Column::default().kind, ColumnKind::Data);
}
}
mod dax_expressions {
use super::*;
#[test]
fn enumerates_every_kind_with_exact_owner_home_and_text() {
let db = every_kind_fixture();
assert_eq!(
dax_tuples(&db),
vec![
(
DaxExpressionKind::Measure,
measure_id("Sales", "Total Sales"),
Some("Sales"),
"SUM('Sales'[Amount])",
),
(
DaxExpressionKind::MeasureFormatString,
measure_id("Sales", "Total Sales"),
Some("Sales"),
"\"#,##0\"",
),
(
DaxExpressionKind::MeasureDetailRows,
measure_id("Sales", "Total Sales"),
Some("Sales"),
"SELECTCOLUMNS('Sales')",
),
(
DaxExpressionKind::KpiTarget,
measure_id("Sales", "Total Sales"),
Some("Sales"),
"[Budget]",
),
(
DaxExpressionKind::KpiStatus,
measure_id("Sales", "Total Sales"),
Some("Sales"),
"IF([Total Sales] > 0, 1, -1)",
),
(
DaxExpressionKind::KpiTrend,
measure_id("Sales", "Total Sales"),
Some("Sales"),
"[Total Sales] - [Prior]",
),
(
DaxExpressionKind::CalculatedColumn,
column_id("Sales", "Margin"),
Some("Sales"),
"'Sales'[Amount] * 0.2",
),
(
DaxExpressionKind::ChangeDetection,
partition_id("Sales", "Sales-Part1"),
Some("Sales"),
"EVALUATE ROW(\"Bookmark\", [Total Sales])",
),
(
DaxExpressionKind::TableDetailRows,
table_id("Sales"),
Some("Sales"),
"SELECTCOLUMNS('Sales', \"A\", [Amount])",
),
(
DaxExpressionKind::CalculatedTable,
partition_id("Top Products", "Top Products"),
Some("Top Products"),
"TOPN(10, 'Product', [Total Sales])",
),
(
DaxExpressionKind::CalculationItem,
calc_item_id("Time Intelligence", "YTD"),
Some("Time Intelligence"),
"TOTALYTD(SELECTEDMEASURE(), 'Date'[Date])",
),
(
DaxExpressionKind::CalculationItemFormatString,
calc_item_id("Time Intelligence", "YTD"),
Some("Time Intelligence"),
"\"#,##0;;\"",
),
(
DaxExpressionKind::CalculationGroupNoSelection,
table_id("Time Intelligence"),
Some("Time Intelligence"),
"SELECTEDMEASURE()",
),
(
DaxExpressionKind::CalculationGroupNoSelectionFormatString,
table_id("Time Intelligence"),
Some("Time Intelligence"),
"SELECTEDMEASUREFORMATSTRING()",
),
(
DaxExpressionKind::CalculationGroupMultipleOrEmptySelection,
table_id("Time Intelligence"),
Some("Time Intelligence"),
"ERROR(\"Pick one item\")",
),
(
DaxExpressionKind::CalculationGroupMultipleOrEmptySelectionFormatString,
table_id("Time Intelligence"),
Some("Time Intelligence"),
"\"General\"",
),
(
DaxExpressionKind::RlsFilter,
role_id("Reader"),
Some("Sales"),
"'Sales'[Amount] > 0",
),
(
DaxExpressionKind::Function,
function_id("Sales.NetPrice"),
None,
"(price: SCALAR) => price * (1 - [Discount Pct])",
),
]
);
}
#[test]
fn enumerates_one_expression_per_populated_site() {
assert_eq!(dax_tuples(&every_kind_fixture()).len(), 18);
}
#[test]
fn owners_preserve_source_casing() {
let db = every_kind_fixture();
let displayed: Vec<String> = owners(&db).iter().map(ObjectId::to_string).collect();
assert_eq!(
displayed,
vec![
"'Sales'[Total Sales]",
"'Sales'[Total Sales]",
"'Sales'[Total Sales]",
"'Sales'[Total Sales]",
"'Sales'[Total Sales]",
"'Sales'[Total Sales]",
"'Sales'[Margin]",
"partition 'Sales'[Sales-Part1]",
"table 'Sales'",
"partition 'Top Products'[Top Products]",
"calculation item 'Time Intelligence'[YTD]",
"calculation item 'Time Intelligence'[YTD]",
"table 'Time Intelligence'",
"table 'Time Intelligence'",
"table 'Time Intelligence'",
"table 'Time Intelligence'",
"role 'Reader'",
"function 'Sales.NetPrice'",
]
);
}
#[rstest]
#[case::a_data_column(column_id("Sales", "Amount"))]
fn excludes(#[case] unwanted: ObjectId) {
let db = every_kind_fixture();
assert!(
!owners(&db).contains(&unwanted),
"{unwanted} owns no DAX and must not be enumerated"
);
}
#[test]
fn excludes_m_partition_text() {
let db = every_kind_fixture();
assert!(
!dax_tuples(&db)
.iter()
.any(|(_, _, _, text)| text.starts_with("let Source")),
"an M query must never be handed to the DAX lexer"
);
}
#[test]
fn a_change_detection_expression_is_emitted_once_per_partition() {
let db = TabularDatabase {
tables: vec![Table {
name: "Sales".to_string(),
partitions: vec![
partition(
"Sales-2023",
PartitionSource::M {
expression: "let Source = 1 in Source".to_string(),
},
),
partition(
"Sales-2024",
PartitionSource::M {
expression: "let Source = 2 in Source".to_string(),
},
),
],
refresh_policy: Some(RefreshPolicy {
change_detection: Some("[Total Sales]".to_string()),
..Default::default()
}),
..Default::default()
}],
..Default::default()
};
let found = dax_tuples(&db);
assert_eq!(found.len(), 2);
assert!(found.iter().all(|(kind, _, _, text)| *kind
== DaxExpressionKind::ChangeDetection
&& *text == "[Total Sales]"));
let owners: Vec<ObjectId> = found.into_iter().map(|(_, owner, _, _)| owner).collect();
assert!(owners.contains(&partition_id("Sales", "Sales-2023")));
assert!(owners.contains(&partition_id("Sales", "Sales-2024")));
}
#[test]
fn a_change_detection_expression_without_partitions_is_not_emitted() {
let db = TabularDatabase {
tables: vec![Table {
name: "Sales".to_string(),
refresh_policy: Some(RefreshPolicy {
change_detection: Some("[Total Sales]".to_string()),
..Default::default()
}),
..Default::default()
}],
..Default::default()
};
assert_eq!(db.dax_expressions().len(), 0);
}
#[test]
fn emits_one_filter_for_a_role_with_one_filtered_permission() {
let db = every_kind_fixture();
assert_eq!(
dax_tuples(&db)
.iter()
.filter(|(kind, _, _, _)| *kind == DaxExpressionKind::RlsFilter)
.count(),
1
);
}
#[test]
fn excludes_metadata_only_permissions() {
let db = every_kind_fixture();
assert!(
!dax_tuples(&db).iter().any(|(kind, _, home, _)| {
*kind == DaxExpressionKind::RlsFilter && *home == Some("Top Products")
}),
"a permission with no filter expression contributes nothing"
);
}
#[test]
fn is_empty_for_a_model_with_no_dax() {
let db = TabularDatabase {
tables: vec![Table {
name: "Sales".to_string(),
columns: vec![Column {
name: "Amount".to_string(),
..Default::default()
}],
partitions: vec![partition(
"Sales",
PartitionSource::M {
expression: "let Source = 1 in Source".to_string(),
},
)],
..Default::default()
}],
..Default::default()
};
assert_eq!(db.dax_expressions().len(), 0);
}
#[test]
fn preserves_model_order_within_a_table() {
let db = every_kind_fixture();
let kinds: Vec<DaxExpressionKind> =
db.dax_expressions().iter().map(|e| e.kind).collect();
assert_eq!(
kinds,
vec![
DaxExpressionKind::Measure,
DaxExpressionKind::MeasureFormatString,
DaxExpressionKind::MeasureDetailRows,
DaxExpressionKind::KpiTarget,
DaxExpressionKind::KpiStatus,
DaxExpressionKind::KpiTrend,
DaxExpressionKind::CalculatedColumn,
DaxExpressionKind::ChangeDetection,
DaxExpressionKind::TableDetailRows,
DaxExpressionKind::CalculatedTable,
DaxExpressionKind::CalculationItem,
DaxExpressionKind::CalculationItemFormatString,
DaxExpressionKind::CalculationGroupNoSelection,
DaxExpressionKind::CalculationGroupNoSelectionFormatString,
DaxExpressionKind::CalculationGroupMultipleOrEmptySelection,
DaxExpressionKind::CalculationGroupMultipleOrEmptySelectionFormatString,
DaxExpressionKind::RlsFilter,
DaxExpressionKind::Function,
]
);
}
#[test]
fn follows_table_and_measure_declaration_order() {
let measure = |name: &str, expression: &str| Measure {
name: name.to_string(),
expression: expression.to_string(),
..Default::default()
};
let db = TabularDatabase {
tables: vec![
Table {
name: "Zebra".to_string(),
measures: vec![measure("M2", "2"), measure("M1", "1")],
..Default::default()
},
Table {
name: "Apple".to_string(),
measures: vec![measure("M3", "3")],
..Default::default()
},
],
..Default::default()
};
let found: Vec<(ObjectId, &str)> = db
.dax_expressions()
.into_iter()
.map(|e| (e.owner.to_object_id(), e.text))
.collect();
assert_eq!(
found,
vec![
(measure_id("Zebra", "M2"), "2"),
(measure_id("Zebra", "M1"), "1"),
(measure_id("Apple", "M3"), "3"),
]
);
}
}
mod m_expressions {
use super::*;
#[test]
fn covers_m_partitions_and_shared_expressions_with_exact_owner_and_text() {
let db = m_fixture();
assert_eq!(
m_tuples(&db),
vec![
(
partition_id("Sales", "Sales-M"),
"let Source = Sql.Database(Server) in Source",
),
(expression_id("Server"), "\"contoso.database.windows.net\""),
(expression_id("Database"), "\"AdventureWorks\""),
]
);
}
#[rstest]
#[case::a_native_query_partition(partition_id("Sales", "Sales-Native"))]
#[case::an_unrecognized_source_partition(partition_id("Sales", "Sales-Lake"))]
fn excludes(#[case] unwanted: ObjectId) {
let db = m_fixture();
assert!(
!m_tuples(&db)
.into_iter()
.any(|(owner, _)| owner == unwanted),
"{unwanted} holds no M and must not be enumerated"
);
}
#[test]
fn leaves_native_query_partitions_out_of_the_dax_enumeration_too() {
assert_eq!(m_fixture().dax_expressions().len(), 0);
}
#[test]
fn is_empty_for_a_model_with_no_m() {
let db = TabularDatabase {
tables: vec![Table {
name: "Top Products".to_string(),
partitions: vec![partition(
"Top Products",
PartitionSource::Calculated {
expression: "TOPN(10, 'Product')".to_string(),
},
)],
..Default::default()
}],
..Default::default()
};
assert_eq!(db.m_expressions().len(), 0);
}
#[test]
fn refresh_policy_expressions_flow_through_the_m_enumeration() {
let db = every_kind_fixture();
assert_eq!(
m_tuples(&db),
vec![
(
partition_id("Sales", "Sales-Part1"),
"let Source = Sql.Database() in Source",
),
(
partition_id("Sales", "Sales-Part1"),
"let Source = Sql.Database(Server, DB) in Source",
),
(
partition_id("Sales", "Sales-Part1"),
"EVALUATE ROW(\"Bookmark\", [Total Sales])",
),
]
);
}
}
}