├── .gitignore ├── BPARules-PowerBI.json ├── BPARules-contrib.json ├── BPARules-standard-lax.json ├── BPARules-standard.json ├── README.md ├── build ├── combine-rules.ps1 └── split-rules.ps1 └── src └── standard ├── DAX Expressions ├── DAX_COLUMNS_FULLY_QUALIFIED.json ├── DAX_DIVISION_COLUMNS.json ├── DAX_MEASURES_UNQUALIFIED.json └── DAX_TODO.json ├── Formatting ├── APPLY_FORMAT_STRING_COLUMNS.json └── APPLY_FORMAT_STRING_MEASURES.json ├── Metadata ├── DISABLE_ATTRIBUTE_HIERACHIES.json ├── META_AVOID_FLOAT.json └── META_SUMMARIZE_NONE.json ├── Model Layout ├── LAYOUT_ADD_TO_PERSPECTIVES.json ├── LAYOUT_COLUMNS_HIERARCHIES_DF.json ├── LAYOUT_HIDE_FK_COLUMNS.json ├── LAYOUT_LOCALIZE_DF.json ├── LAYOUT_MEASURES_DF.json ├── TRANSLATE_DESCRIPTIONS.json ├── TRANSLATE_HIDEABLE_OBJECT_NAMES.json ├── TRANSLATE_HIERARCHY_LEVEL_NAMES.json └── TRANSLATE_OTHER_NAMES.json ├── Naming Conventions ├── NO_CAMELCASE_COLUMNS_HIERARCHIES.json ├── NO_CAMELCASE_MEASURES_TABLES.json ├── PARTITION_NAMES_SHOULD_MATCH_TABLE_NAMES.json ├── RELATIONSHIP_COLUMN_NAMES.json ├── UPPERCASE_FIRST_LETTER_COLUMNS_HIERARCHIES.json └── UPPERCASE_FIRST_LETTER_MEASURES_TABLES.json └── Performance ├── AVOID_SINGLE_ATTRIBUTE_DIMENSIONS.json ├── PERF_UNUSED_COLUMNS.json ├── PERF_UNUSED_MEASURES.json ├── SPECIFY_APPLICATION_NAME_IN_CONNECTION_STRING.json └── USE_MSOLEDBSQL_PROVIDER.json /.gitignore: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # This .gitignore file was automatically created by Microsoft(R) Visual Studio. 3 | ################################################################################ 4 | 5 | /.vs 6 | -------------------------------------------------------------------------------- /BPARules-PowerBI.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ID": "DAX_COLUMNS_FULLY_QUALIFIED", 4 | "Name": "Column references should be fully qualified", 5 | "Category": "DAX Expressions", 6 | "Description": "Using fully qualified column references makes it easier to distinguish between column and measure references, and also helps avoid certain errors.", 7 | "Severity": 2, 8 | "Scope": "Measure, CalculatedColumn, CalculatedTable, KPI", 9 | "Expression": "DependsOn.Any(Key.ObjectType = \"Column\" and Value.Any(not FullyQualified))", 10 | "CompatibilityLevel": 1200 11 | }, 12 | { 13 | "ID": "DAX_DIVISION_COLUMNS", 14 | "Name": "Avoid division (use DIVIDE function instead)", 15 | "Category": "DAX Expressions", 16 | "Description": "Calculated Columns, Measures or Calculated Tables should not use the division symbol in their expressions (/) unless the denominator is a constant value. Instead, it is advised to always use the DIVIDE(,) function.", 17 | "Severity": 3, 18 | "Scope": "Measure, CalculatedColumn, CalculatedTable", 19 | "Expression": "Tokenize().Any(\n Type = DIV and\n Next.Type <> INTEGER_LITERAL and\n Next.Type <> REAL_LITERAL\n)", 20 | "CompatibilityLevel": 1200 21 | }, 22 | { 23 | "ID": "DAX_MEASURES_UNQUALIFIED", 24 | "Name": "Measure references should be unqualified", 25 | "Category": "DAX Expressions", 26 | "Description": "Using unqualified measure references makes it easier to distinguish between column and measure references, and also helps avoid certain errors.", 27 | "Severity": 2, 28 | "Scope": "Measure, CalculatedColumn, CalculatedTable, KPI", 29 | "Expression": "DependsOn.Any(Key.ObjectType = \"Measure\" and Value.Any(FullyQualified))", 30 | "CompatibilityLevel": 1200 31 | }, 32 | { 33 | "ID": "DAX_TODO", 34 | "Name": "Revisit TODO expressions", 35 | "Category": "DAX Expressions", 36 | "Description": "Objects with an expression containing the word \"TODO\" (typically as a comment), should most likely be revisited.", 37 | "Severity": 1, 38 | "Scope": "Measure, Partition, CalculatedColumn, CalculatedTable", 39 | "Expression": "Expression.IndexOf(\"TODO\", StringComparison.OrdinalIgnoreCase) >= 0", 40 | "CompatibilityLevel": 1200 41 | }, 42 | { 43 | "ID": "APPLY_FORMAT_STRING_COLUMNS", 44 | "Name": "Provide format string for visible numeric columns", 45 | "Category": "Formatting", 46 | "Description": "Visible numeric columns should have their Format String property assigned", 47 | "Severity": 2, 48 | "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", 49 | "Expression": "IsVisible\nand string.IsNullOrWhitespace(FormatString)\nand (DataType = \"Int64\" or DataType = \"DateTime\" or DataType = \"Double\" or DataType = \"Decimal\")\n", 50 | "CompatibilityLevel": 1200 51 | }, 52 | { 53 | "ID": "APPLY_FORMAT_STRING_MEASURES", 54 | "Name": "Provide format string for visible numeric measures", 55 | "Category": "Formatting", 56 | "Description": "Visible measures should have their Format String property assigned", 57 | "Severity": 2, 58 | "Scope": "Measure", 59 | "Expression": "IsVisible\nand string.IsNullOrWhitespace(FormatString)\nand (DataType = \"Int64\" or DataType = \"DateTime\" or DataType = \"Double\" or DataType = \"Decimal\")", 60 | "CompatibilityLevel": 1200 61 | }, 62 | { 63 | "ID": "META_AVOID_FLOAT", 64 | "Name": "Do not use floating point data types", 65 | "Category": "Metadata", 66 | "Description": "Floating point datatypes can cause unexpected results when evaluating values close to 0. Use Currency / Fixed Decimal Number (decimal) instead.", 67 | "Severity": 3, 68 | "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", 69 | "Expression": "DataType = \"Double\"", 70 | "FixExpression": "DataType = DataType.Decimal", 71 | "CompatibilityLevel": 1200 72 | }, 73 | { 74 | "ID": "META_SUMMARIZE_NONE", 75 | "Name": "Don't summarize numeric columns", 76 | "Category": "Metadata", 77 | "Description": "Set the SummarizeBy property of all visible numeric columns to \"None\", to avoid unintentional summarization in client tools. Create measures for columns that are supposed to be summarized.", 78 | "Severity": 1, 79 | "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", 80 | "Expression": "IsVisible\nand SummarizeBy <> \"None\"\nand (DataType = \"Double\" or DataType = \"Decimal\" or DataType = \"Int64\")", 81 | "FixExpression": "SummarizeBy = AggregateFunction.None", 82 | "CompatibilityLevel": 1200 83 | }, 84 | { 85 | "ID": "LAYOUT_ADD_TO_PERSPECTIVES", 86 | "Name": "Add objects to perspectives", 87 | "Category": "Model Layout", 88 | "Description": "Visible tables, columns, measures and hierarchies should be assigned to at least one perspective, if the Tabular Model uses perspectives. Otherwise, the objects will only be visible when connecting directly to the model.", 89 | "Severity": 1, 90 | "Scope": "Table, Measure, Hierarchy, DataColumn, CalculatedColumn, CalculatedTableColumn", 91 | "Expression": "IsVisible\nand Model.Perspectives.Any()\nand not InPerspective.Any(it)", 92 | "CompatibilityLevel": 1200 93 | }, 94 | { 95 | "ID": "LAYOUT_COLUMNS_HIERARCHIES_DF", 96 | "Name": "Organize columns and hierarchies in display folders", 97 | "Category": "Model Layout", 98 | "Description": "Tables with more than 10 visible columns and/or hierarchies should have them organized in display folders for improved usability.", 99 | "Severity": 1, 100 | "Scope": "Table", 101 | "Expression": "Columns.Count(IsVisible and string.IsNullOrEmpty(DisplayFolder)) +\nHierarchies.Count(IsVisible and string.IsNullOrEmpty(DisplayFolder))\n> 10", 102 | "CompatibilityLevel": 1200 103 | }, 104 | { 105 | "ID": "LAYOUT_HIDE_FK_COLUMNS", 106 | "Name": "Hide foreign key columns", 107 | "Category": "Model Layout", 108 | "Description": "Columns used on the Many side of a relationship should be hidden, as the related (dimension) table is likely the best place to apply a filter context.", 109 | "Severity": 1, 110 | "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", 111 | "Expression": "IsVisible\nand Model.Relationships.Any(FromColumn = outerIt)", 112 | "FixExpression": "IsHidden = true", 113 | "CompatibilityLevel": 1200 114 | }, 115 | { 116 | "ID": "LAYOUT_LOCALIZE_DF", 117 | "Name": "Translate Display Folders", 118 | "Category": "Model Layout", 119 | "Description": "Display Folder translations should be assigned for objects where the base DisplayFolder property has been assigned. Otherwise, users connecting to the model using a specific Culture will not see the Display Folder structure.", 120 | "Severity": 1, 121 | "Scope": "Measure, Hierarchy, DataColumn, CalculatedColumn, CalculatedTableColumn", 122 | "Expression": "IsVisible\nand not string.IsNullOrEmpty(DisplayFolder)\nand Model.Cultures.Any(it.Name != Model.Culture and string.IsNullOrEmpty(outerIt.TranslatedDisplayFolders[it]))", 123 | "FixExpression": "TranslatedDisplayFolders.Reset()", 124 | "CompatibilityLevel": 1200 125 | }, 126 | { 127 | "ID": "LAYOUT_MEASURES_DF", 128 | "Name": "Organize measures in display folders", 129 | "Category": "Model Layout", 130 | "Description": "Tables with more than 10 visible measures should have them organized in display folders for improved usability", 131 | "Severity": 1, 132 | "Scope": "Table", 133 | "Expression": "Measures.Count(IsVisible and string.IsNullOrEmpty(DisplayFolder)) > 10", 134 | "CompatibilityLevel": 1200 135 | }, 136 | { 137 | "ID": "TRANSLATE_DESCRIPTIONS", 138 | "Name": "Translate Object Descriptions", 139 | "Category": "Model Layout", 140 | "Description": "When the model contains one or more cultures, all objects that have descriptions applied, should also have translated descriptions applied.", 141 | "Severity": 1, 142 | "Scope": "Model, Table, Measure, Hierarchy, Level, Perspective, DataColumn, CalculatedColumn, CalculatedTable, CalculatedTableColumn", 143 | "Expression": "not string.IsNullOrEmpty(Description) and Model.Cultures.Any(it.Name != Model.Culture and string.IsNullOrEmpty(outerIt.TranslatedDescriptions[it]))", 144 | "CompatibilityLevel": 1200 145 | }, 146 | { 147 | "ID": "TRANSLATE_HIDEABLE_OBJECT_NAMES", 148 | "Name": "Translate Visible Object Names", 149 | "Category": "Model Layout", 150 | "Description": "When the model contains one or more cultures, all visible objects should have a name translation provided in that culture.", 151 | "Severity": 1, 152 | "Scope": "Table, Measure, Hierarchy, DataColumn, CalculatedColumn, CalculatedTable, CalculatedTableColumn", 153 | "Expression": "IsVisible and Model.Cultures.Any(it.Name != Model.Culture and string.IsNullOrEmpty(outerIt.TranslatedNames[it]))", 154 | "CompatibilityLevel": 1200 155 | }, 156 | { 157 | "ID": "TRANSLATE_HIERARCHY_LEVEL_NAMES", 158 | "Name": "Translate Hierarchy Levels", 159 | "Category": "Model Layout", 160 | "Description": "When the model contains one or more cultures, all levels on visible hirearchies should have their a translation applied to their name in all cultures.", 161 | "Severity": 1, 162 | "Scope": "Level", 163 | "Expression": "Hierarchy.IsVisible and Model.Cultures.Any(it.Name != Model.Culture and string.IsNullOrEmpty(outerIt.TranslatedNames[it]))", 164 | "CompatibilityLevel": 1200 165 | }, 166 | { 167 | "ID": "TRANSLATE_OTHER_NAMES", 168 | "Name": "Translate Perspectives", 169 | "Category": "Model Layout", 170 | "Description": "When the model contains one or more cultures, the model object and any perspectives in the model should have a translated name assigned in all cultures.", 171 | "Severity": 1, 172 | "Scope": "Model, Perspective", 173 | "Expression": "Model.Cultures.Any(it.Name != Model.Culture and string.IsNullOrEmpty(outerIt.TranslatedNames[it]))", 174 | "CompatibilityLevel": 1200 175 | }, 176 | { 177 | "ID": "NO_CAMELCASE_COLUMNS_HIERARCHIES", 178 | "Name": "Avoid CamelCase on visible columns and hierarchies", 179 | "Category": "Naming Conventions", 180 | "Description": "Visible columns and hierarchies should not use CamelCase in their names, unless translations are applied", 181 | "Severity": 2, 182 | "Scope": "Hierarchy, DataColumn, CalculatedColumn, CalculatedTableColumn", 183 | "Expression": "IsVisible \nand RegEx.IsMatch(Name, \"[A-Z]([A-Z0-9]*[a-z][a-z0-9]*[A-Z]|[a-z0-9]*[A-Z][A-Z0-9]*[a-z])[A-Za-z0-9]*\") \nand not Name.Contains(\" \") \nand (Model.Cultures.Count = 0 or TranslatedNames.Any(it = \"\" or it = outerIt.Name))", 184 | "CompatibilityLevel": 1200 185 | }, 186 | { 187 | "ID": "NO_CAMELCASE_MEASURES_TABLES", 188 | "Name": "Avoid CamelCase on visible measures and tables", 189 | "Category": "Naming Conventions", 190 | "Description": "Visible measures and tables should not use CamelCase in their names, unless translations are applied", 191 | "Severity": 2, 192 | "Scope": "Table, Measure, CalculatedTable", 193 | "Expression": "IsVisible \nand RegEx.IsMatch(Name, \"[A-Z]([A-Z0-9]*[a-z][a-z0-9]*[A-Z]|[a-z0-9]*[A-Z][A-Z0-9]*[a-z])[A-Za-z0-9]*\") \nand not Name.Contains(\" \") \nand (Model.Cultures.Count = 0 or TranslatedNames.Any(it = \"\" or it = outerIt.Name))", 194 | "CompatibilityLevel": 1200 195 | }, 196 | { 197 | "ID": "RELATIONSHIP_COLUMN_NAMES", 198 | "Name": "Names of columns in relationships should be the same", 199 | "Category": "Naming Conventions", 200 | "Description": "When a single relationship exists between two tables, the columns on both sides of the relationship must have the same name. When multiple relationships exist between two tables, the name of the FromColumn must end with the name of the ToColumn (for example OrderDateKey, ShipDateKey, DueDateKey, etc.)", 201 | "Severity": 2, 202 | "Scope": "Relationship", 203 | "Expression": "(Model.Relationships.Count(FromTable = OuterIt.FromTable and ToTable = OuterIt.ToTable) = 1 and FromColumn.Name <> ToColumn.Name) or\n(Model.Relationships.Count(FromTable = OuterIt.FromTable and ToTable = OuterIt.ToTable) > 1 and not FromColumn.Name.EndsWith(ToColumn.Name))", 204 | "CompatibilityLevel": 1200 205 | }, 206 | { 207 | "ID": "UPPERCASE_FIRST_LETTER_COLUMNS_HIERARCHIES", 208 | "Name": "Column and hierarchy names must start with uppercase letter", 209 | "Category": "Naming Conventions", 210 | "Description": "Avoid using prefixes and camelCasing. Use \"Sales\" instead of \"dimSales\" or \"mSales\".", 211 | "Severity": 2, 212 | "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", 213 | "Expression": "IsVisible\nand char.IsLower(Name[0])\nand (Model.Cultures.Count = 0 or TranslatedNames.Any(it = \"\" or it = outerIt.Name))", 214 | "CompatibilityLevel": 1200 215 | }, 216 | { 217 | "ID": "UPPERCASE_FIRST_LETTER_MEASURES_TABLES", 218 | "Name": "Measure and table names must start with uppercase letter", 219 | "Category": "Naming Conventions", 220 | "Description": "Avoid using prefixes and camelCasing. Use \"Sales\" instead of \"dimSales\" or \"mSales\".", 221 | "Severity": 2, 222 | "Scope": "Table, Measure, CalculatedTable", 223 | "Expression": "IsVisible\nand char.IsLower(Name[0])\nand (Model.Cultures.Count = 0 or TranslatedNames.Any(it = \"\" or it = outerIt.Name))\n", 224 | "CompatibilityLevel": 1200 225 | }, 226 | { 227 | "ID": "AVOID_SINGLE_ATTRIBUTE_DIMENSIONS", 228 | "Name": "Avoid single-attribute dimensions that are not shared by multiple facts", 229 | "Category": "Performance", 230 | "Description": "In general, over-normalization should be avoided. If a dimension only holds a single attribute and the dimension is not shared by multiple facts, consider moving the attribute to the fact table.", 231 | "Severity": 2, 232 | "Scope": "Table", 233 | "Expression": "Columns.Count(IsVisible and not UsedInRelationships.Any()) <= 1 and\nModel.Relationships.Count(ToTable = outerIt) = 1", 234 | "CompatibilityLevel": 1200 235 | }, 236 | { 237 | "ID": "PERF_UNUSED_COLUMNS", 238 | "Name": "Remove unused columns", 239 | "Category": "Performance", 240 | "Description": "Hidden columns, which do not have any dependencies, are not used in any relationships, not used in any hierarchies and not used as the SortByColumn for other columns, will likely not be used by clients and thus take up unnecessary space. Consider removing the columns from the model to save space and improve processing time, if you are certain that no external DAX or MDX queries make use of the columns.", 241 | "Severity": 2, 242 | "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", 243 | "Expression": "not IsVisible\n\nand ReferencedBy.Count = 0 \n\nand (not UsedInRelationships.Any())\n\nand (not UsedInSortBy.Any())\n\nand (not UsedInHierarchies.Any())\n\nand (not UsedInVariations.Any())", 244 | "FixExpression": "Delete()", 245 | "CompatibilityLevel": 1200 246 | }, 247 | { 248 | "ID": "PERF_UNUSED_MEASURES", 249 | "Name": "Remove unused measures", 250 | "Category": "Performance", 251 | "Description": "Hidden measures, that are not referenced by any DAX expression, should be removed.", 252 | "Severity": 1, 253 | "Scope": "Measure", 254 | "Expression": "not IsVisible\nand ReferencedBy.Count = 0", 255 | "FixExpression": "Delete()", 256 | "CompatibilityLevel": 1200 257 | }, 258 | { 259 | "ID": "DIABLE_AUTO_DATE/TIME", 260 | "Name": "Diable auto date/time", 261 | "Category": "Model Layout", 262 | "Description": "Provide your own Calendar tables instead of relying on Power BI Desktop's built-in auto date/time feature (go to File > Options and settings > Options > Current File > Data Load and remove the checkmark from \"Auto date/time\").", 263 | "Severity": 3, 264 | "Scope": "Model", 265 | "Expression": "Tables.Any(HasAnnotation(\"__PBI_LocalDateTable\"))", 266 | "CompatibilityLevel": 1200 267 | } 268 | ] -------------------------------------------------------------------------------- /BPARules-contrib.json: -------------------------------------------------------------------------------- 1 | [ 2 | 3 | ] 4 | -------------------------------------------------------------------------------- /BPARules-standard-lax.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ID": "DAX_COLUMNS_FULLY_QUALIFIED", 4 | "Name": "Column references should be fully qualified", 5 | "Category": "DAX Expressions", 6 | "Description": "Using fully qualified column references makes it easier to distinguish between column and measure references, and also helps avoid certain errors.", 7 | "Severity": 2, 8 | "Scope": "Measure, CalculatedColumn, CalculatedTable, KPI", 9 | "Expression": "DependsOn.Any(Key.ObjectType = \"Column\" and Value.Any(not FullyQualified))", 10 | "FixExpression": null, 11 | "CompatibilityLevel": 1200, 12 | "Source": "standard\\DAX Expressions" 13 | }, 14 | { 15 | "ID": "DAX_DIVISION_COLUMNS", 16 | "Name": "Avoid division (use DIVIDE function instead)", 17 | "Category": "DAX Expressions", 18 | "Description": "Calculated Columns, Measures or Calculated Tables should not use the division symbol in their expressions (/). Instead, it is advised to always use the DIVIDE(\u003cnumerator\u003e,\u003cdenominator\u003e) function.", 19 | "Severity": 2, 20 | "Scope": "Measure, CalculatedColumn, CalculatedTable", 21 | "Expression": "Expression.Contains(\"/\")", 22 | "Remarks": "This rule may flag false positives, if a slash (/) is used in an object name or in a comment. To fix this, we need access to the tokens from lexing the expression.", 23 | "Source": "standard\\DAX Expressions" 24 | }, 25 | { 26 | "ID": "DAX_MEASURES_UNQUALIFIED", 27 | "Name": "Measure references should be unqualified", 28 | "Category": "DAX Expressions", 29 | "Description": "Using unqualified measure references makes it easier to distinguish between column and measure references, and also helps avoid certain errors.", 30 | "Severity": 2, 31 | "Scope": "Measure, CalculatedColumn, CalculatedTable, KPI", 32 | "Expression": "DependsOn.Any(Key.ObjectType = \"Measure\" and Value.Any(FullyQualified))", 33 | "FixExpression": null, 34 | "CompatibilityLevel": 1200, 35 | "Source": "standard\\DAX Expressions" 36 | }, 37 | { 38 | "ID": "DAX_TODO", 39 | "Name": "Revisit TODO expressions", 40 | "Category": "DAX Expressions", 41 | "Description": "Objects with an expression containing the word \"TODO\" (typically as a comment), should most likely be revisited.", 42 | "Severity": 1, 43 | "Scope": "Measure, Partition, CalculatedColumn, CalculatedTable", 44 | "Expression": "Expression.IndexOf(\"TODO\", StringComparison.OrdinalIgnoreCase) \u003e= 0", 45 | "Source": "standard\\DAX Expressions" 46 | }, 47 | { 48 | "ID": "APPLY_FORMAT_STRING_MEASURES", 49 | "Name": "Provide format string for all visible measures", 50 | "Category": "Formatting", 51 | "Description": "Visible measures should have their Format String property assigned", 52 | "Severity": 2, 53 | "Scope": "Measure", 54 | "Expression": "IsVisible \nand string.IsNullOrWhitespace(FormatString)", 55 | "FixExpression": null, 56 | "CompatibilityLevel": 1200, 57 | "Source": "standard\\Formatting" 58 | }, 59 | { 60 | "ID": "META_AVOID_FLOAT", 61 | "Name": "Do not use floating point data types", 62 | "Category": "Metadata", 63 | "Description": "Floating point datatypes can cause unexpected results when evaluating values close to 0. Use Currency / Fixed Decimal Number (decimal) instead.", 64 | "Severity": 3, 65 | "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", 66 | "Expression": "DataType = \"Double\"", 67 | "FixExpression": "DataType = DataType.Decimal", 68 | "Source": "standard\\Metadata" 69 | }, 70 | { 71 | "ID": "META_SUMMARIZE_NONE", 72 | "Name": "Don\u0027t summarize numeric columns", 73 | "Category": "Metadata", 74 | "Description": "Set the SummarizeBy property of all visible numeric columns to \"None\", to avoid unintentional summarization in client tools. Create measures for columns that are supposed to be summarized.", 75 | "Severity": 1, 76 | "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", 77 | "Expression": "IsVisible and SummarizeBy \u003c\u003e \"None\" and (DataType = \"Double\" or DataType = \"Decimal\" or DataType = \"Int64\")", 78 | "FixExpression": "SummarizeBy = AggregateFunction.None", 79 | "Source": "standard\\Metadata" 80 | }, 81 | { 82 | "ID": "LAYOUT_ADD_TO_PERSPECTIVES", 83 | "Name": "Add objects to perspectives", 84 | "Category": "Model Layout", 85 | "Description": "Visible tables, columns, measures and hierarchies should be assigned to at least one perspective, if the Tabular Model uses perspectives. Otherwise, the objects will only be visible when connecting directly to the model.", 86 | "Severity": 1, 87 | "Scope": "Table, Measure, Hierarchy, DataColumn, CalculatedColumn, CalculatedTableColumn", 88 | "Expression": "Model.Perspectives.Any() and IsVisible and not InPerspective.Any(it)", 89 | "Source": "standard\\Model Layout" 90 | }, 91 | { 92 | "ID": "LAYOUT_COLUMNS_HIERARCHIES_DF", 93 | "Name": "Organize columns and hierarchies in display folders", 94 | "Category": "Model Layout", 95 | "Description": "Tables with more than 10 visible columns and/or hierarchies should have them organized in display folders for improved usability.", 96 | "Severity": 1, 97 | "Scope": "Table", 98 | "Expression": "IsVisible and \n (Columns.Count(IsVisible and string.IsNullOrEmpty(DisplayFolder)) +\n Hierarchies.Count(IsVisible and string.IsNullOrEmpty(DisplayFolder))\n) \u003e 10", 99 | "Source": "standard\\Model Layout" 100 | }, 101 | { 102 | "ID": "LAYOUT_HIDE_FK_COLUMNS", 103 | "Name": "Hide foreign key columns", 104 | "Category": "Model Layout", 105 | "Description": "Columns used on the Many side of a relationship should be hidden, as the related (dimension) table is likely the best place to apply a filter context.", 106 | "Severity": 1, 107 | "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", 108 | "Expression": "Model.Relationships.Any(FromColumn = outerIt) and IsVisible", 109 | "FixExpression": "IsHidden = true", 110 | "Source": "standard\\Model Layout" 111 | }, 112 | { 113 | "ID": "LAYOUT_LOCALIZE_DF", 114 | "Name": "Translate Display Folders", 115 | "Category": "Model Layout", 116 | "Description": "Display Folder translations should be assigned for objects where the base DisplayFolder property has been assigned. Otherwise, users connecting to the model using a specific Culture will not see the Display Folder structure.", 117 | "Severity": 1, 118 | "Scope": "Measure, Hierarchy, DataColumn, CalculatedColumn, CalculatedTableColumn", 119 | "Expression": "Model.Cultures.Any() and IsVisible and DisplayFolder \u003c\u003e \"\"", 120 | "FixExpression": "TranslatedDisplayFolder.Reset()", 121 | "Source": "standard\\Model Layout" 122 | }, 123 | { 124 | "ID": "LAYOUT_MEASURES_DF", 125 | "Name": "Organize measures in display folders", 126 | "Category": "Model Layout", 127 | "Description": "Tables with more than 10 visible measures should have them organized in display folders for improved usability", 128 | "Severity": 1, 129 | "Scope": "Table", 130 | "Expression": "IsVisible and Measures.Count(IsVisible and string.IsNullOrEmpty(DisplayFolder)) \u003e 10", 131 | "Source": "standard\\Model Layout" 132 | }, 133 | { 134 | "ID": "NO_CAMELCASE_COLUMNS_HIERARCHIES", 135 | "Name": "Avoid CamelCase on visible columns and hierarchies", 136 | "Category": "Naming Conventions", 137 | "Description": "Visible columns and hierarchies should not use CamelCase in their names, unless translations are applied", 138 | "Severity": 2, 139 | "Scope": "Hierarchy, DataColumn, CalculatedColumn, CalculatedTableColumn", 140 | "Expression": "IsVisible \nand RegEx.IsMatch(Name, \"[A-Z]([A-Z0-9]*[a-z][a-z0-9]*[A-Z]|[a-z0-9]*[A-Z][A-Z0-9]*[a-z])[A-Za-z0-9]*\") \nand not Name.Contains(\" \") \nand (Model.Cultures.Count = 0 or TranslatedNames.Any(it = \"\" or it = outerIt.Name))", 141 | "FixExpression": null, 142 | "CompatibilityLevel": 1200, 143 | "Source": "standard\\Naming" 144 | }, 145 | { 146 | "ID": "NO_CAMELCASE_MEASURES_TABLES", 147 | "Name": "Avoid CamelCase on visible measures and tables", 148 | "Category": "Naming Conventions", 149 | "Description": "Visible measures and tables should not use CamelCase in their names, unless translations are applied", 150 | "Severity": 2, 151 | "Scope": "Measure, Table, CalculatedTable", 152 | "Expression": "IsVisible \nand RegEx.IsMatch(Name, \"[A-Z]([A-Z0-9]*[a-z][a-z0-9]*[A-Z]|[a-z0-9]*[A-Z][A-Z0-9]*[a-z])[A-Za-z0-9]*\") \nand not Name.Contains(\" \") \nand (Model.Cultures.Count = 0 or TranslatedNames.Any(it = \"\" or it = outerIt.Name))", 153 | "FixExpression": null, 154 | "CompatibilityLevel": 1200, 155 | "Source": "standard\\Naming" 156 | }, 157 | { 158 | "ID": "RELATIONSHIP_COLUMN_NAMES", 159 | "Name": "Names of columns in relationships should be the same", 160 | "Category": "Naming Conventions", 161 | "Description": "When a single relationship exists between two tables, the columns on both sides of the relationship must have the same name. When multiple relationships exist between two tables, the name of the FromColumn must end with the name of the ToColumn (for example OrderDateKey, ShipDateKey, DueDateKey, etc.)", 162 | "Severity": 2, 163 | "Scope": "Relationship", 164 | "Expression": "(Model.Relationships.Count(FromTable = OuterIt.FromTable and ToTable = OuterIt.ToTable) = 1 and FromColumn.Name \u003c\u003e ToColumn.Name) or\n(Model.Relationships.Count(FromTable = OuterIt.FromTable and ToTable = OuterIt.ToTable) \u003e 1 and not FromColumn.Name.EndsWith(ToColumn.Name))", 165 | "FixExpression": null, 166 | "CompatibilityLevel": 1200, 167 | "Source": "standard\\Naming" 168 | }, 169 | { 170 | "ID": "UPPERCASE_FIRST_LETTER_COLUMNS_HIERARCHIES", 171 | "Name": "Column and hierarchy names must start with uppercase letter", 172 | "Category": "Naming Conventions", 173 | "Description": "Avoid using prefixes and camelCasing. Use \"Sales\" instead of \"dimSales\" or \"mSales\".", 174 | "Severity": 2, 175 | "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", 176 | "Expression": "IsVisible and\nchar.IsLower(Name[0])\nand (Model.Cultures.Count = 0 or TranslatedNames.Any(it = \"\" or it = outerIt.Name))\n", 177 | "FixExpression": null, 178 | "CompatibilityLevel": 1200, 179 | "Source": "standard\\Naming" 180 | }, 181 | { 182 | "ID": "UPPERCASE_FIRST_LETTER_MEASURES_TABLES", 183 | "Name": "Measure and table names must start with uppercase letter", 184 | "Category": "Naming Conventions", 185 | "Description": "Avoid using prefixes and camelCasing. Use \"Sales\" instead of \"dimSales\" or \"mSales\".", 186 | "Severity": 2, 187 | "Scope": "Table, Measure, CalculatedTable", 188 | "Expression": "IsVisible\nand char.IsLower(Name[0])\nand (Model.Cultures.Count = 0 or TranslatedNames.Any(it = \"\" or it = outerIt.Name))\n", 189 | "FixExpression": null, 190 | "CompatibilityLevel": 1200, 191 | "Source": "standard\\Naming" 192 | }, 193 | { 194 | "ID": "PERF_UNUSED_COLUMNS", 195 | "Name": "Remove unused columns", 196 | "Category": "Performance", 197 | "Description": "Hidden columns, which do not have any dependencies, are not used in any relationships, not used in any hierarchies and not used as the SortByColumn for other columns, will likely not be used by clients and thus take up unnecessary space. Consider removing the columns from the model to save space and improve processing time, if you are certain that no external DAX or MDX queries make use of the columns.", 198 | "Severity": 2, 199 | "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", 200 | "Expression": "(IsHidden or Table.IsHidden)\n\nand ReferencedBy.Count = 0 \n\nand (not UsedInRelationships.Any())\n\nand (not UsedInSortBy.Any())\n\nand (not UsedInHierarchies.Any())\n\nand (not Table.RowLevelSecurity.Any(\n it \u003c\u003e null and \n it.IndexOf(\"[\" + current.Name + \"]\", \"OrdinalIgnoreCase\") \u003e= 0\n))\n\nand (not Model.Roles.Any(RowLevelSecurity.Any(\n it \u003c\u003e null and \n (\n it.IndexOf(current.Table.Name + \"[\" + current.Name + \"]\", \"OrdinalIgnoreCase\") \u003e= 0 or\n it.IndexOf(\"\u0027\" + current.Table.Name + \"\u0027[\" + current.Name + \"]\", \"OrdinalIgnoreCase\") \u003e= 0\n )\n)))", 201 | "FixExpression": "Delete()", 202 | "CompatibilityLevel": 1200, 203 | "Source": "standard\\Performance" 204 | }, 205 | { 206 | "ID": "PERF_UNUSED_MEASURES", 207 | "Name": "Remove unused measures", 208 | "Category": "Performance", 209 | "Description": "Hidden measures, that are not referenced by any DAX expression, should be removed.", 210 | "Severity": 1, 211 | "Scope": "Measure", 212 | "Expression": "(Table.IsHidden or IsHidden) and ReferencedBy.Count = 0", 213 | "FixExpression": "Delete()", 214 | "CompatibilityLevel": 1200, 215 | "Source": "standard\\Performance" 216 | } 217 | ] 218 | -------------------------------------------------------------------------------- /BPARules-standard.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ID": "DAX_COLUMNS_FULLY_QUALIFIED", 4 | "Name": "Column references should be fully qualified", 5 | "Category": "DAX Expressions", 6 | "Description": "Using fully qualified column references makes it easier to distinguish between column and measure references, and also helps avoid certain errors.", 7 | "Severity": 2, 8 | "Scope": "Measure, CalculatedColumn, CalculatedTable, KPI", 9 | "Expression": "DependsOn.Any(Key.ObjectType = \"Column\" and Value.Any(not FullyQualified))", 10 | "CompatibilityLevel": 1200, 11 | "Source": "standard\\DAX Expressions" 12 | }, 13 | { 14 | "ID": "DAX_DIVISION_COLUMNS", 15 | "Name": "Avoid division (use DIVIDE function instead)", 16 | "Category": "DAX Expressions", 17 | "Description": "Calculated Columns, Measures or Calculated Tables should not use the division symbol in their expressions (/) unless the denominator is a constant value. Instead, it is advised to always use the DIVIDE(\u003cnumerator\u003e,\u003cdenominator\u003e) function.", 18 | "Severity": 3, 19 | "Scope": "Measure, CalculatedColumn, CalculatedTable", 20 | "Expression": "Tokenize().Any(\n Type = DIV and\n Next.Type \u003c\u003e INTEGER_LITERAL and\n Next.Type \u003c\u003e REAL_LITERAL\n)", 21 | "CompatibilityLevel": 1200, 22 | "Source": "standard\\DAX Expressions" 23 | }, 24 | { 25 | "ID": "DAX_MEASURES_UNQUALIFIED", 26 | "Name": "Measure references should be unqualified", 27 | "Category": "DAX Expressions", 28 | "Description": "Using unqualified measure references makes it easier to distinguish between column and measure references, and also helps avoid certain errors.", 29 | "Severity": 2, 30 | "Scope": "Measure, CalculatedColumn, CalculatedTable, KPI", 31 | "Expression": "DependsOn.Any(Key.ObjectType = \"Measure\" and Value.Any(FullyQualified))", 32 | "CompatibilityLevel": 1200, 33 | "Source": "standard\\DAX Expressions" 34 | }, 35 | { 36 | "ID": "DAX_TODO", 37 | "Name": "Revisit TODO expressions", 38 | "Category": "DAX Expressions", 39 | "Description": "Objects with an expression containing the word \"TODO\" (typically as a comment), should most likely be revisited.", 40 | "Severity": 1, 41 | "Scope": "Measure, Partition, CalculatedColumn, CalculatedTable", 42 | "Expression": "Expression.IndexOf(\"TODO\", StringComparison.OrdinalIgnoreCase) \u003e= 0", 43 | "CompatibilityLevel": 1200, 44 | "Source": "standard\\DAX Expressions" 45 | }, 46 | { 47 | "ID": "APPLY_FORMAT_STRING_COLUMNS", 48 | "Name": "Provide format string for visible numeric columns", 49 | "Category": "Formatting", 50 | "Description": "Visible numeric columns should have their Format String property assigned", 51 | "Severity": 2, 52 | "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", 53 | "Expression": "IsVisible\nand string.IsNullOrWhitespace(FormatString)\nand (DataType = \"Int64\" or DataType = \"DateTime\" or DataType = \"Double\" or DataType = \"Decimal\")\n", 54 | "CompatibilityLevel": 1200, 55 | "Source": "standard\\Formatting" 56 | }, 57 | { 58 | "ID": "APPLY_FORMAT_STRING_MEASURES", 59 | "Name": "Provide format string for visible numeric measures", 60 | "Category": "Formatting", 61 | "Description": "Visible measures should have their Format String property assigned", 62 | "Severity": 2, 63 | "Scope": "Measure", 64 | "Expression": "IsVisible\nand string.IsNullOrWhitespace(FormatString)\nand (DataType = \"Int64\" or DataType = \"DateTime\" or DataType = \"Double\" or DataType = \"Decimal\")", 65 | "CompatibilityLevel": 1200, 66 | "Source": "standard\\Formatting" 67 | }, 68 | { 69 | "ID": "DISABLE_ATTRIBUTE_HIERACHIES", 70 | "Name": "Disable attribute hierachies to decrease processing", 71 | "Category": "Metadata", 72 | "Description": "Disable Attribute hierarchies for hidden collumns. This will ensure faster processing.", 73 | "Severity": 2, 74 | "Scope": "DataColumn", 75 | "Expression": "not IsVisible\nand IsAvailableInMDX\nand not UsedInHierarchies.Any()\nand not UsedInVariations.Any()\nand not UsedInSortBy.Any()", 76 | "FixExpression": "IsAvailableInMDX = false", 77 | "CompatibilityLevel": 1400, 78 | "Source": "standard\\Metadata" 79 | }, 80 | { 81 | "ID": "META_AVOID_FLOAT", 82 | "Name": "Do not use floating point data types", 83 | "Category": "Metadata", 84 | "Description": "Floating point datatypes can cause unexpected results when evaluating values close to 0. Use Currency / Fixed Decimal Number (decimal) instead.", 85 | "Severity": 3, 86 | "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", 87 | "Expression": "DataType = \"Double\"", 88 | "FixExpression": "DataType = DataType.Decimal", 89 | "CompatibilityLevel": 1200, 90 | "Source": "standard\\Metadata" 91 | }, 92 | { 93 | "ID": "META_SUMMARIZE_NONE", 94 | "Name": "Don\u0027t summarize numeric columns", 95 | "Category": "Metadata", 96 | "Description": "Set the SummarizeBy property of all visible numeric columns to \"None\", to avoid unintentional summarization in client tools. Create measures for columns that are supposed to be summarized.", 97 | "Severity": 1, 98 | "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", 99 | "Expression": "IsVisible\nand SummarizeBy \u003c\u003e \"None\"\nand (DataType = \"Double\" or DataType = \"Decimal\" or DataType = \"Int64\")", 100 | "FixExpression": "SummarizeBy = AggregateFunction.None", 101 | "CompatibilityLevel": 1200, 102 | "Source": "standard\\Metadata" 103 | }, 104 | { 105 | "ID": "LAYOUT_ADD_TO_PERSPECTIVES", 106 | "Name": "Add objects to perspectives", 107 | "Category": "Model Layout", 108 | "Description": "Visible tables, columns, measures and hierarchies should be assigned to at least one perspective, if the Tabular Model uses perspectives. Otherwise, the objects will only be visible when connecting directly to the model.", 109 | "Severity": 1, 110 | "Scope": "Table, Measure, Hierarchy, DataColumn, CalculatedColumn, CalculatedTableColumn", 111 | "Expression": "IsVisible\nand Model.Perspectives.Any()\nand not InPerspective.Any(it)", 112 | "CompatibilityLevel": 1200, 113 | "Source": "standard\\Model Layout" 114 | }, 115 | { 116 | "ID": "LAYOUT_COLUMNS_HIERARCHIES_DF", 117 | "Name": "Organize columns and hierarchies in display folders", 118 | "Category": "Model Layout", 119 | "Description": "Tables with more than 10 visible columns and/or hierarchies should have them organized in display folders for improved usability.", 120 | "Severity": 1, 121 | "Scope": "Table", 122 | "Expression": "Columns.Count(IsVisible and string.IsNullOrEmpty(DisplayFolder)) +\nHierarchies.Count(IsVisible and string.IsNullOrEmpty(DisplayFolder))\n\u003e 10", 123 | "CompatibilityLevel": 1200, 124 | "Source": "standard\\Model Layout" 125 | }, 126 | { 127 | "ID": "LAYOUT_HIDE_FK_COLUMNS", 128 | "Name": "Hide foreign key columns", 129 | "Category": "Model Layout", 130 | "Description": "Columns used on the Many side of a relationship should be hidden, as the related (dimension) table is likely the best place to apply a filter context.", 131 | "Severity": 1, 132 | "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", 133 | "Expression": "IsVisible\nand Model.Relationships.Any(FromColumn = outerIt)", 134 | "FixExpression": "IsHidden = true", 135 | "CompatibilityLevel": 1200, 136 | "Source": "standard\\Model Layout" 137 | }, 138 | { 139 | "ID": "LAYOUT_LOCALIZE_DF", 140 | "Name": "Translate Display Folders", 141 | "Category": "Model Layout", 142 | "Description": "Display Folder translations should be assigned for objects where the base DisplayFolder property has been assigned. Otherwise, users connecting to the model using a specific Culture will not see the Display Folder structure.", 143 | "Severity": 1, 144 | "Scope": "Measure, Hierarchy, DataColumn, CalculatedColumn, CalculatedTableColumn", 145 | "Expression": "IsVisible\nand not string.IsNullOrEmpty(DisplayFolder)\nand Model.Cultures.Any(string.IsNullOrEmpty(outerIt.TranslatedDisplayFolders[it]))", 146 | "FixExpression": "TranslatedDisplayFolders.Reset()", 147 | "CompatibilityLevel": 1200, 148 | "Source": "standard\\Model Layout" 149 | }, 150 | { 151 | "ID": "LAYOUT_MEASURES_DF", 152 | "Name": "Organize measures in display folders", 153 | "Category": "Model Layout", 154 | "Description": "Tables with more than 10 visible measures should have them organized in display folders for improved usability", 155 | "Severity": 1, 156 | "Scope": "Table", 157 | "Expression": "Measures.Count(IsVisible and string.IsNullOrEmpty(DisplayFolder)) \u003e 10", 158 | "CompatibilityLevel": 1200, 159 | "Source": "standard\\Model Layout" 160 | }, 161 | { 162 | "ID": "TRANSLATE_DESCRIPTIONS", 163 | "Name": "Translate Object Descriptions", 164 | "Category": "Model Layout", 165 | "Description": "When the model contains one or more cultures, all objects that have descriptions applied, should also have translated descriptions applied.", 166 | "Severity": 1, 167 | "Scope": "Model, Table, Measure, Hierarchy, Level, Perspective, DataColumn, CalculatedColumn, CalculatedTable, CalculatedTableColumn", 168 | "Expression": "not string.IsNullOrEmpty(Description) and Model.Cultures.Any(string.IsNullOrEmpty(outerIt.TranslatedDescriptions[it]))", 169 | "CompatibilityLevel": 1200, 170 | "Source": "standard\\Model Layout" 171 | }, 172 | { 173 | "ID": "TRANSLATE_HIDEABLE_OBJECT_NAMES", 174 | "Name": "Translate Visible Object Names", 175 | "Category": "Model Layout", 176 | "Description": "When the model contains one or more cultures, all visible objects should have a name translation provided in that culture.", 177 | "Severity": 1, 178 | "Scope": "Table, Measure, Hierarchy, DataColumn, CalculatedColumn, CalculatedTable, CalculatedTableColumn", 179 | "Expression": "IsVisible and Model.Cultures.Any(string.IsNullOrEmpty(outerIt.TranslatedNames[it]))", 180 | "CompatibilityLevel": 1200, 181 | "Source": "standard\\Model Layout" 182 | }, 183 | { 184 | "ID": "TRANSLATE_HIERARCHY_LEVEL_NAMES", 185 | "Name": "Translate Hierarchy Levels", 186 | "Category": "Model Layout", 187 | "Description": "When the model contains one or more cultures, all levels on visible hirearchies should have their a translation applied to their name in all cultures.", 188 | "Severity": 1, 189 | "Scope": "Level", 190 | "Expression": "Hierarchy.IsVisible and Model.Cultures.Any(string.IsNullOrEmpty(outerIt.TranslatedNames[it]))", 191 | "CompatibilityLevel": 1200, 192 | "Source": "standard\\Model Layout" 193 | }, 194 | { 195 | "ID": "TRANSLATE_OTHER_NAMES", 196 | "Name": "Translate Perspectives", 197 | "Category": "Model Layout", 198 | "Description": "When the model contains one or more cultures, the model object and any perspectives in the model should have a translated name assigned in all cultures.", 199 | "Severity": 1, 200 | "Scope": "Model, Perspective", 201 | "Expression": "Model.Cultures.Any(string.IsNullOrEmpty(outerIt.TranslatedNames[it]))", 202 | "CompatibilityLevel": 1200, 203 | "Source": "standard\\Model Layout" 204 | }, 205 | { 206 | "ID": "NO_CAMELCASE_COLUMNS_HIERARCHIES", 207 | "Name": "Avoid CamelCase on visible columns and hierarchies", 208 | "Category": "Naming Conventions", 209 | "Description": "Visible columns and hierarchies should not use CamelCase in their names, unless translations are applied", 210 | "Severity": 2, 211 | "Scope": "Hierarchy, DataColumn, CalculatedColumn, CalculatedTableColumn", 212 | "Expression": "IsVisible \nand RegEx.IsMatch(Name, \"[A-Z]([A-Z0-9]*[a-z][a-z0-9]*[A-Z]|[a-z0-9]*[A-Z][A-Z0-9]*[a-z])[A-Za-z0-9]*\") \nand not Name.Contains(\" \") \nand (Model.Cultures.Count = 0 or TranslatedNames.Any(it = \"\" or it = outerIt.Name))", 213 | "CompatibilityLevel": 1200, 214 | "Source": "standard\\Naming Conventions" 215 | }, 216 | { 217 | "ID": "NO_CAMELCASE_MEASURES_TABLES", 218 | "Name": "Avoid CamelCase on visible measures and tables", 219 | "Category": "Naming Conventions", 220 | "Description": "Visible measures and tables should not use CamelCase in their names, unless translations are applied", 221 | "Severity": 2, 222 | "Scope": "Table, Measure, CalculatedTable", 223 | "Expression": "IsVisible \nand RegEx.IsMatch(Name, \"[A-Z]([A-Z0-9]*[a-z][a-z0-9]*[A-Z]|[a-z0-9]*[A-Z][A-Z0-9]*[a-z])[A-Za-z0-9]*\") \nand not Name.Contains(\" \") \nand (Model.Cultures.Count = 0 or TranslatedNames.Any(it = \"\" or it = outerIt.Name))", 224 | "CompatibilityLevel": 1200, 225 | "Source": "standard\\Naming Conventions" 226 | }, 227 | { 228 | "ID": "PARTITION_NAMES_SHOULD_MATCH_TABLE_NAMES", 229 | "Name": "Partition names should match table names", 230 | "Category": "Naming Conventions", 231 | "Description": "Tables that only have a single partition should ensure that the partition has the same name as the table. On tables with multiple partitions, each partition name should start with the table name.", 232 | "Severity": 1, 233 | "Scope": "Partition", 234 | "Expression": "(Table.Partitions.Count = 1 and Name \u003c\u003e Table.Name and Table.ObjectType \u003c\u003e ObjectType.CalculationGroupTable) or \n(Table.Partitions.Count \u003e 1 and not Name.StartsWith(Table.Name) and Table.ObjectType \u003c\u003e ObjectType.CalculationGroupTable)", 235 | "CompatibilityLevel": 1200, 236 | "Source": "standard\\Naming Conventions" 237 | }, 238 | { 239 | "ID": "RELATIONSHIP_COLUMN_NAMES", 240 | "Name": "Names of columns in relationships should be the same", 241 | "Category": "Naming Conventions", 242 | "Description": "When a single relationship exists between two tables, the columns on both sides of the relationship must have the same name. When multiple relationships exist between two tables, the name of the FromColumn must end with the name of the ToColumn (for example OrderDateKey, ShipDateKey, DueDateKey, etc.)", 243 | "Severity": 2, 244 | "Scope": "Relationship", 245 | "Expression": "(Model.Relationships.Count(FromTable = OuterIt.FromTable and ToTable = OuterIt.ToTable) = 1 and FromColumn.Name \u003c\u003e ToColumn.Name) or\n(Model.Relationships.Count(FromTable = OuterIt.FromTable and ToTable = OuterIt.ToTable) \u003e 1 and not FromColumn.Name.EndsWith(ToColumn.Name))", 246 | "CompatibilityLevel": 1200, 247 | "Source": "standard\\Naming Conventions" 248 | }, 249 | { 250 | "ID": "UPPERCASE_FIRST_LETTER_COLUMNS_HIERARCHIES", 251 | "Name": "Column and hierarchy names must start with uppercase letter", 252 | "Category": "Naming Conventions", 253 | "Description": "Avoid using prefixes and camelCasing. Use \"Sales\" instead of \"dimSales\" or \"mSales\".", 254 | "Severity": 2, 255 | "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", 256 | "Expression": "IsVisible\nand char.IsLower(Name[0])\nand (Model.Cultures.Count = 0 or TranslatedNames.Any(it = \"\" or it = outerIt.Name))", 257 | "CompatibilityLevel": 1200, 258 | "Source": "standard\\Naming Conventions" 259 | }, 260 | { 261 | "ID": "UPPERCASE_FIRST_LETTER_MEASURES_TABLES", 262 | "Name": "Measure and table names must start with uppercase letter", 263 | "Category": "Naming Conventions", 264 | "Description": "Avoid using prefixes and camelCasing. Use \"Sales\" instead of \"dimSales\" or \"mSales\".", 265 | "Severity": 2, 266 | "Scope": "Table, Measure, CalculatedTable", 267 | "Expression": "IsVisible\nand char.IsLower(Name[0])\nand (Model.Cultures.Count = 0 or TranslatedNames.Any(it = \"\" or it = outerIt.Name))\n", 268 | "CompatibilityLevel": 1200, 269 | "Source": "standard\\Naming Conventions" 270 | }, 271 | { 272 | "ID": "AVOID_SINGLE_ATTRIBUTE_DIMENSIONS", 273 | "Name": "Avoid single-attribute dimensions that are not shared by multiple facts", 274 | "Category": "Performance", 275 | "Description": "In general, over-normalization should be avoided. If a dimension only holds a single attribute and the dimension is not shared by multiple facts, consider moving the attribute to the fact table.", 276 | "Severity": 2, 277 | "Scope": "Table", 278 | "Expression": "Columns.Count(IsVisible and not UsedInRelationships.Any()) \u003c= 1 and\nModel.Relationships.Count(ToTable = outerIt) = 1", 279 | "CompatibilityLevel": 1200, 280 | "Source": "standard\\Performance" 281 | }, 282 | { 283 | "ID": "PERF_UNUSED_COLUMNS", 284 | "Name": "Remove unused columns", 285 | "Category": "Performance", 286 | "Description": "Hidden columns, which do not have any dependencies, are not used in any relationships, not used in any hierarchies and not used as the SortByColumn for other columns, will likely not be used by clients and thus take up unnecessary space. Consider removing the columns from the model to save space and improve processing time, if you are certain that no external DAX or MDX queries make use of the columns.", 287 | "Severity": 2, 288 | "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", 289 | "Expression": "not IsVisible\n\nand ReferencedBy.Count = 0 \n\nand (not UsedInRelationships.Any())\n\nand (not UsedInSortBy.Any())\n\nand (not UsedInHierarchies.Any())\n\nand (not UsedInVariations.Any())", 290 | "FixExpression": "Delete()", 291 | "CompatibilityLevel": 1200, 292 | "Source": "standard\\Performance" 293 | }, 294 | { 295 | "ID": "PERF_UNUSED_MEASURES", 296 | "Name": "Remove unused measures", 297 | "Category": "Performance", 298 | "Description": "Hidden measures, that are not referenced by any DAX expression, should be removed.", 299 | "Severity": 1, 300 | "Scope": "Measure", 301 | "Expression": "not IsVisible\nand ReferencedBy.Count = 0", 302 | "FixExpression": "Delete()", 303 | "CompatibilityLevel": 1200, 304 | "Source": "standard\\Performance" 305 | }, 306 | { 307 | "ID": "SPECIFY_APPLICATION_NAME_IN_CONNECTION_STRING", 308 | "Name": "Specify Application Name in connection string", 309 | "Category": "Performance", 310 | "Description": "When connecting to a SQL Server data source, specify an Application Name in your connection string to let your DBA know where the connection is coming from. For example, you could specify \"AnalysisServicesTabular \u003cServerName\u003e \u003cModelName\u003e\" replacing \u003cServerName\u003e with the name of your AS instance and \u003cModelName\u003e with the name of your model.", 311 | "Severity": 1, 312 | "Scope": "ProviderDataSource", 313 | "Expression": "(ConnectionString.IndexOf(\"SQLNCLI\", StringComparison.OrdinalIgnoreCase) \u003e= 0 or\nConnectionString.IndexOf(\"SQLOLEDB\", StringComparison.OrdinalIgnoreCase) \u003e= 0 or\nConnectionString.IndexOf(\"MSOLEDBSQL\", StringComparison.OrdinalIgnoreCase) \u003e= 0) and ConnectionString.IndexOf(\"Application Name\", StringComparison.OrdinalIgnoreCase) \u003c 0", 314 | "CompatibilityLevel": 1200, 315 | "Source": "standard\\Performance" 316 | }, 317 | { 318 | "ID": "USE_MSOLEDBSQL_PROVIDER", 319 | "Name": "Use MSOLEDBSQL provider", 320 | "Category": "Performance", 321 | "Description": "Data source providers SQLOLEDB and SQLNCLI have been deprecated. Use MSOLEDBSQL instead. Set the \"Provider\" property to \"System.Data.OleDb\" and ensure that the connection string specified MSOLEDBSQL as \"Provider\". More information: https://docs.microsoft.com/en-us/sql/connect/oledb/oledb-driver-for-sql-server?view=sql-server-ver15", 322 | "Severity": 2, 323 | "Scope": "ProviderDataSource", 324 | "Expression": "ConnectionString.IndexOf(\"SQLNCLI\", StringComparison.OrdinalIgnoreCase) \u003e= 0 or\nConnectionString.IndexOf(\"SQLOLEDB\", StringComparison.OrdinalIgnoreCase) \u003e= 0", 325 | "CompatibilityLevel": 1200, 326 | "Source": "standard\\Performance" 327 | } 328 | ] 329 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Best Practice Rules 2 | This repository holds a recommended set of rules for the [Best Practice Analyzer](https://github.com/otykier/TabularEditor/wiki/Best-Practice-Analyzer) of [Tabular Editor](https://tabulareditor.github.io/) . 3 | 4 | To use these rules, simply download the BPARules.json file from the releases page. Store the file in one of the following locations: 5 | 6 | * `%AppData%\..\Local\TabularEditor` to make the rules available only for you. 7 | * `%ProgramData%\TabularEditor` to make the rules available for everyone on your local machine. 8 | 9 | ...and restart Tabular Editor. You should then see the rules show up in Best Practice Analyzer: 10 | 11 | ![image](https://user-images.githubusercontent.com/8976200/31409928-d60dc69c-ae0d-11e7-9372-6944dafec1ee.png) 12 | 13 | ## Contributing 14 | 15 | The community is encouraged to contribute rules to the collection of Best Practices published here. You can contribute in various ways: 16 | 17 | - If you have an idea for a Best Practice Rule, [open a new \[Rule Request\] issue](https://github.com/TabularEditor/BestPracticeRules/issues/new?title=[Rule%20Request]%20Provide%20short%20rule%20description) describing what you would like the rule to do. Someone from the community will then provide the Dynamic LINQ expression to use in the Best Practice Analyzer, so you can validate if the rule behaves as you expect. 18 | - If you have already created a rule in Best Practice Analyzer, [open a new \[Rule Submit\] issue](https://github.com/TabularEditor/BestPracticeRules/issues/new?title=[Rule%20Submit]%20Your%20rule%20name) and attach the JSON definition of your rule (see below). The community will evaluate whether the rule will make it into the set of recommended Best Practices. 19 | - If you have a question regarding how to use the Best Practice Analyzer or if you are missing some features of the Dynamic LINQ expression language, [open a new issue](https://github.com/otykier/TabularEditor/issues/new) on the main Tabular Editor repository. 20 | 21 | ### Conventions 22 | 23 | Rules should be submitted in their JSON representation: 24 | 25 | ```json 26 | { 27 | "ID": string, 28 | "Name": string, 29 | "Category": string, 30 | "Description": string, 31 | "Severity": int, 32 | "Scope": string, 33 | "Expression": string, 34 | "FixExpression": string (optional), 35 | "Remarks": string (optional) 36 | } 37 | ``` 38 | 39 | Rule **ID** must be META_ALL_UPPERCASE_WITH_UNDERSCORES, and include the category prefix (see below). Rule **Name** should be proper case and kept as short as possible, while still describing the essential function of the rule. Rule **Description** should contain a detailed developer-oriented description of the rule and suggestions on how to fix objects that are catched by the rule. Rule **Severity** should be an integer between 1 and 5: 40 | 41 | 1. Not important / cosmetic only 42 | 2. Minor importance / may cause end-user confusion or a less-than-optimal user experience 43 | 3. Important / may cause functional issues, performance degradation or end-user confusion 44 | 4. Very important / similar to 3, but with a higher risk of causing issues 45 | 5. Critical / similar to 4, but guaranteed to cause issues such as deployment/processing errors or logical errors 46 | 47 | You may add **Remarks** to the rule to provide comments to the community regarding the behaviour and reasoning behind the rule, and also any limitations or exceptions. 48 | 49 | Use one of the following values for the **Category** of the rule: 50 | 51 | - **DAX Expressions** (Prefix: DAX) 52 | Rules that relate to the way DAX formulas are written, for example to encourage the use of `DIVIDE(,)` instead of ` / `. 53 | - **Metadata** (Prefix: META) 54 | Rules that relate to metadata that alter the behaviour of client tools when browsing the model. The *SummarizeBy* property of columns is an example of such metadata. 55 | - **Model Layout** (Prefix: LAYOUT) 56 | Rules that govern visibility, perspectives and localization of objects in the model. For example, whether objects should be visible or not given certain conditions. 57 | - **Performance** (Prefix: PERF) 58 | Rules that are transparent to model users, but may impact processing or querying performance of the model. 59 | - **Naming** (Prefix: NAME) 60 | Rules that enforce specific naming conventions. 61 | 62 | More categories may be added over time. 63 | 64 | **Expression** is the [Dynamic LINQ query](https://github.com/otykier/TabularEditor/wiki/Best-Practice-Analyzer#rule-expression-samples) that will identify objects in violation of the rule. **FixExpression** is an optional expression of the form `PropertyName = Value` which will be applied to all objects in violation of the rule, if the developer lets the Best Practice Analyzer "fix" the rule. 65 | -------------------------------------------------------------------------------- /build/combine-rules.ps1: -------------------------------------------------------------------------------- 1 | $standardRules = @() 2 | $contribRules = @() 3 | 4 | Get-ChildItem -Recurse -Filter *.json | 5 | ForEach-Object { 6 | $src = $_.DirectoryName.Substring($_.DirectoryName.IndexOf("\src") + 5) 7 | $ruleJson = Get-Content -Raw -Path $_.FullName 8 | $rule = ConvertFrom-Json $ruleJson 9 | 10 | $rule | Add-Member Source $src 11 | 12 | if($src.StartsWith("standard")) { $standardRules += $rule } 13 | if($src.StartsWith("contrib")) { $contribRules += $rule } 14 | 15 | Write-Host "Processing rule:" $_.Name 16 | } 17 | 18 | $jsonStandard = ConvertTo-Json $standardRules 19 | $jsonContrib = ConvertTo-Json $contribRules 20 | if($jsonStandard -eq "") { $jsonStandard = "[]" } 21 | if($jsonContrib -eq "") { $jsonContrib = "[]" } 22 | 23 | Set-Content ($env:Build_SourcesDirectory + "\BPARules-standard.json") $jsonStandard 24 | Set-Content ($env:Build_SourcesDirectory + "\BPARules-contrib.json") $jsonContrib 25 | Write-Host "Finished combining" $standardRules.Length "standard rule(s)" 26 | Write-Host "Finished combining" $contribRules.Length "contrib rule(s)" 27 | 28 | cd $env:Build_SourcesDirectory 29 | Write-Host '##[command]git config --global user.email "$env:user_email"' 30 | git config --global user.email "$env:user_email" 31 | Write-Host '##[command]git config --global user.name "$env:user_name"' 32 | git config --global user.name "$env:user_name" 33 | Write-Host "##[command]git add ." 34 | git add . 35 | Write-Host "##[command]git commit -m ""Combined rules""" 36 | git commit -m "Combined rules" 37 | Write-Host "##[command]git push origin HEAD:master" 38 | git push origin HEAD:master *>> $env:Build_SourcesDirectory\GitPush_Output.txt 39 | -------------------------------------------------------------------------------- /build/split-rules.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true, ValueFromPipeline = $true)] 3 | [string]$combinedRulesFile, 4 | [Parameter(Mandatory = $true)] 5 | [string]$outputPath 6 | ) 7 | 8 | import-module Newtonsoft.Json 9 | 10 | $rulesJson = Get-Content -Raw $combinedRulesFile 11 | $rules = [Newtonsoft.Json.Linq.JArray]::Parse($rulesJson) 12 | ForEach($rule in $rules) { 13 | $dummy = New-Item -ItemType Directory -Force -Path "$outputPath\$($rule.Category)" 14 | $parsedRule = $rule.ToString() | ConvertFrom-Json 15 | Write-Host "Category: " $parsedRule.Category 16 | Set-Content "$outputPath\$($parsedRule.Category)\$($parsedRule.ID).json" $rule.ToString() 17 | } -------------------------------------------------------------------------------- /src/standard/DAX Expressions/DAX_COLUMNS_FULLY_QUALIFIED.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "DAX_COLUMNS_FULLY_QUALIFIED", 3 | "Name": "Column references should be fully qualified", 4 | "Category": "DAX Expressions", 5 | "Description": "Using fully qualified column references makes it easier to distinguish between column and measure references, and also helps avoid certain errors.", 6 | "Severity": 2, 7 | "Scope": "Measure, CalculatedColumn, CalculatedTable, KPI", 8 | "Expression": "DependsOn.Any(Key.ObjectType = \"Column\" and Value.Any(not FullyQualified))", 9 | "CompatibilityLevel": 1200 10 | } 11 | -------------------------------------------------------------------------------- /src/standard/DAX Expressions/DAX_DIVISION_COLUMNS.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "DAX_DIVISION_COLUMNS", 3 | "Name": "Avoid division (use DIVIDE function instead)", 4 | "Category": "DAX Expressions", 5 | "Description": "Calculated Columns, Measures or Calculated Tables should not use the division symbol in their expressions (/) unless the denominator is a constant value. Instead, it is advised to always use the DIVIDE(,) function.", 6 | "Severity": 3, 7 | "Scope": "Measure, CalculatedColumn, CalculatedTable", 8 | "Expression": "Tokenize().Any(\n Type = DIV and\n Next.Type <> INTEGER_LITERAL and\n Next.Type <> REAL_LITERAL\n)", 9 | "CompatibilityLevel": 1200 10 | } 11 | -------------------------------------------------------------------------------- /src/standard/DAX Expressions/DAX_MEASURES_UNQUALIFIED.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "DAX_MEASURES_UNQUALIFIED", 3 | "Name": "Measure references should be unqualified", 4 | "Category": "DAX Expressions", 5 | "Description": "Using unqualified measure references makes it easier to distinguish between column and measure references, and also helps avoid certain errors.", 6 | "Severity": 2, 7 | "Scope": "Measure, CalculatedColumn, CalculatedTable, KPI", 8 | "Expression": "DependsOn.Any(Key.ObjectType = \"Measure\" and Value.Any(FullyQualified))", 9 | "CompatibilityLevel": 1200 10 | } 11 | -------------------------------------------------------------------------------- /src/standard/DAX Expressions/DAX_TODO.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "DAX_TODO", 3 | "Name": "Revisit TODO expressions", 4 | "Category": "DAX Expressions", 5 | "Description": "Objects with an expression containing the word \"TODO\" (typically as a comment), should most likely be revisited.", 6 | "Severity": 1, 7 | "Scope": "Measure, Partition, CalculatedColumn, CalculatedTable", 8 | "Expression": "Expression.IndexOf(\"TODO\", StringComparison.OrdinalIgnoreCase) >= 0", 9 | "CompatibilityLevel": 1200 10 | } 11 | -------------------------------------------------------------------------------- /src/standard/Formatting/APPLY_FORMAT_STRING_COLUMNS.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "APPLY_FORMAT_STRING_COLUMNS", 3 | "Name": "Provide format string for visible numeric columns", 4 | "Category": "Formatting", 5 | "Description": "Visible numeric columns should have their Format String property assigned", 6 | "Severity": 2, 7 | "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", 8 | "Expression": "IsVisible\nand string.IsNullOrWhitespace(FormatString)\nand (DataType = \"Int64\" or DataType = \"DateTime\" or DataType = \"Double\" or DataType = \"Decimal\")\n", 9 | "CompatibilityLevel": 1200 10 | } 11 | -------------------------------------------------------------------------------- /src/standard/Formatting/APPLY_FORMAT_STRING_MEASURES.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "APPLY_FORMAT_STRING_MEASURES", 3 | "Name": "Provide format string for visible numeric measures", 4 | "Category": "Formatting", 5 | "Description": "Visible measures should have their Format String property assigned", 6 | "Severity": 2, 7 | "Scope": "Measure", 8 | "Expression": "IsVisible\nand string.IsNullOrWhitespace(FormatString)\nand (DataType = \"Int64\" or DataType = \"DateTime\" or DataType = \"Double\" or DataType = \"Decimal\")", 9 | "CompatibilityLevel": 1200 10 | } 11 | -------------------------------------------------------------------------------- /src/standard/Metadata/DISABLE_ATTRIBUTE_HIERACHIES.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "DISABLE_ATTRIBUTE_HIERACHIES", 3 | "Name": "Disable attribute hierachies to decrease processing", 4 | "Category": "Metadata", 5 | "Description": "Disable Attribute hierarchies for hidden collumns. This will ensure faster processing.", 6 | "Severity": 2, 7 | "Scope": "DataColumn", 8 | "Expression": "not IsVisible\nand IsAvailableInMDX\nand not UsedInHierarchies.Any()\nand not UsedInVariations.Any()\nand not UsedInSortBy.Any()", 9 | "FixExpression": "IsAvailableInMDX = false", 10 | "CompatibilityLevel": 1400 11 | } 12 | -------------------------------------------------------------------------------- /src/standard/Metadata/META_AVOID_FLOAT.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "META_AVOID_FLOAT", 3 | "Name": "Do not use floating point data types", 4 | "Category": "Metadata", 5 | "Description": "Floating point datatypes can cause unexpected results when evaluating values close to 0. Use Currency / Fixed Decimal Number (decimal) instead.", 6 | "Severity": 3, 7 | "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", 8 | "Expression": "DataType = \"Double\"", 9 | "FixExpression": "DataType = DataType.Decimal", 10 | "CompatibilityLevel": 1200 11 | } 12 | -------------------------------------------------------------------------------- /src/standard/Metadata/META_SUMMARIZE_NONE.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "META_SUMMARIZE_NONE", 3 | "Name": "Don't summarize numeric columns", 4 | "Category": "Metadata", 5 | "Description": "Set the SummarizeBy property of all visible numeric columns to \"None\", to avoid unintentional summarization in client tools. Create measures for columns that are supposed to be summarized.", 6 | "Severity": 1, 7 | "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", 8 | "Expression": "IsVisible\nand SummarizeBy <> \"None\"\nand (DataType = \"Double\" or DataType = \"Decimal\" or DataType = \"Int64\")", 9 | "FixExpression": "SummarizeBy = AggregateFunction.None", 10 | "CompatibilityLevel": 1200 11 | } 12 | -------------------------------------------------------------------------------- /src/standard/Model Layout/LAYOUT_ADD_TO_PERSPECTIVES.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "LAYOUT_ADD_TO_PERSPECTIVES", 3 | "Name": "Add objects to perspectives", 4 | "Category": "Model Layout", 5 | "Description": "Visible tables, columns, measures and hierarchies should be assigned to at least one perspective, if the Tabular Model uses perspectives. Otherwise, the objects will only be visible when connecting directly to the model.", 6 | "Severity": 1, 7 | "Scope": "Table, Measure, Hierarchy, DataColumn, CalculatedColumn, CalculatedTableColumn", 8 | "Expression": "IsVisible\nand Model.Perspectives.Any()\nand not InPerspective.Any(it)", 9 | "CompatibilityLevel": 1200 10 | } 11 | -------------------------------------------------------------------------------- /src/standard/Model Layout/LAYOUT_COLUMNS_HIERARCHIES_DF.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "LAYOUT_COLUMNS_HIERARCHIES_DF", 3 | "Name": "Organize columns and hierarchies in display folders", 4 | "Category": "Model Layout", 5 | "Description": "Tables with more than 10 visible columns and/or hierarchies should have them organized in display folders for improved usability.", 6 | "Severity": 1, 7 | "Scope": "Table", 8 | "Expression": "Columns.Count(IsVisible and string.IsNullOrEmpty(DisplayFolder)) +\nHierarchies.Count(IsVisible and string.IsNullOrEmpty(DisplayFolder))\n> 10", 9 | "CompatibilityLevel": 1200 10 | } 11 | -------------------------------------------------------------------------------- /src/standard/Model Layout/LAYOUT_HIDE_FK_COLUMNS.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "LAYOUT_HIDE_FK_COLUMNS", 3 | "Name": "Hide foreign key columns", 4 | "Category": "Model Layout", 5 | "Description": "Columns used on the Many side of a relationship should be hidden, as the related (dimension) table is likely the best place to apply a filter context.", 6 | "Severity": 1, 7 | "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", 8 | "Expression": "IsVisible\nand Model.Relationships.Any(FromColumn = outerIt)", 9 | "FixExpression": "IsHidden = true", 10 | "CompatibilityLevel": 1200 11 | } 12 | -------------------------------------------------------------------------------- /src/standard/Model Layout/LAYOUT_LOCALIZE_DF.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "LAYOUT_LOCALIZE_DF", 3 | "Name": "Translate Display Folders", 4 | "Category": "Model Layout", 5 | "Description": "Display Folder translations should be assigned for objects where the base DisplayFolder property has been assigned. Otherwise, users connecting to the model using a specific Culture will not see the Display Folder structure.", 6 | "Severity": 1, 7 | "Scope": "Measure, Hierarchy, DataColumn, CalculatedColumn, CalculatedTableColumn", 8 | "Expression": "IsVisible\nand not string.IsNullOrEmpty(DisplayFolder)\nand Model.Cultures.Any(string.IsNullOrEmpty(outerIt.TranslatedDisplayFolders[it]))", 9 | "FixExpression": "TranslatedDisplayFolders.Reset()", 10 | "CompatibilityLevel": 1200 11 | } 12 | -------------------------------------------------------------------------------- /src/standard/Model Layout/LAYOUT_MEASURES_DF.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "LAYOUT_MEASURES_DF", 3 | "Name": "Organize measures in display folders", 4 | "Category": "Model Layout", 5 | "Description": "Tables with more than 10 visible measures should have them organized in display folders for improved usability", 6 | "Severity": 1, 7 | "Scope": "Table", 8 | "Expression": "Measures.Count(IsVisible and string.IsNullOrEmpty(DisplayFolder)) > 10", 9 | "CompatibilityLevel": 1200 10 | } 11 | -------------------------------------------------------------------------------- /src/standard/Model Layout/TRANSLATE_DESCRIPTIONS.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "TRANSLATE_DESCRIPTIONS", 3 | "Name": "Translate Object Descriptions", 4 | "Category": "Model Layout", 5 | "Description": "When the model contains one or more cultures, all objects that have descriptions applied, should also have translated descriptions applied.", 6 | "Severity": 1, 7 | "Scope": "Model, Table, Measure, Hierarchy, Level, Perspective, DataColumn, CalculatedColumn, CalculatedTable, CalculatedTableColumn", 8 | "Expression": "not string.IsNullOrEmpty(Description) and Model.Cultures.Any(string.IsNullOrEmpty(outerIt.TranslatedDescriptions[it]))", 9 | "CompatibilityLevel": 1200 10 | } -------------------------------------------------------------------------------- /src/standard/Model Layout/TRANSLATE_HIDEABLE_OBJECT_NAMES.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "TRANSLATE_HIDEABLE_OBJECT_NAMES", 3 | "Name": "Translate Visible Object Names", 4 | "Category": "Model Layout", 5 | "Description": "When the model contains one or more cultures, all visible objects should have a name translation provided in that culture.", 6 | "Severity": 1, 7 | "Scope": "Table, Measure, Hierarchy, DataColumn, CalculatedColumn, CalculatedTable, CalculatedTableColumn", 8 | "Expression": "IsVisible and Model.Cultures.Any(string.IsNullOrEmpty(outerIt.TranslatedNames[it]))", 9 | "CompatibilityLevel": 1200 10 | } -------------------------------------------------------------------------------- /src/standard/Model Layout/TRANSLATE_HIERARCHY_LEVEL_NAMES.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "TRANSLATE_HIERARCHY_LEVEL_NAMES", 3 | "Name": "Translate Hierarchy Levels", 4 | "Category": "Model Layout", 5 | "Description": "When the model contains one or more cultures, all levels on visible hirearchies should have their a translation applied to their name in all cultures.", 6 | "Severity": 1, 7 | "Scope": "Level", 8 | "Expression": "Hierarchy.IsVisible and Model.Cultures.Any(string.IsNullOrEmpty(outerIt.TranslatedNames[it]))", 9 | "CompatibilityLevel": 1200 10 | } -------------------------------------------------------------------------------- /src/standard/Model Layout/TRANSLATE_OTHER_NAMES.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "TRANSLATE_OTHER_NAMES", 3 | "Name": "Translate Perspectives", 4 | "Category": "Model Layout", 5 | "Description": "When the model contains one or more cultures, the model object and any perspectives in the model should have a translated name assigned in all cultures.", 6 | "Severity": 1, 7 | "Scope": "Model, Perspective", 8 | "Expression": "Model.Cultures.Any(string.IsNullOrEmpty(outerIt.TranslatedNames[it]))", 9 | "CompatibilityLevel": 1200 10 | } -------------------------------------------------------------------------------- /src/standard/Naming Conventions/NO_CAMELCASE_COLUMNS_HIERARCHIES.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "NO_CAMELCASE_COLUMNS_HIERARCHIES", 3 | "Name": "Avoid CamelCase on visible columns and hierarchies", 4 | "Category": "Naming Conventions", 5 | "Description": "Visible columns and hierarchies should not use CamelCase in their names, unless translations are applied", 6 | "Severity": 2, 7 | "Scope": "Hierarchy, DataColumn, CalculatedColumn, CalculatedTableColumn", 8 | "Expression": "IsVisible \nand RegEx.IsMatch(Name, \"[A-Z]([A-Z0-9]*[a-z][a-z0-9]*[A-Z]|[a-z0-9]*[A-Z][A-Z0-9]*[a-z])[A-Za-z0-9]*\") \nand not Name.Contains(\" \") \nand (Model.Cultures.Count = 0 or TranslatedNames.Any(it = \"\" or it = outerIt.Name))", 9 | "CompatibilityLevel": 1200 10 | } 11 | -------------------------------------------------------------------------------- /src/standard/Naming Conventions/NO_CAMELCASE_MEASURES_TABLES.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "NO_CAMELCASE_MEASURES_TABLES", 3 | "Name": "Avoid CamelCase on visible measures and tables", 4 | "Category": "Naming Conventions", 5 | "Description": "Visible measures and tables should not use CamelCase in their names, unless translations are applied", 6 | "Severity": 2, 7 | "Scope": "Table, Measure, CalculatedTable", 8 | "Expression": "IsVisible \nand RegEx.IsMatch(Name, \"[A-Z]([A-Z0-9]*[a-z][a-z0-9]*[A-Z]|[a-z0-9]*[A-Z][A-Z0-9]*[a-z])[A-Za-z0-9]*\") \nand not Name.Contains(\" \") \nand (Model.Cultures.Count = 0 or TranslatedNames.Any(it = \"\" or it = outerIt.Name))", 9 | "CompatibilityLevel": 1200 10 | } 11 | -------------------------------------------------------------------------------- /src/standard/Naming Conventions/PARTITION_NAMES_SHOULD_MATCH_TABLE_NAMES.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "PARTITION_NAMES_SHOULD_MATCH_TABLE_NAMES", 3 | "Name": "Partition names should match table names", 4 | "Category": "Naming Conventions", 5 | "Description": "Tables that only have a single partition should ensure that the partition has the same name as the table. On tables with multiple partitions, each partition name should start with the table name.", 6 | "Severity": 1, 7 | "Scope": "Partition", 8 | "Expression": "(Table.Partitions.Count = 1 and Name <> Table.Name) or \n(Table.Partitions.Count > 1 and not Name.StartsWith(Table.Name))", 9 | "CompatibilityLevel": 1200 10 | } 11 | -------------------------------------------------------------------------------- /src/standard/Naming Conventions/RELATIONSHIP_COLUMN_NAMES.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "RELATIONSHIP_COLUMN_NAMES", 3 | "Name": "Names of columns in relationships should be the same", 4 | "Category": "Naming Conventions", 5 | "Description": "When a single relationship exists between two tables, the columns on both sides of the relationship must have the same name. When multiple relationships exist between two tables, the name of the FromColumn must end with the name of the ToColumn (for example OrderDateKey, ShipDateKey, DueDateKey, etc.)", 6 | "Severity": 2, 7 | "Scope": "Relationship", 8 | "Expression": "(Model.Relationships.Count(FromTable = OuterIt.FromTable and ToTable = OuterIt.ToTable) = 1 and FromColumn.Name <> ToColumn.Name) or\n(Model.Relationships.Count(FromTable = OuterIt.FromTable and ToTable = OuterIt.ToTable) > 1 and not FromColumn.Name.EndsWith(ToColumn.Name))", 9 | "CompatibilityLevel": 1200 10 | } 11 | -------------------------------------------------------------------------------- /src/standard/Naming Conventions/UPPERCASE_FIRST_LETTER_COLUMNS_HIERARCHIES.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "UPPERCASE_FIRST_LETTER_COLUMNS_HIERARCHIES", 3 | "Name": "Column and hierarchy names must start with uppercase letter", 4 | "Category": "Naming Conventions", 5 | "Description": "Avoid using prefixes and camelCasing. Use \"Sales\" instead of \"dimSales\" or \"mSales\".", 6 | "Severity": 2, 7 | "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", 8 | "Expression": "IsVisible\nand char.IsLower(Name[0])\nand (Model.Cultures.Count = 0 or TranslatedNames.Any(it = \"\" or it = outerIt.Name))", 9 | "CompatibilityLevel": 1200 10 | } 11 | -------------------------------------------------------------------------------- /src/standard/Naming Conventions/UPPERCASE_FIRST_LETTER_MEASURES_TABLES.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "UPPERCASE_FIRST_LETTER_MEASURES_TABLES", 3 | "Name": "Measure and table names must start with uppercase letter", 4 | "Category": "Naming Conventions", 5 | "Description": "Avoid using prefixes and camelCasing. Use \"Sales\" instead of \"dimSales\" or \"mSales\".", 6 | "Severity": 2, 7 | "Scope": "Table, Measure, CalculatedTable", 8 | "Expression": "IsVisible\nand char.IsLower(Name[0])\nand (Model.Cultures.Count = 0 or TranslatedNames.Any(it = \"\" or it = outerIt.Name))\n", 9 | "CompatibilityLevel": 1200 10 | } 11 | -------------------------------------------------------------------------------- /src/standard/Performance/AVOID_SINGLE_ATTRIBUTE_DIMENSIONS.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "AVOID_SINGLE_ATTRIBUTE_DIMENSIONS", 3 | "Name": "Avoid single-attribute dimensions that are not shared by multiple facts", 4 | "Category": "Performance", 5 | "Description": "In general, over-normalization should be avoided. If a dimension only holds a single attribute and the dimension is not shared by multiple facts, consider moving the attribute to the fact table.", 6 | "Severity": 2, 7 | "Scope": "Table", 8 | "Expression": "Columns.Count(IsVisible and not UsedInRelationships.Any()) <= 1 and\nModel.Relationships.Count(ToTable = outerIt) = 1", 9 | "CompatibilityLevel": 1200 10 | } 11 | -------------------------------------------------------------------------------- /src/standard/Performance/PERF_UNUSED_COLUMNS.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "PERF_UNUSED_COLUMNS", 3 | "Name": "Remove unused columns", 4 | "Category": "Performance", 5 | "Description": "Hidden columns, which do not have any dependencies, are not used in any relationships, not used in any hierarchies and not used as the SortByColumn for other columns, will likely not be used by clients and thus take up unnecessary space. Consider removing the columns from the model to save space and improve processing time, if you are certain that no external DAX or MDX queries make use of the columns.", 6 | "Severity": 2, 7 | "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", 8 | "Expression": "not IsVisible\n\nand ReferencedBy.Count = 0 \n\nand (not UsedInRelationships.Any())\n\nand (not UsedInSortBy.Any())\n\nand (not UsedInHierarchies.Any())\n\nand (not UsedInVariations.Any())", 9 | "FixExpression": "Delete()", 10 | "CompatibilityLevel": 1200 11 | } 12 | -------------------------------------------------------------------------------- /src/standard/Performance/PERF_UNUSED_MEASURES.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "PERF_UNUSED_MEASURES", 3 | "Name": "Remove unused measures", 4 | "Category": "Performance", 5 | "Description": "Hidden measures, that are not referenced by any DAX expression, should be removed.", 6 | "Severity": 1, 7 | "Scope": "Measure", 8 | "Expression": "not IsVisible\nand ReferencedBy.Count = 0", 9 | "FixExpression": "Delete()", 10 | "CompatibilityLevel": 1200 11 | } 12 | -------------------------------------------------------------------------------- /src/standard/Performance/SPECIFY_APPLICATION_NAME_IN_CONNECTION_STRING.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "SPECIFY_APPLICATION_NAME_IN_CONNECTION_STRING", 3 | "Name": "Specify Application Name in connection string", 4 | "Category": "Performance", 5 | "Description": "When connecting to a SQL Server data source, specify an Application Name in your connection string to let your DBA know where the connection is coming from. For example, you could specify \"AnalysisServicesTabular \" replacing with the name of your AS instance and with the name of your model.", 6 | "Severity": 1, 7 | "Scope": "ProviderDataSource", 8 | "Expression": "(ConnectionString.IndexOf(\"SQLNCLI\", StringComparison.OrdinalIgnoreCase) >= 0 or\nConnectionString.IndexOf(\"SQLOLEDB\", StringComparison.OrdinalIgnoreCase) >= 0 or\nConnectionString.IndexOf(\"MSOLEDBSQL\", StringComparison.OrdinalIgnoreCase) >= 0) and ConnectionString.IndexOf(\"Application Name\", StringComparison.OrdinalIgnoreCase) < 0", 9 | "CompatibilityLevel": 1200 10 | } 11 | -------------------------------------------------------------------------------- /src/standard/Performance/USE_MSOLEDBSQL_PROVIDER.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "USE_MSOLEDBSQL_PROVIDER", 3 | "Name": "Use MSOLEDBSQL provider", 4 | "Category": "Performance", 5 | "Description": "Data source providers SQLOLEDB and SQLNCLI have been deprecated. Use MSOLEDBSQL instead. Set the \"Provider\" property to \"System.Data.OleDb\" and ensure that the connection string specified MSOLEDBSQL as \"Provider\". More information: https://docs.microsoft.com/en-us/sql/connect/oledb/oledb-driver-for-sql-server?view=sql-server-ver15", 6 | "Severity": 2, 7 | "Scope": "ProviderDataSource", 8 | "Expression": "ConnectionString.IndexOf(\"SQLNCLI\", StringComparison.OrdinalIgnoreCase) >= 0 or\nConnectionString.IndexOf(\"SQLOLEDB\", StringComparison.OrdinalIgnoreCase) >= 0", 9 | "CompatibilityLevel": 1200 10 | } 11 | --------------------------------------------------------------------------------