diff --git a/README.md b/README.md index 5fad992a..c9cae108 100644 --- a/README.md +++ b/README.md @@ -43,11 +43,11 @@ The plugin is configured by editing the [configuration file](./TeamTools.TSQL.Li Rules can be enabled or disabled, and their severity levels can be adjusted by setting the desired value next to the rule ID in the `rules` section of the config file: | Value | Meaning | -|-------|---------| -| **off** | 🚫 Rule is disabled | -| **hint** | ℹ️ Rule violation is treated as an info message, suggestion, or recommendation | +| :-- | :-- | +| **off** | 🚫 Rule is disabled | +| **hint** | ℹ️ Rule violation is treated as an info message, suggestion, or recommendation | | **warning** | ⚠️ Rule violation indicates a potentially significant warning, but not an explicit error | -| **error** | ⛔ Explicit compilation or runtime error | +| **error** | ⛔ Explicit compilation or runtime error | However, avoid overstating the importance of certain rules by setting their severity to `error` for violations of conventions or optimization suggestions. This could unnecessarily fail CI pipelines. Instead, adjust the console utility’s overall **sensitivity level** (e.g., `--severity warning`). See the utility’s documentation for details. @@ -68,6 +68,20 @@ Code parsing is performed by the [Microsoft/SqlScriptDOM](https://github.com/mic Some rules may still work with other compatibility levels, but this has not been specifically tested. +## ⚠️ Known issues + +| Rule Id | Issue | Description | +| :-- | :-- | :-- | +| **[CS0197](./TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0197.md):CURSOR_COMMAND_ORDER** | False-positive | When cursor command is prepended with check of CURSOR_STATUS function then it’s fine no matter if command order looks like mistake. Rule implementation does not follow all code-flow branches and does not check IF-ELSE conditions. | +| **[CS0521](./TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0521.md):SYSPROC_RETURN_NOT_CHECKED** | False-positive | If `sp_` or `xp_` proc which has no RETURN code is not mentioned in ignore list then it might be falsely reported by this rule. | +| **[CS0920](./TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0920.md):UNPAIRED_TRAN_STATEMENT** | False-positive | False-positive detection: TRAN control may be fine because of IF-ELSE, TRY-CATCH logic. Rule implementation does not follow all code-flow branches and does not check IF-ELSE conditions. | +| **[CS0921](./TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0921.md):UNPAIRED_XMLDOC_STATEMENT** | False-positive | False-positive detection: XML doc control may be fine because of IF-ELSE, TRY-CATCH logic. Rule implementation does not follow all code-flow branches and does not check IF-ELSE conditions. | +| **[PF0929](./TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0929.md):NON_SARGABLE_PREDICATE** | False-positive | Complex predicates may contain a _primary_ filter which leads to fine execution plan alongside with _minor_ filter considered as non-sargable predicate which has no negative effect on query performance. | +| **[FA0904](./TeamTools.TSQL.Linter/Resources/Docs/en-us/FA0904.md):INDEX_REFERS_UNKNOWN_COL** | False-positive | If a table name is reused along the script with different structure then unnecessary "missing column" warnings are shown. | +| **[FA0949](./TeamTools.TSQL.Linter/Resources/Docs/en-us/FA0949.md):COLUMN_NOT_IN_GROUP_BY** | False-positive | If a similar in it's essence expression is written in different ways in SELECT and GROUP BY clauses it may be reporter as not grouped. | +| **[RD0236](./TeamTools.TSQL.Linter/Resources/Docs/en-us/RD0236.md):REDUNDANT_NEWLINE** | Low performance | Poor implementation leads to a substantial slowdown in the analysis process. | +| **[CD0215](./TeamTools.TSQL.Linter/Resources/Docs/en-us/CD0215.md):COMPUTED_COLS_ORDER** | Controversial | Regarding PERSISTED-colums the rule is right and not: such a column is anyways _computed_ ALTER, and at the same time is _stored_ thus moving the column towards column list tail means data reload. | + ## Acknowledgments Initially, the library was developed as a plugin for the [tsqllint](https://github.com/tsqllint/tsqllint) linter. Over time, its functionality outgrew that product’s capabilities, leading to its evolution into a standalone tool with its own runner and plugin protocol. Nevertheless, the team expresses deep gratitude to the authors of the mentioned project. diff --git a/README.ru-ru.md b/README.ru-ru.md index 9911c6e4..82d549fa 100644 --- a/README.ru-ru.md +++ b/README.ru-ru.md @@ -34,18 +34,18 @@ ## Настройка -💡 _Для пробного запуска воспользуйтесь конфигом [EvaluateConfig.json](./TeamTools.TSQL.Linter/EvaluateConfig.json), в котором отключены большинство правил, касающихся соблюдения соглашний +💡 _Для пробного запуска воспользуйтесь конфигом [EvaluateConfig.json](./TeamTools.TSQL.Linter/EvaluateConfig.json), в котором отключены большинство правил, касающихся соблюдения соглашний о форматировании, именовании и подобном. Укажите путь к этому конфигу в настройках консольного раннера либо просто подмените этим файлом DefaultConfig.json_ Плагин настраивается путем редайтирования [файла конфигурации](./TeamTools.TSQL.Linter/DefaultConfig.json). Таких конфигурационных файлов можно создать несколько, например, один для линтинга с учетом требований к именованию и форматированию, другой — только для поиска явных и потенциальных проблем. Правила можно отключить или включить обратно, повысить или понизить серьёзность нарушения любого правила, установив нужное значение напротив идентификатора правила в разделе `rules` конфигурационного файла: -||| -|-|-| -| **off** | 🚫 правило отключено -| **hint** | ℹ️ нарушение правила учитывается как info-сообщение, подсказка, рекомендация +| Значение | Описание | +| :-- | :-- | +| **off** | 🚫 правило отключено +| **hint** | ℹ️ нарушение правила учитывается как info-сообщение, подсказка, рекомендация | **warning** | ⚠️ нарушение правила означает потенциально значимое предупреждение, но не явную ошибку -| **error** | ⛔ явная ошибка времени компиляции или времени выполнения +| **error** | ⛔ явная ошибка времени компиляции или времени выполнения Однако не стоит завышать значимость некоторых правил и превращать в ошибку (уровень значимости `error`) то, что является нарушением действующих соглашений или предложением по оптимизации, чтобы таким образом переводить статус CI-пайплайна в состояние неуспеха. Вместо повышения severity конкретного правила @@ -69,6 +69,19 @@ В других уровнях совместимости часть правил вполне может исправно работать, но это специально не тестировалось. +## ⚠️ Известные проблемы + +| Идентификатор правила | Проблема | Описание | +| :-- | :-- | :-- | +| **[CS0197](./TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0197.md):CURSOR_COMMAND_ORDER** | Ложное срабатывание | Если команде курсора предшествует проверка функции CURSOR_STATUS, то всё в порядке — независимо от того, выглядит ли порядок команд как ошибка. Реализация правила не отслеживает ветвление кода и не проверяет условия IF-ELSE. | +| **[CS0521](./TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0521.md):SYSPROC_RETURN_NOT_CHECKED** | Ложное срабатывание | Если `sp_` or `xp_` хранимка фактически не возвращает статус через RETURN-код, но при этом не упомянута в списке игнорируемых, то на ней может быть ложное срабатывание. | +| **[CS0920](./TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0920.md):UNPAIRED_TRAN_STATEMENT** | Ложное срабатывание | Ложное срабатывание: управление транзакциями может быть корректным из-за логики IF-ELSE, TRY-CATCH. Реализация правила не отслеживает ветвление кода и не проверяет условия IF-ELSE. | +| **[CS0921](./TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0921.md):UNPAIRED_XMLDOC_STATEMENT** | Ложное срабатывание | Управление XML-документов может быть корректным из-за логики IF-ELSE, TRY-CATCH. Реализация правила не отслеживает ветвление кода и не проверяет условия IF-ELSE. | +| **[PF0929](./TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0929.md):NON_SARGABLE_PREDICATE** | Ложное срабатывание | Сложные предикаты могут содержать _основной_ фильтр, который обеспечивает хороший план выполнения, наряду с _дополнительным_ фильтром, считающимся несаргабельным предикатом, который не оказывает негативного влияния на производительность запроса. | +| **[FA0904](./TeamTools.TSQL.Linter/Resources/Docs/ru-ru/FA0904.md):INDEX_REFERS_UNKNOWN_COL** | Ложное срабатывание | Если название таблицы переиспользуется в скрипте несколько раз с отличающимися структурами, то возможны излишние замечания про "несуществующую колонку". | +| **[FA0949](./TeamTools.TSQL.Linter/Resources/Docs/ru-ru/FA0949.md):COLUMN_NOT_IN_GROUP_BY** | Ложное срабатывание | Если одинаковое по сути выражение по-разному написано в SELECT и GROUP BY, то правило может на него среагировать. | +| **[RD0236](./TeamTools.TSQL.Linter/Resources/Docs/ru-ru/RD0236.md):REDUNDANT_NEWLINE** | Низкая производительность | Неудачная реализация приводит к существенному замедлению анализа. | +| **[CD0215](./TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CD0215.md):COMPUTED_COLS_ORDER** | Спорное | В отношение PERSISTED-столбцов правило и право, и нет: такие столбцы всё же _вычисляемые_, поэтому к ним невозможно применять ALTER, но в то же время они _хранимые_, поэтому сдвиг такого столбца в конец таблицы означает перезаливку данных. | ## Благодарности diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/EndOfMonth.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/EndOfMonth.cs index 1f3ecfe6..229c3870 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/EndOfMonth.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/EndOfMonth.cs @@ -10,14 +10,17 @@ namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.DateFunctions { public class EndOfMonth : SqlGenericFunctionHandler { - private static readonly int RequiredArgumentCount = 1; + private static readonly int MinArgumentCount = 1; + private static readonly int MaxArgumentCount = 2; + private static readonly string FuncName = "EOMONTH"; private static readonly string OutputType = TSqlDomainAttributes.Types.Date; - public EndOfMonth() : base(FuncName, RequiredArgumentCount) + public EndOfMonth() : base(FuncName, MinArgumentCount, MaxArgumentCount) { } + // TODO : respect optional second argument public override bool ValidateArgumentValues(CallSignature call) { return ValidationScenario diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/DbId.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/DbId.cs index 5767a808..94eff2fd 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/DbId.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/DbId.cs @@ -50,7 +50,7 @@ protected override SqlValue DoEvaluateResultValue(CallSignature call) return new CurrentDatabaseId(value.TypeHandler, call.Context.NewSource); } - if (call.ValidatedArgs.DatabaseName.IsNull) + if (call.ValidatedArgs.DatabaseName != null && call.ValidatedArgs.DatabaseName.IsNull) { call.Context.RedundantCall("DB name is NULL"); diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/MicrosoftVersion.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/MicrosoftVersion.cs new file mode 100644 index 00000000..7c155c6a --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/MicrosoftVersion.cs @@ -0,0 +1,16 @@ +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.Abstractions; +using TeamTools.TSQL.ExpressionEvaluator.Routines; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.SysFunctions +{ + public class MicrosoftVersion : GlobalVariableHandler + { + private static readonly string FuncName = "@@MICROSOFTVERSION"; + private static readonly string ResultTypeName = TSqlDomainAttributes.Types.Int; + + // TODO : Report on undocumented feature call? + public MicrosoftVersion() : base(FuncName, ResultTypeName) + { + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/Version.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/Version.cs new file mode 100644 index 00000000..1a31e05f --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/Version.cs @@ -0,0 +1,15 @@ +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.Abstractions; +using TeamTools.TSQL.ExpressionEvaluator.Routines; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.SysFunctions +{ + public class Version : GlobalVariableHandler + { + private static readonly string FuncName = "@@VERSION"; + private static readonly string ResultTypeName = TSqlDomainAttributes.Types.NVarchar; + + public Version() : base(FuncName, ResultTypeName) + { + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/SysFunctions/DbIdTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/SysFunctions/DbIdTests.cs index ec16f1b7..7a07fd3d 100644 --- a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/SysFunctions/DbIdTests.cs +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/SysFunctions/DbIdTests.cs @@ -1,4 +1,6 @@ using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentDto; using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.SysFunctions; using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; using TeamTools.TSQL.ExpressionEvaluator.Values; @@ -30,6 +32,17 @@ public void Test_DbId_ReturnsNullOnNullArgs() Assert.That(res.IsNull, Is.True); } + [Test] + public void Test_DbId_ReturnsApproximateValueIfInputIsNotStringLiteral() + { + var res = func.Evaluate(new List { new ValueArgument(null) }, Context); + + Assert.That(res, Is.Not.Null); + Assert.That(res.IsNull, Is.False); + Assert.That(res.IsPreciseValue, Is.False); + Assert.That(res, Is.InstanceOf()); + } + [Test] public void Test_DbId_ReturnsApproximateValue() { diff --git a/TeamTools.TSQL.Linter/DefaultConfig.json b/TeamTools.TSQL.Linter/DefaultConfig.json index d480f6de..07792e19 100644 --- a/TeamTools.TSQL.Linter/DefaultConfig.json +++ b/TeamTools.TSQL.Linter/DefaultConfig.json @@ -20,6 +20,10 @@ "FA0256:SEMICOLON_BEFORE_THROW": "warning", "FA0281:UNIQUE_DERIVED_DEF_COL_NAME": "error", "FA0283:INVALID_XML_LITERAL": "error", + "FA0451:UNRESOLVED_WINDOW_NAME": "error", + "FA0457:VARIABLE_REDECLARED": "error", + "FA0458:UNRESOLVED_VARIABLED_NAME": "error", + "FA0459:UNRESOLVED_TABLE_VAR_NAME": "error", "FA0703:BAD_TYPE_FOR_COLUMNSTORE": "error", "FA0704:INVALID_ARGUMENT_COUNT": "warning", "FA0705:INVALID_ARGUMENT": "hint", @@ -39,6 +43,8 @@ "FA0770:SPARSE_UNSUPPORTED_FEATURE": "error", "FA0771:SPARSE_UNSUPPORTED_TYPE": "error", "FA0822:INVALID_CLR_OPTION": "error", + "FA0884:INVALID_EXTENDED_PROPERTY_PARAM": "error", + "FA0885:RAISERROR_NEEDS_WITH_LOG": "error", "FA0901:DUP_COLUMN_MODIFIER": "error", "FA0902:CURSOR_OPTION_INCOMPATIBLE": "warning", "FA0904:INDEX_REFERS_UNKNOWN_COL": "error", @@ -75,6 +81,8 @@ "VU0512:PRIVILEGE_MANAGEMENT": "warning", "VU0518:EXECUTE_AS_OWNER": "hint", "VU0519:SYS_DYNAMIC_VIEW": "warning", + "VU0525:OUTPUT_SECRET": "hint", + "VU0526:READ_SECRET": "hint", "DE0401:DEPRECATED_UNIT": "warning", "DE0402:DEPRECATED_TYPE": "warning", @@ -103,6 +111,7 @@ "AM0168:NONUNIQUE_COLUMN_ALIAS": "warning", "AM0169:NONUNIQUE_TABLE_ALIAS": "warning", "AM0170:TABLE_ALIAS_MIMICKS_OTHER_TABLE": "warning", + "AM0891:SELECT_NULL_TYPE": "hint", "AM0903:SAME_VAR_MULTIPLE_OUTPUT": "warning", "AM0935:AMBIGUOUS_COL_SOURCE": "hint", "AM0996:MULTI_SET_SAME_VAR": "warning", @@ -137,6 +146,7 @@ "RD0293:REDUNDANT_CASE_ELSE_NULL": "warning", "RD0296:REDUNDANT_ORDER_BY_CONST": "warning", "RD0307:EOF_REDUNDANT_NEWLINE": "warning", + "RD0452:UNUSED_WINDOW_CLAUSE": "warning", "RD0706:REDUNDANT_TYPE_CONVERSION": "warning", "RD0707:REDUNDANT_ARGUMENT": "hint", "RD0713:COLUMN_ALIAS_IS_THE_SAME": "hint", @@ -154,6 +164,8 @@ "RD0814:IN_DUP_VAR": "warning", "RD0849:REDUNDANT_INDEX_FILTER": "hint", "RD0850:EXTRA_WHERE_PREDICATE": "hint", + "RD0882:REDUNDANT_MAX_RECURSION": "warning", + "RD0883:REDUNDANT_ISNULL_NOT_EQUALS": "hint", "RD0925:REDUNDANT_LIKE": "warning", "RD0926:REDUNDANT_NOT_FOR_REPLICATION": "warning", "RD0927:REDUNDANT_COL_NULLABILITY_CHECK": "warning", @@ -171,6 +183,21 @@ "PF0720:SORTED_CTE": "warning", "PF0775:SPARSE_COL_INDEX_FILTER": "warning", "PF0823:RECOMPILE_RECOMPILE": "hint", + "PF0867:INDEX_FK": "hint", + "PF0868:OPTIMIZER_FIGHT_JOIN_HINT": "hint", + "PF0869:OPTIMIZER_FIGHT_TABLE_HINT": "hint", + "PF0870:OPTIMIZER_FIGHT_QUERY_OPTION": "hint", + "PF0871:OPTIMIZER_FIGHT_FORCE_PLAN": "hint", + "PF0872:SERIAL_PLAN_FORCED": "hint", + "PF0873:SERIAL_PLAN_FORCED_TABLE_VAR": "hint", + "PF0874:SERIAL_PLAN_ZONE_FORCED": "hint", + "PF0875:DDL_DML_MIX": "hint", + "PF0876:EXPAND_DATE_FUNC": "warning", + "PF0877:EXPAND_ISNULL_FOR_OPTIONAL_ARG": "warning", + "PF0878:SINGLE_USER_MODE_ZONE_FORCED": "hint", + "PF0879:TEMP_TABLE_CACHING_PREVENTED": "hint", + "PF0880:NO_EQUALITY_FILTER_JOIN": "hint", + "PF0881:NO_EQUALITY_FILTER_WHERE": "hint", "PF0910:INDEXING_COL_WITH_DEFAULT": "hint", "PF0928:FILTERED_IDX_FOR_NULL_COL_NOT_INCLUDED": "warning", "PF0929:NON_SARGABLE_PREDICATE": "hint", @@ -305,7 +332,12 @@ "CV0837:SP_XML_TO_XQUERY": "hint", "CV0838:SYSTEM_TYPE_UPPERCASE": "hint", "CV0839:GLOBAL_VAR_UPPERCASE": "hint", + "CV0858:FETCH_FULLY_QUALIFIED": "warning", + "CV0897:SET_OPTIONS_ASC_ORDER": "hint", + "SI0453:EXTRACT_WINDOW_CLAUSE": "hint", + "SI0454:REUSE_EXPRESSION_ALIAS": "hint", + "SI0455:EXTRACT_EXPRESSION": "hint", "SI0727:NOT_FOR_SIMPLE_PREDICATE": "hint", "SI0735:SET_TO_DECLARE": "hint", "SI0753:DROP_STATEMENTS_INTO_ONE": "hint", @@ -314,6 +346,9 @@ "SI0846:MULTIPLE_AND_TO_NOT_IN": "hint", "SI0847:MULTIPLE_IN_TO_SINGLE": "hint", "SI0848:MULTIPLE_NOT_IN_TO_SINGLE": "hint", + "SI0859:MULTIPLE_INSERT_VALUES_COLLAPSE": "hint", + "SI0889:USE_TYPE_NOT_PROPERTY": "hint", + "SI0896:MULTIPLE_SET_INTO_ONE": "hint", "DD0153:FK_MULTIPLE_COL": "hint", "DD0158:TABLE_ALL_COL_NULL": "hint", @@ -340,6 +375,14 @@ "DD0828:COMPUTED_COL_SYNONYM": "hint", "DD0829:FK_ON_TMP": "warning", "DD0831:HISTORY_IN_SAME_SCHEMA": "hint", + "DD0855:SCALAR_UDT": "hint", + "DD0860:HISTORY_PERIOD_SET": "warning", + "DD0861:HISTORY_SET_STORAGE_NAME": "warning", + "DD0862:HISTORY_CONSISTENCY_CHECK": "warning", + "DD0863:HISTORY_SAME_DATE_RANGE_PRECISION": "warning", + "DD0864:HISTORY_FUTURE_DATE": "warning", + "DD0865:HISTORY_PERIOD_DEFAULT_PRECISION": "warning", + "DD0866:HISTORY_MAX_DATE_PRECISION": "hint", "DD0906:FK_RECURSION": "hint", "DD0908:NONCLUSTERED_IDX_INCLUDES_CLUSTERED": "hint", "DD0909:INDEX_DUP_COLUMN": "warning", @@ -368,6 +411,8 @@ "CD0726:CODE_IN_SCRIPT_ROOT": "warning", "CD0779:EXECUTE_AS_SELF": "warning", "CD0833:PARTITIONING_RANGE_VALUES": "warning", + "CD0857:SINGLE_OBJECT_PER_FILE": "warning", + "CD0895:CREATE_OPTIONS_INSIDE": "hint", "FL0301:FILE_ENCODING": "warning", "FL0303:CRLF": "warning", @@ -425,6 +470,7 @@ "CS0266:TABLE_LEVEL_CONSTRAINT_IN_COL": "warning", "CS0295:ORDER_BY_POSITION": "warning", "CS0299:COMPLICATED_IIF_TO_CASE": "warning", + "CS0456:USELESS_UNIT": "hint", "CS0514:SHOWING_STATS": "warning", "CS0520:APP_LOCK": "warning", "CS0521:SYSPROC_RETURN_NOT_CHECKED": "warning", @@ -471,6 +517,14 @@ "CS0851:FAKE_OUTER_JOIN": "hint", "CS0852:NON_CORRELATED_JOIN_PREDICATE": "hint", "CS0853:COMPARISON_LEFT_EQUALS_RIGHT": "hint", + "CS0856:SELF_CALL": "hint", + "CS0886:EXTENDED_PROPERTY_MISDIRECTED": "warning", + "CS0887:EXTENDED_PROPERTY_FOR_MISSING_COL": "warning", + "CS0888:EXTENDED_PROPERTY_DUP": "warning", + "CS0890:OBJECT_PROPERTY_FIDDLING": "warning", + "CS0892:CAST_XML_TO_STRING": "hint", + "CS0893:ISOLATION_CONTRADICTION": "hint", + "CS0894:ISOLATION_CHAOS_PER_QUERY": "hint", "CS0905:VAR_LACKS_PRECISION": "warning", "CS0914:INTERSECT_EXCEPT_BROKEN_BY_LITERAL": "warning", "CS0917:FORBIDDEN_INSERT_HINTS": "warning", @@ -537,6 +591,12 @@ "MA0812:INSERT_EXEC", "MA0817:ZERO_ARGS", "DD0825:SINGLE_COL_TABLE", + "PF0872:SERIAL_PLAN_FORCED", + "PF0873:SERIAL_PLAN_FORCED_TABLE_VAR", + "PF0874:SERIAL_PLAN_ZONE_FORCED", + "PF0875:DDL_DML_MIX", + "PF0881:NO_EQUALITY_FILTER_WHERE", + "AM0891:SELECT_NULL_TYPE", "DD0941:TABLE_HAS_PK", "PF0953:TOP_TAKES_ALL", "DD0997:BAD_TYPE_FOR_KEY", @@ -544,6 +604,7 @@ "DD0999:BLOATED_CLUSTERED_IDX" ], "test*.setup.sql": [ + "CS0456:USELESS_UNIT", "DD0158:TABLE_ALL_COL_NULL", "DM0515:DROPPING_TABLES", "DM0516:TRUNCATING_TABLES", @@ -558,11 +619,17 @@ "MA0812:INSERT_EXEC", "MA0817:ZERO_ARGS", "DD0825:SINGLE_COL_TABLE", + "PF0872:SERIAL_PLAN_FORCED", + "PF0873:SERIAL_PLAN_FORCED_TABLE_VAR", + "PF0874:SERIAL_PLAN_ZONE_FORCED", + "PF0875:DDL_DML_MIX", + "PF0881:NO_EQUALITY_FILTER_WHERE", "DD0997:BAD_TYPE_FOR_KEY", "DD0998:BAD_TYPE_FOR_CLUSTERED_IDX", "DD0999:BLOATED_CLUSTERED_IDX" ], "test*.stub*.sql": [ + "CS0456:USELESS_UNIT", "PF0953:TOP_TAKES_ALL", "CS0919:UNUSED_PARAMETER", "CS0748:IDENTITY_INSERT", @@ -570,6 +637,10 @@ "MA0812:INSERT_EXEC", "MA0817:ZERO_ARGS", "MA0818:TOO_MANY_ARGS", + "PF0872:SERIAL_PLAN_FORCED", + "PF0873:SERIAL_PLAN_FORCED_TABLE_VAR", + "PF0874:SERIAL_PLAN_ZONE_FORCED", + "PF0875:DDL_DML_MIX", "DD0997:BAD_TYPE_FOR_KEY", "DD0998:BAD_TYPE_FOR_CLUSTERED_IDX", "DD0158:TABLE_ALL_COL_NULL", @@ -577,6 +648,7 @@ ], "test*.mock*.sql": [ "DD0158:TABLE_ALL_COL_NULL", + "CS0456:USELESS_UNIT", "PF0953:TOP_TAKES_ALL", "CS0919:UNUSED_PARAMETER", "CS0748:IDENTITY_INSERT", @@ -584,11 +656,16 @@ "MA0812:INSERT_EXEC", "MA0817:ZERO_ARGS", "MA0818:TOO_MANY_ARGS", + "PF0872:SERIAL_PLAN_FORCED", + "PF0873:SERIAL_PLAN_FORCED_TABLE_VAR", + "PF0874:SERIAL_PLAN_ZONE_FORCED", + "PF0875:DDL_DML_MIX", "DD0941:TABLE_HAS_PK", "DD0997:BAD_TYPE_FOR_KEY", "DD0998:BAD_TYPE_FOR_CLUSTERED_IDX" ], "tsqlt.*.sql": [ + "CS0456:USELESS_UNIT", "AM0718:OBJECT_ID_WITHOUT_TYPE", "DM0515:DROPPING_TABLES", "DM0516:TRUNCATING_TABLES", @@ -604,6 +681,10 @@ "MA0812:INSERT_EXEC", "MA0817:ZERO_ARGS", "MA0818:TOO_MANY_ARGS", + "PF0872:SERIAL_PLAN_FORCED", + "PF0873:SERIAL_PLAN_FORCED_TABLE_VAR", + "PF0874:SERIAL_PLAN_ZONE_FORCED", + "PF0875:DDL_DML_MIX", "DD0997:BAD_TYPE_FOR_KEY", "DD0998:BAD_TYPE_FOR_CLUSTERED_IDX", "DD0999:BLOATED_CLUSTERED_IDX" @@ -614,6 +695,7 @@ "DD0755:SMALL_SIZE_COL_NULL" ], "*.predeploy.sql": [ + "CS0456:USELESS_UNIT", "VU0507:SP_PROC_CALL", "VU0509:DYNAMIC_SQL", "VU0519:SYS_DYNAMIC_VIEW", @@ -626,15 +708,23 @@ "CD0726:CODE_IN_SCRIPT_ROOT", "FL0305:FILE_OBJECT_NAME", "MA0812:INSERT_EXEC", - "CD0833:PARTITIONING_RANGE_VALUES" + "CD0833:PARTITIONING_RANGE_VALUES", + "CD0857:SINGLE_OBJECT_PER_FILE", + "CS0886:EXTENDED_PROPERTY_MISDIRECTED", + "CS0890:OBJECT_PROPERTY_FIDDLING" ], "*.postdeploy.sql": [ + "CS0456:USELESS_UNIT", "CD0726:CODE_IN_SCRIPT_ROOT", "VU0509:DYNAMIC_SQL", "FL0305:FILE_OBJECT_NAME", - "MA0812:INSERT_EXEC" + "MA0812:INSERT_EXEC", + "CD0857:SINGLE_OBJECT_PER_FILE", + "CS0886:EXTENDED_PROPERTY_MISDIRECTED", + "CS0890:OBJECT_PROPERTY_FIDDLING" ], "*.revert.sql": [ + "CS0456:USELESS_UNIT", "VU0507:SP_PROC_CALL", "VU0509:DYNAMIC_SQL", "VU0519:SYS_DYNAMIC_VIEW", @@ -646,7 +736,10 @@ "CD0725:ALTER_TO_DEFINITION", "CD0726:CODE_IN_SCRIPT_ROOT", "FL0305:FILE_OBJECT_NAME", - "MA0812:INSERT_EXEC" + "MA0812:INSERT_EXEC", + "CD0857:SINGLE_OBJECT_PER_FILE", + "CS0886:EXTENDED_PROPERTY_MISDIRECTED", + "CS0890:OBJECT_PROPERTY_FIDDLING" ] } } diff --git a/TeamTools.TSQL.Linter/EvaluateConfig.json b/TeamTools.TSQL.Linter/EvaluateConfig.json index b33a3a04..744bb6a9 100644 --- a/TeamTools.TSQL.Linter/EvaluateConfig.json +++ b/TeamTools.TSQL.Linter/EvaluateConfig.json @@ -20,6 +20,10 @@ "FA0256:SEMICOLON_BEFORE_THROW": "warning", "FA0281:UNIQUE_DERIVED_DEF_COL_NAME": "error", "FA0283:INVALID_XML_LITERAL": "error", + "FA0451:UNRESOLVED_WINDOW_NAME": "error", + "FA0457:VARIABLE_REDECLARED": "error", + "FA0458:UNRESOLVED_VARIABLED_NAME": "error", + "FA0459:UNRESOLVED_TABLE_VAR_NAME": "error", "FA0703:BAD_TYPE_FOR_COLUMNSTORE": "error", "FA0704:INVALID_ARGUMENT_COUNT": "warning", "FA0705:INVALID_ARGUMENT": "hint", @@ -39,6 +43,8 @@ "FA0770:SPARSE_UNSUPPORTED_FEATURE": "error", "FA0771:SPARSE_UNSUPPORTED_TYPE": "error", "FA0822:INVALID_CLR_OPTION": "error", + "FA0884:INVALID_EXTENDED_PROPERTY_PARAM": "error", + "FA0885:RAISERROR_NEEDS_WITH_LOG": "error", "FA0901:DUP_COLUMN_MODIFIER": "error", "FA0902:CURSOR_OPTION_INCOMPATIBLE": "warning", "FA0904:INDEX_REFERS_UNKNOWN_COL": "error", @@ -74,18 +80,20 @@ "VU0511:JOB_MANAGEMENT": "warning", "VU0512:PRIVILEGE_MANAGEMENT": "warning", "VU0518:EXECUTE_AS_OWNER": "hint", - "VU0519:SYS_DYNAMIC_VIEW": "hint", + "VU0519:SYS_DYNAMIC_VIEW": "off", + "VU0525:OUTPUT_SECRET": "hint", + "VU0526:READ_SECRET": "hint", "DE0401:DEPRECATED_UNIT": "warning", - "DE0402:DEPRECATED_TYPE": "warning", + "DE0402:DEPRECATED_TYPE": "hint", "DE0403:DEPRECATED_INSTRUCTION": "warning", "DE0404:DEPRECATED_DBCC_COMMAND": "warning", "DE0405:DEPRECATED_OPTIONS": "warning", "DE0406:DEPRECATED_CONSTRAINT": "warning", - "DE0407:DEPRECATED_TOP_SYNTAX": "warning", + "DE0407:DEPRECATED_TOP_SYNTAX": "hint", "DE0408:DEPRECATED_SYS_VIEW": "warning", "DE0409:DEPRECATED_RAISERROR_SYNTAX": "warning", - "DE0273:HINT_SYNTAX": "warning", + "DE0273:HINT_SYNTAX": "hint", "DE0274:NUMBERED_SP": "warning", "DE0741:INDEX_OPTION_SYNTAX_DEPRECATED": "warning", "DE0819:MODIFY_WITH_NOLOCK": "warning", @@ -103,6 +111,7 @@ "AM0168:NONUNIQUE_COLUMN_ALIAS": "warning", "AM0169:NONUNIQUE_TABLE_ALIAS": "warning", "AM0170:TABLE_ALIAS_MIMICKS_OTHER_TABLE": "warning", + "AM0891:SELECT_NULL_TYPE": "off", "AM0903:SAME_VAR_MULTIPLE_OUTPUT": "warning", "AM0935:AMBIGUOUS_COL_SOURCE": "off", "AM0996:MULTI_SET_SAME_VAR": "warning", @@ -121,32 +130,33 @@ "RD0194:REDUNDANT_COALESCE_ARGUMENT": "hint", "RD0221:BEGIN_BEGIN_END_END": "off", "RD0228:GO_GO": "off", - "RD0235:WHITESPACE_IN_PARENTHESIS": "hint", + "RD0235:WHITESPACE_IN_PARENTHESIS": "off", "RD0236:REDUNDANT_NEWLINE": "off", - "RD0240:SL_COMMENT_LEADING_DASHES": "hint", - "RD0241:SL_COMMENT_TRAILING_DASHES": "hint", - "RD0242:ML_COMMENT_ASTERISK_COUNT": "hint", - "RD0247:REDUNDANT_PARENTHESIS": "hint", - "RD0248:REDUNDANT_SEMICOLON": "hint", - "RD0249:REDUNDANT_BRACKETS": "hint", - "RD0261:ML_COMMENT_REDUNDANT_NEWLINE": "hint", + "RD0240:SL_COMMENT_LEADING_DASHES": "off", + "RD0241:SL_COMMENT_TRAILING_DASHES": "off", + "RD0242:ML_COMMENT_ASTERISK_COUNT": "off", + "RD0247:REDUNDANT_PARENTHESIS": "off", + "RD0248:REDUNDANT_SEMICOLON": "off", + "RD0249:REDUNDANT_BRACKETS": "off", + "RD0261:ML_COMMENT_REDUNDANT_NEWLINE": "off", "RD0284:REDUNDANT_FUNCTION_CALL": "hint", "RD0286:REDUNDANT_NEGATION": "hint", "RD0287:REDUNDANT_NESTED_CASE": "hint", "RD0288:REDUNDANT_SELECT_SCALAR": "hint", - "RD0293:REDUNDANT_CASE_ELSE_NULL": "hint", + "RD0293:REDUNDANT_CASE_ELSE_NULL": "off", "RD0296:REDUNDANT_ORDER_BY_CONST": "warning", - "RD0307:EOF_REDUNDANT_NEWLINE": "hint", + "RD0307:EOF_REDUNDANT_NEWLINE": "off", + "RD0452:UNUSED_WINDOW_CLAUSE": "off", "RD0706:REDUNDANT_TYPE_CONVERSION": "hint", "RD0707:REDUNDANT_ARGUMENT": "hint", "RD0713:COLUMN_ALIAS_IS_THE_SAME": "hint", - "RD0716:IF_ELSE_REDUNDANT_BEGIN_END": "hint", + "RD0716:IF_ELSE_REDUNDANT_BEGIN_END": "off", "RD0719:SCALAR_PREDICATE_AS_EXISTS": "hint", "RD0721:NULL_HANDLING_FOR_CONCAT": "hint", "RD0724:REDUNDANT_INDEX_OPTION": "hint", "RD0730:REDUNDANT_CONTINUE": "hint", "RD0731:SIGNED_ZERO": "warning", - "RD0780:JOIN_PREDICATE_PARENTHETHIS": "hint", + "RD0780:JOIN_PREDICATE_PARENTHETHIS": "off", "RD0782:REDUNDANT_NESTED_CONDITION": "hint", "RD0784:REDUNDANT_INTERSECT_EXCEPT": "hint", "RD0798:REDUNDANT_INIT_NULL": "hint", @@ -154,8 +164,10 @@ "RD0814:IN_DUP_VAR": "warning", "RD0849:REDUNDANT_INDEX_FILTER": "hint", "RD0850:EXTRA_WHERE_PREDICATE": "hint", + "RD0882:REDUNDANT_MAX_RECURSION": "off", + "RD0883:REDUNDANT_ISNULL_NOT_EQUALS": "hint", "RD0925:REDUNDANT_LIKE": "warning", - "RD0926:REDUNDANT_NOT_FOR_REPLICATION": "hint", + "RD0926:REDUNDANT_NOT_FOR_REPLICATION": "off", "RD0927:REDUNDANT_COL_NULLABILITY_CHECK": "hint", "RD0934:CTE_UNUSED": "hint", "RD0943:REDUNDANT_AGGREGATE": "hint", @@ -171,6 +183,21 @@ "PF0720:SORTED_CTE": "warning", "PF0775:SPARSE_COL_INDEX_FILTER": "warning", "PF0823:RECOMPILE_RECOMPILE": "off", + "PF0867:INDEX_FK": "hint", + "PF0868:OPTIMIZER_FIGHT_JOIN_HINT": "off", + "PF0869:OPTIMIZER_FIGHT_TABLE_HINT": "off", + "PF0870:OPTIMIZER_FIGHT_QUERY_OPTION": "off", + "PF0871:OPTIMIZER_FIGHT_FORCE_PLAN": "off", + "PF0872:SERIAL_PLAN_FORCED": "off", + "PF0873:SERIAL_PLAN_FORCED_TABLE_VAR": "off", + "PF0874:SERIAL_PLAN_ZONE_FORCED": "off", + "PF0875:DDL_DML_MIX": "off", + "PF0876:EXPAND_DATE_FUNC": "warning", + "PF0877:EXPAND_ISNULL_FOR_OPTIONAL_ARG": "warning", + "PF0878:SINGLE_USER_MODE_ZONE_FORCED": "off", + "PF0879:TEMP_TABLE_CACHING_PREVENTED": "off", + "PF0880:NO_EQUALITY_FILTER_JOIN": "off", + "PF0881:NO_EQUALITY_FILTER_WHERE": "off", "PF0910:INDEXING_COL_WITH_DEFAULT": "hint", "PF0928:FILTERED_IDX_FOR_NULL_COL_NOT_INCLUDED": "warning", "PF0929:NON_SARGABLE_PREDICATE": "off", @@ -185,7 +212,7 @@ "MA0148:UNIQUE_ERR_NO_OR_STATE": "off", "MA0167:COMPUTED_OUTPUT_NO_ALIAS": "hint", "MA0174:CLOCK_BASED_CODE_FLOW": "off", - "MA0812:INSERT_EXEC": "hint", + "MA0812:INSERT_EXEC": "off", "MA0816:IN_VALUES_TOO_MANY": "hint", "MA0817:ZERO_ARGS": "off", "MA0818:TOO_MANY_ARGS": "off", @@ -204,7 +231,7 @@ "NM0203:CONSTRAINT_NAME_PATTERN": "off", "NM0204:VAR_NAME_MISSPELLED": "off", "NM0205:VAR_NAME_NOTATION_MIX": "off", - "NM0206:SYS_LIKE_NAME": "hint", + "NM0206:SYS_LIKE_NAME": "off", "NM0207:ALIAS_LENGTH": "off", "NM0222:ID_FOR_INT": "off", "NM0259:ALPHABET_MIX_IDENTIFIER": "hint", @@ -305,15 +332,23 @@ "CV0837:SP_XML_TO_XQUERY": "hint", "CV0838:SYSTEM_TYPE_UPPERCASE": "off", "CV0839:GLOBAL_VAR_UPPERCASE": "off", + "CV0858:FETCH_FULLY_QUALIFIED": "off", + "CV0897:SET_OPTIONS_ASC_ORDER": "off", - "SI0727:NOT_FOR_SIMPLE_PREDICATE": "hint", - "SI0735:SET_TO_DECLARE": "hint", - "SI0753:DROP_STATEMENTS_INTO_ONE": "hint", - "SI0754:ALTER_STATEMENTS_INTO_ONE": "hint", - "SI0845:MULTIPLE_OR_TO_IN": "hint", - "SI0846:MULTIPLE_AND_TO_NOT_IN": "hint", - "SI0847:MULTIPLE_IN_TO_SINGLE": "hint", - "SI0848:MULTIPLE_NOT_IN_TO_SINGLE": "hint", + "SI0453:EXTRACT_WINDOW_CLAUSE": "off", + "SI0454:REUSE_EXPRESSION_ALIAS": "hint", + "SI0455:EXTRACT_EXPRESSION": "off", + "SI0727:NOT_FOR_SIMPLE_PREDICATE": "off", + "SI0735:SET_TO_DECLARE": "off", + "SI0753:DROP_STATEMENTS_INTO_ONE": "off", + "SI0754:ALTER_STATEMENTS_INTO_ONE": "off", + "SI0845:MULTIPLE_OR_TO_IN": "off", + "SI0846:MULTIPLE_AND_TO_NOT_IN": "off", + "SI0847:MULTIPLE_IN_TO_SINGLE": "off", + "SI0848:MULTIPLE_NOT_IN_TO_SINGLE": "off", + "SI0859:MULTIPLE_INSERT_VALUES_COLLAPSE": "off", + "SI0889:USE_TYPE_NOT_PROPERTY": "off", + "SI0896:MULTIPLE_SET_INTO_ONE": "off", "DD0153:FK_MULTIPLE_COL": "hint", "DD0158:TABLE_ALL_COL_NULL": "hint", @@ -340,6 +375,14 @@ "DD0828:COMPUTED_COL_SYNONYM": "hint", "DD0829:FK_ON_TMP": "warning", "DD0831:HISTORY_IN_SAME_SCHEMA": "hint", + "DD0855:SCALAR_UDT": "off", + "DD0860:HISTORY_PERIOD_SET": "warning", + "DD0861:HISTORY_SET_STORAGE_NAME": "owarningff", + "DD0862:HISTORY_CONSISTENCY_CHECK": "warning", + "DD0863:HISTORY_SAME_DATE_RANGE_PRECISION": "warning", + "DD0864:HISTORY_FUTURE_DATE": "warning", + "DD0865:HISTORY_PERIOD_DEFAULT_PRECISION": "warning", + "DD0866:HISTORY_MAX_DATE_PRECISION": "hint", "DD0906:FK_RECURSION": "hint", "DD0908:NONCLUSTERED_IDX_INCLUDES_CLUSTERED": "hint", "DD0909:INDEX_DUP_COLUMN": "hint", @@ -352,38 +395,40 @@ "DM0513:STATISTICS_MANAGEMENT": "warning", "DM0515:DROPPING_TABLES": "warning", "DM0516:TRUNCATING_TABLES": "hint", - "DM0517:TABLE_PARTITIONING": "warning", + "DM0517:TABLE_PARTITIONING": "hint", "DM0729:INDEX_MAINTENANCE": "warning", "DM0752:FULLTEXT_CATALOG_MANAGEMENT": "warning", - "CD0213:UNNECESSARY_GRANTOR": "warning", - "CD0214:LIST_IN_CONSTRAINT": "warning", + "CD0213:UNNECESSARY_GRANTOR": "hint", + "CD0214:LIST_IN_CONSTRAINT": "off", "CD0215:COMPUTED_COLS_ORDER": "warning", - "CD0216:CAST_IN_CONSTRAINT": "warning", - "CD0277:DATE_FN_IN_CONSTRAINT": "warning", - "CD0280:IIF_IN_CONSTRAINT": "warning", - "CD0282:STRING_FN_IN_CONSTRAINT": "warning", - "CD0717:CREATE_OR_ALTER": "error", - "CD0725:ALTER_TO_DEFINITION": "warning", - "CD0726:CODE_IN_SCRIPT_ROOT": "warning", - "CD0779:EXECUTE_AS_SELF": "warning", + "CD0216:CAST_IN_CONSTRAINT": "off", + "CD0277:DATE_FN_IN_CONSTRAINT": "off", + "CD0280:IIF_IN_CONSTRAINT": "off", + "CD0282:STRING_FN_IN_CONSTRAINT": "off", + "CD0717:CREATE_OR_ALTER": "hint", + "CD0725:ALTER_TO_DEFINITION": "hint", + "CD0726:CODE_IN_SCRIPT_ROOT": "off", + "CD0779:EXECUTE_AS_SELF": "off", "CD0833:PARTITIONING_RANGE_VALUES": "warning", + "CD0857:SINGLE_OBJECT_PER_FILE": "warning", + "CD0895:CREATE_OPTIONS_INSIDE": "hint", - "FL0301:FILE_ENCODING": "hint", - "FL0303:CRLF": "hint", - "FL0304:FILE_NAME_SQUARE_BRACKETS": "hint", - "FL0305:FILE_OBJECT_NAME": "hint", - "FL0306:EOF_NEWLINE": "hint", - "FL0308:EOF_GO": "hint", - "FL0272:BOF_REDUNDANT_WHITESPACE": "hint", + "FL0301:FILE_ENCODING": "off", + "FL0303:CRLF": "off", + "FL0304:FILE_NAME_SQUARE_BRACKETS": "off", + "FL0305:FILE_OBJECT_NAME": "off", + "FL0306:EOF_NEWLINE": "off", + "FL0308:EOF_GO": "off", + "FL0272:BOF_REDUNDANT_WHITESPACE": "off", - "CS0101:SELECT_INTO": "warning", + "CS0101:SELECT_INTO": "hint", "CS0102:POSITIONAL_PROC_ARGS": "warning", "CS0103:INSERT_COLUMN_LIST": "warning", "CS0105:GOTO": "warning", "CS0107:TMP_NAMED_CONSTRAINT": "warning", "CS0111:VAR_TYPE_LENGTH": "warning", - "CS0112:SELECT_STAR": "hint", + "CS0112:SELECT_STAR": "off", "CS0118:AUTHORIZED_SCHEMA": "hint", "CS0125:GRANT_GRANTOR": "warning", "CS0126:BAD_ROWCOUNT_CHECK": "warning", @@ -396,8 +441,8 @@ "CS0141:DATETIME2_SYSDATETIME": "warning", "CS0142:PROC_RETURN_VALUE_REQUIRED": "hint", "CS0143:PROC_RETURN_REQUIRED_AFTER_CATCH": "hint", - "CS0146:SYSNAME_FOR_SCALAR_VAR": "hint", - "CS0147:SYSNAME_FOR_TABLE_COL": "hint", + "CS0146:SYSNAME_FOR_SCALAR_VAR": "off", + "CS0147:SYSNAME_FOR_TABLE_COL": "off", "CS0151:COMMIT_IN_CATCH": "warning", "CS0152:GROUPBY_DISTINCT": "warning", "CS0159:NON_ANSI_NULL_COMPARISON": "warning", @@ -425,6 +470,7 @@ "CS0266:TABLE_LEVEL_CONSTRAINT_IN_COL": "hint", "CS0295:ORDER_BY_POSITION": "hint", "CS0299:COMPLICATED_IIF_TO_CASE": "hint", + "CS0456:USELESS_UNIT": "hint", "CS0514:SHOWING_STATS": "warning", "CS0520:APP_LOCK": "warning", "CS0521:SYSPROC_RETURN_NOT_CHECKED": "warning", @@ -471,6 +517,14 @@ "CS0851:FAKE_OUTER_JOIN": "hint", "CS0852:NON_CORRELATED_JOIN_PREDICATE": "hint", "CS0853:COMPARISON_LEFT_EQUALS_RIGHT": "hint", + "CS0856:SELF_CALL": "hint", + "CS0886:EXTENDED_PROPERTY_MISDIRECTED": "off", + "CS0887:EXTENDED_PROPERTY_FOR_MISSING_COL": "off", + "CS0888:EXTENDED_PROPERTY_DUP": "off", + "CS0890:OBJECT_PROPERTY_FIDDLING": "off", + "CS0892:CAST_XML_TO_STRING": "hint", + "CS0893:ISOLATION_CONTRADICTION": "hint", + "CS0894:ISOLATION_CHAOS_PER_QUERY": "hint", "CS0905:VAR_LACKS_PRECISION": "warning", "CS0914:INTERSECT_EXCEPT_BROKEN_BY_LITERAL": "warning", "CS0917:FORBIDDEN_INSERT_HINTS": "off", @@ -478,7 +532,7 @@ "CS0920:UNPAIRED_TRAN_STATEMENT": "hint", "CS0921:UNPAIRED_XMLDOC_STATEMENT": "hint", "CS0922:PARAM_EXPECTED_AS_OUTPUT": "warning", - "CS0923:SORTED_INSERT": "warning", + "CS0923:SORTED_INSERT": "hint", "CS0924:PARAM_VALUE_IGNORED": "hint", "CS0930:NAME_REUSED_CURSOR": "hint", "CS0931:NAME_REUSED_TRANSACTION": "hint", @@ -537,6 +591,12 @@ "MA0812:INSERT_EXEC", "MA0817:ZERO_ARGS", "DD0825:SINGLE_COL_TABLE", + "PF0872:SERIAL_PLAN_FORCED", + "PF0873:SERIAL_PLAN_FORCED_TABLE_VAR", + "PF0874:SERIAL_PLAN_ZONE_FORCED", + "PF0875:DDL_DML_MIX", + "PF0881:NO_EQUALITY_FILTER_WHERE", + "AM0891:SELECT_NULL_TYPE", "DD0941:TABLE_HAS_PK", "PF0953:TOP_TAKES_ALL", "DD0997:BAD_TYPE_FOR_KEY", @@ -544,6 +604,7 @@ "DD0999:BLOATED_CLUSTERED_IDX" ], "test*.setup.sql": [ + "CS0456:USELESS_UNIT", "DD0158:TABLE_ALL_COL_NULL", "DM0515:DROPPING_TABLES", "DM0516:TRUNCATING_TABLES", @@ -558,11 +619,17 @@ "MA0812:INSERT_EXEC", "MA0817:ZERO_ARGS", "DD0825:SINGLE_COL_TABLE", + "PF0872:SERIAL_PLAN_FORCED", + "PF0873:SERIAL_PLAN_FORCED_TABLE_VAR", + "PF0874:SERIAL_PLAN_ZONE_FORCED", + "PF0875:DDL_DML_MIX", + "PF0881:NO_EQUALITY_FILTER_WHERE", "DD0997:BAD_TYPE_FOR_KEY", "DD0998:BAD_TYPE_FOR_CLUSTERED_IDX", "DD0999:BLOATED_CLUSTERED_IDX" ], "test*.stub*.sql": [ + "CS0456:USELESS_UNIT", "PF0953:TOP_TAKES_ALL", "CS0919:UNUSED_PARAMETER", "CS0748:IDENTITY_INSERT", @@ -570,6 +637,10 @@ "MA0812:INSERT_EXEC", "MA0817:ZERO_ARGS", "MA0818:TOO_MANY_ARGS", + "PF0872:SERIAL_PLAN_FORCED", + "PF0873:SERIAL_PLAN_FORCED_TABLE_VAR", + "PF0874:SERIAL_PLAN_ZONE_FORCED", + "PF0875:DDL_DML_MIX", "DD0997:BAD_TYPE_FOR_KEY", "DD0998:BAD_TYPE_FOR_CLUSTERED_IDX", "DD0158:TABLE_ALL_COL_NULL", @@ -577,6 +648,7 @@ ], "test*.mock*.sql": [ "DD0158:TABLE_ALL_COL_NULL", + "CS0456:USELESS_UNIT", "PF0953:TOP_TAKES_ALL", "CS0919:UNUSED_PARAMETER", "CS0748:IDENTITY_INSERT", @@ -584,11 +656,16 @@ "MA0812:INSERT_EXEC", "MA0817:ZERO_ARGS", "MA0818:TOO_MANY_ARGS", + "PF0872:SERIAL_PLAN_FORCED", + "PF0873:SERIAL_PLAN_FORCED_TABLE_VAR", + "PF0874:SERIAL_PLAN_ZONE_FORCED", + "PF0875:DDL_DML_MIX", "DD0941:TABLE_HAS_PK", "DD0997:BAD_TYPE_FOR_KEY", "DD0998:BAD_TYPE_FOR_CLUSTERED_IDX" ], "tsqlt.*.sql": [ + "CS0456:USELESS_UNIT", "AM0718:OBJECT_ID_WITHOUT_TYPE", "DM0515:DROPPING_TABLES", "DM0516:TRUNCATING_TABLES", @@ -604,6 +681,10 @@ "MA0812:INSERT_EXEC", "MA0817:ZERO_ARGS", "MA0818:TOO_MANY_ARGS", + "PF0872:SERIAL_PLAN_FORCED", + "PF0873:SERIAL_PLAN_FORCED_TABLE_VAR", + "PF0874:SERIAL_PLAN_ZONE_FORCED", + "PF0875:DDL_DML_MIX", "DD0997:BAD_TYPE_FOR_KEY", "DD0998:BAD_TYPE_FOR_CLUSTERED_IDX", "DD0999:BLOATED_CLUSTERED_IDX" @@ -614,6 +695,7 @@ "DD0755:SMALL_SIZE_COL_NULL" ], "*.predeploy.sql": [ + "CS0456:USELESS_UNIT", "VU0507:SP_PROC_CALL", "VU0509:DYNAMIC_SQL", "VU0519:SYS_DYNAMIC_VIEW", @@ -626,15 +708,23 @@ "CD0726:CODE_IN_SCRIPT_ROOT", "FL0305:FILE_OBJECT_NAME", "MA0812:INSERT_EXEC", - "CD0833:PARTITIONING_RANGE_VALUES" + "CD0833:PARTITIONING_RANGE_VALUES", + "CD0857:SINGLE_OBJECT_PER_FILE", + "CS0886:EXTENDED_PROPERTY_MISDIRECTED", + "CS0890:OBJECT_PROPERTY_FIDDLING" ], "*.postdeploy.sql": [ + "CS0456:USELESS_UNIT", "CD0726:CODE_IN_SCRIPT_ROOT", "VU0509:DYNAMIC_SQL", "FL0305:FILE_OBJECT_NAME", - "MA0812:INSERT_EXEC" + "MA0812:INSERT_EXEC", + "CD0857:SINGLE_OBJECT_PER_FILE", + "CS0886:EXTENDED_PROPERTY_MISDIRECTED", + "CS0890:OBJECT_PROPERTY_FIDDLING" ], "*.revert.sql": [ + "CS0456:USELESS_UNIT", "VU0507:SP_PROC_CALL", "VU0509:DYNAMIC_SQL", "VU0519:SYS_DYNAMIC_VIEW", @@ -646,7 +736,10 @@ "CD0725:ALTER_TO_DEFINITION", "CD0726:CODE_IN_SCRIPT_ROOT", "FL0305:FILE_OBJECT_NAME", - "MA0812:INSERT_EXEC" + "MA0812:INSERT_EXEC", + "CD0857:SINGLE_OBJECT_PER_FILE", + "CS0886:EXTENDED_PROPERTY_MISDIRECTED", + "CS0890:OBJECT_PROPERTY_FIDDLING" ] } } diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/AM0836.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/AM0836.md new file mode 100644 index 00000000..6e95eefd --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/AM0836.md @@ -0,0 +1,52 @@ + + +# COLUMNS_UPDATED function result may create ambiguity + +||| +|-|-| +| Id | **AM0836** +| Mnemo | COLUMNS_UPDATED +| Severity | ⚠ Warning +| Category | [Ambiguity](./Category_Ambiguity.md), [Triggers](./Group_Triggers.md) +| Source code | [ColumnsUpdatedRule.cs](../../../Rules/Ambiguity/ColumnsUpdatedRule.cs) + +## Cause + +This rule is triggered when the function COLUMNS_UPDATED() is used in a trigger. +

Use the function UPDATE() instead of COLUMNS_UPDATED().

+ +## Examples + +Bad + +```sql +CREATE TRIGGER Sales.Notificator +ON Sales.Customer +AFTER INSERT, UPDATE +AS +BEGIN + IF COLUMNS_UPDATED() & 2 > 0 + BEGIN + PRINT 'Some Warning'; + END; +END; +``` + +Good + +```sql +CREATE TRIGGER Sales.Notificator +ON Sales.Customer +AFTER INSERT, UPDATE +AS +BEGIN + IF UPDATE(LastName) + BEGIN + PRINT 'Some Warning'; + END; +END; +``` + +## Tips + +💡 The result of the COLUMNS_UPDATED() function depends on the ColumnID property of each column that may differ on the different servers. Use the function UPDATE() instead, here you can use a name of the column. diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/AM0891.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/AM0891.md new file mode 100644 index 00000000..8e8afb08 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/AM0891.md @@ -0,0 +1,36 @@ + + +# Ambiguous NULL column output type + +||| +|-|-| +| Id | **AM0891** +| Mnemo | SELECT_NULL_TYPE +| Severity | ℹ Hint +| Category | [Ambiguity](./Category_Ambiguity.md) +| Source code | [SelectNullTypeRule.cs](../../../Rules/Ambiguity/SelectNullTypeRule.cs) + +## Cause + +This rule is triggered when the output type of the NULL column is not explicitly specified. +

Explicitly specify the NULL column type.

+ +## Examples + +Bad + +```sql +SELECT + foo.title + , NULL AS start_time +FROM foo; +``` + +Good + +```sql +SELECT + foo.title + , CAST(NULL AS DATETIME2(7)) AS start_time +FROM foo; +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/CD0857.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CD0857.md new file mode 100644 index 00000000..61153f67 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CD0857.md @@ -0,0 +1,51 @@ + + +# Multiple objects are defined in the same file + +||| +|-|-| +| Id | **CD0857** +| Mnemo | SINGLE_OBJECT_PER_FILE +| Severity | ⚠ Warning +| Category | [ContinuousDelivery](./Category_ContinuousDelivery.md) +| Source code | [SingleObjectPerFileRule.cs](../../../Rules/ContinuousDeployment/SingleObjectPerFileRule.cs) + +## Cause + +This rule is triggered when multiple objects are defined in the same file. +

Distribute the object definitions across different files.

+ +## Examples + +Bad + +```sql +CREATE SCHEMA sch AUTHORIZATION dbo; +GO + +CREATE TABLE sch.Users +( + Id INT NOT NULL + , Name VARCHAR(50) NOT NULL +); +GO +``` + +Good + +```sql +CREATE TABLE sch.Users +( + Id INT NOT NULL + , Name VARCHAR(50) NOT NULL +); +GO + +CREATE NONCLUSTERED INDEX IX_sch_Users_Name + ON sch.Users (Name); +GO +``` + +## Tips + +💡 The rule ignores table modification and index creation in the table creation script. diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0851.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0851.md new file mode 100644 index 00000000..954413d3 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0851.md @@ -0,0 +1,38 @@ + + +# Join is defined as OUTER but seems to behave as INNER + +||| +|-|-| +| Id | **CS0851** +| Mnemo | FAKE_OUTER_JOIN +| Severity | ℹ Hint +| Category | [CodeSmell](./Category_CodeSmell.md) +| Source code | [FakeOuterJoinRule.cs](../../../Rules/CodeSmell/FakeOuterJoinRule.cs) + +## Cause + +This rule is triggered when join is defined as OUTER but seems to behave as INNER. +

Fix join type or filter.

+ +## Examples + +Bad + +```sql +SELECT * +FROM dbo.product_category AS pc +LEFT JOIN dbo.product_item AS pi + ON pi.category_id = pc.category_id +WHERE pi.is_deleted = 0; +``` + +Good + +```sql +SELECT * +FROM dbo.product_category AS pc +INNER JOIN dbo.product_item AS pi + ON pi.category_id = pc.category_id +WHERE pi.is_deleted = 0; +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0852.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0852.md new file mode 100644 index 00000000..e10ea5fc --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0852.md @@ -0,0 +1,38 @@ + + +# Join predicate is not correlated with the joined sources + +||| +|-|-| +| Id | **CS0852** +| Mnemo | NON_CORRELATED_JOIN_PREDICATE +| Severity | ℹ Hint +| Category | [CodeSmell](./Category_CodeSmell.md) +| Source code | [NonCorrelatedJoinPredicateRule.cs](../../../Rules/CodeSmell/NonCorrelatedJoinPredicateRule.cs) + +## Cause + +This rule is triggered when join predicate is not correlated with the joined sources. +

Fix the join predicate.

+ +## Examples + +Bad + +```sql +SELECT * +FROM dbo.product_category AS pc +INNER JOIN dbo.product_item AS pi + ON pi.is_deleted = 0 +WHERE pi.category_id = pc.category_id; +``` + +Good + +```sql +SELECT * +FROM dbo.product_category AS pc +INNER JOIN dbo.product_item AS pi + ON pi.is_deleted = 0 + AND pi.category_id = pc.category_id; +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0853.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0853.md new file mode 100644 index 00000000..2c2d1398 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0853.md @@ -0,0 +1,40 @@ + + +# Left part of comparison is similar to the right part + +||| +|-|-| +| Id | **CS0853** +| Mnemo | COMPARISON_LEFT_EQUALS_RIGHT +| Severity | ℹ Hint +| Category | [CodeSmell](./Category_CodeSmell.md) +| Source code | [ComparedExpressionsEqualRule.cs](../../../Rules/CodeSmell/ComparedExpressionsEqualRule.cs) + +## Cause + +This rule is triggered when the left part of comparison is similar to the right part. +

Fix the comparison.

+ +## Examples + +Bad + +```sql +SELECT * +FROM dbo.product_category AS pc +LEFT JOIN dbo.product_item AS pi + ON pi.category_id = pi.category_id; +``` + +Good + +```sql +SELECT * +FROM dbo.product_category AS pc +LEFT JOIN dbo.product_item AS pi + ON pi.category_id = pc.category_id; +``` + +## Tips + +💡 The rule ignores 1 = 1 and 0 = 0. diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0856.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0856.md new file mode 100644 index 00000000..c3f0da90 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0856.md @@ -0,0 +1,59 @@ + + +# Programmability invokes itself + +||| +|-|-| +| Id | **CS0856** +| Mnemo | SELF_CALL +| Severity | ℹ Hint +| Category | [CodeSmell](./Category_CodeSmell.md) +| Source code | [SelfCallRule.cs](../../../Rules/CodeSmell/SelfCallRule.cs) + +## Cause + +This rule is triggered when programmability invokes itself. +

Remove the recursive call.

+ +## Examples + +Bad + +```sql +CREATE FUNCTION dbo.inc +( + @id INT, + @value INT +) +RETURNS INT +AS +BEGIN + SET @id = @id + 1; + + IF (@value > 1) + BEGIN + RETURN dbo.inc(@id, @value - 1); + END; + + RETURN @id; +END; +GO +``` + +Good + +```sql +CREATE FUNCTION dbo.inc +( + @id INT, + @value INT +) +RETURNS INT +AS +BEGIN + SET @id = @id + @value; + + RETURN @id; +END; +GO +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0886.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0886.md new file mode 100644 index 00000000..5abf8c67 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0886.md @@ -0,0 +1,62 @@ + + +# Extended property is defined for wrong object + +||| +|-|-| +| Id | **CS0886** +| Mnemo | EXTENDED_PROPERTY_MISDIRECTED +| Severity | ⚠ Warning +| Category | [CodeSmell](./Category_CodeSmell.md) +| Source code | [ExtendedPropertyMisdirectedRule.cs](../../../Rules/CodeSmell/ExtendedPropertyMisdirectedRule.cs) + +## Cause + +This rule is triggered when the `sp_addextendedproperty` procedure in the object creation script refers to another object. +

Fix sp_addextendedproperty call.

+ +## Examples + +Bad + +```sql +CREATE TABLE dbo.users +( + id INT NOT NULL + , login VARCHAR(100) NOT NULL +); +GO + +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'dbo' + , @level1type = N'TABLE' + , @level1name = N'positions' + , @level2type = N'COLUMN' + , @level2name = N'id'; +GO +``` + +Good + +```sql +CREATE TABLE dbo.users +( + id INT NOT NULL + , login VARCHAR(100) NOT NULL +); +GO + +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'dbo' + , @level1type = N'TABLE' + , @level1name = N'users' + , @level2type = N'COLUMN' + , @level2name = N'id'; +GO +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0887.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0887.md new file mode 100644 index 00000000..35fd1f2d --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0887.md @@ -0,0 +1,62 @@ + + +# Extended property is defined for missing column + +||| +|-|-| +| Id | **CS0887** +| Mnemo | EXTENDED_PROPERTY_FOR_MISSING_COL +| Severity | ⚠ Warning +| Category | [CodeSmell](./Category_CodeSmell.md) +| Source code | [ExtendedPropertyAddressesMissingColumnRule.cs](../../../Rules/CodeSmell/ExtendedPropertyAddressesMissingColumnRule.cs) + +## Cause + +This rule is triggered when the `sp_addextendedproperty` procedure in the object creation script refers to missing column. +

Fix sp_addextendedproperty call.

+ +## Examples + +Bad + +```sql +CREATE TABLE dbo.users +( + id INT NOT NULL + , login VARCHAR(100) NOT NULL +); +GO + +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'dbo' + , @level1type = N'TABLE' + , @level1name = N'users' + , @level2type = N'COLUMN' + , @level2name = N'rest'; +GO +``` + +Good + +```sql +CREATE TABLE dbo.users +( + id INT NOT NULL + , login VARCHAR(100) NOT NULL +); +GO + +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'dbo' + , @level1type = N'TABLE' + , @level1name = N'users' + , @level2type = N'COLUMN' + , @level2name = N'login'; +GO +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0888.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0888.md new file mode 100644 index 00000000..5f8ca2f1 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0888.md @@ -0,0 +1,84 @@ + + +# Extended property duplicate + +||| +|-|-| +| Id | **CS0888** +| Mnemo | EXTENDED_PROPERTY_DUP +| Severity | ⚠ Warning +| Category | [CodeSmell](./Category_CodeSmell.md) +| Source code | [ExtendedPropertyDupRule.cs](../../../Rules/CodeSmell/ExtendedPropertyDupRule.cs) + +## Cause + +This rule is triggered when the `sp_addextendedproperty` procedure in the object creation script defines extended properties with the same names to the same object. +

Fix sp_addextendedproperty call.

+ +## Examples + +Bad + +```sql +CREATE TABLE dbo.users +( + id INT NOT NULL + , login VARCHAR(100) NOT NULL +); +GO + +EXEC sp_addextendedproperty + @name = N'First Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'dbo' + , @level1type = N'TABLE' + , @level1name = N'users' + , @level2type = N'COLUMN' + , @level2name = N'login'; +GO + +EXEC sp_addextendedproperty + @name = N'First Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'dbo' + , @level1type = N'TABLE' + , @level1name = N'users' + , @level2type = N'COLUMN' + , @level2name = N'login'; +GO +``` + +Good + +```sql +CREATE TABLE dbo.users +( + id INT NOT NULL + , login VARCHAR(100) NOT NULL +); +GO + +EXEC sp_addextendedproperty + @name = N'First Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'dbo' + , @level1type = N'TABLE' + , @level1name = N'users' + , @level2type = N'COLUMN' + , @level2name = N'login'; +GO + +EXEC sp_addextendedproperty + @name = N'Second Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'dbo' + , @level1type = N'TABLE' + , @level1name = N'users' + , @level2type = N'COLUMN' + , @level2name = N'login'; +GO +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0890.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0890.md new file mode 100644 index 00000000..12284087 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0890.md @@ -0,0 +1,41 @@ + + +# Accessing Database- or Server-level object properties + +||| +|-|-| +| Id | **CS0890** +| Mnemo | OBJECT_PROPERTY_FIDDLING +| Severity | ⚠ Warning +| Category | [CodeSmell](./Category_CodeSmell.md) +| Source code | [ObjectPropertyFiddlingRule.cs](../../../Rules/CodeSmell/ObjectPropertyFiddlingRule.cs) + +## Cause + +This rule is triggered when Database- or Server-level object properties is used. +

Get rid of access to Database- or Server-level object properties.

+ +## Examples + +Bad + +```sql +SELECT SERVERPROPERTY('MachineName'); +``` + +## Tips + +💡 This rule checks: + +- `OBJECTPROPERTY` +- `OBJECTPROPERTYEX` +- `COLUMNPROPERTY` +- `DATABASEPROPERTYEX` +- `FILEGROUPPROPERTY` +- `FILEPROPERTY` +- `FILEPROPERTYEX` +- `FULLTEXTCATALOGPROPERTY` +- `FULLTEXTSERVICEPROPERTY` +- `INDEXKEY_PROPERTY` +- `SERVERPROPERTY` +- `TYPEPROPERTY` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/CV0837.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CV0837.md new file mode 100644 index 00000000..772ff510 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CV0837.md @@ -0,0 +1,47 @@ + + +# Consider utilizing XQuery technique + +||| +|-|-| +| Id | **CV0837** +| Mnemo | SP_XML_TO_XQUERY +| Severity | ℹ Hint +| Category | [CodingConvention](./Category_CodingConvention.md) +| Source code | [SpXmlToXQueryRule.cs](../../../Rules/CodingConvention/SpXmlToXQueryRule.cs) + +## Cause + +This rule is triggered when the functions sp_xml_preparedocument and OPENXML() are used for XML parsing. +

Consider utilizing XQuery technique.

+ +## Examples + +Bad + +```sql +DECLARE + @idoc INT + , @dx VARCHAR(1000) = '111'; + +EXEC sp_xml_preparedocument @idoc OUTPUT, @doc; + +SELECT * +FROM + OPENXML(@idoc, '/ROOT/a', 1) + WITH (CustomerID VARCHAR(10), ContactName VARCHAR(20)); +``` + +Good + +```sql +DECLARE @x XML; + +SET @x = '111'; + +SELECT @x.query('/ROOT/a'); +``` + +## Tips + +💡 The XQuery can help you make your code simpler while working with XML in most cases. diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Ambiguity.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Ambiguity.md index 5ff5cd89..5e707401 100644 --- a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Ambiguity.md +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Ambiguity.md @@ -17,13 +17,15 @@ | [AM0168](./AM0168.md) | Non-unique column alias in output | [AM0169](./AM0169.md) | Non-unique table alias - possible reference ambiguity | [AM0170](./AM0170.md) | Table alias mimics other table - possible reference ambiguity -| [AM0903](./AM0903.md) | Multiple output to the same variable -| [AM0935](./AM0935.md) | Column missing table alias - ambiguous source table -| [AM0996](./AM0996.md) | Ambiguous variable modifications in one statement | [AM0291](./AM0291.md) | Missing CLUSTERED/NONCLUSTERED index option | [AM0702](./AM0702.md) | Ambiguous or redundant uniqueness definition | [AM0718](./AM0718.md) | Possible ambiguity: object type should be provided | [AM0728](./AM0728.md) | Possible ambiguity of negated complex expression. Use additional parenthesis. | [AM0740](./AM0740.md) | Possible ambiguity if schema omitted in ALTER or DROP +| [AM0836](./AM0836.md) | COLUMNS_UPDATED function result may create ambiguity +| [AM0891](./AM0891.md) | Ambiguous NULL column output type +| [AM0903](./AM0903.md) | Multiple output to the same variable +| [AM0935](./AM0935.md) | Column missing table alias - ambiguous source table +| [AM0996](./AM0996.md) | Ambiguous variable modifications in one statement [To docs homepage](./readme.md) diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_CodeSmell.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_CodeSmell.md index 060c2df2..7b09796a 100644 --- a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_CodeSmell.md +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_CodeSmell.md @@ -93,6 +93,14 @@ | [CS0842](./CS0842.md) | Comment contains non-printable character | [CS0843](./CS0843.md) | The expression doesn't fill the used system table | [CS0844](./CS0844.md) | All flow branches lead to the same behavior +| [CS0851](./CS0851.md) | Join is defined as OUTER but seems to behave as INNER +| [CS0852](./CS0852.md) | Join predicate is not correlated with the joined sources +| [CS0853](./CS0853.md) | Left part of comparison is similar to the right part +| [CS0856](./CS0856.md) | Programmability invokes itself +| [CS0886](./CS0886.md) | Extended property is defined for wrong object +| [CS0887](./CS0887.md) | Extended property is defined for missing column +| [CS0888](./CS0888.md) | Extended property duplicate +| [CS0890](./CS0890.md) | Accessing Database- or Server-level object properties | [CS0905](./CS0905.md) | Argument does not have requested details | [CS0914](./CS0914.md) | Different literals in INTERSECT/EXCEPT construction | [CS0917](./CS0917.md) | Forbidden INSERT hint is used diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_CodingConvention.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_CodingConvention.md index ef38bbca..41cbf6df 100644 --- a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_CodingConvention.md +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_CodingConvention.md @@ -40,6 +40,7 @@ | [CV0803](./CV0803.md) | System types should be used without schema | [CV0805](./CV0805.md) | PRINT in business-logic | [CV0810](./CV0810.md) | Stored procedure call should start with EXEC +| [CV0837](./CV0837.md) | Consider utilizing XQuery technique | [CV0838](./CV0838.md) | System type is not in UPPERCASE | [CV0839](./CV0839.md) | Global variable is not in UPPERCASE diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_ContinuousDelivery.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_ContinuousDelivery.md index 3d2aca55..a09becd9 100644 --- a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_ContinuousDelivery.md +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_ContinuousDelivery.md @@ -15,5 +15,6 @@ | [CD0725](./CD0725.md) | ALTER should be rewritten into table definition | [CD0726](./CD0726.md) | Unexpected code block in script root | [CD0779](./CD0779.md) | Execute as SELF is not welcome +| [CD0857](./CD0857.md) | Multiple objects are defined in the same file [To docs homepage](./readme.md) diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_DatabaseDesign.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_DatabaseDesign.md index b5f035cf..9005acb4 100644 --- a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_DatabaseDesign.md +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_DatabaseDesign.md @@ -27,6 +27,15 @@ | [DD0827](./DD0827.md) | Computed column result is constant | [DD0828](./DD0828.md) | Computed column mirrors other column | [DD0829](./DD0829.md) | Foreign keys with temporary tables +| [DD0831](./DD0831.md) | History table not on the same schema as the origin +| [DD0855](./DD0855.md) | User-defined scalar type derived from a system type +| [DD0860](./DD0860.md) | PERIOD is undefined on temporal table +| [DD0861](./DD0861.md) | History storage table name is undefined +| [DD0862](./DD0862.md) | DATA_CONSISTENCY_CHECK option is disabled +| [DD0863](./DD0863.md) | Different precision of historical period columns +| [DD0864](./DD0864.md) | Probably incorrect rounding start date of historical period +| [DD0865](./DD0865.md) | Precision of DEFAULT value is lower than precision of historical period column +| [DD0866](./DD0866.md) | Insufficient precision of historical period columns | [DD0906](./DD0906.md) | Recursive foreign key | [DD0908](./DD0908.md) | Nonclustered index includes clustered index columns | [DD0909](./DD0909.md) | Column is included in the index more than once diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Failure.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Failure.md index 3ce34d34..1fe7bfda 100644 --- a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Failure.md +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Failure.md @@ -35,6 +35,8 @@ | [FA0770](./FA0770.md) | SPARSE columns aren't supported the specified options or aren't allowed in the given construction | [FA0771](./FA0771.md) | Illegal datatype is used for SPARSE column | [FA0822](./FA0822.md) | Invalid option for CLR module +| [FA0884](./FA0884.md) | Invalid extended property editing parameter value +| [FA0885](./FA0885.md) | RAISERROR with severity 19 or higher missing WITH LOG option | [FA0901](./FA0901.md) | Column can be modified only once per statement | [FA0902](./FA0902.md) | Incompatible cursor options | [FA0904](./FA0904.md) | Index refers to unknown column diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Naming.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Naming.md index ec96b4d4..00c4652f 100644 --- a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Naming.md +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Naming.md @@ -16,6 +16,7 @@ | [NM0271](./NM0271.md) | Forbidden name @@, ## or similar | [NM0712](./NM0712.md) | Object of this type cannot be temporary | [NM0714](./NM0714.md) | Keyword is used for alias +| [NM0854](./NM0854.md) | Look-alike char mix in indentifier | [NM0961](./NM0961.md) | Index name violates naming pattern | [NM0962](./NM0962.md) | Trigger name violates naming pattern | [NM0963](./NM0963.md) | Table naming convention violation - lower_snake_case expected diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Performance.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Performance.md index 34ceff3b..a3eb5bcc 100644 --- a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Performance.md +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Performance.md @@ -11,6 +11,13 @@ | [PF0720](./PF0720.md) | Unexpected sorting of CTE output | [PF0775](./PF0775.md) | SPARSE column not filtered in index | [PF0823](./PF0823.md) | Double recompile requested +| [PF0868](./PF0868.md) | Manual query plan control via JOIN hint +| [PF0869](./PF0869.md) | Manual query plan control via table hint +| [PF0870](./PF0870.md) | Manual query plan control via query hint +| [PF0871](./PF0871.md) | Manual query plan control via USE PLAN or SET FORCEPLAN ON +| [PF0875](./PF0875.md) | DDL is mixed with DML +| [PF0876](./PF0876.md) | Date function is used instead of range filter +| [PF0877](./PF0877.md) | Non-SARGable ISNULL is used instead of OR | [PF0910](./PF0910.md) | Indexing column allowing NULL/with default defined without filter on default value | [PF0928](./PF0928.md) | Index column is filtered for NULL but not included into index | [PF0929](./PF0929.md) | Non-sargable predicate diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Redundancy.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Redundancy.md index ea7d744d..7c5195f9 100644 --- a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Redundancy.md +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Redundancy.md @@ -44,6 +44,7 @@ | [RD0811](./RD0811.md) | Redundant EXECUTE AS CALLER directive | [RD0814](./RD0814.md) | Variable is specified more than once for IN predicate | [RD0849](./RD0849.md) | The index constraint is already defined at the table level +| [RD0850](./RD0850.md) | The WHERE predicate was already applied at INNER JOIN level | [RD0925](./RD0925.md) | Redundant LIKE without wildcards | [RD0926](./RD0926.md) | Redundant NOT FOR REPLICATION option | [RD0927](./RD0927.md) | Nullability check constraint used instead of column attribute diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Simplification.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Simplification.md index 7adc481d..308cce3f 100644 --- a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Simplification.md +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Simplification.md @@ -12,5 +12,6 @@ | [SI0846](./SI0846.md) | Multiple inequality checks can be combined into single NOT IN predicate | [SI0847](./SI0847.md) | Multiple similar IN predicates can be combined into single one | [SI0848](./SI0848.md) | Multiple similar NOT IN predicates can be combined into single one +| [SI0889](./SI0889.md) | Object property requested instead of type constraint [To docs homepage](./readme.md) diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0825.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0825.md index d0ccda18..dd94fb38 100644 --- a/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0825.md +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0825.md @@ -12,7 +12,7 @@ ## Cause This rule is triggered if a table declaration consists of a single column. -

Add columns.

+

Add columns.

## Examples diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0831.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0831.md new file mode 100644 index 00000000..6f35db5f --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0831.md @@ -0,0 +1,48 @@ + + +# History table not on the same schema as the origin + +||| +|-|-| +| Id | **DD0831** +| Mnemo | HISTORY_IN_SAME_SCHEMA +| Severity | ℹ Hint +| Category | [DatabaseDesign](./Category_DatabaseDesign.md) +| Source code | [TemporalTableSameSchemaRule.cs](../../../Rules/DatabaseDesign/TemporalTableSameSchemaRule.cs) + +## Cause + +This rule is triggered when the history part of the temporal table belongs to a different schema. +

Specify the same schemas.

+ +## Examples + +Bad + +```sql +CREATE TABLE SchemaOne.Person +( + ParsonID INT NOT NULL PRIMARY KEY CLUSTERED + , FirstName VARCHAR(128) NOT NULL + , LastName VARCHAR(128) NOT NULL + , BirthDate DATE +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = SchemaTwo.PersonHistory)); +``` + +Good + +```sql +CREATE TABLE SchemaOne.Person +( + ParsonID INT NOT NULL PRIMARY KEY CLUSTERED + , FirstName VARCHAR(128) NOT NULL + , LastName VARCHAR(128) NOT NULL + , BirthDate DATE +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = SchemaOne.PersonHistory)); +``` + +## Tips + +💡 Usually it is not needed to create the history part of a temporal table in another schema, please check it. diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0855.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0855.md new file mode 100644 index 00000000..555f2afa --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0855.md @@ -0,0 +1,24 @@ + + +# User-defined scalar type derived from a system type + +||| +|-|-| +| Id | **DD0855** +| Mnemo | SCALAR_UDT +| Severity | ℹ Hint +| Category | [DatabaseDesign](./Category_DatabaseDesign.md) +| Source code | [ScalarUdtRule.cs](../../../Rules/DatabaseDesign/ScalarUdtRule.cs) + +## Cause + +This rule is triggered when user-defined scalar type derived from a system type. +

Remove user-defined scalar type derived from a system type.

+ +## Examples + +Bad + +```sql +CREATE TYPE Id FROM INT; +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0860.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0860.md new file mode 100644 index 00000000..fd174350 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0860.md @@ -0,0 +1,47 @@ + + +# PERIOD is undefined on temporal table + +||| +|-|-| +| Id | **DD0860** +| Mnemo | HISTORY_PERIOD_SET +| Severity | ⚠ Warning +| Category | [DatabaseDesign](./Category_DatabaseDesign.md) +| Source code | [TemporalTablePeriodDefinedRule.cs](../../../Rules/DatabaseDesign/TemporalTablePeriodDefinedRule.cs) + +## Cause + +This rule is triggered when `PERIOD` is undefined on temporal table. +

Specify PERIOD.

+ +## Examples + +Bad + +```sql +CREATE TABLE dbo.Person +( + ParsonID INT NOT NULL PRIMARY KEY CLUSTERED + , FirstName VARCHAR(128) NOT NULL + , LastName VARCHAR(128) NOT NULL + , BirthDate DATE NOT NULL +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.PersonHistory)); +``` + +Good + +```sql +CREATE TABLE dbo.Person +( + ParsonID INT NOT NULL PRIMARY KEY CLUSTERED + , FirstName VARCHAR(128) NOT NULL + , LastName VARCHAR(128) NOT NULL + , BirthDate DATE NOT NULL + , SysStartTime DATETIME2(7) GENERATED ALWAYS AS ROW START NOT NULL + , SysEndTime DATETIME2(7) GENERATED ALWAYS AS ROW END NOT NULL + , PERIOD FOR SYSTEM_TIME(SysStartTime, SysEndTime) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.PersonHistory)); +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0861.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0861.md new file mode 100644 index 00000000..58c1cb15 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0861.md @@ -0,0 +1,50 @@ + + +# History storage table name is undefined + +||| +|-|-| +| Id | **DD0861** +| Mnemo | HISTORY_SET_STORAGE_NAME +| Severity | ⚠ Warning +| Category | [DatabaseDesign](./Category_DatabaseDesign.md) +| Source code | [TemporalTableNameHistoryTableRule.cs](../../../Rules/DatabaseDesign/TemporalTableNameHistoryTableRule.cs) + +## Cause + +This rule is triggered when history storage table name is undefined on temporal table. +

Specify history storage table name.

+ +## Examples + +Bad + +```sql +CREATE TABLE dbo.Person +( + ParsonID INT NOT NULL PRIMARY KEY CLUSTERED + , FirstName VARCHAR(128) NOT NULL + , LastName VARCHAR(128) NOT NULL + , BirthDate DATE NOT NULL + , SysStartTime DATETIME2(7) GENERATED ALWAYS AS ROW START NOT NULL + , SysEndTime DATETIME2(7) GENERATED ALWAYS AS ROW END NOT NULL + , PERIOD FOR SYSTEM_TIME(SysStartTime, SysEndTime) +) +WITH (SYSTEM_VERSIONING = ON); +``` + +Good + +```sql +CREATE TABLE dbo.Person +( + ParsonID INT NOT NULL PRIMARY KEY CLUSTERED + , FirstName VARCHAR(128) NOT NULL + , LastName VARCHAR(128) NOT NULL + , BirthDate DATE NOT NULL + , SysStartTime DATETIME2(7) GENERATED ALWAYS AS ROW START NOT NULL + , SysEndTime DATETIME2(7) GENERATED ALWAYS AS ROW END NOT NULL + , PERIOD FOR SYSTEM_TIME(SysStartTime, SysEndTime) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.PersonHistory)); +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0862.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0862.md new file mode 100644 index 00000000..8cb8c4b4 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0862.md @@ -0,0 +1,50 @@ + + +# DATA_CONSISTENCY_CHECK option is disabled + +||| +|-|-| +| Id | **DD0862** +| Mnemo | HISTORY_CONSISTENCY_CHECK +| Severity | ⚠ Warning +| Category | [DatabaseDesign](./Category_DatabaseDesign.md) +| Source code | [TemporalTableConsistencyCheckRule.cs](../../../Rules/DatabaseDesign/TemporalTableConsistencyCheckRule.cs) + +## Cause + +This rule is triggered when `DATA_CONSISTENCY_CHECK` option is disabled on temporal table. +

Enable DATA_CONSISTENCY_CHECK.

+ +## Examples + +Bad + +```sql +CREATE TABLE dbo.Person +( + ParsonID INT NOT NULL PRIMARY KEY CLUSTERED + , FirstName VARCHAR(128) NOT NULL + , LastName VARCHAR(128) NOT NULL + , BirthDate DATE NOT NULL + , SysStartTime DATETIME2(7) GENERATED ALWAYS AS ROW START NOT NULL + , SysEndTime DATETIME2(7) GENERATED ALWAYS AS ROW END NOT NULL + , PERIOD FOR SYSTEM_TIME(SysStartTime, SysEndTime) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.PersonHistory, DATA_CONSISTENCY_CHECK = OFF)); +``` + +Good + +```sql +CREATE TABLE dbo.Person +( + ParsonID INT NOT NULL PRIMARY KEY CLUSTERED + , FirstName VARCHAR(128) NOT NULL + , LastName VARCHAR(128) NOT NULL + , BirthDate DATE NOT NULL + , SysStartTime DATETIME2(7) GENERATED ALWAYS AS ROW START NOT NULL + , SysEndTime DATETIME2(7) GENERATED ALWAYS AS ROW END NOT NULL + , PERIOD FOR SYSTEM_TIME(SysStartTime, SysEndTime) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.PersonHistory)); +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0863.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0863.md new file mode 100644 index 00000000..c836ad04 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0863.md @@ -0,0 +1,50 @@ + + +# Different precision of historical period columns + +||| +|-|-| +| Id | **DD0863** +| Mnemo | HISTORY_SAME_DATE_RANGE_PRECISION +| Severity | ⚠ Warning +| Category | [DatabaseDesign](./Category_DatabaseDesign.md) +| Source code | [TemporalTableSimilarDateRangePrecision.cs](../../../Rules/DatabaseDesign/TemporalTableSimilarDateRangePrecision.cs) + +## Cause + +This rule is triggered when different precision are specified for the historical period columns on temporal table. +

Specify the same precision for historical period columns.

+ +## Examples + +Bad + +```sql +CREATE TABLE dbo.Person +( + ParsonID INT NOT NULL PRIMARY KEY CLUSTERED + , FirstName VARCHAR(128) NOT NULL + , LastName VARCHAR(128) NOT NULL + , BirthDate DATE NOT NULL + , SysStartTime DATETIME2(3) GENERATED ALWAYS AS ROW START NOT NULL + , SysEndTime DATETIME2(7) GENERATED ALWAYS AS ROW END NOT NULL + , PERIOD FOR SYSTEM_TIME(SysStartTime, SysEndTime) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.PersonHistory)); +``` + +Good + +```sql +CREATE TABLE dbo.Person +( + ParsonID INT NOT NULL PRIMARY KEY CLUSTERED + , FirstName VARCHAR(128) NOT NULL + , LastName VARCHAR(128) NOT NULL + , BirthDate DATE NOT NULL + , SysStartTime DATETIME2(7) GENERATED ALWAYS AS ROW START NOT NULL + , SysEndTime DATETIME2(7) GENERATED ALWAYS AS ROW END NOT NULL + , PERIOD FOR SYSTEM_TIME(SysStartTime, SysEndTime) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.PersonHistory)); +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0864.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0864.md new file mode 100644 index 00000000..b0bbd816 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0864.md @@ -0,0 +1,54 @@ + + +# Probably incorrect rounding start date of historical period + +||| +|-|-| +| Id | **DD0864** +| Mnemo | HISTORY_FUTURE_DATE +| Severity | ⚠ Warning +| Category | [DatabaseDesign](./Category_DatabaseDesign.md) +| Source code | [TemporalTablePossibleFutureDateRule.cs](../../../Rules/DatabaseDesign/TemporalTablePossibleFutureDateRule.cs) + +## Cause + +This rule is triggered when the start date of a historical period is set to a non-deterministic `DEFAULT` value with a greater precision than the column's precision. +

Use DATETIME2(7) for historical period columns, or a negative offset for the DEFAULT value.

+ +## Examples + +Bad + +```sql +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(2) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time DEFAULT SYSUTCDATETIME() + , sys_end_time DATETIME2(2) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO +``` + +Good + +```sql +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(2) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time DEFAULT DATEADD(SECOND, -1, SYSUTCDATETIME()) + , sys_end_time DATETIME2(2) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO +``` + +## Tips + +💡 When DATETIME/DATETIME2 casting to a type with a precision lower than the original, rounding up (to a date in the future) is likely to occur, which will cause an error. diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0865.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0865.md new file mode 100644 index 00000000..4ecf87fb --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0865.md @@ -0,0 +1,50 @@ + + +# Precision of DEFAULT value is lower than precision of historical period column + +||| +|-|-| +| Id | **DD0865** +| Mnemo | HISTORY_PERIOD_DEFAULT_PRECISION +| Severity | ⚠ Warning +| Category | [DatabaseDesign](./Category_DatabaseDesign.md) +| Source code | [TemporalTableRangeDefaultPrecisionLackRule.cs](../../../Rules/DatabaseDesign/TemporalTableRangeDefaultPrecisionLackRule.cs) + +## Cause + +This rule is triggered when precision of DEFAULT value is lower than precision of historical period column. +

Use data types of similar precision for columns and DEFAULT values.

+ +## Examples + +Bad + +```sql +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(7) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time DEFAULT GETDATE() + , sys_end_time DATETIME2(7) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_end_time DEFAULT CONVERT(DATETIME2(3), '9999-12-31') + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO +``` + +Good + +```sql +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(7) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time DEFAULT SYSUTCDATETIME() + , sys_end_time DATETIME2(7) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_end_time DEFAULT CONVERT(DATETIME2(7), '9999-12-31 23:59:59.9999999') + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0866.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0866.md new file mode 100644 index 00000000..b6c29818 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/DD0866.md @@ -0,0 +1,50 @@ + + +# Insufficient precision of historical period columns + +||| +|-|-| +| Id | **DD0866** +| Mnemo | HISTORY_MAX_DATE_PRECISION +| Severity | ℹ Hint +| Category | [DatabaseDesign](./Category_DatabaseDesign.md) +| Source code | [TemporalTableRangeMaxPrecisionRule.cs](../../../Rules/DatabaseDesign/TemporalTableRangeMaxPrecisionRule.cs) + +## Cause + +This rule is triggered when precision of historical period column is lower than `DATETIME2(7)`. +

Use DATETIME2(7) for historical period columns.

+ +## Examples + +Bad + +```sql +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(3) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL + , sys_end_time DATETIME2(3) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO +``` + +Good + +```sql +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(7) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL + , sys_end_time DATETIME2(7) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/FA0884.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/FA0884.md new file mode 100644 index 00000000..fa8a61ff --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/FA0884.md @@ -0,0 +1,58 @@ + + +# Invalid extended property editing parameter value + +||| +|-|-| +| Id | **FA0884** +| Mnemo | INVALID_EXTENDED_PROPERTY_PARAM +| Severity | ⛔ Error +| Category | [Failure](./Category_Failure.md) +| Source code | [InvalidExtendedPropertyParameterRule.cs](../../../Rules/Failure/InvalidExtendedPropertyParameterRule.cs) + +## Cause + +This rule is triggered when the invalid argument value is specified for the extended properties editing procedure. +

Specify a valid argument value.

+ +## Examples + +Bad + +```sql +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'UNKNOWN' + , @level0name = N'my_schema' + , @level1type = N'TABLE' + , @level1name = N'my_table' + , @level2type = N'COLUMN' + , @level2name = N'my_column'; +``` + +Good + +```sql +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'my_schema' + , @level1type = N'TABLE' + , @level1name = N'my_table' + , @level2type = N'COLUMN' + , @level2name = N'my_column'; +``` + +## Tips + +💡 This rule checks: + +- sp_addextendedproperty +- sp_updateextendedproperty +- sp_dropextendedproperty + +## Links + +[sp_addextendedproperty](https://learn.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-addextendedproperty-transact-sql?view=sql-server-ver17#----level0type) diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/FA0885.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/FA0885.md new file mode 100644 index 00000000..c473635c --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/FA0885.md @@ -0,0 +1,30 @@ + + +# RAISERROR with severity 19 or higher missing WITH LOG option + +||| +|-|-| +| Id | **FA0885** +| Mnemo | RAISERROR_NEEDS_WITH_LOG +| Severity | ⛔ Error +| Category | [Failure](./Category_Failure.md) +| Source code | [RaiseErrorNeedsWithLogRule.cs](../../../Rules/Failure/RaiseErrorNeedsWithLogRule.cs) + +## Cause + +This rule is triggered when `RAISERROR` with severity 19 or higher is specified without `WITH LOG`. +

Specify WITH LOG option.

+ +## Examples + +Bad + +```sql +RAISERROR ('error', 19, 2); +``` + +Good + +```sql +RAISERROR ('error', 19, 2) WITH LOG; +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Group_Triggers.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Group_Triggers.md index 342a3e37..3e3258a8 100644 --- a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Group_Triggers.md +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Group_Triggers.md @@ -5,6 +5,7 @@ ||| |-|-| | [AM0162](./AM0162.md) | Ambiguous system table reference: INSERTED/DELETED from OUTPUT or from the trigger +| [AM0836](./AM0836.md) | COLUMNS_UPDATED function result may create ambiguity | [CS0139](./CS0139.md) | Trigger ordering specified | [CS0163](./CS0163.md) | Unexpected data fetching from a trigger | [CS0186](./CS0186.md) | RAISERROR is used instead of THROW in trigger diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/NM0854.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/NM0854.md new file mode 100644 index 00000000..4204bcfe --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/NM0854.md @@ -0,0 +1,36 @@ + + +# Look-alike char mix in indentifier + +||| +|-|-| +| Id | **NM0854** +| Mnemo | IDENTIFIER_LOOK_ALIKE_CHAR +| Severity | ℹ Hint +| Category | [Naming](./Category_Naming.md) +| Source code | [IdentifierContainsLookAlikeCharRule.cs](../../../Rules/Naming/IdentifierContainsLookAlikeCharRule.cs) + +## Cause + +This rule is triggered when in the indentifier, typed primarily in characters of one alphabet, contains a character from another alphabet that has a visually similar counterpart in the first alphabet. +

Rewrite using characters of only one alphabet.

+ +## Examples + +Bad + +```sql +SELECT 'John' AS Name; -- cyrillic "a" + +CREATE TYPE [Cтрока] -- latin "c" +FROM VARCHAR(MAX); +``` + +Good + +```sql +SELECT 'John' AS Name; -- latin only + +CREATE TYPE [Строка] -- cyrillic only +FROM VARCHAR(MAX); +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0868.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0868.md new file mode 100644 index 00000000..63c8cbd7 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0868.md @@ -0,0 +1,40 @@ + + +# Manual query plan control via JOIN hint + +||| +|-|-| +| Id | **PF0868** +| Mnemo | OPTIMIZER_FIGHT_JOIN_HINT +| Severity | ℹ Hint +| Category | [Performance](./Category_Performance.md) +| Source code | [FightOptimizerByJoinHintRule.cs](../../../Rules/Performance/FightOptimizerByJoinHintRule.cs) + +## Cause + +This rule is triggered when the JOIN hint is specified in the query. +

Remove JOIN hint.

+ +## Examples + +Bad + +```sql +SELECT + emp.firts_name + , pos.name +FROM dbo.employees AS emp +INNER MERGE JOIN dbo.positions AS pos + ON pos.position_id = emp.position_id; +``` + +Good + +```sql +SELECT + emp.firts_name + , pos.name +FROM dbo.employees AS emp +INNER JOIN dbo.positions AS pos + ON pos.position_id = emp.position_id; +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0869.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0869.md new file mode 100644 index 00000000..33175b5c --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0869.md @@ -0,0 +1,40 @@ + + +# Manual query plan control via table hint + +||| +|-|-| +| Id | **PF0869** +| Mnemo | OPTIMIZER_FIGHT_TABLE_HINT +| Severity | ℹ Hint +| Category | [Performance](./Category_Performance.md) +| Source code | [FightOptimizerByTableHintRule.cs](../../../Rules/Performance/FightOptimizerByTableHintRule.cs) + +## Cause + +This rule is triggered when `FORCESEEK`, `FORCESCAN`, or `WITH INDEX` is specified in the query. +

Remove table hint.

+ +## Examples + +Bad + +```sql +SELECT + emp.firts_name + , pos.name +FROM dbo.employees AS emp WITH (FORCESEEK) +INNER JOIN dbo.positions AS pos + ON pos.position_id = emp.position_id; +``` + +Good + +```sql +SELECT + emp.firts_name + , pos.name +FROM dbo.employees AS emp +INNER JOIN dbo.positions AS pos + ON pos.position_id = emp.position_id; +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0870.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0870.md new file mode 100644 index 00000000..c72ddc34 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0870.md @@ -0,0 +1,45 @@ + + +# Manual query plan control via query hint + +||| +|-|-| +| Id | **PF0870** +| Mnemo | OPTIMIZER_FIGHT_QUERY_OPTION +| Severity | ℹ Hint +| Category | [Performance](./Category_Performance.md) +| Source code | [FightOptimizerByQueryOptionRule.cs](../../../Rules/Performance/FightOptimizerByQueryOptionRule.cs) + +## Cause + +This rule is triggered when the query hint `OPTION` is specified in the query. +

Remove query hint.

+ +## Examples + +Bad + +```sql +SELECT + emp.firts_name + , pos.name +FROM dbo.employees AS emp +INNER JOIN dbo.positions AS pos + ON pos.position_id = emp.position_id +OPTION (FORCE ORDER); +``` + +Good + +```sql +SELECT + emp.firts_name + , pos.name +FROM dbo.employees AS emp +INNER JOIN dbo.positions AS pos + ON pos.position_id = emp.position_id; +``` + +## Tips + +💡 This rule ignores RECOMPILE, MAXRECURSION and USE PLAN. diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0871.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0871.md new file mode 100644 index 00000000..5d6b6521 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0871.md @@ -0,0 +1,43 @@ + + +# Manual query plan control via USE PLAN or SET FORCEPLAN ON + +||| +|-|-| +| Id | **PF0871** +| Mnemo | OPTIMIZER_FIGHT_FORCE_PLAN +| Severity | ℹ Hint +| Category | [Performance](./Category_Performance.md) +| Source code | [FightOptimizerByForcePlanRule.cs](../../../Rules/Performance/FightOptimizerByForcePlanRule.cs) + +## Cause + +This rule is triggered when `USE PLAN`, or `SET FORCEPLAN ON` is specified in the query. +

Remove `USE PLAN` and `SET FORCEPLAN ON`.

+ +## Examples + +Bad + +```sql +SET FORCEPLAN ON; + +SELECT + emp.firts_name + , pos.name +FROM dbo.employees AS emp +INNER JOIN dbo.positions AS pos + ON pos.position_id = emp.position_id +OPTION (USE PLAN ''); +``` + +Good + +```sql +SELECT + emp.firts_name + , pos.name +FROM dbo.employees AS emp +INNER JOIN dbo.positions AS pos + ON pos.position_id = emp.position_id; +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0875.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0875.md new file mode 100644 index 00000000..1ed4142a --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0875.md @@ -0,0 +1,56 @@ + + +# DDL is mixed with DML + +||| +|-|-| +| Id | **PF0875** +| Mnemo | DDL_DML_MIX +| Severity | ℹ Hint +| Category | [Performance](./Category_Performance.md) +| Source code | [MixOfDdlAndDmlRule.cs](../../../Rules/Performance/MixOfDdlAndDmlRule.cs) + +## Cause + +This rule is triggered when DDL is mixed with DML. +

Move all DDL commands to the beginning of the query.

+ +## Examples + +Bad + +```sql +CREATE PROC dbo.delete_data +AS +BEGIN + CREATE TABLE #t (id INT); + + SELECT br.id FROM dbo.bar AS br INNER JOIN #t AS tt ON tt.id = br.parent_id; + + CREATE INDEX ix ON #t (id); + + DELETE bar FROM dbo.bar AS br INNER JOIN #t AS tt ON tt.id = br.parent_id; +END; +GO +``` + +Good + +```sql +CREATE PROC dbo.delete_data +AS +BEGIN + CREATE TABLE #t (id INT); + + CREATE INDEX ix ON #t (id); + + SELECT br.id FROM dbo.bar AS br INNER JOIN #t AS tt ON tt.id = br.parent_id; + + DELETE bar FROM dbo.bar AS br INNER JOIN #t AS tt ON tt.id = br.parent_id; +END; +GO +``` + +## Tips + +💡 This rule ignores queries without a source such as STRING_SPLIT, OPENJSON, OPENXML and manipulations of table variables. diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0876.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0876.md new file mode 100644 index 00000000..a09e67bb --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0876.md @@ -0,0 +1,35 @@ + + +# Date function is used instead of range filter + +||| +|-|-| +| Id | **PF0876** +| Mnemo | EXPAND_DATE_FUNC +| Severity | ⚠ Warning +| Category | [Performance](./Category_Performance.md) +| Source code | [ExpandDateFunctionRule.cs](../../../Rules/Performance/ExpandDateFunctionRule.cs) + +## Cause + +This rule is triggered when the date function is used instead of range filter in the predicate. +

Use range filter instead of date function in the predicate.

+ +## Examples + +Bad + +```sql +SELECT sum(payment) +FROM taxes +WHERE YEAR(pay_period) = @tax_year; +``` + +Good + +```sql +SELECT sum(payment) +FROM taxes +WHERE pay_period >= @tax_year_begin + AND pay_period < @next_tax_year +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0877.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0877.md new file mode 100644 index 00000000..90e6e65f --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0877.md @@ -0,0 +1,34 @@ + + +# Non-SARGable ISNULL is used instead of OR + +||| +|-|-| +| Id | **PF0877** +| Mnemo | EXPAND_ISNULL_FOR_OPTIONAL_ARG +| Severity | ⚠ Warning +| Category | [Performance](./Category_Performance.md) +| Source code | [OptionalParameterIsNullPredicateRule.cs](../../../Rules/Performance/OptionalParameterIsNullPredicateRule.cs) + +## Cause + +This rule is triggered when the predicate uses ISNULL instead of OR to handle the optional parameter logic. +

Use OR instead of ISNULL.

+ +## Examples + +Bad + +```sql +SELECT ord.id +FROM dbo.orders AS ord +WHERE o.client_id = ISNULL(@client_id, ord.client_id); +``` + +Good + +```sql +SELECT ord.id +FROM dbo.orders AS ord +WHERE ord.client_id = @client_id OR @client_id IS NULL; +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/RD0850.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/RD0850.md new file mode 100644 index 00000000..68bdc762 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/RD0850.md @@ -0,0 +1,39 @@ + + +# The WHERE predicate was already applied at INNER JOIN level + +||| +|-|-| +| Id | **RD0850** +| Mnemo | EXTRA_WHERE_PREDICATE +| Severity | ℹ Hint +| Category | [Redundancy](./Category_Redundancy.md) +| Source code | [WherePredicateSameAsJoinPredicateRule.cs](../../../Rules/Redundancy/WherePredicateSameAsJoinPredicateRule.cs) + +## Cause + +This rule is triggered when the `WHERE` predicate was already applied at `INNER JOIN` level. +

Remove the redundant predicate.

+ +## Examples + +Bad + +```sql +SELECT * +FROM dbo.product_category AS pc +INNER JOIN dbo.product_item AS pi + ON pi.category_id = pc.category_id + AND pi.is_deleted = 0 +WHERE pi.is_deleted = 0; +``` + +Good + +```sql +SELECT * +FROM dbo.product_category AS pc +INNER JOIN dbo.product_item AS pi + ON pi.category_id = pc.category_id + AND pi.is_deleted = 0; +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/SI0889.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/SI0889.md new file mode 100644 index 00000000..4d0a8367 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/SI0889.md @@ -0,0 +1,36 @@ + + +# Object property requested instead of type constraint + +||| +|-|-| +| Id | **SI0889** +| Mnemo | USE_TYPE_NOT_PROPERTY +| Severity | ℹ Hint +| Category | [Simplification](./Category_Simplification.md) +| Source code | [AskingPropertyNotTypeRule.cs](../../../Rules/Simplification/AskingPropertyNotTypeRule.cs) + +## Cause + +This rule is triggered when instead of passing a type to `OBJECT_ID` a construct like `OBJECT_PROPERTY(OBJECT_ID(...))` is used. +

Pass a type in OBJECT_ID and remove the redundant OBJECT_PROPERTY.

+ +## Examples + +Bad + +```sql +IF (OBJECTPROPERTY(OBJECT_ID('dbo.table'), 'IsTable') = 1) +BEGIN + ... +END; +``` + +Good + +```sql +IF OBJECT_ID('dbo.table', 'U') IS NOT NULL +BEGIN + ... +END; +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/AM0836.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/AM0836.md new file mode 100644 index 00000000..462117c7 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/AM0836.md @@ -0,0 +1,52 @@ + + +# Функция COLUMNS_UPDATED может давать неоднозначный результат + +||| +|-|-| +| Id | **AM0836** +| Мнемо | COLUMNS_UPDATED +| Серьёзность | ⚠ Предупреждение +| Категория | [Неоднозначность](./Категория_Неоднозначность.md), [Триггеры](./Группа_Триггеры.md) +| Исходный код | [ColumnsUpdatedRule.cs](../../../Rules/Ambiguity/ColumnsUpdatedRule.cs) + +## Причина + +Правило срабатывает, если в триггере была использована функция COLUMNS_UPDATED(). +

Используйте функцию UPDATE().

+ +## Примеры + +Некорректно + +```sql +CREATE TRIGGER Sales.Notificator +ON Sales.Customer +AFTER INSERT, UPDATE +AS +BEGIN + IF COLUMNS_UPDATED() & 2 > 0 + BEGIN + PRINT 'Some Warning'; + END; +END; +``` + +Корректно + +```sql +CREATE TRIGGER Sales.Notificator +ON Sales.Customer +AFTER INSERT, UPDATE +AS +BEGIN + IF UPDATE(LastName) + BEGIN + PRINT 'Some Warning'; + END; +END; +``` + +## Подсказки + +💡 Результат функции COLUMNS_UPDATED() зависит от свойства ColumnID столбцов таблицы, а его значение может отличаться на разных серверах. Рекомендуется использовать функцию UPDATE() с указанием имени столбца. diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/AM0891.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/AM0891.md new file mode 100644 index 00000000..422a79e1 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/AM0891.md @@ -0,0 +1,36 @@ + + +# Неоднозначность выходного типа NULL-столбца + +||| +|-|-| +| Id | **AM0891** +| Мнемо | SELECT_NULL_TYPE +| Серьёзность | ℹ Подсказка +| Категория | [Неоднозначность](./Категория_Неоднозначность.md) +| Исходный код | [SelectNullTypeRule.cs](../../../Rules/Ambiguity/SelectNullTypeRule.cs) + +## Причина + +Правило срабатывает, если выходной тип NULL-столбца не задан явно. +

Явно задайте тип NULL-столбца.

+ +## Примеры + +Некорректно + +```sql +SELECT + foo.title + , NULL AS start_time +FROM foo; +``` + +Корректно + +```sql +SELECT + foo.title + , CAST(NULL AS DATETIME2(7)) AS start_time +FROM foo; +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CD0857.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CD0857.md new file mode 100644 index 00000000..50a6bd12 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CD0857.md @@ -0,0 +1,51 @@ + + +# В одном файле задекларированы несколько объектов + +||| +|-|-| +| Id | **CD0857** +| Мнемо | SINGLE_OBJECT_PER_FILE +| Серьёзность | ⚠ Предупреждение +| Категория | [НепрерывнаяПоставка](./Категория_НепрерывнаяПоставка.md) +| Исходный код | [SingleObjectPerFileRule.cs](../../../Rules/ContinuousDeployment/SingleObjectPerFileRule.cs) + +## Причина + +Правило срабатывает, если в одном файле задекларированы несколько объектов. +

Распределите объекты по файлам.

+ +## Примеры + +Некорректно + +```sql +CREATE SCHEMA sch AUTHORIZATION dbo; +GO + +CREATE TABLE sch.Users +( + Id INT NOT NULL + , Name VARCHAR(50) NOT NULL +); +GO +``` + +Корректно + +```sql +CREATE TABLE sch.Users +( + Id INT NOT NULL + , Name VARCHAR(50) NOT NULL +); +GO + +CREATE NONCLUSTERED INDEX IX_sch_Users_Name + ON sch.Users (Name); +GO +``` + +## Подсказки + +💡 Правило игнорирует изменение таблицы и создание индексов в скрипте создания таблицы. diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0851.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0851.md new file mode 100644 index 00000000..ff726e74 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0851.md @@ -0,0 +1,38 @@ + + +# Соединение заявлено как внешнее (OUTER), но работает как внутреннее (INNER) + +||| +|-|-| +| Id | **CS0851** +| Мнемо | FAKE_OUTER_JOIN +| Серьёзность | ℹ Подсказка +| Категория | [СомнительноеКодирование](./Категория_СомнительноеКодирование.md) +| Исходный код | [FakeOuterJoinRule.cs](../../../Rules/CodeSmell/FakeOuterJoinRule.cs) + +## Причина + +Правило срабатывает, если соединение заявлено как внешнее (`OUTER`), но работает как внутреннее (`INNER`). +

Исправьте тип соединения или фильтрацию.

+ +## Примеры + +Некорректно + +```sql +SELECT * +FROM dbo.product_category AS pc +LEFT JOIN dbo.product_item AS pi + ON pi.category_id = pc.category_id +WHERE pi.is_deleted = 0; +``` + +Корректно + +```sql +SELECT * +FROM dbo.product_category AS pc +INNER JOIN dbo.product_item AS pi + ON pi.category_id = pc.category_id +WHERE pi.is_deleted = 0; +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0852.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0852.md new file mode 100644 index 00000000..21dcb135 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0852.md @@ -0,0 +1,38 @@ + + +# Предикат соединения не связан с присоединяемыми источниками + +||| +|-|-| +| Id | **CS0852** +| Мнемо | NON_CORRELATED_JOIN_PREDICATE +| Серьёзность | ℹ Подсказка +| Категория | [СомнительноеКодирование](./Категория_СомнительноеКодирование.md) +| Исходный код | [NonCorrelatedJoinPredicateRule.cs](../../../Rules/CodeSmell/NonCorrelatedJoinPredicateRule.cs) + +## Причина + +Правило срабатывает, если предикат соединения не связан с присоединяемыми источниками. +

Исправьте предикат соединения.

+ +## Примеры + +Некорректно + +```sql +SELECT * +FROM dbo.product_category AS pc +INNER JOIN dbo.product_item AS pi + ON pi.is_deleted = 0 +WHERE pi.category_id = pc.category_id; +``` + +Корректно + +```sql +SELECT * +FROM dbo.product_category AS pc +INNER JOIN dbo.product_item AS pi + ON pi.is_deleted = 0 + AND pi.category_id = pc.category_id; +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0853.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0853.md new file mode 100644 index 00000000..bc7a5786 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0853.md @@ -0,0 +1,40 @@ + + +# Левая часть выражения сравнения совпадает с правой + +||| +|-|-| +| Id | **CS0853** +| Мнемо | COMPARISON_LEFT_EQUALS_RIGHT +| Серьёзность | ℹ Подсказка +| Категория | [СомнительноеКодирование](./Категория_СомнительноеКодирование.md) +| Исходный код | [ComparedExpressionsEqualRule.cs](../../../Rules/CodeSmell/ComparedExpressionsEqualRule.cs) + +## Причина + +Правило срабатывает, если левая часть выражения сравнения совпадает с правой. +

Исправьте выражение сравнения.

+ +## Примеры + +Некорректно + +```sql +SELECT * +FROM dbo.product_category AS pc +LEFT JOIN dbo.product_item AS pi + ON pi.category_id = pi.category_id; +``` + +Корректно + +```sql +SELECT * +FROM dbo.product_category AS pc +LEFT JOIN dbo.product_item AS pi + ON pi.category_id = pc.category_id; +``` + +## Подсказки + +💡 Правило игнорирует выражения 1 = 1 и 0 = 0. diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0856.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0856.md new file mode 100644 index 00000000..eb873d9b --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0856.md @@ -0,0 +1,59 @@ + + +# Программный модуль вызывает сам себя + +||| +|-|-| +| Id | **CS0856** +| Мнемо | SELF_CALL +| Серьёзность | ℹ Подсказка +| Категория | [СомнительноеКодирование](./Категория_СомнительноеКодирование.md) +| Исходный код | [SelfCallRule.cs](../../../Rules/CodeSmell/SelfCallRule.cs) + +## Причина + +Правило срабатывает, если программный модуль вызывает сам себя. +

Удалите рекурсивный вызов.

+ +## Примеры + +Некорректно + +```sql +CREATE FUNCTION dbo.inc +( + @id INT, + @value INT +) +RETURNS INT +AS +BEGIN + SET @id = @id + 1; + + IF (@value > 1) + BEGIN + RETURN dbo.inc(@id, @value - 1); + END; + + RETURN @id; +END; +GO +``` + +Корректно + +```sql +CREATE FUNCTION dbo.inc +( + @id INT, + @value INT +) +RETURNS INT +AS +BEGIN + SET @id = @id + @value; + + RETURN @id; +END; +GO +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0886.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0886.md new file mode 100644 index 00000000..3d086125 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0886.md @@ -0,0 +1,62 @@ + + +# Заданы расширенные свойства другого объекта + +||| +|-|-| +| Id | **CS0886** +| Мнемо | EXTENDED_PROPERTY_MISDIRECTED +| Серьёзность | ⚠ Предупреждение +| Категория | [СомнительноеКодирование](./Категория_СомнительноеКодирование.md) +| Исходный код | [ExtendedPropertyMisdirectedRule.cs](../../../Rules/CodeSmell/ExtendedPropertyMisdirectedRule.cs) + +## Причина + +Правило срабатывает, если в скрипте создания объекта процедура `sp_addextendedproperty` ссылается на другой объект. +

Исправьте вызов sp_addextendedproperty.

+ +## Примеры + +Некорректно + +```sql +CREATE TABLE dbo.users +( + id INT NOT NULL + , login VARCHAR(100) NOT NULL +); +GO + +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'dbo' + , @level1type = N'TABLE' + , @level1name = N'positions' + , @level2type = N'COLUMN' + , @level2name = N'id'; +GO +``` + +Корректно + +```sql +CREATE TABLE dbo.users +( + id INT NOT NULL + , login VARCHAR(100) NOT NULL +); +GO + +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'dbo' + , @level1type = N'TABLE' + , @level1name = N'users' + , @level2type = N'COLUMN' + , @level2name = N'id'; +GO +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0887.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0887.md new file mode 100644 index 00000000..2376c56c --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0887.md @@ -0,0 +1,62 @@ + + +# Указанный для расширенного свойства столбец не существует + +||| +|-|-| +| Id | **CS0887** +| Мнемо | EXTENDED_PROPERTY_FOR_MISSING_COL +| Серьёзность | ⚠ Предупреждение +| Категория | [СомнительноеКодирование](./Категория_СомнительноеКодирование.md) +| Исходный код | [ExtendedPropertyAddressesMissingColumnRule.cs](../../../Rules/CodeSmell/ExtendedPropertyAddressesMissingColumnRule.cs) + +## Причина + +Правило срабатывает, если в скрипте создания объекта процедура `sp_addextendedproperty` ссылается на несуществующий столбец. +

Исправьте вызов sp_addextendedproperty.

+ +## Примеры + +Некорректно + +```sql +CREATE TABLE dbo.users +( + id INT NOT NULL + , login VARCHAR(100) NOT NULL +); +GO + +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'dbo' + , @level1type = N'TABLE' + , @level1name = N'users' + , @level2type = N'COLUMN' + , @level2name = N'rest'; +GO +``` + +Корректно + +```sql +CREATE TABLE dbo.users +( + id INT NOT NULL + , login VARCHAR(100) NOT NULL +); +GO + +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'dbo' + , @level1type = N'TABLE' + , @level1name = N'users' + , @level2type = N'COLUMN' + , @level2name = N'login'; +GO +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0888.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0888.md new file mode 100644 index 00000000..d9859209 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0888.md @@ -0,0 +1,84 @@ + + +# Дубликат расширенного свойства + +||| +|-|-| +| Id | **CS0888** +| Мнемо | EXTENDED_PROPERTY_DUP +| Серьёзность | ⚠ Предупреждение +| Категория | [СомнительноеКодирование](./Категория_СомнительноеКодирование.md) +| Исходный код | [ExtendedPropertyDupRule.cs](../../../Rules/CodeSmell/ExtendedPropertyDupRule.cs) + +## Причина + +Правило срабатывает, если в скрипте создания объекта процедура `sp_addextendedproperty` присваивает одному и тому же объекту расширенные свойства с одинаковыми именами. +

Исправьте вызов sp_addextendedproperty.

+ +## Примеры + +Некорректно + +```sql +CREATE TABLE dbo.users +( + id INT NOT NULL + , login VARCHAR(100) NOT NULL +); +GO + +EXEC sp_addextendedproperty + @name = N'First Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'dbo' + , @level1type = N'TABLE' + , @level1name = N'users' + , @level2type = N'COLUMN' + , @level2name = N'login'; +GO + +EXEC sp_addextendedproperty + @name = N'First Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'dbo' + , @level1type = N'TABLE' + , @level1name = N'users' + , @level2type = N'COLUMN' + , @level2name = N'login'; +GO +``` + +Корректно + +```sql +CREATE TABLE dbo.users +( + id INT NOT NULL + , login VARCHAR(100) NOT NULL +); +GO + +EXEC sp_addextendedproperty + @name = N'First Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'dbo' + , @level1type = N'TABLE' + , @level1name = N'users' + , @level2type = N'COLUMN' + , @level2name = N'login'; +GO + +EXEC sp_addextendedproperty + @name = N'Second Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'dbo' + , @level1type = N'TABLE' + , @level1name = N'users' + , @level2type = N'COLUMN' + , @level2name = N'login'; +GO +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0890.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0890.md new file mode 100644 index 00000000..e5c327f2 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0890.md @@ -0,0 +1,41 @@ + + +# Обращение к свойствам объектов уровня БД или сервера + +||| +|-|-| +| Id | **CS0890** +| Мнемо | OBJECT_PROPERTY_FIDDLING +| Серьёзность | ⚠ Предупреждение +| Категория | [СомнительноеКодирование](./Категория_СомнительноеКодирование.md) +| Исходный код | [ObjectPropertyFiddlingRule.cs](../../../Rules/CodeSmell/ObjectPropertyFiddlingRule.cs) + +## Причина + +Правило срабатывает, если используется обращение к свойствам объектов уровня БД или сервера. +

Избавьтесь от обращения к свойствам объектов уровня БД или сервера.

+ +## Примеры + +Некорректно + +```sql +SELECT SERVERPROPERTY('MachineName'); +``` + +## Подсказки + +💡 Правило проверяет: + +- `OBJECTPROPERTY` +- `OBJECTPROPERTYEX` +- `COLUMNPROPERTY` +- `DATABASEPROPERTYEX` +- `FILEGROUPPROPERTY` +- `FILEPROPERTY` +- `FILEPROPERTYEX` +- `FULLTEXTCATALOGPROPERTY` +- `FULLTEXTSERVICEPROPERTY` +- `INDEXKEY_PROPERTY` +- `SERVERPROPERTY` +- `TYPEPROPERTY` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CV0837.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CV0837.md new file mode 100644 index 00000000..a26e7f28 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CV0837.md @@ -0,0 +1,47 @@ + + +# Рассмотрите переход на XQuery + +||| +|-|-| +| Id | **CV0837** +| Мнемо | SP_XML_TO_XQUERY +| Серьёзность | ℹ Подсказка +| Категория | [СоглашенияПоКодированию](./Категория_СоглашенияПоКодированию.md) +| Исходный код | [SpXmlToXQueryRule.cs](../../../Rules/CodingConvention/SpXmlToXQueryRule.cs) + +## Причина + +Правило срабатывает, если для парсинга XML используется sp_xml_preparedocument и OPENXML(). +

Рассмотрите переход на XQuery.

+ +## Примеры + +Некорректно + +```sql +DECLARE + @idoc INT + , @dx VARCHAR(1000) = '111'; + +EXEC sp_xml_preparedocument @idoc OUTPUT, @doc; + +SELECT * +FROM + OPENXML(@idoc, '/ROOT/a', 1) + WITH (CustomerID VARCHAR(10), ContactName VARCHAR(20)); +``` + +Корректно + +```sql +DECLARE @x XML; + +SET @x = '111'; + +SELECT @x.query('/ROOT/a'); +``` + +## Подсказки + +💡 В большинстве случаев работа с XML через XQuery поможет создать более простой код. diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0831.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0831.md new file mode 100644 index 00000000..b5e3030e --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0831.md @@ -0,0 +1,48 @@ + + +# Историческая таблица размещена не в схеме исходника + +||| +|-|-| +| Id | **DD0831** +| Мнемо | HISTORY_IN_SAME_SCHEMA +| Серьёзность | ℹ Подсказка +| Категория | [ДизайнБазДанных](./Категория_ДизайнБазДанных.md) +| Исходный код | [TemporalTableSameSchemaRule.cs](../../../Rules/DatabaseDesign/TemporalTableSameSchemaRule.cs) + +## Причина + +Правило срабатывает, если историческая часть темпоральной таблицы находится в другой схеме. +

Укажите одинаковые схемы.

+ +## Примеры + +Некорректно + +```sql +CREATE TABLE SchemaOne.Person +( + ParsonID INT NOT NULL PRIMARY KEY CLUSTERED + , FirstName VARCHAR(128) NOT NULL + , LastName VARCHAR(128) NOT NULL + , BirthDate DATE +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = SchemaTwo.PersonHistory)); +``` + +Корректно + +```sql +CREATE TABLE SchemaOne.Person +( + ParsonID INT NOT NULL PRIMARY KEY CLUSTERED + , FirstName VARCHAR(128) NOT NULL + , LastName VARCHAR(128) NOT NULL + , BirthDate DATE +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = SchemaOne.PersonHistory)); +``` + +## Подсказки + +💡 Обычно не требуется создавать историческую часть темпоральной таблицы в другой схеме, рекомендуется проверить необходимость такого решения. diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0855.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0855.md new file mode 100644 index 00000000..f5028f64 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0855.md @@ -0,0 +1,24 @@ + + +# Скалярный пользовательский тип на основе системного + +||| +|-|-| +| Id | **DD0855** +| Мнемо | SCALAR_UDT +| Серьёзность | ℹ Подсказка +| Категория | [ДизайнБазДанных](./Категория_ДизайнБазДанных.md) +| Исходный код | [ScalarUdtRule.cs](../../../Rules/DatabaseDesign/ScalarUdtRule.cs) + +## Причина + +Правило срабатывает, если создаётся скалярный пользовательский тип на основе системного. +

Удалите скалярный пользовательский тип на основе системного.

+ +## Примеры + +Некорректно + +```sql +CREATE TYPE Id FROM INT; +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0860.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0860.md new file mode 100644 index 00000000..5536b30e --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0860.md @@ -0,0 +1,47 @@ + + +# Для темпоральной таблицы не указан PERIOD + +||| +|-|-| +| Id | **DD0860** +| Мнемо | HISTORY_PERIOD_SET +| Серьёзность | ⚠ Предупреждение +| Категория | [ДизайнБазДанных](./Категория_ДизайнБазДанных.md) +| Исходный код | [TemporalTablePeriodDefinedRule.cs](../../../Rules/DatabaseDesign/TemporalTablePeriodDefinedRule.cs) + +## Причина + +Правило срабатывает, если для темпоральной таблицы не указан `PERIOD`. +

Укажите PERIOD.

+ +## Примеры + +Некорректно + +```sql +CREATE TABLE dbo.Person +( + ParsonID INT NOT NULL PRIMARY KEY CLUSTERED + , FirstName VARCHAR(128) NOT NULL + , LastName VARCHAR(128) NOT NULL + , BirthDate DATE NOT NULL +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.PersonHistory)); +``` + +Корректно + +```sql +CREATE TABLE dbo.Person +( + ParsonID INT NOT NULL PRIMARY KEY CLUSTERED + , FirstName VARCHAR(128) NOT NULL + , LastName VARCHAR(128) NOT NULL + , BirthDate DATE NOT NULL + , SysStartTime DATETIME2(7) GENERATED ALWAYS AS ROW START NOT NULL + , SysEndTime DATETIME2(7) GENERATED ALWAYS AS ROW END NOT NULL + , PERIOD FOR SYSTEM_TIME(SysStartTime, SysEndTime) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.PersonHistory)); +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0861.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0861.md new file mode 100644 index 00000000..c86c81c0 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0861.md @@ -0,0 +1,50 @@ + + +# Не указано имя исторической таблицы + +||| +|-|-| +| Id | **DD0861** +| Мнемо | HISTORY_SET_STORAGE_NAME +| Серьёзность | ⚠ Предупреждение +| Категория | [ДизайнБазДанных](./Категория_ДизайнБазДанных.md) +| Исходный код | [TemporalTableNameHistoryTableRule.cs](../../../Rules/DatabaseDesign/TemporalTableNameHistoryTableRule.cs) + +## Причина + +Правило срабатывает, если для темпоральной таблицы не указано имя исторической таблицы. +

Укажите имя исторической таблицы.

+ +## Примеры + +Некорректно + +```sql +CREATE TABLE dbo.Person +( + ParsonID INT NOT NULL PRIMARY KEY CLUSTERED + , FirstName VARCHAR(128) NOT NULL + , LastName VARCHAR(128) NOT NULL + , BirthDate DATE NOT NULL + , SysStartTime DATETIME2(7) GENERATED ALWAYS AS ROW START NOT NULL + , SysEndTime DATETIME2(7) GENERATED ALWAYS AS ROW END NOT NULL + , PERIOD FOR SYSTEM_TIME(SysStartTime, SysEndTime) +) +WITH (SYSTEM_VERSIONING = ON); +``` + +Корректно + +```sql +CREATE TABLE dbo.Person +( + ParsonID INT NOT NULL PRIMARY KEY CLUSTERED + , FirstName VARCHAR(128) NOT NULL + , LastName VARCHAR(128) NOT NULL + , BirthDate DATE NOT NULL + , SysStartTime DATETIME2(7) GENERATED ALWAYS AS ROW START NOT NULL + , SysEndTime DATETIME2(7) GENERATED ALWAYS AS ROW END NOT NULL + , PERIOD FOR SYSTEM_TIME(SysStartTime, SysEndTime) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.PersonHistory)); +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0862.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0862.md new file mode 100644 index 00000000..07176388 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0862.md @@ -0,0 +1,50 @@ + + +# Опция DATA_CONSISTENCY_CHECK отключена + +||| +|-|-| +| Id | **DD0862** +| Мнемо | HISTORY_CONSISTENCY_CHECK +| Серьёзность | ⚠ Предупреждение +| Категория | [ДизайнБазДанных](./Категория_ДизайнБазДанных.md) +| Исходный код | [TemporalTableConsistencyCheckRule.cs](../../../Rules/DatabaseDesign/TemporalTableConsistencyCheckRule.cs) + +## Причина + +Правило срабатывает, если для темпоральной таблицы опция `DATA_CONSISTENCY_CHECK` отключена. +

Включите DATA_CONSISTENCY_CHECK.

+ +## Примеры + +Некорректно + +```sql +CREATE TABLE dbo.Person +( + ParsonID INT NOT NULL PRIMARY KEY CLUSTERED + , FirstName VARCHAR(128) NOT NULL + , LastName VARCHAR(128) NOT NULL + , BirthDate DATE NOT NULL + , SysStartTime DATETIME2(7) GENERATED ALWAYS AS ROW START NOT NULL + , SysEndTime DATETIME2(7) GENERATED ALWAYS AS ROW END NOT NULL + , PERIOD FOR SYSTEM_TIME(SysStartTime, SysEndTime) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.PersonHistory, DATA_CONSISTENCY_CHECK = OFF)); +``` + +Корректно + +```sql +CREATE TABLE dbo.Person +( + ParsonID INT NOT NULL PRIMARY KEY CLUSTERED + , FirstName VARCHAR(128) NOT NULL + , LastName VARCHAR(128) NOT NULL + , BirthDate DATE NOT NULL + , SysStartTime DATETIME2(7) GENERATED ALWAYS AS ROW START NOT NULL + , SysEndTime DATETIME2(7) GENERATED ALWAYS AS ROW END NOT NULL + , PERIOD FOR SYSTEM_TIME(SysStartTime, SysEndTime) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.PersonHistory)); +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0863.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0863.md new file mode 100644 index 00000000..8cd8e440 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0863.md @@ -0,0 +1,50 @@ + + +# Разная точность столбцов исторического периода + +||| +|-|-| +| Id | **DD0863** +| Мнемо | HISTORY_SAME_DATE_RANGE_PRECISION +| Серьёзность | ⚠ Предупреждение +| Категория | [ДизайнБазДанных](./Категория_ДизайнБазДанных.md) +| Исходный код | [TemporalTableSimilarDateRangePrecision.cs](../../../Rules/DatabaseDesign/TemporalTableSimilarDateRangePrecision.cs) + +## Причина + +Правило срабатывает, если для темпоральной таблицы указана разная точность столбцов исторического периода. +

Укажите одинаковую точность столбцов исторического периода.

+ +## Примеры + +Некорректно + +```sql +CREATE TABLE dbo.Person +( + ParsonID INT NOT NULL PRIMARY KEY CLUSTERED + , FirstName VARCHAR(128) NOT NULL + , LastName VARCHAR(128) NOT NULL + , BirthDate DATE NOT NULL + , SysStartTime DATETIME2(3) GENERATED ALWAYS AS ROW START NOT NULL + , SysEndTime DATETIME2(7) GENERATED ALWAYS AS ROW END NOT NULL + , PERIOD FOR SYSTEM_TIME(SysStartTime, SysEndTime) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.PersonHistory)); +``` + +Корректно + +```sql +CREATE TABLE dbo.Person +( + ParsonID INT NOT NULL PRIMARY KEY CLUSTERED + , FirstName VARCHAR(128) NOT NULL + , LastName VARCHAR(128) NOT NULL + , BirthDate DATE NOT NULL + , SysStartTime DATETIME2(7) GENERATED ALWAYS AS ROW START NOT NULL + , SysEndTime DATETIME2(7) GENERATED ALWAYS AS ROW END NOT NULL + , PERIOD FOR SYSTEM_TIME(SysStartTime, SysEndTime) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.PersonHistory)); +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0864.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0864.md new file mode 100644 index 00000000..00fee43c --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0864.md @@ -0,0 +1,54 @@ + + +# Вероятно некорректное округление даты начала исторического периода + +||| +|-|-| +| Id | **DD0864** +| Мнемо | HISTORY_FUTURE_DATE +| Серьёзность | ⚠ Предупреждение +| Категория | [ДизайнБазДанных](./Категория_ДизайнБазДанных.md) +| Исходный код | [TemporalTablePossibleFutureDateRule.cs](../../../Rules/DatabaseDesign/TemporalTablePossibleFutureDateRule.cs) + +## Причина + +Правило срабатывает, если для даты начала исторического периода задано недетерминированное `DEFAULT` значение с большей точностью, чем точность столбца. +

Используйте DATETIME2(7) для столбцов исторического периода, или отрицательное смещение для DEFAULT значения.

+ +## Примеры + +Некорректно + +```sql +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(2) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time DEFAULT SYSUTCDATETIME() + , sys_end_time DATETIME2(2) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO +``` + +Корректно + +```sql +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(2) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time DEFAULT DATEADD(SECOND, -1, SYSUTCDATETIME()) + , sys_end_time DATETIME2(2) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO +``` + +## Подсказки + +💡 При приведении DATETIME/DATETIME2 к типу с точностью ниже исходной, вероятно округление в большую сторону (к дате в будущем), что вызовет ошибку. diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0865.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0865.md new file mode 100644 index 00000000..d5f58cd0 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0865.md @@ -0,0 +1,50 @@ + + +# Точность DEFAULT значения ниже точности столбца исторического периода + +||| +|-|-| +| Id | **DD0865** +| Мнемо | HISTORY_PERIOD_DEFAULT_PRECISION +| Серьёзность | ⚠ Предупреждение +| Категория | [ДизайнБазДанных](./Категория_ДизайнБазДанных.md) +| Исходный код | [TemporalTableRangeDefaultPrecisionLackRule.cs](../../../Rules/DatabaseDesign/TemporalTableRangeDefaultPrecisionLackRule.cs) + +## Причина + +Правило срабатывает, если точность `DEFAULT` значения ниже точности столбца исторического периода. +

Используйте типы данных схожей точности для столбцов и DEFAULT значений.

+ +## Примеры + +Некорректно + +```sql +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(7) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time DEFAULT GETDATE() + , sys_end_time DATETIME2(7) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_end_time DEFAULT CONVERT(DATETIME2(3), '9999-12-31') + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO +``` + +Корректно + +```sql +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(7) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time DEFAULT SYSUTCDATETIME() + , sys_end_time DATETIME2(7) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_end_time DEFAULT CONVERT(DATETIME2(7), '9999-12-31 23:59:59.9999999') + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0866.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0866.md new file mode 100644 index 00000000..a22f6282 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/DD0866.md @@ -0,0 +1,50 @@ + + +# Недостаточная точность столбцов исторического периода + +||| +|-|-| +| Id | **DD0866** +| Мнемо | HISTORY_MAX_DATE_PRECISION +| Серьёзность | ℹ Подсказка +| Категория | [ДизайнБазДанных](./Категория_ДизайнБазДанных.md) +| Исходный код | [TemporalTableRangeMaxPrecisionRule.cs](../../../Rules/DatabaseDesign/TemporalTableRangeMaxPrecisionRule.cs) + +## Причина + +Правило срабатывает, если точность столбцов исторического периода ниже `DATETIME2(7)`. +

Используйте DATETIME2(7) для столбцов исторического периода.

+ +## Примеры + +Некорректно + +```sql +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(3) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL + , sys_end_time DATETIME2(3) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO +``` + +Корректно + +```sql +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(7) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL + , sys_end_time DATETIME2(7) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/FA0884.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/FA0884.md new file mode 100644 index 00000000..73edf668 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/FA0884.md @@ -0,0 +1,58 @@ + + +# Недопустимое значение параметра редактирования расширенного свойства объекта + +||| +|-|-| +| Id | **FA0884** +| Мнемо | INVALID_EXTENDED_PROPERTY_PARAM +| Серьёзность | ⛔ Ошибка +| Категория | [Сбой](./Категория_Сбой.md) +| Исходный код | [InvalidExtendedPropertyParameterRule.cs](../../../Rules/Failure/InvalidExtendedPropertyParameterRule.cs) + +## Причина + +Правило срабатывает, если для для процедуры редактирования расширенных свойств указано недопустимое значение аргумента. +

Укажите корректное значение аргумента.

+ +## Примеры + +Некорректно + +```sql +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'UNKNOWN' + , @level0name = N'my_schema' + , @level1type = N'TABLE' + , @level1name = N'my_table' + , @level2type = N'COLUMN' + , @level2name = N'my_column'; +``` + +Корректно + +```sql +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'my_schema' + , @level1type = N'TABLE' + , @level1name = N'my_table' + , @level2type = N'COLUMN' + , @level2name = N'my_column'; +``` + +## Подсказки + +💡 Правило проверяет: + +- sp_addextendedproperty +- sp_updateextendedproperty +- sp_dropextendedproperty + +## Ссылки + +[sp_addextendedproperty](https://learn.microsoft.com/ru-ru/sql/relational-databases/system-stored-procedures/sp-addextendedproperty-transact-sql?view=sql-server-ver17#----level0type) diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/FA0885.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/FA0885.md new file mode 100644 index 00000000..f8520aac --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/FA0885.md @@ -0,0 +1,30 @@ + + +# RAISERROR с severity от 19 и выше без WITH LOG + +||| +|-|-| +| Id | **FA0885** +| Мнемо | RAISERROR_NEEDS_WITH_LOG +| Серьёзность | ⛔ Ошибка +| Категория | [Сбой](./Категория_Сбой.md) +| Исходный код | [RaiseErrorNeedsWithLogRule.cs](../../../Rules/Failure/RaiseErrorNeedsWithLogRule.cs) + +## Причина + +Правило срабатывает, если `RAISERROR` с severity от 19 и выше написан без `WITH LOG`. +

Добавьте WITH LOG.

+ +## Примеры + +Некорректно + +```sql +RAISERROR ('error', 19, 2); +``` + +Корректно + +```sql +RAISERROR ('error', 19, 2) WITH LOG; +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/NM0854.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/NM0854.md new file mode 100644 index 00000000..083d1d24 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/NM0854.md @@ -0,0 +1,36 @@ + + +# Символ другого алфавита в идентификаторе + +||| +|-|-| +| Id | **NM0854** +| Мнемо | IDENTIFIER_LOOK_ALIKE_CHAR +| Серьёзность | ℹ Подсказка +| Категория | [Именование](./Категория_Именование.md) +| Исходный код | [IdentifierContainsLookAlikeCharRule.cs](../../../Rules/Naming/IdentifierContainsLookAlikeCharRule.cs) + +## Причина + +Правило срабатывает, если в идентификаторе, набранном преимущественно символами одного алфавита, обнаружен символ из другого алфавита, имеющий визуально близкий аналог в первом алфавите. +

Перепишите, используя символы только одного алфавита.

+ +## Примеры + +Некорректно + +```sql +SELECT 'John' AS Name; -- cyrillic "a" + +CREATE TYPE [Cтрока] -- latin "c" +FROM VARCHAR(MAX); +``` + +Корректно + +```sql +SELECT 'John' AS Name; -- latin only + +CREATE TYPE [Строка] -- cyrillic only +FROM VARCHAR(MAX); +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0868.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0868.md new file mode 100644 index 00000000..9e0cd63e --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0868.md @@ -0,0 +1,40 @@ + + +# Ручное управление планом запроса через JOIN hint + +||| +|-|-| +| Id | **PF0868** +| Мнемо | OPTIMIZER_FIGHT_JOIN_HINT +| Серьёзность | ℹ Подсказка +| Категория | [Производительность](./Категория_Производительность.md) +| Исходный код | [FightOptimizerByJoinHintRule.cs](../../../Rules/Performance/FightOptimizerByJoinHintRule.cs) + +## Причина + +Правило срабатывает, если в запросе указан JOIN hint. +

Удалите JOIN hint.

+ +## Примеры + +Некорректно + +```sql +SELECT + emp.firts_name + , pos.name +FROM dbo.employees AS emp +INNER MERGE JOIN dbo.positions AS pos + ON pos.position_id = emp.position_id; +``` + +Корректно + +```sql +SELECT + emp.firts_name + , pos.name +FROM dbo.employees AS emp +INNER JOIN dbo.positions AS pos + ON pos.position_id = emp.position_id; +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0869.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0869.md new file mode 100644 index 00000000..5db91b8e --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0869.md @@ -0,0 +1,40 @@ + + +# Ручное управление планом запроса через table hint + +||| +|-|-| +| Id | **PF0869** +| Мнемо | OPTIMIZER_FIGHT_TABLE_HINT +| Серьёзность | ℹ Подсказка +| Категория | [Производительность](./Категория_Производительность.md) +| Исходный код | [FightOptimizerByTableHintRule.cs](../../../Rules/Performance/FightOptimizerByTableHintRule.cs) + +## Причина + +Правило срабатывает, если в запросе указаны `FORCESEEK`, `FORCESCAN` или `WITH INDEX`. +

Удалите table hint.

+ +## Примеры + +Некорректно + +```sql +SELECT + emp.firts_name + , pos.name +FROM dbo.employees AS emp WITH (FORCESEEK) +INNER JOIN dbo.positions AS pos + ON pos.position_id = emp.position_id; +``` + +Корректно + +```sql +SELECT + emp.firts_name + , pos.name +FROM dbo.employees AS emp +INNER JOIN dbo.positions AS pos + ON pos.position_id = emp.position_id; +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0870.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0870.md new file mode 100644 index 00000000..31c752c7 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0870.md @@ -0,0 +1,45 @@ + + +# Ручное управление планом запроса через query hint + +||| +|-|-| +| Id | **PF0870** +| Мнемо | OPTIMIZER_FIGHT_QUERY_OPTION +| Серьёзность | ℹ Подсказка +| Категория | [Производительность](./Категория_Производительность.md) +| Исходный код | [FightOptimizerByQueryOptionRule.cs](../../../Rules/Performance/FightOptimizerByQueryOptionRule.cs) + +## Причина + +Правило срабатывает, если в запросе указан query hint `OPTION`. +

Удалите query hint.

+ +## Примеры + +Некорректно + +```sql +SELECT + emp.firts_name + , pos.name +FROM dbo.employees AS emp +INNER JOIN dbo.positions AS pos + ON pos.position_id = emp.position_id +OPTION (FORCE ORDER); +``` + +Корректно + +```sql +SELECT + emp.firts_name + , pos.name +FROM dbo.employees AS emp +INNER JOIN dbo.positions AS pos + ON pos.position_id = emp.position_id; +``` + +## Подсказки + +💡 Правило игнорирует RECOMPILE, MAXRECURSION и USE PLAN. diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0871.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0871.md new file mode 100644 index 00000000..fc44043a --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0871.md @@ -0,0 +1,43 @@ + + +# Ручное управление планом запроса через USE PLAN или SET FORCEPLAN ON + +||| +|-|-| +| Id | **PF0871** +| Мнемо | OPTIMIZER_FIGHT_FORCE_PLAN +| Серьёзность | ℹ Подсказка +| Категория | [Производительность](./Категория_Производительность.md) +| Исходный код | [FightOptimizerByForcePlanRule.cs](../../../Rules/Performance/FightOptimizerByForcePlanRule.cs) + +## Причина + +Правило срабатывает, если в запросе указаны `USE PLAN` или `SET FORCEPLAN ON`. +

Удалите `USE PLAN` и `SET FORCEPLAN ON`.

+ +## Примеры + +Некорректно + +```sql +SET FORCEPLAN ON; + +SELECT + emp.firts_name + , pos.name +FROM dbo.employees AS emp +INNER JOIN dbo.positions AS pos + ON pos.position_id = emp.position_id +OPTION (USE PLAN ''); +``` + +Корректно + +```sql +SELECT + emp.firts_name + , pos.name +FROM dbo.employees AS emp +INNER JOIN dbo.positions AS pos + ON pos.position_id = emp.position_id; +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0875.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0875.md new file mode 100644 index 00000000..3d222f22 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0875.md @@ -0,0 +1,56 @@ + + +# DDL вперемешку с DML + +||| +|-|-| +| Id | **PF0875** +| Мнемо | DDL_DML_MIX +| Серьёзность | ℹ Подсказка +| Категория | [Производительность](./Категория_Производительность.md) +| Исходный код | [MixOfDdlAndDmlRule.cs](../../../Rules/Performance/MixOfDdlAndDmlRule.cs) + +## Причина + +Правило срабатывает, если DDL и DML команды в запросе перемешаны. +

Переместите все DDL команды в начало запроса.

+ +## Примеры + +Некорректно + +```sql +CREATE PROC dbo.delete_data +AS +BEGIN + CREATE TABLE #t (id INT); + + SELECT br.id FROM dbo.bar AS br INNER JOIN #t AS tt ON tt.id = br.parent_id; + + CREATE INDEX ix ON #t (id); + + DELETE bar FROM dbo.bar AS br INNER JOIN #t AS tt ON tt.id = br.parent_id; +END; +GO +``` + +Корректно + +```sql +CREATE PROC dbo.delete_data +AS +BEGIN + CREATE TABLE #t (id INT); + + CREATE INDEX ix ON #t (id); + + SELECT br.id FROM dbo.bar AS br INNER JOIN #t AS tt ON tt.id = br.parent_id; + + DELETE bar FROM dbo.bar AS br INNER JOIN #t AS tt ON tt.id = br.parent_id; +END; +GO +``` + +## Подсказки + +💡 Правило игнорирует запросы без источника такие как STRING_SPLIT, OPENJSON, OPENXML и манипуляции над табличными переменными. diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0876.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0876.md new file mode 100644 index 00000000..cb14808e --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0876.md @@ -0,0 +1,35 @@ + + +# Вместо фильтра по диапазону применена функция преобразования даты + +||| +|-|-| +| Id | **PF0876** +| Мнемо | EXPAND_DATE_FUNC +| Серьёзность | ⚠ Предупреждение +| Категория | [Производительность](./Категория_Производительность.md) +| Исходный код | [ExpandDateFunctionRule.cs](../../../Rules/Performance/ExpandDateFunctionRule.cs) + +## Причина + +Правило срабатывает, если в предикате вместо фильтра по диапазону применена функция преобразования даты. +

Используйте предикат по диапазону вместо преобразования даты.

+ +## Примеры + +Некорректно + +```sql +SELECT sum(payment) +FROM taxes +WHERE YEAR(pay_period) = @tax_year; +``` + +Корректно + +```sql +SELECT sum(payment) +FROM taxes +WHERE pay_period >= @tax_year_begin + AND pay_period < @next_tax_year +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0877.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0877.md new file mode 100644 index 00000000..11e34d0e --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0877.md @@ -0,0 +1,34 @@ + + +# Non-SARGable ISNULL вместо OR + +||| +|-|-| +| Id | **PF0877** +| Мнемо | EXPAND_ISNULL_FOR_OPTIONAL_ARG +| Серьёзность | ⚠ Предупреждение +| Категория | [Производительность](./Категория_Производительность.md) +| Исходный код | [OptionalParameterIsNullPredicateRule.cs](../../../Rules/Performance/OptionalParameterIsNullPredicateRule.cs) + +## Причина + +Правило срабатывает, если в предикате используется ISNULL вместо OR для обработки логики необязательного параметра. +

Используйте OR вместо ISNULL.

+ +## Примеры + +Некорректно + +```sql +SELECT ord.id +FROM dbo.orders AS ord +WHERE o.client_id = ISNULL(@client_id, ord.client_id); +``` + +Корректно + +```sql +SELECT ord.id +FROM dbo.orders AS ord +WHERE ord.client_id = @client_id OR @client_id IS NULL; +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/RD0850.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/RD0850.md new file mode 100644 index 00000000..7da7f47b --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/RD0850.md @@ -0,0 +1,39 @@ + + +# Предикат WHERE уже применён на уровне INNER JOIN + +||| +|-|-| +| Id | **RD0850** +| Мнемо | EXTRA_WHERE_PREDICATE +| Серьёзность | ℹ Подсказка +| Категория | [Избыточность](./Категория_Избыточность.md) +| Исходный код | [WherePredicateSameAsJoinPredicateRule.cs](../../../Rules/Redundancy/WherePredicateSameAsJoinPredicateRule.cs) + +## Причина + +Правило срабатывает, если предикат `WHERE` уже применён на уровне `INNER JOIN`. +

Удалите избыточный предикат.

+ +## Примеры + +Некорректно + +```sql +SELECT * +FROM dbo.product_category AS pc +INNER JOIN dbo.product_item AS pi + ON pi.category_id = pc.category_id + AND pi.is_deleted = 0 +WHERE pi.is_deleted = 0; +``` + +Корректно + +```sql +SELECT * +FROM dbo.product_category AS pc +INNER JOIN dbo.product_item AS pi + ON pi.category_id = pc.category_id + AND pi.is_deleted = 0; +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/SI0889.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/SI0889.md new file mode 100644 index 00000000..3dffac7e --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/SI0889.md @@ -0,0 +1,36 @@ + + +# Обращение к свойству объекта вместо ограничения типа + +||| +|-|-| +| Id | **SI0889** +| Мнемо | USE_TYPE_NOT_PROPERTY +| Серьёзность | ℹ Подсказка +| Категория | [Упрощение](./Категория_Упрощение.md) +| Исходный код | [AskingPropertyNotTypeRule.cs](../../../Rules/Simplification/AskingPropertyNotTypeRule.cs) + +## Причина + +Правило срабатывает, если вместо передачи типа в `OBJECT_ID`, используется конструкция вида `OBJECT_PROPERTY(OBJECT_ID(...))`. +

Передайте тип в OBJECT_ID и удалите избыточное OBJECT_PROPERTY.

+ +## Примеры + +Некорректно + +```sql +IF (OBJECTPROPERTY(OBJECT_ID('dbo.table'), 'IsTable') = 1) +BEGIN + ... +END; +``` + +Корректно + +```sql +IF OBJECT_ID('dbo.table', 'U') IS NOT NULL +BEGIN + ... +END; +``` diff --git "a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\223\321\200\321\203\320\277\320\277\320\260_\320\242\321\200\320\270\320\263\320\263\320\265\321\200\321\213.md" "b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\223\321\200\321\203\320\277\320\277\320\260_\320\242\321\200\320\270\320\263\320\263\320\265\321\200\321\213.md" index 43829c61..7d8ed3b5 100644 --- "a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\223\321\200\321\203\320\277\320\277\320\260_\320\242\321\200\320\270\320\263\320\263\320\265\321\200\321\213.md" +++ "b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\223\321\200\321\203\320\277\320\277\320\260_\320\242\321\200\320\270\320\263\320\263\320\265\321\200\321\213.md" @@ -4,6 +4,7 @@ ||| |-|-| | [AM0162](./AM0162.md) | Неоднозначность при использовании INSERTED/DELETED в OUTPUT в триггере +| [AM0836](./AM0836.md) | Функция COLUMNS_UPDATED может давать неоднозначный результат | [CS0139](./CS0139.md) | Задан порядок выполнения триггеров | [CS0163](./CS0163.md) | Неожиданный возврат данных из триггера | [CS0186](./CS0186.md) | Использование RAISERROR вместо THROW в триггере diff --git "a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\224\320\270\320\267\320\260\320\271\320\275\320\221\320\260\320\267\320\224\320\260\320\275\320\275\321\213\321\205.md" "b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\224\320\270\320\267\320\260\320\271\320\275\320\221\320\260\320\267\320\224\320\260\320\275\320\275\321\213\321\205.md" index 1ea7d52c..57d84f08 100644 --- "a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\224\320\270\320\267\320\260\320\271\320\275\320\221\320\260\320\267\320\224\320\260\320\275\320\275\321\213\321\205.md" +++ "b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\224\320\270\320\267\320\260\320\271\320\275\320\221\320\260\320\267\320\224\320\260\320\275\320\275\321\213\321\205.md" @@ -27,6 +27,15 @@ | [DD0827](./DD0827.md) | Значение вычисляемого столбца - константа | [DD0828](./DD0828.md) | Вычисляемый столбец повторяет значение другого столбца | [DD0829](./DD0829.md) | Внешние ключи с временными таблицами +| [DD0831](./DD0831.md) | Историческая таблица размещена не в схеме исходника +| [DD0855](./DD0855.md) | Скалярный пользовательский тип на основе системного +| [DD0860](./DD0860.md) | Для темпоральной таблицы не указан PERIOD +| [DD0861](./DD0861.md) | Не указано имя исторической таблицы +| [DD0862](./DD0862.md) | Опция DATA_CONSISTENCY_CHECK отключена +| [DD0863](./DD0863.md) | Разная точность столбцов исторического периода +| [DD0864](./DD0864.md) | Вероятно некорректное округление даты начала исторического периода +| [DD0865](./DD0865.md) | Точность DEFAULT значения ниже точности столбца исторического периода +| [DD0866](./DD0866.md) | Недостаточная точность столбцов исторического периода | [DD0906](./DD0906.md) | Рекурсивный внешний ключ | [DD0908](./DD0908.md) | Некластерный индекс включает столбцы кластерного индекса | [DD0909](./DD0909.md) | Столбец включён в индекс более одного раза diff --git "a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\230\320\267\320\261\321\213\321\202\320\276\321\207\320\275\320\276\321\201\321\202\321\214.md" "b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\230\320\267\320\261\321\213\321\202\320\276\321\207\320\275\320\276\321\201\321\202\321\214.md" index 65d1e150..a5918205 100644 --- "a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\230\320\267\320\261\321\213\321\202\320\276\321\207\320\275\320\276\321\201\321\202\321\214.md" +++ "b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\230\320\267\320\261\321\213\321\202\320\276\321\207\320\275\320\276\321\201\321\202\321\214.md" @@ -43,6 +43,7 @@ | [RD0811](./RD0811.md) | Избыточное указание EXECUTE AS CALLER | [RD0814](./RD0814.md) | Переменная указана более одного раза в предикате IN | [RD0849](./RD0849.md) | Ограничение индекса уже задано на уровне таблицы +| [RD0850](./RD0850.md) | Предикат WHERE уже применён на уровне INNER JOIN | [RD0925](./RD0925.md) | Использование LIKE без спецсимволов | [RD0926](./RD0926.md) | Избыточная опция NOT FOR REPLICATION | [RD0927](./RD0927.md) | Использование CHECK CONSTRAINT вместо атрибута столбца diff --git "a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\230\320\274\320\265\320\275\320\276\320\262\320\260\320\275\320\270\320\265.md" "b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\230\320\274\320\265\320\275\320\276\320\262\320\260\320\275\320\270\320\265.md" index 17d2d278..762f8e44 100644 --- "a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\230\320\274\320\265\320\275\320\276\320\262\320\260\320\275\320\270\320\265.md" +++ "b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\230\320\274\320\265\320\275\320\276\320\262\320\260\320\275\320\270\320\265.md" @@ -16,6 +16,7 @@ | [NM0271](./NM0271.md) | Запрещены наименования @@, ## или подобные | [NM0712](./NM0712.md) | Объект данного типа не может быть временным | [NM0714](./NM0714.md) | Ключевое слово использовано для алиаса +| [NM0854](./NM0854.md) | Символ другого алфавита в идентификаторе | [NM0961](./NM0961.md) | Наименование индекса нарушает установленный шаблон | [NM0962](./NM0962.md) | Наименование триггера нарушает установленный шаблон | [NM0963](./NM0963.md) | Нарушение соглашения об именовании таблиц - ожидается lower_snake_case diff --git "a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\235\320\265\320\276\320\264\320\275\320\276\320\267\320\275\320\260\321\207\320\275\320\276\321\201\321\202\321\214.md" "b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\235\320\265\320\276\320\264\320\275\320\276\320\267\320\275\320\260\321\207\320\275\320\276\321\201\321\202\321\214.md" index 4cf2d542..4e676c8b 100644 --- "a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\235\320\265\320\276\320\264\320\275\320\276\320\267\320\275\320\260\321\207\320\275\320\276\321\201\321\202\321\214.md" +++ "b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\235\320\265\320\276\320\264\320\275\320\276\320\267\320\275\320\260\321\207\320\275\320\276\321\201\321\202\321\214.md" @@ -16,13 +16,15 @@ | [AM0168](./AM0168.md) | Неуникальный алиас или имя столбца | [AM0169](./AM0169.md) | Неуникальный алиас таблицы или подзапроса | [AM0170](./AM0170.md) | Алиас таблицы или подзапроса совпадает с именем другой таблицы -| [AM0903](./AM0903.md) | Множественный возврат в одну и ту же переменную -| [AM0935](./AM0935.md) | Не указан алиас таблицы-источника для столбца -| [AM0996](./AM0996.md) | Множественные изменения переменной в одном выражении | [AM0291](./AM0291.md) | Не указан тип индекса CLUSTERED/NONCLUSTERED | [AM0702](./AM0702.md) | Неоднозначность или избыточность определения уникальности индекса | [AM0718](./AM0718.md) | Вызов OBJECT_ID() без указания типа объекта | [AM0728](./AM0728.md) | Неоднозначность отрицания составного предиката | [AM0740](./AM0740.md) | Модификация или удаление объекта без указания схемы +| [AM0836](./AM0836.md) | Функция COLUMNS_UPDATED может давать неоднозначный результат +| [AM0891](./AM0891.md) | Неоднозначность выходного типа NULL-столбца +| [AM0903](./AM0903.md) | Множественный возврат в одну и ту же переменную +| [AM0935](./AM0935.md) | Не указан алиас таблицы-источника для столбца +| [AM0996](./AM0996.md) | Множественные изменения переменной в одном выражении [На основную страницу документации](./readme.md) diff --git "a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\235\320\265\320\277\321\200\320\265\321\200\321\213\320\262\320\275\320\260\321\217\320\237\320\276\321\201\321\202\320\260\320\262\320\272\320\260.md" "b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\235\320\265\320\277\321\200\320\265\321\200\321\213\320\262\320\275\320\260\321\217\320\237\320\276\321\201\321\202\320\260\320\262\320\272\320\260.md" index c2e0d2f4..d1a0166f 100644 --- "a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\235\320\265\320\277\321\200\320\265\321\200\321\213\320\262\320\275\320\260\321\217\320\237\320\276\321\201\321\202\320\260\320\262\320\272\320\260.md" +++ "b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\235\320\265\320\277\321\200\320\265\321\200\321\213\320\262\320\275\320\260\321\217\320\237\320\276\321\201\321\202\320\260\320\262\320\272\320\260.md" @@ -15,5 +15,6 @@ | [CD0726](./CD0726.md) | Неожиданный блок кода в корне скрипта | [CD0779](./CD0779.md) | EXECUTE AS SELF нежелателен | [CD0833](./CD0833.md) | Перечисление секций в функции секционирования +| [CD0857](./CD0857.md) | В одном файле задекларированы несколько объектов [На основную страницу документации](./readme.md) diff --git "a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\237\321\200\320\276\320\270\320\267\320\262\320\276\320\264\320\270\321\202\320\265\320\273\321\214\320\275\320\276\321\201\321\202\321\214.md" "b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\237\321\200\320\276\320\270\320\267\320\262\320\276\320\264\320\270\321\202\320\265\320\273\321\214\320\275\320\276\321\201\321\202\321\214.md" index 109a2cd8..99c44632 100644 --- "a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\237\321\200\320\276\320\270\320\267\320\262\320\276\320\264\320\270\321\202\320\265\320\273\321\214\320\275\320\276\321\201\321\202\321\214.md" +++ "b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\237\321\200\320\276\320\270\320\267\320\262\320\276\320\264\320\270\321\202\320\265\320\273\321\214\320\275\320\276\321\201\321\202\321\214.md" @@ -11,6 +11,13 @@ | [PF0720](./PF0720.md) | Непредвиденная сортировка результирующего набора CTE | [PF0775](./PF0775.md) | Не отфильтрованный SPARSE столбец в индексе | [PF0823](./PF0823.md) | Указано двойное требование рекомпиляции +| [PF0868](./PF0868.md) | Ручное управление планом запроса через JOIN hint +| [PF0869](./PF0869.md) | Ручное управление планом запроса через table hint +| [PF0870](./PF0870.md) | Ручное управление планом запроса через query hint +| [PF0871](./PF0871.md) | Ручное управление планом запроса через USE PLAN или SET FORCEPLAN ON +| [PF0875](./PF0875.md) | DDL вперемешку с DML +| [PF0876](./PF0876.md) | Вместо фильтра по диапазону применена функция преобразования даты +| [PF0877](./PF0877.md) | Non-SARGable ISNULL вместо OR | [PF0910](./PF0910.md) | Индексация столбца, допускающего NULL или имеющего значение по умолчанию | [PF0928](./PF0928.md) | Индекс имеет фильтр по NULL для столбца, который не включён в состав индекса | [PF0929](./PF0929.md) | Неоптимальный предикат diff --git "a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\241\320\261\320\276\320\271.md" "b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\241\320\261\320\276\320\271.md" index 0f0c7ca8..815c9bc5 100644 --- "a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\241\320\261\320\276\320\271.md" +++ "b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\241\320\261\320\276\320\271.md" @@ -35,6 +35,8 @@ | [FA0770](./FA0770.md) | SPARSE столбцы не поддерживают указанные опции или недопустимы в данной конструкции | [FA0771](./FA0771.md) | Недопустимый тип данных для SPARSE столбца | [FA0822](./FA0822.md) | Недопустимая опция для CLR-модуля +| [FA0884](./FA0884.md) | Недопустимое значение параметра редактирования расширенного свойства объекта +| [FA0885](./FA0885.md) | RAISERROR с severity от 19 и выше без WITH LOG | [FA0901](./FA0901.md) | Столбец может быть изменен только один раз в одной инструкции | [FA0902](./FA0902.md) | Несовместимые опции курсора | [FA0904](./FA0904.md) | Индекс ссылается на неизвестный столбец diff --git "a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\241\320\276\320\263\320\273\320\260\321\210\320\265\320\275\320\270\321\217\320\237\320\276\320\232\320\276\320\264\320\270\321\200\320\276\320\262\320\260\320\275\320\270\321\216.md" "b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\241\320\276\320\263\320\273\320\260\321\210\320\265\320\275\320\270\321\217\320\237\320\276\320\232\320\276\320\264\320\270\321\200\320\276\320\262\320\260\320\275\320\270\321\216.md" index 26500bb9..9680b3ce 100644 --- "a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\241\320\276\320\263\320\273\320\260\321\210\320\265\320\275\320\270\321\217\320\237\320\276\320\232\320\276\320\264\320\270\321\200\320\276\320\262\320\260\320\275\320\270\321\216.md" +++ "b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\241\320\276\320\263\320\273\320\260\321\210\320\265\320\275\320\270\321\217\320\237\320\276\320\232\320\276\320\264\320\270\321\200\320\276\320\262\320\260\320\275\320\270\321\216.md" @@ -40,6 +40,7 @@ | [CV0803](./CV0803.md) | Системные типы нужно использовать без схемы | [CV0805](./CV0805.md) | PRINT в бизнес-логике | [CV0810](./CV0810.md) | Вызов хранимой процедуры нужно предварять словом EXEC +| [CV0837](./CV0837.md) | Рассмотрите переход на XQuery | [CV0838](./CV0838.md) | Регистр при написании имён системных типов | [CV0839](./CV0839.md) | Регистр в упоминаниях глобальных переменных diff --git "a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\241\320\276\320\274\320\275\320\270\321\202\320\265\320\273\321\214\320\275\320\276\320\265\320\232\320\276\320\264\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265.md" "b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\241\320\276\320\274\320\275\320\270\321\202\320\265\320\273\321\214\320\275\320\276\320\265\320\232\320\276\320\264\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265.md" index 4724d3fa..fd8efd93 100644 --- "a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\241\320\276\320\274\320\275\320\270\321\202\320\265\320\273\321\214\320\275\320\276\320\265\320\232\320\276\320\264\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265.md" +++ "b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\241\320\276\320\274\320\275\320\270\321\202\320\265\320\273\321\214\320\275\320\276\320\265\320\232\320\276\320\264\320\270\321\200\320\276\320\262\320\260\320\275\320\270\320\265.md" @@ -94,6 +94,14 @@ | [CS0842](./CS0842.md) | В комментарии присутствуют непечатные символы | [CS0843](./CS0843.md) | Выражение не наполняет используемую служебную таблицу | [CS0844](./CS0844.md) | Все ветки условного поведения приводят к одинаковому результату +| [CS0851](./CS0851.md) | Соединение заявлено как внешнее (OUTER), но работает как внутреннее (INNER) +| [CS0852](./CS0852.md) | Предикат соединения не связан с присоединяемыми источниками +| [CS0853](./CS0853.md) | Левая часть выражения сравнения совпадает с правой +| [CS0856](./CS0856.md) | Программный модуль вызывает сам себя +| [CS0886](./CS0886.md) | Заданы расширенные свойства другого объекта +| [CS0887](./CS0887.md) | Указанный для расширенного свойства столбец не существует +| [CS0888](./CS0888.md) | Дубликат расширенного свойства +| [CS0890](./CS0890.md) | Обращение к свойствам объектов уровня БД или сервера | [CS0905](./CS0905.md) | Аргумент даты или времени не имеет запрошенной детализации | [CS0914](./CS0914.md) | Различающиеся литералы в конструкциях INTERSECT/EXCEPT | [CS0917](./CS0917.md) | Недопустимый hint в INSERT diff --git "a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\243\320\277\321\200\320\276\321\211\320\265\320\275\320\270\320\265.md" "b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\243\320\277\321\200\320\276\321\211\320\265\320\275\320\270\320\265.md" index 70afa765..3e743bfc 100644 --- "a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\243\320\277\321\200\320\276\321\211\320\265\320\275\320\270\320\265.md" +++ "b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\232\320\260\321\202\320\265\320\263\320\276\321\200\320\270\321\217_\320\243\320\277\321\200\320\276\321\211\320\265\320\275\320\270\320\265.md" @@ -11,5 +11,6 @@ | [SI0846](./SI0846.md) | Проверки на неравенство могут быть объединены в предикат NOT IN | [SI0847](./SI0847.md) | Схожие предикаты IN могут быть объединены | [SI0848](./SI0848.md) | Схожие предикаты NOT IN могут быть объединены +| [SI0889](./SI0889.md) | Обращение к свойству объекта вместо ограничения типа [На основную страницу документации](./readme.md) diff --git a/TeamTools.TSQL.Linter/Resources/SqlServerMetadata.json b/TeamTools.TSQL.Linter/Resources/SqlServerMetadata.json index 15359536..f5ed1be5 100644 --- a/TeamTools.TSQL.Linter/Resources/SqlServerMetadata.json +++ b/TeamTools.TSQL.Linter/Resources/SqlServerMetadata.json @@ -907,7 +907,8 @@ }, "EOMONTH": { "ResultType": "DATE", - "ParamCountMin": 1 + "ParamCountMin": 1, + "ParamCountMax": 2 }, "DATALENGTH": { diff --git a/TeamTools.TSQL.Linter/Resources/ViolationMessages.json b/TeamTools.TSQL.Linter/Resources/ViolationMessages.json index f6f486ab..2f0f03b7 100644 --- a/TeamTools.TSQL.Linter/Resources/ViolationMessages.json +++ b/TeamTools.TSQL.Linter/Resources/ViolationMessages.json @@ -13,6 +13,10 @@ "FA0256:SEMICOLON_BEFORE_THROW": "Missing semicolon before calling THROW", "FA0281:UNIQUE_DERIVED_DEF_COL_NAME": "Subquery definition has duplicate column names", "FA0283:INVALID_XML_LITERAL": "Invalid XML literal", + "FA0451:UNRESOLVED_WINDOW_NAME": "Unresolved WINDOW reference", + "FA0457:VARIABLE_REDECLARED": "Variable redeclared", + "FA0458:UNRESOLVED_VARIABLED_NAME": "Unresolved variable reference", + "FA0459:UNRESOLVED_TABLE_VAR_NAME": "Unresolved table variable reference", "FA0703:BAD_TYPE_FOR_COLUMNSTORE": "COLUMNSTORE index cannot contain columns of this type", "FA0704:INVALID_ARGUMENT_COUNT": "Invalid number of arguments", "FA0705:INVALID_ARGUMENT": "Invalid argument value", @@ -32,6 +36,8 @@ "FA0770:SPARSE_UNSUPPORTED_FEATURE": "SPARSE columns aren't supported the specified options or aren't allowed in the given construction", "FA0771:SPARSE_UNSUPPORTED_TYPE": "Illegal datatype is used for SPARSE column", "FA0822:INVALID_CLR_OPTION": "Invalid option for CLR module", + "FA0884:INVALID_EXTENDED_PROPERTY_PARAM": "Invalid extended property editing parameter value", + "FA0885:RAISERROR_NEEDS_WITH_LOG": "RAISERROR with severity 19 or higher missing WITH LOG option", "FA0901:DUP_COLUMN_MODIFIER": "Column can be modified only once per statement", "FA0902:CURSOR_OPTION_INCOMPATIBLE": "Incompatible cursor options", "FA0904:INDEX_REFERS_UNKNOWN_COL": "Index refers to unknown column", @@ -68,6 +74,8 @@ "VU0512:PRIVILEGE_MANAGEMENT": "Insecure database participant privilege management from code", "VU0518:EXECUTE_AS_OWNER": "Possibly insecure user context switch to schema owner", "VU0519:SYS_DYNAMIC_VIEW": "Forbidden system dynamic view reference", + "VU0525:OUTPUT_SECRET": "Attempt to output a secret", + "VU0526:READ_SECRET": "Reading DB user privileges from system tables", "DE0401:DEPRECATED_UNIT": "Deprecated unit is used", "DE0402:DEPRECATED_TYPE": "Deprecated type is used", @@ -96,6 +104,7 @@ "AM0168:NONUNIQUE_COLUMN_ALIAS": "Non-unique column alias in output", "AM0169:NONUNIQUE_TABLE_ALIAS": "Non-unique table alias - possible reference ambiguity", "AM0170:TABLE_ALIAS_MIMICKS_OTHER_TABLE": "Table alias mimics other table - possible reference ambiguity", + "AM0891:SELECT_NULL_TYPE": "Ambiguous NULL column output type", "AM0903:SAME_VAR_MULTIPLE_OUTPUT": "Multiple output to the same variable", "AM0935:AMBIGUOUS_COL_SOURCE": "Column missing table alias - ambiguous source table", "AM0996:MULTI_SET_SAME_VAR": "Ambiguous variable modifications in one statement", @@ -130,6 +139,7 @@ "RD0293:REDUNDANT_CASE_ELSE_NULL": "Unnecessary ELSE NULL option", "RD0296:REDUNDANT_ORDER_BY_CONST": "Redundant ordering by constant value attempt", "RD0307:EOF_REDUNDANT_NEWLINE": "Redundant empty line at end of file", + "RD0452:UNUSED_WINDOW_CLAUSE": "Unused WINDOW definition", "RD0706:REDUNDANT_TYPE_CONVERSION": "Redundant type conversion", "RD0707:REDUNDANT_ARGUMENT": "Redundant argument", "RD0713:COLUMN_ALIAS_IS_THE_SAME": "Alias equals column name and can be omitted", @@ -147,6 +157,8 @@ "RD0814:IN_DUP_VAR": "Variable is specified more than once for IN predicate", "RD0849:REDUNDANT_INDEX_FILTER": "The index constraint is already defined at the table level", "RD0850:EXTRA_WHERE_PREDICATE": "The WHERE predicate was already applied at INNER JOIN level", + "RD0882:REDUNDANT_MAX_RECURSION": "Redundant MAXRECURSION option in a query with no recursive CTE", + "RD0883:REDUNDANT_ISNULL_NOT_EQUALS": "Redundant ISNULL computation for inequality comparison", "RD0925:REDUNDANT_LIKE": "Redundant LIKE without wildcards", "RD0926:REDUNDANT_NOT_FOR_REPLICATION": "Redundant NOT FOR REPLICATION option", "RD0927:REDUNDANT_COL_NULLABILITY_CHECK": "Nullability check constraint used instead of column attribute", @@ -164,6 +176,21 @@ "PF0720:SORTED_CTE": "Unexpected sorting of CTE output", "PF0775:SPARSE_COL_INDEX_FILTER": "SPARSE column not filtered in index", "PF0823:RECOMPILE_RECOMPILE": "Double recompile requested", + "PF0867:INDEX_FK": "Foreign key columns not indexed", + "PF0868:OPTIMIZER_FIGHT_JOIN_HINT": "Manual query plan control via JOIN hint", + "PF0869:OPTIMIZER_FIGHT_TABLE_HINT": "Manual query plan control via table hint", + "PF0870:OPTIMIZER_FIGHT_QUERY_OPTION": "Manual query plan control via query hint", + "PF0871:OPTIMIZER_FIGHT_FORCE_PLAN": "Manual query plan control via USE PLAN or SET FORCEPLAN ON", + "PF0872:SERIAL_PLAN_FORCED": "Serial plan forced manually", + "PF0873:SERIAL_PLAN_FORCED_TABLE_VAR": "Serial plan forced because of table variable modification", + "PF0874:SERIAL_PLAN_ZONE_FORCED": "Serial plan zone forced by instruction", + "PF0875:DDL_DML_MIX": "DDL is mixed with DML", + "PF0876:EXPAND_DATE_FUNC": "Date function is used instead of range filter", + "PF0877:EXPAND_ISNULL_FOR_OPTIONAL_ARG": "Non-SARGable ISNULL is used instead of OR", + "PF0878:SINGLE_USER_MODE_ZONE_FORCED": "Single-user mode zone forced by instruction", + "PF0879:TEMP_TABLE_CACHING_PREVENTED": "Temp table caching prevented by instruction", + "PF0880:NO_EQUALITY_FILTER_JOIN": "JOIN predicate is missing straight equality filter", + "PF0881:NO_EQUALITY_FILTER_WHERE": "WHERE predicate is missing straight equality filter", "PF0910:INDEXING_COL_WITH_DEFAULT": "Indexing column allowing NULL/with default defined without filter on default value", "PF0928:FILTERED_IDX_FOR_NULL_COL_NOT_INCLUDED": "Index column is filtered for NULL but not included into index", "PF0929:NON_SARGABLE_PREDICATE": "Non-sargable predicate", @@ -298,7 +325,12 @@ "CV0837:SP_XML_TO_XQUERY": "Consider utilizing XQuery technique", "CV0838:SYSTEM_TYPE_UPPERCASE": "System type is not in UPPERCASE", "CV0839:GLOBAL_VAR_UPPERCASE": "Global variable is not in UPPERCASE", + "CV0858:FETCH_FULLY_QUALIFIED": "FETCH statement is not fully qualified", + "CV0897:SET_OPTIONS_ASC_ORDER": "Options enumerated not alphabetically", + "SI0453:EXTRACT_WINDOW_CLAUSE": "Reusable WINDOW definition can be extracted", + "SI0454:REUSE_EXPRESSION_ALIAS": "Expression can be reused via column alias", + "SI0455:EXTRACT_EXPRESSION": "Repeated expression can be reused", "SI0727:NOT_FOR_SIMPLE_PREDICATE": "Negated comparison can be simplified", "SI0735:SET_TO_DECLARE": "Variable assignment can be simplified - set value in DECLARE", "SI0753:DROP_STATEMENTS_INTO_ONE": "Multiple DROP statements can be collapsed into one", @@ -307,6 +339,9 @@ "SI0846:MULTIPLE_AND_TO_NOT_IN": "Multiple inequality checks can be combined into single NOT IN predicate", "SI0847:MULTIPLE_IN_TO_SINGLE": "Multiple similar IN predicates can be combined into single one", "SI0848:MULTIPLE_NOT_IN_TO_SINGLE": "Multiple similar NOT IN predicates can be combined into single one", + "SI0859:MULTIPLE_INSERT_VALUES_COLLAPSE": "Multiple INSERT-VALUES can be collapsed into one", + "SI0889:USE_TYPE_NOT_PROPERTY": "Object property requested instead of type constraint", + "SI0896:MULTIPLE_SET_INTO_ONE": "SET option statements can be combined", "DD0153:FK_MULTIPLE_COL": "Composite foreign key", "DD0158:TABLE_ALL_COL_NULL": "All table columns can contain a NULL value", @@ -333,6 +368,14 @@ "DD0828:COMPUTED_COL_SYNONYM": "Computed column mirrors other column", "DD0829:FK_ON_TMP": "Foreign keys with temporary tables", "DD0831:HISTORY_IN_SAME_SCHEMA": "History table not on the same schema as the origin", + "DD0855:SCALAR_UDT": "User-defined scalar type derived from a system type", + "DD0860:HISTORY_PERIOD_SET": "PERIOD is undefined on temporal table", + "DD0861:HISTORY_SET_STORAGE_NAME": "History storage table name is undefined", + "DD0862:HISTORY_CONSISTENCY_CHECK": "DATA_CONSISTENCY_CHECK option is disabled", + "DD0863:HISTORY_SAME_DATE_RANGE_PRECISION": "Different precision of historical period columns", + "DD0864:HISTORY_FUTURE_DATE": "Probably incorrect rounding start date of historical period", + "DD0865:HISTORY_PERIOD_DEFAULT_PRECISION": "Precision of DEFAULT value is lower than precision of historical period column", + "DD0866:HISTORY_MAX_DATE_PRECISION": "Insufficient precision of historical period columns", "DD0906:FK_RECURSION": "Recursive foreign key", "DD0908:NONCLUSTERED_IDX_INCLUDES_CLUSTERED": "Nonclustered index includes clustered index columns", "DD0909:INDEX_DUP_COLUMN": "Column is included in the index more than once", @@ -361,6 +404,8 @@ "CD0726:CODE_IN_SCRIPT_ROOT": "Unexpected code block in script root", "CD0779:EXECUTE_AS_SELF": "Execute as SELF is not welcome", "CD0833:PARTITIONING_RANGE_VALUES": "Partition range values in source code", + "CD0857:SINGLE_OBJECT_PER_FILE": "Multiple objects are defined in the same file", + "CD0895:CREATE_OPTIONS_INSIDE": "Create options defined inside object definition", "FL0301:FILE_ENCODING": "Wrong encoding, use UTF-8 with BOM instead", "FL0303:CRLF": "Line not end with the CRLF", @@ -411,13 +456,14 @@ "CS0190:ALWAYS_EMPTY_SOURCE": "Reading from empty table", "CS0191:NEVER_USED_SOURCE": "Source table filled but never used", "CS0195:CURSOR_LOCAL": "Cursor isn't declared as LOCAL", - "CS0196:CURSOR_READONLY": "Cursor isn't declared explicitly as READONLY or FOR UPDATEY", + "CS0196:CURSOR_READONLY": "Cursor isn't declared explicitly as READONLY or FOR UPDATE", "CS0197:CURSOR_COMMAND_ORDER": "The order of cursor handling commands is incorrect", "CS0198:TRIGGER_CIRCULAR_ACTION": "Trigger cyclical execution", "CS0250:REDUNDANT_OPERATOR": "Redundant unary operator", "CS0266:TABLE_LEVEL_CONSTRAINT_IN_COL": "Table-level constraint is defined not at table level", "CS0295:ORDER_BY_POSITION": "ORDER BY uses column position", "CS0299:COMPLICATED_IIF_TO_CASE": "IIF includes nested conditional constructs or queries", + "CS0456:USELESS_UNIT": "Useless unit", "CS0514:SHOWING_STATS": "Debug construction is used in code", "CS0520:APP_LOCK": "App-lock is used in code", "CS0521:SYSPROC_RETURN_NOT_CHECKED": "System procedure return value isn't checked", @@ -464,6 +510,14 @@ "CS0851:FAKE_OUTER_JOIN": "Join is defined as OUTER but seems to behave as INNER", "CS0852:NON_CORRELATED_JOIN_PREDICATE": "Join predicate is not correlated with the joined sources", "CS0853:COMPARISON_LEFT_EQUALS_RIGHT": "Left part of comparison is similar to the right part", + "CS0856:SELF_CALL": "Programmability invokes itself", + "CS0886:EXTENDED_PROPERTY_MISDIRECTED": "Extended property is defined for wrong object", + "CS0887:EXTENDED_PROPERTY_FOR_MISSING_COL": "Extended property is defined for missing column", + "CS0888:EXTENDED_PROPERTY_DUP": "Extended property duplicate", + "CS0890:OBJECT_PROPERTY_FIDDLING": "Accessing Database- or Server-level object properties", + "CS0892:CAST_XML_TO_STRING": "Casting XML directly to string without unescaping", + "CS0893:ISOLATION_CONTRADICTION": "Jumping between isolation levels", + "CS0894:ISOLATION_CHAOS_PER_QUERY": "Isolation levels chaos per query", "CS0905:VAR_LACKS_PRECISION": "Argument does not have requested details", "CS0914:INTERSECT_EXCEPT_BROKEN_BY_LITERAL": "Different literals in INTERSECT/EXCEPT construction", "CS0917:FORBIDDEN_INSERT_HINTS": "Forbidden INSERT hint is used", diff --git a/TeamTools.TSQL.Linter/Resources/ViolationMessages.ru-ru.json b/TeamTools.TSQL.Linter/Resources/ViolationMessages.ru-ru.json index 1de49274..28b789a5 100644 --- a/TeamTools.TSQL.Linter/Resources/ViolationMessages.ru-ru.json +++ b/TeamTools.TSQL.Linter/Resources/ViolationMessages.ru-ru.json @@ -13,6 +13,10 @@ "FA0256:SEMICOLON_BEFORE_THROW": "Отсутствует точка с запятой перед THROW", "FA0281:UNIQUE_DERIVED_DEF_COL_NAME": "Определение подзапроса содержит повторяющиеся имена столбцов", "FA0283:INVALID_XML_LITERAL": "Не валидный XML-литерал", + "FA0451:UNRESOLVED_WINDOW_NAME": "Неразрешимая ссылка на определение WINDOW", + "FA0457:VARIABLE_REDECLARED": "Переменная объявлена повторно", + "FA0458:UNRESOLVED_VARIABLED_NAME": "Неразрешимая ссылка на переменную", + "FA0459:UNRESOLVED_TABLE_VAR_NAME": "Неразрешимая ссылка на табличную переменную", "FA0703:BAD_TYPE_FOR_COLUMNSTORE": "COLUMNSTORE индекс не может содержать столбцы данного типа", "FA0704:INVALID_ARGUMENT_COUNT": "Недопустимое число аргументов", "FA0705:INVALID_ARGUMENT": "Недопустимое значение аргумента", @@ -32,6 +36,8 @@ "FA0770:SPARSE_UNSUPPORTED_FEATURE": "SPARSE столбцы не поддерживают указанные опции или недопустимы в данной конструкции", "FA0771:SPARSE_UNSUPPORTED_TYPE": "Недопустимый тип данных для SPARSE столбца", "FA0822:INVALID_CLR_OPTION": "Недопустимая опция для CLR-модуля", + "FA0884:INVALID_EXTENDED_PROPERTY_PARAM": "Недопустимое значение параметра редактирования расширенного свойства объекта", + "FA0885:RAISERROR_NEEDS_WITH_LOG": "RAISERROR с severity от 19 и выше без WITH LOG", "FA0901:DUP_COLUMN_MODIFIER": "Столбец может быть изменен только один раз в одной инструкции", "FA0902:CURSOR_OPTION_INCOMPATIBLE": "Несовместимые опции курсора", "FA0904:INDEX_REFERS_UNKNOWN_COL": "Индекс ссылается на неизвестный столбец", @@ -68,6 +74,8 @@ "VU0512:PRIVILEGE_MANAGEMENT": "Небезопасное изменение привелегий пользователя", "VU0518:EXECUTE_AS_OWNER": "Вероятно небезопасное переключение контекста пользователя на владельца схемы", "VU0519:SYS_DYNAMIC_VIEW": "Запрещенное использование системных динамических представлений", + "VU0525:OUTPUT_SECRET": "Попытка выдачи наружу секрета", + "VU0526:READ_SECRET": "Чтение системных данных о привилегия пользователей", "DE0401:DEPRECATED_UNIT": "Использован устаревший модуль", "DE0402:DEPRECATED_TYPE": "Использован устаревший тип данных", @@ -96,6 +104,7 @@ "AM0168:NONUNIQUE_COLUMN_ALIAS": "Неуникальный алиас или имя столбца", "AM0169:NONUNIQUE_TABLE_ALIAS": "Неуникальный алиас таблицы или подзапроса", "AM0170:TABLE_ALIAS_MIMICKS_OTHER_TABLE": "Алиас таблицы или подзапроса совпадает с именем другой таблицы", + "AM0891:SELECT_NULL_TYPE": "Неоднозначность выходного типа NULL-столбца", "AM0903:SAME_VAR_MULTIPLE_OUTPUT": "Множественный возврат в одну и ту же переменную", "AM0935:AMBIGUOUS_COL_SOURCE": "Не указан алиас таблицы-источника для столбца", "AM0996:MULTI_SET_SAME_VAR": "Множественные изменения переменной в одном выражении", @@ -130,6 +139,7 @@ "RD0293:REDUNDANT_CASE_ELSE_NULL": "Избыточное указание ELSE NULL", "RD0296:REDUNDANT_ORDER_BY_CONST": "Попытка избыточного упорядочивания по константе", "RD0307:EOF_REDUNDANT_NEWLINE": "Избыточная пустая строка в конце файла", + "RD0452:UNUSED_WINDOW_CLAUSE": "Неиспользуемое определение окна", "RD0706:REDUNDANT_TYPE_CONVERSION": "Избыточное преобразование типов", "RD0707:REDUNDANT_ARGUMENT": "Избыточный аргумент", "RD0713:COLUMN_ALIAS_IS_THE_SAME": "Псевдоним равен имени столбца и может быть опущен", @@ -146,7 +156,9 @@ "RD0811:EXECUTE_AS_CALLER": "Избыточное указание EXECUTE AS CALLER", "RD0814:IN_DUP_VAR": "Переменная указана более одного раза в предикате IN", "RD0849:REDUNDANT_INDEX_FILTER": "Ограничение индекса уже задано на уровне таблицы", - "RD0850:EXTRA_WHERE_PREDICATE": "Фильтр WHERE уже применён на уровне INNER JOIN", + "RD0850:EXTRA_WHERE_PREDICATE": "Предикат WHERE уже применён на уровне INNER JOIN", + "RD0882:REDUNDANT_MAX_RECURSION": "Избыточное указание MAXRECURSION в запросе без рекурсивных CTE", + "RD0883:REDUNDANT_ISNULL_NOT_EQUALS": "Избыточное вычисление ISNULL в неравенстве", "RD0925:REDUNDANT_LIKE": "Использование LIKE без спецсимволов", "RD0926:REDUNDANT_NOT_FOR_REPLICATION": "Избыточная опция NOT FOR REPLICATION", "RD0927:REDUNDANT_COL_NULLABILITY_CHECK": "Использование CHECK CONSTRAINT вместо атрибута столбца", @@ -164,6 +176,21 @@ "PF0720:SORTED_CTE": "Непредвиденная сортировка результирующего набора CTE", "PF0775:SPARSE_COL_INDEX_FILTER": "Не отфильтрованный SPARSE столбец в индексе", "PF0823:RECOMPILE_RECOMPILE": "Указано двойное требование рекомпиляции", + "PF0867:INDEX_FK": "Столбцы внешнего ключа не индексированы", + "PF0868:OPTIMIZER_FIGHT_JOIN_HINT": "Ручное управление планом запроса через JOIN hint", + "PF0869:OPTIMIZER_FIGHT_TABLE_HINT": "Ручное управление планом запроса через table hint", + "PF0870:OPTIMIZER_FIGHT_QUERY_OPTION": "Ручное управление планом запроса через query hint", + "PF0871:OPTIMIZER_FIGHT_FORCE_PLAN": "Ручное управление планом запроса через USE PLAN или SET FORCEPLAN ON", + "PF0872:SERIAL_PLAN_FORCED": "Ручное принуждение к однопоточному плану выполнения", + "PF0873:SERIAL_PLAN_FORCED_TABLE_VAR": "Принуждение к однопоточному плану выполнения из-за модификации табличной переменной", + "PF0874:SERIAL_PLAN_ZONE_FORCED": "Принуждение к однопоточному фрагменту плана выполнения явной инструкцией", + "PF0875:DDL_DML_MIX": "DDL вперемешку с DML", + "PF0876:EXPAND_DATE_FUNC": "Вместо фильтра по диапазону применена функция преобразования даты", + "PF0877:EXPAND_ISNULL_FOR_OPTIONAL_ARG": "Non-SARGable ISNULL вместо OR", + "PF0878:SINGLE_USER_MODE_ZONE_FORCED": "Принуждение к однопользовательскому доступу к данным явной инструкцией", + "PF0879:TEMP_TABLE_CACHING_PREVENTED": "Кеширование временных таблиц невозможно из-за явной инструкции", + "PF0880:NO_EQUALITY_FILTER_JOIN": "JOIN не содержит явного фильтра по строгому равенству", + "PF0881:NO_EQUALITY_FILTER_WHERE": "WHERE не содержит явного фильтра по строгому равенству", "PF0910:INDEXING_COL_WITH_DEFAULT": "Индексация столбца, допускающего NULL или имеющего значение по умолчанию", "PF0928:FILTERED_IDX_FOR_NULL_COL_NOT_INCLUDED": "Индекс имеет фильтр по NULL для столбца, который не включён в состав индекса", "PF0929:NON_SARGABLE_PREDICATE": "Неоптимальный предикат", @@ -298,7 +325,12 @@ "CV0837:SP_XML_TO_XQUERY": "Рассмотрите переход на XQuery", "CV0838:SYSTEM_TYPE_UPPERCASE": "Регистр при написании имён системных типов", "CV0839:GLOBAL_VAR_UPPERCASE": "Регистр в упоминаниях глобальных переменных", + "CV0858:FETCH_FULLY_QUALIFIED": "В выражении FETCH отсутствуют нужные инструкции", + "CV0897:SET_OPTIONS_ASC_ORDER": "Опции перечислены не в алфавитном порядке", + "SI0453:EXTRACT_WINDOW_CLAUSE": "Определение окна может быть переиспользовано", + "SI0454:REUSE_EXPRESSION_ALIAS": "Выражение может быть переиспользовано через алиас столбца", + "SI0455:EXTRACT_EXPRESSION": "Повторяющееся выражение может быть переиспользовано", "SI0727:NOT_FOR_SIMPLE_PREDICATE": "Отрицание простого сравнения можно упростить", "SI0735:SET_TO_DECLARE": "Значение переменной можно указать в DECLARE", "SI0753:DROP_STATEMENTS_INTO_ONE": "Несколько выражений DROP можно объединить в одно", @@ -307,6 +339,9 @@ "SI0846:MULTIPLE_AND_TO_NOT_IN": "Проверки на неравенство могут быть объединены в предикат NOT IN", "SI0847:MULTIPLE_IN_TO_SINGLE": "Схожие предикаты IN могут быть объединены", "SI0848:MULTIPLE_NOT_IN_TO_SINGLE": "Схожие предикаты NOT IN могут быть объединены", + "SI0859:MULTIPLE_INSERT_VALUES_COLLAPSE": "Несколько подряд INSERT-VALUES можно объединить в один", + "SI0889:USE_TYPE_NOT_PROPERTY": "Обращение к свойству объекта вместо ограничения типа", + "SI0896:MULTIPLE_SET_INTO_ONE": "Выражения установки опций можно объединить", "DD0153:FK_MULTIPLE_COL": "Составной внешний ключ", "DD0158:TABLE_ALL_COL_NULL": "Все столбцы таблицы допускают NULL", @@ -333,6 +368,14 @@ "DD0828:COMPUTED_COL_SYNONYM": "Вычисляемый столбец повторяет значение другого столбца", "DD0829:FK_ON_TMP": "Внешние ключи с временными таблицами", "DD0831:HISTORY_IN_SAME_SCHEMA": "Историческая таблица размещена не в схеме исходника", + "DD0855:SCALAR_UDT": "Скалярный пользовательский тип на основе системного", + "DD0860:HISTORY_PERIOD_SET": "Для темпоральной таблицы не указан PERIOD", + "DD0861:HISTORY_SET_STORAGE_NAME": "Не указано имя исторической таблицы", + "DD0862:HISTORY_CONSISTENCY_CHECK": "Опция DATA_CONSISTENCY_CHECK отключена", + "DD0863:HISTORY_SAME_DATE_RANGE_PRECISION": "Разная точность столбцов исторического периода", + "DD0864:HISTORY_FUTURE_DATE": "Вероятно некорректное округление даты начала исторического периода", + "DD0865:HISTORY_PERIOD_DEFAULT_PRECISION": "Точность DEFAULT значения ниже точности столбца исторического периода", + "DD0866:HISTORY_MAX_DATE_PRECISION": "Недостаточная точность столбцов исторического периода", "DD0906:FK_RECURSION": "Рекурсивный внешний ключ", "DD0908:NONCLUSTERED_IDX_INCLUDES_CLUSTERED": "Некластерный индекс включает столбцы кластерного индекса", "DD0909:INDEX_DUP_COLUMN": "Столбец включён в индекс более одного раза", @@ -361,6 +404,8 @@ "CD0726:CODE_IN_SCRIPT_ROOT": "Неожиданный блок кода в корне скрипта", "CD0779:EXECUTE_AS_SELF": "EXECUTE AS SELF нежелателен", "CD0833:PARTITIONING_RANGE_VALUES": "Перечисление секций в функции секционирования", + "CD0857:SINGLE_OBJECT_PER_FILE": "В одном файле задекларированы несколько объектов", + "CD0895:CREATE_OPTIONS_INSIDE": "Опции создания заданы внутри объекта", "FL0301:FILE_ENCODING": "Неправильная кодировка, используйте UTF-8 с BOM", "FL0303:CRLF": "Строка заканчивается не на CRLF", @@ -418,6 +463,7 @@ "CS0266:TABLE_LEVEL_CONSTRAINT_IN_COL": "Ограничение уровня таблицы задано не на уровне таблицы", "CS0295:ORDER_BY_POSITION": "ORDER BY ссылается на позицию", "CS0299:COMPLICATED_IIF_TO_CASE": "В IIF вложены операторы ветвления или запросы", + "CS0456:USELESS_UNIT": "Бесполезный модуль", "CS0514:SHOWING_STATS": "Отладочные конструкции в коде", "CS0520:APP_LOCK": "Блокировки уровня приложения в коде", "CS0521:SYSPROC_RETURN_NOT_CHECKED": "Не проверяется возвращаемое значение системной процедуры", @@ -464,6 +510,14 @@ "CS0851:FAKE_OUTER_JOIN": "Соединение заявлено как внешнее (OUTER), но работает как внутреннее (INNER)", "CS0852:NON_CORRELATED_JOIN_PREDICATE": "Предикат соединения не связан с присоединяемыми источниками", "CS0853:COMPARISON_LEFT_EQUALS_RIGHT": "Левая часть выражения сравнения совпадает с правой", + "CS0856:SELF_CALL": "Программный модуль вызывает сам себя", + "CS0886:EXTENDED_PROPERTY_MISDIRECTED": "Заданы расширенные свойства другого объекта", + "CS0887:EXTENDED_PROPERTY_FOR_MISSING_COL": "Указанный для расширенного свойства столбец не существует", + "CS0888:EXTENDED_PROPERTY_DUP": "Дубликат расширенного свойства", + "CS0890:OBJECT_PROPERTY_FIDDLING": "Обращение к свойствам объектов уровня БД или сервера", + "CS0892:CAST_XML_TO_STRING": "Конвертация XML к строке без разэкранирования", + "CS0893:ISOLATION_CONTRADICTION": "Резкая смена уровня изоляции", + "CS0894:ISOLATION_CHAOS_PER_QUERY": "Разброс уровней изоляции в пределах одного запроса", "CS0905:VAR_LACKS_PRECISION": "Аргумент даты или времени не имеет запрошенной детализации", "CS0914:INTERSECT_EXCEPT_BROKEN_BY_LITERAL": "Различающиеся литералы в конструкциях INTERSECT/EXCEPT", "CS0917:FORBIDDEN_INSERT_HINTS": " Недопустимый hint в INSERT", diff --git a/TeamTools.TSQL.Linter/Routines/BooleanExpressionHandling/BooleanExpressionParts.cs b/TeamTools.TSQL.Linter/Routines/BooleanExpressionHandling/BooleanExpressionParts.cs index db3962db..9b4118da 100644 --- a/TeamTools.TSQL.Linter/Routines/BooleanExpressionHandling/BooleanExpressionParts.cs +++ b/TeamTools.TSQL.Linter/Routines/BooleanExpressionHandling/BooleanExpressionParts.cs @@ -126,7 +126,7 @@ private static string ExtractExpressionName(ScalarExpression node) } else if (node is ColumnReferenceExpression colRef) { - return colRef?.MultiPartIdentifier.Identifiers.GetFullName(); + return colRef.GetFullName(); } else if (node is StringLiteral str) { diff --git a/TeamTools.TSQL.Linter/Routines/CollapsibleInExtractor.cs b/TeamTools.TSQL.Linter/Routines/CollapsibleInExtractor.cs index 97783c39..69db6643 100644 --- a/TeamTools.TSQL.Linter/Routines/CollapsibleInExtractor.cs +++ b/TeamTools.TSQL.Linter/Routines/CollapsibleInExtractor.cs @@ -87,7 +87,7 @@ private IEnumerable ExpandExpression(BooleanExpression node) && filteredColRef.MultiPartIdentifier != null) { // filteredColRef.MultiPartIdentifier can be null for sys columns like $action - filteredItemName = filteredColRef.MultiPartIdentifier.Identifiers.GetFullName(); + filteredItemName = filteredColRef.GetFullName(); } else { diff --git a/TeamTools.TSQL.Linter/Routines/CombinablePredicateExtractor.cs b/TeamTools.TSQL.Linter/Routines/CombinablePredicateExtractor.cs index e1ae3b2b..41cb4cc7 100644 --- a/TeamTools.TSQL.Linter/Routines/CombinablePredicateExtractor.cs +++ b/TeamTools.TSQL.Linter/Routines/CombinablePredicateExtractor.cs @@ -226,7 +226,7 @@ public static CombinablePredicate Make(ScalarExpression filteredItem, ScalarExpr && filteredColRef.MultiPartIdentifier != null) { // filteredColRef.MultiPartIdentifier can be null for sys columns like $action - filteredItemName = filteredColRef.MultiPartIdentifier.Identifiers.GetFullName(); + filteredItemName = filteredColRef.GetFullName(); } else { diff --git a/TeamTools.TSQL.Linter/Routines/ExtendedPropertyEditingVisitor.cs b/TeamTools.TSQL.Linter/Routines/ExtendedPropertyEditingVisitor.cs new file mode 100644 index 00000000..f9b81018 --- /dev/null +++ b/TeamTools.TSQL.Linter/Routines/ExtendedPropertyEditingVisitor.cs @@ -0,0 +1,102 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + internal abstract class ExtendedPropertyEditingVisitor : TSqlFragmentVisitor + { + private static readonly HashSet PropertyEditProcs = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "sp_addextendedproperty", + "sp_updateextendedproperty", + "sp_dropextendedproperty", + }; + + protected ExtendedPropertyEditingVisitor(Action callback) + { + Callback = callback ?? throw new ArgumentNullException(nameof(callback)); + } + + protected Action Callback { get; } + + public override sealed void Visit(ExecuteSpecification node) + { + if (!(node.ExecutableEntity is ExecutableProcedureReference procRef)) + { + // EXEC 'cmd' + return; + } + + var procName = procRef.ProcedureReference.ProcedureReference?.Name; + + if (procName is null) + { + // EXEC @var + return; + } + + if (procName.SchemaIdentifier != null + && !string.Equals(procName.SchemaIdentifier.Value, TSqlDomainAttributes.SystemSchemaName)) + { + // not a sys proc call + return; + } + + if (!IsPropertyEditingProc(procName.BaseIdentifier.Value)) + { + // not a property editing proc + return; + } + + ValidatePropertyEditingProcArgs(procRef.Parameters, node); + } + + // Only named EXEC parameter provisioning is supported. + // There is a separate rule for preventing passing arguments by position. + protected static int MatchArgs( + IDictionary argValueMap, + IList procParams, + Action onMismatch) + { + int argCollectionFlags = 0; + + for (int i = procParams.Count - 1; i >= 0; i--) + { + var param = procParams[i]; + if (param.Variable != null + && param.ParameterValue is StringLiteral levelTypeValue + && argValueMap.TryGetValue(param.Variable.Name, out var expectedArgValue)) + { + if (string.Equals(expectedArgValue, levelTypeValue.Value, StringComparison.OrdinalIgnoreCase)) + { + if (param.Variable.Name.EndsWith("type", StringComparison.OrdinalIgnoreCase)) + { + // property level type + argCollectionFlags += 10; + } + else + { + // level/object name value + argCollectionFlags += 1; + } + } + else + { + onMismatch?.Invoke(param, expectedArgValue); + } + } + } + + return argCollectionFlags; + } + + protected virtual bool IsPropertyEditingProc(string procName) + { + return PropertyEditProcs.Contains(procName); + } + + protected abstract void ValidatePropertyEditingProcArgs(IList procParams, TSqlFragment call); + } +} diff --git a/TeamTools.TSQL.Linter/Routines/MainScriptObjectDetector.cs b/TeamTools.TSQL.Linter/Routines/MainScriptObjectDetector.cs index 5d0a9fdd..d812863b 100644 --- a/TeamTools.TSQL.Linter/Routines/MainScriptObjectDetector.cs +++ b/TeamTools.TSQL.Linter/Routines/MainScriptObjectDetector.cs @@ -13,6 +13,8 @@ public MainScriptObjectDetector() public string ObjectFullName { get; private set; } = ""; + public SchemaObjectName MainObjectFullName { get; private set; } + public TSqlBatch ObjectDefinitionBatch { get; private set; } = null; public TSqlStatement ObjectDefinitionNode { get; private set; } = null; @@ -68,6 +70,7 @@ protected void FullIdentifierDetected(SchemaObjectName node) } ObjectFullName = node.GetFullName(); + MainObjectFullName = node; detectedIdentifier = node; } diff --git a/TeamTools.TSQL.Linter/Routines/NetStandardExtensions.cs b/TeamTools.TSQL.Linter/Routines/NetStandardExtensions.cs index 04fef7c2..a86e75e1 100644 --- a/TeamTools.TSQL.Linter/Routines/NetStandardExtensions.cs +++ b/TeamTools.TSQL.Linter/Routines/NetStandardExtensions.cs @@ -37,6 +37,11 @@ public static bool Contains(this string str, char c) return str.IndexOf(c) >= 0; } + public static bool Contains(this string str, string substr, StringComparison comparison) + { + return str.IndexOf(substr, comparison) >= 0; + } + public static TVal GetValueOrDefault(this IDictionary dict, TKey key, TVal def) { if (dict.TryGetValue(key, out TVal value)) diff --git a/TeamTools.TSQL.Linter/Routines/ScriptDomExtensions/ScriptDomExtension.cs b/TeamTools.TSQL.Linter/Routines/ScriptDomExtensions/ScriptDomExtension.cs index ad4ad612..a9f3248f 100644 --- a/TeamTools.TSQL.Linter/Routines/ScriptDomExtensions/ScriptDomExtension.cs +++ b/TeamTools.TSQL.Linter/Routines/ScriptDomExtensions/ScriptDomExtension.cs @@ -121,6 +121,17 @@ public static string GetFullName(this DataTypeReference node) typeName); } + public static string GetFullName(this ColumnReferenceExpression col) + { + if (col.MultiPartIdentifier is null) + { + // e.g. $action + return default; + } + + return GetFullName(col.MultiPartIdentifier.Identifiers); + } + public static string GetFullName(this IList node, string delimiter = ".") { if (node.Count == 0) diff --git a/TeamTools.TSQL.Linter/Routines/TableDefinitionExtractor/TableDefinitionElementsEnumerator.cs b/TeamTools.TSQL.Linter/Routines/TableDefinitionExtractor/TableDefinitionElementsEnumerator.cs index 4e7a8301..94361c72 100644 --- a/TeamTools.TSQL.Linter/Routines/TableDefinitionExtractor/TableDefinitionElementsEnumerator.cs +++ b/TeamTools.TSQL.Linter/Routines/TableDefinitionExtractor/TableDefinitionElementsEnumerator.cs @@ -105,6 +105,14 @@ public IEnumerable Keys(string tableName = null) || e.ElementType == SqlTableElementType.PrimaryKey); } + // FK + public IEnumerable ForeignKeys(string tableName = null) + { + return GetFilteredTableElements( + GetTableNameEnumeration(tableName), + e => e.ElementType == SqlTableElementType.ForeignKey); + } + private IEnumerable GetTableNameEnumeration(string tableName) { if (!string.IsNullOrEmpty(tableName)) diff --git a/TeamTools.TSQL.Linter/Rules/Ambiguity/SelectNullTypeRule.cs b/TeamTools.TSQL.Linter/Rules/Ambiguity/SelectNullTypeRule.cs new file mode 100644 index 00000000..e4b40ba8 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Ambiguity/SelectNullTypeRule.cs @@ -0,0 +1,69 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("AM0891", "SELECT_NULL_TYPE")] + internal sealed class SelectNullTypeRule : AbstractRule + { + public SelectNullTypeRule() : base() + { + } + + public override void Visit(OutputClause node) => ValidateSelectedValues(node.SelectColumns); + + // No exception for SELECT-INTO because resulting table is affected by the ambiguity too. + // Not visiting QueryExpression abstraction because it catches too much. + // Also ignoring QueryDerivedTable visiting which may produce many false-positive detections. + public override void Visit(SelectStatement node) + { + if (node.QueryExpression.ForClause != null) + { + // FOR XML / JSON don't really need type for NULL + return; + } + + ValidateCte(node.WithCtesAndXmlNamespaces?.CommonTableExpressions); + ValidateQuery(node.QueryExpression); + } + + private void ValidateCte(IList ctes) + { + if (ctes is null) + { + return; + } + + for (int i = ctes.Count - 1; i >= 0; i--) + { + ValidateQuery(ctes[i].QueryExpression); + } + } + + private void ValidateQuery(QueryExpression q) + { + if (q.ForClause != null) + { + // FOR XML / JSON don't really need type for NULL + return; + } + + ValidateSelectedValues(q.GetQuerySpecification().SelectElements); + } + + private void ValidateSelectedValues(IList selectedElements) + { + for (int i = selectedElements.Count - 1; i >= 0; i--) + { + if (selectedElements[i] is SelectScalarExpression exp + && exp.Expression.ExtractScalarExpression() is NullLiteral selectNull) + { + HandleNodeError(selectNull); + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Ambiguity/TableAliasMimicksOtherTableRule.cs b/TeamTools.TSQL.Linter/Rules/Ambiguity/TableAliasMimicksOtherTableRule.cs index 5c7ca24f..30d34d2a 100644 --- a/TeamTools.TSQL.Linter/Rules/Ambiguity/TableAliasMimicksOtherTableRule.cs +++ b/TeamTools.TSQL.Linter/Rules/Ambiguity/TableAliasMimicksOtherTableRule.cs @@ -68,14 +68,16 @@ private void ValidateAlias(TSqlFragment node, IDictionary aliase return; } - // given alias mimicks any of registered table names - // or name is the same as any of registered aliases - if (aliases.ContainsKey(name) || aliases.ContainsKey(GetLastNamePart(name))) + if (aliases.TryGetValue(GetLastNamePart(name), out string otherName) + && !string.Equals(name, otherName, StringComparison.OrdinalIgnoreCase)) { + // The other alias mimicks this one name + // and this is not the same source name reference (e.g. in subquery) HandleNodeError(node, alias); } else if (ExistsInNames(aliases.Values, alias, aliasAsPartOfName)) { + // The alias mimicks one of registered table names HandleNodeError(node, alias); } } diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/CastXmlToStringRule.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/CastXmlToStringRule.cs new file mode 100644 index 00000000..9866e3b6 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/CastXmlToStringRule.cs @@ -0,0 +1,101 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("CS0892", "CAST_XML_TO_STRING")] + internal sealed class CastXmlToStringRule : AbstractRule + { + private static readonly string StuffFunction = "STUFF"; + + private static readonly HashSet TargetStringTypes = new HashSet(StringComparer.OrdinalIgnoreCase) + { + TSqlDomainAttributes.Types.Char, + TSqlDomainAttributes.Types.Varchar, + TSqlDomainAttributes.Types.NChar, + TSqlDomainAttributes.Types.NVarchar, + TSqlDomainAttributes.Types.SysName, + }; + + public CastXmlToStringRule() : base() + { + } + + public override void Visit(CastCall node) => ValidateExpression(node.Parameter, node.DataType); + + public override void Visit(ConvertCall node) => ValidateExpression(node.Parameter, node.DataType); + + // STUFF(... FOR XML PATH('')), 1, 2, '') is a common solution for string concat + public override void Visit(FunctionCall node) + { + if (!(node.FunctionName.Value.Equals(StuffFunction, StringComparison.OrdinalIgnoreCase) + && node.Parameters.Count == 4)) + { + return; + } + + ValidateExpression(node.Parameters[0], null, TSqlDomainAttributes.Types.Varchar); + } + + private static TSqlFragment ExtractForXmlQuery(ScalarExpression src) + { + while (src is ParenthesisExpression pe) + { + src = pe.Expression; + } + + if (!(src is ScalarSubquery q && q.QueryExpression.ForClause is XmlForClause forXml)) + { + return default; + } + + for (int i = forXml.Options.Count - 1; i >= 0; i--) + { + var opt = forXml.Options[i]; + if (opt.OptionKind == XmlForClauseOptions.BinaryBase64) + { + return default; + } + else if (opt.OptionKind == XmlForClauseOptions.Auto) + { + return default; + } + else if (opt.OptionKind == XmlForClauseOptions.Path + && opt.Value is StringLiteral s && string.IsNullOrEmpty(s.Value)) + { + // FOR XML PATH + return forXml; + } + } + + return default; + } + + private void ValidateExpression(ScalarExpression src, DataTypeReference targetType, string targetTypeName = null) + { + var forXml = ExtractForXmlQuery(src); + + if (forXml is null) + { + // source is not SELECT FOR XML PATH + return; + } + + if (string.IsNullOrEmpty(targetTypeName)) + { + targetTypeName = targetType.GetFullName(); + } + + if (!TargetStringTypes.Contains(targetTypeName)) + { + // not a cast to string + return; + } + + HandleNodeError(forXml); + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/ConstraintNameInTempTableRule.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/ConstraintNameInTempTableRule.cs index 411f1246..52cf55d1 100644 --- a/TeamTools.TSQL.Linter/Rules/CodeSmell/ConstraintNameInTempTableRule.cs +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/ConstraintNameInTempTableRule.cs @@ -1,4 +1,5 @@ using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; using TeamTools.Common.Linting; using TeamTools.TSQL.Linter.Routines; @@ -7,39 +8,51 @@ namespace TeamTools.TSQL.Linter.Rules [RuleIdentity("CS0107", "TMP_NAMED_CONSTRAINT")] internal sealed class ConstraintNameInTempTableRule : AbstractRule { + private readonly ConstraintVisitor constraintVisitor; + public ConstraintNameInTempTableRule() : base() { + constraintVisitor = new ConstraintVisitor(ViolationHandler); } public override void Visit(CreateTableStatement node) { // only apply rule to temp tables - if (!node.SchemaObjectName.BaseIdentifier.Value.StartsWith(TSqlDomainAttributes.TempTablePrefix)) + if (!IsTempTable(node.SchemaObjectName)) { return; } - var constraintVisitor = new ConstraintVisitor(); - node.AcceptChildren(constraintVisitor); + node.Definition.AcceptChildren(constraintVisitor); + } - if (constraintVisitor.NamedConstraintExists) + public override void Visit(AlterTableAddTableElementStatement node) + { + // only apply rule to temp tables + if (!IsTempTable(node.SchemaObjectName)) { - HandleNodeError(node); + return; } + + node.Definition.AcceptChildren(constraintVisitor); } - private sealed class ConstraintVisitor : TSqlFragmentVisitor + private static bool IsTempTable(SchemaObjectName name) { - public bool NamedConstraintExists { get; private set; } + return name.BaseIdentifier.Value.StartsWith(TSqlDomainAttributes.TempTablePrefix); + } + + private sealed class ConstraintVisitor : VisitorWithCallback + { + public ConstraintVisitor(Action callback) : base(callback) + { } public override void Visit(ConstraintDefinition node) { - if (NamedConstraintExists) + if (node.ConstraintIdentifier != null) { - return; + Callback(node); } - - NamedConstraintExists = node.ConstraintIdentifier != null; } } } diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/ExtendedPropertyAddressesMissingColumnRule.Visitor.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/ExtendedPropertyAddressesMissingColumnRule.Visitor.cs new file mode 100644 index 00000000..aab7fec6 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/ExtendedPropertyAddressesMissingColumnRule.Visitor.cs @@ -0,0 +1,64 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; +using TeamTools.TSQL.Linter.Routines.TableDefinitionExtractor; + +namespace TeamTools.TSQL.Linter.Rules +{ + internal partial class ExtendedPropertyAddressesMissingColumnRule + { + private sealed class ExtendedPropertyVisitor : ExtendedPropertyEditingVisitor + { + private readonly Dictionary argValueMap; + private readonly IDictionary validColumns; + + public ExtendedPropertyVisitor( + SchemaObjectName expectedTarget, + IDictionary validColumns, + Action callback) : base(callback) + { + this.validColumns = validColumns ?? throw new ArgumentNullException(nameof(validColumns)); + + string expectedObjectName = expectedTarget.BaseIdentifier.Value ?? throw new ArgumentNullException(nameof(expectedTarget)); + string expectedSchemaName = expectedTarget.SchemaIdentifier?.Value ?? TSqlDomainAttributes.DefaultSchemaName; + + argValueMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "@level0type", "SCHEMA" }, + { "@level0name", expectedSchemaName }, + { "@level1type", "TABLE" }, + { "@level1name", expectedObjectName }, + { "@level2type", "COLUMN" }, + { "@level2name", "" }, + }; + } + + protected override void ValidatePropertyEditingProcArgs(IList procParams, TSqlFragment call) + { + StringLiteral columnMentionNode = null; + + int argCollectionFlags = MatchArgs( + argValueMap, + procParams, + (param, expectedArgValue) => + { + if (string.Equals(expectedArgValue, "") + && param.ParameterValue is StringLiteral levelTypeValue) + { + columnMentionNode = levelTypeValue; + } + }); + + // 32 - SCHEMA and TABLE names matched, COLUMN name provided + if (argCollectionFlags == 32 + && columnMentionNode != null + && !validColumns.ContainsKey(columnMentionNode.Value)) + { + Callback(columnMentionNode, columnMentionNode.Value); + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/ExtendedPropertyAddressesMissingColumnRule.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/ExtendedPropertyAddressesMissingColumnRule.cs new file mode 100644 index 00000000..2f1c3243 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/ExtendedPropertyAddressesMissingColumnRule.cs @@ -0,0 +1,41 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("CS0887", "EXTENDED_PROPERTY_FOR_MISSING_COL")] + internal sealed partial class ExtendedPropertyAddressesMissingColumnRule : ScriptAnalysisServiceConsumingRule + { + public ExtendedPropertyAddressesMissingColumnRule() : base() + { + } + + protected override void ValidateScript(TSqlScript node) + { + if (node.Batches.Count < 2) + { + // 2 batches needed to create table and add extended property + return; + } + + var mainObject = GetService(node); + if (string.IsNullOrWhiteSpace(mainObject?.ObjectFullName) + || !(mainObject.ObjectDefinitionNode is CreateTableStatement)) + { + // Not a CREATE TABLE script + return; + } + + var tableElements = GetService(node); + + if (!tableElements.Tables.TryGetValue(mainObject.ObjectFullName, out var tblDef)) + { + // Something went wrong + return; + } + + node.AcceptChildren(new ExtendedPropertyVisitor(mainObject.MainObjectFullName, tblDef.Columns, ViolationHandlerWithMessage)); + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/ExtendedPropertyDupRule.Visitor.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/ExtendedPropertyDupRule.Visitor.cs new file mode 100644 index 00000000..828e47f5 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/ExtendedPropertyDupRule.Visitor.cs @@ -0,0 +1,87 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using System.Text; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + internal partial class ExtendedPropertyDupRule + { + private sealed class ExtendedPropertyDupDetector : ExtendedPropertyEditingVisitor + { + private static readonly string AddPropertyProc = "sp_addextendedproperty"; + + // It is supposed to store all added properties across multiple sp_addextendedproperty calls + // so it can catch dups. + private readonly HashSet addedProperties = new HashSet(StringComparer.OrdinalIgnoreCase); + + public ExtendedPropertyDupDetector(StringBuilder stringBuilder, Action callback) + : base(callback) + { + PropertyNameBuilder = stringBuilder ?? throw new ArgumentNullException(nameof(stringBuilder)); + } + + private StringBuilder PropertyNameBuilder { get; } + + protected override bool IsPropertyEditingProc(string procName) + { + return string.Equals(procName, AddPropertyProc, StringComparison.OrdinalIgnoreCase); + } + + // Only named EXEC parameter provisioning is supported. + // There is a separate rule for preventing passing arguments by position. + protected override void ValidatePropertyEditingProcArgs(IList procParams, TSqlFragment call) + { + // all args except @value + // a little reordering by ASC sorting is fine + var argValueMap = new SortedDictionary(StringComparer.OrdinalIgnoreCase) + { + { "@level0type", null }, + { "@level0name", null }, + { "@level1type", null }, + { "@level1name", null }, + { "@level2type", null }, + { "@level2name", null }, + { "@name", null }, + }; + + for (int i = procParams.Count - 1; i >= 0; i--) + { + var param = procParams[i]; + if (param.Variable != null && argValueMap.ContainsKey(param.Variable.Name)) + { + if (param.ParameterValue is StringLiteral s) + { + argValueMap[param.Variable.Name] = s.Value; + } + } + } + + if (string.IsNullOrEmpty(argValueMap["@name"])) + { + // property name not detected + return; + } + + foreach (var arg in argValueMap) + { + PropertyNameBuilder + .Append(arg.Key) + .Append('=') + .Append(arg.Value) + .Append(';'); + } + + string propertyFullPath = PropertyNameBuilder.ToString(); + PropertyNameBuilder.Length = 0; // cleanup before next proc call analysis + + if (!addedProperties.Add(propertyFullPath)) + { + Callback(call, argValueMap["@name"]); + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/ExtendedPropertyDupRule.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/ExtendedPropertyDupRule.cs new file mode 100644 index 00000000..8e8a6170 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/ExtendedPropertyDupRule.cs @@ -0,0 +1,36 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("CS0888", "EXTENDED_PROPERTY_DUP")] + internal sealed partial class ExtendedPropertyDupRule : AbstractRule + { + public ExtendedPropertyDupRule() : base() + { + } + + protected override void ValidateScript(TSqlScript node) + { + var sb = ObjectPools.StringBuilderPool.Get(); + + var detector = new ExtendedPropertyDupDetector(sb, ViolationHandlerWithMessage); + + for (int i = 0, n = node.Batches.Count; i < n; i++) + { + var batch = node.Batches[i]; + for (int j = 0, m = batch.Statements.Count; j < m; j++) + { + var stmt = batch.Statements[j]; + if (stmt is ExecuteStatement exec) + { + exec.AcceptChildren(detector); + } + } + } + + ObjectPools.StringBuilderPool.Return(sb); + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/ExtendedPropertyMisdirectedRule.Visitor.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/ExtendedPropertyMisdirectedRule.Visitor.cs new file mode 100644 index 00000000..b233d5d8 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/ExtendedPropertyMisdirectedRule.Visitor.cs @@ -0,0 +1,58 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + internal partial class ExtendedPropertyMisdirectedRule + { + private sealed class ExtendedPropertyVisitor : ExtendedPropertyEditingVisitor + { + private readonly Dictionary argValueMap; + + public ExtendedPropertyVisitor( + SchemaObjectName expectedTarget, + string targetObjectType, + Action callback) : base(callback) + { + if (string.IsNullOrEmpty(targetObjectType)) + { + throw new ArgumentNullException(nameof(targetObjectType)); + } + + string expectedObjectName = expectedTarget.BaseIdentifier.Value ?? throw new ArgumentNullException(nameof(expectedTarget)); + string expectedSchemaName = expectedTarget.SchemaIdentifier?.Value ?? TSqlDomainAttributes.DefaultSchemaName; + + argValueMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "@level0type", "SCHEMA" }, + { "@level0name", expectedSchemaName }, + { "@level1type", targetObjectType }, + { "@level1name", expectedObjectName }, + }; + } + + protected override void ValidatePropertyEditingProcArgs(IList procParams, TSqlFragment call) + { + TSqlFragment lastMismatch = null; + + int argCollectionFlags = MatchArgs( + argValueMap, + procParams, + (param, _) => lastMismatch = param); + + // argCollectionFlags >= 20 - both SCHEMA and object type (e.g. TABLE) level filters were provided + // argCollectionFlags != 22 - either schema or object name has wrong value + // argCollectionFlags < 20 - extended property points to something completely unexpected + if (argCollectionFlags < 22) + { + // If lastMismatch is null then the whole call is either broken + // or relates to something completely unexpected + Callback(lastMismatch ?? call, default); + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/ExtendedPropertyMisdirectedRule.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/ExtendedPropertyMisdirectedRule.cs new file mode 100644 index 00000000..98d6222d --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/ExtendedPropertyMisdirectedRule.cs @@ -0,0 +1,63 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("CS0886", "EXTENDED_PROPERTY_MISDIRECTED")] + internal sealed partial class ExtendedPropertyMisdirectedRule : ScriptAnalysisServiceConsumingRule + { + public ExtendedPropertyMisdirectedRule() : base() + { + } + + protected override void ValidateScript(TSqlScript node) + { + if (node.Batches.Count < 2) + { + // CREATE [TABLE] only + return; + } + + var mainObject = GetService(node); + if (mainObject?.MainObjectFullName is null) + { + // no main object could be determined + return; + } + + string mainObjectType = GetMainObjectType(mainObject.ObjectDefinitionNode); + + if (string.IsNullOrEmpty(mainObjectType)) + { + // unsupported object type + return; + } + + node.AcceptChildren(new ExtendedPropertyVisitor(mainObject.MainObjectFullName, mainObjectType, ViolationHandlerWithMessage)); + } + + // TODO : support more object types? + private static string GetMainObjectType(TSqlFragment node) + { + if (node is CreateTableStatement) + { + return "TABLE"; + } + + if (node is CreateTypeTableStatement) + { + // There is also 'TABLE_TYPE' but it does not seem to work as expected + return "TYPE"; + } + + // Keep after CreateTypeTableStatement because it is a descendant of CreateTypeStatement + if (node is CreateTypeStatement) + { + return "TYPE"; + } + + return default; + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/GeneralSetOptionRule.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/GeneralSetOptionRule.cs index c5423701..78f98695 100644 --- a/TeamTools.TSQL.Linter/Rules/CodeSmell/GeneralSetOptionRule.cs +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/GeneralSetOptionRule.cs @@ -1,4 +1,6 @@ using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; using TeamTools.Common.Linting; namespace TeamTools.TSQL.Linter.Rules @@ -6,6 +8,17 @@ namespace TeamTools.TSQL.Linter.Rules [RuleIdentity("CS0747", "GENERAL_SET_CONTROL")] internal sealed class GeneralSetOptionRule : AbstractRule { + private static readonly Dictionary SetOptionNames = new Dictionary + { + { GeneralSetCommandType.ContextInfo, "CONTEXT_INFO" }, + { GeneralSetCommandType.DateFormat, "DATEFORMAT" }, + { GeneralSetCommandType.DateFirst, "DATEFIRST" }, + { GeneralSetCommandType.DeadlockPriority, "DEADLOCK_PRIORITY" }, + { GeneralSetCommandType.Language, "LANGUAGE" }, + { GeneralSetCommandType.LockTimeout, "LOCK_TIMEOUT" }, + { GeneralSetCommandType.QueryGovernorCostLimit, "QUERY_GOVERNOR_COST_LIMIT" }, + }; + public GeneralSetOptionRule() : base() { } @@ -15,7 +28,7 @@ public override void Visit(GeneralSetCommand node) // Ignored command types are handled by separate rules if (!IsIgnorable(node.CommandType)) { - HandleNodeError(node); + HandleNodeError(node, SetOptionNames[node.CommandType]); } } diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/IsolationLevelChaosPerQueryRule.Visitor.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/IsolationLevelChaosPerQueryRule.Visitor.cs new file mode 100644 index 00000000..2e799bcb --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/IsolationLevelChaosPerQueryRule.Visitor.cs @@ -0,0 +1,102 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; + +namespace TeamTools.TSQL.Linter.Rules +{ + internal partial class IsolationLevelChaosPerQueryRule + { + private sealed class IsolationLevelHintVisitor : TSqlFragmentVisitor + { + private static readonly Dictionary HintWeights = new Dictionary + { + { TableHintKind.NoLock, 1 }, + { TableHintKind.ReadUncommitted, 1 }, + { TableHintKind.ReadCommitted, 2 }, + { TableHintKind.RepeatableRead, 3 }, + { TableHintKind.HoldLock, 4 }, + { TableHintKind.Serializable, 4 }, + { TableHintKind.TabLockX, 4 }, + { TableHintKind.UpdLock, 3 }, + { TableHintKind.ReadPast, 2 }, + }; + + private static readonly Dictionary HintNames = new Dictionary + { + { TableHintKind.NoLock, "NOLOCK" }, + { TableHintKind.ReadUncommitted, "READUNCOMMITTED" }, + { TableHintKind.ReadCommitted, "READCOMMITTED" }, + { TableHintKind.RepeatableRead, "REPEATABLEREAD" }, + { TableHintKind.HoldLock, "HOLDLOCK" }, + { TableHintKind.Serializable, "SERIALIZABLE" }, + { TableHintKind.TabLockX, "TABLOCKX" }, + { TableHintKind.UpdLock, "UPDLOCK" }, + { TableHintKind.ReadPast, "READPAST" }, + }; + + private TableHintKind priorIsolationLevelMin = TableHintKind.None; + private int priorIsolationLevelMinWeight = 0; + + private TableHintKind priorIsolationLevelMax = TableHintKind.None; + private int priorIsolationLevelMaxWeight = 0; + + public IsolationLevelHintVisitor(Action callback) + { + Callback = callback; + } + + private Action Callback { get; } + + public override void Visit(TableHint node) + { + SetNextIsolationLevel(node); + } + + private void SetNextIsolationLevel(TableHint node) + { + var nextIsolationLevel = node.HintKind; + + if (!HintWeights.TryGetValue(nextIsolationLevel, out int nextIsolationLevelWeight)) + { + // something unsupported + return; + } + + if (priorIsolationLevelMinWeight == 0) + { + // the very first hint in a query + priorIsolationLevelMin = nextIsolationLevel; + priorIsolationLevelMinWeight = nextIsolationLevelWeight; + priorIsolationLevelMax = nextIsolationLevel; + priorIsolationLevelMaxWeight = nextIsolationLevelWeight; + + return; + } + + if (Math.Abs(nextIsolationLevelWeight - priorIsolationLevelMinWeight) > 1) + { + // too big difference + Callback(node, $"{HintNames[nextIsolationLevel]} vs {HintNames[priorIsolationLevelMin]}"); + } + else if (Math.Abs(nextIsolationLevelWeight - priorIsolationLevelMaxWeight) > 1) + { + // too big difference + Callback(node, $"{HintNames[nextIsolationLevel]} vs {HintNames[priorIsolationLevelMax]}"); + } + + // FIXME : actually this allows moderate downscaling from SERIALIZABLE + // to NOCLOCK in a single query without getting violation. + if (nextIsolationLevelWeight > priorIsolationLevelMaxWeight) + { + priorIsolationLevelMax = nextIsolationLevel; + priorIsolationLevelMaxWeight = nextIsolationLevelWeight; + } + else if (nextIsolationLevelWeight < priorIsolationLevelMinWeight) + { + priorIsolationLevelMin = nextIsolationLevel; + priorIsolationLevelMinWeight = nextIsolationLevelWeight; + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/IsolationLevelChaosPerQueryRule.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/IsolationLevelChaosPerQueryRule.cs new file mode 100644 index 00000000..36fbc29f --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/IsolationLevelChaosPerQueryRule.cs @@ -0,0 +1,18 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using TeamTools.Common.Linting; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("CS0894", "ISOLATION_CHAOS_PER_QUERY")] + internal sealed partial class IsolationLevelChaosPerQueryRule : AbstractRule + { + public IsolationLevelChaosPerQueryRule() : base() + { + } + + public override void Visit(StatementWithCtesAndXmlNamespaces node) + { + node.AcceptChildren(new IsolationLevelHintVisitor(ViolationHandlerWithMessage)); + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/IsolationLevelContradictionRule.Visitor.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/IsolationLevelContradictionRule.Visitor.cs new file mode 100644 index 00000000..0f015893 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/IsolationLevelContradictionRule.Visitor.cs @@ -0,0 +1,95 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; + +namespace TeamTools.TSQL.Linter.Rules +{ + internal partial class IsolationLevelContradictionRule + { + private sealed class IsolationLevelSwitchVisitor : TSqlFragmentVisitor + { + private static readonly Dictionary HintWeights = new Dictionary + { + { TableHintKind.NoLock, 1 }, + { TableHintKind.ReadUncommitted, 1 }, + { TableHintKind.ReadCommitted, 2 }, + { TableHintKind.RepeatableRead, 3 }, + { TableHintKind.HoldLock, 4 }, + { TableHintKind.Serializable, 4 }, + { TableHintKind.TabLockX, 3 }, + { TableHintKind.UpdLock, 3 }, + { TableHintKind.ReadPast, 2 }, + }; + + private static readonly Dictionary IsolationWeights = new Dictionary + { + { IsolationLevel.ReadUncommitted, 1 }, + { IsolationLevel.ReadCommitted, 2 }, + { IsolationLevel.RepeatableRead, 3 }, + { IsolationLevel.Serializable, 4 }, + }; + + private static readonly Dictionary HintNames = new Dictionary + { + { TableHintKind.NoLock, "NOLOCK" }, + { TableHintKind.ReadUncommitted, "READUNCOMMITTED" }, + { TableHintKind.ReadCommitted, "READCOMMITTED" }, + { TableHintKind.RepeatableRead, "REPEATABLEREAD" }, + { TableHintKind.HoldLock, "HOLDLOCK" }, + { TableHintKind.Serializable, "SERIALIZABLE" }, + { TableHintKind.TabLockX, "TABLOCKX" }, + { TableHintKind.UpdLock, "UPDLOCK" }, + { TableHintKind.ReadPast, "READPAST" }, + }; + + private static readonly Dictionary IsolationNames = new Dictionary + { + { IsolationLevel.ReadUncommitted, "READ UNCOMMITTED" }, + { IsolationLevel.ReadCommitted, "READ COMMITTED" }, + { IsolationLevel.RepeatableRead, "REPEATABLE READ" }, + { IsolationLevel.Serializable, "SERIALIZABLE" }, + }; + + public IsolationLevelSwitchVisitor(Action callback) + { + Callback = callback; + } + + private Action Callback { get; } + + private IsolationLevel DefinedIsolationLevel { get; set; } = IsolationLevel.None; + + public override void Visit(SetTransactionIsolationLevelStatement node) + { + DefinedIsolationLevel = node.Level; + } + + public override void Visit(TableHint node) + { + OverrideIsolationLevel(node); + } + + private void OverrideIsolationLevel(TableHint hint) + { + if (DefinedIsolationLevel == IsolationLevel.None) + { + // no directive was found yet + return; + } + + if (!HintWeights.TryGetValue(hint.HintKind, out int overridenIsolationLevel) + || !IsolationWeights.TryGetValue(DefinedIsolationLevel, out int definedIsolationWeight)) + { + // something unsupported + return; + } + + if (Math.Abs(overridenIsolationLevel - definedIsolationWeight) > 1) + { + // too big difference + Callback(hint, $"{HintNames[hint.HintKind]} vs {IsolationNames[DefinedIsolationLevel]}"); + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/IsolationLevelContradictionRule.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/IsolationLevelContradictionRule.cs new file mode 100644 index 00000000..3ba4b289 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/IsolationLevelContradictionRule.cs @@ -0,0 +1,37 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using TeamTools.Common.Linting; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("CS0893", "ISOLATION_CONTRADICTION")] + internal sealed partial class IsolationLevelContradictionRule : AbstractRule + { + public IsolationLevelContradictionRule() : base() + { + } + + protected override void ValidateBatch(TSqlBatch batch) + { + // CREATE PROC/TRIGGER/FUNC must be the first statement in a batch + var firstStmt = batch.Statements[0]; + if (firstStmt is ProcedureStatementBody proc) + { + proc.StatementList?.Accept(MakeVisitor()); + } + else if (firstStmt is TriggerStatementBody trg) + { + trg.StatementList?.Accept(MakeVisitor()); + } + else + { + // no proc/trigger/func - validating this ad-hoc batch + batch.AcceptChildren(MakeVisitor()); + } + } + + private TSqlFragmentVisitor MakeVisitor() + { + return new IsolationLevelSwitchVisitor(ViolationHandlerWithMessage); + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/LiteralContainsLookAlikeCharRule.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/LiteralContainsLookAlikeCharRule.cs index dbc9ec96..0a4ab0ca 100644 --- a/TeamTools.TSQL.Linter/Rules/CodeSmell/LiteralContainsLookAlikeCharRule.cs +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/LiteralContainsLookAlikeCharRule.cs @@ -14,5 +14,22 @@ public LiteralContainsLookAlikeCharRule() : base() // TODO : utilize ExpressionEvaluator here? public override void Visit(StringLiteral node) => LookAlikeCharDetector.ValidateChars(node.Value, node.StartLine, node.StartColumn, ViolationHandlerPerLine); + + public override void ExplicitVisit(LikePredicate node) + { + if (!(node.SecondExpression is StringLiteral s)) + { + return; + } + + // TODO : Check other parts of the pattern except the [] contents. + // Pay attention to violation positioning after string adaptation. + if (s.Value.Contains("[")) + { + return; + } + + s.Accept(this); + } } } diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/NonCorrelatedJoinPredicateRule.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/NonCorrelatedJoinPredicateRule.cs index 4c2ef796..cd74662b 100644 --- a/TeamTools.TSQL.Linter/Rules/CodeSmell/NonCorrelatedJoinPredicateRule.cs +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/NonCorrelatedJoinPredicateRule.cs @@ -108,7 +108,13 @@ private static bool IsJoinPredicateCorrelated(BooleanExpression predicate, IColl if (predicate is BooleanTernaryExpression between) { return CorrelatedColumnReferenceDetector.HasCorrelatedRefs(between.FirstExpression, between.SecondExpression, leftNames, rightNames) - && CorrelatedColumnReferenceDetector.HasCorrelatedRefs(between.FirstExpression, between.ThirdExpression, leftNames, rightNames); + || CorrelatedColumnReferenceDetector.HasCorrelatedRefs(between.FirstExpression, between.ThirdExpression, leftNames, rightNames); + } + + if (predicate is InPredicate inValues && inValues.Values != null) + { + return CorrelatedColumnReferenceDetector.HasCorrelatedRefs(inValues.Expression, inValues.Values[0], leftNames, rightNames) + || CorrelatedColumnReferenceDetector.HasCorrelatedRefs(inValues.Expression, inValues.Values[inValues.Values.Count - 1], leftNames, rightNames); } // IS [NOT] NULL and such cannot be correlated diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/ObjectPropertyFiddlingRule.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/ObjectPropertyFiddlingRule.cs new file mode 100644 index 00000000..ead36163 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/ObjectPropertyFiddlingRule.cs @@ -0,0 +1,40 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("CS0890", "OBJECT_PROPERTY_FIDDLING")] + internal sealed class ObjectPropertyFiddlingRule : AbstractRule + { + private static readonly HashSet GetPropertyFunctions = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "OBJECTPROPERTY", + "OBJECTPROPERTYEX", + "COLUMNPROPERTY", + "DATABASEPROPERTYEX", + "FILEGROUPPROPERTY", + "FILEPROPERTY", + "FILEPROPERTYEX", + "FULLTEXTCATALOGPROPERTY", + "FULLTEXTSERVICEPROPERTY", + "INDEXKEY_PROPERTY", + "SERVERPROPERTY", + "TYPEPROPERTY", + }; + + public ObjectPropertyFiddlingRule() : base() + { + } + + public override void Visit(FunctionCall node) + { + string funcName = node.FunctionName.Value; + if (GetPropertyFunctions.Contains(funcName)) + { + HandleNodeError(node, funcName); + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/RecursiveCteMaxRecursionRule.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/RecursiveCteMaxRecursionRule.cs index 1ec35a6e..31dda8ca 100644 --- a/TeamTools.TSQL.Linter/Rules/CodeSmell/RecursiveCteMaxRecursionRule.cs +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/RecursiveCteMaxRecursionRule.cs @@ -38,7 +38,7 @@ private static bool IsRecursive(CommonTableExpression cte) private void ValidateCtes(IList ctes) { - for (int i = 0, n = ctes.Count; i < n; i++) + for (int i = ctes.Count - 1; i >= 0; i--) { var cte = ctes[i]; if (IsRecursive(cte)) @@ -49,7 +49,7 @@ private void ValidateCtes(IList ctes) } } - private class SelfReferenceVisitor : TSqlViolationDetector + private sealed class SelfReferenceVisitor : TSqlViolationDetector { private readonly string selfName; diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/SelfCallRule.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/SelfCallRule.cs new file mode 100644 index 00000000..11df0888 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/SelfCallRule.cs @@ -0,0 +1,131 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("CS0856", "SELF_CALL")] + internal sealed class SelfCallRule : AbstractRule + { + public SelfCallRule() : base() + { + } + + public override void ExplicitVisit(CreateFunctionStatement node) + { + string funcName = node.Name.GetFullName(); + + if (node.ReturnType is SelectFunctionReturnType sel) + { + // inline table function has no body + DetectSelfCall(sel.SelectStatement, funcName); + return; + } + + DetectSelfCall(node.StatementList, funcName); + } + + public override void ExplicitVisit(CreateProcedureStatement node) + { + string procName = node.ProcedureReference.Name.GetFullName(); + if (node.ProcedureReference.Number != null) + { + // If proc is numbered then the fully qualified reference must use the same number. + // There is a separate rule to prevent use of numbered procs which is a deprecated feature. + procName += TSqlDomainAttributes.NamePartSeparator + node.ProcedureReference.Number.Value; + } + + DetectSelfCall(node.StatementList, procName); + } + + private void DetectSelfCall(SelectStatement body, string selfName) => DoValidate(body, selfName); + + private void DetectSelfCall(StatementList body, string selfName) + { + if ((body?.Statements?.Count ?? 0) == 0) + { + // Empty body or this is an external module + return; + } + + if (string.IsNullOrEmpty(selfName)) + { + return; + } + + DoValidate(body, selfName); + } + + private void DoValidate(TSqlFragment node, string selfName) + { + node.Accept(new SelfCallDetector(selfName, ViolationHandlerWithMessage)); + } + + private sealed class SelfCallDetector : TSqlFragmentVisitor + { + private readonly string selfName; + private readonly Action callback; + + public SelfCallDetector(string selfName, Action callback) + { + this.selfName = selfName; + this.callback = callback; + } + + public override void Visit(ExecutableProcedureReference node) + { + var procRef = node.ProcedureReference.ProcedureReference; + if (procRef is null) + { + // EXEC @proc + return; + } + + string procName = procRef.Name.GetFullName(); + if (procRef.Number != null) + { + // Numbered sp call + procName += TSqlDomainAttributes.NamePartSeparator + procRef.Number.Value; + } + + Validate(node, procName); + } + + public override void Visit(SchemaObjectFunctionTableReference node) + { + // SELECT from a table-valued function + Validate(node.SchemaObject, node.SchemaObject.GetFullName()); + } + + public override void Visit(FunctionCall node) + { + string funcName = node.FunctionName.Value; + if (node.CallTarget != null) + { + if (node.CallTarget is MultiPartIdentifierCallTarget id) + { + // Full function name is usually divided into CallTarget (schema) + Name (base name). + // However it may be something else e.g. 4-part fully qualified reference or CLR method call. + funcName = id.MultiPartIdentifier.Identifiers.GetFullName() + TSqlDomainAttributes.NamePartSeparator + funcName; + } + else + { + // XML method call or something alike - not our case + return; + } + } + + Validate(node, funcName); + } + + private void Validate(TSqlFragment node, string calledName) + { + if (string.Equals(calledName, selfName, StringComparison.OrdinalIgnoreCase)) + { + callback(node, calledName); + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/SystemProcedureReturnCodeCheckRule.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/SystemProcedureReturnCodeCheckRule.cs index 559c659a..d14f76e6 100644 --- a/TeamTools.TSQL.Linter/Rules/CodeSmell/SystemProcedureReturnCodeCheckRule.cs +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/SystemProcedureReturnCodeCheckRule.cs @@ -9,14 +9,96 @@ namespace TeamTools.TSQL.Linter.Rules [RuleIdentity("CS0521", "SYSPROC_RETURN_NOT_CHECKED")] internal sealed class SystemProcedureReturnCodeCheckRule : AbstractRule { + // TODO : The list is quite long for today. Could it be reversed to avoid false-positive detections? + // TODO : Consolidate all the metadata in resource file + // Some of them don't return success code, + // some are considered "write only" procs - return code can be ignored. private static readonly HashSet IgnoredSystemProcs = new HashSet(StringComparer.OrdinalIgnoreCase) { "sp_addextendedproperty", + "sp_updateextendedproperty", + "sp_dropextendedproperty", "sp_executesql", "sp_bindefault", "sp_unbindefault", "sp_bindrule", "sp_unbindrule", + + // SQLDMO + "sp_OADestroy", + "sp_OAStop", + "sp_OASetProperty", + "sp_OAMethod", + "sp_OAGetProperty", + "sp_OAGetErrorInfo", + + // job management + "sp_add_alert", + "sp_add_category", + "sp_add_job", + "sp_add_jobschedule", + "sp_add_jobserver", + "sp_add_jobstep", + "sp_add_notification", + "sp_add_operator", + "sp_add_proxy", + "sp_add_schedule", + "sp_add_targetservergroup", + "sp_add_targetsvrgrp_member", + + "sp_delete_alert", + "sp_delete_category", + "sp_delete_job", + "sp_delete_jobschedule", + "sp_delete_jobserver", + "sp_delete_jobstep", + "sp_delete_notification", + "sp_delete_operator", + "sp_delete_proxy", + "sp_delete_schedule", + "sp_delete_targetservergroup", + "sp_delete_targetsvrgrp_member", + + "sp_update_alert", + "sp_update_category", + "sp_update_job", + "sp_update_jobschedule", + "sp_update_jobserver", + "sp_update_jobstep", + "sp_update_notification", + "sp_update_operator", + "sp_update_proxy", + "sp_update_schedule", + "sp_update_targetservergroup", + + "sp_apply_job_to_targets", + "sp_remove_job_from_targets", + "sp_attach_schedule", + "sp_detach_schedule", + "sp_manage_jobs_by_login", + "sp_notify_operator", + "sp_start_job", + "sp_stop_job", + "sp_purge_jobhistory", + "sp_readerrorlog", + "xp_dirtree", + + "sp_column_privileges", + "sp_columns", + "sp_databases", + "sp_fkeys", + "sp_pkeys", + "sp_server_info", + "sp_special_columns", + "sp_sproc_columns", + "sp_statistics", + "sp_stored_procedures", + "sp_table_privileges", + "sp_tables", + + "sp_who", + "sp_help", + "sp_helptext", }; public SystemProcedureReturnCodeCheckRule() : base() @@ -56,7 +138,7 @@ public override void Visit(ExecuteSpecification node) return; } - HandleNodeError(node); + HandleNodeError(node, procName); } } } diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/UselessUnitRule.Visitor.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/UselessUnitRule.Visitor.cs new file mode 100644 index 00000000..24274921 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/UselessUnitRule.Visitor.cs @@ -0,0 +1,126 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System.Diagnostics.CodeAnalysis; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + internal partial class UselessUnitRule + { + // No need in examples for each and every statement kind + [ExcludeFromCodeCoverage] + private sealed class BodyVisitor : TSqlFragmentVisitor + { + private const int MinFunctionCalls = 2; + private int funcCallCount = 0; + + public BodyVisitor() + { } + + public bool ContainsMeaningfulStatement { get; private set; } + + public override void Visit(TSqlFragment node) + { + } + + // Generic visitor for all kind of statements except + // excluded by ExplicitVisit + public override void Visit(TSqlStatement node) + { + ContainsMeaningfulStatement = true; + } + + public override void Visit(ScalarSubquery node) + { + ContainsMeaningfulStatement = true; + } + + // Let's say that CASE is complex enough + public override void Visit(SearchedCaseExpression node) + { + if (ContainsMeaningfulStatement) + { + return; + } + + ContainsMeaningfulStatement = node.WhenClauses.Count > 1; + } + + public override void Visit(SimpleCaseExpression node) + { + if (ContainsMeaningfulStatement) + { + return; + } + + ContainsMeaningfulStatement = node.WhenClauses.Count > 1; + } + + // Complex expressions + public override void Visit(IIfCall node) + { + ContainsMeaningfulStatement = true; + } + + public override void Visit(BooleanBinaryExpression node) + { + ContainsMeaningfulStatement = true; + } + + public override void Visit(FunctionCall node) + { + if (ContainsMeaningfulStatement) + { + return; + } + + ContainsMeaningfulStatement = ++funcCallCount >= MinFunctionCalls; + } + + // Ignoring code blocks themselves but going deeper + public override void ExplicitVisit(TryCatchStatement node) => node.AcceptChildren(this); + + public override void ExplicitVisit(BeginEndBlockStatement node) => node.AcceptChildren(this); + + public override void ExplicitVisit(WhileStatement node) => node.AcceptChildren(this); + + // Declare may contain scalar subqueries + public override void ExplicitVisit(DeclareVariableStatement node) => node.AcceptChildren(this); + + // Ignoring statements which cannot be reasonable purpose for a unit to exist + public override void ExplicitVisit(PredicateSetStatement node) { } + + public override void ExplicitVisit(SetIdentityInsertStatement node) { } + + public override void ExplicitVisit(SetOffsetsStatement node) { } + + public override void ExplicitVisit(SetStatisticsStatement node) { } + + public override void ExplicitVisit(SetCommandStatement node) { } + + public override void ExplicitVisit(PrintStatement node) { } + + public override void ExplicitVisit(ThrowStatement node) { } + + public override void ExplicitVisit(RaiseErrorStatement node) { } + + public override void ExplicitVisit(DeclareTableVariableStatement node) { } + + public override void ExplicitVisit(DeclareCursorStatement node) { } + + public override void ExplicitVisit(GoToStatement node) { } + + public override void ExplicitVisit(LabelStatement node) { } + + public override void ExplicitVisit(BeginTransactionStatement node) { } + + public override void ExplicitVisit(CommitTransactionStatement node) { } + + public override void ExplicitVisit(RollbackTransactionStatement node) { } + + public override void ExplicitVisit(SaveTransactionStatement node) { } + + // Return expression may be complex: function call, query + public override void ExplicitVisit(ReturnStatement node) => node.Expression?.Accept(this); + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/UselessUnitRule.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/UselessUnitRule.cs new file mode 100644 index 00000000..6687c373 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/UselessUnitRule.cs @@ -0,0 +1,122 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("CS0456", "USELESS_UNIT")] + internal sealed partial class UselessUnitRule : AbstractRule + { + public UselessUnitRule() : base() + { + } + + protected override void ValidateBatch(TSqlBatch batch) + { + // CREATE PROC/TRIGGER/FUNC must be the first statement in a batch + var firstStmt = batch.Statements[0]; + if (firstStmt is ProcedureStatementBody proc) + { + if (proc.MethodSpecifier != null) + { + // external proc + return; + } + + ValidateBody(proc.StatementList, proc); + } + else if (firstStmt is TriggerStatementBody trg) + { + if (trg.MethodSpecifier != null) + { + // external trg + return; + } + + ValidateBody(trg.StatementList, trg); + } + else if (firstStmt is FunctionStatementBody fn) + { + if (fn.MethodSpecifier != null) + { + // external fn + return; + } + + if (fn.ReturnType is SelectFunctionReturnType inlineTableFunc) + { + // let's say that SELECT is good enough as a reason for a function to exist + return; + } + + ValidateBody(fn.StatementList, fn); + } + } + + private static bool ContainsMeaningfulStatementInBody(TSqlStatement node) + { + var visitor = new BodyVisitor(); + node.AcceptChildren(visitor); + return visitor.ContainsMeaningfulStatement; + } + + private static bool IsSimpleExampleOfUselessUnit(TSqlStatement firstStmt) + { + if (firstStmt is PrintStatement) + { + return true; + } + + if (firstStmt is ReturnStatement ret + && (ret.Expression is null || ret.Expression is Literal)) + { + return true; + } + + if (firstStmt is ThrowStatement) + { + return true; + } + + if (firstStmt is RaiseErrorStatement) + { + return true; + } + + return false; + } + + private void ValidateBody(StatementList body, TSqlStatement stmt) + { + // Expanding outermost and nested BEGIN-ENDs if any + while (body != null && body.Statements.Count > 0 + && body.Statements[0] is BeginEndBlockStatement be) + { + body = be.StatementList; + } + + if (body is null || body.Statements.Count == 0) + { + // no body + HandleNodeError(stmt); + return; + } + + var firstStatement = body.Statements[0]; + + // No need to instantiate and run visitor for simple cases. + if (body.Statements.Count == 1 && IsSimpleExampleOfUselessUnit(firstStatement)) + { + HandleNodeError(firstStatement); + return; + } + + if (ContainsMeaningfulStatementInBody(stmt)) + { + return; + } + + HandleNodeError(firstStatement); + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodingConvention/FetchFullyQualifiedRule.cs b/TeamTools.TSQL.Linter/Rules/CodingConvention/FetchFullyQualifiedRule.cs new file mode 100644 index 00000000..3f13e4af --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodingConvention/FetchFullyQualifiedRule.cs @@ -0,0 +1,25 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("CV0858", "FETCH_FULLY_QUALIFIED")] + internal sealed class FetchFullyQualifiedRule : AbstractRule + { + public FetchFullyQualifiedRule() : base() + { + } + + public override void Visit(FetchCursorStatement node) + { + if (node.FetchType is null) + { + // Direction (NEXT, PRIOR, etc) omitted + // FROM cannot be omitted if direction is specified + HandleNodeError(node.Cursor); + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodingConvention/SetOptionsInAscOrderRule.cs b/TeamTools.TSQL.Linter/Rules/CodingConvention/SetOptionsInAscOrderRule.cs new file mode 100644 index 00000000..bbf7396f --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodingConvention/SetOptionsInAscOrderRule.cs @@ -0,0 +1,65 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("CV0897", "SET_OPTIONS_ASC_ORDER")] + internal sealed class SetOptionsInAscOrderRule : AbstractRule + { + private static readonly HashSet Options = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "ANSI_DEFAULTS", + "ANSI_NULLS", + "ANSI_NULL_DFLT_OFF", + "ANSI_NULL_DFLT_ON", + "ANSI_PADDING", + "ANSI_WARNINGS", + "ARITHABORT", + "ARITHIGNORE", + "CONCAT_NULL_YIELDS_NULL", + "CURSOR_CLOSE_ON_COMMIT", + "DISABLE_DEF_CNST_CHK", + "FMTONLY", + "FORCEPLAN", + "IMPLICIT_TRANSACTIONS", + "NOCOUNT", + "NOEXEC", + "NO_BROWSETABLE", + "NUMERIC_ROUNDABORT", + "PARSEONLY", + "QUOTED_IDENTIFIER", + "REMOTE_PROC_TRANSACTIONS", + "SHOWPLAN_ALL", + "SHOWPLAN_TEXT", + "SHOWPLAN_XML", + "XACT_ABORT", + }; + + public SetOptionsInAscOrderRule() : base() + { + } + + public override void Visit(PredicateSetStatement node) + { + string lastOptionName = null; + + for (int i = node.LastTokenIndex - 1, start = node.FirstTokenIndex; i >= start; i--) + { + var token = node.ScriptTokenStream[i]; + if (token.TokenType == TSqlTokenType.Identifier && Options.Contains(token.Text)) + { + if (lastOptionName != null + && string.Compare(lastOptionName, token.Text, StringComparison.OrdinalIgnoreCase) < 0) + { + HandleTokenError(token); + } + + lastOptionName = token.Text; + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/ContinuousDeployment/CreateOptionsInsideRule.cs b/TeamTools.TSQL.Linter/Rules/ContinuousDeployment/CreateOptionsInsideRule.cs new file mode 100644 index 00000000..054941ce --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/ContinuousDeployment/CreateOptionsInsideRule.cs @@ -0,0 +1,39 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using TeamTools.Common.Linting; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("CD0895", "CREATE_OPTIONS_INSIDE")] + internal sealed class CreateOptionsInsideRule : AbstractRule + { + public CreateOptionsInsideRule() : base() + { + } + + public override void Visit(PredicateSetStatement node) + { + if (node.Options.HasFlag(SetOptions.AnsiNulls)) + { + HandleNodeError(node, "ANSI_NULLS"); + } + else if (node.Options.HasFlag(SetOptions.QuotedIdentifier)) + { + HandleNodeError(node, "QUOTED_IDENTIFIER"); + } + } + + protected override void ValidateBatch(TSqlBatch batch) + { + // CREATE PROC/TRIGGER must be the first statement in a batch + var firstStmt = batch.Statements[0]; + if (firstStmt is ProcedureStatementBody proc) + { + proc.StatementList?.AcceptChildren(this); + } + else if (firstStmt is TriggerStatementBody trg) + { + trg.StatementList?.AcceptChildren(this); + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/ContinuousDeployment/SingleObjectPerFileRule.Visitor.cs b/TeamTools.TSQL.Linter/Rules/ContinuousDeployment/SingleObjectPerFileRule.Visitor.cs new file mode 100644 index 00000000..7ae7b0b6 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/ContinuousDeployment/SingleObjectPerFileRule.Visitor.cs @@ -0,0 +1,81 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Diagnostics.CodeAnalysis; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + internal partial class SingleObjectPerFileRule + { + [ExcludeFromCodeCoverage] + private sealed class CreateVisitor : VisitorWithCallback + { + public CreateVisitor(Action callback) : base(callback) + { } + + public int CreateCount { get; private set; } = 0; + + // Explicit - because such programmabilities might contain some other object creation + // which is valid in case of current rule. + public override void ExplicitVisit(CreateProcedureStatement node) => CreateDetected(node); + + public override void ExplicitVisit(CreateOrAlterProcedureStatement node) => CreateDetected(node); + + public override void ExplicitVisit(AlterProcedureStatement node) => CreateDetected(node); + + public override void ExplicitVisit(CreateTriggerStatement node) => CreateDetected(node); + + public override void ExplicitVisit(CreateOrAlterTriggerStatement node) => CreateDetected(node); + + public override void ExplicitVisit(AlterTriggerStatement node) => CreateDetected(node); + + public override void Visit(ViewStatementBody node) => CreateDetected(node); + + public override void Visit(FunctionStatementBody node) => CreateDetected(node); + + // ALTER TABLE ADD , CREATE INDEX are fine to be in the same script with table creation + public override void Visit(CreateTableStatement node) => CreateDetected(node); + + public override void Visit(CreateTypeStatement node) => CreateDetected(node); + + public override void Visit(CreateSynonymStatement node) => CreateDetected(node); + + public override void Visit(CreateSchemaStatement node) => CreateDetected(node); + + public override void Visit(CreateServiceStatement node) => CreateDetected(node); + + public override void Visit(CreateQueueStatement node) => CreateDetected(node); + + public override void Visit(CreateMessageTypeStatement node) => CreateDetected(node); + + public override void Visit(CreateContractStatement node) => CreateDetected(node); + + public override void Visit(CreateDefaultStatement node) => CreateDetected(node); + + public override void Visit(CreateRuleStatement node) => CreateDetected(node); + + public override void Visit(CreateRoleStatement node) => CreateDetected(node); + + public override void Visit(CreateUserStatement node) => CreateDetected(node); + + public override void Visit(CreateLoginStatement node) => CreateDetected(node); + + public override void Visit(CreatePartitionSchemeStatement node) => CreateDetected(node); + + public override void Visit(CreatePartitionFunctionStatement node) => CreateDetected(node); + + public override void Visit(CreateAssemblyStatement node) => CreateDetected(node); + + public override void Visit(CreateSequenceStatement node) => CreateDetected(node); + + private void CreateDetected(TSqlFragment node) + { + if (++CreateCount > 1) + { + Callback(node); + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/ContinuousDeployment/SingleObjectPerFileRule.cs b/TeamTools.TSQL.Linter/Rules/ContinuousDeployment/SingleObjectPerFileRule.cs new file mode 100644 index 00000000..96728b3a --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/ContinuousDeployment/SingleObjectPerFileRule.cs @@ -0,0 +1,15 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using TeamTools.Common.Linting; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("CD0857", "SINGLE_OBJECT_PER_FILE")] + internal sealed partial class SingleObjectPerFileRule : AbstractRule + { + public SingleObjectPerFileRule() : base() + { + } + + protected override void ValidateScript(TSqlScript node) => node.AcceptChildren(new CreateVisitor(ViolationHandler)); + } +} diff --git a/TeamTools.TSQL.Linter/Rules/DatabaseDesign/BaseTemporalTableRangeValidator.DataTypeValidator.cs b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/BaseTemporalTableRangeValidator.DataTypeValidator.cs new file mode 100644 index 00000000..022fcb37 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/BaseTemporalTableRangeValidator.DataTypeValidator.cs @@ -0,0 +1,50 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + internal partial class BaseTemporalTableRangeValidator + { + protected static readonly string BestTypeForHistory = "DATETIME2"; + protected static readonly int BestPrecision = 7; + + protected static bool TryExtractDateTimePrecision(DataTypeReference dataType, out int precision) + { + precision = -1; + + if (!(dataType is SqlDataTypeReference systemType)) + { + // something unknown + return true; + } + + if (!systemType.GetFullName().Equals(BestTypeForHistory, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (systemType.Parameters.Count != 1) + { + // no precision or broken syntax + return true; + } + + if (!int.TryParse(systemType.Parameters[0].Value, out precision)) + { + // probably broken syntax + precision = -1; + } + + return true; + } + + protected virtual bool IsDataTypeAlright(DataTypeReference dataType) + { + return TryExtractDateTimePrecision(dataType, out int definedPrecision) + && (definedPrecision == -1 || definedPrecision >= BestPrecision); + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/DatabaseDesign/BaseTemporalTableRangeValidator.DefaultValidator.cs b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/BaseTemporalTableRangeValidator.DefaultValidator.cs new file mode 100644 index 00000000..98bef537 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/BaseTemporalTableRangeValidator.DefaultValidator.cs @@ -0,0 +1,51 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + internal partial class BaseTemporalTableRangeValidator + { + protected static DefaultConstraintDefinition ExtractDefaultConstraint(ColumnDefinition col) + { + if (col.DefaultConstraint != null) + { + return col.DefaultConstraint; + } + + for (int i = col.Constraints.Count - 1; i >= 0; i--) + { + if (col.Constraints[i] is DefaultConstraintDefinition d) + { + return d; + } + } + + return default; + } + + protected static ScalarExpression ExpandExpression(ScalarExpression expr) + { + while (expr is ParenthesisExpression pe) + { + expr = pe.Expression; + } + + return expr; + } + + [ExcludeFromCodeCoverage] + protected virtual bool IsDefaultAlright(DefaultConstraintDefinition def) + { + return true; + } + + protected virtual bool IsDefaultAlright(ColumnDefinition col) + { + return IsDefaultAlright(ExtractDefaultConstraint(col)); + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/DatabaseDesign/BaseTemporalTableRangeValidator.cs b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/BaseTemporalTableRangeValidator.cs new file mode 100644 index 00000000..eabe011f --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/BaseTemporalTableRangeValidator.cs @@ -0,0 +1,61 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + internal abstract partial class BaseTemporalTableRangeValidator : AbstractRule + { + public override void Visit(CreateTableStatement node) + { + if (node.Definition is null) + { + // e.g. filetable + return; + } + + if (node.Definition.SystemTimePeriod is null) + { + // PERIOD not defined + return; + } + + var startCol = node.Definition.SystemTimePeriod.StartTimeColumn.Value; + var endCol = node.Definition.SystemTimePeriod.EndTimeColumn.Value; + + ValidateHistoryColumns(node.Definition.ColumnDefinitions, startCol, endCol); + } + + protected virtual void ValidateHistoryColumns(IList cols, string startCol, string endCol) + { + for (int i = cols.Count - 1; i >= 0; i--) + { + var col = cols[i]; + if (DoesColumnDefinePeriodRangeIncorrectly(col, startCol, endCol)) + { + HandleNodeError(col.DataType, col.ColumnIdentifier.Value); + } + } + } + + protected virtual bool DoesColumnDefinePeriodRangeIncorrectly(ColumnDefinition col, string startCol, string endCol) + { + return DoesColumnDefineTemporalRange(col, startCol, endCol) + && !DoesColumnHaveCorrectDefinition(col); + } + + protected virtual bool DoesColumnHaveCorrectDefinition(ColumnDefinition col) + { + return IsDataTypeAlright(col.DataType) || IsDefaultAlright(col); + } + + protected virtual bool DoesColumnDefineTemporalRange(ColumnDefinition col, string startCol, string endCol) + { + return col.GeneratedAlways != null + || col.ColumnIdentifier.Value.Equals(startCol, StringComparison.OrdinalIgnoreCase) + || col.ColumnIdentifier.Value.Equals(endCol, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/DatabaseDesign/ScalarUdtRule.cs b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/ScalarUdtRule.cs new file mode 100644 index 00000000..4e31be09 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/ScalarUdtRule.cs @@ -0,0 +1,16 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("DD0855", "SCALAR_UDT")] + internal sealed class ScalarUdtRule : AbstractRule + { + public ScalarUdtRule() : base() + { + } + + public override void ExplicitVisit(CreateTypeUddtStatement node) => HandleNodeError(node.DataType, node.Name.GetFullName()); + } +} diff --git a/TeamTools.TSQL.Linter/Rules/DatabaseDesign/SingleColumnTableRule.cs b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/SingleColumnTableRule.cs index dc0b374d..4c8d8cb5 100644 --- a/TeamTools.TSQL.Linter/Rules/DatabaseDesign/SingleColumnTableRule.cs +++ b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/SingleColumnTableRule.cs @@ -1,5 +1,6 @@ using Microsoft.SqlServer.TransactSql.ScriptDom; using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; namespace TeamTools.TSQL.Linter.Rules { @@ -10,8 +11,18 @@ public SingleColumnTableRule() : base() { } - // FIXME : ignore # and @ - public override void Visit(TableDefinition node) + public override void ExplicitVisit(CreateTableStatement node) + { + if (node.SchemaObjectName.BaseIdentifier.Value.StartsWith(TSqlDomainAttributes.TempTablePrefix)) + { + // Temp tables should be ignored + return; + } + + ValidateDefinition(node.Definition); + } + + private void ValidateDefinition(TableDefinition node) { if (node?.ColumnDefinitions is null) { @@ -21,7 +32,7 @@ public override void Visit(TableDefinition node) if (node.ColumnDefinitions.Count == 1) { - HandleNodeError(node.ColumnDefinitions[0]); + HandleNodeError(node.ColumnDefinitions[0].ColumnIdentifier); } } } diff --git a/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTableConsistencyCheckRule.cs b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTableConsistencyCheckRule.cs new file mode 100644 index 00000000..6ff75c5c --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTableConsistencyCheckRule.cs @@ -0,0 +1,30 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("DD0862", "HISTORY_CONSISTENCY_CHECK")] + [CompatibilityLevel(SqlVersion.Sql130)] + internal sealed class TemporalTableConsistencyCheckRule : AbstractRule + { + public TemporalTableConsistencyCheckRule() + { + } + + public override void Visit(CreateTableStatement node) + { + for (int i = node.Options.Count - 1; i >= 0; i--) + { + if (node.Options[i] is SystemVersioningTableOption history + && history.ConsistencyCheckEnabled == OptionState.Off) + { + HandleNodeError(history); + return; + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTableNameHistoryTableRule.cs b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTableNameHistoryTableRule.cs new file mode 100644 index 00000000..3469afc7 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTableNameHistoryTableRule.cs @@ -0,0 +1,29 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("DD0861", "HISTORY_SET_STORAGE_NAME")] + [CompatibilityLevel(SqlVersion.Sql130)] + internal sealed class TemporalTableNameHistoryTableRule : AbstractRule + { + public TemporalTableNameHistoryTableRule() + { + } + + public override void Visit(CreateTableStatement node) + { + for (int i = node.Options.Count - 1; i >= 0; i--) + { + if (node.Options[i] is SystemVersioningTableOption history + && history.HistoryTable is null) + { + HandleNodeError(history); + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTablePeriodDefinedRule.cs b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTablePeriodDefinedRule.cs new file mode 100644 index 00000000..21839d55 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTablePeriodDefinedRule.cs @@ -0,0 +1,41 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("DD0860", "HISTORY_PERIOD_SET")] + [CompatibilityLevel(SqlVersion.Sql130)] + internal sealed class TemporalTablePeriodDefinedRule : AbstractRule + { + public TemporalTablePeriodDefinedRule() + { + } + + public override void Visit(CreateTableStatement node) + { + if (node.Definition is null) + { + // e.g. filetable + return; + } + + if (node.Definition.SystemTimePeriod != null) + { + // PERIOD defined + return; + } + + for (int i = node.Options.Count - 1; i >= 0; i--) + { + // If no such option then this is not a temporal table + if (node.Options[i] is SystemVersioningTableOption history) + { + HandleNodeError(history); + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTablePossibleFutureDateRule.cs b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTablePossibleFutureDateRule.cs new file mode 100644 index 00000000..365ab806 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTablePossibleFutureDateRule.cs @@ -0,0 +1,54 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("DD0864", "HISTORY_FUTURE_DATE")] + [CompatibilityLevel(SqlVersion.Sql130)] + internal sealed partial class TemporalTablePossibleFutureDateRule : BaseTemporalTableRangeValidator + { + public TemporalTablePossibleFutureDateRule() : base() + { + } + + // TODO : Should it check the case when for DATETIME(7) default has DATEADD with positive argument? + protected override bool IsDefaultAlright(DefaultConstraintDefinition def) + { + const int dateAddArgCount = 3; + const string dateAddFunction = "DATEADD"; + + if (def is null) + { + // no default + return true; + } + + var defValue = ExpandExpression(def.Expression); + + if (defValue is Literal) + { + return true; + } + + // DATEADD with negative offset + return + defValue is FunctionCall fn + && string.Equals(fn.FunctionName.Value, dateAddFunction, StringComparison.OrdinalIgnoreCase) + && fn.Parameters.Count == dateAddArgCount + && ExpandExpression(fn.Parameters[1]) is UnaryExpression ue + && ue.UnaryExpressionType == UnaryExpressionType.Negative + && ExpandExpression(ue.Expression) is IntegerLiteral literal + && int.TryParse(literal.Value, out int dateAddValue) + && dateAddValue != 0; + } + + protected override bool DoesColumnDefineTemporalRange(ColumnDefinition col, string startCol, string endCol) + { + // Only the Start column cannot accept future date + return col.ColumnIdentifier.Value.Equals(startCol, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTableRangeDefaultPrecisionLackRule.cs b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTableRangeDefaultPrecisionLackRule.cs new file mode 100644 index 00000000..56bcaaa8 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTableRangeDefaultPrecisionLackRule.cs @@ -0,0 +1,102 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using System.Linq; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("DD0865", "HISTORY_PERIOD_DEFAULT_PRECISION")] + [CompatibilityLevel(SqlVersion.Sql130)] + internal sealed class TemporalTableRangeDefaultPrecisionLackRule : BaseTemporalTableRangeValidator, ISqlServerMetadataConsumer + { + private IDictionary dateFunctions; + + public TemporalTableRangeDefaultPrecisionLackRule() + { + } + + // Storing output types of system datetime functions + public void LoadMetadata(SqlServerMetadata data) + { + dateFunctions = data.Functions + .Where(f => f.Value.DataType != null && f.Value.DataType.Contains("DATE", StringComparison.OrdinalIgnoreCase)) + .ToDictionary(f => f.Key, f => f.Value.DataType, StringComparer.OrdinalIgnoreCase); + } + + protected override bool DoesColumnHaveCorrectDefinition(ColumnDefinition col) => IsDefaultAlright(col); + + protected override bool IsDefaultAlright(ColumnDefinition col) + { + var def = ExtractDefaultConstraint(col); + + if (def is null) + { + return true; + } + + var defaultValueType = GetOutputType(ExpandExpression(def.Expression), out int defaultPrecision); + + if (string.IsNullOrEmpty(defaultValueType)) + { + // something unsupported/unrecognizable + return true; + } + + if (!string.Equals(defaultValueType, BestTypeForHistory, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (TryExtractDateTimePrecision(col.DataType, out int columnPrecision) + && columnPrecision == -1) + { + // default precision for DATETIME2 + columnPrecision = 7; + } + + // Rule was unable to determine either of precisions or DEFAULT precision is fine + return defaultPrecision == -1 || columnPrecision == -1 || defaultPrecision >= columnPrecision; + } + + private string GetOutputType(ScalarExpression expr, out int precision) + { + precision = -1; // no precision set + + if (expr is null) + { + return default; + } + + if (expr is CastCall cast) + { + TryExtractDateTimePrecision(cast.DataType, out precision); + return cast.DataType.GetFullName(); + } + + if (expr is ConvertCall convert) + { + TryExtractDateTimePrecision(convert.DataType, out precision); + return convert.DataType.GetFullName(); + } + + if (expr is FunctionCall func) + { + if (func.FunctionName.Value.Equals("DATEADD", StringComparison.OrdinalIgnoreCase) + && func.Parameters.Count == 3) + { + // depends on what we are "date adding" to + return GetOutputType(ExpandExpression(func.Parameters[2]), out precision); + } + + if (dateFunctions.TryGetValue(func.FunctionName.Value, out var outputType)) + { + return outputType; + } + } + + return default; + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTableRangeMaxPrecisionRule.cs b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTableRangeMaxPrecisionRule.cs new file mode 100644 index 00000000..280d9860 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTableRangeMaxPrecisionRule.cs @@ -0,0 +1,20 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("DD0866", "HISTORY_MAX_DATE_PRECISION")] + [CompatibilityLevel(SqlVersion.Sql130)] + internal sealed partial class TemporalTableRangeMaxPrecisionRule : BaseTemporalTableRangeValidator + { + public TemporalTableRangeMaxPrecisionRule() : base() + { + } + + // This rules ensures that max precision is defined only + protected override bool DoesColumnHaveCorrectDefinition(ColumnDefinition col) => IsDataTypeAlright(col.DataType); + } +} diff --git a/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTableSameSchemaRule.cs b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTableSameSchemaRule.cs index b703a5b2..0132533d 100644 --- a/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTableSameSchemaRule.cs +++ b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTableSameSchemaRule.cs @@ -17,18 +17,23 @@ public override void Visit(CreateTableStatement node) { var schemaName = node.SchemaObjectName.SchemaIdentifier?.Value ?? TSqlDomainAttributes.DefaultSchemaName; - for (int i = 0, n = node.Options.Count; i < n; i++) + for (int i = node.Options.Count - 1; i >= 0; i--) { if (node.Options[i] is SystemVersioningTableOption history) { - var historySchema = history.HistoryTable.SchemaIdentifier?.Value ?? TSqlDomainAttributes.DefaultSchemaName; - - if (!string.Equals(schemaName, historySchema, StringComparison.OrdinalIgnoreCase)) - { - HandleNodeError(history.HistoryTable, $"{historySchema} != {schemaName}"); - } + ValidateHistoryTableSchema(history, schemaName); } } } + + private void ValidateHistoryTableSchema(SystemVersioningTableOption history, string expectedSchema) + { + var historySchema = history.HistoryTable.SchemaIdentifier?.Value ?? TSqlDomainAttributes.DefaultSchemaName; + + if (!string.Equals(expectedSchema, historySchema, StringComparison.OrdinalIgnoreCase)) + { + HandleNodeError(history.HistoryTable, $"{historySchema} != {expectedSchema}"); + } + } } } diff --git a/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTableSimilarDateRangePrecision.cs b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTableSimilarDateRangePrecision.cs new file mode 100644 index 00000000..aff5fa7c --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/DatabaseDesign/TemporalTableSimilarDateRangePrecision.cs @@ -0,0 +1,89 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("DD0863", "HISTORY_SAME_DATE_RANGE_PRECISION")] + [CompatibilityLevel(SqlVersion.Sql130)] + internal sealed class TemporalTableSimilarDateRangePrecision : BaseTemporalTableRangeValidator + { + public TemporalTableSimilarDateRangePrecision() : base() + { + } + + protected override void ValidateHistoryColumns(IList cols, string startCol, string endCol) + { + DataTypeReference startType = null; + DataTypeReference endType = null; + + for (int i = cols.Count - 1; i >= 0; i--) + { + var col = cols[i]; + + if (col.ColumnIdentifier.Value.Equals(startCol, StringComparison.OrdinalIgnoreCase)) + { + startType = col.DataType; + } + else if (col.ColumnIdentifier.Value.Equals(endCol, StringComparison.OrdinalIgnoreCase)) + { + endType = col.DataType; + } + + if (startType != null && endType != null) + { + break; + } + } + + if (startType?.Name is null || endType?.Name is null) + { + // probably broken syntax or something incompatible + return; + } + + if (!DataTypesMatch(startType, endType)) + { + HandleNodeError(endType); + } + } + + private static bool DataTypesMatch(DataTypeReference startType, DataTypeReference endType) + { + if (!string.Equals(startType.GetFullName(), endType.GetFullName(), StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!(startType is SqlDataTypeReference start)) + { + // it must be system type + return false; + } + + if (!(endType is SqlDataTypeReference end)) + { + // it must be system type + return false; + } + + // 7 is the default precision for DATETIME2 + int startPrecision = 7; + int endPrecision = 7; + + if (start.Parameters.Count == 1 && int.TryParse(start.Parameters[0].Value, out int p1)) + { + startPrecision = p1; + } + + if (end.Parameters.Count == 1 && int.TryParse(end.Parameters[0].Value, out int p2)) + { + endPrecision = p2; + } + + return startPrecision == endPrecision; + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Failure/ColumnNotIncludedInGroupByRule.ColumnReferenceVisitor.cs b/TeamTools.TSQL.Linter/Rules/Failure/ColumnNotIncludedInGroupByRule.ColumnReferenceVisitor.cs new file mode 100644 index 00000000..44885738 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Failure/ColumnNotIncludedInGroupByRule.ColumnReferenceVisitor.cs @@ -0,0 +1,38 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + internal partial class ColumnNotIncludedInGroupByRule + { + private sealed class ColumnReferenceVisitor : TSqlViolationDetector + { + private readonly HashSet nonColumnIdentifiers; + + public ColumnReferenceVisitor(HashSet nonColumnIdentifiers) + { + this.nonColumnIdentifiers = nonColumnIdentifiers; + } + + public void Reset() => Detected = false; + + public override void Visit(ColumnReferenceExpression node) + { + if (node.ColumnType == ColumnType.Wildcard) + { + return; + } + + if (node.MultiPartIdentifier.Identifiers.Count == 1 + && nonColumnIdentifiers.Contains(node.MultiPartIdentifier.Identifiers[0].Value)) + { + return; + } + + MarkDetected(node); + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Failure/ColumnNotIncludedInGroupByRule.GroupByExtractor.cs b/TeamTools.TSQL.Linter/Rules/Failure/ColumnNotIncludedInGroupByRule.GroupByExtractor.cs new file mode 100644 index 00000000..bdfa7400 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Failure/ColumnNotIncludedInGroupByRule.GroupByExtractor.cs @@ -0,0 +1,84 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using System.Linq; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + internal partial class ColumnNotIncludedInGroupByRule + { + private static IEnumerable ExtractGroupingSpecification(IList grp) + { + for (int i = grp.Count - 1; i >= 0; i--) + { + foreach (var g in ExtractGroupingSpecification(grp[i])) + { + yield return g; + } + } + } + + private static IEnumerable ExtractGroupingSpecification(GroupingSpecification grp) + { + if (grp is ExpressionGroupingSpecification grpExpr) + { + if (grpExpr.Expression is ValueExpression) + { + // not interested in vars ans literals + return Enumerable.Empty(); + } + + return Enumerable.Repeat(grpExpr.Expression, 1); + } + + if (grp is RollupGroupingSpecification rollup) + { + return ExtractGroupingSpecification(rollup.Arguments); + } + + if (grp is CubeGroupingSpecification cube) + { + return ExtractGroupingSpecification(cube.Arguments); + } + + if (grp is CompositeGroupingSpecification composite) + { + return ExtractGroupingSpecification(composite.Items); + } + + if (grp is GroupingSetsGroupingSpecification sets) + { + return ExtractGroupingSpecification(sets.Sets); + } + + return Enumerable.Empty(); + } + + private static HashSet ExtractGroupedExpressions(IList groupby) + { + var groupedExpressions = new HashSet(StringComparer.OrdinalIgnoreCase); + + for (int i = groupby.Count - 1; i >= 0; i--) + { + foreach (var groupedExpression in ExtractGroupingSpecification(groupby[i])) + { + groupedExpressions.Add(groupedExpression.GetFragmentCleanedText()); + + if (groupedExpression is ColumnReferenceExpression colRef + && colRef.ColumnType != ColumnType.Wildcard) + { + string colName = colRef.MultiPartIdentifier.GetLastIdentifier().Value; + + // TODO : validate column belonging + // also registering column name without alias + // because the rule currently is not able to check all the aliases + groupedExpressions.Add(colName); + } + } + } + + return groupedExpressions; + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Failure/ColumnNotIncludedInGroupByRule.Meta.cs b/TeamTools.TSQL.Linter/Rules/Failure/ColumnNotIncludedInGroupByRule.Meta.cs new file mode 100644 index 00000000..d639df8e --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Failure/ColumnNotIncludedInGroupByRule.Meta.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + internal partial class ColumnNotIncludedInGroupByRule : ISqlServerMetadataConsumer + { + private HashSet nonColumnIdentifiers; + + // TODO : load AGGREGATE and WINDOW(?) functions + public void LoadMetadata(SqlServerMetadata data) + { + if (data.Enums.TryGetValue(TSqlDomainAttributes.DateTimePartEnum, out var dateParts)) + { + nonColumnIdentifiers = new HashSet(dateParts.Select(dtp => dtp.Name), StringComparer.OrdinalIgnoreCase); + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Failure/ColumnNotIncludedInGroupByRule.Validator.cs b/TeamTools.TSQL.Linter/Rules/Failure/ColumnNotIncludedInGroupByRule.Validator.cs new file mode 100644 index 00000000..0989881d --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Failure/ColumnNotIncludedInGroupByRule.Validator.cs @@ -0,0 +1,209 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + // TODO : no GROUP BY but Aggregate function used -> this is the same case actually just no columns are grouped + internal partial class ColumnNotIncludedInGroupByRule + { + private static string GetExpressionDefinitionText(ScalarExpression expr) + { + if (expr is null) + { + return default; + } + + while (expr is ParenthesisExpression pe) + { + expr = pe.Expression; + } + + return expr.GetFragmentCleanedText(); + } + + private static bool ContainsColumnReference(ScalarExpression expr, ColumnReferenceVisitor visitor) + { + visitor.Reset(); + expr.Accept(visitor); + return visitor.Detected; + } + + private bool ContainsInvalidExpression(IList expressions, HashSet groupedExpressions) + { + int n = expressions.Count; + for (int i = 0; i < n; i++) + { + var expr = expressions[i]; + if (IsInvalidInSelect(expr, groupedExpressions)) + { + return true; + } + } + + return false; + } + + private bool ContainsInvalidExpression(IList expressions, HashSet groupedExpressions) + { + int n = expressions.Count; + for (int i = 0; i < n; i++) + { + var expr = expressions[i]; + if (IsInvalidInSelect(expr.WhenExpression, groupedExpressions) + || IsInvalidInSelect(expr.ThenExpression, groupedExpressions)) + { + return true; + } + } + + return false; + } + + private bool ContainsInvalidExpression(IList expressions, HashSet groupedExpressions) + { + int n = expressions.Count; + for (int i = 0; i < n; i++) + { + var expr = expressions[i]; + if (IsInvalidInSelect(expr.WhenExpression, groupedExpressions) + || IsInvalidInSelect(expr.ThenExpression, groupedExpressions)) + { + return true; + } + } + + return false; + } + + private bool IsInvalidInSelect(ScalarExpression node, HashSet groupedExpressions) + { + Debug.Assert(nonColumnIdentifiers != null && nonColumnIdentifiers.Count > 0, "nonColumnIdentifiers not loaded"); + + if (node is null) + { + return false; + } + + // TODO : save flag if groupedExpressions contains expressions and compute full node text only if it does + if (groupedExpressions.Contains(GetExpressionDefinitionText(node))) + { + return false; + } + + if (node is ParenthesisExpression pe) + { + return IsInvalidInSelect(pe.Expression, groupedExpressions); + } + + if (node is BinaryExpression bin) + { + return IsInvalidInSelect(bin.FirstExpression, groupedExpressions) + || IsInvalidInSelect(bin.SecondExpression, groupedExpressions); + } + + if (node is UnaryExpression un) + { + return IsInvalidInSelect(un.Expression, groupedExpressions); + } + + if (node is ValueExpression) + { + return false; + } + + if (node is IIfCall iif) + { + return IsInvalidInSelect(iif.Predicate, groupedExpressions) + || IsInvalidInSelect(iif.ThenExpression, groupedExpressions) + || IsInvalidInSelect(iif.ElseExpression, groupedExpressions); + } + + if (node is CastCall castCall) + { + return IsInvalidInSelect(castCall.Parameter, groupedExpressions); + } + + if (node is ConvertCall convertCall) + { + return IsInvalidInSelect(convertCall.Parameter, groupedExpressions); + } + + if (node is NullIfExpression nlif) + { + return IsInvalidInSelect(nlif.FirstExpression, groupedExpressions) + || IsInvalidInSelect(nlif.SecondExpression, groupedExpressions); + } + + if (node is CoalesceExpression clsc) + { + return ContainsInvalidExpression(clsc.Expressions, groupedExpressions); + } + + if (node is SearchedCaseExpression searchedCase) + { + return ContainsInvalidExpression(searchedCase.WhenClauses, groupedExpressions) + || IsInvalidInSelect(searchedCase.ElseExpression, groupedExpressions); + } + + if (node is SimpleCaseExpression simpleCase) + { + return ContainsInvalidExpression(simpleCase.WhenClauses, groupedExpressions) + || IsInvalidInSelect(simpleCase.InputExpression, groupedExpressions) + || IsInvalidInSelect(simpleCase.ElseExpression, groupedExpressions); + } + + // TODO : allow only AGGREGATE functions and UDF (they could be aggregate fn too) + if (node is FunctionCall funcCall) + { + if (funcCall.FunctionName.Value.Equals("ISNULL", StringComparison.OrdinalIgnoreCase)) + { + return ContainsInvalidExpression(funcCall.Parameters, groupedExpressions); + } + + return false; + } + + if (node is ColumnReferenceExpression colRef) + { + if (colRef.ColumnType == ColumnType.Wildcard) + { + return true; + } + + if (nonColumnIdentifiers.Contains(colRef.MultiPartIdentifier.GetLastIdentifier().Value)) + { + return false; + } + + return !groupedExpressions.Contains(colRef.GetFragmentCleanedText()); + } + + return false; + } + + private bool IsInvalidInSelect(BooleanExpression node, HashSet groupedExpressions) + { + if (node is BooleanParenthesisExpression pe) + { + return IsInvalidInSelect(pe.Expression, groupedExpressions); + } + + if (node is BooleanBinaryExpression bin) + { + return IsInvalidInSelect(bin.FirstExpression, groupedExpressions) + || IsInvalidInSelect(bin.SecondExpression, groupedExpressions); + } + + if (node is BooleanComparisonExpression cmp) + { + return IsInvalidInSelect(cmp.FirstExpression, groupedExpressions) + || IsInvalidInSelect(cmp.SecondExpression, groupedExpressions); + } + + return false; + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Failure/ColumnNotIncludedInGroupByRule.cs b/TeamTools.TSQL.Linter/Rules/Failure/ColumnNotIncludedInGroupByRule.cs index 98bb3c3b..02e01606 100644 --- a/TeamTools.TSQL.Linter/Rules/Failure/ColumnNotIncludedInGroupByRule.cs +++ b/TeamTools.TSQL.Linter/Rules/Failure/ColumnNotIncludedInGroupByRule.cs @@ -1,32 +1,19 @@ using Microsoft.SqlServer.TransactSql.ScriptDom; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using TeamTools.Common.Linting; -using TeamTools.TSQL.Linter.Routines; namespace TeamTools.TSQL.Linter.Rules { // TODO : no GROUP BY but Aggregate function used -> this is the same case actually just no columns are grouped [RuleIdentity("FA0949", "COLUMN_NOT_IN_GROUP_BY")] - internal sealed class ColumnNotIncludedInGroupByRule : AbstractRule, ISqlServerMetadataConsumer + internal sealed partial class ColumnNotIncludedInGroupByRule : AbstractRule { - private HashSet nonColumnIdentifiers; - public ColumnNotIncludedInGroupByRule() : base() { } - // TODO : load AGGREGATE and WINDOW(?) functions - public void LoadMetadata(SqlServerMetadata data) - { - if (data.Enums.TryGetValue(TSqlDomainAttributes.DateTimePartEnum, out var dateParts)) - { - nonColumnIdentifiers = new HashSet(dateParts.Select(dtp => dtp.Name), StringComparer.OrdinalIgnoreCase); - } - } - public override void Visit(QuerySpecification node) { if (node.GroupByClause is null) @@ -41,244 +28,6 @@ public override void Visit(QuerySpecification node) } } - private static string GetExpressionDefinitionText(ScalarExpression expr) - { - if (expr is null) - { - return default; - } - - while (expr is ParenthesisExpression pe) - { - expr = pe.Expression; - } - - return expr.GetFragmentCleanedText(); - } - - private static bool ContainsColumnReference(ScalarExpression expr, ColumnReferenceVisitor visitor) - { - visitor.Reset(); - expr.Accept(visitor); - return visitor.Detected; - } - - private bool ContainsInvalidExpression(IList expressions, HashSet groupedExpressions) - { - int n = expressions.Count; - for (int i = 0; i < n; i++) - { - var expr = expressions[i]; - if (IsInvalidInSelect(expr, groupedExpressions)) - { - return true; - } - } - - return false; - } - - private bool ContainsInvalidExpression(IList expressions, HashSet groupedExpressions) - { - int n = expressions.Count; - for (int i = 0; i < n; i++) - { - var expr = expressions[i]; - if (IsInvalidInSelect(expr.WhenExpression, groupedExpressions) - || IsInvalidInSelect(expr.ThenExpression, groupedExpressions)) - { - return true; - } - } - - return false; - } - - private bool ContainsInvalidExpression(IList expressions, HashSet groupedExpressions) - { - int n = expressions.Count; - for (int i = 0; i < n; i++) - { - var expr = expressions[i]; - if (IsInvalidInSelect(expr.WhenExpression, groupedExpressions) - || IsInvalidInSelect(expr.ThenExpression, groupedExpressions)) - { - return true; - } - } - - return false; - } - - private bool IsInvalidInSelect(ScalarExpression node, HashSet groupedExpressions) - { - Debug.Assert(nonColumnIdentifiers != null && nonColumnIdentifiers.Count > 0, "nonColumnIdentifiers not loaded"); - - if (node is null) - { - return false; - } - - // TODO : save flag if groupedExpressions contains expressions and compute full node text only if it does - if (groupedExpressions.Contains(GetExpressionDefinitionText(node))) - { - return false; - } - - if (node is ParenthesisExpression pe) - { - return IsInvalidInSelect(pe.Expression, groupedExpressions); - } - - if (node is BinaryExpression bin) - { - return IsInvalidInSelect(bin.FirstExpression, groupedExpressions) - || IsInvalidInSelect(bin.SecondExpression, groupedExpressions); - } - - if (node is UnaryExpression un) - { - return IsInvalidInSelect(un.Expression, groupedExpressions); - } - - if (node is ValueExpression) - { - return false; - } - - if (node is IIfCall iif) - { - return IsInvalidInSelect(iif.Predicate, groupedExpressions) - || IsInvalidInSelect(iif.ThenExpression, groupedExpressions) - || IsInvalidInSelect(iif.ElseExpression, groupedExpressions); - } - - if (node is CastCall castCall) - { - return IsInvalidInSelect(castCall.Parameter, groupedExpressions); - } - - if (node is ConvertCall convertCall) - { - return IsInvalidInSelect(convertCall.Parameter, groupedExpressions); - } - - if (node is NullIfExpression nlif) - { - return IsInvalidInSelect(nlif.FirstExpression, groupedExpressions) - || IsInvalidInSelect(nlif.SecondExpression, groupedExpressions); - } - - if (node is CoalesceExpression clsc) - { - return ContainsInvalidExpression(clsc.Expressions, groupedExpressions); - } - - if (node is SearchedCaseExpression searchedCase) - { - return ContainsInvalidExpression(searchedCase.WhenClauses, groupedExpressions) - || IsInvalidInSelect(searchedCase.ElseExpression, groupedExpressions); - } - - if (node is SimpleCaseExpression simpleCase) - { - return ContainsInvalidExpression(simpleCase.WhenClauses, groupedExpressions) - || IsInvalidInSelect(simpleCase.InputExpression, groupedExpressions) - || IsInvalidInSelect(simpleCase.ElseExpression, groupedExpressions); - } - - // TODO : allow only AGGREGATE functions and UDF (they could be aggregate fn too) - if (node is FunctionCall funcCall) - { - if (funcCall.FunctionName.Value.Equals("ISNULL", StringComparison.OrdinalIgnoreCase)) - { - return ContainsInvalidExpression(funcCall.Parameters, groupedExpressions); - } - - return false; - } - - if (node is ColumnReferenceExpression colRef) - { - if (colRef.ColumnType == ColumnType.Wildcard) - { - return true; - } - - if (nonColumnIdentifiers.Contains(colRef.MultiPartIdentifier.GetLastIdentifier().Value)) - { - return false; - } - - return !groupedExpressions.Contains(colRef.GetFragmentCleanedText()); - } - - return false; - } - - private bool IsInvalidInSelect(BooleanExpression node, HashSet groupedExpressions) - { - if (node is BooleanParenthesisExpression pe) - { - return IsInvalidInSelect(pe.Expression, groupedExpressions); - } - - if (node is BooleanBinaryExpression bin) - { - return IsInvalidInSelect(bin.FirstExpression, groupedExpressions) - || IsInvalidInSelect(bin.SecondExpression, groupedExpressions); - } - - if (node is BooleanComparisonExpression cmp) - { - return IsInvalidInSelect(cmp.FirstExpression, groupedExpressions) - || IsInvalidInSelect(cmp.SecondExpression, groupedExpressions); - } - - return false; - } - - private HashSet ExtractGroupedExpressions(IList groupby) - { - var groupedExpressions = new HashSet(StringComparer.OrdinalIgnoreCase); - - int n = groupby.Count; - for (int i = 0; i < n; i++) - { - var grp = groupby[i]; - if (!(grp is ExpressionGroupingSpecification grpExpr)) - { - // unsupported case - break; - } - - if (grpExpr.Expression is ValueExpression) - { - // not interested in vars ans literals - continue; - } - - groupedExpressions.Add(grpExpr.Expression.GetFragmentCleanedText()); - - if (grpExpr.Expression is ColumnReferenceExpression colRef) - { - if (colRef.ColumnType == ColumnType.Wildcard) - { - continue; - } - - string colName = colRef.MultiPartIdentifier.GetLastIdentifier().Value; - - // TODO : validate column belonging - // also registering column name without alias - // because the rule currently is not able to check all the aliases - groupedExpressions.Add(colName); - } - } - - return groupedExpressions; - } - private void ValidateSelectedExpressions(IList selected, HashSet groupedExpressions) { var colVisitor = new ColumnReferenceVisitor(nonColumnIdentifiers); @@ -315,33 +64,5 @@ private void ValidateSelectedExpressions(IList selected, HashSet< HandleNodeError(col, selText); } } - - private sealed class ColumnReferenceVisitor : TSqlViolationDetector - { - private readonly HashSet nonColumnIdentifiers; - - public ColumnReferenceVisitor(HashSet nonColumnIdentifiers) - { - this.nonColumnIdentifiers = nonColumnIdentifiers; - } - - public void Reset() => Detected = false; - - public override void Visit(ColumnReferenceExpression node) - { - if (node.ColumnType == ColumnType.Wildcard) - { - return; - } - - if (node.MultiPartIdentifier.Identifiers.Count == 1 - && nonColumnIdentifiers.Contains(node.MultiPartIdentifier.Identifiers[0].Value)) - { - return; - } - - MarkDetected(node); - } - } } } diff --git a/TeamTools.TSQL.Linter/Rules/Failure/InvalidExtendedPropertyParameterRule.Visitor.cs b/TeamTools.TSQL.Linter/Rules/Failure/InvalidExtendedPropertyParameterRule.Visitor.cs new file mode 100644 index 00000000..0745b9cf --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Failure/InvalidExtendedPropertyParameterRule.Visitor.cs @@ -0,0 +1,89 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; + +namespace TeamTools.TSQL.Linter.Rules +{ + internal partial class InvalidExtendedPropertyParameterRule + { + private sealed class ExtendedPropertyValidator : ExtendedPropertyEditingVisitor + { + // docs: https://learn.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-addextendedproperty-transact-sql?view=sql-server-ver17#arguments + private static readonly HashSet Level0Types = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "ASSEMBLY", + "CONTRACT", + "EVENT NOTIFICATION", + "FILEGROUP", + "MESSAGE TYPE", + "PARTITION FUNCTION", + "PARTITION SCHEME", + "REMOTE SERVICE BINDING", + "ROUTE", + "SCHEMA", + "SERVICE", + "USER", + "TRIGGER", + "TYPE", + "PLAN GUIDE", + }; + + private static readonly HashSet Level1Types = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "AGGREGATE", + "DEFAULT", + "FUNCTION", + "LOGICAL FILE NAME", + "PROCEDURE", + "QUEUE", + "RULE", + "SEQUENCE", + "SYNONYM", + "TABLE", + "TABLE_TYPE", + "TYPE", + "VIEW", + "XML SCHEMA COLLECTION", + }; + + private static readonly HashSet Level2Types = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "COLUMN", + "CONSTRAINT", + "EVENT NOTIFICATION", + "INDEX", + "PARAMETER", + "TRIGGER", + }; + + private static readonly Dictionary> LevelArgTypeMap = new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + { "@level0type", Level0Types }, + { "@level1type", Level1Types }, + { "@level2type", Level2Types }, + }; + + public ExtendedPropertyValidator(Action callback) : base(callback) + { } + + // Only named EXEC parameter provisioning is supported. + // There is a separate rule for preventing passing arguments by position. + protected override void ValidatePropertyEditingProcArgs(IList procParams, TSqlFragment call) + { + for (int i = procParams.Count - 1; i >= 0; i--) + { + var param = procParams[i]; + if (param.Variable != null + && param.ParameterValue is StringLiteral levelTypeValue + && LevelArgTypeMap.TryGetValue(param.Variable.Name, out var validLevelTypes)) + { + if (!validLevelTypes.Contains(levelTypeValue.Value)) + { + Callback(param, levelTypeValue.Value); + } + } + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Failure/InvalidExtendedPropertyParameterRule.cs b/TeamTools.TSQL.Linter/Rules/Failure/InvalidExtendedPropertyParameterRule.cs new file mode 100644 index 00000000..deb191c1 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Failure/InvalidExtendedPropertyParameterRule.cs @@ -0,0 +1,21 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("FA0884", "INVALID_EXTENDED_PROPERTY_PARAM")] + internal sealed partial class InvalidExtendedPropertyParameterRule : AbstractRule + { + private readonly ExtendedPropertyValidator validator; + + public InvalidExtendedPropertyParameterRule() : base() + { + validator = new ExtendedPropertyValidator(ViolationHandlerWithMessage); + } + + public override void Visit(ExecuteSpecification node) => node.Accept(validator); + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Failure/RaiseErrorNeedsWithLogRule.cs b/TeamTools.TSQL.Linter/Rules/Failure/RaiseErrorNeedsWithLogRule.cs new file mode 100644 index 00000000..81f16914 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Failure/RaiseErrorNeedsWithLogRule.cs @@ -0,0 +1,31 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Properties; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("FA0885", "RAISERROR_NEEDS_WITH_LOG")] + internal sealed class RaiseErrorNeedsWithLogRule : AbstractRule + { + private static readonly int MinSeverityToNeedWithLog = 19; + + public RaiseErrorNeedsWithLogRule() : base() + { + } + + public override void Visit(RaiseErrorStatement node) + { + if ((node.RaiseErrorOptions & RaiseErrorOptions.Log) != 0) + { + // WITH LOG defined + return; + } + + if (node.SecondParameter is Literal l && int.TryParse(l.Value, out int severityValue) + && severityValue >= MinSeverityToNeedWithLog) + { + HandleNodeError(node, l.Value); + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Failure/SpExecuteSqlNoAsciiSupportRule.cs b/TeamTools.TSQL.Linter/Rules/Failure/SpExecuteSqlNoAsciiSupportRule.cs index 2d11589e..d2c8332b 100644 --- a/TeamTools.TSQL.Linter/Rules/Failure/SpExecuteSqlNoAsciiSupportRule.cs +++ b/TeamTools.TSQL.Linter/Rules/Failure/SpExecuteSqlNoAsciiSupportRule.cs @@ -70,12 +70,12 @@ private void ValidateSpExecuteSqlArg(ExecuteParameter arg) } if (arg.ParameterValue is VariableReference varRef - && variables.TryGetValue(varRef.Name, out var varType) - && (string.Equals(varType, "NVARCHAR", StringComparison.OrdinalIgnoreCase) + && (!variables.TryGetValue(varRef.Name, out var varType) + || string.Equals(varType, "NVARCHAR", StringComparison.OrdinalIgnoreCase) || string.Equals(varType, "NCHAR", StringComparison.OrdinalIgnoreCase) || string.Equals(varType, "NTEXT", StringComparison.OrdinalIgnoreCase))) { - // variable has on of supported types + // variable has one of supported types or not registered return; } diff --git a/TeamTools.TSQL.Linter/Rules/Failure/UnresolvedTableVariableReferenceRule.cs b/TeamTools.TSQL.Linter/Rules/Failure/UnresolvedTableVariableReferenceRule.cs new file mode 100644 index 00000000..18402013 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Failure/UnresolvedTableVariableReferenceRule.cs @@ -0,0 +1,61 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("FA0459", "UNRESOLVED_TABLE_VAR_NAME")] + internal sealed class UnresolvedTableVariableReferenceRule : AbstractRule + { + public UnresolvedTableVariableReferenceRule() : base() + { + } + + protected override void ValidateBatch(TSqlBatch node) => node.AcceptChildren(new VarVisitor(ViolationHandlerWithMessage)); + + private sealed class VarVisitor : TSqlFragmentVisitor + { + private readonly HashSet variables = new HashSet(StringComparer.OrdinalIgnoreCase); + private readonly Action callback; + + public VarVisitor(Action callback) + { + this.callback = callback; + } + + public override void ExplicitVisit(DeclareTableVariableBody node) + { + variables.Add(node.VariableName.Value); + } + + // "Explicit" will prevent visiting the same object as DeclareVariableElement + public override void ExplicitVisit(ProcedureParameter node) + { + // table-type parameter + if (node.Modifier == ParameterModifier.ReadOnly) + { + variables.Add(node.VariableName.Value); + } + } + + // DECLARE statement can contain table-type var declarations + public override void Visit(DeclareVariableElement node) + { + if (node.DataType is UserDataTypeReference) + { + variables.Add(node.VariableName.Value); + } + } + + // Validating reference + public override void Visit(VariableTableReference node) + { + if (!variables.Contains(node.Variable.Name)) + { + callback(node.Variable, node.Variable.Name); + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Failure/UnresolvedVariableReferenceRule.cs b/TeamTools.TSQL.Linter/Rules/Failure/UnresolvedVariableReferenceRule.cs new file mode 100644 index 00000000..aa293257 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Failure/UnresolvedVariableReferenceRule.cs @@ -0,0 +1,52 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("FA0458", "UNRESOLVED_VARIABLED_NAME")] + internal sealed class UnresolvedVariableReferenceRule : AbstractRule + { + public UnresolvedVariableReferenceRule() : base() + { + } + + protected override void ValidateBatch(TSqlBatch node) => node.AcceptChildren(new VarVisitor(ViolationHandlerWithMessage)); + + private sealed class VarVisitor : TSqlFragmentVisitor + { + private readonly HashSet variables = new HashSet(StringComparer.OrdinalIgnoreCase); + private readonly Action callback; + + public VarVisitor(Action callback) + { + this.callback = callback; + } + + // This will also visit input parameter declarations + public override void Visit(DeclareVariableElement node) + { + variables.Add(node.VariableName.Value); + } + + public override void ExplicitVisit(ExecuteParameter node) + { + // parameter value can be variable reference + node.ParameterValue?.Accept(this); + } + + // Ignoring table variable references - they are validated by separate rule + public override void ExplicitVisit(VariableTableReference node) + { } + + public override void Visit(VariableReference node) + { + if (!variables.Contains(node.Name)) + { + callback(node, node.Name); + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Failure/UnresolvedWindowNameRule.cs b/TeamTools.TSQL.Linter/Rules/Failure/UnresolvedWindowNameRule.cs new file mode 100644 index 00000000..70b2127a --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Failure/UnresolvedWindowNameRule.cs @@ -0,0 +1,60 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("FA0451", "UNRESOLVED_WINDOW_NAME")] + [CompatibilityLevel(SqlVersion.Sql160)] + internal sealed class UnresolvedWindowNameRule : AbstractRule + { + public UnresolvedWindowNameRule() : base() + { + } + + public override void Visit(QuerySpecification node) + { + var definedWindows = ExtractWindowNames(node.WindowClause?.WindowDefinition); + node.AcceptChildren(new OverClauseVisitor(definedWindows, ViolationHandlerWithMessage)); + } + + private static HashSet ExtractWindowNames(IList windows) + { + if (windows is null) + { + return default; + } + + var result = new HashSet(StringComparer.OrdinalIgnoreCase); + + for (int i = windows.Count - 1; i >= 0; i--) + { + result.Add(windows[i].WindowName.Value); + } + + return result; + } + + private sealed class OverClauseVisitor : TSqlFragmentVisitor + { + private readonly HashSet knownWindows; + private readonly Action callback; + + public OverClauseVisitor(HashSet knownWindows, Action callback) + { + this.knownWindows = knownWindows; + this.callback = callback; + } + + public override void Visit(OverClause node) + { + if (node.WindowName != null + && (knownWindows is null || !knownWindows.Contains(node.WindowName.Value))) + { + callback(node.WindowName, node.WindowName.Value); + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Failure/VariableRedeclaredRule.cs b/TeamTools.TSQL.Linter/Rules/Failure/VariableRedeclaredRule.cs new file mode 100644 index 00000000..4dc3c947 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Failure/VariableRedeclaredRule.cs @@ -0,0 +1,43 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("FA0457", "VARIABLE_REDECLARED")] + internal sealed class VariableRedeclaredRule : AbstractRule + { + public VariableRedeclaredRule() : base() + { + } + + protected override void ValidateBatch(TSqlBatch node) => node.AcceptChildren(new VarVisitor(ViolationHandlerWithMessage)); + + private sealed class VarVisitor : TSqlFragmentVisitor + { + private readonly HashSet variables = new HashSet(StringComparer.OrdinalIgnoreCase); + private readonly Action callback; + + public VarVisitor(Action callback) + { + this.callback = callback; + } + + // This will also visit input parameter declarations + public override void Visit(DeclareVariableElement node) => ValidateName(node.VariableName); + + // Same var name cannot be used both in scalar and table vars + public override void Visit(DeclareTableVariableBody node) => ValidateName(node.VariableName); + + private void ValidateName(Identifier varName) + { + if (!variables.Add(varName.Value)) + { + // already registered + callback(varName, varName.Value); + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Performance/BaseNoEqualityFilterRule.cs b/TeamTools.TSQL.Linter/Rules/Performance/BaseNoEqualityFilterRule.cs new file mode 100644 index 00000000..388813ba --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Performance/BaseNoEqualityFilterRule.cs @@ -0,0 +1,101 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + internal abstract class BaseNoEqualityFilterRule : AbstractRule + { + protected BaseNoEqualityFilterRule() : base() + { + } + + // TODO : shouldn't it verify that a ColumnReferenceExpression is on the left side? + protected static bool HasEqualityFilter(BooleanExpression node) + { + if (node is BooleanParenthesisExpression pe) + { + node = pe.Expression; + } + + if (node is BooleanBinaryExpression bin) + { + if (bin.BinaryExpressionType == BooleanBinaryExpressionType.And) + { + return HasEqualityFilter(bin.FirstExpression) + || HasEqualityFilter(bin.SecondExpression); + } + else + { + return HasEqualityFilter(bin.FirstExpression) + && HasEqualityFilter(bin.SecondExpression); + } + } + + if (node is BooleanComparisonExpression cmp) + { + // 1 = 1 is no good + return cmp.ComparisonType == BooleanComparisonType.Equals + && ExpressionsDiffer(cmp.FirstExpression, cmp.SecondExpression); + } + + if (node is InPredicate inpred) + { + // NOT IN does not limit much + return !inpred.NotDefined + && !inpred.Expression.IsMadeOfLiteral(); + } + + // LIKE can lead to INDEX SEEK + if (node is LikePredicate like) + { + return ExpressionsDiffer(like.FirstExpression, like.SecondExpression); + } + + // BETWEEN does not mean equality but limits range from both sides + if (node is BooleanTernaryExpression between) + { + return ExpressionsDiffer(between.FirstExpression, between.SecondExpression) + && ExpressionsDiffer(between.FirstExpression, between.ThirdExpression); + } + + if (node is BooleanIsNullExpression isnull) + { + // IS NOT NULL means any other value + // however IS NULL seems to bee good enough + return !isnull.IsNot + && !isnull.Expression.IsMadeOfLiteral(); + } + + if (node is BooleanNotExpression not) + { + // TODO : not sure if this negation + recursion is correct for any expression + // return !HasEqualityFilter(not.Expression); + return false; + } + + if (node is ExistsPredicate exists) + { + // EXISTS can be treated as some kind of INNER JOIN with equality predicate + return true; + } + + return false; + } + + protected void ValidatePredicate(BooleanExpression node) + { + if (HasEqualityFilter(node)) + { + return; + } + + HandleNodeError(node); + } + + private static bool ExpressionsDiffer(ScalarExpression first, ScalarExpression second) + { + return !BooleanExpressionComparer.AreEqualExpressions(first, second); + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Performance/ExpandDateFunctionRule.Visitor.cs b/TeamTools.TSQL.Linter/Rules/Performance/ExpandDateFunctionRule.Visitor.cs new file mode 100644 index 00000000..4ec58be7 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Performance/ExpandDateFunctionRule.Visitor.cs @@ -0,0 +1,127 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + internal partial class ExpandDateFunctionRule + { + private sealed class BadPredicateDetector : TSqlFragmentVisitor + { + public BadPredicateDetector(Action callback) + { + Callback = callback; + } + + private Action Callback { get; } + + // TODO : BooleanTernaryExpression (BETWEEN), InPredicate + public override void Visit(BooleanComparisonExpression node) + { + var left = ExpandExpression(node.FirstExpression); + var right = ExpandExpression(node.SecondExpression); + + if (left is VariableReference || left is Literal) + { + (left, right) = (right, left); + } + else if (!(right is VariableReference || right is Literal)) + { + // expression cannot be converted into sargable datetime range predicate + return; + } + + if (!(left is FunctionCall func)) + { + return; + } + + DetectConvertibleFunctionCall(func); + } + + private static ScalarExpression ExpandExpression(ScalarExpression expr) + { + while (expr is ParenthesisExpression pe) + { + expr = pe.Expression; + } + + return expr; + } + + private static bool CanBePrecomputed(ScalarExpression expr) + { + expr = expr.ExtractScalarExpression(); + return expr is Literal || expr is VariableReference; + } + + private static bool IsConvertibleDatePartCall(FunctionCall func) + { + if (func.Parameters.Count != 2) + { + // broken syntax + return false; + } + + if (CanBePrecomputed(func.Parameters[1])) + { + // expression can be precomputed + return false; + } + + // only YEAR can be easily converted to range filter + return ExpandExpression(func.Parameters[0]) is ColumnReferenceExpression col + && col.MultiPartIdentifier.GetLastIdentifier().Value.Equals("YEAR", StringComparison.OrdinalIgnoreCase); + } + + private void DetectConvertibleFunctionCall(FunctionCall func) + { + string funcName = func.FunctionName.Value; + + if (funcName.Equals("YEAR", StringComparison.OrdinalIgnoreCase)) + { + if (func.Parameters.Count != 1 || CanBePrecomputed(func.Parameters[0])) + { + return; + } + + Callback(func, "YEAR"); + } + else if (funcName.Equals("DATETRUNC", StringComparison.OrdinalIgnoreCase)) + { + if (func.Parameters.Count != 2 || CanBePrecomputed(func.Parameters[1])) + { + return; + } + + Callback(func, "DATETRUNC"); + } + else if (funcName.Equals("DATEPART", StringComparison.OrdinalIgnoreCase)) + { + if (IsConvertibleDatePartCall(func)) + { + Callback(func, "DATEPART"); + } + } + else if (funcName.Equals("DATENAME", StringComparison.OrdinalIgnoreCase)) + { + if (IsConvertibleDatePartCall(func)) + { + Callback(func, "DATENAME"); + } + } + else if (funcName.Equals("DATEADD", StringComparison.OrdinalIgnoreCase)) + { + if (func.Parameters.Count != 3 + || (CanBePrecomputed(func.Parameters[1]) && CanBePrecomputed(func.Parameters[2]))) + { + return; + } + + Callback(func, "DATEADD"); + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Performance/ExpandDateFunctionRule.cs b/TeamTools.TSQL.Linter/Rules/Performance/ExpandDateFunctionRule.cs new file mode 100644 index 00000000..042dbcde --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Performance/ExpandDateFunctionRule.cs @@ -0,0 +1,20 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using TeamTools.Common.Linting; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("PF0876", "EXPAND_DATE_FUNC")] + internal sealed partial class ExpandDateFunctionRule : AbstractRule + { + private readonly TSqlFragmentVisitor badPredicateDetector; + + public ExpandDateFunctionRule() : base() + { + badPredicateDetector = new BadPredicateDetector(ViolationHandlerWithMessage); + } + + public override void Visit(QualifiedJoin node) => node.SearchCondition.Accept(badPredicateDetector); + + public override void Visit(WhereClause node) => node.SearchCondition?.Accept(badPredicateDetector); + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Performance/FightOptimizerByForcePlanRule.cs b/TeamTools.TSQL.Linter/Rules/Performance/FightOptimizerByForcePlanRule.cs new file mode 100644 index 00000000..0dfa5a6c --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Performance/FightOptimizerByForcePlanRule.cs @@ -0,0 +1,29 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using TeamTools.Common.Linting; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("PF0871", "OPTIMIZER_FIGHT_FORCE_PLAN")] + internal sealed class FightOptimizerByForcePlanRule : AbstractRule + { + public FightOptimizerByForcePlanRule() : base() + { + } + + public override void Visit(PredicateSetStatement node) + { + if (node.Options.HasFlag(SetOptions.ForcePlan)) + { + HandleNodeError(node); + } + } + + public override void Visit(OptimizerHint node) + { + if (node.HintKind == OptimizerHintKind.UsePlan) + { + HandleNodeError(node); + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Performance/FightOptimizerByJoinHintRule.cs b/TeamTools.TSQL.Linter/Rules/Performance/FightOptimizerByJoinHintRule.cs new file mode 100644 index 00000000..fcaedf44 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Performance/FightOptimizerByJoinHintRule.cs @@ -0,0 +1,31 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System.Collections.Generic; +using TeamTools.Common.Linting; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("PF0868", "OPTIMIZER_FIGHT_JOIN_HINT")] + internal sealed class FightOptimizerByJoinHintRule : AbstractRule + { + // Except None and Remote + // Storing names to avoid unnecessary string generation by ToString() + private static readonly Dictionary JoinHints = new Dictionary + { + { JoinHint.Loop, "LOOP" }, + { JoinHint.Hash, "HASH" }, + { JoinHint.Merge, "MERGE" }, + }; + + public FightOptimizerByJoinHintRule() : base() + { + } + + public override void Visit(QualifiedJoin node) + { + if (JoinHints.TryGetValue(node.JoinHint, out string hint)) + { + HandleNodeError(node, hint); + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Performance/FightOptimizerByQueryOptionRule.cs b/TeamTools.TSQL.Linter/Rules/Performance/FightOptimizerByQueryOptionRule.cs new file mode 100644 index 00000000..98e715e6 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Performance/FightOptimizerByQueryOptionRule.cs @@ -0,0 +1,43 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System.Collections.Generic; +using TeamTools.Common.Linting; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("PF0870", "OPTIMIZER_FIGHT_QUERY_OPTION")] + internal sealed class FightOptimizerByQueryOptionRule : AbstractRule + { + public FightOptimizerByQueryOptionRule() : base() + { + } + + public override void Visit(OptimizerHint node) + { + if (node.HintKind == OptimizerHintKind.Recompile + || node.HintKind == OptimizerHintKind.MaxRecursion) + { + // Allowed hints + return; + } + + if (node is OptimizeForOptimizerHint optimizeFor + && optimizeFor.IsForUnknown) + { + // OPTIMIZE FOR UNKNOWN may fix parameter sniffig issues + return; + } + + if (node.HintKind == OptimizerHintKind.MaxDop + && node is LiteralOptimizerHint maxdop + && int.TryParse(maxdop.Value.Value, out int maxdopValue) + && maxdopValue > 0) + { + // MAXDOP 0 overrides DB and Server level settings + // otherwise it might be fine + return; + } + + HandleNodeError(node, node.HintKind.ToString()); + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Performance/FightOptimizerByTableHintRule.cs b/TeamTools.TSQL.Linter/Rules/Performance/FightOptimizerByTableHintRule.cs new file mode 100644 index 00000000..7b825110 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Performance/FightOptimizerByTableHintRule.cs @@ -0,0 +1,32 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("PF0869", "OPTIMIZER_FIGHT_TABLE_HINT")] + internal sealed class FightOptimizerByTableHintRule : AbstractRule + { + // Except locking hints. + // Storing names to avoid unnecessary string generation by ToString() + private static readonly Dictionary PerformanceHints = new Dictionary + { + { TableHintKind.ForceSeek, "FORCESEEK" }, + { TableHintKind.ForceScan, "FORCESCAN" }, + { TableHintKind.Index, "INDEX" }, + }; + + public FightOptimizerByTableHintRule() : base() + { + } + + public override void Visit(TableHint node) + { + if (PerformanceHints.TryGetValue(node.HintKind, out var hint)) + { + HandleNodeError(node, hint); + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Performance/ForeignKeyIndexRule.cs b/TeamTools.TSQL.Linter/Rules/Performance/ForeignKeyIndexRule.cs new file mode 100644 index 00000000..0aeeb4c7 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Performance/ForeignKeyIndexRule.cs @@ -0,0 +1,76 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using System.Linq; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; +using TeamTools.TSQL.Linter.Routines.TableDefinitionExtractor; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("PF0867", "INDEX_FK")] + [IndexRule] + internal sealed class ForeignKeyIndexRule : ScriptAnalysisServiceConsumingRule + { + public ForeignKeyIndexRule() : base() + { + } + + protected override void ValidateScript(TSqlScript node) + { + var info = GetService(node); + + if (info.Tables.Count == 0) + { + return; + } + + foreach (var tbl in info.Tables) + { + var indices = info.Indices(tbl.Key).OfType().ToList(); + + foreach (var fk in info.ForeignKeys(tbl.Key)) + { + if (!AreFkColumnsIndexed(fk.Columns, indices)) + { + HandleNodeError(fk.Definition); + } + } + } + } + + private static bool AreFkColumnsIndexed(IList keyColumns, IList indices) + { + for (int i = indices.Count - 1; i >= 0; i--) + { + if (DoColumnsMatch(keyColumns, indices[i].Columns)) + { + return true; + } + } + + // no match found + return false; + } + + private static bool DoColumnsMatch(IList keyColumns, IList idxColumns) + { + int keyColCount = keyColumns.Count; + + if (idxColumns.Count < keyColCount) + { + // Not enough columns indexed to cover FK + return false; + } + + int i = 0; + while (i < keyColCount + && string.Equals(keyColumns[i].Name, idxColumns[i].Name, StringComparison.OrdinalIgnoreCase)) + { + ++i; + } + + return i == keyColCount; + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Performance/MixOfDdlAndDmlRule.Visitor.cs b/TeamTools.TSQL.Linter/Rules/Performance/MixOfDdlAndDmlRule.Visitor.cs new file mode 100644 index 00000000..a0e2cf48 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Performance/MixOfDdlAndDmlRule.Visitor.cs @@ -0,0 +1,188 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + internal partial class MixOfDdlAndDmlRule + { + private sealed class DdlDmlMixVisitor : VisitorWithCallback + { + private TSqlFragment lastDDL; + private TSqlFragment lastDML; + + public DdlDmlMixVisitor(Action callback) : base(callback) + { } + + // DDL + public override void Visit(AlterTableStatement node) => DDL(node); + + public override void ExplicitVisit(CreateTableStatement node) => DDL(node); + + public override void ExplicitVisit(DropTableStatement node) => DDL(node); + + public override void ExplicitVisit(CreateIndexStatement node) => DDL(node); + + public override void ExplicitVisit(AlterIndexStatement node) => DDL(node); + + public override void ExplicitVisit(DropIndexStatement node) => DDL(node); + + public override void ExplicitVisit(CreateStatisticsStatement node) => DDL(node); + + public override void ExplicitVisit(DropStatisticsStatement node) => DDL(node); + + public override void ExplicitVisit(CreateSchemaStatement node) => DDL(node); + + public override void ExplicitVisit(DropSchemaStatement node) => DDL(node); + + // DML + public override void Visit(DataModificationStatement node) => DML(node); + + public override void Visit(SelectStatement node) => DML(node); + + // Detection + private static bool HasRecompileDirective(IList hints) + { + for (int i = hints.Count - 1; i >= 0; i--) + { + if (hints[i].HintKind == OptimizerHintKind.Recompile) + { + return true; + } + } + + return false; + } + + private static bool HasSourceQuery(StatementWithCtesAndXmlNamespaces node) + { + FromClause from = null; + DataModificationSpecification dml = null; + + if (node is SelectStatement sel) + { + if (sel.Into != null) + { + // SELECT INTO is both DDL and DML + return true; + } + + from = sel.QueryExpression.GetQuerySpecification()?.FromClause; + } + else if (node is InsertStatement ins && ins.InsertSpecification.InsertSource is SelectInsertSource inssel) + { + dml = ins.InsertSpecification; + from = inssel.Select.GetQuerySpecification()?.FromClause; + } + else if (node is UpdateStatement upd) + { + dml = upd.UpdateSpecification; + from = upd.UpdateSpecification.FromClause; + } + else if (node is DeleteStatement del) + { + dml = del.DeleteSpecification; + from = del.DeleteSpecification.FromClause; + } + else + { + // Let's say MERGE is always a candidate for recompile + return true; + } + + if (dml?.Target is NamedTableReference) + { + // Modifiing temporary or persistent table + return true; + } + + if ((from?.TableReferences?.Count ?? 0) == 0) + { + // no FROM + return false; + } + + return HasRealTableReference(from.TableReferences); + } + + private static bool HasRealTableReference(IList tables) + { + for (int i = tables.Count - 1; i >= 0; i--) + { + if (IsRealTableReference(tables[i])) + { + return true; + } + } + + return false; + } + + private static bool IsRealTableReference(TableReference tbl) + { + if (tbl is JoinTableReference join) + { + return IsRealTableReference(join.FirstTableReference) + || IsRealTableReference(join.SecondTableReference); + } + + if (tbl is JoinParenthesisTableReference jp) + { + return IsRealTableReference(jp.Join); + } + + if (tbl is VariableTableReference) + { + // table variable cannot be altered and is not schema-bound + return false; + } + + if (tbl is GlobalFunctionTableReference) + { + // e.g. STRING_SPLIT + return false; + } + + if (tbl is OpenJsonTableReference) + { + // OPENJSON is kinda scalar thing + return false; + } + + if (tbl is OpenXmlTableReference) + { + // OPENXML is kinda scalar thing + return false; + } + + return true; + } + + private void DDL(TSqlFragment node) + { + lastDDL = node; + } + + private void DML(StatementWithCtesAndXmlNamespaces node) + { + // If there already was a DDL and a DML statement + // and the DDL statement was after DML and now we are doing DML + // again right after DDL then this is the case for implicit recompilation. + // However OPTION (RECOMPILE) means that recompilation is expected. + if (lastDML?.StartLine < lastDDL?.StartLine + && !HasRecompileDirective(node.OptimizerHints)) + { + Callback(lastDDL); + lastDDL = null; + lastDML = null; + } + else if (HasSourceQuery(node)) + { + lastDML = node; + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Performance/MixOfDdlAndDmlRule.cs b/TeamTools.TSQL.Linter/Rules/Performance/MixOfDdlAndDmlRule.cs new file mode 100644 index 00000000..04f1b4f8 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Performance/MixOfDdlAndDmlRule.cs @@ -0,0 +1,30 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("PF0875", "DDL_DML_MIX")] + internal sealed partial class MixOfDdlAndDmlRule : AbstractRule + { + public MixOfDdlAndDmlRule() : base() + { + } + + protected override void ValidateBatch(TSqlBatch batch) + { + // CREATE PROC/TRIGGER/FUNC must be the first statement in a batch + var firstStmt = batch.Statements[0]; + if (firstStmt is ProcedureStatementBody proc) + { + proc.StatementList?.Accept(new DdlDmlMixVisitor(ViolationHandler)); + } + else if (firstStmt is TriggerStatementBody trg) + { + trg.StatementList?.Accept(new DdlDmlMixVisitor(ViolationHandler)); + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Performance/NoEqualityFilterInJoinRule.cs b/TeamTools.TSQL.Linter/Rules/Performance/NoEqualityFilterInJoinRule.cs new file mode 100644 index 00000000..42ef6c44 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Performance/NoEqualityFilterInJoinRule.cs @@ -0,0 +1,16 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("PF0880", "NO_EQUALITY_FILTER_JOIN")] + internal sealed class NoEqualityFilterInJoinRule : BaseNoEqualityFilterRule + { + public NoEqualityFilterInJoinRule() : base() + { + } + + public override void Visit(QualifiedJoin node) => ValidatePredicate(node.SearchCondition); + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Performance/NoEqualityFilterInWhereRule.cs b/TeamTools.TSQL.Linter/Rules/Performance/NoEqualityFilterInWhereRule.cs new file mode 100644 index 00000000..024e77a6 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Performance/NoEqualityFilterInWhereRule.cs @@ -0,0 +1,96 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("PF0881", "NO_EQUALITY_FILTER_WHERE")] + internal sealed class NoEqualityFilterInWhereRule : BaseNoEqualityFilterRule + { + public NoEqualityFilterInWhereRule() : base() + { + } + + public override void Visit(QuerySpecification node) + { + if (node.WhereClause?.SearchCondition is null) + { + // No WHERE or WHERE CURRENT OF + return; + } + + if ((node.FromClause?.TableReferences?.Count ?? 0) == 0) + { + // Ignoring queries with no source + return; + } + + if (node.FromClause.TableReferences.Count == 1 + && SourceHasLimitedVolume(node.FromClause.TableReferences[0])) + { + // Ignoring trivial queries + return; + } + + if (ResultSetIsLimitedByJoinedSource(node.FromClause.TableReferences)) + { + // If query has INNER JOIN to table var/temp table + // then probably everything is not that bad + return; + } + + // TODO : ignore if INNER JOIN with equality exists + ValidatePredicate(node.WhereClause.SearchCondition); + } + + private static bool SourceHasLimitedVolume(TableReference src) + { + return src is VariableTableReference + || src is GlobalFunctionTableReference + || src is OpenJsonTableReference + || src is OpenXmlTableReference + || src is InlineDerivedTable + || IsTempTable(src); + } + + private static bool IsTempTable(TableReference tbl) + { + if (!(tbl is NamedTableReference name)) + { + return false; + } + + string tblName = name.SchemaObject.BaseIdentifier.Value; + + return tblName.StartsWith(TSqlDomainAttributes.TempTablePrefix) + || TSqlDomainAttributes.IsTriggerSystemTable(tblName); + } + + private static bool ResultSetIsLimitedByJoinedSource(IList tables) + { + for (int i = tables.Count - 1; i >= 0; i--) + { + var tbl = tables[i]; + + if (tbl is QualifiedJoin j) + { + if (j.QualifiedJoinType == QualifiedJoinType.FullOuter) + { + // FULL JOIN + return false; + } + + if (j.QualifiedJoinType == QualifiedJoinType.Inner + && (SourceHasLimitedVolume(j.FirstTableReference) || SourceHasLimitedVolume(j.SecondTableReference)) + && HasEqualityFilter(j.SearchCondition)) + { + return true; + } + } + } + + return false; + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Performance/OptionalParameterIsNullPredicateRule.Visitor.cs b/TeamTools.TSQL.Linter/Rules/Performance/OptionalParameterIsNullPredicateRule.Visitor.cs new file mode 100644 index 00000000..55d30a86 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Performance/OptionalParameterIsNullPredicateRule.Visitor.cs @@ -0,0 +1,109 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + internal partial class OptionalParameterIsNullPredicateRule + { + private sealed class BadPredicateDetector : TSqlFragmentVisitor + { + public BadPredicateDetector(Action callback) + { + Callback = callback; + } + + private Action Callback { get; } + + public override void Visit(BooleanComparisonExpression node) + { + if (node.ComparisonType == BooleanComparisonType.GreaterThan + || node.ComparisonType == BooleanComparisonType.LessThan + || node.ComparisonType == BooleanComparisonType.NotEqualToExclamation + || node.ComparisonType == BooleanComparisonType.NotEqualToBrackets) + { + // inequality does not seem to be equivalently replaceable with OR + return; + } + + var left = ExpandExpression(node.FirstExpression); + var right = ExpandExpression(node.SecondExpression); + + if (right is ColumnReferenceExpression) + { + (left, right) = (right, left); + } + + if (!(left is ColumnReferenceExpression filteredColumnReference)) + { + // neither of comparison sides is a column reference - not our case + return; + } + + ScalarExpression optionalArgument = null; + ScalarExpression argumentAlternative = null; + + if (right is FunctionCall isnull + && isnull.Parameters.Count == 2 + && isnull.FunctionName.Value.Equals("ISNULL", StringComparison.OrdinalIgnoreCase)) + { + // tbl.col = ISNULL(@arg, tbl.col) + optionalArgument = isnull.Parameters[0]; + argumentAlternative = isnull.Parameters[1]; + } + else if (right is CoalesceExpression coalesce + && coalesce.Expressions.Count >= 2) + { + // tbl.col = COALESCE(@arg, tbl.col) + optionalArgument = coalesce.Expressions[0]; + argumentAlternative = coalesce.Expressions[1]; + } + else + { + // not our case + return; + } + + DetectConvertibleFunctionCall(filteredColumnReference, ExpandExpression(optionalArgument), ExpandExpression(argumentAlternative)); + } + + private static ScalarExpression ExpandExpression(ScalarExpression expr) + { + while (expr is ParenthesisExpression pe) + { + expr = pe.Expression; + } + + return expr; + } + + private void DetectConvertibleFunctionCall(ColumnReferenceExpression col, ScalarExpression arg, ScalarExpression alt) + { + if (col?.MultiPartIdentifier is null) + { + // e.g. $action system col + return; + } + + if (!(alt is ColumnReferenceExpression altCol) || altCol.MultiPartIdentifier is null) + { + return; + } + + if (!(arg is VariableReference varRef)) + { + return; + } + + string filteredColumnReference = col.GetFullName(); + if (!filteredColumnReference.Equals(altCol.GetFullName(), StringComparison.OrdinalIgnoreCase)) + { + // some other column referenced on the right side of comparison + return; + } + + Callback(varRef, $"({filteredColumnReference} = {varRef.Name} OR {varRef.Name} IS NULL)"); + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Performance/OptionalParameterIsNullPredicateRule.cs b/TeamTools.TSQL.Linter/Rules/Performance/OptionalParameterIsNullPredicateRule.cs new file mode 100644 index 00000000..612fa033 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Performance/OptionalParameterIsNullPredicateRule.cs @@ -0,0 +1,20 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using TeamTools.Common.Linting; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("PF0877", "EXPAND_ISNULL_FOR_OPTIONAL_ARG")] + internal sealed partial class OptionalParameterIsNullPredicateRule : AbstractRule + { + private readonly TSqlFragmentVisitor badPredicateDetector; + + public OptionalParameterIsNullPredicateRule() : base() + { + badPredicateDetector = new BadPredicateDetector(ViolationHandlerWithMessage); + } + + public override void Visit(QualifiedJoin node) => node.SearchCondition.Accept(badPredicateDetector); + + public override void Visit(WhereClause node) => node.SearchCondition?.Accept(badPredicateDetector); + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Performance/SerialPlanForcedByTableVariableRule.cs b/TeamTools.TSQL.Linter/Rules/Performance/SerialPlanForcedByTableVariableRule.cs new file mode 100644 index 00000000..6ad0e27a --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Performance/SerialPlanForcedByTableVariableRule.cs @@ -0,0 +1,138 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + // TODO : Not sure if MERGE should be supported by this rule + // TODO : DELETE @tbl, UPDATE @tbl via alias + // Table variable modification does not support parallelization + [RuleIdentity("PF0873", "SERIAL_PLAN_FORCED_TABLE_VAR")] + internal sealed class SerialPlanForcedByTableVariableRule : AbstractRule + { + public override void Visit(OutputIntoClause node) => DetectTableVariableModification(node.IntoTable); + + public override void Visit(InsertSpecification node) + { + if (node.InsertSource is ValuesInsertSource) + { + // Item by item INSERT-VALUES does not need parallelization + return; + } + + if (node.InsertSource is SelectInsertSource sel + && sel.Select is QuerySpecification q + && IsSimpleSource(q.FromClause)) + { + return; + } + + DetectTableVariableModification(node.Target); + } + + public override void Visit(UpdateDeleteSpecificationBase node) + { + if (IsSimpleSource(node.FromClause)) + { + return; + } + + DetectTableVariableModification(node.Target); + } + + // Natively compiled modules cannot use temp tables thus table variables are the only options + protected override void ValidateBatch(TSqlBatch batch) + { + // CREATE PROC/TRIGGER/FUNC must be the first statement in a batch + var firstStmt = batch.Statements[0]; + if (firstStmt is ProcedureStatementBody proc) + { + if (proc.Options.HasOption(ProcedureOptionKind.NativeCompilation)) + { + return; + } + } + else if (firstStmt is TriggerStatementBody trg) + { + if (trg.Options.HasOption(TriggerOptionKind.NativeCompile)) + { + return; + } + } + else if (firstStmt is FunctionStatementBody fn) + { + if (fn.Options.HasOption(FunctionOptionKind.NativeCompilation)) + { + return; + } + } + + // If not natively compiled then lets detect violations + batch.AcceptChildren(this); + } + + private static bool IsSimpleSource(FromClause from) + { + if (from?.TableReferences is null + || from.TableReferences.Count == 0) + { + // Select without source + return true; + } + + if (from.TableReferences.Count > 1) + { + return false; + } + + var src = from.TableReferences[0]; + + if (src is VariableTableReference) + { + // Select from the same or another table variable + return true; + } + + if (src is OpenJsonTableReference) + { + // OPENJSON is kinda scalar thing + return true; + } + + if (src is OpenXmlTableReference) + { + // OPENXML is kinda scalar thing + return true; + } + + if (src is GlobalFunctionTableReference) + { + // e.g. STRING_SPLIT + return true; + } + + if (src is NamedTableReference tbl && tbl.SchemaObject.SchemaIdentifier is null) + { + // Moving data from a temp table, INSERTED and DELETED system tables + // is unlikely to be parallelized in SELECT part + string tblName = tbl.SchemaObject.BaseIdentifier.Value; + + if (tblName.StartsWith(TSqlDomainAttributes.TempTablePrefix) + || TSqlDomainAttributes.IsTriggerSystemTable(tblName)) + { + return true; + } + } + + return false; + } + + private void DetectTableVariableModification(TableReference target) + { + if (target is VariableTableReference varRef) + { + HandleNodeError(varRef, varRef.Variable.Name); + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Performance/SerialPlanForcedRule.cs b/TeamTools.TSQL.Linter/Rules/Performance/SerialPlanForcedRule.cs new file mode 100644 index 00000000..28350ea8 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Performance/SerialPlanForcedRule.cs @@ -0,0 +1,40 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using TeamTools.Common.Linting; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("PF0872", "SERIAL_PLAN_FORCED")] + internal sealed class SerialPlanForcedRule : AbstractRule + { + public SerialPlanForcedRule() : base() + { + } + + public override void Visit(OptimizerHint node) + { + if (node.HintKind == OptimizerHintKind.MaxDop + && node is LiteralOptimizerHint maxdop + && int.TryParse(maxdop.Value.Value, out int maxdopValue) + && maxdopValue == 1) + { + // MAXDOP 1 means no parallelization + HandleNodeError(node, "MAXDOP 1"); + } + } + + public override void Visit(QualifiedJoin node) + { + if (node.JoinHint == JoinHint.Remote) + { + // REMOTE means no parallelization + HandleNodeError(node, "REMOTE"); + } + } + + public override void Visit(OutputClause node) + { + // DML OUTPUT to client means no parallelization + HandleNodeError(node, "OUTPUT"); + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Performance/SerialPlanZoneForcedRule.cs b/TeamTools.TSQL.Linter/Rules/Performance/SerialPlanZoneForcedRule.cs new file mode 100644 index 00000000..b6e695cc --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Performance/SerialPlanZoneForcedRule.cs @@ -0,0 +1,57 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using TeamTools.Common.Linting; + +namespace TeamTools.TSQL.Linter.Rules +{ + [CompatibilityLevel(SqlVersion.Sql100, SqlVersion.Sql130)] + [RuleIdentity("PF0874", "SERIAL_PLAN_ZONE_FORCED")] + internal sealed class SerialPlanZoneForcedRule : AbstractRule + { + public SerialPlanZoneForcedRule() : base() + { + } + + public override void Visit(QualifiedJoin node) + { + if (node.JoinHint == JoinHint.Loop) + { + HandleNodeError(node, "LOOP JOIN"); + } + } + + public override void Visit(TopRowFilter node) + { + HandleNodeError(node, "TOP"); + } + + public override void Visit(OptimizerHint node) + { + if (node.HintKind == OptimizerHintKind.MaxRecursion) + { + HandleNodeError(node, "MAXRECURSION"); + } + } + + public override void Visit(FunctionCall node) + { + if (node.OverClause?.OrderByClause is null) + { + // not a windowed function call + return; + } + + if (IsNonParallelFunction(node.FunctionName.Value)) + { + HandleNodeError(node, "ROW_NUMBER/RANK"); + } + } + + private static bool IsNonParallelFunction(string functionName) + { + return functionName.Equals("ROW_NUMBER", StringComparison.OrdinalIgnoreCase) + || functionName.Equals("RANK", StringComparison.OrdinalIgnoreCase) + || functionName.Equals("STRING_AGG", StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Performance/SingleUserModeZoneForcedRule.cs b/TeamTools.TSQL.Linter/Rules/Performance/SingleUserModeZoneForcedRule.cs new file mode 100644 index 00000000..eb2f6765 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Performance/SingleUserModeZoneForcedRule.cs @@ -0,0 +1,85 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("PF0878", "SINGLE_USER_MODE_ZONE_FORCED")] + internal sealed class SingleUserModeZoneForcedRule : AbstractRule + { + public SingleUserModeZoneForcedRule() : base() + { + } + + public override void Visit(ProcedureReference node) + { + if (node.Name is null) + { + // EXEC @var + return; + } + + if (node.Name.BaseIdentifier.Value.Equals("sp_getapplock", StringComparison.OrdinalIgnoreCase) + && (node.Name.SchemaIdentifier is null + || node.Name.SchemaIdentifier.Value.Equals(TSqlDomainAttributes.SystemSchemaName, StringComparison.OrdinalIgnoreCase))) + { + HandleNodeError(node, "APP LOCK"); + } + } + + public override void Visit(TableHint node) + { + if (node.HintKind == TableHintKind.TabLockX) + { + HandleNodeError(node, "TABLOCKX"); + } + } + + public override void Visit(NamedTableReference node) + { + DetectBadHintCombination(node.TableHints); + } + + public override void Visit(StatementWithCtesAndXmlNamespaces node) + { + for (int i = node.OptimizerHints.Count - 1; i >= 0; i--) + { + if (node.OptimizerHints[i] is TableHintsOptimizerHint hints) + { + DetectBadHintCombination(hints.TableHints); + } + } + } + + // TODO : ForceScan + X-Lock? HoldLock / Serializable? + // TabLock + [UpdLock | XLock] + private void DetectBadHintCombination(IList hints) + { + TableHint tabLock = null; + TableHint badHint = null; + + for (int i = hints.Count - 1; i >= 0; i--) + { + var hint = hints[i]; + + // Ignoring TABLOCKX here because there is a dedicated visitor method above + if (hint.HintKind == TableHintKind.TabLock) + { + tabLock = hint; + } + else if (hint.HintKind == TableHintKind.UpdLock + || hint.HintKind == TableHintKind.XLock) + { + badHint = hint; + } + } + + if (tabLock != null && badHint != null) + { + HandleNodeError(tabLock, badHint.HintKind.ToString()); + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Performance/SubstringPredicateToLikeRule.cs b/TeamTools.TSQL.Linter/Rules/Performance/SubstringPredicateToLikeRule.cs index 1a66b69b..3751e770 100644 --- a/TeamTools.TSQL.Linter/Rules/Performance/SubstringPredicateToLikeRule.cs +++ b/TeamTools.TSQL.Linter/Rules/Performance/SubstringPredicateToLikeRule.cs @@ -15,7 +15,7 @@ public SubstringPredicateToLikeRule() : base() visitor = new LikeCandidateVisitor(ViolationHandlerWithMessage); } - public override void Visit(WhereClause node) => node.SearchCondition.AcceptChildren(visitor); + public override void Visit(WhereClause node) => node.SearchCondition?.AcceptChildren(visitor); public override void Visit(QualifiedJoin node) => node.SearchCondition.AcceptChildren(visitor); diff --git a/TeamTools.TSQL.Linter/Rules/Performance/TempTableCachingRule.OptionDetector.cs b/TeamTools.TSQL.Linter/Rules/Performance/TempTableCachingRule.OptionDetector.cs new file mode 100644 index 00000000..17194d00 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Performance/TempTableCachingRule.OptionDetector.cs @@ -0,0 +1,21 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System.Collections.Generic; + +namespace TeamTools.TSQL.Linter.Rules +{ + internal partial class TempTableCachingRule + { + private void DetectBadProcOption(IList options) + { + for (int i = options.Count - 1; i >= 0; i--) + { + var opt = options[i]; + if (opt.OptionKind == ProcedureOptionKind.Recompile) + { + HandleNodeError(opt, "RECOMPILE"); + return; + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Performance/TempTableCachingRule.TableVisitor.cs b/TeamTools.TSQL.Linter/Rules/Performance/TempTableCachingRule.TableVisitor.cs new file mode 100644 index 00000000..6049c4fc --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Performance/TempTableCachingRule.TableVisitor.cs @@ -0,0 +1,110 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + internal partial class TempTableCachingRule + { + // Reasons for non-cacheable temp tables: + // - temp table name reused + // - named constraint + // - DDL over table after creation including + // - alter table + // - create index + // - create statistics + // Docs: + // - https://www.sql.kiwi/2012/08/temporary-object-caching-explained/ + // - https://learn.microsoft.com/en-us/shows/sql-workshops/temp-table-caching-in-sql-server + private sealed class TempTableVisitor : TSqlFragmentVisitor + { + public TempTableVisitor(Action callback) + { + Callback = callback; + } + + public HashSet TempTables { get; } = new HashSet(StringComparer.OrdinalIgnoreCase); + + private Action Callback { get; } + + // Table variables are very similar to temp tables and can be cached as well + public override void ExplicitVisit(DeclareTableVariableBody node) + { + RegisterTempTable(node.VariableName); + node.Definition.AcceptChildren(this); + } + + public override void ExplicitVisit(CreateTableStatement node) + { + if (!IsTempTable(node.SchemaObjectName)) + { + return; + } + + RegisterTempTable(node.SchemaObjectName); + node.Definition.AcceptChildren(this); + } + + // ExplicitVisit to prevent visiting ConstraintDefinition defined + // in an ALTER of non-temp tables + public override void ExplicitVisit(AlterTableAddTableElementStatement node) + { + if (!IsTempTable(node.SchemaObjectName)) + { + return; + } + + DetectTempTableDdl(node.SchemaObjectName); + node.Definition.AcceptChildren(this); + } + + // Visiting any kind of constraint + public override void Visit(ConstraintDefinition node) + { + if (node.ConstraintIdentifier != null) + { + // TODO : move message to resources, add translations + // named constraint + Callback(node.ConstraintIdentifier, "named constraint"); + } + } + + // Any DDL on a temp table prevents its caching + public override void Visit(AlterTableStatement node) => DetectTempTableDdl(node.SchemaObjectName); + + public override void Visit(IndexStatement node) => DetectTempTableDdl(node.OnName); + + public override void Visit(CreateStatisticsStatement node) => DetectTempTableDdl(node.OnName); + + private static bool IsTempTable(SchemaObjectName tableName) + { + return tableName.BaseIdentifier.Value.StartsWith(TSqlDomainAttributes.TempTablePrefix); + } + + private void DetectTempTableDdl(SchemaObjectName tableName) + { + if (IsTempTable(tableName)) + { + Callback(tableName, "DDL"); + } + } + + private void RegisterTempTable(Identifier tableName) + { + // temp table name reuse is not allowed so no check for existence in the collection + TempTables.Add(tableName.Value); + } + + private void RegisterTempTable(SchemaObjectName tableName) + { + if (!TempTables.Add(tableName.BaseIdentifier.Value)) + { + // TODO : move message to resources, add translations + // name reused + Callback(tableName, "name reused"); + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Performance/TempTableCachingRule.cs b/TeamTools.TSQL.Linter/Rules/Performance/TempTableCachingRule.cs new file mode 100644 index 00000000..848e81aa --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Performance/TempTableCachingRule.cs @@ -0,0 +1,62 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("PF0879", "TEMP_TABLE_CACHING_PREVENTED")] + internal sealed partial class TempTableCachingRule : AbstractRule + { + public TempTableCachingRule() : base() + { + } + + protected override void ValidateBatch(TSqlBatch batch) + { + // CREATE PROC/TRIGGER/FUNC must be the first statement in a batch + var firstStmt = batch.Statements[0]; + if (firstStmt is ProcedureStatementBody proc) + { + ValidateProc(proc); + } + else if (firstStmt is TriggerStatementBody trg) + { + ValidateTrigger(trg); + } + } + + private void ValidateProc(ProcedureStatementBody node) + { + if ((node.StatementList?.Statements?.Count ?? 0) == 0) + { + // external, no body + return; + } + + var tempTableVisitor = MakeTempTableVisitor(); + node.StatementList.AcceptChildren(tempTableVisitor); + + if (tempTableVisitor.TempTables.Count > 0) + { + DetectBadProcOption(node.Options); + } + } + + private void ValidateTrigger(TriggerStatementBody node) + { + if ((node.StatementList?.Statements?.Count ?? 0) == 0) + { + // external, no body + return; + } + + var tempTableVisitor = MakeTempTableVisitor(); + node.StatementList.AcceptChildren(tempTableVisitor); + } + + private TempTableVisitor MakeTempTableVisitor() + { + return new TempTableVisitor(ViolationHandlerWithMessage); + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Redundancy/RedundantIndexFilterRule.cs b/TeamTools.TSQL.Linter/Rules/Redundancy/RedundantIndexFilterRule.cs index db7eccb3..577e8d73 100644 --- a/TeamTools.TSQL.Linter/Rules/Redundancy/RedundantIndexFilterRule.cs +++ b/TeamTools.TSQL.Linter/Rules/Redundancy/RedundantIndexFilterRule.cs @@ -116,7 +116,7 @@ private static IEnumerable> Extract if (colFilter.FirstExpression is ColumnReferenceExpression colRef) { - string colName = colRef.MultiPartIdentifier.Identifiers.GetFullName(); + string colName = colRef.GetFullName(); yield return new KeyValuePair(colName, colFilter); } } diff --git a/TeamTools.TSQL.Linter/Rules/Redundancy/RedundantIsNullForInequalityRule.cs b/TeamTools.TSQL.Linter/Rules/Redundancy/RedundantIsNullForInequalityRule.cs new file mode 100644 index 00000000..4b371c73 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Redundancy/RedundantIsNullForInequalityRule.cs @@ -0,0 +1,54 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("RD0883", "REDUNDANT_ISNULL_NOT_EQUALS")] + internal sealed class RedundantIsNullForInequalityRule : AbstractRule + { + private static readonly string IsNullFunction = "ISNULL"; + + public RedundantIsNullForInequalityRule() : base() + { + } + + public override void Visit(BooleanComparisonExpression node) + { + if (node.ComparisonType == BooleanComparisonType.Equals + || node.ComparisonType == BooleanComparisonType.GreaterThanOrEqualTo + || node.ComparisonType == BooleanComparisonType.LessThanOrEqualTo + || node.ComparisonType == BooleanComparisonType.NotLessThan + || node.ComparisonType == BooleanComparisonType.NotGreaterThan + || node.ComparisonType == BooleanComparisonType.LeftOuterJoin + || node.ComparisonType == BooleanComparisonType.RightOuterJoin) + { + // Possible equality is not our case + return; + } + + var left = node.FirstExpression.ExtractScalarExpression(); + var right = node.SecondExpression.ExtractScalarExpression(); + + if (left is FunctionCall) + { + (left, right) = (right, left); + } + + if (!(right is FunctionCall isnull + && isnull.FunctionName.Value.Equals(IsNullFunction, StringComparison.OrdinalIgnoreCase) + && isnull.Parameters.Count == 2)) + { + // No ISNULL on the right side of comparison + return; + } + + if (BooleanExpressionComparer.AreEqualExpressions(left, isnull.Parameters[1].ExtractScalarExpression())) + { + // x <> ISNULL(y, x) + HandleNodeError(isnull); + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Redundancy/RedundantMaxRecursionRule.cs b/TeamTools.TSQL.Linter/Rules/Redundancy/RedundantMaxRecursionRule.cs new file mode 100644 index 00000000..8d765f39 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Redundancy/RedundantMaxRecursionRule.cs @@ -0,0 +1,94 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + // Opposite of RecursiveCteMaxRecursionRule + [RuleIdentity("RD0882", "REDUNDANT_MAX_RECURSION")] + internal sealed class RedundantMaxRecursionRule : AbstractRule + { + public RedundantMaxRecursionRule() : base() + { + } + + public override void Visit(StatementWithCtesAndXmlNamespaces node) + { + var hint = GetMaxRecursionHint(node.OptimizerHints); + if (hint is null) + { + // no MAXRECURSION + return; + } + + if (IsRecursive(node.WithCtesAndXmlNamespaces?.CommonTableExpressions)) + { + return; + } + + HandleNodeError(hint); + } + + private static OptimizerHint GetMaxRecursionHint(IList hints) + { + for (int i = hints.Count - 1; i >= 0; i--) + { + var hint = hints[i]; + if (hint.HintKind == OptimizerHintKind.MaxRecursion) + { + return hint; + } + } + + return default; + } + + private static bool IsRecursive(CommonTableExpression cte) + { + var selfVisitor = new SelfReferenceVisitor(cte.ExpressionName.Value); + cte.AcceptChildren(selfVisitor); + + return selfVisitor.Detected; + } + + private static bool IsRecursive(IList ctes) + { + if (ctes is null) + { + return false; + } + + for (int i = ctes.Count - 1; i >= 0; i--) + { + if (IsRecursive(ctes[i])) + { + return true; + } + } + + return false; + } + + // Copy from RecursiveCteMaxRecursionRule + private sealed class SelfReferenceVisitor : TSqlViolationDetector + { + private readonly string selfName; + + public SelfReferenceVisitor(string selfName) + { + this.selfName = selfName; + } + + public override void Visit(NamedTableReference node) + { + if (node.SchemaObject.SchemaIdentifier is null + && node.SchemaObject.BaseIdentifier.Value.Equals(selfName, StringComparison.OrdinalIgnoreCase)) + { + MarkDetected(node); + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Redundancy/RedundantSelectScalarRule.cs b/TeamTools.TSQL.Linter/Rules/Redundancy/RedundantSelectScalarRule.cs index 3b94cafa..bc50a4a9 100644 --- a/TeamTools.TSQL.Linter/Rules/Redundancy/RedundantSelectScalarRule.cs +++ b/TeamTools.TSQL.Linter/Rules/Redundancy/RedundantSelectScalarRule.cs @@ -108,16 +108,13 @@ public override void Visit(BinaryQueryExpression node) IgnoreSelectScalarFromUnion(node); } - public override void Visit(UnqualifiedJoin node) + public override void Visit(QueryDerivedTable node) { - // applies improve DRY sometimes - if (!(node.SecondTableReference is QueryDerivedTable q - && q.QueryExpression is QuerySpecification spec)) + // APPLY or subquery improve DRY sometimes + if (node.QueryExpression is QuerySpecification spec) { - return; + DoIgnoreSelectedElements(spec.SelectElements); } - - DoIgnoreSelectedElements(spec.SelectElements); } private static ScalarExpression ExtractSelectScalarExpressionIfAny(TSqlFragment node, bool diveIntoSubQueries = true) diff --git a/TeamTools.TSQL.Linter/Rules/Redundancy/UnusedWindowClauseRule.cs b/TeamTools.TSQL.Linter/Rules/Redundancy/UnusedWindowClauseRule.cs new file mode 100644 index 00000000..fb73b2ac --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Redundancy/UnusedWindowClauseRule.cs @@ -0,0 +1,101 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("RD0452", "UNUSED_WINDOW_CLAUSE")] + [CompatibilityLevel(SqlVersion.Sql160)] + internal sealed class UnusedWindowClauseRule : AbstractRule + { + public UnusedWindowClauseRule() : base() + { + } + + public override void Visit(QuerySpecification node) + { + var definedWindows = ExtractWindowNames(node.WindowClause?.WindowDefinition); + + if (definedWindows is null || definedWindows.Count == 0) + { + return; + } + + RemoveUsedWindows(node.WindowClause.WindowDefinition, definedWindows); + RemoveUsedWindows(node.SelectElements, definedWindows); + + if (definedWindows.Count == 0) + { + return; + } + + foreach (var w in definedWindows) + { + HandleNodeError(w.Value, w.Key); + } + } + + private static Dictionary ExtractWindowNames(IList windows) + { + if (windows is null) + { + return default; + } + + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + + for (int i = windows.Count - 1; i >= 0; i--) + { + var windowName = windows[i].WindowName; + result.Add(windowName.Value, windowName); + } + + return result; + } + + private static void RemoveUsedWindows(IList allWindows, Dictionary definedWindows) + { + for (int i = allWindows.Count - 1; i >= 0; i--) + { + var referencedWindowName = allWindows[i].RefWindowName; + if (referencedWindowName != null) + { + definedWindows.Remove(referencedWindowName.Value); + } + } + } + + private static void RemoveUsedWindows(IList selectedItems, Dictionary definedWindows) + { + var visitor = new OverClauseVisitor(definedWindows); + for (int i = selectedItems.Count - 1; i >= 0; i--) + { + if (selectedItems[i] is SelectScalarExpression selExpr + && !(selExpr.Expression is Literal || selExpr.Expression is VariableReference)) + { + selExpr.Expression.Accept(visitor); + } + } + } + + private sealed class OverClauseVisitor : TSqlFragmentVisitor + { + private readonly Dictionary definedWindows; + + public OverClauseVisitor(Dictionary definedWindows) + { + this.definedWindows = definedWindows; + } + + public override void Visit(OverClause node) + { + if (node.WindowName != null) + { + definedWindows.Remove(node.WindowName.Value); + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Simplification/AskingPropertyNotTypeRule.cs b/TeamTools.TSQL.Linter/Rules/Simplification/AskingPropertyNotTypeRule.cs new file mode 100644 index 00000000..d961f184 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Simplification/AskingPropertyNotTypeRule.cs @@ -0,0 +1,72 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("SI0889", "USE_TYPE_NOT_PROPERTY")] + internal sealed partial class AskingPropertyNotTypeRule : AbstractRule + { + // docs: https://learn.microsoft.com/en-us/sql/t-sql/functions/objectpropertyex-transact-sql?view=sql-server-ver17#property + private static readonly Dictionary PropertiesForObjectTypes = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "IsInlineFunction", "IF" }, + { "IsProcedure", "P" }, + { "IsScalarFunction", "FN" }, + { "IsTable", "U" }, + { "IsTableFunction", "TF" }, + { "IsTrigger", "TR" }, + { "IsView", "V" }, + }; + + public AskingPropertyNotTypeRule() : base() + { + } + + public override void Visit(FunctionCall node) + { + if (!node.FunctionName.Value.Equals("OBJECTPROPERTY", StringComparison.OrdinalIgnoreCase) + && !node.FunctionName.Value.Equals("OBJECTPROPERTYEX", StringComparison.OrdinalIgnoreCase)) + { + return; + } + + if (node.Parameters.Count != 2) + { + return; + } + + var propertyName = node.Parameters[1].ExtractScalarExpression(); + + if (!(propertyName is StringLiteral s + && PropertiesForObjectTypes.TryGetValue(s.Value, out var objectType))) + { + // not supported property name + return; + } + + var firstArg = ExpandExpression(node.Parameters[0]); + + if (!(firstArg is FunctionCall func + && func.FunctionName.Value.Equals("OBJECT_ID", StringComparison.OrdinalIgnoreCase))) + { + // first arg is not OBJECT_ID() call + return; + } + + HandleNodeError(node, objectType); + } + + private static ScalarExpression ExpandExpression(ScalarExpression expr) + { + while (expr is ParenthesisExpression pe) + { + expr = pe.Expression; + } + + return expr; + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Simplification/ExtractExpressionRule.Visitor.cs b/TeamTools.TSQL.Linter/Rules/Simplification/ExtractExpressionRule.Visitor.cs new file mode 100644 index 00000000..1cf02289 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Simplification/ExtractExpressionRule.Visitor.cs @@ -0,0 +1,125 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + internal partial class ExtractExpressionRule + { + private sealed class ComplexExpressionVisitor : VisitorWithCallback + { + // TODO : load all built-in functions from metadata? + private static readonly HashSet IgnorableFunctions = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "ERROR_LINE", + "ERROR_MESSAGE", + "ERROR_NUMBER", + "ERROR_PROCEDURE", + "ERROR_SEVERITY", + "ERROR_STATE", + "GETDATE", + "GETUTCDATE", + "ISNULL", + "SYSDATETIME", + }; + + private readonly HashSet complexExpressions = new HashSet(StringComparer.OrdinalIgnoreCase); + + public ComplexExpressionVisitor(Action callback) : base(callback) + { } + + // Explicit - to avoid unnecessary nested expression processing (see Validate details) + public override void ExplicitVisit(BinaryExpression node) => Validate(node); + + public override void ExplicitVisit(FunctionCall node) => Validate(node); + + public override void ExplicitVisit(SearchedCaseExpression node) => Validate(node); + + public override void ExplicitVisit(SimpleCaseExpression node) => Validate(node); + + // Derived subqueries have different scope and their expressions should be ignored + public override void ExplicitVisit(ScalarSubquery node) + { } + + public override void ExplicitVisit(QueryDerivedTable node) + { } + + private static ScalarExpression DetectComplexExpression(ScalarExpression src) + { + if (src is null) + { + return default; + } + + while (src is ParenthesisExpression pe) + { + src = pe.Expression; + } + + // Some functions seem to be trivial enough. + // Also some built-in functions are not derived from FunctionCall (e.g. CastCall) + // and will be ignored as well. + if (src is FunctionCall func + && !IgnorableFunctions.Contains(func.FunctionName.Value)) + { + return func; + } + + // Except trivial, very short expressions + if (src is BinaryExpression bin + && (bin.LastTokenIndex - bin.FirstTokenIndex) > 5) + { + return bin; + } + + if (src is SimpleCaseExpression simpleCase + && simpleCase.WhenClauses.Count > 1) + { + return simpleCase; + } + + if (src is SearchedCaseExpression searchCase + && searchCase.WhenClauses.Count > 1) + { + return searchCase; + } + + return default; + } + + private void Validate(ScalarExpression node) + { + // If external (more complex) expression can be reused then no need to go deeper + // to internal (less complex) nested expressions + if (ExpressionContainsExtractableParts(node)) + { + node.AcceptChildren(this); + } + } + + private bool ExpressionContainsExtractableParts(ScalarExpression expr) + { + var complexExpression = DetectComplexExpression(expr); + if (complexExpression is null) + { + // Not a complex expression + return false; + } + + if (complexExpressions.Add(complexExpression.GetFragmentCleanedText())) + { + // Expression is complex but new - going deeper + return true; + } + + Callback(expr); + + // Expression is complex but it is a dup and should be extracted + // no need to analyze nested expressions + return false; + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Simplification/ExtractExpressionRule.cs b/TeamTools.TSQL.Linter/Rules/Simplification/ExtractExpressionRule.cs new file mode 100644 index 00000000..1368896f --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Simplification/ExtractExpressionRule.cs @@ -0,0 +1,28 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using TeamTools.Common.Linting; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("SI0455", "EXTRACT_EXPRESSION")] + internal sealed partial class ExtractExpressionRule : AbstractRule + { + public ExtractExpressionRule() : base() + { + } + + public override void Visit(QuerySpecification node) + { + var visitor = new ComplexExpressionVisitor(ViolationHandler); + + for (int i = 0, n = node.SelectElements.Count; i < n; i++) + { + node.SelectElements[i].Accept(visitor); + } + + // Detecting expression repeats in other clauses + node.FromClause?.Accept(visitor); + node.GroupByClause?.Accept(visitor); + node.WhereClause?.Accept(visitor); + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Simplification/ExtractWindowClauseRule.Visitor.cs b/TeamTools.TSQL.Linter/Rules/Simplification/ExtractWindowClauseRule.Visitor.cs new file mode 100644 index 00000000..63a56c2d --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Simplification/ExtractWindowClauseRule.Visitor.cs @@ -0,0 +1,204 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using System.Text; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + internal partial class ExtractWindowClauseRule + { + private sealed class OverClauseVisitor : VisitorWithCallback + { + private Dictionary windows; + + public OverClauseVisitor(Action callback) : base(callback) + { } + + public override void Visit(OverClause node) + { + if (node.WindowName != null) + { + // already using external window definition referenced by name + return; + } + + string windowDefinition = GetWindowDefinition(node); + + if (string.IsNullOrEmpty(windowDefinition)) + { + return; + } + + if (windows is null) + { + // Postponed creation because not every query has an OVER clause + windows = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + if (!windows.TryAdd(windowDefinition, node)) + { + Callback(node); + } + } + + // To prevent detection of windows already extrected to WINDOW clause + public override void ExplicitVisit(WindowDefinition node) + { } + + // To prevent diving into nested subqueries which have different scope + public override void ExplicitVisit(QueryDerivedTable node) + { } + + public override void ExplicitVisit(QueryParenthesisExpression node) + { } + + public override void ExplicitVisit(ScalarSubquery node) + { } + + private static string GetWindowDefinition(OverClause node) + { + var sb = new StringBuilder(); + + if (node.Partitions != null && node.Partitions.Count > 0) + { + AppendPartitionsInfo(sb, node.Partitions); + } + + if (node.OrderByClause != null) + { + AppendOrderByInfo(sb, node.OrderByClause.OrderByElements); + } + + if (node.WindowFrameClause != null) + { + AppendWindowFrameInfo(sb, node.WindowFrameClause); + } + + return sb.ToString(); + } + + private static void AppendOrderByInfo(StringBuilder sb, IList orderBy) + { + if (sb.Length > 0) + { + // ORDER BY can be defined without PARTITION BY clause before it + sb.Append(' '); + } + + sb.Append("ORDER BY"); + + for (int i = 0, n = orderBy.Count; i < n; i++) + { + var orderedElement = orderBy[i]; + if (i > 0) + { + sb.Append(','); + } + + sb.Append(' '); + + sb.Append(GetExpressionText(orderedElement.Expression)); + + // ASC is the default and can be omitted + if (orderedElement.SortOrder == SortOrder.Descending) + { + sb.Append(" DESC"); + } + } + } + + private static void AppendPartitionsInfo(StringBuilder sb, IList partitions) + { + sb.Append("PARTITION BY"); + + for (int i = 0, n = partitions.Count; i < n; i++) + { + var partitionby = partitions[i]; + if (i > 0) + { + sb.Append(','); + } + + sb.Append(' '); + + sb.Append(GetExpressionText(partitionby)); + } + } + + private static void AppendWindowFrameInfo(StringBuilder sb, WindowFrameClause windowFrame) + { + if (windowFrame.WindowFrameType == WindowFrameType.Range + && windowFrame.Top.WindowDelimiterType == WindowDelimiterType.UnboundedPreceding + && windowFrame.Bottom.WindowDelimiterType == WindowDelimiterType.CurrentRow) + { + // The default frame declaration can be omitted: + // RANGE BETWEEN UNBOUND PRECEDING AND CURRENT ROW + return; + } + + if (windowFrame.WindowFrameType == WindowFrameType.Range) + { + sb.Append(" RANGE "); + } + else + { + sb.Append(" ROWS "); + } + + if (windowFrame.Bottom != null) + { + sb.Append("BETWEEN "); + } + + sb.Append(WindowDelimiterTypeToString(windowFrame.Top.WindowDelimiterType, windowFrame.Top.OffsetValue)); + + if (windowFrame.Bottom != null) + { + sb.Append(" AND "); + sb.Append(WindowDelimiterTypeToString(windowFrame.Bottom.WindowDelimiterType, windowFrame.Bottom.OffsetValue)); + } + } + + private static string WindowDelimiterTypeToString(WindowDelimiterType delim, ScalarExpression offset) + { + switch (delim) + { + case WindowDelimiterType.UnboundedPreceding: + return "UNBOUNDED PRECEDING"; + + case WindowDelimiterType.UnboundedFollowing: + return "UNBOUNDED FOLLOWING"; + + case WindowDelimiterType.CurrentRow: + return "CURRENT ROW"; + + case WindowDelimiterType.ValuePreceding: + return GetExpressionText(offset) + " PRECEDING"; + + case WindowDelimiterType.ValueFollowing: + return GetExpressionText(offset) + " FOLLOWING"; + + default: + return ""; + } + } + + private static string GetExpressionText(ScalarExpression node) + { + if (node is ColumnReferenceExpression c) + { + if (c.Collation is null) + { + return c.GetFullName(); + } + + return c.GetFullName() + " " + c.Collation.Value; + } + + return node.GetFragmentCleanedText(); + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Simplification/ExtractWindowClauseRule.cs b/TeamTools.TSQL.Linter/Rules/Simplification/ExtractWindowClauseRule.cs new file mode 100644 index 00000000..01bda007 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Simplification/ExtractWindowClauseRule.cs @@ -0,0 +1,21 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using System.Text; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("SI0453", "EXTRACT_WINDOW_CLAUSE")] + [CompatibilityLevel(SqlVersion.Sql160)] + internal sealed partial class ExtractWindowClauseRule : AbstractRule + { + public ExtractWindowClauseRule() : base() + { + } + + public override void Visit(QuerySpecification node) + => node.AcceptChildren(new OverClauseVisitor(ViolationHandler)); + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Simplification/MultipleInsertValuesIntoOneRule.cs b/TeamTools.TSQL.Linter/Rules/Simplification/MultipleInsertValuesIntoOneRule.cs new file mode 100644 index 00000000..4e2ecedc --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Simplification/MultipleInsertValuesIntoOneRule.cs @@ -0,0 +1,97 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("SI0859", "MULTIPLE_INSERT_VALUES_COLLAPSE")] + internal sealed class MultipleInsertValuesIntoOneRule : AbstractRule + { + // Too long statements dont look fine + private static readonly int MaxRowsTogether = 40; + + // Sometimes inserts are divided into separate statements: + // with short list of target cols and values and with the long one. + private static readonly int MaxColumnNumberDifference = 3; + + public MultipleInsertValuesIntoOneRule() : base() + { + } + + // ScriptDom does not fire Visit for StatementList itself nevertheless it is a descendant of TSqlFragment + public override void Visit(BeginEndBlockStatement node) => Validate(node.StatementList); + + public override void Visit(TryCatchStatement node) => Validate(node.TryStatements); + + protected override void ValidateBatch(TSqlBatch node) + { + Validate(node.Statements); + + // Visit nested blocks + node.Accept(this); + } + + private static string GetTableName(TableReference tbl) + { + if (tbl is VariableTableReference var) + { + return var.Variable.Name; + } + + if (tbl is NamedTableReference name) + { + return name.SchemaObject.GetFullName(); + } + + return default; + } + + private void Validate(StatementList node) + { + if ((node.Statements?.Count ?? 0) == 0) + { + return; + } + + Validate(node.Statements); + } + + private void Validate(IList statements) + { + string lastInsertTarget = null; + int lastInsertValuesCount = 0; + + for (int i = 0, n = statements.Count; i < n; i++) + { + if (statements[i] is InsertStatement ins + && ins.InsertSpecification.InsertSource is ValuesInsertSource val + && val.RowValues.Count > 0 + && val.RowValues.Count < MaxRowsTogether + && Math.Abs(lastInsertValuesCount - val.RowValues[0].ColumnValues.Count) <= MaxColumnNumberDifference) + { + string targetName = GetTableName(ins.InsertSpecification.Target); + + if (!string.IsNullOrEmpty(lastInsertTarget) + && !string.IsNullOrEmpty(targetName) + && string.Equals(lastInsertTarget, targetName, StringComparison.OrdinalIgnoreCase)) + { + HandleNodeError(ins.InsertSpecification.Target, targetName); + } + else + { + lastInsertTarget = targetName; + } + + lastInsertValuesCount = val.RowValues[0].ColumnValues.Count; + } + else + { + // reset until next sequence of inserts found + lastInsertTarget = null; + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Simplification/MultipleSetOptionIntoOneRule.cs b/TeamTools.TSQL.Linter/Rules/Simplification/MultipleSetOptionIntoOneRule.cs new file mode 100644 index 00000000..8fae969a --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Simplification/MultipleSetOptionIntoOneRule.cs @@ -0,0 +1,77 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("SI0896", "MULTIPLE_SET_INTO_ONE")] + internal sealed class MultipleSetOptionIntoOneRule : AbstractRule + { + public MultipleSetOptionIntoOneRule() : base() + { + } + + protected override void ValidateBatch(TSqlBatch batch) => batch.Accept(new SetVisitor(ViolationHandler)); + + private sealed class SetVisitor : VisitorWithCallback + { + public SetVisitor(Action callback) : base(callback) + { } + + private TSqlFragment LastSetOnNode { get; set; } + + private TSqlFragment LastSetOffNode { get; set; } + + public override void Visit(TSqlBatch node) => Validate(node.Statements); + + public override void Visit(BeginEndBlockStatement node) => Validate(node.StatementList.Statements); + + public override void Visit(TryCatchStatement node) => Validate(node.TryStatements.Statements); + + private void Validate(IList statements) + { + for (int i = 0, n = statements.Count; i < n; i++) + { + if (statements[i] is PredicateSetStatement set) + { + HandleSet(set); + } + else + { + Reset(); + } + } + } + + private void HandleSet(PredicateSetStatement node) + { + if (node.IsOn) + { + if (LastSetOnNode != null) + { + Callback(node); + } + + LastSetOnNode = node; + } + else + { + if (LastSetOffNode != null) + { + Callback(node); + } + + LastSetOffNode = node; + } + } + + private void Reset() + { + LastSetOnNode = null; + LastSetOffNode = null; + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Simplification/ReuseExpressionAliasRule.cs b/TeamTools.TSQL.Linter/Rules/Simplification/ReuseExpressionAliasRule.cs new file mode 100644 index 00000000..46ed33dc --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Simplification/ReuseExpressionAliasRule.cs @@ -0,0 +1,63 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("SI0454", "REUSE_EXPRESSION_ALIAS")] + internal sealed class ReuseExpressionAliasRule : AbstractRule + { + public ReuseExpressionAliasRule() : base() + { + } + + public override void Visit(QuerySpecification node) + { + if (node.OrderByClause is null) + { + return; + } + + DetectReusableExpressions(node.SelectElements, node.OrderByClause.OrderByElements); + } + + // TODO : should it take into account FunctionCall expressions? + private void DetectReusableExpressions(IList selectItems, IList orderByItems) + { + int n = orderByItems.Count; + var sortExpressions = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Detecting complex expressions in ORDER BY clause + for (int i = n - 1; i >= 0; i--) + { + // only somewhat complex expression are allowed + if (BooleanExpressionPartsExtractor.ExtractExpression(orderByItems[i].Expression) is BinaryExpression expr) + { + sortExpressions.Add(expr.GetFragmentCleanedText(), expr); + } + } + + if (sortExpressions.Count == 0) + { + return; + } + + // Detecting similar expressions in SELECT list clause + // which can be reused in ORDER BY via given alias + for (int i = selectItems.Count - 1; i >= 0; i--) + { + if (selectItems[i] is SelectScalarExpression sel + && BooleanExpressionPartsExtractor.ExtractExpression(sel.Expression) is BinaryExpression expr) + { + var exprDefinition = expr.GetFragmentCleanedText(); + if (sortExpressions.TryGetValue(exprDefinition, out var sortElement)) + { + HandleNodeError(sortElement); + } + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Vulnerability/OutputSecretRule.Validation.cs b/TeamTools.TSQL.Linter/Rules/Vulnerability/OutputSecretRule.Validation.cs new file mode 100644 index 00000000..85e4c7e2 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Vulnerability/OutputSecretRule.Validation.cs @@ -0,0 +1,110 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + internal partial class OutputSecretRule + { + private static readonly List SecretNames = new List + { + "password", + "pass_word", + "pwd", + "pswd", + "secret", + }; + + private static bool IsValidOutput(string name) + { + if (string.IsNullOrEmpty(name)) + { + return true; + } + + for (int i = SecretNames.Count - 1; i >= 0; i--) + { + if (name.Contains(SecretNames[i], StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + + return true; + } + + private void ValidateOutputFromQueries(QueryExpression node) + { + if (node is BinaryQueryExpression bin) + { + ValidateOutputFromQueries(bin.FirstQueryExpression); + ValidateOutputFromQueries(bin.SecondQueryExpression); + } + else if (node is QueryParenthesisExpression pe) + { + ValidateOutputFromQueries(pe.QueryExpression); + } + else if (node is QuerySpecification spec) + { + ValidateOutput(spec.SelectElements); + } + } + + private void ValidateOutput(TSqlFragment expr, string outputName) + { + if (!IsValidOutput(outputName)) + { + HandleNodeError(expr, outputName); + } + } + + private void ValidateOutput(Identifier id) + { + if (id is null) + { + return; + } + + ValidateOutput(id, id.Value); + } + + private void ValidateOutput(ScalarExpression expr) + { + if (expr is null) + { + return; + } + + while (expr is ParenthesisExpression pe) + { + expr = pe.Expression; + } + + if (expr is ColumnReferenceExpression col) + { + ValidateOutput(col, col.MultiPartIdentifier?.GetLastIdentifier().Value); + } + + if (expr is VariableReference v) + { + ValidateOutput(v, v.Name); + } + } + + private void ValidateOutput(IList cols) + { + for (int i = cols.Count - 1; i >= 0; i--) + { + // it also can be select-set var or "*" + if (cols[i] is SelectScalarExpression sel + && !(sel.Expression is NullLiteral)) + { + ValidateOutput(sel.Expression); + ValidateOutput(sel.ColumnName?.Identifier); + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Vulnerability/OutputSecretRule.cs b/TeamTools.TSQL.Linter/Rules/Vulnerability/OutputSecretRule.cs new file mode 100644 index 00000000..cf8f39a5 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Vulnerability/OutputSecretRule.cs @@ -0,0 +1,42 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("VU0525", "OUTPUT_SECRET")] + internal sealed partial class OutputSecretRule : AbstractRule + { + public OutputSecretRule() : base() + { + } + + public override void Visit(SelectStatement node) + { + if (node.Into != null) + { + // SELECT-INTO does not provide resultset to a client + return; + } + + ValidateOutputFromQueries(node.QueryExpression); + } + + public override void Visit(ProcedureParameter node) + { + if (node.Modifier != ParameterModifier.Output) + { + // validating OUTPUT parameters + return; + } + + ValidateOutput(node.VariableName); + } + + public override void Visit(OutputClause node) => ValidateOutput(node.SelectColumns); + + public override void Visit(PrintStatement node) => ValidateOutput(node.Expression); + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Vulnerability/ReadingSecretDataRule.cs b/TeamTools.TSQL.Linter/Rules/Vulnerability/ReadingSecretDataRule.cs new file mode 100644 index 00000000..51aeda08 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Vulnerability/ReadingSecretDataRule.cs @@ -0,0 +1,43 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("VU0526", "READ_SECRET")] + internal sealed class ReadingSecretDataRule : AbstractRule + { + private static readonly HashSet PrincipalStorages = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "sys.credentials", + "sys.database_credentials", + "sys.database_permissions", + "sys.database_principals", + "sys.database_role_members", + "sys.key_encryptions", + "sys.login_token", + "sys.master_key_passwords", + "sys.server_permissions", + "sys.server_principals", + "sys.server_role_members", + "sys.sql_logins", + "sys.user_token", + }; + + public ReadingSecretDataRule() : base() + { + } + + public override void Visit(NamedTableReference node) + { + string srcName = node.SchemaObject.GetFullName(); + + if (PrincipalStorages.Contains(srcName)) + { + HandleNodeError(node, srcName); + } + } + } +} diff --git a/TeamTools.TSQL.LinterTests/PluginTests/PluginTests.cs b/TeamTools.TSQL.LinterTests/PluginTests/PluginTests.cs index 21d6eaec..09f414f8 100644 --- a/TeamTools.TSQL.LinterTests/PluginTests/PluginTests.cs +++ b/TeamTools.TSQL.LinterTests/PluginTests/PluginTests.cs @@ -16,16 +16,16 @@ namespace TeamTools.TSQL.LinterTests [Category("Linter.TSQL.PluginTests")] internal sealed class PluginTests { - private static string RuleIdSeparator => RuleIdentityAttribute.IdSeparator; - #if Windows - private const string BaseSrcPath = @"c:\src\"; + private const string BaseSrcPath = @"c:\src\"; #elif Linux - private const string BaseSrcPath = @"/usr/local/src/"; + private const string BaseSrcPath = @"/usr/local/src/"; #else - private const string BaseSrcPath = @"~/src/"; + private const string BaseSrcPath = @"~/src/"; #endif + private static string RuleIdSeparator => RuleIdentityAttribute.IdSeparator; + [Test] public void TestPluginRunDeliversRuleViolations() { diff --git a/TeamTools.TSQL.LinterTests/TeamTools.TSQL.LinterTests.csproj b/TeamTools.TSQL.LinterTests/TeamTools.TSQL.LinterTests.csproj index 85a95a18..79048c40 100644 --- a/TeamTools.TSQL.LinterTests/TeamTools.TSQL.LinterTests.csproj +++ b/TeamTools.TSQL.LinterTests/TeamTools.TSQL.LinterTests.csproj @@ -20,8 +20,8 @@ - - + + diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/SelectNullTypeRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/SelectNullTypeRuleTests.cs new file mode 100644 index 00000000..bc586c4f --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/SelectNullTypeRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Ambiguity")] + [TestOfRule(typeof(SelectNullTypeRule))] + public class SelectNullTypeRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(SelectNullTypeRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..54cbda79 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,26 @@ +WITH cte (title, id) AS +( + SELECT + foo.title, + 1 + FROM foo +) +SELECT + title, id, 1 + 1 +FROM cte +INNER JOIN +( + SELECT + foo.title, + GETDATE() as start_time + FROM foo +) far +on cte.id = far.id + +UPDATE t SET + lastmod = GETDATE() + OUTPUT + INSERTED.lastmod, + DELETED.id, + 1 +FROM tmp AS t diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/cte_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/cte_raise_1_violations.sql new file mode 100644 index 00000000..4711f04b --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/cte_raise_1_violations.sql @@ -0,0 +1,8 @@ +WITH cte AS +( + SELECT + foo.title, + NULL as start_time -- here + FROM foo +) +SELECT * FROM cte diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/derived_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/derived_raise_0_violations.sql new file mode 100644 index 00000000..bfa7c7b4 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/derived_raise_0_violations.sql @@ -0,0 +1,9 @@ +SELECT * FROM bar +INNER JOIN +( + SELECT + foo.title, + NULL as start_time + FROM foo +) far +ON id = id diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/explicit_cast_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/explicit_cast_raise_0_violations.sql new file mode 100644 index 00000000..985974b8 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/explicit_cast_raise_0_violations.sql @@ -0,0 +1,11 @@ +SELECT + foo.title, + CAST(NULL AS DATETIME2(3)) as start_time +FROM foo + +UPDATE t SET + lastmod = GETDATE() + OUTPUT + INSERTED.lastmod, + CONVERT(BIT, NULL) as flag +FROM tmp AS t diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/for_xml_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/for_xml_raise_0_violations.sql new file mode 100644 index 00000000..b0214e25 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/for_xml_raise_0_violations.sql @@ -0,0 +1,9 @@ +SELECT + title, + NULL as parent_id, + ( + SELECT src.group_id, NULL as is_deleted + FOR XML AUTO, TYPE + ) as xml_value +FROM src +FOR XML PATH('node') diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/merge_source_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/merge_source_raise_0_violations.sql new file mode 100644 index 00000000..553b46d2 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/merge_source_raise_0_violations.sql @@ -0,0 +1,9 @@ +MERGE t +USING +( + SELECT NULL as value +) s +ON id = id +WHEN NOT MATCHED THEN +INSERT (value) +VALUES (s.value); diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/output_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/output_raise_1_violations.sql new file mode 100644 index 00000000..e79f949e --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/output_raise_1_violations.sql @@ -0,0 +1,6 @@ +UPDATE t SET + lastmod = GETDATE() + OUTPUT + INSERTED.lastmod, + NULL as flag -- here +FROM tmp AS t diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/select_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/select_raise_1_violations.sql new file mode 100644 index 00000000..34e75a96 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/select_raise_1_violations.sql @@ -0,0 +1,4 @@ +SELECT + foo.title, + (SELECT (NULL)) as start_time -- here +FROM foo diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/union_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/union_raise_0_violations.sql new file mode 100644 index 00000000..fd9b3837 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SelectNullTypeRule/TestSources/union_raise_0_violations.sql @@ -0,0 +1,12 @@ +SELECT + foo.title, + foo.start_time +FROM foo + +UNION ALL + +-- types are provided by the first part in union +SELECT + '' + , NULL +FROM bar diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/TableAliasMimicksOtherTableRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/TableAliasMimicksOtherTableRule/TestSources/all_good_raise_0_violations.sql index 3ad4fe56..5f41af37 100644 --- a/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/TableAliasMimicksOtherTableRule/TestSources/all_good_raise_0_violations.sql +++ b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/TableAliasMimicksOtherTableRule/TestSources/all_good_raise_0_violations.sql @@ -20,3 +20,9 @@ WHERE NOT ( AND bal.turnover = 0 ) AND abl.flag IS NULL; + + +select * +from somedb.schm.tbl as tbl -- alias is the same as the base name - this is fine +inner join anotherdb..anothersource as src +on id = parenta_id diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/TableAliasMimicksOtherTableRule/TestSources/bad_alias_select_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/TableAliasMimicksOtherTableRule/TestSources/bad_alias_select_raise_2_violations.sql index 339c65a0..1c146307 100644 --- a/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/TableAliasMimicksOtherTableRule/TestSources/bad_alias_select_raise_2_violations.sql +++ b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/TableAliasMimicksOtherTableRule/TestSources/bad_alias_select_raise_2_violations.sql @@ -1,6 +1,8 @@ -select t.*, old.id as name, new.name as id -from dbo.tbl as t -inner join t as old - on 1=1 +select new.new_value, old.old_value +from src as old inner join old as new - on 1=1 + on id = id +inner join new as src + on id = id +where new.new_value <> old.old_value +and src.need_check = 1 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/TableAliasMimicksOtherTableRule/TestSources/same_alias_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/TableAliasMimicksOtherTableRule/TestSources/same_alias_raise_0_violations.sql new file mode 100644 index 00000000..ef8d0b0a --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/TableAliasMimicksOtherTableRule/TestSources/same_alias_raise_0_violations.sql @@ -0,0 +1,6 @@ +-- another rule handles this ambiguity +SELECT f.* +FROM foo AS f +INNER JOIN far AS f + on foo.id = far.id +ORDER BY f.id diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/TableAliasMimicksOtherTableRule/TestSources/subquery_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/TableAliasMimicksOtherTableRule/TestSources/subquery_raise_0_violations.sql new file mode 100644 index 00000000..12b97065 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/TableAliasMimicksOtherTableRule/TestSources/subquery_raise_0_violations.sql @@ -0,0 +1,10 @@ +WITH b AS +( + SELECT * FROM a AS b +) +SELECT + b.ID, + (SELECT TOP 1 last_upd FROM b WHERE id = parent_Id ORDER BY last_upd DESC) AS last_upd, + (SELECT c.code FROM b AS c FOR XML PATH) AS codes +FROM b +ORDER BY d.ID diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/CastXmlToStringRule/CastXmlToStringRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/CastXmlToStringRule/CastXmlToStringRuleTests.cs new file mode 100644 index 00000000..2456da52 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/CastXmlToStringRule/CastXmlToStringRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.CodeSmell")] + [TestOfRule(typeof(CastXmlToStringRule))] + public sealed class CastXmlToStringRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(CastXmlToStringRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/CastXmlToStringRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/CastXmlToStringRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..33d82af5 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/CastXmlToStringRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,39 @@ +-- correct approach +SELECT STUFF( + ( + SELECT ', ' + v.name + FROM ( + VALUES + ('bonnie & clyde'), + ('thelma & louise') + )v(NAME) + FOR XML PATH(''), TYPE + ).value('.', 'VARCHAR(MAX)'), 1, 2, '') + +-- FOR XML AUTO +SELECT CAST( + ( + SELECT ', ' + v.name + FROM ( + VALUES + ('bonnie & clyde'), + ('thelma & louise') + )v(NAME) + FOR XML AUTO, ROOT('values')) + AS VARCHAR(1000)) + +-- no FOR XML +SELECT CONVERT(NCHAR(10), (SELECT 1)) + +-- escaped XML is what expected +SELECT '' + + CAST( + ( + SELECT v.name as [td] + FROM ( + VALUES + ('bonnie & clyde'), + ('thelma & louise') + )v(NAME) + FOR XML PATH('tr') + ) AS VARCHAR(100)) + '
' diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/CastXmlToStringRule/TestSources/for_xml_to_string_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/CastXmlToStringRule/TestSources/for_xml_to_string_raise_2_violations.sql new file mode 100644 index 00000000..06f55a4c --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/CastXmlToStringRule/TestSources/for_xml_to_string_raise_2_violations.sql @@ -0,0 +1,21 @@ +SELECT STUFF( + ( + SELECT ', ' + v.name + FROM ( + VALUES + ('bonnie & clyde'), + ('thelma & louise') + )v(NAME) + FOR XML PATH(''), TYPE + ), 1, 2, '') -- 1 + +SELECT CAST( + ( + SELECT ', ' + v.name + FROM ( + VALUES + ('bonnie & clyde'), + ('thelma & louise') + )v(NAME) + FOR XML PATH('') + ) AS VARCHAR(100)) -- 2 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConstraintNameInTempTableRule/TestSources/alter_add_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConstraintNameInTempTableRule/TestSources/alter_add_raise_1_violations.sql new file mode 100644 index 00000000..91774680 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConstraintNameInTempTableRule/TestSources/alter_add_raise_1_violations.sql @@ -0,0 +1,2 @@ +alter table #foo +add constraint PK_FOO primary key clustered (id) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConstraintNameInTempTableRule/TestSources/filestream_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConstraintNameInTempTableRule/TestSources/filestream_raise_0_violations.sql new file mode 100644 index 00000000..8623ad74 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConstraintNameInTempTableRule/TestSources/filestream_raise_0_violations.sql @@ -0,0 +1,3 @@ +-- compatibility level min: 110 +CREATE TABLE files.docs_storage AS FILETABLE FILESTREAM_ON document_filestream_group +WITH (FILETABLE_COLLATE_FILENAME = Cyrillic_General_CI_AS, FILETABLE_DIRECTORY = N'docs_storage'); diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConstraintNameInTempTableRule/TestSources/noname_default_and_pk_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConstraintNameInTempTableRule/TestSources/noname_default_and_pk_raise_0_violations.sql index 25a9fb0c..1eba0bf3 100644 --- a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConstraintNameInTempTableRule/TestSources/noname_default_and_pk_raise_0_violations.sql +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConstraintNameInTempTableRule/TestSources/noname_default_and_pk_raise_0_violations.sql @@ -3,3 +3,5 @@ bar int not null default 1, primary key (bar) ) + +alter table #foo add title varchar(100) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyAddressesMissingColumnRule/ExtendedPropertyAddressesMissingColumnRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyAddressesMissingColumnRule/ExtendedPropertyAddressesMissingColumnRuleTests.cs new file mode 100644 index 00000000..52b31ef0 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyAddressesMissingColumnRule/ExtendedPropertyAddressesMissingColumnRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.CodeSmell")] + [TestOfRule(typeof(ExtendedPropertyAddressesMissingColumnRule))] + public class ExtendedPropertyAddressesMissingColumnRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(ExtendedPropertyAddressesMissingColumnRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyAddressesMissingColumnRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyAddressesMissingColumnRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..ccbd9345 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyAddressesMissingColumnRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,15 @@ +CREATE TABLE foo.bar +( + id INT NOT NULL +) +GO + +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'foo' + , @level1type = N'TABLE' + , @level1name = N'bar' + , @level2type = N'COLUMN' + , @level2name = N'id'; diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyAddressesMissingColumnRule/TestSources/bad_col_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyAddressesMissingColumnRule/TestSources/bad_col_raise_1_violations.sql new file mode 100644 index 00000000..b3c34faf --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyAddressesMissingColumnRule/TestSources/bad_col_raise_1_violations.sql @@ -0,0 +1,15 @@ +CREATE TABLE foo.bar +( + id INT NOT NULL +) +GO + +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'foo' + , @level1type = N'TABLE' + , @level1name = N'bar' + , @level2type = N'COLUMN' + , @level2name = N'bad_name'; -- here diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyAddressesMissingColumnRule/TestSources/misdirected_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyAddressesMissingColumnRule/TestSources/misdirected_raise_0_violations.sql new file mode 100644 index 00000000..56a77b99 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyAddressesMissingColumnRule/TestSources/misdirected_raise_0_violations.sql @@ -0,0 +1,15 @@ +CREATE TABLE foo.bar +( + id INT NOT NULL +) +GO + +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'foo' + , @level1type = N'TABLE' + , @level1name = N'asdfasdf' -- different table name must be ignored + , @level2type = N'COLUMN' + , @level2name = N'bad_name'; diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyAddressesMissingColumnRule/TestSources/no_main_object_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyAddressesMissingColumnRule/TestSources/no_main_object_raise_0_violations.sql new file mode 100644 index 00000000..9332c125 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyAddressesMissingColumnRule/TestSources/no_main_object_raise_0_violations.sql @@ -0,0 +1 @@ +select 1 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyAddressesMissingColumnRule/TestSources/not_create_table_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyAddressesMissingColumnRule/TestSources/not_create_table_raise_0_violations.sql new file mode 100644 index 00000000..638597ad --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyAddressesMissingColumnRule/TestSources/not_create_table_raise_0_violations.sql @@ -0,0 +1,2 @@ +CREATE PROC foo AS; +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyAddressesMissingColumnRule/TestSources/unsupported_exec_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyAddressesMissingColumnRule/TestSources/unsupported_exec_raise_0_violations.sql new file mode 100644 index 00000000..8a41699e --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyAddressesMissingColumnRule/TestSources/unsupported_exec_raise_0_violations.sql @@ -0,0 +1,17 @@ +CREATE PROC foo AS; +GO + +EXEC ('cmd') +GO + +EXEC @var + @arg1 = @val1 +GO + + +EXEC schm.test + @arg1 = @val1 +GO + +EXEC sys.test + @arg1 = @val1 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyDupRule/ExtendedPropertyDupRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyDupRule/ExtendedPropertyDupRuleTests.cs new file mode 100644 index 00000000..1f073488 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyDupRule/ExtendedPropertyDupRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.CodeSmell")] + [TestOfRule(typeof(ExtendedPropertyDupRule))] + public class ExtendedPropertyDupRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(ExtendedPropertyDupRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyDupRule/TestSources/dup_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyDupRule/TestSources/dup_raise_2_violations.sql new file mode 100644 index 00000000..6a016d4d --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyDupRule/TestSources/dup_raise_2_violations.sql @@ -0,0 +1,34 @@ +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'my_schema' + , @level1type = N'TABLE' + , @level1name = N'my_table' + , @level2type = N'COLUMN' + , @level2name = N'my_column'; +GO + +EXEC sp_addextendedproperty + @name = N'MS_Description' -- 1 + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'my_schema' + , @level1type = N'TABLE' + , @level1name = N'my_table' + , @level2type = N'COLUMN' + , @level2name = N'my_column'; +GO + +EXEC sp_addextendedproperty + @name = N'Custom property' + , @value = N'adsf' + , @level0type = N'ASSEMBLY' + , @level0name = N'some_assm' +GO + +EXEC sp_addextendedproperty + @name = N'Custom property' -- 2 + , @value = N'xxx' + , @level0type = N'ASSEMBLY' + , @level0name = N'some_assm' diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyDupRule/TestSources/no_dup_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyDupRule/TestSources/no_dup_raise_0_violations.sql new file mode 100644 index 00000000..44d5265d --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyDupRule/TestSources/no_dup_raise_0_violations.sql @@ -0,0 +1,31 @@ +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'my_schema' + , @level1type = N'TABLE' + , @level1name = N'my_table' + , @level2type = N'COLUMN' + , @level2name = N'my_column'; + +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'my_schema' + , @level1type = N'TABLE' + , @level1name = N'my_table' + , @level2type = N'COLUMN' + , @level2name = N'another_column'; -- different col + +EXEC sp_addextendedproperty + @name = N'Custom property' + , @value = N'adsf' + , @level0type = N'ASSEMBLY' + , @level0name = N'some_assm' + +EXEC sp_addextendedproperty + @name = N'Additional property' -- different property + , @value = N'xxx' + , @level0type = N'ASSEMBLY' + , @level0name = N'some_assm' diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyDupRule/TestSources/unsupported_exec_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyDupRule/TestSources/unsupported_exec_raise_0_violations.sql new file mode 100644 index 00000000..8a41699e --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyDupRule/TestSources/unsupported_exec_raise_0_violations.sql @@ -0,0 +1,17 @@ +CREATE PROC foo AS; +GO + +EXEC ('cmd') +GO + +EXEC @var + @arg1 = @val1 +GO + + +EXEC schm.test + @arg1 = @val1 +GO + +EXEC sys.test + @arg1 = @val1 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyMisdirectedRule/ExtendedPropertyMisdirectedRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyMisdirectedRule/ExtendedPropertyMisdirectedRuleTests.cs new file mode 100644 index 00000000..84bd510d --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyMisdirectedRule/ExtendedPropertyMisdirectedRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.CodeSmell")] + [TestOfRule(typeof(ExtendedPropertyMisdirectedRule))] + public class ExtendedPropertyMisdirectedRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(ExtendedPropertyMisdirectedRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyMisdirectedRule/TestSources/bad_calls_raise_3_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyMisdirectedRule/TestSources/bad_calls_raise_3_violations.sql new file mode 100644 index 00000000..6d8c842f --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyMisdirectedRule/TestSources/bad_calls_raise_3_violations.sql @@ -0,0 +1,32 @@ +CREATE TABLE foo.bar +( + id INT NOT NULL +) +GO + +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'asdf' -- 1 + , @level1type = N'TABLE' + , @level1name = N'bar' + , @level2type = N'COLUMN' + , @level2name = N'id'; +GO + +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'foo' + , @level1type = N'TABLE' + , @level1name = N'adsf' -- 2 + , @level2type = N'COLUMN' + , @level2name = N'id'; +GO + +EXEC sp_addextendedproperty -- 3 + @name = N'MS_Description' + , @value = N'Some description' +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyMisdirectedRule/TestSources/no_create_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyMisdirectedRule/TestSources/no_create_raise_0_violations.sql new file mode 100644 index 00000000..3f60a4db --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyMisdirectedRule/TestSources/no_create_raise_0_violations.sql @@ -0,0 +1,9 @@ +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'my_schema' + , @level1type = N'TABLE' + , @level1name = N'my_table' + , @level2type = N'COLUMN' + , @level2name = N'my_column'; diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyMisdirectedRule/TestSources/no_props_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyMisdirectedRule/TestSources/no_props_raise_0_violations.sql new file mode 100644 index 00000000..1388159f --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyMisdirectedRule/TestSources/no_props_raise_0_violations.sql @@ -0,0 +1,10 @@ +CREATE TYPE foo.bar AS TABLE +( + id INT NOT NULL +) +GO + +PRINT 'test' +GO + +PRINT 'test' diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyMisdirectedRule/TestSources/no_supported_main_object_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyMisdirectedRule/TestSources/no_supported_main_object_raise_0_violations.sql new file mode 100644 index 00000000..7232baf9 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyMisdirectedRule/TestSources/no_supported_main_object_raise_0_violations.sql @@ -0,0 +1,7 @@ +SELECT 1 + +PRINT 'test' +GO + +CREATE QUEUE q; +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyMisdirectedRule/TestSources/not_create_table_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyMisdirectedRule/TestSources/not_create_table_raise_0_violations.sql new file mode 100644 index 00000000..638597ad --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyMisdirectedRule/TestSources/not_create_table_raise_0_violations.sql @@ -0,0 +1,2 @@ +CREATE PROC foo AS; +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyMisdirectedRule/TestSources/unsupported_exec_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyMisdirectedRule/TestSources/unsupported_exec_raise_0_violations.sql new file mode 100644 index 00000000..8a41699e --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyMisdirectedRule/TestSources/unsupported_exec_raise_0_violations.sql @@ -0,0 +1,17 @@ +CREATE PROC foo AS; +GO + +EXEC ('cmd') +GO + +EXEC @var + @arg1 = @val1 +GO + + +EXEC schm.test + @arg1 = @val1 +GO + +EXEC sys.test + @arg1 = @val1 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyMisdirectedRule/TestSources/valid_target_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyMisdirectedRule/TestSources/valid_target_raise_0_violations.sql new file mode 100644 index 00000000..ccbd9345 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ExtendedPropertyMisdirectedRule/TestSources/valid_target_raise_0_violations.sql @@ -0,0 +1,15 @@ +CREATE TABLE foo.bar +( + id INT NOT NULL +) +GO + +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'foo' + , @level1type = N'TABLE' + , @level1name = N'bar' + , @level2type = N'COLUMN' + , @level2name = N'id'; diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IsolationLevelChaosPerQueryRule/IsolationLevelChaosPerQueryRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IsolationLevelChaosPerQueryRule/IsolationLevelChaosPerQueryRuleTests.cs new file mode 100644 index 00000000..3d158ddb --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IsolationLevelChaosPerQueryRule/IsolationLevelChaosPerQueryRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.CodeSmell")] + [TestOfRule(typeof(IsolationLevelChaosPerQueryRule))] + public class IsolationLevelChaosPerQueryRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(IsolationLevelChaosPerQueryRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IsolationLevelChaosPerQueryRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IsolationLevelChaosPerQueryRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..df9585ad --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IsolationLevelChaosPerQueryRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,7 @@ +SELECT * FROM tbl + +SELECT * FROM tbl WITH (TABLOCKX) + +SELECT * FROM foo WITH (HOLDLOCK) +INNER JOIN bar WITH (UPDLOCK) +ON id = parent_id diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IsolationLevelChaosPerQueryRule/TestSources/bad_combination_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IsolationLevelChaosPerQueryRule/TestSources/bad_combination_raise_2_violations.sql new file mode 100644 index 00000000..ea85f4fd --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IsolationLevelChaosPerQueryRule/TestSources/bad_combination_raise_2_violations.sql @@ -0,0 +1,10 @@ +SELECT * FROM foo WITH (HOLDLOCK) +INNER JOIN bar WITH (READPAST) +ON id = parent_id + + +UPDATE foo WITH (TABLOCKX) SET + lastmod = GETDATE() +FROM foo +INNER JOIN bar WITH (READCOMMITTED) +ON id = parent_id diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IsolationLevelContradictionRule/IsolationLevelContradictionRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IsolationLevelContradictionRule/IsolationLevelContradictionRuleTests.cs new file mode 100644 index 00000000..443af4be --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IsolationLevelContradictionRule/IsolationLevelContradictionRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.CodeSmell")] + [TestOfRule(typeof(IsolationLevelContradictionRule))] + public class IsolationLevelContradictionRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(IsolationLevelContradictionRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IsolationLevelContradictionRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IsolationLevelContradictionRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..971afccc --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IsolationLevelContradictionRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,3 @@ +SET TRANSACTION ISOLATION LEVEL REPEATABLE READ + +SELECT * FROM tbl WITH (TABLOCKX) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IsolationLevelContradictionRule/TestSources/bad_combination_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IsolationLevelContradictionRule/TestSources/bad_combination_raise_2_violations.sql new file mode 100644 index 00000000..3e015824 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IsolationLevelContradictionRule/TestSources/bad_combination_raise_2_violations.sql @@ -0,0 +1,11 @@ +SET TRANSACTION ISOLATION LEVEL REPEATABLE READ + +-- downgrade +SELECT * FROM tbl WITH (NOLOCK) +GO + +SET TRANSACTION ISOLATION LEVEL READ COMMITTED + +-- upgrate +SELECT * FROM tbl WITH (SERIALIZABLE) +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/LiteralContainsLookAlikeCharRule/TestSources/format_wildcard_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/LiteralContainsLookAlikeCharRule/TestSources/format_wildcard_raise_0_violations.sql new file mode 100644 index 00000000..2554d026 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/LiteralContainsLookAlikeCharRule/TestSources/format_wildcard_raise_0_violations.sql @@ -0,0 +1,3 @@ +SELECT FORMATMESSAGE('Откуда %o от %s до %u', @oct, @name, @number) + +RAISERROR('%x хорош', 16, 1, 123) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/LiteralContainsLookAlikeCharRule/TestSources/like_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/LiteralContainsLookAlikeCharRule/TestSources/like_raise_0_violations.sql new file mode 100644 index 00000000..b655c249 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/LiteralContainsLookAlikeCharRule/TestSources/like_raise_0_violations.sql @@ -0,0 +1,10 @@ +IF @nick_name NOT LIKE '[A-Za-zА-Яа-я]%' + PRINT 1 + +SELECT 1 +FROM foo +WHERE title LIKE @pattern -- not a literal + +SELECT 1 +FROM foo +WHERE title LIKE 'asdf%' -- no char mix diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/between_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/between_raise_0_violations.sql index 20b52153..d66e3bb3 100644 --- a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/between_raise_0_violations.sql +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/between_raise_0_violations.sql @@ -2,3 +2,8 @@ FROM dbo.foo AS f INNER JOIN scm.bar AS b ON scm.bar.dt BETWEEN f.period_start AND dbo.foo.period_end + +SELECT 1 +FROM dbo.foo AS f +LEFT JOIN scm.bar AS b + ON b.number BETWEEN 1 AND f.repeat_count diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/in_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/in_raise_0_violations.sql new file mode 100644 index 00000000..a95ae7a3 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/in_raise_0_violations.sql @@ -0,0 +1,4 @@ +SELECT 1 +FROM dbo.foo AS f +INNER JOIN scm.bar AS b + ON b.value IN (f.val_1, f.val_2) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ObjectPropertyFiddlingRule/ObjectPropertyFiddlingRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ObjectPropertyFiddlingRule/ObjectPropertyFiddlingRuleTests.cs new file mode 100644 index 00000000..f21542b3 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ObjectPropertyFiddlingRule/ObjectPropertyFiddlingRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.CodeSmell")] + [TestOfRule(typeof(ObjectPropertyFiddlingRule))] + public sealed class ObjectPropertyFiddlingRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(ObjectPropertyFiddlingRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ObjectPropertyFiddlingRule/TestSources/no_props_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ObjectPropertyFiddlingRule/TestSources/no_props_raise_0_violations.sql new file mode 100644 index 00000000..dee90a12 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ObjectPropertyFiddlingRule/TestSources/no_props_raise_0_violations.sql @@ -0,0 +1 @@ +PRINT OBJECT_ID('my_tbl', 'U') diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ObjectPropertyFiddlingRule/TestSources/props_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ObjectPropertyFiddlingRule/TestSources/props_raise_2_violations.sql new file mode 100644 index 00000000..f71ae9cd --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ObjectPropertyFiddlingRule/TestSources/props_raise_2_violations.sql @@ -0,0 +1,3 @@ +PRINT FILEPROPERTY('myfile', 'IsReadOnly') + +SELECT SERVERPROPERTY('hello') diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/SelfCallRule/SelfCallRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/SelfCallRule/SelfCallRuleTests.cs new file mode 100644 index 00000000..7f5fc9ee --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/SelfCallRule/SelfCallRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.CodeSmell")] + [TestOfRule(typeof(SelfCallRule))] + public class SelfCallRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(SelfCallRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/SelfCallRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/SelfCallRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..885564cf --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/SelfCallRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,27 @@ +create proc dbo.foo +as +begin + exec sp_help 'sys.objects' + + return 1 +end +GO + +create proc dbo.foo;3 +as +begin + exec dbo.foo;2 -- different number + + return 1 +end +GO + +CREATE FUNCTION foo.bar (@value int) +RETURNS INT +AS +BEGIN + SET @value = @value + 1; + + RETURN @value; +END +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/SelfCallRule/TestSources/fn_self_call_raise_3_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/SelfCallRule/TestSources/fn_self_call_raise_3_violations.sql new file mode 100644 index 00000000..6c428b71 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/SelfCallRule/TestSources/fn_self_call_raise_3_violations.sql @@ -0,0 +1,34 @@ +CREATE FUNCTION foo.bar (@id int) +RETURNS INT +AS +BEGIN + SET @id = @id + 1; + + RETURN foo.[bar](@id); +END +GO + +CREATE FUNCTION x.far (@id int) +RETURNS TABLE +AS +RETURN +( + SELECT * + FROM x.far(@id) +) +GO + +CREATE FUNCTION jar (@id int) +RETURNS @res TABLE +( + title VARCHAR(100) +) +AS +BEGIN + INSERT @res + SELECT * + FROM dbo.jar(@id) + + RETURN +END +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/SelfCallRule/TestSources/no_body_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/SelfCallRule/TestSources/no_body_raise_0_violations.sql new file mode 100644 index 00000000..7cf5cb89 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/SelfCallRule/TestSources/no_body_raise_0_violations.sql @@ -0,0 +1,5 @@ +create proc foo as; +GO + +create proc foo as +EXTERNAL NAME assm.bar.method; diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/SelfCallRule/TestSources/sp_self_call_raise_3_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/SelfCallRule/TestSources/sp_self_call_raise_3_violations.sql new file mode 100644 index 00000000..52c86545 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/SelfCallRule/TestSources/sp_self_call_raise_3_violations.sql @@ -0,0 +1,15 @@ +create proc dbo.foo +as +begin + exec foo + + exec [dbo].[foo] +end +GO + +create proc far.bar;3 +as +begin + exec [far].bar;3 +end +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/calling_builtin_func_raise_4_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/calling_builtin_func_raise_4_violations.sql new file mode 100644 index 00000000..eb7d7e73 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/calling_builtin_func_raise_4_violations.sql @@ -0,0 +1,31 @@ +CREATE FUNCTION dbo.foo(@title VARCHAR(100)) +RETURNS NVARCHAR(100) +AS +BEGIN + RETURN CAST(@title AS NVARCHAR(100)) +END +GO + +CREATE FUNCTION dbo.foo(@a INT, @b INT, @c INT) +RETURNS INT +AS +BEGIN + RETURN COALESCE(@a, @b, @c) +END +GO + +CREATE FUNCTION dbo.f_today() +RETURNS DATETIME +AS +BEGIN + RETURN GETDATE() +END +GO + +CREATE FUNCTION dbo.f_tomorrow() +RETURNS DATETIME +AS +BEGIN + RETURN GETDATE() + 1 +END +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/empty_func_body_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/empty_func_body_raise_2_violations.sql new file mode 100644 index 00000000..e82187cb --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/empty_func_body_raise_2_violations.sql @@ -0,0 +1,18 @@ +CREATE FUNCTION dbo.foo() +RETURNS DATETIME +AS +BEGIN + RETURN NULL +END +GO + +CREATE FUNCTION dbo.foo() +RETURNS @res TABLE +( + id INT +) +AS +BEGIN + RETURN; +END +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/empty_proc_body_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/empty_proc_body_raise_2_violations.sql new file mode 100644 index 00000000..254614ad --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/empty_proc_body_raise_2_violations.sql @@ -0,0 +1,9 @@ +CREATE PROC dbo.foo AS; +GO + +CREATE PROC dbo.foo AS +BEGIN + RETURN; +END +GO + diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/empty_trigger_body_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/empty_trigger_body_raise_2_violations.sql new file mode 100644 index 00000000..2d1533a7 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/empty_trigger_body_raise_2_violations.sql @@ -0,0 +1,15 @@ +CREATE TRIGGER dbo.tr ON dbo.bar AFTER INSERT AS +BEGIN + -- same as nothing + PRINT ''; +END +GO + +CREATE TRIGGER dbo.tr ON dbo.bar AFTER INSERT AS +BEGIN + SET NOCOUNT ON + + -- nested begin-ends should be handled by another rule + RETURN; +END +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/external_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/external_raise_0_violations.sql new file mode 100644 index 00000000..1f22e509 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/external_raise_0_violations.sql @@ -0,0 +1,15 @@ +CREATE FUNCTION dbo.foo (@Certificate VARBINARY(8000), @SignText VARBINARY(8000)) +RETURNS NVARCHAR(4000) +AS + EXTERNAL NAME CryptoSQLLibs.CryptoSQLLibs.VerifyCryptoMessage; +GO + +CREATE PROCEDURE dbo.bar (@Certificate VARBINARY(8000), @SignText VARBINARY(8000)) +AS + EXTERNAL NAME CryptoSQLLibs.CryptoSQLLibs.VerifyCryptoMessage; +GO + +CREATE TRIGGER dbo.far ON dbo.jar AFTER INSERT, UPDATE, DELETE +AS + EXTERNAL NAME CryptoSQLLibs.CryptoSQLLibs.VerifyCryptoMessage; +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/good_func_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/good_func_raise_0_violations.sql new file mode 100644 index 00000000..e3492bb7 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/good_func_raise_0_violations.sql @@ -0,0 +1,41 @@ +-- scalar +CREATE FUNCTION dbo.foo(@id INT) +RETURNS VARCHAR(100) +AS +BEGIN + DECLARE @title VARCHAR(100) + + -- TODO : isn't this clearly a bad example? + -- or such case should be detected by another rule? + SELECT TOP 1 @title = title + FROM dbo.bar + WHERE id = @id + + RETURN @title +END +GO + +-- inline +CREATE FUNCTION dbo.far() +RETURNS TABLE +AS + RETURN + ( + SELECT src.id + FROM jar + WHERE src.id > 0 + ); +GO + +-- case +CREATE FUNCTION dbo.foo(@id INT) +RETURNS VARCHAR(100) +AS +BEGIN + RETURN CASE + WHEN @id = 1 THEN 'A' + WHEN @id = 2 THEN 'B' + ELSE 'C' + END +END +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/good_proc_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/good_proc_raise_0_violations.sql new file mode 100644 index 00000000..e77e6da7 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/good_proc_raise_0_violations.sql @@ -0,0 +1,21 @@ +CREATE PROC dbo.foo + @arg INT +AS +BEGIN + SET NOCOUNT ON + + CREATE TABLE #t (id INT) + + INSERT #t(id) + SELECT id + FROM dbo.bar + WHERE category_id = @arg + + IF @@ROWCOUNT = 0 + BEGIN + RAISERROR ('Bad key', 16, 1) + RETURN 1 + END + + RETURN 0 +END diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/good_trigger_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/good_trigger_raise_0_violations.sql new file mode 100644 index 00000000..888b4875 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/good_trigger_raise_0_violations.sql @@ -0,0 +1,14 @@ +CREATE TRIGGER dbo.tr ON dbo.bar AFTER INSERT AS +BEGIN + UPDATE t SET + some_value = i.new_value + FROM dbo.far AS t + INNER JOIN INSERTED AS i + ON i.id = t.id + WHERE t.some_value IS NULL + + EXEC dbo.notify; + + RETURN; +END +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/return_input_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/return_input_raise_2_violations.sql new file mode 100644 index 00000000..f685e48e --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/return_input_raise_2_violations.sql @@ -0,0 +1,14 @@ +CREATE FUNCTION dbo.foo(@title VARCHAR(100)) +RETURNS NVARCHAR(100) +AS +BEGIN + RETURN @title +END +GO + +CREATE PROC dbo.foo @id INT +AS +BEGIN + RETURN @id + 1; +END +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/table_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/table_raise_0_violations.sql new file mode 100644 index 00000000..7100f7a6 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/TestSources/table_raise_0_violations.sql @@ -0,0 +1,4 @@ +CREATE TABLE dbo.foo +( + id INT NOT NULL PRIMARY KEY +) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/UselessUnitRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/UselessUnitRuleTests.cs new file mode 100644 index 00000000..6842a4a2 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/UselessUnitRule/UselessUnitRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.CodeSmell")] + [TestOfRule(typeof(UselessUnitRule))] + public class UselessUnitRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(UselessUnitRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodingConvention/FetchFullyQualifiedRule/FetchFullyQualifiedRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/CodingConvention/FetchFullyQualifiedRule/FetchFullyQualifiedRuleTests.cs new file mode 100644 index 00000000..6a222a09 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodingConvention/FetchFullyQualifiedRule/FetchFullyQualifiedRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.CodingConvention")] + [TestOfRule(typeof(FetchFullyQualifiedRule))] + public class FetchFullyQualifiedRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(FetchFullyQualifiedRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodingConvention/FetchFullyQualifiedRule/TestSources/omitted_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodingConvention/FetchFullyQualifiedRule/TestSources/omitted_raise_2_violations.sql new file mode 100644 index 00000000..35b62b9f --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodingConvention/FetchFullyQualifiedRule/TestSources/omitted_raise_2_violations.sql @@ -0,0 +1,5 @@ +FETCH FROM @cr +INTO @a, @b + + +FETCH my_cursor diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodingConvention/FetchFullyQualifiedRule/TestSources/qualified_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodingConvention/FetchFullyQualifiedRule/TestSources/qualified_raise_0_violations.sql new file mode 100644 index 00000000..05929649 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodingConvention/FetchFullyQualifiedRule/TestSources/qualified_raise_0_violations.sql @@ -0,0 +1,5 @@ +FETCH NEXT FROM @cr +INTO @a, @b + + +FETCH LAST FROM my_cursor diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodingConvention/SetOptionsInAscOrderRule/SetOptionsInAscOrderRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/CodingConvention/SetOptionsInAscOrderRule/SetOptionsInAscOrderRuleTests.cs new file mode 100644 index 00000000..fc37acd3 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodingConvention/SetOptionsInAscOrderRule/SetOptionsInAscOrderRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.CodingConvention")] + [TestOfRule(typeof(SetOptionsInAscOrderRule))] + public class SetOptionsInAscOrderRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(SetOptionsInAscOrderRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodingConvention/SetOptionsInAscOrderRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodingConvention/SetOptionsInAscOrderRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..e735a928 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodingConvention/SetOptionsInAscOrderRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,2 @@ +SET ANSI_NULLS, QUOTED_IDENTIFIER ON; + diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodingConvention/SetOptionsInAscOrderRule/TestSources/bad_order_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodingConvention/SetOptionsInAscOrderRule/TestSources/bad_order_raise_1_violations.sql new file mode 100644 index 00000000..cf474618 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodingConvention/SetOptionsInAscOrderRule/TestSources/bad_order_raise_1_violations.sql @@ -0,0 +1,2 @@ +SET QUOTED_IDENTIFIER, ANSI_NULLS ON; + diff --git a/TeamTools.TSQL.LinterTests/UnitTests/ContinuousDeployment/CreateOptionsInsideRule/CreateOptionsInsideRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/ContinuousDeployment/CreateOptionsInsideRule/CreateOptionsInsideRuleTests.cs new file mode 100644 index 00000000..d3c8128d --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/ContinuousDeployment/CreateOptionsInsideRule/CreateOptionsInsideRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.ContinuousDeployment")] + [TestOfRule(typeof(CreateOptionsInsideRule))] + public sealed class CreateOptionsInsideRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(CreateOptionsInsideRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/ContinuousDeployment/CreateOptionsInsideRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/ContinuousDeployment/CreateOptionsInsideRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..995edd94 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/ContinuousDeployment/CreateOptionsInsideRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,13 @@ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE PROC foo +AS +BEGIN + SET NOCOUNT ON + SET ARITHABORT OFF + + RETURN 1 +END +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/ContinuousDeployment/CreateOptionsInsideRule/TestSources/external_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/ContinuousDeployment/CreateOptionsInsideRule/TestSources/external_raise_0_violations.sql new file mode 100644 index 00000000..3ea7abe9 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/ContinuousDeployment/CreateOptionsInsideRule/TestSources/external_raise_0_violations.sql @@ -0,0 +1,17 @@ +CREATE PROCEDURE dbo.make_web_request + @url NVARCHAR(4000) + , @err NVARCHAR(MAX) OUTPUT +WITH EXECUTE AS CALLER +AS +EXTERNAL NAME [Sql.Web.Rest].StoredProcedures.WebRequestInvoke; +GO + +CREATE TRIGGER ddl ON ALL SERVER +AFTER LOGON +AS RETURN 1 +GO + +CREATE TRIGGER dml ON dbo.foo +AFTER insert +AS EXTERNAL NAME UtilCLR.whatever.itis; +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/ContinuousDeployment/CreateOptionsInsideRule/TestSources/opt_inside_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/ContinuousDeployment/CreateOptionsInsideRule/TestSources/opt_inside_raise_2_violations.sql new file mode 100644 index 00000000..967b14e0 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/ContinuousDeployment/CreateOptionsInsideRule/TestSources/opt_inside_raise_2_violations.sql @@ -0,0 +1,10 @@ +CREATE PROC foo +AS +BEGIN + SET NOCOUNT, QUOTED_IDENTIFIER, CONCAT_NULL_YIELDS_NULL, ANSI_NULLS ON + + SET ANSI_NULLS OFF + + RETURN 1 +END +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/ContinuousDeployment/SingleObjectPerFileRule/SingleObjectPerFileRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/ContinuousDeployment/SingleObjectPerFileRule/SingleObjectPerFileRuleTests.cs new file mode 100644 index 00000000..9c2ee8d9 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/ContinuousDeployment/SingleObjectPerFileRule/SingleObjectPerFileRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.ContinuousDeployment")] + [TestOfRule(typeof(SingleObjectPerFileRule))] + public class SingleObjectPerFileRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(SingleObjectPerFileRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/ContinuousDeployment/SingleObjectPerFileRule/TestSources/multiple_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/ContinuousDeployment/SingleObjectPerFileRule/TestSources/multiple_raise_2_violations.sql new file mode 100644 index 00000000..83584e56 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/ContinuousDeployment/SingleObjectPerFileRule/TestSources/multiple_raise_2_violations.sql @@ -0,0 +1,11 @@ +create proc dbo.foo as; +GO + +create table t +( + id int +) +GO + +create schema s authorization dbo; +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/ContinuousDeployment/SingleObjectPerFileRule/TestSources/single_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/ContinuousDeployment/SingleObjectPerFileRule/TestSources/single_raise_0_violations.sql new file mode 100644 index 00000000..72d696a1 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/ContinuousDeployment/SingleObjectPerFileRule/TestSources/single_raise_0_violations.sql @@ -0,0 +1,9 @@ +create proc dbo.foo as +begin + -- this is not a separate object + create table #t + ( + id int + ) +end + diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/ScalarUdtRule/ScalarUdtRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/ScalarUdtRule/ScalarUdtRuleTests.cs new file mode 100644 index 00000000..12887597 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/ScalarUdtRule/ScalarUdtRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.DatabaseDesign")] + [TestOfRule(typeof(ScalarUdtRule))] + public sealed class ScalarUdtRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(ScalarUdtRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/ScalarUdtRule/TestSources/clr_type_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/ScalarUdtRule/TestSources/clr_type_raise_0_violations.sql new file mode 100644 index 00000000..e063d489 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/ScalarUdtRule/TestSources/clr_type_raise_0_violations.sql @@ -0,0 +1,2 @@ +CREATE TYPE udt EXTERNAL NAME assm.[class.name]; + diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/ScalarUdtRule/TestSources/scalar_type_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/ScalarUdtRule/TestSources/scalar_type_raise_1_violations.sql new file mode 100644 index 00000000..693a2010 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/ScalarUdtRule/TestSources/scalar_type_raise_1_violations.sql @@ -0,0 +1 @@ +CREATE TYPE udt FROM INT diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/ScalarUdtRule/TestSources/table_type_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/ScalarUdtRule/TestSources/table_type_raise_0_violations.sql new file mode 100644 index 00000000..963c093f --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/ScalarUdtRule/TestSources/table_type_raise_0_violations.sql @@ -0,0 +1,4 @@ +CREATE TYPE udt AS TABLE +( + id INT +) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/SingleColumnTableRule/TestSources/no_good_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/SingleColumnTableRule/TestSources/no_good_raise_1_violations.sql similarity index 52% rename from TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/SingleColumnTableRule/TestSources/no_good_raise_2_violations.sql rename to TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/SingleColumnTableRule/TestSources/no_good_raise_1_violations.sql index ea460dfe..6b3d1e2b 100644 --- a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/SingleColumnTableRule/TestSources/no_good_raise_2_violations.sql +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/SingleColumnTableRule/TestSources/no_good_raise_1_violations.sql @@ -2,8 +2,3 @@ ( id INT ) - -DECLARE @foo TABLE -( - id INT -) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/SingleColumnTableRule/TestSources/temp_table_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/SingleColumnTableRule/TestSources/temp_table_raise_0_violations.sql new file mode 100644 index 00000000..fb3064e1 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/SingleColumnTableRule/TestSources/temp_table_raise_0_violations.sql @@ -0,0 +1,6 @@ +declare @t table (id int) + +CREATE TABLE #output +( + action_code CHAR(1) +) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/SingleColumnTableRule/TestSources/tvf_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/SingleColumnTableRule/TestSources/tvf_raise_0_violations.sql new file mode 100644 index 00000000..1f60ea7f --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/SingleColumnTableRule/TestSources/tvf_raise_0_violations.sql @@ -0,0 +1,12 @@ +CREATE FUNCTION foo (@arg INT) +RETURNS @res TABLE +( + some_id INT +) +AS +BEGIN + INSERT @res(id) + VALUES (1) + + RETURN +END diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableConsistencyCheckRule/TemporalTableConsistencyCheckRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableConsistencyCheckRule/TemporalTableConsistencyCheckRuleTests.cs new file mode 100644 index 00000000..005d860d --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableConsistencyCheckRule/TemporalTableConsistencyCheckRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.DatabaseDesign")] + [TestOfRule(typeof(TemporalTableConsistencyCheckRule))] + public sealed class TemporalTableConsistencyCheckRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(TemporalTableConsistencyCheckRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableConsistencyCheckRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableConsistencyCheckRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..aa8a8f18 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableConsistencyCheckRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,25 @@ +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(7) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time DEFAULT SYSUTCDATETIME() + , sys_end_time DATETIME2(7) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_end_time DEFAULT CONVERT(DATETIME2(7), '9999-12-31 23:59:59.9999999') + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +-- consistency check omitted (default is ON) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO + +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(7) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time DEFAULT SYSUTCDATETIME() + , sys_end_time DATETIME2(7) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_end_time DEFAULT CONVERT(DATETIME2(7), '9999-12-31 23:59:59.9999999') + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +-- explicit ON +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history, DATA_CONSISTENCY_CHECK = ON)); +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableConsistencyCheckRule/TestSources/filestream_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableConsistencyCheckRule/TestSources/filestream_raise_0_violations.sql new file mode 100644 index 00000000..8623ad74 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableConsistencyCheckRule/TestSources/filestream_raise_0_violations.sql @@ -0,0 +1,3 @@ +-- compatibility level min: 110 +CREATE TABLE files.docs_storage AS FILETABLE FILESTREAM_ON document_filestream_group +WITH (FILETABLE_COLLATE_FILENAME = Cyrillic_General_CI_AS, FILETABLE_DIRECTORY = N'docs_storage'); diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableConsistencyCheckRule/TestSources/off_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableConsistencyCheckRule/TestSources/off_raise_1_violations.sql new file mode 100644 index 00000000..a8e89950 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableConsistencyCheckRule/TestSources/off_raise_1_violations.sql @@ -0,0 +1,13 @@ +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(7) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time DEFAULT SYSUTCDATETIME() + , sys_end_time DATETIME2(7) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_end_time DEFAULT CONVERT(DATETIME2(7), '9999-12-31 23:59:59.9999999') + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history, + DATA_CONSISTENCY_CHECK = OFF -- here +)); +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableNameHistoryTableRule/TemporalTableNameHistoryTableRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableNameHistoryTableRule/TemporalTableNameHistoryTableRuleTests.cs new file mode 100644 index 00000000..184ca457 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableNameHistoryTableRule/TemporalTableNameHistoryTableRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.DatabaseDesign")] + [TestOfRule(typeof(TemporalTableNameHistoryTableRule))] + public sealed class TemporalTableNameHistoryTableRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(TemporalTableNameHistoryTableRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableNameHistoryTableRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableNameHistoryTableRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..15558161 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableNameHistoryTableRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,23 @@ +-- not temporal +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(1) + , sys_end_time DATETIME2(1) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +GO + +-- all fine +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(7) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time DEFAULT SYSUTCDATETIME() + , sys_end_time DATETIME2(7) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_end_time DEFAULT CONVERT(DATETIME2(7), '9999-12-31 23:59:59.9999999') + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableNameHistoryTableRule/TestSources/filestream_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableNameHistoryTableRule/TestSources/filestream_raise_0_violations.sql new file mode 100644 index 00000000..8623ad74 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableNameHistoryTableRule/TestSources/filestream_raise_0_violations.sql @@ -0,0 +1,3 @@ +-- compatibility level min: 110 +CREATE TABLE files.docs_storage AS FILETABLE FILESTREAM_ON document_filestream_group +WITH (FILETABLE_COLLATE_FILENAME = Cyrillic_General_CI_AS, FILETABLE_DIRECTORY = N'docs_storage'); diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableNameHistoryTableRule/TestSources/no_name_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableNameHistoryTableRule/TestSources/no_name_raise_1_violations.sql new file mode 100644 index 00000000..c687dbba --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableNameHistoryTableRule/TestSources/no_name_raise_1_violations.sql @@ -0,0 +1,11 @@ +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(7) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time DEFAULT SYSUTCDATETIME() + , sys_end_time DATETIME2(7) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_end_time DEFAULT CONVERT(DATETIME2(7), '9999-12-31 23:59:59.9999999') + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON); +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePeriodDefinedRule/TemporalTablePeriodDefinedRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePeriodDefinedRule/TemporalTablePeriodDefinedRuleTests.cs new file mode 100644 index 00000000..9251669f --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePeriodDefinedRule/TemporalTablePeriodDefinedRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.DatabaseDesign")] + [TestOfRule(typeof(TemporalTablePeriodDefinedRule))] + public sealed class TemporalTablePeriodDefinedRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(TemporalTablePeriodDefinedRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePeriodDefinedRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePeriodDefinedRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..15558161 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePeriodDefinedRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,23 @@ +-- not temporal +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(1) + , sys_end_time DATETIME2(1) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +GO + +-- all fine +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(7) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time DEFAULT SYSUTCDATETIME() + , sys_end_time DATETIME2(7) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_end_time DEFAULT CONVERT(DATETIME2(7), '9999-12-31 23:59:59.9999999') + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePeriodDefinedRule/TestSources/filestream_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePeriodDefinedRule/TestSources/filestream_raise_0_violations.sql new file mode 100644 index 00000000..8623ad74 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePeriodDefinedRule/TestSources/filestream_raise_0_violations.sql @@ -0,0 +1,3 @@ +-- compatibility level min: 110 +CREATE TABLE files.docs_storage AS FILETABLE FILESTREAM_ON document_filestream_group +WITH (FILETABLE_COLLATE_FILENAME = Cyrillic_General_CI_AS, FILETABLE_DIRECTORY = N'docs_storage'); diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePeriodDefinedRule/TestSources/no_period_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePeriodDefinedRule/TestSources/no_period_raise_1_violations.sql new file mode 100644 index 00000000..42bb08d8 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePeriodDefinedRule/TestSources/no_period_raise_1_violations.sql @@ -0,0 +1,8 @@ +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePossibleFutureDateRule/TemporalTablePossibleFutureDateRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePossibleFutureDateRule/TemporalTablePossibleFutureDateRuleTests.cs new file mode 100644 index 00000000..b737fc99 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePossibleFutureDateRule/TemporalTablePossibleFutureDateRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.DatabaseDesign")] + [TestOfRule(typeof(TemporalTablePossibleFutureDateRule))] + public sealed class TemporalTablePossibleFutureDateRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(TemporalTablePossibleFutureDateRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePossibleFutureDateRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePossibleFutureDateRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..44279095 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePossibleFutureDateRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,24 @@ +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(7) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time DEFAULT SYSUTCDATETIME() + , sys_end_time DATETIME2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_end_time DEFAULT CONVERT(DATETIME2(7), '9999-12-31 23:59:59.9999999') + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO + +-- no default or literal +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(7) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL + , sys_end_time DATETIME2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_end_time DEFAULT '9999-12-31 23:59:59.9999999' + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePossibleFutureDateRule/TestSources/bad_precision_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePossibleFutureDateRule/TestSources/bad_precision_raise_1_violations.sql new file mode 100644 index 00000000..e929d636 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePossibleFutureDateRule/TestSources/bad_precision_raise_1_violations.sql @@ -0,0 +1,13 @@ +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + -- only START date is reported by the rule + , sys_start_time DATETIME2(2) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time + DEFAULT SYSUTCDATETIME() + , sys_end_time DATETIME2(1) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_end_time DEFAULT DATEADD(DAY, -1, CAST(GETDATE() AS DATE)) + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePossibleFutureDateRule/TestSources/dateadd_to_bad_precision_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePossibleFutureDateRule/TestSources/dateadd_to_bad_precision_raise_1_violations.sql new file mode 100644 index 00000000..cd2e7e8a --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePossibleFutureDateRule/TestSources/dateadd_to_bad_precision_raise_1_violations.sql @@ -0,0 +1,13 @@ +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + -- only START date is reported by the rule + , sys_start_time DATETIME2(2) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time + DEFAULT DATEADD(DAY, 1, SYSUTCDATETIME()) -- here + , sys_end_time DATETIME2(1) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePossibleFutureDateRule/TestSources/filestream_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePossibleFutureDateRule/TestSources/filestream_raise_0_violations.sql new file mode 100644 index 00000000..8623ad74 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePossibleFutureDateRule/TestSources/filestream_raise_0_violations.sql @@ -0,0 +1,3 @@ +-- compatibility level min: 110 +CREATE TABLE files.docs_storage AS FILETABLE FILESTREAM_ON document_filestream_group +WITH (FILETABLE_COLLATE_FILENAME = Cyrillic_General_CI_AS, FILETABLE_DIRECTORY = N'docs_storage'); diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePossibleFutureDateRule/TestSources/negative_dateadd_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePossibleFutureDateRule/TestSources/negative_dateadd_raise_0_violations.sql new file mode 100644 index 00000000..81b04cba --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTablePossibleFutureDateRule/TestSources/negative_dateadd_raise_0_violations.sql @@ -0,0 +1,14 @@ +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(2) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL + CONSTRAINT DF_foo_bar_sys_start_time DEFAULT DATEADD(SECOND,(-(1)), SYSUTCDATETIME()) + , sys_end_time DATETIME2(2) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL + CONSTRAINT DF_foo_bar_sys_end_time DEFAULT CONVERT(DATETIME2(7), '9999-12-31 23:59:59.9999999') + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO + diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableRangeDefaultPrecisionLackRule/TemporalTableRangeDefaultPrecisionLackRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableRangeDefaultPrecisionLackRule/TemporalTableRangeDefaultPrecisionLackRuleTests.cs new file mode 100644 index 00000000..976154f3 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableRangeDefaultPrecisionLackRule/TemporalTableRangeDefaultPrecisionLackRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.DatabaseDesign")] + [TestOfRule(typeof(TemporalTableRangeDefaultPrecisionLackRule))] + public sealed class TemporalTableRangeDefaultPrecisionLackRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(TemporalTableRangeDefaultPrecisionLackRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableRangeDefaultPrecisionLackRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableRangeDefaultPrecisionLackRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..03c5783d --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableRangeDefaultPrecisionLackRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,25 @@ +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(7) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time DEFAULT SYSUTCDATETIME() + , sys_end_time DATETIME2(7) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_end_time DEFAULT CONVERT(DATETIME2(7), '9999-12-31 23:59:59.9999999') + , calc AS 1 + 1 + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO + +-- no default or literal +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(7) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL + , sys_end_time DATETIME2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_end_time DEFAULT '9999-12-31 23:59:59.9999999' + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableRangeDefaultPrecisionLackRule/TestSources/bad_default_raise_3_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableRangeDefaultPrecisionLackRule/TestSources/bad_default_raise_3_violations.sql new file mode 100644 index 00000000..50edce1b --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableRangeDefaultPrecisionLackRule/TestSources/bad_default_raise_3_violations.sql @@ -0,0 +1,28 @@ + +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(7) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time + DEFAULT GETDATE() -- 1 + , sys_end_time DATETIME2(7) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_end_time + DEFAULT CONVERT(DATETIME2(3), '9999-12-31') -- 2 + , calc AS 1 + 1 + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO + +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(2) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time + DEFAULT DATEADD(DAY, -1, CAST(SYSUTCDATETIME() AS DATE)) -- 3 + , sys_end_time DATETIME2(1) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableRangeDefaultPrecisionLackRule/TestSources/filestream_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableRangeDefaultPrecisionLackRule/TestSources/filestream_raise_0_violations.sql new file mode 100644 index 00000000..8623ad74 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableRangeDefaultPrecisionLackRule/TestSources/filestream_raise_0_violations.sql @@ -0,0 +1,3 @@ +-- compatibility level min: 110 +CREATE TABLE files.docs_storage AS FILETABLE FILESTREAM_ON document_filestream_group +WITH (FILETABLE_COLLATE_FILENAME = Cyrillic_General_CI_AS, FILETABLE_DIRECTORY = N'docs_storage'); diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableRangeMaxPrecisionRule/TemporalTableRangeMaxPrecisionRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableRangeMaxPrecisionRule/TemporalTableRangeMaxPrecisionRuleTests.cs new file mode 100644 index 00000000..4ce297db --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableRangeMaxPrecisionRule/TemporalTableRangeMaxPrecisionRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.DatabaseDesign")] + [TestOfRule(typeof(TemporalTableRangeMaxPrecisionRule))] + public sealed class TemporalTableRangeMaxPrecisionRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(TemporalTableRangeMaxPrecisionRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableRangeMaxPrecisionRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableRangeMaxPrecisionRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..c8ed4856 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableRangeMaxPrecisionRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,47 @@ +-- not temporal +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(1) + , sys_end_time DATETIME2(1) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +GO + +-- missing temporal cols +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO + +-- date precedence omitted +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time DEFAULT SYSUTCDATETIME() + , sys_end_time DATETIME2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_end_time DEFAULT CONVERT(DATETIME2(7), '9999-12-31 23:59:59.9999999') + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO + +-- all fine +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(7) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time DEFAULT SYSUTCDATETIME() + , sys_end_time DATETIME2(7) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_end_time DEFAULT CONVERT(DATETIME2(7), '9999-12-31 23:59:59.9999999') + , calc AS 1 + 1 + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableRangeMaxPrecisionRule/TestSources/filestream_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableRangeMaxPrecisionRule/TestSources/filestream_raise_0_violations.sql new file mode 100644 index 00000000..8623ad74 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableRangeMaxPrecisionRule/TestSources/filestream_raise_0_violations.sql @@ -0,0 +1,3 @@ +-- compatibility level min: 110 +CREATE TABLE files.docs_storage AS FILETABLE FILESTREAM_ON document_filestream_group +WITH (FILETABLE_COLLATE_FILENAME = Cyrillic_General_CI_AS, FILETABLE_DIRECTORY = N'docs_storage'); diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableRangeMaxPrecisionRule/TestSources/rounding_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableRangeMaxPrecisionRule/TestSources/rounding_raise_2_violations.sql new file mode 100644 index 00000000..0f104ca9 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableRangeMaxPrecisionRule/TestSources/rounding_raise_2_violations.sql @@ -0,0 +1,12 @@ +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + -- here + , sys_start_time DATETIME2(3) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time DEFAULT SYSUTCDATETIME() + , sys_end_time DATETIME2(1) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_end_time DEFAULT CONVERT(DATETIME2(7), '9999-12-31 23:59:59.9999999') + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableSimilarDateRangePrecision/TemporalTableSimilarDateRangePrecisionTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableSimilarDateRangePrecision/TemporalTableSimilarDateRangePrecisionTests.cs new file mode 100644 index 00000000..7d66afd6 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableSimilarDateRangePrecision/TemporalTableSimilarDateRangePrecisionTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.DatabaseDesign")] + [TestOfRule(typeof(TemporalTableSimilarDateRangePrecision))] + public sealed class TemporalTableSimilarDateRangePrecisionTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(TemporalTableSimilarDateRangePrecisionTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableSimilarDateRangePrecision/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableSimilarDateRangePrecision/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..87809a59 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableSimilarDateRangePrecision/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,25 @@ +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(7) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time DEFAULT SYSUTCDATETIME() + , sys_end_time DATETIME2(7) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_end_time DEFAULT CONVERT(DATETIME2(7), '9999-12-31 23:59:59.9999999') + , calc AS 1 + 1 + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO + +-- precision omitted +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(7) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_start_time DEFAULT SYSUTCDATETIME() + , sys_end_time DATETIME2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT DF_foo_bar_sys_end_time DEFAULT CONVERT(DATETIME2(7), '9999-12-31 23:59:59.9999999') + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableSimilarDateRangePrecision/TestSources/different_precision_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableSimilarDateRangePrecision/TestSources/different_precision_raise_1_violations.sql new file mode 100644 index 00000000..4d7b2acc --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableSimilarDateRangePrecision/TestSources/different_precision_raise_1_violations.sql @@ -0,0 +1,11 @@ +CREATE TABLE foo.bar +( + code VARCHAR(100) NOT NULL + , descr VARCHAR(4000) NOT NULL + , sys_start_time DATETIME2(1) GENERATED ALWAYS AS ROW START + , sys_end_time DATETIME2(3) GENERATED ALWAYS AS ROW END + , PERIOD FOR SYSTEM_TIME(sys_start_time, sys_end_time) + , CONSTRAINT PK_foo_bar PRIMARY KEY NONCLUSTERED (code) +) +WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = foo.bar_history)); +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableSimilarDateRangePrecision/TestSources/filestream_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableSimilarDateRangePrecision/TestSources/filestream_raise_0_violations.sql new file mode 100644 index 00000000..8623ad74 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/DatabaseDesign/TemporalTableSimilarDateRangePrecision/TestSources/filestream_raise_0_violations.sql @@ -0,0 +1,3 @@ +-- compatibility level min: 110 +CREATE TABLE files.docs_storage AS FILETABLE FILESTREAM_ON document_filestream_group +WITH (FILETABLE_COLLATE_FILENAME = Cyrillic_General_CI_AS, FILETABLE_DIRECTORY = N'docs_storage'); diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/ColumnNotIncludedInGroupByRule/TestSources/cube_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Failure/ColumnNotIncludedInGroupByRule/TestSources/cube_raise_0_violations.sql new file mode 100644 index 00000000..a10f6994 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/ColumnNotIncludedInGroupByRule/TestSources/cube_raise_0_violations.sql @@ -0,0 +1,7 @@ +select client_id, account_id, sum(val) val +from my_table +group by client_id, cube(account_id) + +select client_id, isnull(account_id, 0) as account_id, sum(val) val +from my_table +group by client_id, cube(account_id) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/ColumnNotIncludedInGroupByRule/TestSources/rollup_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Failure/ColumnNotIncludedInGroupByRule/TestSources/rollup_raise_0_violations.sql new file mode 100644 index 00000000..6d865839 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/ColumnNotIncludedInGroupByRule/TestSources/rollup_raise_0_violations.sql @@ -0,0 +1,7 @@ +select client_id, account_id, sum(val) val +from my_table +group by client_id, rollup(account_id) + +select client_id, isnull(account_id, 0) as account_id, sum(val) val +from my_table +group by client_id, rollup(account_id) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/ImplicitConversionImpossibleRule/TestSources/sql_variant_in_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Failure/ImplicitConversionImpossibleRule/TestSources/sql_variant_in_raise_0_violations.sql new file mode 100644 index 00000000..b9ca11bb --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/ImplicitConversionImpossibleRule/TestSources/sql_variant_in_raise_0_violations.sql @@ -0,0 +1,2 @@ +IF SERVERPROPERTY('EngineEdition') NOT IN (5,8) + PRINT 'OK' diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/InvalidExtendedPropertyParameterRule/InvalidExtendedPropertyParameterRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Failure/InvalidExtendedPropertyParameterRule/InvalidExtendedPropertyParameterRuleTests.cs new file mode 100644 index 00000000..9364f31d --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/InvalidExtendedPropertyParameterRule/InvalidExtendedPropertyParameterRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Failure")] + [TestOfRule(typeof(InvalidExtendedPropertyParameterRule))] + public sealed class InvalidExtendedPropertyParameterRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(InvalidExtendedPropertyParameterRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/InvalidExtendedPropertyParameterRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Failure/InvalidExtendedPropertyParameterRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..0358b667 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/InvalidExtendedPropertyParameterRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,31 @@ +-- by position not supported +EXEC sp_dropextendedproperty + N'MS_Description' + , N'SCHEMA' + , N'my_schema' + , N'TABLE' + , N'my_table' + , N'COLUMN' + , N'my_column'; + +-- db level +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = NULL + , @level0name = NULL + , @level1type = NULL + , @level1name = NULL + , @level2type = NULL + , @level2name = NULL; + +-- all good +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'my_schema' + , @level1type = N'TABLE' + , @level1name = N'my_table' + , @level2type = N'COLUMN' + , @level2name = N'my_column'; diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/InvalidExtendedPropertyParameterRule/TestSources/bad_level_type_raise_3_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Failure/InvalidExtendedPropertyParameterRule/TestSources/bad_level_type_raise_3_violations.sql new file mode 100644 index 00000000..86c3d19c --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/InvalidExtendedPropertyParameterRule/TestSources/bad_level_type_raise_3_violations.sql @@ -0,0 +1,28 @@ +EXEC sp_addextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'UNKNOWN' -- 1 + , @level0name = N'my_schema' + , @level1type = N'TABLE' + , @level1name = N'my_table' + , @level2type = N'COLUMN' + , @level2name = N'my_column'; + +EXEC sp_updateextendedproperty + @name = N'MS_Description' + , @value = N'Some description' + , @level0type = N'SCHEMA' + , @level0name = N'my_schema' + , @level1type = N'UNKNOWN' -- 2 + , @level1name = N'my_table' + , @level2type = N'COLUMN' + , @level2name = N'my_column'; + +EXEC sp_dropextendedproperty + @name = N'MS_Description' + , @level0type = N'SCHEMA' + , @level0name = N'my_schema' + , @level1type = N'TABLE' + , @level1name = N'my_table' + , @level2type = N'UNKNOWN' -- 3 + , @level2name = N'my_column'; diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/InvalidExtendedPropertyParameterRule/TestSources/unsupported_exec_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Failure/InvalidExtendedPropertyParameterRule/TestSources/unsupported_exec_raise_0_violations.sql new file mode 100644 index 00000000..8a41699e --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/InvalidExtendedPropertyParameterRule/TestSources/unsupported_exec_raise_0_violations.sql @@ -0,0 +1,17 @@ +CREATE PROC foo AS; +GO + +EXEC ('cmd') +GO + +EXEC @var + @arg1 = @val1 +GO + + +EXEC schm.test + @arg1 = @val1 +GO + +EXEC sys.test + @arg1 = @val1 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/RaiseErrorNeedsWithLogRule/RaiseErrorNeedsWithLogRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Failure/RaiseErrorNeedsWithLogRule/RaiseErrorNeedsWithLogRuleTests.cs new file mode 100644 index 00000000..99a4d2ac --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/RaiseErrorNeedsWithLogRule/RaiseErrorNeedsWithLogRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Failure")] + [TestOfRule(typeof(RaiseErrorNeedsWithLogRule))] + public class RaiseErrorNeedsWithLogRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(RaiseErrorNeedsWithLogRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/RaiseErrorNeedsWithLogRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Failure/RaiseErrorNeedsWithLogRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..d876d001 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/RaiseErrorNeedsWithLogRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,3 @@ +RAISERROR ('error', 16, 1) + +RAISERROR ('error', 19, 2) WITH LOG diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/RaiseErrorNeedsWithLogRule/TestSources/bad_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Failure/RaiseErrorNeedsWithLogRule/TestSources/bad_raise_1_violations.sql new file mode 100644 index 00000000..6e6e0e27 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/RaiseErrorNeedsWithLogRule/TestSources/bad_raise_1_violations.sql @@ -0,0 +1 @@ +RAISERROR ('error', 19, 0) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/SpExecuteSqlNoAsciiSupportRule/TestSources/unknown_var_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Failure/SpExecuteSqlNoAsciiSupportRule/TestSources/unknown_var_raise_0_violations.sql new file mode 100644 index 00000000..12799b77 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/SpExecuteSqlNoAsciiSupportRule/TestSources/unknown_var_raise_0_violations.sql @@ -0,0 +1,5 @@ +-- another rule should detect undeclared variable references +EXEC sys.sp_executesql + @script, + @args, + @variable; diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedTableVariableReferenceRule/TestSources/defined_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedTableVariableReferenceRule/TestSources/defined_raise_0_violations.sql new file mode 100644 index 00000000..4cd86f6c --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedTableVariableReferenceRule/TestSources/defined_raise_0_violations.sql @@ -0,0 +1,4 @@ +DECLARE @src TABLE (id INT) + +SELECT * +FROM @src diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedTableVariableReferenceRule/TestSources/table_arg_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedTableVariableReferenceRule/TestSources/table_arg_raise_0_violations.sql new file mode 100644 index 00000000..cb2e2501 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedTableVariableReferenceRule/TestSources/table_arg_raise_0_violations.sql @@ -0,0 +1,8 @@ +CREATE PROC foo + @src some_table_type READONLY +AS +BEGIN + SELECT * + FROM @src +END +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedTableVariableReferenceRule/TestSources/table_var_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedTableVariableReferenceRule/TestSources/table_var_raise_0_violations.sql new file mode 100644 index 00000000..336fdfb5 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedTableVariableReferenceRule/TestSources/table_var_raise_0_violations.sql @@ -0,0 +1,4 @@ +DECLARE @src my.table_type; + +INSERT @src +VALUES (1, 2, 3) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedTableVariableReferenceRule/TestSources/unknown_name_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedTableVariableReferenceRule/TestSources/unknown_name_raise_2_violations.sql new file mode 100644 index 00000000..83dfff0c --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedTableVariableReferenceRule/TestSources/unknown_name_raise_2_violations.sql @@ -0,0 +1,9 @@ +DECLARE @foo TABLE (id INT) + +SELECT * +FROM @bar +GO + +-- table vars are not passed across batches +SELECT * +FROM @foo diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedTableVariableReferenceRule/TestSources/unknown_scalar_var_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedTableVariableReferenceRule/TestSources/unknown_scalar_var_raise_0_violations.sql new file mode 100644 index 00000000..18f73bd5 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedTableVariableReferenceRule/TestSources/unknown_scalar_var_raise_0_violations.sql @@ -0,0 +1,2 @@ +-- another rule should validate this +PRINT @var diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedTableVariableReferenceRule/TestSources/wrong_order_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedTableVariableReferenceRule/TestSources/wrong_order_raise_1_violations.sql new file mode 100644 index 00000000..9a259e5b --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedTableVariableReferenceRule/TestSources/wrong_order_raise_1_violations.sql @@ -0,0 +1,4 @@ +SELECT * +FROM @src + +DECLARE @src TABLE (id INT) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedTableVariableReferenceRule/UnresolvedTableVariableReferenceRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedTableVariableReferenceRule/UnresolvedTableVariableReferenceRuleTests.cs new file mode 100644 index 00000000..12702c41 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedTableVariableReferenceRule/UnresolvedTableVariableReferenceRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Failure")] + [TestOfRule(typeof(UnresolvedTableVariableReferenceRule))] + public sealed class UnresolvedTableVariableReferenceRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(UnresolvedTableVariableReferenceRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedVariableReferenceRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedVariableReferenceRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..f724f8b0 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedVariableReferenceRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,23 @@ +-- inside proc with respect to args +CREATE PROC foo + @a INT +AS +BEGIN + DECLARE @b CHAR, @cr CURSOR + + EXEC bar + @b, + @value_a = @a + + SELECT @a + WHERE @b > 0 + + OPEN @cr +END +GO + +-- in script root +DECLARE @foo INT = 1 + +PRINT @foo +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedVariableReferenceRule/TestSources/unknown_name_raise_3_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedVariableReferenceRule/TestSources/unknown_name_raise_3_violations.sql new file mode 100644 index 00000000..a5e0622d --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedVariableReferenceRule/TestSources/unknown_name_raise_3_violations.sql @@ -0,0 +1,4 @@ +EXEC sys.sp_executesql + @script, + @args, + @variable; diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedVariableReferenceRule/TestSources/unknown_table_var_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedVariableReferenceRule/TestSources/unknown_table_var_raise_0_violations.sql new file mode 100644 index 00000000..008b454c --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedVariableReferenceRule/TestSources/unknown_table_var_raise_0_violations.sql @@ -0,0 +1,3 @@ +-- another rule should validate this +SELECT * +FROM @tbl diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedVariableReferenceRule/TestSources/wrong_order_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedVariableReferenceRule/TestSources/wrong_order_raise_1_violations.sql new file mode 100644 index 00000000..0e5c51cf --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedVariableReferenceRule/TestSources/wrong_order_raise_1_violations.sql @@ -0,0 +1,3 @@ +SELECT @a + +DECLARE @a INT diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedVariableReferenceRule/UnresolvedVariableReferenceRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedVariableReferenceRule/UnresolvedVariableReferenceRuleTests.cs new file mode 100644 index 00000000..0abbe63c --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedVariableReferenceRule/UnresolvedVariableReferenceRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Failure")] + [TestOfRule(typeof(UnresolvedVariableReferenceRule))] + public sealed class UnresolvedVariableReferenceRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(UnresolvedVariableReferenceRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedWindowNameRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedWindowNameRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..ca4a08b9 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedWindowNameRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,16 @@ +-- no WINDOW clause +SELECT SalesOrderID, + ProductID, + OrderQty, + SUM(OrderQty) OVER (PARTITION BY SalesOrderID) AS [Total] +FROM Sales.SalesOrderDetail +WHERE SalesOrderID IN (43659, 43664); + +-- name "foo" resolved +SELECT SalesOrderID, + ProductID, + OrderQty, + SUM(OrderQty) OVER foo AS [Total] +FROM Sales.SalesOrderDetail +WHERE SalesOrderID IN (43659, 43664) +WINDOW foo AS (PARTITION BY SalesOrderID); diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedWindowNameRule/TestSources/bad_name_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedWindowNameRule/TestSources/bad_name_raise_1_violations.sql new file mode 100644 index 00000000..1c2af416 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedWindowNameRule/TestSources/bad_name_raise_1_violations.sql @@ -0,0 +1,7 @@ +SELECT SalesOrderID, + ProductID, + OrderQty, + SUM(OrderQty) OVER foo AS [Total] +FROM Sales.SalesOrderDetail +WHERE SalesOrderID IN (43659, 43664) +WINDOW bar AS (PARTITION BY SalesOrderID); diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedWindowNameRule/UnresolvedWindowNameRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedWindowNameRule/UnresolvedWindowNameRuleTests.cs new file mode 100644 index 00000000..b99162bf --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/UnresolvedWindowNameRule/UnresolvedWindowNameRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Failure")] + [TestOfRule(typeof(UnresolvedWindowNameRule))] + public sealed class UnresolvedWindowNameRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(UnresolvedWindowNameRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/VariableRedeclaredRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Failure/VariableRedeclaredRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..8e299ac2 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/VariableRedeclaredRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,26 @@ +CREATE PROC foo + @a INT +AS +BEGIN + DECLARE @b CHAR, @cr CURSOR + + EXEC bar + @b, + @value_a = @a + + SELECT @a, @b + + DECLARE @tbl TABLE (id INT) + + CREATE TABLE #tbl (id INT) + + OPEN @cr +END +GO +-- in script root +DECLARE @foo INT = 1 + +PRINT @foo + +DECLARE @tbl TABLE (id INT) +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/VariableRedeclaredRule/TestSources/scalar_redeclared_raise_3_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Failure/VariableRedeclaredRule/TestSources/scalar_redeclared_raise_3_violations.sql new file mode 100644 index 00000000..e7088de7 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/VariableRedeclaredRule/TestSources/scalar_redeclared_raise_3_violations.sql @@ -0,0 +1,21 @@ +CREATE PROC foo + @a INT +AS +BEGIN + DECLARE @a CHAR -- 1 + , @cr CURSOR + + EXEC bar + @b, + @value_a = @a + + DECLARE @cr CURSOR -- 2 +END +GO + +DECLARE @foo INT = 1 + +PRINT @foo + +DECLARE @foo INT -- 3 +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/VariableRedeclaredRule/TestSources/table_redeclared_raise_3_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Failure/VariableRedeclaredRule/TestSources/table_redeclared_raise_3_violations.sql new file mode 100644 index 00000000..38f77743 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/VariableRedeclaredRule/TestSources/table_redeclared_raise_3_violations.sql @@ -0,0 +1,12 @@ +CREATE PROC foo + @a INT +AS +BEGIN + DECLARE @a TABLE (id INT) -- 1 + + DECLARE @t TABLE (title VARCHAR) + + DECLARE @t TABLE (dt DATE) -- 2 + + DECLARE @t TIME -- 3 +END diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Failure/VariableRedeclaredRule/VariableRedeclaredRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Failure/VariableRedeclaredRule/VariableRedeclaredRuleTests.cs new file mode 100644 index 00000000..99f7d9b1 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Failure/VariableRedeclaredRule/VariableRedeclaredRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Failure")] + [TestOfRule(typeof(VariableRedeclaredRule))] + public sealed class VariableRedeclaredRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(VariableRedeclaredRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/ExpandDateFunctionRule/ExpandDateFunctionRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Performance/ExpandDateFunctionRule/ExpandDateFunctionRuleTests.cs new file mode 100644 index 00000000..6a1471c5 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/ExpandDateFunctionRule/ExpandDateFunctionRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Performance")] + [TestOfRule(typeof(ExpandDateFunctionRule))] + public sealed class ExpandDateFunctionRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(ExpandDateFunctionRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/ExpandDateFunctionRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/ExpandDateFunctionRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..b0c32e50 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/ExpandDateFunctionRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,17 @@ +-- no func in predicate +SELECT sum(payment) +FROM taxes +WHERE pay_period >= @tax_year_begin + AND pay_period < @next_tax_year + +-- YEAR can be precomputed +SELECT sum(payment) +FROM taxes +JOIN report_periods +ON tax_period = @report_period_start +AND YEAR(@report_period_start) = @tax_year + +-- can't be fixed +SELECT sum(payment) +FROM taxes AS t +WHERE YEAR(t.pay_period) = t.some_other_year diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/ExpandDateFunctionRule/TestSources/bad_predicate_raise_5_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/ExpandDateFunctionRule/TestSources/bad_predicate_raise_5_violations.sql new file mode 100644 index 00000000..9be35a38 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/ExpandDateFunctionRule/TestSources/bad_predicate_raise_5_violations.sql @@ -0,0 +1,18 @@ +SELECT sum(payment) +FROM taxes +WHERE YEAR(pay_period) = @tax_year -- 1 + +SELECT sum(payment) +FROM taxes +WHERE (DATEPART(YEAR, ((pay_period)))) = @tax_year -- 2 +OR DATENAME(YEAR, pay_period) = '2020' -- 3 + +SELECT sum(payment) +FROM taxes +JOIN report_periods +ON (@report_period) = DATETRUNC(YEAR, tax_period) -- 4 + +SELECT sum(payment) +FROM taxes +JOIN report_periods +ON DATEADD(DAY, 1, pay_date) < @max_date -- 5 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/ExpandDateFunctionRule/TestSources/where_current_of_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/ExpandDateFunctionRule/TestSources/where_current_of_raise_0_violations.sql new file mode 100644 index 00000000..d62b7149 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/ExpandDateFunctionRule/TestSources/where_current_of_raise_0_violations.sql @@ -0,0 +1,4 @@ +UPDATE t +SET lastmod = GETDATE() +FROM t +WHERE CURRENT OF cr; diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByForcePlanRule/FightOptimizerByForcePlanRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByForcePlanRule/FightOptimizerByForcePlanRuleTests.cs new file mode 100644 index 00000000..41e13802 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByForcePlanRule/FightOptimizerByForcePlanRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Performance")] + [TestOfRule(typeof(FightOptimizerByForcePlanRule))] + public sealed class FightOptimizerByForcePlanRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(FightOptimizerByForcePlanRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByForcePlanRule/TestSources/hint_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByForcePlanRule/TestSources/hint_raise_2_violations.sql new file mode 100644 index 00000000..c3fe6003 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByForcePlanRule/TestSources/hint_raise_2_violations.sql @@ -0,0 +1,7 @@ +SET FORCEPLAN ON -- 1 + +select 1 +from foo +inner join bar +on id = parent_id +OPTION (USE PLAN '') -- 2 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByForcePlanRule/TestSources/no_hint_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByForcePlanRule/TestSources/no_hint_raise_0_violations.sql new file mode 100644 index 00000000..080de7eb --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByForcePlanRule/TestSources/no_hint_raise_0_violations.sql @@ -0,0 +1,4 @@ +select 1 +from foo +inner join bar +on id = parent_id diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByJoinHintRule/FightOptimizerByJoinHintRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByJoinHintRule/FightOptimizerByJoinHintRuleTests.cs new file mode 100644 index 00000000..9907438d --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByJoinHintRule/FightOptimizerByJoinHintRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Performance")] + [TestOfRule(typeof(FightOptimizerByJoinHintRule))] + public sealed class FightOptimizerByJoinHintRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(FightOptimizerByJoinHintRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByJoinHintRule/TestSources/hint_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByJoinHintRule/TestSources/hint_raise_1_violations.sql new file mode 100644 index 00000000..66e3d3c1 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByJoinHintRule/TestSources/hint_raise_1_violations.sql @@ -0,0 +1,4 @@ +select 1 +from foo +inner HASH join bar -- here +on id = parent_id diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByJoinHintRule/TestSources/no_hint_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByJoinHintRule/TestSources/no_hint_raise_0_violations.sql new file mode 100644 index 00000000..080de7eb --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByJoinHintRule/TestSources/no_hint_raise_0_violations.sql @@ -0,0 +1,4 @@ +select 1 +from foo +inner join bar +on id = parent_id diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByQueryOptionRule/FightOptimizerByQueryOptionRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByQueryOptionRule/FightOptimizerByQueryOptionRuleTests.cs new file mode 100644 index 00000000..92a6eebe --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByQueryOptionRule/FightOptimizerByQueryOptionRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Performance")] + [TestOfRule(typeof(FightOptimizerByQueryOptionRule))] + public sealed class FightOptimizerByQueryOptionRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(FightOptimizerByQueryOptionRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByQueryOptionRule/TestSources/allowed_option_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByQueryOptionRule/TestSources/allowed_option_raise_0_violations.sql new file mode 100644 index 00000000..a9d444d4 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByQueryOptionRule/TestSources/allowed_option_raise_0_violations.sql @@ -0,0 +1,5 @@ +select 1 +from foo +inner join bar +on id = parent_id +OPTION (RECOMPILE, MAXRECURSION 10, MAXDOP 2, OPTIMIZE FOR UNKNOWN); diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByQueryOptionRule/TestSources/hint_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByQueryOptionRule/TestSources/hint_raise_2_violations.sql new file mode 100644 index 00000000..0206dc78 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByQueryOptionRule/TestSources/hint_raise_2_violations.sql @@ -0,0 +1,11 @@ +select 1 +from foo +inner join bar +on id = parent_id +OPTION (FORCE ORDER) -- 1 + +select 1 +from foo +inner join bar +on id = parent_id +OPTION (MAXDOP 0) -- 2 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByQueryOptionRule/TestSources/no_hint_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByQueryOptionRule/TestSources/no_hint_raise_0_violations.sql new file mode 100644 index 00000000..080de7eb --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByQueryOptionRule/TestSources/no_hint_raise_0_violations.sql @@ -0,0 +1,4 @@ +select 1 +from foo +inner join bar +on id = parent_id diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByTableHintRule/FightOptimizerByTableHintRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByTableHintRule/FightOptimizerByTableHintRuleTests.cs new file mode 100644 index 00000000..b28c0de5 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByTableHintRule/FightOptimizerByTableHintRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Performance")] + [TestOfRule(typeof(FightOptimizerByTableHintRule))] + public sealed class FightOptimizerByTableHintRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(FightOptimizerByTableHintRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByTableHintRule/TestSources/hint_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByTableHintRule/TestSources/hint_raise_1_violations.sql new file mode 100644 index 00000000..74f3d9e1 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByTableHintRule/TestSources/hint_raise_1_violations.sql @@ -0,0 +1,4 @@ +select 1 +from foo WITH (FORCESEEK) -- here +inner join bar +on id = parent_id diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByTableHintRule/TestSources/no_hint_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByTableHintRule/TestSources/no_hint_raise_0_violations.sql new file mode 100644 index 00000000..080de7eb --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/FightOptimizerByTableHintRule/TestSources/no_hint_raise_0_violations.sql @@ -0,0 +1,4 @@ +select 1 +from foo +inner join bar +on id = parent_id diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/ForeignKeyIndexRule/ForeignKeyIndexRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Performance/ForeignKeyIndexRule/ForeignKeyIndexRuleTests.cs new file mode 100644 index 00000000..154d46aa --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/ForeignKeyIndexRule/ForeignKeyIndexRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Performance")] + [TestOfRule(typeof(ForeignKeyIndexRule))] + public sealed class ForeignKeyIndexRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(ForeignKeyIndexRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/ForeignKeyIndexRule/TestSources/filestream_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/ForeignKeyIndexRule/TestSources/filestream_raise_0_violations.sql new file mode 100644 index 00000000..8623ad74 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/ForeignKeyIndexRule/TestSources/filestream_raise_0_violations.sql @@ -0,0 +1,3 @@ +-- compatibility level min: 110 +CREATE TABLE files.docs_storage AS FILETABLE FILESTREAM_ON document_filestream_group +WITH (FILETABLE_COLLATE_FILENAME = Cyrillic_General_CI_AS, FILETABLE_DIRECTORY = N'docs_storage'); diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/ForeignKeyIndexRule/TestSources/indexed_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/ForeignKeyIndexRule/TestSources/indexed_raise_0_violations.sql new file mode 100644 index 00000000..5a09384f --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/ForeignKeyIndexRule/TestSources/indexed_raise_0_violations.sql @@ -0,0 +1,11 @@ +CREATE TABLE foo +( + id INT, + group_id INT, + option_a BIT, + CONSTRAINT FK_GRP FOREIGN KEY (group_id, option_a) REFERENCES foo_groups (id, option_a) +) +GO + +CREATE INDEX IX_FK ON foo(group_id, option_a) +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/ForeignKeyIndexRule/TestSources/inline_index_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/ForeignKeyIndexRule/TestSources/inline_index_raise_0_violations.sql new file mode 100644 index 00000000..636a4031 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/ForeignKeyIndexRule/TestSources/inline_index_raise_0_violations.sql @@ -0,0 +1,9 @@ +-- compatibility level min: 130 +CREATE TABLE bar +( + id INT, + category_id INT FOREIGN KEY REFERENCES bar_categories (id), + some_valued DECIMAL(18,3), + INDEX ix (category_id, some_valued) -- starting from FK fields is fine +) +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/ForeignKeyIndexRule/TestSources/no_fk_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/ForeignKeyIndexRule/TestSources/no_fk_raise_0_violations.sql new file mode 100644 index 00000000..f17029f1 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/ForeignKeyIndexRule/TestSources/no_fk_raise_0_violations.sql @@ -0,0 +1,4 @@ +CREATE TABLE #t +( + id int +) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/ForeignKeyIndexRule/TestSources/not_index_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/ForeignKeyIndexRule/TestSources/not_index_raise_1_violations.sql new file mode 100644 index 00000000..fb0edf86 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/ForeignKeyIndexRule/TestSources/not_index_raise_1_violations.sql @@ -0,0 +1,8 @@ +CREATE TABLE foo +( + id INT, + group_id INT, + option_a BIT, + CONSTRAINT FK_GRP FOREIGN KEY (group_id, option_a) REFERENCES foo_groups (id, option_a) +) +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/ForeignKeyIndexRule/TestSources/wrong_index_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/ForeignKeyIndexRule/TestSources/wrong_index_raise_1_violations.sql new file mode 100644 index 00000000..1a3b172d --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/ForeignKeyIndexRule/TestSources/wrong_index_raise_1_violations.sql @@ -0,0 +1,11 @@ +CREATE TABLE foo +( + id INT, + group_id INT, + option_a BIT, + CONSTRAINT FK_GRP FOREIGN KEY (group_id, option_a) REFERENCES foo_groups (id, option_a) +) +GO + +CREATE INDEX IX ON foo(id, option_a) +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/MixOfDdlAndDmlRule/MixOfDdlAndDmlRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Performance/MixOfDdlAndDmlRule/MixOfDdlAndDmlRuleTests.cs new file mode 100644 index 00000000..58a5ff30 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/MixOfDdlAndDmlRule/MixOfDdlAndDmlRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Performance")] + [TestOfRule(typeof(MixOfDdlAndDmlRule))] + public sealed class MixOfDdlAndDmlRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(MixOfDdlAndDmlRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/MixOfDdlAndDmlRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/MixOfDdlAndDmlRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..dd670fd9 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/MixOfDdlAndDmlRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,45 @@ +-- empty body +CREATE PROC dbo.foo +AS; +GO + +CREATE TRIGGER dbo.foo ON dbo.bar +AFTER INSERT +AS +BEGIN + return; +end; +GO + +-- fine order +CREATE PROC dbo.foo +AS +BEGIN + CREATE TABLE #t + ( + id INT + ) + + CREATE INDEX ix ON #t(id) + + UPDATE t SET + lastmod = GETDATE() + FROM dbo.bar as t + WHERE is_for_select = 1 +END +GO + +CREATE TRIGGER dbo.foo ON dbo.bar +AFTER INSERT +AS +BEGIN + CREATE TABLE #t + ( + id INT + ) + + INSERT #t(id) + SELECT id + FROM INSERTED +END +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/MixOfDdlAndDmlRule/TestSources/dml_no_from_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/MixOfDdlAndDmlRule/TestSources/dml_no_from_raise_0_violations.sql new file mode 100644 index 00000000..42177b2b --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/MixOfDdlAndDmlRule/TestSources/dml_no_from_raise_0_violations.sql @@ -0,0 +1,31 @@ +-- compatibility level min: 130 +-- becaust of OPENJSON +CREATE PROC dbo.foo + @on_date DATETIME + , @list VARCHAR(100) +AS +BEGIN + CREATE TABLE #t + ( + id INT + ) + + -- not a "real DML" + INSERT @t(id) + SELECT 1 + + SELECT @on_date = ISNULL(@on_date, EOMONTH(GETDATE())); + + SELECT @s = value + FROM STRING_SPLIT(@list, ';') + + SELECT * FROM OPENJSON('{}') + + ALTER TABLE #t ADD title VARCHAR(100) + + INSERT #t(title) + SELECT id + FROM dbo.bar + WHERE is_for_select = 1 +END +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/MixOfDdlAndDmlRule/TestSources/external_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/MixOfDdlAndDmlRule/TestSources/external_raise_0_violations.sql new file mode 100644 index 00000000..f71c283e --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/MixOfDdlAndDmlRule/TestSources/external_raise_0_violations.sql @@ -0,0 +1,6 @@ +CREATE PROCEDURE dbo.make_web_request + @url NVARCHAR(4000) + , @err NVARCHAR(MAX) OUTPUT +WITH EXECUTE AS CALLER +AS +EXTERNAL NAME [Sql.Web.Rest].StoredProcedures.WebRequestInvoke; diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/MixOfDdlAndDmlRule/TestSources/mix_in_proc_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/MixOfDdlAndDmlRule/TestSources/mix_in_proc_raise_1_violations.sql new file mode 100644 index 00000000..c98328ee --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/MixOfDdlAndDmlRule/TestSources/mix_in_proc_raise_1_violations.sql @@ -0,0 +1,22 @@ +CREATE PROC dbo.foo +AS +BEGIN + CREATE TABLE #t + ( + id INT + ) + + SELECT id + FROM dbo.bar + INNER JOIN #t + ON id = parent_id + WHERE is_for_select = 1 + + ALTER TABLE #t ADD title VARCHAR(100) -- here + + DELETE bar + FROM dbo.bar bar + INNER JOIN #t t ON t.id = bar.id + OPTION (FORCE ORDER) +END +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/MixOfDdlAndDmlRule/TestSources/mix_in_trigger_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/MixOfDdlAndDmlRule/TestSources/mix_in_trigger_raise_1_violations.sql new file mode 100644 index 00000000..7e74c6cc --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/MixOfDdlAndDmlRule/TestSources/mix_in_trigger_raise_1_violations.sql @@ -0,0 +1,22 @@ +CREATE TRIGGER dbo.foo ON dbo.bar +AFTER INSERT +AS +BEGIN + CREATE TABLE #t + ( + id INT + ) + + INSERT #t(id) + SELECT id + FROM INSERTED + WHERE is_for_select = 1 + + ALTER TABLE #t ADD title VARCHAR(100) -- here + + INSERT #t(title) + SELECT id + FROM dbo.bar + WHERE is_for_select = 1 +END +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/MixOfDdlAndDmlRule/TestSources/recompile_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/MixOfDdlAndDmlRule/TestSources/recompile_raise_0_violations.sql new file mode 100644 index 00000000..2f3873e4 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/MixOfDdlAndDmlRule/TestSources/recompile_raise_0_violations.sql @@ -0,0 +1,22 @@ +CREATE PROC dbo.foo +AS +BEGIN + CREATE TABLE #t + ( + id INT + ) + + INSERT #t(id) + SELECT id + FROM dbo.bar + WHERE is_for_select = 1 + + ALTER TABLE #t ADD title VARCHAR(100) + + INSERT #t(title) + SELECT id + FROM dbo.bar + WHERE is_for_select = 1 + OPTION (RECOMPILE) -- recompilaton is what we want +END +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/NoEqualityFilterInJoinRule/NoEqualityFilterInJoinRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Performance/NoEqualityFilterInJoinRule/NoEqualityFilterInJoinRuleTests.cs new file mode 100644 index 00000000..88919927 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/NoEqualityFilterInJoinRule/NoEqualityFilterInJoinRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Performance")] + [TestOfRule(typeof(NoEqualityFilterInJoinRule))] + public sealed class NoEqualityFilterInJoinRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(NoEqualityFilterInJoinRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/NoEqualityFilterInJoinRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/NoEqualityFilterInJoinRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..3370db91 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/NoEqualityFilterInJoinRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,16 @@ +SELECT * +FROM foo +INNER JOIN bar +ON x = y + +SELECT * +FROM foo +FULL JOIN bar +ON bar.x = foo.y +OR (bar.z = foo.t) + +SELECT * +FROM foo +FULL JOIN bar +ON bar.x BETWEEN 1 AND foo.y +AND (bar.z > @z) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/NoEqualityFilterInJoinRule/TestSources/bad_filter_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/NoEqualityFilterInJoinRule/TestSources/bad_filter_raise_2_violations.sql new file mode 100644 index 00000000..4bf6df45 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/NoEqualityFilterInJoinRule/TestSources/bad_filter_raise_2_violations.sql @@ -0,0 +1,10 @@ +SELECT * +FROM foo +INNER JOIN bar +ON x > y + +SELECT * +FROM foo +FULL JOIN bar +ON @a = 1 +OR bar.value IS NOT NULL diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/NoEqualityFilterInWhereRule/NoEqualityFilterInWhereRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Performance/NoEqualityFilterInWhereRule/NoEqualityFilterInWhereRuleTests.cs new file mode 100644 index 00000000..d902ce27 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/NoEqualityFilterInWhereRule/NoEqualityFilterInWhereRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Performance")] + [TestOfRule(typeof(NoEqualityFilterInWhereRule))] + public sealed class NoEqualityFilterInWhereRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(NoEqualityFilterInWhereRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/NoEqualityFilterInWhereRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/NoEqualityFilterInWhereRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..3668ea8d --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/NoEqualityFilterInWhereRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,37 @@ +-- no where +SELECT * +FROM foo +INNER JOIN bar +ON x = y + +-- no from +SELECT * +WHERE @a > 1 + +-- temp tables are ignored +SELECT * +FROM @foo +WHERE value > 1 + +SELECT * +FROM #foo +WHERE x <= z + +-- good filter +SELECT * +FROM foo +INNER JOIN bar +ON x = y +WHERE bar.date_year = @year + +SELECT * +FROM foo +INNER JOIN bar +ON x = y +WHERE bar.name LIKE 'asdf%' + +SELECT * +FROM foo +INNER JOIN bar +ON x = y +WHERE bar.parent_id IN (1, 2, @three) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/NoEqualityFilterInWhereRule/TestSources/bad_filter_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/NoEqualityFilterInWhereRule/TestSources/bad_filter_raise_2_violations.sql new file mode 100644 index 00000000..46b6c823 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/NoEqualityFilterInWhereRule/TestSources/bad_filter_raise_2_violations.sql @@ -0,0 +1,11 @@ +SELECT * +FROM foo +INNER JOIN bar +ON x = y +WHERE bar.date > @year_begin + +SELECT * +FROM foo +LEFT JOIN bar +ON x = y +WHERE bar.flag IS NOT NULL diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/OptionalParameterIsNullPredicateRule/OptionalParameterIsNullPredicateRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Performance/OptionalParameterIsNullPredicateRule/OptionalParameterIsNullPredicateRuleTests.cs new file mode 100644 index 00000000..da8bf6fe --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/OptionalParameterIsNullPredicateRule/OptionalParameterIsNullPredicateRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Performance")] + [TestOfRule(typeof(OptionalParameterIsNullPredicateRule))] + public class OptionalParameterIsNullPredicateRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(OptionalParameterIsNullPredicateRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/OptionalParameterIsNullPredicateRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/OptionalParameterIsNullPredicateRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..d6295604 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/OptionalParameterIsNullPredicateRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,31 @@ +-- fine looking filter +select * +from orders +where client_id = @client_id OR @client_id IS NULL + +-- no col involved +select * +from orders +where ISNULL(@var, 0) = 123 + +-- different cols +select * +from orders +where client_id = ISNULL(@client_id, company_id) + +-- not a column on the left side +select * +from orders o +join clients c +on o.client_id = c.client_id +AND @filter_value = ISNULL(@tariff_id, c.tariff_id) + +-- not a var in ISNULL +select * +from orders +where client_id = ISNULL(dbo.my_func(), client_id) + +-- a > b +select * +from clients c +WHERE c.tariff_id > COALESCE(@tariff_id, c.tariff_id) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/OptionalParameterIsNullPredicateRule/TestSources/bad_predicate_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/OptionalParameterIsNullPredicateRule/TestSources/bad_predicate_raise_2_violations.sql new file mode 100644 index 00000000..99b6afee --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/OptionalParameterIsNullPredicateRule/TestSources/bad_predicate_raise_2_violations.sql @@ -0,0 +1,9 @@ +select * +from orders o +where ((ISNULL((@client_id), o.client_id)) = (o.client_id)) + +select * +from orders o +join clients c +on o.client_id = c.client_id +AND c.tariff_id = COALESCE(@tariff_id, c.tariff_id) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/OptionalParameterIsNullPredicateRule/TestSources/where_current_of_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/OptionalParameterIsNullPredicateRule/TestSources/where_current_of_raise_0_violations.sql new file mode 100644 index 00000000..d62b7149 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/OptionalParameterIsNullPredicateRule/TestSources/where_current_of_raise_0_violations.sql @@ -0,0 +1,4 @@ +UPDATE t +SET lastmod = GETDATE() +FROM t +WHERE CURRENT OF cr; diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/SerialPlanForcedByTableVariableRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/SerialPlanForcedByTableVariableRuleTests.cs new file mode 100644 index 00000000..d88e206c --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/SerialPlanForcedByTableVariableRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Performance")] + [TestOfRule(typeof(SerialPlanForcedByTableVariableRule))] + public sealed class SerialPlanForcedByTableVariableRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(SerialPlanForcedByTableVariableRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..22a1fb3f --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,14 @@ +SELECT 1 +FROM foo +INNER JOIN bar +ON id = parent_id + +INSERT INTO foo(title) +SELECT title +FROM @bar + +INSERT INTO foo(title) + OUTPUT INSERTED.id + INTO hist.foo_records(inserted_id) +SELECT title +FROM bar diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/TestSources/from_openjson_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/TestSources/from_openjson_raise_0_violations.sql new file mode 100644 index 00000000..38f60d9f --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/TestSources/from_openjson_raise_0_violations.sql @@ -0,0 +1,8 @@ +-- compatibility level min: 130 +INSERT @batch (group_id, group_code) +SELECT + group_id + , group_code +FROM + OPENJSON(@data) + WITH (group_id INT, group_code VARCHAR(100)); diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/TestSources/from_openxml_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/TestSources/from_openxml_raise_0_violations.sql new file mode 100644 index 00000000..e166d39c --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/TestSources/from_openxml_raise_0_violations.sql @@ -0,0 +1,7 @@ +INSERT @Customers(cust_id, contact_name) +SELECT * +FROM OPENXML(@idoc, '/ROOT/Customer', 1) +WITH ( + CustomerID VARCHAR(10), + ContactName VARCHAR(20) +); diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/TestSources/from_string_split_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/TestSources/from_string_split_raise_0_violations.sql new file mode 100644 index 00000000..8234d1b1 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/TestSources/from_string_split_raise_0_violations.sql @@ -0,0 +1,6 @@ +-- compatibility level min: 130 +INSERT INTO @codes (code) +SELECT DISTINCT + TRY_CAST(value AS VARCHAR(20)) AS code +FROM STRING_SPLIT(@code_list, ',') +WHERE value <> ''; diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/TestSources/from_temp_table_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/TestSources/from_temp_table_raise_0_violations.sql new file mode 100644 index 00000000..8b65b03f --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/TestSources/from_temp_table_raise_0_violations.sql @@ -0,0 +1,8 @@ +INSERT INTO @foo(title) +SELECT * FROM #bar + +INSERT INTO @foo(title) +SELECT * FROM INSERTED + +INSERT INTO @foo(title) +SELECT * FROM DELETED diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/TestSources/natively_compiled_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/TestSources/natively_compiled_raise_0_violations.sql new file mode 100644 index 00000000..4a2bc03d --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/TestSources/natively_compiled_raise_0_violations.sql @@ -0,0 +1,17 @@ +-- compatibility level min: 130 +CREATE PROC dbo.foo +WITH NATIVE_COMPILATION, SCHEMABINDING, EXECUTE AS OWNER +AS +BEGIN ATOMIC WITH (TRANSACTION ISOLATION LEVEL = SNAPSHOT, LANGUAGE = N'English') + DECLARE @t TABLE (id INT) + + INSERT @t(id) + SELECT id + FROM bar + INNER JOIN far + on id = parent_id + WHERE is_disabled = 0 + + RETURN 1 +END +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/TestSources/simple_source_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/TestSources/simple_source_raise_0_violations.sql new file mode 100644 index 00000000..f4bd2437 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/TestSources/simple_source_raise_0_violations.sql @@ -0,0 +1,10 @@ +-- per-item insert +INSERT INTO @foo(title) +VALUES ('asdf') + +-- no from +DELETE @foo + +-- from another table variable +INSERT INTO @foo(title) +select * from @bar as b diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/TestSources/table_var_dml_raise_3_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/TestSources/table_var_dml_raise_3_violations.sql new file mode 100644 index 00000000..1005dcbe --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedByTableVariableRule/TestSources/table_var_dml_raise_3_violations.sql @@ -0,0 +1,12 @@ +INSERT INTO @foo(title) -- 1 +SELECT title +FROM bar + +DELETE @foo -- 2 +FROM bar + +UPDATE @foo SET -- 3 + is_archived = 1 +FROM bar +WHERE dt < @range_start +and bar.id = @foo.parent_id diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedRule/SerialPlanForcedRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedRule/SerialPlanForcedRuleTests.cs new file mode 100644 index 00000000..5957dd8d --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedRule/SerialPlanForcedRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Performance")] + [TestOfRule(typeof(SerialPlanForcedRule))] + public sealed class SerialPlanForcedRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(SerialPlanForcedRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..22a1fb3f --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,14 @@ +SELECT 1 +FROM foo +INNER JOIN bar +ON id = parent_id + +INSERT INTO foo(title) +SELECT title +FROM @bar + +INSERT INTO foo(title) + OUTPUT INSERTED.id + INTO hist.foo_records(inserted_id) +SELECT title +FROM bar diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedRule/TestSources/maxdop_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedRule/TestSources/maxdop_raise_1_violations.sql new file mode 100644 index 00000000..99d75d35 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedRule/TestSources/maxdop_raise_1_violations.sql @@ -0,0 +1,5 @@ +SELECT 1 +FROM foo +INNER JOIN bar +ON id = parent_id +OPTION (MAXDOP 1) -- here diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedRule/TestSources/output_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedRule/TestSources/output_raise_1_violations.sql new file mode 100644 index 00000000..5341f0ea --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedRule/TestSources/output_raise_1_violations.sql @@ -0,0 +1,4 @@ +INSERT INTO foo(title) +OUTPUT INSERTED.id -- here +SELECT title +FROM bar diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedRule/TestSources/remote_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedRule/TestSources/remote_raise_1_violations.sql new file mode 100644 index 00000000..29b6e647 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanForcedRule/TestSources/remote_raise_1_violations.sql @@ -0,0 +1,4 @@ +SELECT 1 +FROM foo +INNER REMOTE JOIN bar -- here +ON id = parent_id diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanZoneForcedRule/SerialPlanZoneForcedRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanZoneForcedRule/SerialPlanZoneForcedRuleTests.cs new file mode 100644 index 00000000..7b8d7449 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanZoneForcedRule/SerialPlanZoneForcedRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Performance")] + [TestOfRule(typeof(SerialPlanZoneForcedRule))] + public sealed class SerialPlanZoneForcedRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(SerialPlanZoneForcedRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanZoneForcedRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanZoneForcedRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..82f7ee5f --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanZoneForcedRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,4 @@ +SELECT 1 +FROM foo +INNER JOIN bar +ON id = parent_id diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanZoneForcedRule/TestSources/bad_plan_raise_4_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanZoneForcedRule/TestSources/bad_plan_raise_4_violations.sql new file mode 100644 index 00000000..5bccf5ac --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SerialPlanZoneForcedRule/TestSources/bad_plan_raise_4_violations.sql @@ -0,0 +1,7 @@ +SELECT TOP(100) -- 1 + ROW_NUMBER() -- 2 + OVER(ORDER BY parent_id) +FROM foo +INNER LOOP JOIN bar -- 3 +ON id = parent_id +OPTION (MAXRECURSION 10) -- 4 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SingleUserModeZoneForcedRule/SingleUserModeZoneForcedRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SingleUserModeZoneForcedRule/SingleUserModeZoneForcedRuleTests.cs new file mode 100644 index 00000000..5a50e4f6 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SingleUserModeZoneForcedRule/SingleUserModeZoneForcedRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Performance")] + [TestOfRule(typeof(SingleUserModeZoneForcedRule))] + public sealed class SingleUserModeZoneForcedRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(SingleUserModeZoneForcedRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SingleUserModeZoneForcedRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SingleUserModeZoneForcedRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..6f12761e --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SingleUserModeZoneForcedRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,12 @@ +-- no hints +SELECT * +FROM dbo.foo + +DELETE dbo.foo + +-- fine hints +SELECT * +FROM dbo.foo WITH (ROWLOCK, HOLDLOCK) + +DELETE dbo.foo +OPTION (FORCE ORDER, MAXDOP 2) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SingleUserModeZoneForcedRule/TestSources/applock_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SingleUserModeZoneForcedRule/TestSources/applock_raise_2_violations.sql new file mode 100644 index 00000000..294ec707 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SingleUserModeZoneForcedRule/TestSources/applock_raise_2_violations.sql @@ -0,0 +1,3 @@ +EXEC sys.sp_getapplock + +EXEC sp_getapplock diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SingleUserModeZoneForcedRule/TestSources/hints_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SingleUserModeZoneForcedRule/TestSources/hints_raise_2_violations.sql new file mode 100644 index 00000000..e5dce964 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SingleUserModeZoneForcedRule/TestSources/hints_raise_2_violations.sql @@ -0,0 +1,5 @@ +SELECT * +FROM dbo.foo WITH (TABLOCKX) + +DELETE dbo.foo +OPTION (TABLE HINT(FORCESEEK, TABLOCK, UPDLOCK)) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SubstringPredicateToLikeRule/TestSources/where_current_of_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SubstringPredicateToLikeRule/TestSources/where_current_of_raise_0_violations.sql new file mode 100644 index 00000000..d62b7149 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SubstringPredicateToLikeRule/TestSources/where_current_of_raise_0_violations.sql @@ -0,0 +1,4 @@ +UPDATE t +SET lastmod = GETDATE() +FROM t +WHERE CURRENT OF cr; diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/TempTableCachingRule/TempTableCachingRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Performance/TempTableCachingRule/TempTableCachingRuleTests.cs new file mode 100644 index 00000000..b353f6dd --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/TempTableCachingRule/TempTableCachingRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Performance")] + [TestOfRule(typeof(TempTableCachingRule))] + public sealed class TempTableCachingRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(TempTableCachingRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/TempTableCachingRule/TestSources/external_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/TempTableCachingRule/TestSources/external_raise_0_violations.sql new file mode 100644 index 00000000..f71c283e --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/TempTableCachingRule/TestSources/external_raise_0_violations.sql @@ -0,0 +1,6 @@ +CREATE PROCEDURE dbo.make_web_request + @url NVARCHAR(4000) + , @err NVARCHAR(MAX) OUTPUT +WITH EXECUTE AS CALLER +AS +EXTERNAL NAME [Sql.Web.Rest].StoredProcedures.WebRequestInvoke; diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/TempTableCachingRule/TestSources/no_temp_table_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/TempTableCachingRule/TestSources/no_temp_table_raise_0_violations.sql new file mode 100644 index 00000000..7685708c --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/TempTableCachingRule/TestSources/no_temp_table_raise_0_violations.sql @@ -0,0 +1,16 @@ +CREATE PROC foo + @client_id INT +WITH RECOMPILE +AS +BEGIN + SET NOCOUNT ON; + + SELECT * FROM dbo.bar + WHERE client_id = @client_id + + ALTER TABLE dbo.far + DROP CONSTRAINT CS + + RETURN 1 +END +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/TempTableCachingRule/TestSources/proc_raise_6_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/TempTableCachingRule/TestSources/proc_raise_6_violations.sql new file mode 100644 index 00000000..b3cfd9c6 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/TempTableCachingRule/TestSources/proc_raise_6_violations.sql @@ -0,0 +1,34 @@ +CREATE PROC foo + @client_id INT +WITH EXECUTE AS OWNER, +RECOMPILE -- 1 - RECOMPILE +AS +BEGIN + SET NOCOUNT ON; + + CREATE TABLE #tmp + ( + id INT + ) + + DROP TABLE #tmp + + CREATE TABLE #tmp -- 2 - name reused + ( + another_id INT + , CONSTRAINT pk -- 3 - named constraint + PRIMARY KEY (another_id) + ) + + ALTER TABLE #tmp -- 4 - DDL ALTER + ADD title VARCHAR(100) + + CREATE INDEX ix -- 5 - DDL INDEX + ON #tmp (another_id) + + CREATE STATISTICS st -- 6 - DDL STATISTICS + ON #tmp (title) + + RETURN 1 +END +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/TempTableCachingRule/TestSources/proc_var_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/TempTableCachingRule/TestSources/proc_var_raise_1_violations.sql new file mode 100644 index 00000000..584928f3 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/TempTableCachingRule/TestSources/proc_var_raise_1_violations.sql @@ -0,0 +1,13 @@ +CREATE PROC foo + @client_id INT +WITH EXECUTE AS OWNER, +RECOMPILE -- 1 - RECOMPILE +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @t TABLE(id INT) + + RETURN 1 +END +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/TempTableCachingRule/TestSources/trigger_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/TempTableCachingRule/TestSources/trigger_raise_1_violations.sql new file mode 100644 index 00000000..c655798b --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/TempTableCachingRule/TestSources/trigger_raise_1_violations.sql @@ -0,0 +1,14 @@ +CREATE TRIGGER tr ON foo +AFTER INSERT, UPDATE, DELETE +AS +BEGIN + SET NOCOUNT ON; + + CREATE TABLE #tmp + ( + another_id INT + , CONSTRAINT pk -- named constraint + PRIMARY KEY (another_id) + ) +END +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIsNullForInequalityRule/RedundantIsNullForInequalityRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIsNullForInequalityRule/RedundantIsNullForInequalityRuleTests.cs new file mode 100644 index 00000000..256ddbd0 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIsNullForInequalityRule/RedundantIsNullForInequalityRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Redundancy")] + [TestOfRule(typeof(RedundantIsNullForInequalityRule))] + public sealed class RedundantIsNullForInequalityRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(RedundantIsNullForInequalityRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIsNullForInequalityRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIsNullForInequalityRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..398f22fe --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIsNullForInequalityRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,14 @@ +-- fine ISNULL +select * +from foo +where col > ISNULL(@arg, 1) + +-- no ISNULL +select * +from foo +where col > @arg + +-- equality is allowed +select * +from foo +where col >= ISNULL(@arg, col) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIsNullForInequalityRule/TestSources/redundancy_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIsNullForInequalityRule/TestSources/redundancy_raise_2_violations.sql new file mode 100644 index 00000000..2b6ed031 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIsNullForInequalityRule/TestSources/redundancy_raise_2_violations.sql @@ -0,0 +1,7 @@ +select * +from foo +where foo.col <> ISNULL(@arg, foo.col) + +select * +from foo +where (ISNULL(@arg, foo.col)) > (foo.col) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantMaxRecursionRule/RedundantMaxRecursionRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantMaxRecursionRule/RedundantMaxRecursionRuleTests.cs new file mode 100644 index 00000000..bd94e8af --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantMaxRecursionRule/RedundantMaxRecursionRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Redundancy")] + [TestOfRule(typeof(RedundantMaxRecursionRule))] + public sealed class RedundantMaxRecursionRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(RedundantMaxRecursionRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantMaxRecursionRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantMaxRecursionRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..edd6558a --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantMaxRecursionRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,26 @@ +-- bare select +SELECT 1 + +-- no options +with cte AS(select id from foo) +select * from cte + +-- no cte +select * from cte +option(FORCE ORDER) + +-- fine recursive cte +with cte AS( + select id, parent_id + from foo + where parent_id is null + + union all + + select f.id, f.parent_id + from cte c + inner join foo f + on f.parent_id = c.id +) +select * from cte +option(MAXRECURSION 10) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantMaxRecursionRule/TestSources/redundancy_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantMaxRecursionRule/TestSources/redundancy_raise_2_violations.sql new file mode 100644 index 00000000..451a013b --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantMaxRecursionRule/TestSources/redundancy_raise_2_violations.sql @@ -0,0 +1,12 @@ +-- no cte +select * from cte +option(MAXRECURSION 10) + +-- cte is not recursive +with cte AS( + select id, parent_id + from foo + where parent_id is null +) +select * from cte +option(MAXRECURSION 10) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantSelectScalarRule/TestSources/join_scalar_subquery_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantSelectScalarRule/TestSources/join_scalar_subquery_raise_0_violations.sql new file mode 100644 index 00000000..23fa3a4d --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantSelectScalarRule/TestSources/join_scalar_subquery_raise_0_violations.sql @@ -0,0 +1,2 @@ +SELECT t.pos +FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/UnusedWindowClauseRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/UnusedWindowClauseRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..9128dd81 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/UnusedWindowClauseRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,27 @@ + +SELECT 1 +FROM Sales.SalesOrderDetail + +SELECT SalesOrderID, + ProductID, + OrderQty, + SUM(OrderQty) OVER (PARTITION BY SalesOrderID) AS [Total] +FROM Sales.SalesOrderDetail + +SELECT SalesOrderID, + ProductID, + OrderQty, + SUM(OrderQty) OVER win AS [Total] +FROM Sales.SalesOrderDetail +WHERE SalesOrderID IN (43659, 43664) +WINDOW win AS (PARTITION BY SalesOrderID); + +SELECT SalesOrderID, + ProductID, + OrderQty, + SUM(OrderQty) OVER foo AS [Total] +FROM Sales.SalesOrderDetail +WHERE SalesOrderID IN (43659, 43664) +WINDOW + foo AS (bar), -- bar name reused here + bar AS (PARTITION BY SalesOrderID); diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/UnusedWindowClauseRule/TestSources/window_never_used_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/UnusedWindowClauseRule/TestSources/window_never_used_raise_2_violations.sql new file mode 100644 index 00000000..66fbbfa4 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/UnusedWindowClauseRule/TestSources/window_never_used_raise_2_violations.sql @@ -0,0 +1,17 @@ +SELECT SalesOrderID, + ProductID, + OrderQty, + SUM(OrderQty) OVER (PARTITION BY SalesOrderID) AS [Total] +FROM Sales.SalesOrderDetail +WHERE SalesOrderID IN (43659, 43664) +WINDOW win AS (PARTITION BY SalesOrderID); -- 1 + +SELECT SalesOrderID, + ProductID, + OrderQty, + SUM(OrderQty) OVER foo AS [Total] +FROM Sales.SalesOrderDetail +WHERE SalesOrderID IN (43659, 43664) +WINDOW + foo AS (PARTITION BY SalesOrderID ORDER BY SalesOrderID), + bar AS (PARTITION BY ProductID); -- 2 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/UnusedWindowClauseRule/UnusedWindowClauseRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/UnusedWindowClauseRule/UnusedWindowClauseRuleTests.cs new file mode 100644 index 00000000..f402c6ab --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/UnusedWindowClauseRule/UnusedWindowClauseRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Redundancy")] + [TestOfRule(typeof(UnusedWindowClauseRule))] + public sealed class UnusedWindowClauseRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(UnusedWindowClauseRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/AskingPropertyNotTypeRule/AskingPropertyNotTypeRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/AskingPropertyNotTypeRule/AskingPropertyNotTypeRuleTests.cs new file mode 100644 index 00000000..ff72ddff --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/AskingPropertyNotTypeRule/AskingPropertyNotTypeRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Simplification")] + [TestOfRule(typeof(AskingPropertyNotTypeRule))] + public sealed class AskingPropertyNotTypeRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(AskingPropertyNotTypeRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/AskingPropertyNotTypeRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/AskingPropertyNotTypeRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..06a6759b --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/AskingPropertyNotTypeRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,3 @@ +SELECT OBJECTPROPERTYEX(1, 'IsView') + +SELECT OBJECT_ID('foo', 'TR') diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/AskingPropertyNotTypeRule/TestSources/property_type_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/AskingPropertyNotTypeRule/TestSources/property_type_raise_2_violations.sql new file mode 100644 index 00000000..11dd95ea --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/AskingPropertyNotTypeRule/TestSources/property_type_raise_2_violations.sql @@ -0,0 +1,3 @@ +SELECT OBJECTPROPERTYEX(((OBJECT_ID('foo'))), 'IsView') + +SELECT OBJECTPROPERTY(OBJECT_ID('foo'), (('IsTrigger'))) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractExpressionRule/ExtractExpressionRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractExpressionRule/ExtractExpressionRuleTests.cs new file mode 100644 index 00000000..31d0db7a --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractExpressionRule/ExtractExpressionRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Simplification")] + [TestOfRule(typeof(ExtractExpressionRule))] + public sealed class ExtractExpressionRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(ExtractExpressionRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractExpressionRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractExpressionRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..f85dd2da --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractExpressionRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,16 @@ +-- all expressions are different +SELECT + NULL as id, + (1 + 1) as num, + dbo.my_fn(x, y) as z +from foo +inner join bar +on parent_id = some_id - 1 +order by id + +-- order by should be ignored - there is a separate rule for that +SELECT + NULL as id, + (1 + 1) as num +from foo +order by (1 + 1) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractExpressionRule/TestSources/case_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractExpressionRule/TestSources/case_raise_2_violations.sql new file mode 100644 index 00000000..ac6c17d6 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractExpressionRule/TestSources/case_raise_2_violations.sql @@ -0,0 +1,27 @@ +SELECT + CASE + WHEN a > b THEN 1 + WHEN b < 100 THEN 2 + ELSE 3 + END +FROM foo +WHERE + CASE + WHEN a > b THEN 1 + WHEN b < 100 THEN 2 + ELSE 3 + END > 0 + +SELECT + CASE a + WHEN 'a' THEN 1 + WHEN 'b' THEN 2 + ELSE 3 + END +FROM foo +WHERE + CASE a + WHEN 'a' THEN 1 + WHEN 'b' THEN 2 + ELSE 3 + END IS NOT NULL diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractExpressionRule/TestSources/different_subquery_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractExpressionRule/TestSources/different_subquery_raise_0_violations.sql new file mode 100644 index 00000000..364f6a0d --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractExpressionRule/TestSources/different_subquery_raise_0_violations.sql @@ -0,0 +1,11 @@ +SELECT num1, num2 +FROM +( + SELECT (1 + 2 * value) as num1 + FROM foo +) as foo +CROSS JOIN +( + SELECT (1 + 2 * value) as num2 + FROM bar +) as bar diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractExpressionRule/TestSources/extract_raise_4_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractExpressionRule/TestSources/extract_raise_4_violations.sql new file mode 100644 index 00000000..2155fc3d --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractExpressionRule/TestSources/extract_raise_4_violations.sql @@ -0,0 +1,10 @@ +SELECT + NULL as id, + (1 + 2 * 3) as num, + dbo.my_fn(x, y) as z, + (dbo.my_fn(x, y)) as pin -- 1 +from foo +inner join bar +on parent_id = dbo.my_fn(x, y) -- 2 +and volume > (1 + 2 * 3) -- 3 +group by dbo.my_fn(x, y) -- 4 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractWindowClauseRule/ExtractWindowClauseRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractWindowClauseRule/ExtractWindowClauseRuleTests.cs new file mode 100644 index 00000000..6bd247d6 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractWindowClauseRule/ExtractWindowClauseRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Simplification")] + [TestOfRule(typeof(ExtractWindowClauseRule))] + public sealed class ExtractWindowClauseRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(ExtractWindowClauseRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractWindowClauseRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractWindowClauseRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..bc5f272d --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractWindowClauseRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,45 @@ +-- different windows +SELECT SalesOrderID, + ProductID, + OrderQty, + SUM(OrderQty) OVER (PARTITION BY SalesOrderID ORDER BY ProducId ASC) AS [Total], + SUM(OrderQty) OVER (PARTITION BY SalesOrderID ORDER BY ProducId DESC) AS [TotalDesc], + AVG(OrderQty) OVER (PARTITION BY SalesOrderID) AS [Avg], + COUNT(OrderQty) OVER (PARTITION BY ProductID ORDER BY SalesOrderID) AS [Count], + MIN(OrderQty) OVER (ORDER BY ProductID) AS [Min], + MAX(OrderQty) OVER (PARTITION BY ProductID ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS [Max], + MAX(OrderQty) OVER (PARTITION BY ProductID RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS [Max2], + MAX(OrderQty) OVER (PARTITION BY ProductID ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING) AS [Max2] +FROM Sales.SalesOrderDetail +WHERE SalesOrderID IN (43659, 43664); + +-- window clause +SELECT SalesOrderID, + ProductID, + OrderQty, + SUM(OrderQty) OVER win AS [Total], + AVG(OrderQty) OVER win AS [Avg], + COUNT(OrderQty) OVER win AS [Count], + MIN(OrderQty) OVER win AS [Min], + MAX(OrderQty) OVER win AS [Max] +FROM Sales.SalesOrderDetail +WHERE SalesOrderID IN (43659, 43664) +WINDOW win AS (PARTITION BY SalesOrderID); + + +SELECT SalesOrderID, + ProductID, + OrderQty, + COUNT(OrderQty) OVER foo AS [Count] +FROM Sales.SalesOrderDetail +WHERE SalesOrderID IN (43659, 43664) +WINDOW bar AS (PARTITION BY SalesOrderID); + +-- different ranges +SELECT SalesOrderID, + ProductID, + OrderQty, + SUM(OrderQty) OVER (ORDER BY SalesOrderID ROWS BETWEEN 3 PRECEDING AND 2 FOLLOWING) AS [Total], + AVG(OrderQty) OVER (ORDER BY SalesOrderID ROWS BETWEEN 2 PRECEDING AND 3 FOLLOWING) AS [Avg] +FROM Sales.SalesOrderDetail +WHERE SalesOrderID IN (43659, 43664); diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractWindowClauseRule/TestSources/same_offset_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractWindowClauseRule/TestSources/same_offset_raise_1_violations.sql new file mode 100644 index 00000000..ae3e23d5 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractWindowClauseRule/TestSources/same_offset_raise_1_violations.sql @@ -0,0 +1,7 @@ +SELECT SalesOrderID, + ProductID, + OrderQty, + SUM(OrderQty) OVER (PARTITION BY SalesOrderID ROWS BETWEEN 2 PRECEDING AND 3 FOLLOWING) AS [Total], + AVG(OrderQty) OVER (PARTITION BY SalesOrderID ROWS BETWEEN 2 PRECEDING AND 3 FOLLOWING) AS [Avg] +FROM Sales.SalesOrderDetail +WHERE SalesOrderID IN (43659, 43664); diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractWindowClauseRule/TestSources/same_window_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractWindowClauseRule/TestSources/same_window_raise_2_violations.sql new file mode 100644 index 00000000..e2bc44e3 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ExtractWindowClauseRule/TestSources/same_window_raise_2_violations.sql @@ -0,0 +1,11 @@ +SELECT SalesOrderID, + ProductID, + OrderQty, + SUM(OrderQty) OVER (PARTITION BY SalesOrderID ORDER BY OrderQty) AS [Total], + -- ASC is the default sort order + AVG(OrderQty) OVER (PARTITION BY SalesOrderID ORDER BY OrderQty ASC) AS [Avg], -- 1 + -- RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW is the default range definition + COUNT(OrderQty) OVER (PARTITION BY [SalesOrderID] ORDER BY OrderQty + RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS [Count] -- 2 +FROM Sales.SalesOrderDetail +WHERE SalesOrderID IN (43659, 43664); diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInsertValuesIntoOneRule/MultipleInsertValuesIntoOneRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInsertValuesIntoOneRule/MultipleInsertValuesIntoOneRuleTests.cs new file mode 100644 index 00000000..febb9c58 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInsertValuesIntoOneRule/MultipleInsertValuesIntoOneRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Simplification")] + [TestOfRule(typeof(MultipleInsertValuesIntoOneRule))] + public sealed class MultipleInsertValuesIntoOneRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(MultipleInsertValuesIntoOneRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInsertValuesIntoOneRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInsertValuesIntoOneRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..9bb73630 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInsertValuesIntoOneRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,21 @@ +insert @t +values (1, 2) + +-- too big difference in inserted cols +insert @t +values (1, 2, 3, 4, 5, 6, 7) + +insert foo +values ('') + +-- insert with select source +insert foo +select 1 +from bar + +-- not an insert +update t set + lastmod = getdate() + output inserted.lastmod + into foo (lastmod) +from bar diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInsertValuesIntoOneRule/TestSources/one_after_another_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInsertValuesIntoOneRule/TestSources/one_after_another_raise_2_violations.sql new file mode 100644 index 00000000..a63e4ad2 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInsertValuesIntoOneRule/TestSources/one_after_another_raise_2_violations.sql @@ -0,0 +1,13 @@ +insert @t +values (1, 2) + +insert @t +values (1, 2, 3) -- 1 + +insert foo +values +(null, null), +(null, null) + +insert dbo.[foo] +values ('') -- 2 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInsertValuesIntoOneRule/TestSources/test_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInsertValuesIntoOneRule/TestSources/test_raise_0_violations.sql new file mode 100644 index 00000000..46d8e963 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInsertValuesIntoOneRule/TestSources/test_raise_0_violations.sql @@ -0,0 +1,2 @@ +-- compatibility level min: 100 +INSERT INTO #T1 DEFAULT VALUES; diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleSetOptionIntoOneRule/MultipleSetOptionIntoOneRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleSetOptionIntoOneRule/MultipleSetOptionIntoOneRuleTests.cs new file mode 100644 index 00000000..7ecf4776 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleSetOptionIntoOneRule/MultipleSetOptionIntoOneRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Simplification")] + [TestOfRule(typeof(MultipleSetOptionIntoOneRule))] + public class MultipleSetOptionIntoOneRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(MultipleSetOptionIntoOneRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleSetOptionIntoOneRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleSetOptionIntoOneRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..7c5ba6ea --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleSetOptionIntoOneRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,12 @@ +SET ANSI_NULLS, QUOTED_IDENTIFIER ON; +SET ARITHABORT OFF; +GO + +BEGIN + SET XACT_ABORT ON + + -- statement between + SELECT 1 + + SET CONCAT_NULL_YIELDS_NULL ON +END diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleSetOptionIntoOneRule/TestSources/combinable_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleSetOptionIntoOneRule/TestSources/combinable_raise_2_violations.sql new file mode 100644 index 00000000..98bc6062 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleSetOptionIntoOneRule/TestSources/combinable_raise_2_violations.sql @@ -0,0 +1,5 @@ +SET QUOTED_IDENTIFIER ON; +SET ANSI_NULLS ON; + +SET ARITHABORT OFF; +SET ANSI_PADDING OFF; diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ReuseExpressionAliasRule/ReuseExpressionAliasRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ReuseExpressionAliasRule/ReuseExpressionAliasRuleTests.cs new file mode 100644 index 00000000..9577c27f --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ReuseExpressionAliasRule/ReuseExpressionAliasRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Simplification")] + [TestOfRule(typeof(ReuseExpressionAliasRule))] + public sealed class ReuseExpressionAliasRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(ReuseExpressionAliasRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ReuseExpressionAliasRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ReuseExpressionAliasRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..8497e89a --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ReuseExpressionAliasRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,18 @@ +-- no ORDER BY +SELECT 1 + +-- no selected elements +SELECT @x = y +ORDER BY y + +-- simple expression sorted +SELECT + foo.bar as far +FROM foo +ORDER BY foo.bar ASC + +-- expression reused +SELECT + (foo.bar * foo.jar - 1) as far +FROM foo +ORDER BY far DESC diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ReuseExpressionAliasRule/TestSources/reusable_expression_in_order_by_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ReuseExpressionAliasRule/TestSources/reusable_expression_in_order_by_raise_2_violations.sql new file mode 100644 index 00000000..b6c46c84 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/ReuseExpressionAliasRule/TestSources/reusable_expression_in_order_by_raise_2_violations.sql @@ -0,0 +1,14 @@ +SELECT + (foo.bar * foo.jar - 1) as far +FROM foo +ORDER BY a, + b, + ((foo.bar*foo.jar-1)) DESC -- 1 + + +SELECT + 1 + a +FROM foo +ORDER BY + (1 + A) -- 2 + , far diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Vulnerability/OutputSecretRule/OutputSecretRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Vulnerability/OutputSecretRule/OutputSecretRuleTests.cs new file mode 100644 index 00000000..c649da37 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Vulnerability/OutputSecretRule/OutputSecretRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Vulnerability")] + [TestOfRule(typeof(OutputSecretRule))] + public class OutputSecretRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(OutputSecretRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Vulnerability/OutputSecretRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Vulnerability/OutputSecretRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..b1e096fc --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Vulnerability/OutputSecretRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,29 @@ +SELECT * +FROM dbo.foo +union +SELECT a, b, c +FROM dbo.bar + +PRINT 'test' + +PRINT @var + +UPDATE t SET + lastmod = GETDATE() + OUTPUT INSERTED.lastmod +FROM tbl AS t + +select + NULL as pwd -- why not + +-- select into +SELECT @password as pwd +INTO #tmp +from dbo.foo +GO + +CREATE PROCEDURE foo + @password VARCHAR(10) -- not for OUTPUT +AS; +GO + diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Vulnerability/OutputSecretRule/TestSources/bad_output_raise_5_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Vulnerability/OutputSecretRule/TestSources/bad_output_raise_5_violations.sql new file mode 100644 index 00000000..8e677062 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Vulnerability/OutputSecretRule/TestSources/bad_output_raise_5_violations.sql @@ -0,0 +1,14 @@ +SELECT f.secret -- 1 +FROM dbo.foo f +union +SELECT bar.col as [password] -- 2 +FROM dbo.bar + +PRINT @secret -- 3 + +PRINT @pwd -- 4 + +UPDATE t SET + lastmod = GETDATE() + OUTPUT INSERTED.password -- 5 +FROM tbl AS t diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Vulnerability/OutputSecretRule/TestSources/out_param_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Vulnerability/OutputSecretRule/TestSources/out_param_raise_1_violations.sql new file mode 100644 index 00000000..1efae779 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Vulnerability/OutputSecretRule/TestSources/out_param_raise_1_violations.sql @@ -0,0 +1,3 @@ +CREATE PROCEDURE foo + @password VARCHAR(10) OUTPUT -- here +AS; diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Vulnerability/ReadingSecretDataRule/ReadingSecretDataRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Vulnerability/ReadingSecretDataRule/ReadingSecretDataRuleTests.cs new file mode 100644 index 00000000..876153c5 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Vulnerability/ReadingSecretDataRule/ReadingSecretDataRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Vulnerability")] + [TestOfRule(typeof(ReadingSecretDataRule))] + public class ReadingSecretDataRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(ReadingSecretDataRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Vulnerability/ReadingSecretDataRule/TestSources/no_secret_data_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Vulnerability/ReadingSecretDataRule/TestSources/no_secret_data_raise_0_violations.sql new file mode 100644 index 00000000..6c98d2dc --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Vulnerability/ReadingSecretDataRule/TestSources/no_secret_data_raise_0_violations.sql @@ -0,0 +1,4 @@ +SELECT * +FROM foo +JOIN bar +ON id = parent_id diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Vulnerability/ReadingSecretDataRule/TestSources/secret_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Vulnerability/ReadingSecretDataRule/TestSources/secret_raise_1_violations.sql new file mode 100644 index 00000000..cec574aa --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Vulnerability/ReadingSecretDataRule/TestSources/secret_raise_1_violations.sql @@ -0,0 +1 @@ +select * from sys.server_principals diff --git a/TeamTools.TSQL.LinterTests/packages.lock.json b/TeamTools.TSQL.LinterTests/packages.lock.json index 8048422f..35f2bb51 100644 --- a/TeamTools.TSQL.LinterTests/packages.lock.json +++ b/TeamTools.TSQL.LinterTests/packages.lock.json @@ -14,59 +14,15 @@ }, "Microsoft.VisualStudio.Web.CodeGeneration.Design": { "type": "Direct", - "requested": "[9.0.0, )", - "resolved": "9.0.0", - "contentHash": "nO5MUL3iC0WjtAVea5d4v6kVcoL9ae/PnkC6NeEJhWazHKdKj7xfv6D2QvBx8uCIj8FUu9QpvvdN6m/xMp//EQ==", - "dependencies": { - "Humanizer": "2.14.1", - "Microsoft.AspNetCore.Razor.Language": "6.0.24", - "Microsoft.Build": "17.10.4", - "Microsoft.CodeAnalysis.CSharp": "4.8.0", - "Microsoft.CodeAnalysis.CSharp.Features": "4.8.0", - "Microsoft.CodeAnalysis.CSharp.Workspaces": "4.8.0", - "Microsoft.CodeAnalysis.Common": "4.8.0", - "Microsoft.CodeAnalysis.Features": "4.8.0", - "Microsoft.CodeAnalysis.Razor": "6.0.24", - "Microsoft.CodeAnalysis.Workspaces.Common": "4.8.0", - "Microsoft.DotNet.Scaffolding.Shared": "9.0.0", - "Microsoft.Extensions.DependencyInjection": "9.0.0-rc.2.24473.5", - "Microsoft.Extensions.DependencyModel": "9.0.0-rc.2.24473.5", - "Microsoft.VisualStudio.Web.CodeGenerators.Mvc": "9.0.0", - "Mono.TextTemplating": "3.0.0", - "Newtonsoft.Json": "13.0.3", - "NuGet.Packaging": "6.11.0", - "NuGet.ProjectModel": "6.11.0", - "System.Formats.Asn1": "9.0.0-rc.2.24473.5", - "System.Security.Cryptography.ProtectedData": "8.0.0" - } + "requested": "[10.0.2, )", + "resolved": "10.0.2", + "contentHash": "pAO/U3tr7wlUJ5QkAuO+AI0qNm7BJGoT4FaLqIAe8fIZW0Ezn2/d0bLIMKkLzWDu+wFHf/s+HlAhqQMyhtP9Cg==" }, "Microsoft.VisualStudio.Web.CodeGenerators.Mvc": { "type": "Direct", - "requested": "[9.0.0, )", - "resolved": "9.0.0", - "contentHash": "WJhdsFXkpA0XR6PCjoxe9pRIqT8NV8Ggojv2cwaeCwxApzTAbLnglwADteeF7WlgHnr1VmJ+xdgzzNAAcJ9+Rg==", - "dependencies": { - "Humanizer": "2.14.1", - "Microsoft.AspNetCore.Razor.Language": "6.0.24", - "Microsoft.Build": "17.10.4", - "Microsoft.CodeAnalysis.CSharp": "4.8.0", - "Microsoft.CodeAnalysis.CSharp.Features": "4.8.0", - "Microsoft.CodeAnalysis.CSharp.Workspaces": "4.8.0", - "Microsoft.CodeAnalysis.Common": "4.8.0", - "Microsoft.CodeAnalysis.Features": "4.8.0", - "Microsoft.CodeAnalysis.Razor": "6.0.24", - "Microsoft.CodeAnalysis.Workspaces.Common": "4.8.0", - "Microsoft.DotNet.Scaffolding.Shared": "9.0.0", - "Microsoft.Extensions.DependencyInjection": "9.0.0-rc.2.24473.5", - "Microsoft.Extensions.DependencyModel": "9.0.0-rc.2.24473.5", - "Microsoft.VisualStudio.Web.CodeGeneration": "9.0.0", - "Mono.TextTemplating": "3.0.0", - "Newtonsoft.Json": "13.0.3", - "NuGet.Packaging": "6.11.0", - "NuGet.ProjectModel": "6.11.0", - "System.Formats.Asn1": "9.0.0-rc.2.24473.5", - "System.Security.Cryptography.ProtectedData": "8.0.0" - } + "requested": "[10.0.2, )", + "resolved": "10.0.2", + "contentHash": "JQcdTUracjvpN2zzHLhpGuqWqeZJzV08q0us960GRi4EN+3H8/CvE4D6+PzS+mD2eapf6i4DjPQljg9oi+R0+A==" }, "NUnit": { "type": "Direct", @@ -112,450 +68,11 @@ "System.Text.Encodings.Web": "9.0.7" } }, - "Humanizer": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "/FUTD3cEceAAmJSCPN9+J+VhGwmL/C12jvwlyM1DFXShEMsBzvLzLqSrJ2rb+k/W2znKw7JyflZgZpyE+tI7lA==", - "dependencies": { - "Humanizer.Core.af": "2.14.1", - "Humanizer.Core.ar": "2.14.1", - "Humanizer.Core.az": "2.14.1", - "Humanizer.Core.bg": "2.14.1", - "Humanizer.Core.bn-BD": "2.14.1", - "Humanizer.Core.cs": "2.14.1", - "Humanizer.Core.da": "2.14.1", - "Humanizer.Core.de": "2.14.1", - "Humanizer.Core.el": "2.14.1", - "Humanizer.Core.es": "2.14.1", - "Humanizer.Core.fa": "2.14.1", - "Humanizer.Core.fi-FI": "2.14.1", - "Humanizer.Core.fr": "2.14.1", - "Humanizer.Core.fr-BE": "2.14.1", - "Humanizer.Core.he": "2.14.1", - "Humanizer.Core.hr": "2.14.1", - "Humanizer.Core.hu": "2.14.1", - "Humanizer.Core.hy": "2.14.1", - "Humanizer.Core.id": "2.14.1", - "Humanizer.Core.is": "2.14.1", - "Humanizer.Core.it": "2.14.1", - "Humanizer.Core.ja": "2.14.1", - "Humanizer.Core.ko-KR": "2.14.1", - "Humanizer.Core.ku": "2.14.1", - "Humanizer.Core.lv": "2.14.1", - "Humanizer.Core.ms-MY": "2.14.1", - "Humanizer.Core.mt": "2.14.1", - "Humanizer.Core.nb": "2.14.1", - "Humanizer.Core.nb-NO": "2.14.1", - "Humanizer.Core.nl": "2.14.1", - "Humanizer.Core.pl": "2.14.1", - "Humanizer.Core.pt": "2.14.1", - "Humanizer.Core.ro": "2.14.1", - "Humanizer.Core.ru": "2.14.1", - "Humanizer.Core.sk": "2.14.1", - "Humanizer.Core.sl": "2.14.1", - "Humanizer.Core.sr": "2.14.1", - "Humanizer.Core.sr-Latn": "2.14.1", - "Humanizer.Core.sv": "2.14.1", - "Humanizer.Core.th-TH": "2.14.1", - "Humanizer.Core.tr": "2.14.1", - "Humanizer.Core.uk": "2.14.1", - "Humanizer.Core.uz-Cyrl-UZ": "2.14.1", - "Humanizer.Core.uz-Latn-UZ": "2.14.1", - "Humanizer.Core.vi": "2.14.1", - "Humanizer.Core.zh-CN": "2.14.1", - "Humanizer.Core.zh-Hans": "2.14.1", - "Humanizer.Core.zh-Hant": "2.14.1" - } - }, "Humanizer.Core": { "type": "Transitive", "resolved": "2.14.1", "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" }, - "Humanizer.Core.af": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "BoQHyu5le+xxKOw+/AUM7CLXneM/Bh3++0qh1u0+D95n6f9eGt9kNc8LcAHLIOwId7Sd5hiAaaav0Nimj3peNw==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.ar": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "3d1V10LDtmqg5bZjWkA/EkmGFeSfNBcyCH+TiHcHP+HGQQmRq3eBaLcLnOJbVQVn3Z6Ak8GOte4RX4kVCxQlFA==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.az": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "8Z/tp9PdHr/K2Stve2Qs/7uqWPWLUK9D8sOZDNzyv42e20bSoJkHFn7SFoxhmaoVLJwku2jp6P7HuwrfkrP18Q==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.bg": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "S+hIEHicrOcbV2TBtyoPp1AVIGsBzlarOGThhQYCnP6QzEYo/5imtok6LMmhZeTnBFoKhM8yJqRfvJ5yqVQKSQ==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.bn-BD": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "U3bfj90tnUDRKlL1ZFlzhCHoVgpTcqUlTQxjvGCaFKb+734TTu3nkHUWVZltA1E/swTvimo/aXLtkxnLFrc0EQ==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.cs": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "jWrQkiCTy3L2u1T86cFkgijX6k7hoB0pdcFMWYaSZnm6rvG/XJE40tfhYyKhYYgIc1x9P2GO5AC7xXvFnFdqMQ==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.da": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "5o0rJyE/2wWUUphC79rgYDnif/21MKTTx9LIzRVz9cjCIVFrJ2bDyR2gapvI9D6fjoyvD1NAfkN18SHBsO8S9g==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.de": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "9JD/p+rqjb8f5RdZ3aEJqbjMYkbk4VFii2QDnnOdNo6ywEfg/A5YeOQ55CaBJmy7KvV4tOK4+qHJnX/tg3Z54A==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.el": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "Xmv6sTL5mqjOWGGpqY7bvbfK5RngaUHSa8fYDGSLyxY9mGdNbDcasnRnMOvi0SxJS9gAqBCn21Xi90n2SHZbFA==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.es": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "e//OIAeMB7pjBV1HqqI4pM2Bcw3Jwgpyz9G5Fi4c+RJvhqFwztoWxW57PzTnNJE2lbhGGLQZihFZjsbTUsbczA==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.fa": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "nzDOj1x0NgjXMjsQxrET21t1FbdoRYujzbmZoR8u8ou5CBWY1UNca0j6n/PEJR/iUbt4IxstpszRy41wL/BrpA==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.fi-FI": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "Vnxxx4LUhp3AzowYi6lZLAA9Lh8UqkdwRh4IE2qDXiVpbo08rSbokATaEzFS+o+/jCNZBmoyyyph3vgmcSzhhQ==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.fr": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "2p4g0BYNzFS3u9SOIDByp2VClYKO0K1ecDV4BkB9EYdEPWfFODYnF+8CH8LpUrpxL2TuWo2fiFx/4Jcmrnkbpg==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.fr-BE": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "o6R3SerxCRn5Ij8nCihDNMGXlaJ/1AqefteAssgmU2qXYlSAGdhxmnrQAXZUDlE4YWt/XQ6VkNLtH7oMqsSPFQ==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.he": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "FPsAhy7Iw6hb+ZitLgYC26xNcgGAHXb0V823yFAzcyoL5ozM+DCJtYfDPYiOpsJhEZmKFTM9No0jUn1M89WGvg==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.hr": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "chnaD89yOlST142AMkAKLuzRcV5df3yyhDyRU5rypDiqrq2HN8y1UR3h1IicEAEtXLoOEQyjSAkAQ6QuXkn7aw==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.hu": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "hAfnaoF9LTGU/CmFdbnvugN4tIs8ppevVMe3e5bD24+tuKsggMc5hYta9aiydI8JH9JnuVmxvNI4DJee1tK05A==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.hy": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "sVIKxOiSBUb4gStRHo9XwwAg9w7TNvAXbjy176gyTtaTiZkcjr9aCPziUlYAF07oNz6SdwdC2mwJBGgvZ0Sl2g==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.id": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "4Zl3GTvk3a49Ia/WDNQ97eCupjjQRs2iCIZEQdmkiqyaLWttfb+cYXDMGthP42nufUL0SRsvBctN67oSpnXtsg==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.is": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "R67A9j/nNgcWzU7gZy1AJ07ABSLvogRbqOWvfRDn4q6hNdbg/mjGjZBp4qCTPnB2mHQQTCKo3oeCUayBCNIBCw==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.it": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "jYxGeN4XIKHVND02FZ+Woir3CUTyBhLsqxu9iqR/9BISArkMf1Px6i5pRZnvq4fc5Zn1qw71GKKoCaHDJBsLFw==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.ja": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "TM3ablFNoYx4cYJybmRgpDioHpiKSD7q0QtMrmpsqwtiiEsdW5zz/q4PolwAczFnvrKpN6nBXdjnPPKVet93ng==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.ko-KR": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "CtvwvK941k/U0r8PGdEuBEMdW6jv/rBiA9tUhakC7Zd2rA/HCnDcbr1DiNZ+/tRshnhzxy/qwmpY8h4qcAYCtQ==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.ku": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "vHmzXcVMe+LNrF9txpdHzpG7XJX65SiN9GQd/Zkt6gsGIIEeECHrkwCN5Jnlkddw2M/b0HS4SNxdR1GrSn7uCA==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.lv": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "E1/KUVnYBS1bdOTMNDD7LV/jdoZv/fbWTLPtvwdMtSdqLyRTllv6PGM9xVQoFDYlpvVGtEl/09glCojPHw8ffA==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.ms-MY": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "vX8oq9HnYmAF7bek4aGgGFJficHDRTLgp/EOiPv9mBZq0i4SA96qVMYSjJ2YTaxs7Eljqit7pfpE2nmBhY5Fnw==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.mt": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "pEgTBzUI9hzemF7xrIZigl44LidTUhNu4x/P6M9sAwZjkUF0mMkbpxKkaasOql7lLafKrnszs0xFfaxQyzeuZQ==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.nb": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "mbs3m6JJq53ssLqVPxNfqSdTxAcZN3njlG8yhJVx83XVedpTe1ECK9aCa8FKVOXv93Gl+yRHF82Hw9T9LWv2hw==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.nb-NO": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "AsJxrrVYmIMbKDGe8W6Z6//wKv9dhWH7RsTcEHSr4tQt/80pcNvLi0hgD3fqfTtg0tWKtgch2cLf4prorEV+5A==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.nl": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "24b0OUdzJxfoqiHPCtYnR5Y4l/s4Oh7KW7uDp+qX25NMAHLCGog2eRfA7p2kRJp8LvnynwwQxm2p534V9m55wQ==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.pl": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "17mJNYaBssENVZyQHduiq+bvdXS0nhZJGEXtPKoMhKv3GD//WO0mEfd9wjEBsWCSmWI7bjRqhCidxzN+YtJmsg==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.pt": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "8HB8qavcVp2la1GJX6t+G9nDYtylPKzyhxr9LAooIei9MnQvNsjEiIE4QvHoeDZ4weuQ9CsPg1c211XUMVEZ4A==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.ro": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "psXNOcA6R8fSHoQYhpBTtTTYiOk8OBoN3PKCEDgsJKIyeY5xuK81IBdGi77qGZMu/OwBRQjQCBMtPJb0f4O1+A==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.ru": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "zm245xUWrajSN2t9H7BTf84/2APbUkKlUJpcdgsvTdAysr1ag9fi1APu6JEok39RRBXDfNRVZHawQ/U8X0pSvQ==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.sk": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "Ncw24Vf3ioRnbU4MsMFHafkyYi8JOnTqvK741GftlQvAbULBoTz2+e7JByOaasqeSi0KfTXeegJO+5Wk1c0Mbw==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.sl": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "l8sUy4ciAIbVThWNL0atzTS2HWtv8qJrsGWNlqrEKmPwA4SdKolSqnTes9V89fyZTc2Q43jK8fgzVE2C7t009A==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.sr": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "rnNvhpkOrWEymy7R/MiFv7uef8YO5HuXDyvojZ7JpijHWA5dXuVXooCOiA/3E93fYa3pxDuG2OQe4M/olXbQ7w==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.sr-Latn": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "nuy/ykpk974F8ItoQMS00kJPr2dFNjOSjgzCwfysbu7+gjqHmbLcYs7G4kshLwdA4AsVncxp99LYeJgoh1JF5g==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.sv": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "E53+tpAG0RCp+cSSI7TfBPC+NnsEqUuoSV0sU+rWRXWr9MbRWx1+Zj02XMojqjGzHjjOrBFBBio6m74seFl0AA==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.th-TH": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "eSevlJtvs1r4vQarNPfZ2kKDp/xMhuD00tVVzRXkSh1IAZbBJI/x2ydxUOwfK9bEwEp+YjvL1Djx2+kw7ziu7g==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.tr": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "rQ8N+o7yFcFqdbtu1mmbrXFi8TQ+uy+fVH9OPI0CI3Cu1om5hUU/GOMC3hXsTCI6d79y4XX+0HbnD7FT5khegA==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.uk": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "2uEfujwXKNm6bdpukaLtEJD+04uUtQD65nSGCetA1fYNizItEaIBUboNfr3GzJxSMQotNwGVM3+nSn8jTd0VSg==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.uz-Cyrl-UZ": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "TD3ME2sprAvFqk9tkWrvSKx5XxEMlAn1sjk+cYClSWZlIMhQQ2Bp/w0VjX1Kc5oeKjxRAnR7vFcLUFLiZIDk9Q==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.uz-Latn-UZ": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "/kHAoF4g0GahnugZiEMpaHlxb+W6jCEbWIdsq9/I1k48ULOsl/J0pxZj93lXC3omGzVF1BTVIeAtv5fW06Phsg==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.vi": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "rsQNh9rmHMBtnsUUlJbShMsIMGflZtPmrMM6JNDw20nhsvqfrdcoDD8cMnLAbuSovtc3dP+swRmLQzKmXDTVPA==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.zh-CN": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "uH2dWhrgugkCjDmduLdAFO9w1Mo0q07EuvM0QiIZCVm6FMCu/lGv2fpMu4GX+4HLZ6h5T2Pg9FIdDLCPN2a67w==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.zh-Hans": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "WH6IhJ8V1UBG7rZXQk3dZUoP2gsi8a0WkL8xL0sN6WGiv695s8nVcmab9tWz20ySQbuzp0UkSxUQFi5jJHIpOQ==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.zh-Hant": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "VIXB7HCUC34OoaGnO3HJVtSv2/wljPhjV7eKH4+TFPgQdJj2lvHNKY41Dtg0Bphu7X5UaXFR4zrYYyo+GNOjbA==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, "Microsoft.ApplicationInsights": { "type": "Transitive", "resolved": "2.23.0", @@ -564,201 +81,11 @@ "System.Diagnostics.DiagnosticSource": "5.0.0" } }, - "Microsoft.AspNetCore.Razor.Language": { - "type": "Transitive", - "resolved": "6.0.24", - "contentHash": "kBL6ljTREp/3fk8EKN27mrPy3WTqWUjiqCkKFlCKHUKRO3/9rAasKizX3vPWy4ZTcNsIPmVWUHwjDFmiW4MyNA==" - }, - "Microsoft.Bcl.AsyncInterfaces": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "3aeMZ1N0lJoSyzqiP03hqemtb1BijhsJADdobn/4nsMJ8V1H+CrpuduUe4hlRdx+ikBQju1VGjMD1GJ3Sk05Eg==" - }, - "Microsoft.Build": { - "type": "Transitive", - "resolved": "17.10.4", - "contentHash": "ZmGA8vhVXFzC4oo48ybQKlEybVKd0Ntfdr+Enqrn5ES1R6e/krIK9hLk0W33xuT0/G6QYd3YdhJZh+Xle717Ag==", - "dependencies": { - "Microsoft.Build.Framework": "17.10.4", - "Microsoft.NET.StringTools": "17.10.4", - "System.Collections.Immutable": "8.0.0", - "System.Configuration.ConfigurationManager": "8.0.0", - "System.Reflection.Metadata": "8.0.0", - "System.Reflection.MetadataLoadContext": "8.0.0", - "System.Security.Principal.Windows": "5.0.0", - "System.Threading.Tasks.Dataflow": "8.0.0" - } - }, - "Microsoft.Build.Framework": { - "type": "Transitive", - "resolved": "17.10.4", - "contentHash": "4qXCwNOXBR1dyCzuks9SwTwFJQO/xmf2wcMislotDWJu7MN/r3xDNoU8Ae5QmKIHPaLG1xmfDkYS7qBVzxmeKw==" - }, - "Microsoft.CodeAnalysis.Analyzers": { - "type": "Transitive", - "resolved": "3.3.4", - "contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==" - }, - "Microsoft.CodeAnalysis.AnalyzerUtilities": { - "type": "Transitive", - "resolved": "3.3.0", - "contentHash": "gyQ70pJ4T7hu/s0+QnEaXtYfeG/JrttGnxHJlrhpxsQjRIUGuRhVwNBtkHHYOrUAZ/l47L98/NiJX6QmTwAyrg==" - }, - "Microsoft.CodeAnalysis.Common": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.3.4", - "System.Collections.Immutable": "7.0.0", - "System.Reflection.Metadata": "7.0.0", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, - "Microsoft.CodeAnalysis.CSharp": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "+3+qfdb/aaGD8PZRCrsdobbzGs1m9u119SkkJt8e/mk3xLJz/udLtS2T6nY27OTXxBBw10HzAbC8Z9w08VyP/g==", - "dependencies": { - "Microsoft.CodeAnalysis.Common": "[4.8.0]" - } - }, - "Microsoft.CodeAnalysis.CSharp.Features": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "Gpas3l8PE1xz1VDIJNMkYuoFPXtuALxybP04caXh9avC2a0elsoBdukndkJXVZgdKPwraf0a98s7tjqnEk5QIQ==", - "dependencies": { - "Humanizer.Core": "2.14.1", - "Microsoft.CodeAnalysis.CSharp": "[4.8.0]", - "Microsoft.CodeAnalysis.CSharp.Workspaces": "[4.8.0]", - "Microsoft.CodeAnalysis.Common": "[4.8.0]", - "Microsoft.CodeAnalysis.Features": "[4.8.0]", - "Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]" - } - }, - "Microsoft.CodeAnalysis.CSharp.Workspaces": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "3amm4tq4Lo8/BGvg9p3BJh3S9nKq2wqCXfS7138i69TUpo/bD+XvD0hNurpEBtcNZhi1FyutiomKJqVF39ugYA==", - "dependencies": { - "Humanizer.Core": "2.14.1", - "Microsoft.CodeAnalysis.CSharp": "[4.8.0]", - "Microsoft.CodeAnalysis.Common": "[4.8.0]", - "Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]" - } - }, - "Microsoft.CodeAnalysis.Elfie": { - "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "r12elUp4MRjdnRfxEP+xqVSUUfG3yIJTBEJGwbfvF5oU4m0jb9HC0gFG28V/dAkYGMkRmHVi3qvrnBLQSw9X3Q==", - "dependencies": { - "System.Configuration.ConfigurationManager": "4.5.0", - "System.Data.DataSetExtensions": "4.5.0" - } - }, - "Microsoft.CodeAnalysis.Features": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "sCVzMtSETGE16KeScwwlVfxaKRbUMSf/cgRPRPMJuou37SLT7XkIBzJu4e7mlFTzpJbfalV5tOcKpUtLO3eJAg==", - "dependencies": { - "Microsoft.CodeAnalysis.AnalyzerUtilities": "3.3.0", - "Microsoft.CodeAnalysis.Common": "[4.8.0]", - "Microsoft.CodeAnalysis.Elfie": "1.0.0", - "Microsoft.CodeAnalysis.Scripting.Common": "[4.8.0]", - "Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]", - "Microsoft.DiaSymReader": "2.0.0", - "System.Text.Json": "7.0.3" - } - }, - "Microsoft.CodeAnalysis.Razor": { - "type": "Transitive", - "resolved": "6.0.24", - "contentHash": "xIAjR6l/1PO2ILT6/lOGYfe8OzMqfqxh1lxFuM4Exluwc2sQhJw0kS7pEyJ0DE/UMYu6Jcdc53DmjOxQUDT2Pg==", - "dependencies": { - "Microsoft.AspNetCore.Razor.Language": "6.0.24", - "Microsoft.CodeAnalysis.CSharp": "4.0.0", - "Microsoft.CodeAnalysis.Common": "4.0.0" - } - }, - "Microsoft.CodeAnalysis.Scripting.Common": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "ysiNNbAASVhV9wEd5oY2x99EwaVYtB13XZRjHsgWT/R1mQkxZF8jWsf7JWaZxD1+jNoz1QCQ6nbe+vr+6QvlFA==", - "dependencies": { - "Microsoft.CodeAnalysis.Common": "[4.8.0]" - } - }, - "Microsoft.CodeAnalysis.Workspaces.Common": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "LXyV+MJKsKRu3FGJA3OmSk40OUIa/dQCFLOnm5X8MNcujx7hzGu8o+zjXlb/cy5xUdZK2UKYb9YaQ2E8m9QehQ==", - "dependencies": { - "Humanizer.Core": "2.14.1", - "Microsoft.Bcl.AsyncInterfaces": "7.0.0", - "Microsoft.CodeAnalysis.Common": "[4.8.0]", - "System.Composition": "7.0.0", - "System.IO.Pipelines": "7.0.0", - "System.Threading.Channels": "7.0.0" - } - }, "Microsoft.CodeCoverage": { "type": "Transitive", "resolved": "18.0.0", "contentHash": "DFPhMrsIofgJ1DDU3ModqqRArDm15/bNl4ecmcuBspZkZ4ONYnCC0R8U27WzK7cYv6r8l6Q/fRmvg7cb+I/dJA==" }, - "Microsoft.DiaSymReader": { - "type": "Transitive", - "resolved": "2.0.0", - "contentHash": "QcZrCETsBJqy/vQpFtJc+jSXQ0K5sucQ6NUFbTNVHD4vfZZOwjZ/3sBzczkC4DityhD3AVO/+K/+9ioLs1AgRA==" - }, - "Microsoft.DotNet.Scaffolding.Shared": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "9pfRsTzUANgI6J7nFjYip50ifcvmORjMmFByXmdYa//x8toziydhbg0cMylP1S2mRf4/96VKSAfpayYrO3m4Ww==", - "dependencies": { - "Humanizer": "2.14.1", - "Microsoft.CodeAnalysis.CSharp": "4.8.0", - "Microsoft.CodeAnalysis.CSharp.Features": "4.8.0", - "Microsoft.CodeAnalysis.CSharp.Workspaces": "4.8.0", - "Microsoft.CodeAnalysis.Common": "4.8.0", - "Microsoft.CodeAnalysis.Features": "4.8.0", - "Microsoft.CodeAnalysis.Workspaces.Common": "4.8.0", - "Microsoft.Extensions.DependencyModel": "9.0.0-rc.2.24473.5", - "Mono.TextTemplating": "3.0.0", - "Newtonsoft.Json": "13.0.3", - "NuGet.Packaging": "6.11.0", - "NuGet.ProjectModel": "6.11.0", - "System.Formats.Asn1": "9.0.0-rc.2.24473.5", - "System.Security.Cryptography.ProtectedData": "8.0.0" - } - }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", - "resolved": "9.0.0-rc.2.24473.5", - "contentHash": "FHb7uxiX/08FBBtwat7fiBdQltxst1Farux6Ifn1dfke+D8h1rcDj1ZbKzNB9SvLh1XmEXNYmoWGkTjt1mVzXg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0-rc.2.24473.5" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "9.0.0-rc.2.24473.5", - "contentHash": "T8oVF5Kz+J5IVagQAqcFFUrc/JrjaSvpACSm+t6cNolBX1S41PZVe3JLa3bxKnz0GTkUfSPPIkaekLmpAmHu3Q==" - }, - "Microsoft.Extensions.DependencyModel": { - "type": "Transitive", - "resolved": "9.0.0-rc.2.24473.5", - "contentHash": "Y+L5r4bqbSbhTVVQ8wt7AaO8vtB2ZgEfzI1gN3Ia/vBIsfECOQK8bDItAi7ikb1kk9c0unDnJnM08BuO59ydEQ==", - "dependencies": { - "System.Text.Encodings.Web": "9.0.0-rc.2.24473.5", - "System.Text.Json": "9.0.0-rc.2.24473.5" - } - }, - "Microsoft.NET.StringTools": { - "type": "Transitive", - "resolved": "17.10.4", - "contentHash": "wyABaqY+IHCMMSTQmcc3Ca6vbmg5BaEPgicnEgpll+4xyWZWlkQqUwafweUd9VAhBb4jqplMl6voUHQ6yfdUcg==" - }, "Microsoft.NETCore.Platforms": { "type": "Transitive", "resolved": "7.0.4", @@ -833,306 +160,21 @@ "Newtonsoft.Json": "13.0.3" } }, - "Microsoft.VisualStudio.Web.CodeGeneration": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "W9ho78o/92MUDz04r7Al4dMx7djaqtSJE1cR7fMjy+Mm0StL5pVKXF24qnAFWJlip7KEpAa1QP35davXvuis9w==", - "dependencies": { - "Humanizer": "2.14.1", - "Microsoft.AspNetCore.Razor.Language": "6.0.24", - "Microsoft.Build": "17.10.4", - "Microsoft.CodeAnalysis.CSharp": "4.8.0", - "Microsoft.CodeAnalysis.CSharp.Features": "4.8.0", - "Microsoft.CodeAnalysis.CSharp.Workspaces": "4.8.0", - "Microsoft.CodeAnalysis.Common": "4.8.0", - "Microsoft.CodeAnalysis.Features": "4.8.0", - "Microsoft.CodeAnalysis.Razor": "6.0.24", - "Microsoft.CodeAnalysis.Workspaces.Common": "4.8.0", - "Microsoft.Extensions.DependencyInjection": "9.0.0-rc.2.24473.5", - "Microsoft.Extensions.DependencyModel": "9.0.0-rc.2.24473.5", - "Microsoft.VisualStudio.Web.CodeGeneration.EntityFrameworkCore": "9.0.0", - "Mono.TextTemplating": "3.0.0", - "Newtonsoft.Json": "13.0.3", - "NuGet.Packaging": "6.11.0", - "NuGet.ProjectModel": "6.11.0", - "System.Formats.Asn1": "9.0.0-rc.2.24473.5", - "System.Security.Cryptography.ProtectedData": "8.0.0" - } - }, - "Microsoft.VisualStudio.Web.CodeGeneration.Core": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "1VIEZs8DNnefMa0eVDZucz/dk28Sg0QRiNiRJj7SdU8E6UiNJxnkzA748aqA6Qqi8OMTHTBKhzx0Hj9ykIi6/Q==", - "dependencies": { - "Humanizer": "2.14.1", - "Microsoft.AspNetCore.Razor.Language": "6.0.24", - "Microsoft.Build": "17.10.4", - "Microsoft.CodeAnalysis.CSharp": "4.8.0", - "Microsoft.CodeAnalysis.CSharp.Features": "4.8.0", - "Microsoft.CodeAnalysis.CSharp.Workspaces": "4.8.0", - "Microsoft.CodeAnalysis.Common": "4.8.0", - "Microsoft.CodeAnalysis.Features": "4.8.0", - "Microsoft.CodeAnalysis.Razor": "6.0.24", - "Microsoft.CodeAnalysis.Workspaces.Common": "4.8.0", - "Microsoft.Extensions.DependencyInjection": "9.0.0-rc.2.24473.5", - "Microsoft.Extensions.DependencyModel": "9.0.0-rc.2.24473.5", - "Microsoft.VisualStudio.Web.CodeGeneration.Templating": "9.0.0", - "Mono.TextTemplating": "3.0.0", - "Newtonsoft.Json": "13.0.3", - "NuGet.Packaging": "6.11.0", - "NuGet.ProjectModel": "6.11.0", - "System.Formats.Asn1": "9.0.0-rc.2.24473.5", - "System.Security.Cryptography.ProtectedData": "8.0.0" - } - }, - "Microsoft.VisualStudio.Web.CodeGeneration.EntityFrameworkCore": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "F4+A6CaXmof/QoeWpqaMMeoVinfUSIMKa5xLOrwsZxGfYl6Qryhb06bkJ8yJaF05WefMM/wnj73oI3Ms2bBh7g==", - "dependencies": { - "Humanizer": "2.14.1", - "Microsoft.AspNetCore.Razor.Language": "6.0.24", - "Microsoft.Build": "17.10.4", - "Microsoft.CodeAnalysis.CSharp": "4.8.0", - "Microsoft.CodeAnalysis.CSharp.Features": "4.8.0", - "Microsoft.CodeAnalysis.CSharp.Workspaces": "4.8.0", - "Microsoft.CodeAnalysis.Common": "4.8.0", - "Microsoft.CodeAnalysis.Features": "4.8.0", - "Microsoft.CodeAnalysis.Razor": "6.0.24", - "Microsoft.CodeAnalysis.Workspaces.Common": "4.8.0", - "Microsoft.DotNet.Scaffolding.Shared": "9.0.0", - "Microsoft.Extensions.DependencyInjection": "9.0.0-rc.2.24473.5", - "Microsoft.Extensions.DependencyModel": "9.0.0-rc.2.24473.5", - "Microsoft.VisualStudio.Web.CodeGeneration.Core": "9.0.0", - "Mono.TextTemplating": "3.0.0", - "Newtonsoft.Json": "13.0.3", - "NuGet.Packaging": "6.11.0", - "NuGet.ProjectModel": "6.11.0", - "System.Formats.Asn1": "9.0.0-rc.2.24473.5", - "System.Security.Cryptography.ProtectedData": "8.0.0" - } - }, - "Microsoft.VisualStudio.Web.CodeGeneration.Templating": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "euoX0M4JnbzSUcFXfDq+GSSdXNRbKGUBTK+8gcnzHmhY3sHgHn9bgeeZDp+LGuoUQaP+WrWA8Nq92gCTcZLWSA==", - "dependencies": { - "Humanizer": "2.14.1", - "Microsoft.AspNetCore.Razor.Language": "6.0.24", - "Microsoft.Build": "17.10.4", - "Microsoft.CodeAnalysis.CSharp": "4.8.0", - "Microsoft.CodeAnalysis.CSharp.Features": "4.8.0", - "Microsoft.CodeAnalysis.CSharp.Workspaces": "4.8.0", - "Microsoft.CodeAnalysis.Common": "4.8.0", - "Microsoft.CodeAnalysis.Features": "4.8.0", - "Microsoft.CodeAnalysis.Razor": "6.0.24", - "Microsoft.CodeAnalysis.Workspaces.Common": "4.8.0", - "Microsoft.Extensions.DependencyModel": "9.0.0-rc.2.24473.5", - "Microsoft.VisualStudio.Web.CodeGeneration.Utils": "9.0.0", - "Mono.TextTemplating": "3.0.0", - "Newtonsoft.Json": "13.0.3", - "NuGet.Packaging": "6.11.0", - "NuGet.ProjectModel": "6.11.0", - "System.Formats.Asn1": "9.0.0-rc.2.24473.5", - "System.Security.Cryptography.ProtectedData": "8.0.0" - } - }, - "Microsoft.VisualStudio.Web.CodeGeneration.Utils": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "O8uehWLzgQhq3H2f+dxEkuYF8wWoBrT7iKtQXnHAc96qlVdLSARSxt3hlxqFSzK3ZkHp2P6lHt76LRH6J0PDrw==", - "dependencies": { - "Humanizer": "2.14.1", - "Microsoft.Build": "17.10.4", - "Microsoft.CodeAnalysis.CSharp": "4.8.0", - "Microsoft.CodeAnalysis.CSharp.Features": "4.8.0", - "Microsoft.CodeAnalysis.CSharp.Workspaces": "4.8.0", - "Microsoft.CodeAnalysis.Common": "4.8.0", - "Microsoft.CodeAnalysis.Features": "4.8.0", - "Microsoft.CodeAnalysis.Workspaces.Common": "4.8.0", - "Microsoft.DotNet.Scaffolding.Shared": "9.0.0", - "Microsoft.Extensions.DependencyModel": "9.0.0-rc.2.24473.5", - "Mono.TextTemplating": "3.0.0", - "Newtonsoft.Json": "13.0.3", - "NuGet.Packaging": "6.11.0", - "NuGet.ProjectModel": "6.11.0", - "System.Formats.Asn1": "9.0.0-rc.2.24473.5", - "System.Security.Cryptography.ProtectedData": "8.0.0" - } - }, - "Mono.TextTemplating": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "YqueG52R/Xej4VVbKuRIodjiAhV0HR/XVbLbNrJhCZnzjnSjgMJ/dCdV0akQQxavX6hp/LC6rqLGLcXeQYU7XA==", - "dependencies": { - "System.CodeDom": "6.0.0" - } - }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.4", "contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==" }, - "NuGet.Common": { - "type": "Transitive", - "resolved": "6.11.0", - "contentHash": "T3bCiKUSx8wdYpcqr6Dbx93zAqFp689ee/oa1tH22XI/xl7EUzQ7No/WlE1FUqvEX1+Mqar3wRNAn2O/yxo94g==", - "dependencies": { - "NuGet.Frameworks": "6.11.0" - } - }, - "NuGet.Configuration": { - "type": "Transitive", - "resolved": "6.11.0", - "contentHash": "73QprQqmumFrv3Ooi4YWpRYeBj8jZy9gNdOaOCp4pPInpt41SJJAz/aP4je+StwIJvi5HsgPPecLKekDIQEwKg==", - "dependencies": { - "NuGet.Common": "6.11.0", - "System.Security.Cryptography.ProtectedData": "4.4.0" - } - }, - "NuGet.DependencyResolver.Core": { - "type": "Transitive", - "resolved": "6.11.0", - "contentHash": "SoiPKPooA+IF+iCsX1ykwi3M0e+yBL34QnwIP3ujhQEn1dhlP/N1XsYAnKkJPxV15EZCahuuS4HtnBsZx+CHKA==", - "dependencies": { - "NuGet.Configuration": "6.11.0", - "NuGet.LibraryModel": "6.11.0", - "NuGet.Protocol": "6.11.0" - } - }, - "NuGet.Frameworks": { - "type": "Transitive", - "resolved": "6.11.0", - "contentHash": "Ew/mrfmLF5phsprysHbph2+tdZ10HMHAURavsr/Kx1WhybDG4vmGuoNLbbZMZOqnPRdpyCTc42OKWLoedxpYtA==" - }, - "NuGet.LibraryModel": { - "type": "Transitive", - "resolved": "6.11.0", - "contentHash": "KUV2eeMICMb24OPcICn/wgncNzt6+W+lmFVO5eorTdo1qV4WXxYGyG1NTPiCY+Nrv5H/Ilnv9UaUM2ozqSmnjw==", - "dependencies": { - "NuGet.Common": "6.11.0", - "NuGet.Versioning": "6.11.0" - } - }, - "NuGet.Packaging": { - "type": "Transitive", - "resolved": "6.11.0", - "contentHash": "VmUv2LedVuPY1tfNybORO2I9IuqOzeV7I5JBD+PwNvJq2bAqovi4FCw2cYI0g+kjOJXBN2lAJfrfnqtUOlVJdQ==", - "dependencies": { - "Newtonsoft.Json": "13.0.3", - "NuGet.Configuration": "6.11.0", - "NuGet.Versioning": "6.11.0", - "System.Security.Cryptography.Pkcs": "6.0.4" - } - }, - "NuGet.ProjectModel": { - "type": "Transitive", - "resolved": "6.11.0", - "contentHash": "g0KtmDH6fas97WsN73yV2h1F5JT9o6+Y0wlPK+ij9YLKaAXaF6+1HkSaQMMJ+xh9/jCJG9G6nau6InOlb1g48g==", - "dependencies": { - "NuGet.DependencyResolver.Core": "6.11.0" - } - }, - "NuGet.Protocol": { - "type": "Transitive", - "resolved": "6.11.0", - "contentHash": "p5B8oNLLnGhUfMbcS16aRiegj11pD6k+LELyRBqvNFR/pE3yR1XT+g1XS33ME9wvoU+xbCGnl4Grztt1jHPinw==", - "dependencies": { - "NuGet.Packaging": "6.11.0" - } - }, - "NuGet.Versioning": { - "type": "Transitive", - "resolved": "6.11.0", - "contentHash": "v/GGlIj2dd7svplFmASWEueu62veKW0MrMtBaZ7QG8aJTSGv2yE+pgUGhXRcQ4nxNOEq/wLBrz1vkth/1SND7A==" - }, - "System.CodeDom": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" - }, "System.Collections.Immutable": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==" }, - "System.Composition": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "tRwgcAkDd85O8Aq6zHDANzQaq380cek9lbMg5Qma46u5BZXq/G+XvIYmu+UI+BIIZ9zssXLYrkTykEqxxvhcmg==", - "dependencies": { - "System.Composition.AttributedModel": "7.0.0", - "System.Composition.Convention": "7.0.0", - "System.Composition.Hosting": "7.0.0", - "System.Composition.Runtime": "7.0.0", - "System.Composition.TypedParts": "7.0.0" - } - }, - "System.Composition.AttributedModel": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "2QzClqjElKxgI1jK1Jztnq44/8DmSuTSGGahXqQ4TdEV0h9s2KikQZIgcEqVzR7OuWDFPGLHIprBJGQEPr8fAQ==" - }, - "System.Composition.Convention": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "IMhTlpCs4HmlD8B+J8/kWfwX7vrBBOs6xyjSTzBlYSs7W4OET4tlkR/Sg9NG8jkdJH9Mymq0qGdYS1VPqRTBnQ==", - "dependencies": { - "System.Composition.AttributedModel": "7.0.0" - } - }, - "System.Composition.Hosting": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "eB6gwN9S+54jCTBJ5bpwMOVerKeUfGGTYCzz3QgDr1P55Gg/Wb27ShfPIhLMjmZ3MoAKu8uUSv6fcCdYJTN7Bg==", - "dependencies": { - "System.Composition.Runtime": "7.0.0" - } - }, - "System.Composition.Runtime": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "aZJ1Zr5Txe925rbo4742XifEyW0MIni1eiUebmcrP3HwLXZ3IbXUj4MFMUH/RmnJOAQiS401leg/2Sz1MkApDw==" - }, - "System.Composition.TypedParts": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "ZK0KNPfbtxVceTwh+oHNGUOYV2WNOHReX2AXipuvkURC7s/jPwoWfsu3SnDBDgofqbiWr96geofdQ2erm/KTHg==", - "dependencies": { - "System.Composition.AttributedModel": "7.0.0", - "System.Composition.Hosting": "7.0.0", - "System.Composition.Runtime": "7.0.0" - } - }, - "System.Configuration.ConfigurationManager": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "JlYi9XVvIREURRUlGMr1F6vOFLk7YSY4p1vHo4kX3tQ0AGrjqlRWHDi66ImHhy6qwXBG3BJ6Y1QlYQ+Qz6Xgww==", - "dependencies": { - "System.Diagnostics.EventLog": "8.0.0", - "System.Security.Cryptography.ProtectedData": "8.0.0" - } - }, - "System.Data.DataSetExtensions": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "221clPs1445HkTBZPL+K9sDBdJRB8UN8rgjO3ztB0CQ26z//fmJXtlsr6whGatscsKGBrhJl5bwJuKSA8mwFOw==" - }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", "resolved": "5.0.0", "contentHash": "tCQTzPsGZh/A9LhhA6zrqCRV4hOHsK90/G7q3Khxmn6tnB1PuNU0cRaKANP2AWcF9bn0zsuOoZOSrHuJk6oNBA==" }, - "System.Diagnostics.EventLog": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" - }, - "System.Formats.Asn1": { - "type": "Transitive", - "resolved": "9.0.0-rc.2.24473.5", - "contentHash": "T7T1kH5MU5pd3jo+lPxKOvcUrvsNiGJpOVOM641CRWAy8HRdk55fmmOqcGYZQ2p8+qDOeMUikQyi5dbNVGEJjg==" - }, "System.IO.Pipelines": { "type": "Transitive", "resolved": "9.0.7", @@ -1146,53 +188,11 @@ "System.Collections.Immutable": "8.0.0" } }, - "System.Reflection.MetadataLoadContext": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "SZxrQ4sQYnIcdwiO3G/lHZopbPYQ2lW0ioT4JezgccWUrKaKbHLJbAGZaDfkYjWcta1pWssAo3MOXLsR0ie4tQ==", - "dependencies": { - "System.Collections.Immutable": "8.0.0", - "System.Reflection.Metadata": "8.0.0" - } - }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" - }, - "System.Security.Cryptography.Pkcs": { - "type": "Transitive", - "resolved": "6.0.4", - "contentHash": "LGbXi1oUJ9QgCNGXRO9ndzBL/GZgANcsURpMhNR8uO+rca47SZmciS3RSQUvlQRwK3QHZSHNOXzoMUASKA+Anw==", - "dependencies": { - "System.Formats.Asn1": "6.0.0" - } - }, - "System.Security.Cryptography.ProtectedData": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "+TUFINV2q2ifyXauQXRwy4CiBhqvDEDZeVJU7qfxya4aRYOKzVBpN+4acx25VcPB9ywUN6C0n8drWl110PhZEg==" - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" - }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "9.0.7", "contentHash": "WswuKENaV4gC4ZYZi8BhehJHHRdyZQzXEYv/lV8DHW9FwkdnKaTutdRbK/S1wHZtKUUzzptBPAX2XOxdoURkMw==" }, - "System.Threading.Channels": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==" - }, - "System.Threading.Tasks.Dataflow": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "7V0I8tPa9V7UxMx/+7DIwkhls5ouaEMQx6l/GwGm1Y8kJQ61On9B/PxCXFLbgu5/C47g0BP2CUYs+nMv1+Oaqw==" - }, "teamtools.common.linting": { "type": "Project", "dependencies": {