Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
project = 'semantic-link-labs'
copyright = '2026, Microsoft and community'
author = 'Microsoft and community'
release = '0.14.3'
release = '0.15.0'

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name="semantic-link-labs"
authors = [
{ name = "Microsoft Corporation" },
]
version="0.14.3"
version="0.15.0"
description="Semantic Link Labs for Microsoft Fabric"
readme="README.md"
requires-python=">=3.10,<3.13"
Expand Down
2 changes: 1 addition & 1 deletion src/sempy_labs/_a_lib_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from pathlib import Path

lib_name = "semanticlinklabs"
lib_version = "0.14.3"
lib_version = "0.15.0"

NUGET_BASE_URL = "https://www.nuget.org/api/v2/package"
current_dir = Path(__file__).parent
Expand Down
14 changes: 7 additions & 7 deletions src/sempy_labs/_generate_semantic_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,30 +404,30 @@ def deploy_semantic_model(
)

# Remove mini model annotations from the master model if they exist (cleanup)
#ann_to_remove = [
# ann_to_remove = [
# a.Name
# for a in tom.model.Annotations
# if a.Name.startswith(icons.prefix_mini)
#]
#for ann in ann_to_remove:
# ]
# for ann in ann_to_remove:
# tom.remove_annotation(object=tom.model, name=ann)

# Set annotations to the master model
#if filters is not None or perspective is not None:
# if filters is not None or perspective is not None:
# with connect_semantic_model(
# dataset=source_dataset_id, workspace=source_workspace_id, readonly=False
# ) as tom:

# ann_name = f"{icons.prefix_mini}_{perspective}"

# --- Get existing annotation safely ---
# --- Get existing annotation safely ---
# try:
# ann_value = tom.get_annotation_value(object=tom.model, name=ann_name)
# ann_list = ast.literal_eval(ann_value) if ann_value else []
# except Exception:
# ann_list = []

# --- Build lookup (faster than loop) ---
# --- Build lookup (faster than loop) ---
# index = {a.get("datasetId"): a for a in ann_list}

# if target_dataset_id in index:
Expand Down Expand Up @@ -455,7 +455,7 @@ def deploy_semantic_model(
# }
# )

# --- Save once ---
# --- Save once ---
# tom.set_annotation(object=tom.model, name=ann_name, value=str(ann_list))
if refresh_target_dataset:
refresh_semantic_model(dataset=target_dataset_id, workspace=target_workspace_id)
Expand Down
34 changes: 21 additions & 13 deletions src/sempy_labs/_model_bpa_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
import re
from typing import Optional
from sempy._utils._log import log
from sempy_labs.dax._analysis import (
find_fully_qualified_measures,
find_non_numeric_aggregations,
find_unqualified_columns,
uses_function,
)


@log
Expand Down Expand Up @@ -434,20 +440,26 @@ def model_bpa_rules(
"Measure",
"Warning",
"Avoid using the IFERROR function",
lambda obj, tom: re.search(
r"iferror\s*\(", obj.Expression, flags=re.IGNORECASE
),
lambda obj, tom: uses_function(obj.Expression, "IFERROR"),
"Avoid using the IFERROR function as it may cause performance degradation. If you are concerned about a divide-by-zero error, use the DIVIDE function as it naturally resolves such errors as blank (or you can customize what should be shown in case of such an error).",
"https://www.elegantbi.com/post/top10bestpractices",
),
(
"DAX Expressions",
"Measure",
"Error",
"Avoid aggregating non-numeric columns",
lambda obj, tom: any(
find_non_numeric_aggregations(obj.Expression, tom)
),
"Numeric aggregation functions (SUM, SUMX, AVERAGE, AVERAGEX, MIN, MINX, MAX, MAXX, PRODUCT, PRODUCTX) should be applied to numeric columns (Int64, Decimal, Double). Aggregating a non-numeric column will either fail at query time or force an implicit conversion, both of which usually indicate a modeling mistake.",
),
(
"DAX Expressions",
"Measure",
"Warning",
"Use the TREATAS function instead of INTERSECT for virtual relationships",
lambda obj, tom: re.search(
r"intersect\s*\(", obj.Expression, flags=re.IGNORECASE
),
lambda obj, tom: uses_function(obj.Expression, "INTERSECT"),
"The TREATAS function is more efficient and provides better performance than the INTERSECT function when used in virutal relationships.",
"https://www.sqlbi.com/articles/propagate-filters-using-treatas-in-dax",
),
Expand All @@ -456,11 +468,7 @@ def model_bpa_rules(
"Measure",
"Warning",
"The EVALUATEANDLOG function should not be used in production models",
lambda obj, tom: re.search(
r"evaluateandlog\s*\(",
obj.Expression,
flags=re.IGNORECASE,
),
lambda obj, tom: uses_function(obj.Expression, "EVALUATEANDLOG"),
"The EVALUATEANDLOG function is meant to be used only in development/test environments and should not be used in production models.",
"https://pbidax.wordpress.com/2022/08/16/introduce-the-dax-evaluateandlog-function",
),
Expand Down Expand Up @@ -576,7 +584,7 @@ def model_bpa_rules(
"Error",
"Column references should be fully qualified",
lambda obj, tom: any(
tom.unqualified_columns(object=obj, dependencies=dependencies)
find_unqualified_columns(tom._get_expression(obj), tom)
),
"Using fully qualified column references makes it easier to distinguish between column and measure references, and also helps avoid certain errors. When referencing a column in DAX, first specify the table name, then specify the column name in square brackets.",
"https://www.elegantbi.com/post/top10bestpractices",
Expand All @@ -592,7 +600,7 @@ def model_bpa_rules(
"Error",
"Measure references should be unqualified",
lambda obj, tom: any(
tom.fully_qualified_measures(object=obj, dependencies=dependencies)
find_fully_qualified_measures(tom._get_expression(obj), tom)
),
"Using unqualified measure references makes it easier to distinguish between column and measure references, and also helps avoid certain errors. When referencing a measure using DAX, do not specify the table name. Use only the measure name in square brackets.",
"https://www.elegantbi.com/post/top10bestpractices",
Expand Down
11 changes: 11 additions & 0 deletions src/sempy_labs/dax/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from ._parser import (
parse_dax,
)
from ._format import (
format_dax,
)

__all__ = [
"parse_dax",
"format_dax",
]
Loading