From 485e4ca0cb2fb358771b68c53e35bf699bfd07e3 Mon Sep 17 00:00:00 2001 From: Ivan Starostin Date: Fri, 13 Feb 2026 14:07:40 +0300 Subject: [PATCH 1/3] Cumulative update of 2026-02 - new rules - new types support for evaluator --- .../Abstractions/CurrentMomentFunction.cs | 35 ++ .../ExplicitConvertionFunction.cs | 47 ++- .../Abstractions/RoundingFunction.cs | 105 ++++++ .../ArgumentIsValidDateTime.cs | 34 ++ .../ConversionFunctions/Convert.cs | 4 +- .../ConversionFunctions/TryConvert.cs | 4 +- .../DateFunctions/BaseDateTimeFromParts.cs | 185 ++++++++++ .../DateFunctions/CurrentDate.cs | 20 ++ .../DateFunctions/CurrentTimestamp.cs | 20 ++ .../BuiltInFunctions/DateFunctions/DateAdd.cs | 125 +++++++ .../DateFunctions/DateDiff.cs | 84 ++++- .../DateFunctions/DateFromParts.cs | 15 + .../DateFunctions/DateName.cs | 25 +- .../DateFunctions/DatePart.cs | 28 +- .../DateFunctions/DatePartExtractor.cs | 79 +++++ .../DateFunctions/DateTimeFromParts.cs | 15 + .../DateFunctions/EndOfMonth.cs | 61 ++++ .../BuiltInFunctions/DateFunctions/GetDate.cs | 20 ++ .../DateFunctions/SmallDateTimeFromParts.cs | 15 + .../DateFunctions/SpecificDatePartFunction.cs | 25 +- .../DateFunctions/SysDateTime.cs | 20 ++ .../DateFunctions/TimeFromParts.cs | 15 + .../BuiltInFunctions/MathFunctions/Ceiling.cs | 20 ++ .../BuiltInFunctions/MathFunctions/Floor.cs | 20 ++ .../BuiltInFunctions/MathFunctions/Round.cs | 107 ++++++ .../StrFunctions/DataLength.cs | 2 +- .../BuiltInFunctions/StrFunctions/Format.cs | 11 +- .../StrFunctions/FormatMessage.cs | 2 +- .../StrFunctions/HashBytes.cs | 36 ++ .../SysFunctions/CursorRows.cs | 16 + .../SysFunctions/CursorStatus.cs | 87 +++++ .../BuiltInFunctions/SysFunctions/Dbts.cs | 15 + .../SysFunctions/MaxPrecision.cs | 17 + .../SysFunctions/MinActiveRowversion.cs | 16 + .../XQueryFunctions/XQueryValue.cs | 112 ++++++ .../Core/SqlExpressionEvaluator.cs | 7 + .../EvaluateBuiltInExpressionExtensions.cs | 4 +- .../EvaluateXQueryResultExtensions.cs | 21 ++ .../Routines/TSQLDomainAttributes.cs | 2 + .../ScalarExpressionEvaluator.cs | 5 + .../BigInt/SqlBigIntTypeConverter.cs | 13 +- .../TypeHandling/Binary/HexValue.cs | 185 ++++++++++ .../Binary/SqlBinaryTypeConverter.cs | 331 ++++++++++++++++++ .../Binary/SqlBinaryTypeHandler.cs | 103 ++++++ .../Binary/SqlBinaryTypeReference.cs | 12 + .../TypeHandling/Binary/SqlBinaryTypeValue.cs | 42 +++ .../Binary/SqlBinaryTypeValueFactory.cs | 164 +++++++++ .../TypeHandling/Date/SqlDateOnlyValue.cs | 43 +++ .../TypeHandling/Date/SqlDateTypeConverter.cs | 114 ++++++ .../TypeHandling/Date/SqlDateTypeHandler.cs | 39 +++ .../Date/SqlDateTypeValueFactory.cs | 126 +++++++ .../DateTime/SqlDateTimeTypeConverter.cs | 114 ++++++ .../DateTime/SqlDateTimeTypeHandler.cs | 70 ++++ .../DateTime/SqlDateTimeTypeValueFactory.cs | 132 +++++++ .../TypeHandling/DateTime/SqlDateTimeValue.cs | 42 +++ .../Decimal/SqlDecimalTypeConverter.cs | 113 ++++++ .../Decimal/SqlDecimalTypeHandler.cs | 213 +++++++++++ .../Decimal/SqlDecimalTypeReference.cs | 45 +++ .../Decimal/SqlDecimalTypeValue.cs | 43 +++ .../Decimal/SqlDecimalTypeValueFactory.cs | 157 +++++++++ .../Decimal/SqlDecimalValueRange.cs | 50 +++ .../GenericDateTime/DateTimeEnums.cs | 105 ++++++ .../SqlDateTimeRelativeValue.cs | 122 +++++++ .../SqlDateTimeTypeReference.cs | 29 ++ .../GenericDateTime/SqlDateTimeValueRange.cs | 37 ++ .../SqlGenericDateTimeTypeHandler.cs | 23 ++ .../SqlGenericDateTimeTypeValueFactory.cs | 45 +++ .../SqlGenericNumberValueRange.cs | 7 +- .../TypeHandling/Int/SqlIntTypeConverter.cs | 50 +++ .../TypeHandling/SqlGenericTypeReference.cs | 4 - .../TypeHandling/SqlTypeConverter.cs | 20 ++ .../TypeHandling/SqlTypeReference.cs | 4 + .../TypeHandling/Str/SqlStrTypeConverter.cs | 257 +++++++++++++- .../TypeHandling/Str/SqlStrTypeHandler.cs | 1 + .../TypeHandling/Str/SqlStrTypeReference.cs | 22 +- .../Str/SqlStrTypeValueFactory.cs | 2 + .../TypeHandling/Time/SqlTimeOnlyValue.cs | 43 +++ .../TypeHandling/Time/SqlTimeTypeConverter.cs | 114 ++++++ .../TypeHandling/Time/SqlTimeTypeHandler.cs | 39 +++ .../Time/SqlTimeTypeValueFactory.cs | 112 ++++++ .../Values/SqlLiteralValueFactory.cs | 11 + .../Tests/Evaluation/EvaluateXQueryTests.cs | 50 +++ .../Tests/Functions/BaseMockFunctionTest.cs | 13 + .../Functions/DateFunctions/DateAddTests.cs | 52 +++ .../Functions/DateFunctions/DateDiffTests.cs | 39 +++ .../Functions/DateFunctions/DateNameTests.cs | 19 +- .../DateFunctions/DatePartExtractorTests.cs | 59 ++++ .../Functions/DateFunctions/DatePartTests.cs | 25 +- .../DateFunctions/DateTimeFromPartsTests.cs | 52 +++ .../DateFunctions/EndOfMonthTests.cs | 56 +++ .../Functions/DateFunctions/MonthTests.cs | 17 +- .../Functions/MathFunctions/CeilingTests.cs | 47 +++ .../Functions/MathFunctions/RoundTests.cs | 68 ++++ .../SysFunctions/CursorStatusTests.cs | 56 +++ .../Tests/Functions/XQuery/XQueryValueTest.cs | 63 ++++ .../Tests/Mocks/MockSqlTypeConverter.cs | 129 +++++++ .../Tests/Mocks/MockSqlTypeReference.cs | 2 + .../Tests/Mocks/MockSqlValue.cs | 4 + .../Tests/Mocks/MockTypes.cs | 4 + .../Tests/Mocks/MockValueFactory.cs | 9 + .../BaseSqlTypeHandlerTestClass.cs | 9 + .../{ => BigInt}/SqlBigIntTypeHandlerTests.cs | 0 .../SqlBigIntTypeOperatorsTests.cs | 0 .../SqlBigIntTypeReferenceTests.cs | 0 .../SqlBigIntTypeValueFactoryTests.cs | 0 .../{ => BigInt}/SqlBigIntTypeValueTests.cs | 14 + .../{ => BigInt}/SqlBigIntValueRangeTests.cs | 0 .../TypeHandling/Binary/HexValueTests.cs | 195 +++++++++++ .../Binary/SqlBinaryTypeHandlerTests.cs | 225 ++++++++++++ .../Binary/SqlBinaryTypeValueFactoryTests.cs | 167 +++++++++ .../Date/SqlDateTypeHandlerTests.cs | 44 +++ .../Date/SqlDateTypeValueFactoryTests.cs | 69 ++++ .../DateTime/SqlDateTimeRelativeValueTests.cs | 31 ++ .../DateTime/SqlDateTimeTypeHandlerTests.cs | 44 +++ .../SqlDateTimeTypeValueFactoryTests.cs | 69 ++++ .../Decimal/SqlDecimalTypeHandlerTests.cs | 286 +++++++++++++++ .../SqlDecimalTypeValueFactoryTests.cs | 100 ++++++ .../Decimal/SqlDecimalValueRangeTests.cs | 95 +++++ .../{ => Int}/SqlIntTypeHandlerTests.cs | 0 .../{ => Int}/SqlIntTypeOperatorsTests.cs | 0 .../{ => Int}/SqlIntTypeReferenceTests.cs | 0 .../{ => Int}/SqlIntTypeValueFactoryTests.cs | 0 .../{ => Int}/SqlIntTypeValueTests.cs | 14 + .../{ => Int}/SqlIntValueRangeTests.cs | 0 .../SqlStrTypeDefinitionParserTests.cs | 0 .../{ => String}/SqlStrTypeHandlerTests.cs | 0 .../{ => String}/SqlStrTypeReferenceTests.cs | 6 - .../SqlStrTypeValueFactoryTests.cs | 0 .../{ => String}/SqlStrTypeValueTests.cs | 0 .../Time/SqlTimeTypeHandlerTests.cs | 45 +++ .../Time/SqlTimeTypeValueFactoryTests.cs | 70 ++++ TeamTools.TSQL.Linter/DefaultConfig.json | 14 + .../BooleanComparisonConverter.cs | 88 +++++ .../BooleanExpressionComparer.cs | 60 ++++ .../BooleanExpressionNormalizer.cs | 102 ++++++ .../BooleanExpressionParts.cs | 145 ++++++++ .../BooleanExpressionPartsExtractor.cs | 43 +++ .../Routines/CollapsibleInExtractor.cs | 111 ++++++ .../Routines/CombinablePredicateExtractor.cs | 251 +++++++++++++ .../DatabaseObjectIdentifierDetector.cs | 29 +- .../Routines/InvisibleCharDetector.cs | 74 ++++ .../Routines/NetStandardExtensions.cs | 26 +- .../ScriptDomExtensions/ScriptDomExtension.cs | 29 +- .../Ambiguity/SchemaQualifiedProcCallRule.cs | 8 + .../CodeSmell/CommentHasInvisibleCharRule.cs | 37 ++ .../CodeSmell/ComparedExpressionsEqualRule.cs | 58 +++ .../ConditionsLeadToSimilarBehaviorRule.cs | 135 +++++++ .../Rules/CodeSmell/DuplicateConditionRule.cs | 240 +------------ ...FakeOuterJoinRule.OuterSourceExtraction.cs | 107 ++++++ ...keOuterJoinRule.WherePredicateExpansion.cs | 79 +++++ .../Rules/CodeSmell/FakeOuterJoinRule.cs | 101 ++++++ .../IdentifierHasInvisibleCharRule.cs | 24 ++ .../InsertedDeletedIrrelevanceRule.cs | 102 ++++++ .../CodeSmell/LiteralHasInvisibleCharRule.cs | 51 +-- .../NonCorrelatedJoinPredicateRule.cs | 281 +++++++++++++++ .../CodingConvention/ExecWithoutExecRule.cs | 13 + .../Naming/AlphabetMixInIdentifierRule.cs | 16 +- .../IdentifierContainsLookAlikeCharRule.cs | 40 +++ .../Performance/NonSargablePredicateRule.cs | 24 +- .../SubstringPredicateToLikeRule.cs | 105 ++++-- .../Redundancy/RedundantIndexFilterRule.cs | 185 ++++++++++ .../Rules/Redundancy/SignedZeroRule.cs | 2 +- .../WherePredicateSameAsJoinPredicateRule.cs | 135 +++++++ .../ScriptAnalysisServiceConsumingRule.cs | 6 +- .../Simplification/MultipleAndToNotInRule.cs | 40 +++ .../MultipleInPredicateIntoOneRule.cs | 31 ++ .../MultipleNotInPredicateIntoOneRule.cs | 31 ++ .../Simplification/MultipleOrToInRule.cs | 40 +++ .../SimplePredicateNegationRule.cs | 80 +---- .../TestingInfrastructure/MockLinter.cs | 8 + ...ble_char_identifier_raise_0_violations.sql | 4 + .../CommentHasInvisibleCharRuleTests.cs | 22 ++ .../all_good_raise_0_violations.sql | 7 + .../bad_chars_raise_2_violations.sql | 5 + .../ComparedExpressionsEqualRuleTests.cs | 22 ++ .../all_good_raise_0_violations.sql | 7 + .../magic_literals_raise_0_violations.sql | 7 + .../same_sides_raise_4_violations.sql | 7 + ...onditionsLeadToSimilarBehaviorRuleTests.cs | 22 ++ .../all_different_raise_0_violations.sql | 18 + .../case_similar_raise_2_violations.sql | 6 + .../if_else_similar_raise_0_violations.sql | 15 + .../iif_different_raise_0_violations.sql | 2 + .../iif_similar_raise_1_violations.sql | 5 + .../single_option_raise_0_violations.sql | 8 + .../FakeOuterJoinRuleTests.cs | 22 ++ .../all_good_raise_0_violations.sql | 92 +++++ .../on_on_where_raise_1_violations.sql | 17 + .../where_by_alias_raise_3_violations.sql | 20 ++ .../where_by_name_raise_2_violations.sql | 16 + ...e_outer_is_not_null_raise_1_violations.sql | 5 + .../IdentifierHasInvisibleCharRuleTests.cs | 22 ++ .../all_good_raise_0_violations.sql | 7 + .../bad_chars_raise_2_violations.sql | 5 + .../InsertedDeletedIrrelevanceRuleTests.cs | 22 ++ .../insert_delete_bad_raise_4_violations.sql | 23 ++ ..._update_delete_good_raise_0_violations.sql | 57 +++ .../merge_bad_raise_2_violations.sql | 17 + .../merge_good_raise_0_violations.sql | 42 +++ .../TestSources/binary_raise_2_violations.sql | 2 + .../good_size_raise_0_violations.sql | 4 + .../NonCorrelatedJoinPredicateRuleTests.cs | 22 ++ .../all_good_raise_0_violations.sql | 67 ++++ .../bad_join_raise_3_violations.sql | 21 ++ .../bad_left_inner_raise_2_violations.sql | 6 + .../between_raise_0_violations.sql | 4 + .../TestSources/like_raise_0_violations.sql | 4 + ...ble_char_identifier_raise_0_violations.sql | 4 + .../all_good_raise_0_violations.sql | 3 + ..._mix_in_definitions_raise_3_violations.sql | 2 +- ...ad_col_not_invented_raise_0_violations.sql | 3 + .../column_alias_mix_raise_3_violations.sql | 17 + .../complex_names_raise_0_violations.sql | 3 + ...on_column_alias_mix_raise_1_violations.sql | 4 + .../quoted_identifier_raise_0_violations.sql | 3 + ...dentifierContainsLookAlikeCharRuleTests.cs | 22 ++ .../all_good_raise_0_violations.sql | 5 + .../bad_char_raise_2_violations.sql | 5 + .../computations_raise_3_violations.sql | 4 +- .../charindex_raise_2_violations.sql | 4 + .../TestSources/join_raise_2_violations.sql | 5 + .../left_as_like_raise_2_violations.sql | 2 +- .../other_function_raise_0_violations.sql | 1 + ...nown_value_or_start_raise_0_violations.sql | 7 + .../RedundantIndexFilterRuleTests.cs | 22 ++ .../all_good_raise_0_violations.sql | 34 ++ .../check_filter_raise_2_violations.sql | 11 + ...t_fail_on_filetable_raise_0_violations.sql | 3 + .../inline_idx_bad_raise_2_violations.sql | 10 + .../inline_idx_good_raise_0_violations.sql | 9 + ...not_null_col_filter_raise_1_violations.sql | 9 + .../TestSources/or_raise_0_violations.sql | 14 + .../all_good_raise_0_violations.sql | 33 ++ .../extra_predicate_raise_2_violations.sql | 14 + .../left_inner_raise_0_violations.sql | 8 + ...rePredicateSameAsJoinPredicateRuleTests.cs | 22 ++ .../MultipleAndToNotInRuleTests.cs | 22 ++ .../all_good_raise_0_violations.sql | 20 ++ .../not_equals_to_in_raise_2_violations.sql | 14 + .../MultipleInPredicateIntoOneRuleTests.cs | 22 ++ .../all_good_raise_0_violations.sql | 23 ++ .../multiple_in_raise_1_violations.sql | 5 + .../multiple_not_in_raise_0_violations.sql | 5 + .../MultipleNotInPredicateIntoOneRuleTests.cs | 22 ++ .../all_good_raise_0_violations.sql | 23 ++ .../multiple_in_raise_0_violations.sql | 5 + .../multiple_not_in_raise_1_violations.sql | 5 + .../MultipleOrToInRuleTests.cs | 22 ++ .../all_good_raise_0_violations.sql | 20 ++ .../already_has_in_raise_1_violations.sql | 6 + .../many_items_raise_2_violations.sql | 17 + .../TestSources/not_in_raise_0_violations.sql | 4 + .../table_definition_raise_0_violations.sql | 6 + 253 files changed, 10531 insertions(+), 524 deletions(-) create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/Abstractions/CurrentMomentFunction.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/Abstractions/RoundingFunction.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/ArgumentValidators/ArgumentIsValidDateTime.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/BaseDateTimeFromParts.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/CurrentDate.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/CurrentTimestamp.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DateAdd.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DateFromParts.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DatePartExtractor.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DateTimeFromParts.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/EndOfMonth.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/GetDate.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/SmallDateTimeFromParts.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/SysDateTime.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/TimeFromParts.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/MathFunctions/Ceiling.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/MathFunctions/Floor.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/MathFunctions/Round.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/StrFunctions/HashBytes.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/CursorRows.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/CursorStatus.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/Dbts.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/MaxPrecision.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/MinActiveRowversion.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/XQueryFunctions/XQueryValue.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/Evaluation/EvaluateXQueryResultExtensions.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Binary/HexValue.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Binary/SqlBinaryTypeConverter.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Binary/SqlBinaryTypeHandler.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Binary/SqlBinaryTypeReference.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Binary/SqlBinaryTypeValue.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Binary/SqlBinaryTypeValueFactory.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Date/SqlDateOnlyValue.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Date/SqlDateTypeConverter.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Date/SqlDateTypeHandler.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Date/SqlDateTypeValueFactory.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/DateTime/SqlDateTimeTypeConverter.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/DateTime/SqlDateTimeTypeHandler.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/DateTime/SqlDateTimeTypeValueFactory.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/DateTime/SqlDateTimeValue.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Decimal/SqlDecimalTypeConverter.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Decimal/SqlDecimalTypeHandler.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Decimal/SqlDecimalTypeReference.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Decimal/SqlDecimalTypeValue.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Decimal/SqlDecimalTypeValueFactory.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Decimal/SqlDecimalValueRange.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericDateTime/DateTimeEnums.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericDateTime/SqlDateTimeRelativeValue.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericDateTime/SqlDateTimeTypeReference.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericDateTime/SqlDateTimeValueRange.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericDateTime/SqlGenericDateTimeTypeHandler.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericDateTime/SqlGenericDateTimeTypeValueFactory.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Time/SqlTimeOnlyValue.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Time/SqlTimeTypeConverter.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Time/SqlTimeTypeHandler.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Time/SqlTimeTypeValueFactory.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Evaluation/EvaluateXQueryTests.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/DateAddTests.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/DateDiffTests.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/DatePartExtractorTests.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/DateTimeFromPartsTests.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/EndOfMonthTests.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/MathFunctions/CeilingTests.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/MathFunctions/RoundTests.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/SysFunctions/CursorStatusTests.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/XQuery/XQueryValueTest.cs rename TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/{ => BigInt}/SqlBigIntTypeHandlerTests.cs (100%) rename TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/{ => BigInt}/SqlBigIntTypeOperatorsTests.cs (100%) rename TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/{ => BigInt}/SqlBigIntTypeReferenceTests.cs (100%) rename TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/{ => BigInt}/SqlBigIntTypeValueFactoryTests.cs (100%) rename TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/{ => BigInt}/SqlBigIntTypeValueTests.cs (81%) rename TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/{ => BigInt}/SqlBigIntValueRangeTests.cs (100%) create mode 100644 TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Binary/HexValueTests.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Binary/SqlBinaryTypeHandlerTests.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Binary/SqlBinaryTypeValueFactoryTests.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Date/SqlDateTypeHandlerTests.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Date/SqlDateTypeValueFactoryTests.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/DateTime/SqlDateTimeRelativeValueTests.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/DateTime/SqlDateTimeTypeHandlerTests.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/DateTime/SqlDateTimeTypeValueFactoryTests.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Decimal/SqlDecimalTypeHandlerTests.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Decimal/SqlDecimalTypeValueFactoryTests.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Decimal/SqlDecimalValueRangeTests.cs rename TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/{ => Int}/SqlIntTypeHandlerTests.cs (100%) rename TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/{ => Int}/SqlIntTypeOperatorsTests.cs (100%) rename TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/{ => Int}/SqlIntTypeReferenceTests.cs (100%) rename TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/{ => Int}/SqlIntTypeValueFactoryTests.cs (100%) rename TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/{ => Int}/SqlIntTypeValueTests.cs (81%) rename TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/{ => Int}/SqlIntValueRangeTests.cs (100%) rename TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/{ => String}/SqlStrTypeDefinitionParserTests.cs (100%) rename TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/{ => String}/SqlStrTypeHandlerTests.cs (100%) rename TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/{ => String}/SqlStrTypeReferenceTests.cs (93%) rename TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/{ => String}/SqlStrTypeValueFactoryTests.cs (100%) rename TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/{ => String}/SqlStrTypeValueTests.cs (100%) create mode 100644 TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Time/SqlTimeTypeHandlerTests.cs create mode 100644 TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Time/SqlTimeTypeValueFactoryTests.cs create mode 100644 TeamTools.TSQL.Linter/Routines/BooleanExpressionHandling/BooleanComparisonConverter.cs create mode 100644 TeamTools.TSQL.Linter/Routines/BooleanExpressionHandling/BooleanExpressionComparer.cs create mode 100644 TeamTools.TSQL.Linter/Routines/BooleanExpressionHandling/BooleanExpressionNormalizer.cs create mode 100644 TeamTools.TSQL.Linter/Routines/BooleanExpressionHandling/BooleanExpressionParts.cs create mode 100644 TeamTools.TSQL.Linter/Routines/BooleanExpressionHandling/BooleanExpressionPartsExtractor.cs create mode 100644 TeamTools.TSQL.Linter/Routines/CollapsibleInExtractor.cs create mode 100644 TeamTools.TSQL.Linter/Routines/CombinablePredicateExtractor.cs create mode 100644 TeamTools.TSQL.Linter/Routines/InvisibleCharDetector.cs create mode 100644 TeamTools.TSQL.Linter/Rules/CodeSmell/CommentHasInvisibleCharRule.cs create mode 100644 TeamTools.TSQL.Linter/Rules/CodeSmell/ComparedExpressionsEqualRule.cs create mode 100644 TeamTools.TSQL.Linter/Rules/CodeSmell/ConditionsLeadToSimilarBehaviorRule.cs create mode 100644 TeamTools.TSQL.Linter/Rules/CodeSmell/FakeOuterJoinRule.OuterSourceExtraction.cs create mode 100644 TeamTools.TSQL.Linter/Rules/CodeSmell/FakeOuterJoinRule.WherePredicateExpansion.cs create mode 100644 TeamTools.TSQL.Linter/Rules/CodeSmell/FakeOuterJoinRule.cs create mode 100644 TeamTools.TSQL.Linter/Rules/CodeSmell/IdentifierHasInvisibleCharRule.cs create mode 100644 TeamTools.TSQL.Linter/Rules/CodeSmell/InsertedDeletedIrrelevanceRule.cs create mode 100644 TeamTools.TSQL.Linter/Rules/CodeSmell/NonCorrelatedJoinPredicateRule.cs create mode 100644 TeamTools.TSQL.Linter/Rules/Naming/IdentifierContainsLookAlikeCharRule.cs create mode 100644 TeamTools.TSQL.Linter/Rules/Redundancy/RedundantIndexFilterRule.cs create mode 100644 TeamTools.TSQL.Linter/Rules/Redundancy/WherePredicateSameAsJoinPredicateRule.cs create mode 100644 TeamTools.TSQL.Linter/Rules/Simplification/MultipleAndToNotInRule.cs create mode 100644 TeamTools.TSQL.Linter/Rules/Simplification/MultipleInPredicateIntoOneRule.cs create mode 100644 TeamTools.TSQL.Linter/Rules/Simplification/MultipleNotInPredicateIntoOneRule.cs create mode 100644 TeamTools.TSQL.Linter/Rules/Simplification/MultipleOrToInRule.cs create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SchemaQualifiedProcCallRule/TestSources/invisible_char_identifier_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/CommentHasInvisibleCharRule/CommentHasInvisibleCharRuleTests.cs create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/CommentHasInvisibleCharRule/TestSources/all_good_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/CommentHasInvisibleCharRule/TestSources/bad_chars_raise_2_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ComparedExpressionsEqualRule/ComparedExpressionsEqualRuleTests.cs create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ComparedExpressionsEqualRule/TestSources/all_good_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ComparedExpressionsEqualRule/TestSources/magic_literals_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ComparedExpressionsEqualRule/TestSources/same_sides_raise_4_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/ConditionsLeadToSimilarBehaviorRuleTests.cs create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/TestSources/all_different_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/TestSources/case_similar_raise_2_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/TestSources/if_else_similar_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/TestSources/iif_different_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/TestSources/iif_similar_raise_1_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/TestSources/single_option_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/FakeOuterJoinRule/FakeOuterJoinRuleTests.cs create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/FakeOuterJoinRule/TestSources/all_good_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/FakeOuterJoinRule/TestSources/on_on_where_raise_1_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/FakeOuterJoinRule/TestSources/where_by_alias_raise_3_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/FakeOuterJoinRule/TestSources/where_by_name_raise_2_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/FakeOuterJoinRule/TestSources/where_outer_is_not_null_raise_1_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IdentifierHasInvisibleCharRule/IdentifierHasInvisibleCharRuleTests.cs create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IdentifierHasInvisibleCharRule/TestSources/all_good_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IdentifierHasInvisibleCharRule/TestSources/bad_chars_raise_2_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/InsertedDeletedIrrelevanceRule/InsertedDeletedIrrelevanceRuleTests.cs create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/InsertedDeletedIrrelevanceRule/TestSources/insert_delete_bad_raise_4_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/InsertedDeletedIrrelevanceRule/TestSources/insert_update_delete_good_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/InsertedDeletedIrrelevanceRule/TestSources/merge_bad_raise_2_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/InsertedDeletedIrrelevanceRule/TestSources/merge_good_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/LiteralOversizeRule/TestSources/binary_raise_2_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/NonCorrelatedJoinPredicateRuleTests.cs create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/all_good_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/bad_join_raise_3_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/bad_left_inner_raise_2_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/between_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/like_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/CodingConvention/ExecWithoutExecRule/TestSources/invisible_char_identifier_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/bad_col_not_invented_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/column_alias_mix_raise_3_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/complex_names_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/open_json_column_alias_mix_raise_1_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/quoted_identifier_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Naming/IdentifierContainsLookAlikeCharRule/IdentifierContainsLookAlikeCharRuleTests.cs create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Naming/IdentifierContainsLookAlikeCharRule/TestSources/all_good_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Naming/IdentifierContainsLookAlikeCharRule/TestSources/bad_char_raise_2_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Performance/SubstringPredicateToLikeRule/TestSources/charindex_raise_2_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Performance/SubstringPredicateToLikeRule/TestSources/join_raise_2_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Performance/SubstringPredicateToLikeRule/TestSources/unknown_value_or_start_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/RedundantIndexFilterRuleTests.cs create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/all_good_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/check_filter_raise_2_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/does_not_fail_on_filetable_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/inline_idx_bad_raise_2_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/inline_idx_good_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/not_null_col_filter_raise_1_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/or_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Redundancy/WherePredicateSameAsJoinPredicateRule/TestSources/all_good_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Redundancy/WherePredicateSameAsJoinPredicateRule/TestSources/extra_predicate_raise_2_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Redundancy/WherePredicateSameAsJoinPredicateRule/TestSources/left_inner_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Redundancy/WherePredicateSameAsJoinPredicateRule/WherePredicateSameAsJoinPredicateRuleTests.cs create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleAndToNotInRule/MultipleAndToNotInRuleTests.cs create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleAndToNotInRule/TestSources/all_good_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleAndToNotInRule/TestSources/not_equals_to_in_raise_2_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInPredicateIntoOneRule/MultipleInPredicateIntoOneRuleTests.cs create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInPredicateIntoOneRule/TestSources/all_good_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInPredicateIntoOneRule/TestSources/multiple_in_raise_1_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInPredicateIntoOneRule/TestSources/multiple_not_in_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleNotInPredicateIntoOneRule/MultipleNotInPredicateIntoOneRuleTests.cs create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleNotInPredicateIntoOneRule/TestSources/all_good_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleNotInPredicateIntoOneRule/TestSources/multiple_in_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleNotInPredicateIntoOneRule/TestSources/multiple_not_in_raise_1_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleOrToInRule/MultipleOrToInRuleTests.cs create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleOrToInRule/TestSources/all_good_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleOrToInRule/TestSources/already_has_in_raise_1_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleOrToInRule/TestSources/many_items_raise_2_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleOrToInRule/TestSources/not_in_raise_0_violations.sql create mode 100644 TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleOrToInRule/TestSources/table_definition_raise_0_violations.sql diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/Abstractions/CurrentMomentFunction.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/Abstractions/CurrentMomentFunction.cs new file mode 100644 index 00000000..db63a72a --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/Abstractions/CurrentMomentFunction.cs @@ -0,0 +1,35 @@ +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentDto; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.Abstractions +{ + public abstract class CurrentMomentFunction : SqlZeroArgFunctionHandler + where TOut : SqlValue + { + private readonly TimeDetails timeDetails; + private readonly DateDetails dateDetails; + + protected CurrentMomentFunction(string funcName, string outputType, TimeDetails timeDetails, DateDetails dateDetails) : base(funcName, outputType) + { + this.timeDetails = timeDetails; + this.dateDetails = dateDetails; + } + + protected override SqlValue DoEvaluateResultValue(CallSignature call) + { + var value = call.Context.Converter.ImplicitlyConvert(base.DoEvaluateResultValue(call)); + + if (value is null) + { + return default; + } + + var newRange = new SqlDateTimeValueRange(new SqlDateTimeRelativeValue(DateTimeRangeKind.CurrentMoment, timeDetails, dateDetails)); + + return ApplyNewRange(value, newRange, call.Context.NewSource); + } + + protected abstract TOut ApplyNewRange(TOut value, SqlDateTimeValueRange newRange, SqlValueSource src); + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/Abstractions/ExplicitConvertionFunction.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/Abstractions/ExplicitConvertionFunction.cs index 1d5f4203..cb7eddc7 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/Abstractions/ExplicitConvertionFunction.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/Abstractions/ExplicitConvertionFunction.cs @@ -1,4 +1,5 @@ -using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentDto; +using System.Diagnostics.CodeAnalysis; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentDto; using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentValidators; using TeamTools.TSQL.ExpressionEvaluator.Evaluation; using TeamTools.TSQL.ExpressionEvaluator.Properties; @@ -15,6 +16,10 @@ protected ExplicitConvertionFunction(string funcName, int requiredArgs = 2) : ba { } + protected ExplicitConvertionFunction(string funcName, int minArgs, int maxArgs) : base(funcName, minArgs, maxArgs) + { + } + private static string MsgSourceIsAlreadyOfThatType => Strings.ViolationDetails_RedundantTypeConversionViolation_ValueIsAlreadyOfThisType; public override bool ValidateArgumentValues(CallSignature call) @@ -27,6 +32,15 @@ public override bool ValidateArgumentValues(CallSignature call) .When(ArgumentIsValue.Validate) .Then(v => call.ValidatedArgs.SrcValue = v); + if (MaxArgs > 2 && call.RawArgs.Count == 3) + { + ValidationScenario + .For("STYLE", call.RawArgs[2], call.Context) + .When(ArgumentIsValue.Validate) + .And(ArgumentIsValidInt.Validate) + .Then(v => call.ValidatedArgs.ConvertionStyle = v); + } + return ValidationScenario .For("TYPE", call.RawArgs[1], call.Context) .When(ArgumentIsType.Validate) @@ -43,7 +57,12 @@ protected override SqlValue DoEvaluateResultValue(CallSignature cal } if (call.ValidatedArgs.TargetType is SqlStrTypeReference str && str.IsUnicode - && call.ValidatedArgs.SrcValue is SqlIntTypeValue intValue) + && (call.ValidatedArgs.SrcValue is SqlIntTypeValue + || call.ValidatedArgs.SrcValue is SqlBinaryTypeValue + || call.ValidatedArgs.SrcValue is SqlDecimalTypeValue + || call.ValidatedArgs.SrcValue is SqlDateTimeValue + || call.ValidatedArgs.SrcValue is SqlDateOnlyValue + || call.ValidatedArgs.SrcValue is SqlTimeOnlyValue)) { // TODO : If this conversion result is used in concatenation with other NVARCHAR strings // then no violation should be issued. @@ -55,7 +74,26 @@ protected override SqlValue DoEvaluateResultValue(CallSignature cal } else if (call.ValidatedArgs.SrcValue.SourceKind == SqlValueSourceKind.Literal && call.ValidatedArgs.SrcValue.IsPreciseValue) { - msg = string.Format(Strings.ViolationDetails_NumbersHaveNoUnicode_LiteralValueIsNumber, FunctionName, intValue.Value.ToString()); + string numericSourceValue = null; + + if (call.ValidatedArgs.SrcValue is SqlIntTypeValue intValue) + { + numericSourceValue = intValue.Value.ToString(); + } + else if (call.ValidatedArgs.SrcValue is SqlDateTimeValue datetimeValue) + { + numericSourceValue = datetimeValue.Value.ToString(); + } + else if (call.ValidatedArgs.SrcValue is SqlDateOnlyValue dateValue) + { + numericSourceValue = dateValue.Value.ToString(); + } + else if (call.ValidatedArgs.SrcValue is SqlTimeOnlyValue timeValue) + { + numericSourceValue = timeValue.Value.ToString(); + } + + msg = string.Format(Strings.ViolationDetails_NumbersHaveNoUnicode_LiteralValueIsNumber, FunctionName, numericSourceValue); } else { @@ -90,11 +128,14 @@ private static void RegisterRedundantConversionViolation(SqlValue srcValue, Eval context.Violations.RegisterViolation(new RedundantTypeConversionViolation(violationMessage, context.NewSource)); } + [ExcludeFromCodeCoverage] public class ConvertArgs { public SqlTypeReference TargetType { get; set; } public SqlValue SrcValue { get; set; } + + public SqlIntTypeValue ConvertionStyle { get; set; } } } } diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/Abstractions/RoundingFunction.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/Abstractions/RoundingFunction.cs new file mode 100644 index 00000000..a4674df4 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/Abstractions/RoundingFunction.cs @@ -0,0 +1,105 @@ +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentDto; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentValidators; +using TeamTools.TSQL.ExpressionEvaluator.Routines; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.Abstractions +{ + public abstract class RoundingFunction : SqlGenericFunctionHandler + where TArgs : RoundingFunction.RoundingFunctionArgs, new() + { + private static readonly int DefaultRequiredArgumentCount = 1; + private static readonly string FallbackResultType = TSqlDomainAttributes.Types.Int; + + protected RoundingFunction(string funcName, int minArgs, int maxArgs) : base(funcName, minArgs, maxArgs) + { + } + + protected RoundingFunction(string funcName) : base(funcName, DefaultRequiredArgumentCount) + { + } + + public override bool ValidateArgumentValues(CallSignature call) + { + return ValidationScenario + .For("SOURCE_VALUE", call.RawArgs[0], call.Context) + .When(ArgumentIsValue.Validate) + .Then(n => call.ValidatedArgs.SourceValue = n); + } + + protected override string DoEvaluateResultType(CallSignature call) + { + if (call.ValidatedArgs.SourceValue is SqlIntTypeValue i) + { + // For int types output type is always INT + // TODO : except for BIT - it is FLOAT + // docs: https://learn.microsoft.com/en-us/sql/t-sql/functions/ceiling-transact-sql?view=sql-server-ver17#return-types + return i.TypeHandler.IntValueFactory.FallbackTypeName; + } + + if (call.ValidatedArgs.SourceValue is SqlBigIntTypeValue + || call.ValidatedArgs.SourceValue is SqlDecimalTypeValue) + { + return call.ValidatedArgs.SourceValue.TypeName; + } + + return FallbackResultType; + } + + protected override SqlValue DoEvaluateResultValue(CallSignature call) + { + // TODO : could be int or bigint + var value = call.Context.Converter.ImplicitlyConvert(base.DoEvaluateResultValue(call)); + + if (value is null) + { + return value; + } + + if (call.ValidatedArgs.SourceValue.IsNull) + { + return call.ResultTypeHandler.ValueFactory.NewNull(call.Context.Node); + } + + if (call.ValidatedArgs.SourceValue is SqlIntTypeValue i) + { + call.Context.RedundantCall("Source is a number without fractions"); + + // the result is the same as source + return value.ChangeTo((decimal)i.Value, call.Context.NewSource); + } + + if (call.ValidatedArgs.SourceValue is SqlBigIntTypeValue bi) + { + call.Context.RedundantCall("Source is a number without fractions"); + + // the result is the same as source + return value.ChangeTo((decimal)bi.Value, call.Context.NewSource); + } + + if (call.ValidatedArgs.SourceValue is SqlDecimalTypeValue decSrc) + { + if (decSrc.EstimatedSize.Scale == 0) + { + call.Context.RedundantCall("Source is a number without fractions"); + } + + if (decSrc.IsPreciseValue) + { + return value.ChangeTo(ProduceRounding(decSrc.Value, call.ValidatedArgs), call.Context.NewSource); + } + } + + // TODO : return precise value if possible or a limited value range + return value; + } + + protected abstract decimal ProduceRounding(decimal value, TArgs arguments); + + public class RoundingFunctionArgs + { + public SqlValue SourceValue { get; set; } + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/ArgumentValidators/ArgumentIsValidDateTime.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/ArgumentValidators/ArgumentIsValidDateTime.cs new file mode 100644 index 00000000..bb5acbc8 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/ArgumentValidators/ArgumentIsValidDateTime.cs @@ -0,0 +1,34 @@ +using System; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentValidators +{ + public static class ArgumentIsValidDateTime + { + public static Func< + ArgumentValidation, + SqlValue, + Action, + bool> Validate + { get; } = new Func, bool>(DoValidate); + + private static bool DoValidate( + ArgumentValidation argData, + SqlValue argValue, + Action success) + { + var datetimeValue = argData.Context.Converter.ImplicitlyConvert(argValue); + + if (datetimeValue is null) + { + argData.Context.InvalidArgument(argData.ArgumentName, "Conversion to DATETIME failed"); + return false; + } + + success?.Invoke(datetimeValue); + + return true; + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/ConversionFunctions/Convert.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/ConversionFunctions/Convert.cs index 36f5fb3b..310481ff 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/ConversionFunctions/Convert.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/ConversionFunctions/Convert.cs @@ -5,8 +5,10 @@ namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ConversionFunction public class Convert : ExplicitConvertionFunction { private static readonly string FuncName = "CONVERT"; + private static readonly int MinArgumentCount = 2; + private static readonly int MaxArgumentCount = 3; // With conversion Style provided - public Convert() : base(FuncName) + public Convert() : base(FuncName, MinArgumentCount, MaxArgumentCount) { } } diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/ConversionFunctions/TryConvert.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/ConversionFunctions/TryConvert.cs index 6d04b519..34785c24 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/ConversionFunctions/TryConvert.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/ConversionFunctions/TryConvert.cs @@ -5,8 +5,10 @@ namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ConversionFunction public class TryConvert : ExplicitConvertionFunction { private static readonly string FuncName = "TRY_CONVERT"; + private static readonly int MinArgumentCount = 2; + private static readonly int MaxArgumentCount = 3; // With conversion Style provided - public TryConvert() : base(FuncName) + public TryConvert() : base(FuncName, MinArgumentCount, MaxArgumentCount) { } } diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/BaseDateTimeFromParts.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/BaseDateTimeFromParts.cs new file mode 100644 index 00000000..f738aca7 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/BaseDateTimeFromParts.cs @@ -0,0 +1,185 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentDto; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentValidators; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.DateFunctions +{ + public abstract class BaseDateTimeFromParts : SqlGenericFunctionHandler + { + private static readonly SqlIntValueRange MonthRange = new SqlIntValueRange(1, 12); + private static readonly SqlIntValueRange DayOfMonthRange = new SqlIntValueRange(1, 31); + private static readonly SqlIntValueRange HourRange = new SqlIntValueRange(0, 24); + private static readonly SqlIntValueRange MinuteRange = new SqlIntValueRange(0, 60); + private static readonly SqlIntValueRange SecondsRange = new SqlIntValueRange(0, 60); + private static readonly SqlIntValueRange FractionsRange = new SqlIntValueRange(0, 999); + private static readonly SqlIntValueRange PrecisionRange = new SqlIntValueRange(0, 7); + + private readonly bool withDate; + private readonly bool withTime; + private readonly string outputType; + + protected BaseDateTimeFromParts(string funcName, int requiredArgumentCount, string outputType, bool withDate, bool withTime) : base(funcName, requiredArgumentCount) + { + this.withDate = withDate; + this.withTime = withTime; + this.outputType = outputType; + } + + public override bool ValidateArgumentValues(CallSignature call) + { + bool result = true; + int argIdx = -1; + + if (withDate) + { + result &= ValidationScenario + .For("YEAR_VALUE", call.RawArgs[++argIdx], call.Context) + .When(ArgumentIsValue.Validate) + .And(ArgumentIsValidInt.ValidatePositiveInt) + .Then(n => call.ValidatedArgs.Year = n) + && ValidationScenario + .For("MONTH_VALUE", call.RawArgs[++argIdx], call.Context) + .When(ArgumentIsValue.Validate) + .And((arg, val, success) => ArgumentIsValidInt.ValidateWithinRange(arg, val, success, MonthRange)) + .Then(n => call.ValidatedArgs.Month = n) + && ValidationScenario + .For("DAY_VALUE", call.RawArgs[++argIdx], call.Context) + .When(ArgumentIsValue.Validate) + .And((arg, val, success) => ArgumentIsValidInt.ValidateWithinRange(arg, val, success, DayOfMonthRange)) + .Then(n => call.ValidatedArgs.Day = n); + } + + if (withTime) + { + result &= ValidationScenario + .For("HOUR_VALUE", call.RawArgs[++argIdx], call.Context) + .When(ArgumentIsValue.Validate) + .And((arg, val, success) => ArgumentIsValidInt.ValidateWithinRange(arg, val, success, HourRange)) + .Then(n => call.ValidatedArgs.Hour = n) + && ValidationScenario + .For("MINUTE_VALUE", call.RawArgs[++argIdx], call.Context) + .When(ArgumentIsValue.Validate) + .And((arg, val, success) => ArgumentIsValidInt.ValidateWithinRange(arg, val, success, MinuteRange)) + .Then(n => call.ValidatedArgs.Minute = n); + + if (!string.Equals(outputType, "SMALLDATETIME", StringComparison.OrdinalIgnoreCase)) + { + // SMALLDATETIME does not have seconds and fractions + result &= ValidationScenario + .For("SECONDS_VALUE", call.RawArgs[++argIdx], call.Context) + .When(ArgumentIsValue.Validate) + .And((arg, val, success) => ArgumentIsValidInt.ValidateWithinRange(arg, val, success, SecondsRange)) + .Then(n => call.ValidatedArgs.Seconds = n) + && ValidationScenario + .For("FRACTIONS_VALUE", call.RawArgs[++argIdx], call.Context) + .When(ArgumentIsValue.Validate) + .And((arg, val, success) => ArgumentIsValidInt.ValidateWithinRange(arg, val, success, FractionsRange)) + .Then(n => call.ValidatedArgs.Fractions = n); + } + + if (!withDate) + { + // time-only version supports Precision + result &= ValidationScenario + .For("PRECISION_VALUE", call.RawArgs[++argIdx], call.Context) + .When(ArgumentIsValue.Validate) + .And((arg, val, success) => ArgumentIsValidInt.ValidateWithinRange(arg, val, success, PrecisionRange)) + .Then(n => call.ValidatedArgs.Precision = n); + } + } + + return result; + } + + protected override string DoEvaluateResultType(CallSignature call) => outputType; + + protected override sealed SqlValue DoEvaluateResultValue(CallSignature call) + { + var value = call.Context.Converter.ImplicitlyConvert(base.DoEvaluateResultValue(call)); + + if (value is null) + { + return default; + } + + if (withDate) + { + if (call.ValidatedArgs.Year.IsNull + || call.ValidatedArgs.Month.IsNull + || call.ValidatedArgs.Day.IsNull) + { + return call.ResultTypeHandler.ValueFactory.NewNull(call.Context.NewSource.Node); + } + } + + if (withTime) + { + if (call.ValidatedArgs.Hour.IsNull + || call.ValidatedArgs.Minute.IsNull + || call.ValidatedArgs.Seconds.IsNull + || call.ValidatedArgs.Fractions.IsNull + || call.ValidatedArgs.Precision?.IsNull == true) + { + return call.ResultTypeHandler.ValueFactory.NewNull(call.Context.NewSource.Node); + } + } + + if (withDate) + { + if (!call.ValidatedArgs.Year.IsPreciseValue + || !call.ValidatedArgs.Month.IsPreciseValue + || !call.ValidatedArgs.Day.IsPreciseValue) + { + // TODO : make approximation using ranges of arguments if provided + return value; + } + } + + if (withTime) + { + if (!call.ValidatedArgs.Hour.IsPreciseValue + || !call.ValidatedArgs.Minute.IsPreciseValue + || !call.ValidatedArgs.Seconds.IsPreciseValue + || !call.ValidatedArgs.Fractions.IsPreciseValue) + { + // TODO : make approximation using ranges of arguments if provided + return value; + } + } + + var year = call.ValidatedArgs.Year?.Value ?? 0; + var month = call.ValidatedArgs.Month?.Value ?? 0; + var day = call.ValidatedArgs.Day?.Value ?? 0; + + var hour = call.ValidatedArgs.Hour?.Value ?? 0; + var min = call.ValidatedArgs.Minute?.Value ?? 0; + var sec = call.ValidatedArgs.Seconds?.Value ?? 0; + var ms = call.ValidatedArgs.Fractions?.Value ?? 0; + + return value.ChangeTo(new DateTime(year, month, day, hour, min, sec, ms), call.Context.NewSource); + } + + [ExcludeFromCodeCoverage] + public sealed class DateTimeFromPartsArgs + { + public SqlIntTypeValue Year { get; set; } + + public SqlIntTypeValue Month { get; set; } + + public SqlIntTypeValue Day { get; set; } + + public SqlIntTypeValue Hour { get; set; } + + public SqlIntTypeValue Minute { get; set; } + + public SqlIntTypeValue Seconds { get; set; } + + public SqlIntTypeValue Fractions { get; set; } + + public SqlIntTypeValue Precision { get; set; } + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/CurrentDate.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/CurrentDate.cs new file mode 100644 index 00000000..52a99672 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/CurrentDate.cs @@ -0,0 +1,20 @@ +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.Abstractions; +using TeamTools.TSQL.ExpressionEvaluator.Routines; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.DateFunctions +{ + public class CurrentDate : CurrentMomentFunction + { + private static readonly string FuncName = "CURRENT_DATE"; + private static readonly string OutputType = TSqlDomainAttributes.Types.Date; + + public CurrentDate() : base(FuncName, OutputType, TimeDetails.None, DateDetails.Full) + { + } + + protected override SqlDateOnlyValue ApplyNewRange(SqlDateOnlyValue value, SqlDateTimeValueRange newRange, SqlValueSource src) + => value.ChangeTo(newRange, src); + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/CurrentTimestamp.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/CurrentTimestamp.cs new file mode 100644 index 00000000..9b64e121 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/CurrentTimestamp.cs @@ -0,0 +1,20 @@ +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.Abstractions; +using TeamTools.TSQL.ExpressionEvaluator.Routines; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.DateFunctions +{ + public class CurrentTimestamp : CurrentMomentFunction + { + private static readonly string FuncName = "CURRENT_TIMESTAMP"; + private static readonly string OutputType = TSqlDomainAttributes.Types.DateTime; + + public CurrentTimestamp() : base(FuncName, OutputType, TimeDetails.RegularDateTime, DateDetails.Full) + { + } + + protected override SqlDateTimeValue ApplyNewRange(SqlDateTimeValue value, SqlDateTimeValueRange newRange, SqlValueSource src) + => value.ChangeTo(newRange, src); + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DateAdd.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DateAdd.cs new file mode 100644 index 00000000..bd5ea52a --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DateAdd.cs @@ -0,0 +1,125 @@ +using System.Diagnostics.CodeAnalysis; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentDto; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentValidators; +using TeamTools.TSQL.ExpressionEvaluator.Routines; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.DateFunctions +{ + internal class DateAdd : SqlGenericFunctionHandler + { + private static readonly int RequiredArgumentCount = 3; + private static readonly string FuncName = "DATEADD"; + private static readonly string FallbackOutputType = TSqlDomainAttributes.Types.DateTime; + + public DateAdd() : base(FuncName, RequiredArgumentCount) + { + } + + public override bool ValidateArgumentValues(CallSignature call) + { + // we can still evaluate concrete result type even if increment is invalid + ValidationScenario + .For("DATE_INCREMENT", call.RawArgs[1], call.Context) + .When(ArgumentIsValue.Validate) + .And(ArgumentIsValidInt.Validate) + .Then(n => call.ValidatedArgs.Number = n); + + return ValidationScenario + .For("DATE_PART", call.RawArgs[0], call.Context) + .When(ArgumentIsDatePart.Validate) + .Then(s => call.ValidatedArgs.DatePart = s.DatePartValue) + && ValidationScenario + .For("DATE_VALUE", call.RawArgs[2], call.Context) + .When(ArgumentIsValue.Validate) + .Then(d => call.ValidatedArgs.DateValue = d); + } + + protected override string DoEvaluateResultType(CallSignature call) + { + if (call.ValidatedArgs.DateValue is SqlDateOnlyValue + || call.ValidatedArgs.DateValue is SqlTimeOnlyValue + || call.ValidatedArgs.DateValue is SqlDateTimeValue) + { + return call.ValidatedArgs.DateValue.TypeName; + } + + return FallbackOutputType; + } + + protected override SqlValue DoEvaluateResultValue(CallSignature call) + { + var value = call.Context.Converter.ImplicitlyConvert(base.DoEvaluateResultValue(call)); + var dateArg = call.Context.Converter.ImplicitlyConvert(call.ValidatedArgs.DateValue)?.Value; + + if (value is null || dateArg is null || call.ValidatedArgs.Number is null) + { + return value; + } + + if (call.ValidatedArgs.Number.IsNull || call.ValidatedArgs.DateValue.IsNull) + { + return call.ResultTypeHandler.ValueFactory.NewNull(call.Context.NewSource.Node); + } + + if (!call.ValidatedArgs.Number.IsPreciseValue || !call.ValidatedArgs.DateValue.IsPreciseValue) + { + return value; + } + + // we already ensured that both are not null + var dateIncrement = call.ValidatedArgs.Number.Value; + var dateEstimate = dateArg.Value; + + switch (call.ValidatedArgs.DatePart) + { + case DatePartEnum.Year: + dateEstimate = dateEstimate.AddYears(dateIncrement); + break; + + case DatePartEnum.Quarter: + dateEstimate = dateEstimate.AddMonths(dateIncrement * 3); + break; + + case DatePartEnum.Month: + dateEstimate = dateEstimate.AddMonths(dateIncrement); + break; + + case DatePartEnum.Day: + dateEstimate = dateEstimate.AddDays(dateIncrement); + break; + + case DatePartEnum.Hour: + dateEstimate = dateEstimate.AddHours(dateIncrement); + break; + + case DatePartEnum.Minute: + dateEstimate = dateEstimate.AddMinutes(dateIncrement); + break; + + case DatePartEnum.Second: + dateEstimate = dateEstimate.AddSeconds(dateIncrement); + break; + + case DatePartEnum.Millisecond: + dateEstimate = dateEstimate.AddMilliseconds(dateIncrement); + break; + + default: return value; + } + + return value.ChangeTo(dateEstimate, call.Context.NewSource); + } + + [ExcludeFromCodeCoverage] + public sealed class DateAddArgs + { + public DatePartEnum DatePart { get; set; } + + public SqlIntTypeValue Number { get; set; } + + public SqlValue DateValue { get; set; } + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DateDiff.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DateDiff.cs index 7a61cb30..3308fd87 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DateDiff.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DateDiff.cs @@ -1,12 +1,13 @@ -using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentDto; -using TeamTools.TSQL.ExpressionEvaluator.Evaluation; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentValidators; using TeamTools.TSQL.ExpressionEvaluator.Routines; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; using TeamTools.TSQL.ExpressionEvaluator.Values; namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.DateFunctions { - public class DateDiff : SqlFunctionHandler + public class DateDiff : SqlGenericFunctionHandler { private static readonly int RequiredArgumentCount = 3; private static readonly string FuncName = "DATEDIFF"; @@ -15,9 +16,82 @@ public class DateDiff : SqlFunctionHandler public DateDiff() : base(FuncName, RequiredArgumentCount) { } - public override SqlValue Evaluate(List args, EvaluationContext context) + public override bool ValidateArgumentValues(CallSignature call) { - return context.TypeResolver.ResolveType(OutputType).MakeUnknownValue(); + return ValidationScenario + .For("DATE_PART", call.RawArgs[0], call.Context) + .When(ArgumentIsDatePart.Validate) + .Then(d => call.ValidatedArgs.DatePart = d) + && ValidationScenario + .For("DATE_START", call.RawArgs[1], call.Context) + .When(ArgumentIsValue.Validate) + .And(ArgumentIsValidDateTime.Validate) + .Then(d => call.ValidatedArgs.StartDate = d) + && ValidationScenario + .For("DATE_END", call.RawArgs[2], call.Context) + .When(ArgumentIsValue.Validate) + .And(ArgumentIsValidDateTime.Validate) + .Then(d => call.ValidatedArgs.EndDate = d); + } + + protected override string DoEvaluateResultType(CallSignature call) => OutputType; + + protected override SqlValue DoEvaluateResultValue(CallSignature call) + { + var value = call.Context.Converter.ImplicitlyConvert(base.DoEvaluateResultValue(call)); + var dateStart = call.ValidatedArgs.StartDate; + var dateEnd = call.ValidatedArgs.EndDate; + + if (value is null || dateStart is null || dateEnd is null) + { + return value; + } + + if (call.ValidatedArgs.StartDate.IsNull || call.ValidatedArgs.EndDate.IsNull) + { + return call.ResultTypeHandler.ValueFactory.NewNull(call.Context.NewSource.Node); + } + + if (!call.ValidatedArgs.StartDate.IsPreciseValue || !call.ValidatedArgs.EndDate.IsPreciseValue) + { + // TODO : If StartDate is from Past and EndDate is from Future + // then the result can be limited [0 - +MaxInt]. + // If StartDate is from Future and EndDate is from Past + // then the result can be limited [-MaxInt - 0]. + return value; + } + + int dateDiff = 0; + + // TODO : support more date and time parts + switch (call.ValidatedArgs.DatePart.DatePartValue) + { + case DatePartEnum.Year: + dateDiff = dateEnd.Value.Year - dateStart.Value.Year; + break; + + case DatePartEnum.Month: + dateDiff = (12 * (dateEnd.Value.Year - dateStart.Value.Year)) + (dateEnd.Value.Month - dateStart.Value.Month); + break; + + case DatePartEnum.Day: + dateDiff = (dateEnd.Value - dateStart.Value).Days; + break; + + default: return value; + } + + return value.ChangeTo(dateDiff, call.Context.NewSource); + } + + [ExcludeFromCodeCoverage] + public sealed class DateDiffArgs + { + public DatePartArgument DatePart { get; set; } + + public SqlDateTimeValue StartDate { get; set; } + + public SqlDateTimeValue EndDate { get; set; } } } } diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DateFromParts.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DateFromParts.cs new file mode 100644 index 00000000..6c34de72 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DateFromParts.cs @@ -0,0 +1,15 @@ +using TeamTools.TSQL.ExpressionEvaluator.Routines; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.DateFunctions +{ + public class DateFromParts : BaseDateTimeFromParts + { + private static readonly int RequiredArgumentCount = 3; + private static readonly string FuncName = "DATEFROMPARTS"; + private static readonly string OutputType = TSqlDomainAttributes.Types.Date; + + public DateFromParts() : base(FuncName, RequiredArgumentCount, OutputType, true, false) + { + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DateName.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DateName.cs index 749c282e..4c6d6749 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DateName.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DateName.cs @@ -17,9 +17,15 @@ public DateName() : base(FuncName, RequiredArgumentCount) { } - // TODO : validate date arg public override bool ValidateArgumentValues(CallSignature call) { + // we can still estimate result value range even if date value is unknown + ValidationScenario + .For("DATE_VALUE", call.RawArgs[1], call.Context) + .When(ArgumentIsValue.Validate) + .And(ArgumentIsValidDateTime.Validate) + .Then(d => call.ValidatedArgs.DateValue = d); + return ValidationScenario .For("DATEPART", call.RawArgs[0], call.Context) .When(ArgumentIsDatePart.Validate) @@ -43,6 +49,21 @@ protected override SqlValue DoEvaluateResultValue(CallSignature ca return value; } + // Extracting concrete date part from provided precise date/time value + var dateValue = call.ValidatedArgs.DateValue; + if (dateValue?.IsPreciseValue == true + && DatePartExtractor.ExtractDatePartFromSpecificDate(dateValue.Value, call.ValidatedArgs.DatePart.DatePartValue, out int datePartValue)) + { + switch (call.ValidatedArgs.DatePart.DatePartValue) + { + // These two are culture-specific. Others are just numbers. + case DatePartEnum.Month: + case DatePartEnum.DayOfWeek: + break; + default: return value.ChangeTo(datePartValue.ToString(), call.Context.NewSource); + } + } + return value.ChangeTo(lengthEstimate, call.Context.NewSource); } @@ -51,7 +72,7 @@ public class DateNameArgs { public DatePartArgument DatePart { get; set; } - public ValueArgument DateValue { get; set; } + public SqlDateTimeValue DateValue { get; set; } } } } diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DatePart.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DatePart.cs index 305f3e45..5c381f2a 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DatePart.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DatePart.cs @@ -18,17 +18,19 @@ public DatePart() : base(FuncName, RequiredArgumentCount) { } - // TODO : validate date arg public override bool ValidateArgumentValues(CallSignature call) { + // Even if date is invalid we can still estimate result range based on DATEPART value + ValidationScenario + .For("DATE_VALUE", call.RawArgs[1], call.Context) + .When(ArgumentIsValue.Validate) + .And(ArgumentIsValidDateTime.Validate) + .Then(d => call.ValidatedArgs.DateValue = d); + return ValidationScenario - .For("DATEPART", call.RawArgs[0], call.Context) + .For("DATE_PART", call.RawArgs[0], call.Context) .When(ArgumentIsDatePart.Validate) - .Then(d => call.ValidatedArgs.DatePart = d) - && ValidationScenario - .For("DATE", call.RawArgs[1], call.Context) - .When(ArgumentIsValue.Validate) - .Then(s => call.ValidatedArgs.DateValue = s); + .Then(d => call.ValidatedArgs.DatePart = d); } protected override string DoEvaluateResultType(CallSignature call) => OutputType; @@ -48,6 +50,16 @@ protected override SqlValue DoEvaluateResultValue(CallSignature ca return value.ChangeTo(MaxDatePartRange, call.Context.NewSource); } + // TODO : if src is date only then attempt to extract time smells bad + // TODO : if src is time then attempt to extract date from it smells bad + var dt = call.ValidatedArgs.DateValue; + if (dt?.IsPreciseValue == true + && DatePartExtractor.ExtractDatePartFromSpecificDate(dt.Value, call.ValidatedArgs.DatePart.DatePartValue, out int datePartValue)) + { + // precise estimate + return value.ChangeTo(datePartValue, call.Context.NewSource); + } + return value.ChangeTo(datePartEstimate, call.Context.NewSource); } @@ -56,7 +68,7 @@ public class DatePartArgs { public DatePartArgument DatePart { get; set; } - public SqlValue DateValue { get; set; } + public SqlDateTimeValue DateValue { get; set; } } } } diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DatePartExtractor.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DatePartExtractor.cs new file mode 100644 index 00000000..98512b48 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DatePartExtractor.cs @@ -0,0 +1,79 @@ +using System; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentDto; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.DateFunctions +{ + public static class DatePartExtractor + { + public static bool ExtractDatePartFromSpecificDate(DateTime date, DatePartEnum datePart, out int datePartValue) + { + datePartValue = 0; + + if (date.Equals(DateTime.MinValue)) + { + return true; + } + + switch (datePart) + { + case DatePartEnum.Year: + datePartValue = date.Year; + break; + + case DatePartEnum.Month: + datePartValue = date.Month; + break; + + case DatePartEnum.Day: + datePartValue = date.Day; + break; + + case DatePartEnum.Quarter: + datePartValue = 1 + ((date.Month - 1) / 3); + break; + + case DatePartEnum.Week: + datePartValue = 1 + ((date.DayOfYear - 1) / 7); + break; + + case DatePartEnum.DayOfYear: + datePartValue = date.DayOfYear; + break; + + case DatePartEnum.DayOfWeek: + datePartValue = (int)date.DayOfWeek; // TODO : respect current culture and DateFirst + break; + + case DatePartEnum.Hour: + datePartValue = date.Hour; + break; + + case DatePartEnum.Minute: + datePartValue = date.Minute; + break; + + case DatePartEnum.Second: + datePartValue = date.Second; + break; + + case DatePartEnum.Millisecond: + datePartValue = date.Millisecond; + break; + +#if NET8_0_OR_GREATER + case DatePartEnum.Microsecond: + datePartValue = date.Microsecond; + break; + + case DatePartEnum.Nanosecond: + datePartValue = date.Nanosecond; + break; +#endif + + default: return false; + } + + return true; + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DateTimeFromParts.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DateTimeFromParts.cs new file mode 100644 index 00000000..1f02b920 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/DateTimeFromParts.cs @@ -0,0 +1,15 @@ +using TeamTools.TSQL.ExpressionEvaluator.Routines; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.DateFunctions +{ + public class DateTimeFromParts : BaseDateTimeFromParts + { + private static readonly int RequiredArgumentCount = 7; + private static readonly string FuncName = "DATETIMEFROMPARTS"; + private static readonly string OutputType = TSqlDomainAttributes.Types.DateTime; + + public DateTimeFromParts() : base(FuncName, RequiredArgumentCount, OutputType, true, true) + { + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/EndOfMonth.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/EndOfMonth.cs new file mode 100644 index 00000000..1f3ecfe6 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/EndOfMonth.cs @@ -0,0 +1,61 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentDto; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentValidators; +using TeamTools.TSQL.ExpressionEvaluator.Routines; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.DateFunctions +{ + public class EndOfMonth : SqlGenericFunctionHandler + { + private static readonly int RequiredArgumentCount = 1; + private static readonly string FuncName = "EOMONTH"; + private static readonly string OutputType = TSqlDomainAttributes.Types.Date; + + public EndOfMonth() : base(FuncName, RequiredArgumentCount) + { + } + + public override bool ValidateArgumentValues(CallSignature call) + { + return ValidationScenario + .For("DATE_VALUE", call.RawArgs[0], call.Context) + .When(ArgumentIsValue.Validate) + .And(ArgumentIsValidDateTime.Validate) + .Then(d => call.ValidatedArgs.DateValue = d); + } + + protected override string DoEvaluateResultType(CallSignature call) => OutputType; + + protected override SqlValue DoEvaluateResultValue(CallSignature call) + { + var value = call.Context.Converter.ImplicitlyConvert(base.DoEvaluateResultValue(call)); + if (value is null || call.ValidatedArgs.DateValue?.IsPreciseValue != true) + { + return value; + } + + if (call.ValidatedArgs.DateValue.IsNull) + { + return call.ResultTypeHandler.ValueFactory.NewNull(call.Context.NewSource.Node); + } + + var newDate = GetLastDayOfMonth(call.ValidatedArgs.DateValue.Value); + + return value.ChangeTo(newDate, call.Context.NewSource); + } + + private static DateTime GetLastDayOfMonth(DateTime date) + { + return new DateTime(date.Year, date.Month, DateTime.DaysInMonth(date.Year, date.Month)); + } + + [ExcludeFromCodeCoverage] + public sealed class EndOfMonthArgs + { + public SqlDateTimeValue DateValue { get; set; } + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/GetDate.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/GetDate.cs new file mode 100644 index 00000000..11f0890f --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/GetDate.cs @@ -0,0 +1,20 @@ +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.Abstractions; +using TeamTools.TSQL.ExpressionEvaluator.Routines; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.DateFunctions +{ + public class GetDate : CurrentMomentFunction + { + private static readonly string FuncName = "GETDATE"; + private static readonly string OutputType = TSqlDomainAttributes.Types.DateTime; + + public GetDate() : base(FuncName, OutputType, TimeDetails.RegularDateTime, DateDetails.Full) + { + } + + protected override SqlDateTimeValue ApplyNewRange(SqlDateTimeValue value, SqlDateTimeValueRange newRange, SqlValueSource src) + => value.ChangeTo(newRange, src); + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/SmallDateTimeFromParts.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/SmallDateTimeFromParts.cs new file mode 100644 index 00000000..82bb70d7 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/SmallDateTimeFromParts.cs @@ -0,0 +1,15 @@ +using TeamTools.TSQL.ExpressionEvaluator.Routines; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.DateFunctions +{ + public class SmallDateTimeFromParts : BaseDateTimeFromParts + { + private static readonly int RequiredArgumentCount = 5; + private static readonly string FuncName = "SMALLDATETIMEFROMPARTS"; + private static readonly string OutputType = TSqlDomainAttributes.Types.SmallDateTime; + + public SmallDateTimeFromParts() : base(FuncName, RequiredArgumentCount, OutputType, false, true) + { + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/SpecificDatePartFunction.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/SpecificDatePartFunction.cs index 08459f1f..33457339 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/SpecificDatePartFunction.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/SpecificDatePartFunction.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentDto; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentValidators; using TeamTools.TSQL.ExpressionEvaluator.Routines; using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; using TeamTools.TSQL.ExpressionEvaluator.Values; @@ -19,8 +20,17 @@ protected SpecificDatePartFunction(string funcName, SqlIntValueRange valueRange, this.datePart = datePart; } - // TODO : validate date arg - public override bool ValidateArgumentValues(CallSignature call) => true; + public override bool ValidateArgumentValues(CallSignature call) + { + ValidationScenario + .For("DATE_VALUE", call.RawArgs[0], call.Context) + .When(ArgumentIsValue.Validate) + .And(ArgumentIsValidDateTime.Validate) + .Then(d => call.ValidatedArgs.DateValue = d); + + // we still can estimate result range no matter if the date source is unclear + return true; + } protected override string DoEvaluateResultType(CallSignature call) => OutputType; @@ -34,13 +44,22 @@ protected override SqlValue DoEvaluateResultValue(CallSignature ca return default; } + var dt = call.ValidatedArgs.DateValue; + if (dt?.IsPreciseValue == true + && DatePartExtractor.ExtractDatePartFromSpecificDate(dt.Value, datePart, out int datePartValue)) + { + // precise estimate + return value.ChangeTo(datePartValue, call.Context.NewSource); + } + + // Estimated result range based on provided DatePart value return value.ChangeTo(valueRange, call.Context.NewSource); } [ExcludeFromCodeCoverage] public class DatePartArgs { - public ValueArgument DateValue { get; set; } + public SqlDateTimeValue DateValue { get; set; } } } } diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/SysDateTime.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/SysDateTime.cs new file mode 100644 index 00000000..023be3d5 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/SysDateTime.cs @@ -0,0 +1,20 @@ +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.Abstractions; +using TeamTools.TSQL.ExpressionEvaluator.Routines; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.DateFunctions +{ + public class SysDateTime : CurrentMomentFunction + { + private static readonly string FuncName = "SYSDATETIME"; + private static readonly string OutputType = TSqlDomainAttributes.Types.DateTime2; + + public SysDateTime() : base(FuncName, OutputType, TimeDetails.Detailed, DateDetails.Full) + { + } + + protected override SqlDateTimeValue ApplyNewRange(SqlDateTimeValue value, SqlDateTimeValueRange newRange, SqlValueSource src) + => value.ChangeTo(newRange, src); + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/TimeFromParts.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/TimeFromParts.cs new file mode 100644 index 00000000..9937595d --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/DateFunctions/TimeFromParts.cs @@ -0,0 +1,15 @@ +using TeamTools.TSQL.ExpressionEvaluator.Routines; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.DateFunctions +{ + public class TimeFromParts : BaseDateTimeFromParts + { + private static readonly int RequiredArgumentCount = 5; + private static readonly string FuncName = "TIMEFROMPARTS"; + private static readonly string OutputType = TSqlDomainAttributes.Types.Time; + + public TimeFromParts() : base(FuncName, RequiredArgumentCount, OutputType, false, true) + { + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/MathFunctions/Ceiling.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/MathFunctions/Ceiling.cs new file mode 100644 index 00000000..5a6e965b --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/MathFunctions/Ceiling.cs @@ -0,0 +1,20 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.Abstractions; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.MathFunctions +{ + public class Ceiling : RoundingFunction + { + private static readonly string FuncName = "CEILING"; + + public Ceiling() : base(FuncName) + { } + + protected override decimal ProduceRounding(decimal value, CeilingArgs arguments) => Math.Ceiling(value); + + [ExcludeFromCodeCoverage] + public sealed class CeilingArgs : RoundingFunctionArgs + { } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/MathFunctions/Floor.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/MathFunctions/Floor.cs new file mode 100644 index 00000000..ce9e541d --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/MathFunctions/Floor.cs @@ -0,0 +1,20 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.Abstractions; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.MathFunctions +{ + public class Floor : RoundingFunction + { + private static readonly string FuncName = "FLOOR"; + + public Floor() : base(FuncName) + { } + + protected override decimal ProduceRounding(decimal value, FloorArgs arguments) => Math.Floor(value); + + [ExcludeFromCodeCoverage] + public sealed class FloorArgs : RoundingFunctionArgs + { } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/MathFunctions/Round.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/MathFunctions/Round.cs new file mode 100644 index 00000000..18fcdf3b --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/MathFunctions/Round.cs @@ -0,0 +1,107 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.Abstractions; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentDto; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentValidators; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.MathFunctions +{ + public class Round : RoundingFunction + { + private const int MaxRoundingDigit = 28; + + private static readonly string FuncName = "ROUND"; + private static readonly int MinArgumentCount = 2; + private static readonly int MaxArgumentCount = 3; + + public Round() : base(FuncName, MinArgumentCount, MaxArgumentCount) + { + } + + public override bool ValidateArgumentValues(CallSignature call) + { + return base.ValidateArgumentValues(call) + && ValidationScenario + .For("ROUND_LENGTH", call.RawArgs[1], call.Context) + .When(ArgumentIsValue.Validate) + .And(ArgumentIsValidInt.Validate) + .Then(n => call.ValidatedArgs.Length = n); + } + + protected override sealed SqlValue DoEvaluateResultValue(CallSignature call) + { + // For positive length the ancestor scenario is completely fine + if (call.ValidatedArgs.Length.IsPreciseValue && !call.ValidatedArgs.Length.IsNull + && call.ValidatedArgs.Length.Value >= 0) + { + return base.DoEvaluateResultValue(call); + } + + var value = call.Context.Converter.ImplicitlyConvert(call + .ResultTypeHandler + .MakeSqlDataTypeReference(call.ResultType) + .MakeUnknownValue()); + + // TODO : if Length has limited value range, e.g. 0-MaxInt then it is not negative for sure + // more accurate approximation can be made + if (value is null || !call.ValidatedArgs.SourceValue.IsPreciseValue || !call.ValidatedArgs.Length.IsPreciseValue) + { + return value; + } + + if (call.ValidatedArgs.SourceValue.IsNull || call.ValidatedArgs.Length.IsNull) + { + return call.ResultTypeHandler.ValueFactory.NewNull(call.Context.Node); + } + + if (Math.Abs(call.ValidatedArgs.Length.Value) > MaxRoundingDigit) + { + // .net supports rounding for 0-28 digits only + return value; + } + + decimal srcValue; + + // we already checked that SourceValue it is precise + if (call.ValidatedArgs.SourceValue is SqlDecimalTypeValue dec) + { + srcValue = dec.Value; + } + else if (call.ValidatedArgs.SourceValue is SqlIntTypeValue i) + { + srcValue = (decimal)i.Value; + } + else if (call.ValidatedArgs.SourceValue is SqlBigIntTypeValue b) + { + srcValue = (decimal)b.Value; + } + else + { + // TODO : return precise value if possible or a limited value range + return value; + } + + return value.ChangeTo(ProduceRounding(srcValue, call.ValidatedArgs), call.Context.NewSource); + } + + protected override decimal ProduceRounding(decimal value, RoundArgs arguments) + { + if (arguments.Length.Value >= 0) + { + return Math.Round(value, arguments.Length.Value); + } + + var pow = (decimal)Math.Pow(10, -arguments.Length.Value); + + return Math.Round(value / pow, 0) * pow; + } + + [ExcludeFromCodeCoverage] + public sealed class RoundArgs : RoundingFunctionArgs + { + public SqlIntTypeValue Length { get; set; } + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/StrFunctions/DataLength.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/StrFunctions/DataLength.cs index c7dde725..2cd9c89d 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/StrFunctions/DataLength.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/StrFunctions/DataLength.cs @@ -50,7 +50,7 @@ protected override SqlValue DoEvaluateResultValue(CallSignature } // TODO : get rid of magic cast - int bytes = (call.ValidatedArgs.Str.TypeReference as SqlStrTypeReference).Bytes; + int bytes = call.ValidatedArgs.Str.TypeReference.Bytes; if (call.ValidatedArgs.Str.IsPreciseValue) { diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/StrFunctions/Format.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/StrFunctions/Format.cs index 5fa9d6e8..f265af82 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/StrFunctions/Format.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/StrFunctions/Format.cs @@ -24,7 +24,7 @@ static Format() "c", "C", // currency "d", - "D", // digits / day + "D", // digits / date "e", "E", // exponential "f", @@ -32,7 +32,7 @@ static Format() "g", "G", // general "m", - "M", // month + "M", // month with day "n", "N", "o", @@ -47,7 +47,7 @@ static Format() "x", "X", // hexadecimal "y", - "Y", // year + "Y", // year with month }; } @@ -60,6 +60,7 @@ public override bool ValidateArgumentValues(CallSignature call) { // TODO : include into boolean expression after implementing // of any type value support + // TODO : first arg VARCHAR, VARBINARY are invalid (runtime error) ValidationScenario .For("VALUE", call.RawArgs[0], call.Context) .When(ArgumentIsValue.Validate) @@ -81,6 +82,8 @@ public override bool ValidateArgumentValues(CallSignature call) protected override string DoEvaluateResultType(CallSignature call) => ResultType; // TODO : support more scenarios + // TODO : if known format is incompatible for provided source value (e.g. number for date formats) + // then the result will always be NULL -> RedundantFunctionCall + precise NULL output protected override SqlValue DoEvaluateResultValue(CallSignature call) { if (call.ValidatedArgs.SourceValue is SqlStrTypeValue) @@ -113,6 +116,7 @@ protected override SqlValue DoEvaluateResultValue(CallSignature call if (call.ValidatedArgs.FormatString.EstimatedSize > 1) { // if there is a custom template then the output will always be of given template length + // all predefined formats contain 1 symbol only (see KnownFormats above) return call.ValidatedArgs.FormatString.ChangeTo(call.ValidatedArgs.FormatString.EstimatedSize, call.Context.NewSource); } @@ -132,6 +136,7 @@ protected override SqlValue DoEvaluateResultValue(CallSignature call } } + // TODO : Make more accurate precisions for DATE/TIME types var str = call.Context.Converter.ImplicitlyConvert(call.ValidatedArgs.SourceValue); if (str != null) { diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/StrFunctions/FormatMessage.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/StrFunctions/FormatMessage.cs index 96a5714b..106289f0 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/StrFunctions/FormatMessage.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/StrFunctions/FormatMessage.cs @@ -129,7 +129,7 @@ private static BuildResult BuildFormattedString(List groups, Li return default; } - if (arg.EstimatedSize >= MaxSupportedLength) + if (arg.EstimatedSize >= MaxSupportedLength || arg.EstimatedSize < 0) { varcharMax = true; break; diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/StrFunctions/HashBytes.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/StrFunctions/HashBytes.cs new file mode 100644 index 00000000..8465b63d --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/StrFunctions/HashBytes.cs @@ -0,0 +1,36 @@ +using System.Diagnostics.CodeAnalysis; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentDto; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentValidators; +using TeamTools.TSQL.ExpressionEvaluator.Routines; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.StrFunctions +{ + internal class HashBytes : SqlGenericFunctionHandler + { + private static readonly string FuncName = "HASHBYTES"; + private static readonly int RequiredArgumentCount = 2; + private static readonly string OutputType = TSqlDomainAttributes.Types.VarBinary; + + public HashBytes() : base(FuncName, RequiredArgumentCount) + { + } + + // TODO : verify that input is either string or binary data + public override bool ValidateArgumentValues(CallSignature call) + { + return ValidationScenario + .For("INPUT_VALUE", call.RawArgs[0], call.Context) + .When(ArgumentIsValue.Validate) + .Then(s => call.ValidatedArgs.Input = s); + } + + protected override string DoEvaluateResultType(CallSignature call) => OutputType; + + [ExcludeFromCodeCoverage] + public sealed class HashBytesArgs + { + public SqlValue Input { get; set; } + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/CursorRows.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/CursorRows.cs new file mode 100644 index 00000000..56eb4585 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/CursorRows.cs @@ -0,0 +1,16 @@ +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.Abstractions; +using TeamTools.TSQL.ExpressionEvaluator.Routines; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.SysFunctions +{ + public class CursorRows : GlobalVariableHandler + { + private static readonly string FuncName = "@@CURSOR_ROWS"; + private static readonly string ResultTypeName = TSqlDomainAttributes.Types.Int; + + public CursorRows() : base(FuncName, ResultTypeName) + { + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/CursorStatus.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/CursorStatus.cs new file mode 100644 index 00000000..27489034 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/CursorStatus.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentDto; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentValidators; +using TeamTools.TSQL.ExpressionEvaluator.Routines; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.SysFunctions +{ + public class CursorStatus : SqlGenericFunctionHandler + { + private static readonly string FuncName = "CURSOR_STATUS"; + private static readonly string ResultTypeName = TSqlDomainAttributes.Types.SmallInt; + private static readonly int RequiredArgCount = 2; + private static readonly SqlIntValueRange StatusValueRange = new SqlIntValueRange(-3, 1); + + private static readonly HashSet ValidScopes = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "local", + "global", + "variable", + }; + + public CursorStatus() : base(FuncName, RequiredArgCount) + { + } + + public override bool ValidateArgumentValues(CallSignature call) + { + return ValidationScenario + .For("CURSOR_SCOPE", call.RawArgs[0], call.Context) + .When(ArgumentIsValue.Validate) + .And(ArgumentIsValidStr.Validate) + .Then(s => call.ValidatedArgs.CursorScope = s) + && ValidationScenario + .For("CURSOR_NAME", call.RawArgs[1], call.Context) + .When(ArgumentIsValue.Validate) + .And(ArgumentIsValidStr.Validate) + .Then(s => call.ValidatedArgs.CursorName = s); + } + + protected override string DoEvaluateResultType(CallSignature call) => ResultTypeName; + + protected override SqlValue DoEvaluateResultValue(CallSignature call) + { + var res = call.Context.Converter.ImplicitlyConvert(base.DoEvaluateResultValue(call)) + .ChangeTo(StatusValueRange, call.Context.NewSource); + + if (call.ValidatedArgs.CursorScope.IsNull || call.ValidatedArgs.CursorName.IsNull) + { + // TODO : translate + call.Context.InvalidArgument(FuncName + " requires both args"); + return res; + } + + if (call.ValidatedArgs.CursorScope.IsPreciseValue) + { + var cursorScope = call.ValidatedArgs.CursorScope.Value; + + if (!ValidScopes.Contains(cursorScope)) + { + call.Context.InvalidArgument(cursorScope); + } + } + + // TODO : Check variable existence when cursorScope == 'variable' + // after implementing CURSOR variables support (registering variables of any type) in the evaluator + if (call.ValidatedArgs.CursorName.IsPreciseValue && string.IsNullOrWhiteSpace(call.ValidatedArgs.CursorName.Value)) + { + // TODO : translate + call.Context.InvalidArgument("cursor name must not be empty"); + } + + return res; + } + + [ExcludeFromCodeCoverage] + public sealed class CursorStatusArgs + { + public SqlStrTypeValue CursorScope { get; set; } + + public SqlStrTypeValue CursorName { get; set; } + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/Dbts.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/Dbts.cs new file mode 100644 index 00000000..f32238b6 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/Dbts.cs @@ -0,0 +1,15 @@ +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.Abstractions; +using TeamTools.TSQL.ExpressionEvaluator.Routines; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.SysFunctions +{ + internal class Dbts : GlobalVariableHandler + { + private static readonly string FuncName = "@@DBTS"; + private static readonly string ResultTypeName = TSqlDomainAttributes.Types.VarBinary; + + public Dbts() : base(FuncName, ResultTypeName) + { + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/MaxPrecision.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/MaxPrecision.cs new file mode 100644 index 00000000..7d6f4b6f --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/MaxPrecision.cs @@ -0,0 +1,17 @@ +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.Abstractions; +using TeamTools.TSQL.ExpressionEvaluator.Routines; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.SysFunctions +{ + public class MaxPrecision : LimitedIntResultFunctionHandler + { + private static readonly string FuncName = "@@MAX_PRECISION"; + private static readonly string ResultTypeName = TSqlDomainAttributes.Types.TinyInt; + private static readonly SqlIntValueRange MaxPrecisionRange = new SqlIntValueRange(0, 38); + + public MaxPrecision() : base(FuncName, MaxPrecisionRange, ResultTypeName) + { + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/MinActiveRowversion.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/MinActiveRowversion.cs new file mode 100644 index 00000000..fbf4ef7b --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/SysFunctions/MinActiveRowversion.cs @@ -0,0 +1,16 @@ +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.Abstractions; +using TeamTools.TSQL.ExpressionEvaluator.Routines; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.SysFunctions +{ + public class MinActiveRowversion : SqlZeroArgFunctionHandler + { + private static readonly string FuncName = "MIN_ACTIVE_ROWVERSION "; + private static readonly string ResultTypeName = TSqlDomainAttributes.Types.Binary; + + // TODO : set size = 8 + public MinActiveRowversion() : base(FuncName, ResultTypeName) + { + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/XQueryFunctions/XQueryValue.cs b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/XQueryFunctions/XQueryValue.cs new file mode 100644 index 00000000..9f5214fe --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/BuiltInFunctions/XQueryFunctions/XQueryValue.cs @@ -0,0 +1,112 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentDto; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentValidators; +using TeamTools.TSQL.ExpressionEvaluator.Interfaces; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.XQueryFunctions +{ + public class XQueryValue : SqlGenericFunctionHandler + { + private static readonly string FuncName = "XQuery.Value"; + private static readonly int RequiredArgCount = 2; + + private static readonly Regex TypeNameWithParams = new Regex( + @"(?[a-zA-Z]+)[\s(]+(?\d+)[\s,)]*(?\d+)?[\s,)]*$", + RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled); + + public XQueryValue() : base(FuncName, RequiredArgCount) + { + } + + public override bool ValidateArgumentValues(CallSignature call) + { + return ValidationScenario + .For("XPATH_SELECTOR", call.RawArgs[0], call.Context) + .When(ArgumentIsValue.Validate) + .Then(v => call.ValidatedArgs.Selector = v) + && ValidationScenario + .For("OUTPUT_TYPE", call.RawArgs[1], call.Context) + .When(ArgumentIsValue.Validate) + .And(ArgumentIsValidStr.Validate) + .Then(s => call.ValidatedArgs.OutputType = s); + } + + // Finally the XQuery Value() function call will be evaluated to an Unknown value of precise type. + protected override string DoEvaluateResultType(CallSignature call) + { + if (call.ValidatedArgs.OutputType?.IsPreciseValue != true + || call.ValidatedArgs.OutputType.IsNull) + { + return default; + } + + call.ValidatedArgs.OutputTypeRef = ResolveType(call.ValidatedArgs.OutputType.Value, call.Context.TypeResolver); + return call.ValidatedArgs.OutputTypeRef?.TypeName; + } + + protected override SqlValue DoEvaluateResultValue(CallSignature call) + { + if (call.ValidatedArgs.OutputTypeRef != null) + { + // Unkwnown value with respect to datatype parameters + return call.ResultTypeHandler.ValueFactory.NewValue(call.ValidatedArgs.OutputTypeRef, SqlValueKind.Unknown); + } + + return base.DoEvaluateResultValue(call); + } + + private static SqlTypeReference ResolveType(string typeDeclaration, ISqlTypeResolver typeResolver) + { + var typeRef = typeResolver.ResolveType(typeDeclaration); + if (typeRef != null) + { + // Exact match of supported type without parameters, e.g. 'INT' + return typeRef; + } + + return ResolveTypeWithParams(typeDeclaration, typeResolver); + } + + // Type with parameters, e.g. 'VARCHAR(100)', 'DECIMAL(20,10)' + private static SqlTypeReference ResolveTypeWithParams(string typeDeclaration, ISqlTypeResolver typeResolver) + { + // TODO : Parsing by ScriptDom is preferred. But where to get parser instance from? + var match = TypeNameWithParams.Match(typeDeclaration); + if (match is null) + { + // something unsupported + return default; + } + + var typeDefinition = new SqlDataTypeReference + { + Name = new SchemaObjectName(), + }; + + typeDefinition.Name.Identifiers.Add(new Identifier { Value = match.Groups["name"].Value }); + typeDefinition.Parameters.Add(new IntegerLiteral { Value = match.Groups["param1"].Value }); + if (match.Groups["param2"] != null) + { + typeDefinition.Parameters.Add(new IntegerLiteral { Value = match.Groups["param2"].Value }); + } + + // Saving resolved type reference for later use in value evaluation. + // Current method can return type name only. + return typeResolver.ResolveType(typeDefinition); + } + + [ExcludeFromCodeCoverage] + public sealed class XQueryValueArgs + { + public SqlValue Selector { get; set; } + + public SqlStrTypeValue OutputType { get; set; } + + public SqlTypeReference OutputTypeRef { get; set; } + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/Core/SqlExpressionEvaluator.cs b/TeamTools.TSQL.ExpressionEvaluator/Core/SqlExpressionEvaluator.cs index d2b6f8b9..9bfbe07e 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/Core/SqlExpressionEvaluator.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/Core/SqlExpressionEvaluator.cs @@ -111,6 +111,13 @@ public SqlValue EvaluateExpression(ScalarExpression expr) if (expr is FunctionCall fn) { + if (fn.CallTarget is ExpressionCallTarget excall && excall.Expression is ScalarExpression) + { + // CallTarget means `.Func()` style call. It is either XQuery or something unknown. + // Not a regular function call. + return this.EvaluateXQueryResult(fn.FunctionName.Value, this.ToArgs(fn.Parameters), fn); + } + return this.EvaluateFunctionResult(fn.FunctionName.Value, this.ToArgs(fn.Parameters), fn); } diff --git a/TeamTools.TSQL.ExpressionEvaluator/Evaluation/EvaluateBuiltInExpressionExtensions.cs b/TeamTools.TSQL.ExpressionEvaluator/Evaluation/EvaluateBuiltInExpressionExtensions.cs index 86b3a801..59e9a75d 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/Evaluation/EvaluateBuiltInExpressionExtensions.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/Evaluation/EvaluateBuiltInExpressionExtensions.cs @@ -47,7 +47,7 @@ public static SqlValue EvaluateBuiltInExpression(this SqlExpressionEvaluator eva { return eval.EvaluateFunctionResult( "CONVERT", - eval.ToArgs(eval.ToArg(cnv.Parameter), eval.ToArg(cnv.DataType)), + eval.ToArgs(eval.ToArg(cnv.Parameter), eval.ToArg(cnv.DataType), eval.ToArg(cnv.Style)), expr); } @@ -71,7 +71,7 @@ public static SqlValue EvaluateBuiltInExpression(this SqlExpressionEvaluator eva { return eval.EvaluateFunctionResult( "TRY_CONVERT", - eval.ToArgs(eval.ToArg(tcnv.Parameter), eval.ToArg(tcnv.DataType)), + eval.ToArgs(eval.ToArg(tcnv.Parameter), eval.ToArg(tcnv.DataType), eval.ToArg(tcnv.Style)), expr); } diff --git a/TeamTools.TSQL.ExpressionEvaluator/Evaluation/EvaluateXQueryResultExtensions.cs b/TeamTools.TSQL.ExpressionEvaluator/Evaluation/EvaluateXQueryResultExtensions.cs new file mode 100644 index 00000000..7cb20314 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/Evaluation/EvaluateXQueryResultExtensions.cs @@ -0,0 +1,21 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentDto; +using TeamTools.TSQL.ExpressionEvaluator.Core; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.Evaluation +{ + internal static class EvaluateXQueryResultExtensions + { + public static SqlValue EvaluateXQueryResult( + this SqlExpressionEvaluator eval, + string functionName, + List args, + TSqlFragment node) + { + return eval.EvaluateFunctionResult("XQuery." + functionName, args, node); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/Routines/TSQLDomainAttributes.cs b/TeamTools.TSQL.ExpressionEvaluator/Routines/TSQLDomainAttributes.cs index bb1549aa..71f57650 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/Routines/TSQLDomainAttributes.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/Routines/TSQLDomainAttributes.cs @@ -12,6 +12,8 @@ internal static class TSqlDomainAttributes public const string DefaultSchemaName = "dbo"; public const string DefaultSchemaPrefix = "dbo."; + public const int MaxDecimalPrecision = 38; + public static class TriggerSystemTables { public const string Inserted = "INSERTED"; diff --git a/TeamTools.TSQL.ExpressionEvaluator/ScalarExpressionEvaluator.cs b/TeamTools.TSQL.ExpressionEvaluator/ScalarExpressionEvaluator.cs index a79cb797..f9007d7d 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/ScalarExpressionEvaluator.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/ScalarExpressionEvaluator.cs @@ -39,6 +39,11 @@ public ScalarExpressionEvaluator(TSqlBatch batch) typeResolver.RegisterTypeHandler(new SqlStrTypeHandler(typeConverter, violations)); typeResolver.RegisterTypeHandler(new SqlIntTypeHandler(typeConverter, violations)); typeResolver.RegisterTypeHandler(new SqlBigIntTypeHandler(typeConverter, violations)); + typeResolver.RegisterTypeHandler(new SqlDateTypeHandler(typeConverter, violations)); + typeResolver.RegisterTypeHandler(new SqlTimeTypeHandler(typeConverter, violations)); + typeResolver.RegisterTypeHandler(new SqlDateTimeTypeHandler(typeConverter, violations)); + typeResolver.RegisterTypeHandler(new SqlBinaryTypeHandler(typeConverter, violations)); + typeResolver.RegisterTypeHandler(new SqlDecimalTypeHandler(typeConverter, violations)); varReg = new SqlVariableRegistry(typeConverter, violations); diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/BigInt/SqlBigIntTypeConverter.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/BigInt/SqlBigIntTypeConverter.cs index e285e990..96db2df6 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/BigInt/SqlBigIntTypeConverter.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/BigInt/SqlBigIntTypeConverter.cs @@ -109,7 +109,7 @@ private static SqlBigIntTypeValue DoConvertValueFrom( { if (!src.IsPreciseValue) { - return typeHandler.BigIntValueFactory.MakeApproximateValue(targetType.TypeName, bigintSrc.EstimatedSize, bigintSrc.Source); + return typeHandler.BigIntValueFactory.MakeApproximateValue(targetType.TypeName, bigintSrc.EstimatedSize, src.Source); } BigInteger intValue = bigintSrc.Value; @@ -126,6 +126,17 @@ private static SqlBigIntTypeValue DoConvertValueFrom( return typeHandler.BigIntValueFactory.MakePreciseValue(targetType.TypeName, intValue, src.Source); } + // converting from varbinary + if (src is SqlBinaryTypeValue binarySrc) + { + if (!binarySrc.IsPreciseValue) + { + return typeHandler.BigIntValueFactory.MakeUnknownValue(targetType.TypeName); + } + + return typeHandler.BigIntValueFactory.MakePreciseValue(targetType.TypeName, binarySrc.Value.AsNumber, src.Source); + } + // TODO : register violation about impossible conversion? return default; } diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Binary/HexValue.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Binary/HexValue.cs new file mode 100644 index 00000000..0ce91cbc --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Binary/HexValue.cs @@ -0,0 +1,185 @@ +using System; +using System.Globalization; +using System.Numerics; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + // Length limit should be managed by SqlBinaryTypeValue/Handler + public class HexValue : IComparable, IEquatable + { + private const string HexPrefix = "0x"; + private BigInteger asNumber; + private string asString; + + public HexValue(int value, int minBytes = 0) + { + MinBytes = minBytes; + AsNumber = value; + } + + public HexValue(BigInteger value, int minBytes = 0) + { + MinBytes = minBytes; + AsNumber = value; + } + + public HexValue(string value, int minBytes = 0) + { + MinBytes = minBytes; + AsString = value; + } + + public BigInteger AsNumber + { + get + { + return asNumber; + } + + set + { + asNumber = value; + + // Trimming because ToString sometimes prepends value with unexpected extra 0 + asString = value.ToString("X").TrimStart('0'); + + // Prepending result with zeroes if MinLength is defined and the result is shorter + if (MinLength > asString.Length) + { + // FIXME: In case of BINARY(MAX) a huge string will be generated. + // A different approach for such cases is preferred. + asString = asString.PadLeft(MinLength, '0'); + } + + if (asString.Length % 2 != 0) + { + // in SQL SERVER every byte is always represented by 2 symbols + asString = "0" + asString; + } + } + } + + public string AsString + { + get + { + return asString; + } + + set + { + if (value.StartsWith(HexPrefix, StringComparison.OrdinalIgnoreCase)) + { + if (value.Length > 2) + { + value = value.Substring(2); + } + else + { + // there is only 0x prefix, no hex numbers afterwards + AsNumber = 0; + return; + } + } + + // Leading zero to avoid -1 for 0xFF + if (!BigInteger.TryParse("0" + value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var val)) + { + asString = default; + asNumber = default; + } + else + { + // TODO : not sure if this is a correct behavior + // to keep leading zeroes if any + int providedBytes = (value.Length + 1) / 2; + if (MinBytes < providedBytes) + { + MinBytes = providedBytes; + } + + // It will set AsString as well. With expected formatting. + AsNumber = val; + } + } + } + + public int MinBytes { get; private set; } + + private int MinLength => MinBytes * 2; + + public static bool operator ==(HexValue left, HexValue right) + { + if (ReferenceEquals(left, null)) + { + return ReferenceEquals(right, null); + } + + return left.Equals(right); + } + + public static bool operator !=(HexValue left, HexValue right) + { + return !(left == right); + } + + public static bool operator <(HexValue left, HexValue right) + { + return ReferenceEquals(left, null) ? !ReferenceEquals(right, null) : left.CompareTo(right) < 0; + } + + public static bool operator <=(HexValue left, HexValue right) + { + return ReferenceEquals(left, null) || left.CompareTo(right) <= 0; + } + + public static bool operator >(HexValue left, HexValue right) + { + return !ReferenceEquals(left, null) && left.CompareTo(right) > 0; + } + + public static bool operator >=(HexValue left, HexValue right) + { + return ReferenceEquals(left, null) ? ReferenceEquals(right, null) : left.CompareTo(right) >= 0; + } + + public static HexValue operator +(HexValue left, HexValue right) + { + return new HexValue(left.AsString + right.AsString); + } + + public static bool TryConvert(string src, out HexValue hex) + { + hex = new HexValue(src); + return !string.IsNullOrEmpty(hex.AsString); + } + + public int CompareTo(HexValue other) => this.asNumber.CompareTo(other?.asNumber); + + public bool Equals(HexValue other) => this.asNumber.Equals(other?.asNumber); + + public override string ToString() => HexPrefix + AsString; + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (ReferenceEquals(obj, null)) + { + return false; + } + + if (obj is HexValue hex) + { + return this.Equals(hex); + } + + return false; + } + + public override int GetHashCode() => asNumber.GetHashCode(); + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Binary/SqlBinaryTypeConverter.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Binary/SqlBinaryTypeConverter.cs new file mode 100644 index 00000000..797dfd09 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Binary/SqlBinaryTypeConverter.cs @@ -0,0 +1,331 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using TeamTools.TSQL.ExpressionEvaluator.Values; +using TeamTools.TSQL.ExpressionEvaluator.Violations; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + // TODO : refactor first then cover with tests + // FIXME : fix Source for new values. maybe pass from outside + [ExcludeFromCodeCoverage] + public static class SqlBinaryTypeConverter + { + public static SqlBinaryTypeValue ConvertValueFrom(this SqlBinaryTypeHandler typeHandler, SqlValue src, string targetType) + { + int targetSize = typeHandler.BinaryValueFactory.GetDefaultTypeSize(targetType); + + if (src is SqlBinaryTypeValue binSrc) + { + if (string.Equals(binSrc.TypeName, targetType, StringComparison.OrdinalIgnoreCase)) + { + return binSrc; + } + + targetSize = binSrc.EstimatedSize; + } + + var targetTypeRef = typeHandler.BinaryValueFactory + .MakeSqlDataTypeReference(targetType, targetSize); + + return typeHandler.DoConvertValueFrom(src, targetTypeRef, false, false); + } + + public static SqlBinaryTypeValue ConvertValueFrom(this SqlBinaryTypeHandler typeHandler, SqlValue src, SqlBinaryTypeReference targetType, bool forceTargetType) + { + return typeHandler.DoConvertValueFrom(src, targetType, true, forceTargetType); + } + + private static SqlBinaryTypeValue DoConvertValueFrom( + this SqlBinaryTypeHandler typeHandler, + SqlValue src, + SqlBinaryTypeReference targetType, + bool strictTargetSize, + bool forceTargetType) + { + if (src is null) + { + return default; + } + + if (targetType is null) + { + return default; + } + + if (src.IsNull) + { + // TODO : to factory + // return typeHandler.BinaryValueFactory.MakeNullValue(targetType.TypeName); + return new SqlBinaryTypeValue( + typeHandler, + targetType, + SqlValueKind.Null, + src.Source); + } + + // converting from int + if (src is SqlIntTypeValue intSrc) + { + if (!intSrc.IsPreciseValue) + { + int maxLength = Math.Max(EvalNumberAsHexLength(intSrc.EstimatedSize.Low), EvalNumberAsHexLength(intSrc.EstimatedSize.High)); + + if (targetType.Size < maxLength) + { + // target type size is not enough to store source value + // FIXME : this is the very wrong Source provided here + typeHandler.Violations.RegisterViolation(new ImplicitTruncationViolation(targetType.Size, maxLength, null, src.Source)); + } + else if (targetType.Size > maxLength) + { + // source value cannot be that long + // TODO : register specific violation + } + + int targetSize = forceTargetType ? targetType.Size : maxLength; + + return typeHandler.BinaryValueFactory.MakeApproximateValue(targetType.TypeName, targetSize, src.Source); + } + + int minResultLength = 0; + if (strictTargetSize) + { + if (!targetType.HasFixedLength && targetType.Size > intSrc.TypeReference.Bytes) + { + minResultLength = intSrc.TypeReference.Bytes; + } + else + { + minResultLength = targetType.Size; + } + } + + var hexValue = new HexValue(intSrc.Value, minResultLength); + + // FIXME : magic + if (targetType.Size < 0) + { + // TODO : or unknown? + return typeHandler.BinaryValueFactory.MakePreciseValue(targetType.TypeName, hexValue, src.Source); + } + + var resultSrc = src.Source; + const int MaxPowerToCheck = 16; + if (strictTargetSize && targetType.Size <= MaxPowerToCheck && Math.Abs((double)intSrc.Value) > Math.Pow(16, targetType.Size)) + { + // TODO : this is not a _string_ truncation + typeHandler.Violations.RegisterViolation(new ImplicitTruncationViolation(targetType.Size, hexValue.AsString.Length, hexValue.AsString, src.Source)); + // truncate BINARY correctly + hexValue.AsString = hexValue.AsString.Substring(0, targetType.Size * 2); + resultSrc = new SqlValueSource(SqlValueSourceKind.Expression, src.Source.Node); + } + + return typeHandler.BinaryValueFactory.MakePreciseValue(targetType.TypeName, hexValue, resultSrc); + } + + // converting from bigint + if (src is SqlBigIntTypeValue bigintSrc) + { + if (!bigintSrc.IsPreciseValue) + { + int maxLength = Math.Max(EvalNumberAsHexLength(bigintSrc.EstimatedSize.Low), EvalNumberAsHexLength(bigintSrc.EstimatedSize.High)); + + if (targetType.Size < maxLength) + { + typeHandler.Violations.RegisterViolation(new ImplicitTruncationViolation(targetType.Size, maxLength, null, src.Source)); + } + + int targetSize = forceTargetType ? targetType.Size : maxLength; + + return typeHandler.BinaryValueFactory.MakeApproximateValue(targetType.TypeName, targetSize, src.Source); + } + + int minResultLength = 0; + if (strictTargetSize) + { + if (!targetType.HasFixedLength && targetType.Size > bigintSrc.TypeReference.Bytes) + { + minResultLength = bigintSrc.TypeReference.Bytes; + } + else + { + minResultLength = targetType.Size; + } + } + + var hexValue = new HexValue(bigintSrc.Value, minResultLength); + + // FIXME : magic + if (targetType.Size < 0) + { + // TODO : or unknown? + return typeHandler.BinaryValueFactory.MakePreciseValue(targetType.TypeName, hexValue, src.Source); + } + + var resultSrc = src.Source; + const int MaxPowerToCheck = 16; + if (strictTargetSize && targetType.Size <= MaxPowerToCheck && Math.Abs((double)bigintSrc.Value) > Math.Pow(16, targetType.Size)) + { + typeHandler.Violations.RegisterViolation(new ImplicitTruncationViolation(targetType.Size, hexValue.AsString.Length, hexValue.AsString, src.Source)); + // truncate BINARY correctly + hexValue.AsString = hexValue.AsString.Substring(0, targetType.Size * 2); + resultSrc = new SqlValueSource(SqlValueSourceKind.Expression, src.Source.Node); + } + + return typeHandler.BinaryValueFactory.MakePreciseValue(targetType.TypeName, hexValue, resultSrc); + } + + // TODO : see also binary conversion styles + // https://learn.microsoft.com/en-us/sql/t-sql/functions/cast-and-convert-transact-sql?view=sql-server-ver17&f1url=%3FappId%3DDev15IDEF1%26l%3DEN-US%26k%3Dk(convert_TSQL)%3Bk(sql13.swb.tsqlresults.f1)%3Bk(sql13.swb.tsqlquery.f1)%3Bk(MiscellaneousFilesProject)%3Bk(DevLang-TSQL)%26rd%3Dtrue#binary-styles + // converting from other string + if (src is SqlStrTypeValue strSrc) + { + int definedTargetSize = targetType.Size == 0 ? 1 : targetType.Size; + + if (!strSrc.IsPreciseValue) + { + // FIXME : in case of Implicit conversion it should be strSrc.EstimatedSize + // in case of Explicit convertion it should be targetType.Size + int targetSize = forceTargetType ? definedTargetSize : strSrc.EstimatedSize; + + if (definedTargetSize < strSrc.EstimatedSize) + { + // FIXME : in case of explicit convertion + // no ImplicitTruncation violation should be generated + if (strictTargetSize) + { + // FIXME : wrong source is provided here + // current node is expected, not the node where another + // variable was initialized + typeHandler.Violations.RegisterViolation(new ImplicitTruncationViolation(definedTargetSize, strSrc.EstimatedSize, null, src.Source)); + } + + targetSize = definedTargetSize; + } + + if (!targetType.IsUnicode && !strSrc.IsNull + && strSrc.TypeReference is SqlStrTypeReference strRef && strRef.IsUnicode) + { + typeHandler.Violations.RegisterViolation(new NationalSymbolLossViolation(strSrc.TypeName, src.Source)); + } + + return typeHandler.BinaryValueFactory.MakeApproximateValue(targetType.TypeName, targetSize, src.Source); + } + + string strValue = strSrc.Value; + var resultSrc = strSrc.Source; + + // TODO : if no style provided then binary result should be === source string as byte array + // not a valid hex string + if (!HexValue.TryConvert(strValue, out var hexValue)) + { + typeHandler.Violations.RegisterViolation(new UnableToConvertViolation("'" + strValue + "'", targetType.TypeName, src.Source)); + + return typeHandler.BinaryValueFactory.MakeUnknownValue(targetType.TypeName); + } + + // FIXME : magic for unpredictable size + if (targetType.Size < 0) + { + // TODO : or unknown? + return typeHandler.BinaryValueFactory.MakePreciseValue(targetType.TypeName, hexValue, src.Source); + } + + if (strictTargetSize && definedTargetSize < strValue.Length) + { + // FIXME : how to distinct implicit truncation from explicit one? + // in case of implicit a violation warning is needed. + typeHandler.Violations.RegisterViolation(new ImplicitTruncationViolation(definedTargetSize, strValue.Length, strValue, src.Source)); + + hexValue.AsString = hexValue.AsString.Substring(0, definedTargetSize); + resultSrc = new SqlValueSource(SqlValueSourceKind.Expression, src.Source.Node); + } + + return typeHandler.BinaryValueFactory.MakePreciseValue(targetType.TypeName, hexValue, resultSrc); + } + + // converting from another (var)binary value + if (src is SqlBinaryTypeValue bin) + { + int definedTargetSize = targetType.Size == 0 ? 1 : targetType.Size; + // In string representation each byte takes 2 symbols + int maxTargetLength = 2 * definedTargetSize; + + if (!bin.IsPreciseValue) + { + return typeHandler.BinaryValueFactory.MakeUnknownValue(targetType.TypeName); + } + + var strValue = bin.Value.AsString; + var srcByteLength = strValue.Length / 2; + var resultSrc = bin.Source; + var hexValue = new HexValue(bin.Value.AsNumber, srcByteLength); + + if (strictTargetSize && definedTargetSize < srcByteLength) + { + // FIXME : how to distinct implicit truncation from explicit one? + // in case of implicit a violation warning is needed. + typeHandler.Violations.RegisterViolation(new ImplicitTruncationViolation(definedTargetSize, srcByteLength, bin.Value.ToString(), src.Source)); + + hexValue = new HexValue(strValue.Substring(0, maxTargetLength), definedTargetSize); + resultSrc = new SqlValueSource(SqlValueSourceKind.Expression, src.Source.Node); + } + else if (strictTargetSize && targetType.HasFixedLength && maxTargetLength > strValue.Length) + { + // in this case zeroes must be not prepended to the beginning + // but appended to the end (just like spaces for fixed-length CHAR string) + hexValue.AsString = strValue.PadRight(maxTargetLength, '0'); + } + + return typeHandler.BinaryValueFactory.MakePreciseValue(targetType.TypeName, hexValue, src.Source); + } + + // TODO : support date/time types + return default; + } + + // TODO : check for negative values + private static int EvalNumberAsHexLength(int value) + { + if (value == int.MinValue || value == int.MaxValue) + { + // precomputed constant results + return 8; + } + + if (value >= 0 && value < 256) + { + // optimization for small numbers + return 2; + } + + int exp = (int)Math.Ceiling(Math.Log(Math.Abs(value), 16)); + if (exp % 2 > 0) + { + // In T-SQL (VAR)BINARY values always have even number of symbols + exp++; + } + + return exp; + } + + private static int EvalNumberAsHexLength(BigInteger value) + { + if (value >= int.MinValue && value <= int.MaxValue) + { + // implementation for int has some optimization + return EvalNumberAsHexLength((int)value); + } + + int exp = (int)Math.Ceiling(BigInteger.Log(BigInteger.Abs(value), 16)); + if (exp % 2 > 0) + { + // In T-SQL (VAR)BINARY values always have even number of symbols + exp++; + } + + return exp; + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Binary/SqlBinaryTypeHandler.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Binary/SqlBinaryTypeHandler.cs new file mode 100644 index 00000000..949fa807 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Binary/SqlBinaryTypeHandler.cs @@ -0,0 +1,103 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using TeamTools.TSQL.ExpressionEvaluator.Interfaces; +using TeamTools.TSQL.ExpressionEvaluator.Interfaces.OperatorHandlers; +using TeamTools.TSQL.ExpressionEvaluator.Routines; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + public class SqlBinaryTypeHandler : SqlGenericTypeHandler, + IPlusOperatorHandler + { + private readonly SqlBinaryTypeValueFactory typedValueFactory; + private readonly Func getDefaultSize; + private readonly Func getTotalSize; + private readonly Func getConcat; + private readonly Func makeApproximateSum; + + public SqlBinaryTypeHandler(ISqlTypeConverter typeConverter, IViolationRegistrar violations) + : this(new SqlBinaryTypeValueFactory(), typeConverter, violations) + { + } + + protected SqlBinaryTypeHandler(SqlBinaryTypeValueFactory valueFactory, ISqlTypeConverter typeConverter, IViolationRegistrar violations) + : base(valueFactory, typeConverter, violations) + { + valueFactory.TypeHandler = this; + typedValueFactory = valueFactory; + getDefaultSize = new Func(valueFactory.GetDefaultTypeSize); + getTotalSize = new Func((a, b) => a + b); + getConcat = new Func((a, b) => a + b); + + makeApproximateSum = new Func(CombineSize); + } + + public SqlBinaryTypeValueFactory BinaryValueFactory => typedValueFactory; + + public override ISqlValueFactory GetValueFactory() => typedValueFactory; + + public override int CombineSize(int a, int b) + { + // unknown string length + if (a < 0 || b < 0) + { + return -1; + } + + if (a == int.MaxValue || b == int.MaxValue) + { + // MAX cannot be greater than MAX + return int.MaxValue; + } + + return a + b; + } + + public SqlValue Sum(SqlValue augend, SqlValue addend) + { + return Compute( + augend, + addend, + getConcat, + makeApproximateSum); + } + + public override SqlTypeReference MakeSqlDataTypeReference(DataTypeReference dataType) + { + if (dataType?.Name is null) + { + return default; + } + + string typeName = dataType.GetFullName(); + int typeSize = GetTypeSize(dataType); + + if (typeSize <= 0) + { + // could not determine valid string size + return default; + } + + return BinaryValueFactory.MakeSqlDataTypeReference(typeName, typeSize); + } + + public override SqlTypeReference MakeSqlDataTypeReference(string typeName) + => BinaryValueFactory.MakeSqlDataTypeReference(typeName, BinaryValueFactory.GetDefaultTypeSize(typeName)); + + public override SqlValue ConvertFrom(SqlValue from, SqlTypeReference to, bool forceTargetType = false) + => to is SqlBinaryTypeReference strType ? ConvertFrom(from, strType, forceTargetType) : default; + + public SqlBinaryTypeValue ConvertFrom(SqlValue from, SqlBinaryTypeReference to, bool forceTargetType) + => this.ConvertValueFrom(from, to, forceTargetType); + + public override SqlValue ConvertFrom(SqlValue from, string to) + => IsTypeSupported(to) ? this.ConvertValueFrom(from, to) : default; + + protected override int GetTypeSize(DataTypeReference datatype) + { + // Binary is very similar to Char + return SqlStrTypeDefinitionParser.GetTypeSize(datatype, getDefaultSize); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Binary/SqlBinaryTypeReference.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Binary/SqlBinaryTypeReference.cs new file mode 100644 index 00000000..b0181139 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Binary/SqlBinaryTypeReference.cs @@ -0,0 +1,12 @@ +using TeamTools.TSQL.ExpressionEvaluator.Interfaces; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + public class SqlBinaryTypeReference : SqlStrTypeReference + { + public SqlBinaryTypeReference(string typeName, int size, ISqlValueFactory valueFactory) + : base(typeName, size, valueFactory) + { + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Binary/SqlBinaryTypeValue.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Binary/SqlBinaryTypeValue.cs new file mode 100644 index 00000000..e8d25101 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Binary/SqlBinaryTypeValue.cs @@ -0,0 +1,42 @@ +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + public class SqlBinaryTypeValue : SqlGenericValueWithHandler + { + public SqlBinaryTypeValue(SqlBinaryTypeHandler typeHandler, SqlBinaryTypeReference typeReference, SqlValueKind valueKind, SqlValueSource source) + : base(typeHandler, typeReference, valueKind, source) + { + } + + public SqlBinaryTypeValue(SqlBinaryTypeHandler typeHandler, SqlBinaryTypeReference typeReference, HexValue value, SqlValueSource source) + : base(typeHandler, typeReference, value, source) + { + } + + // For cloning + protected SqlBinaryTypeValue(SqlBinaryTypeValue src, HexValue value) : base(src, value) + { + } + + // For cloning + protected SqlBinaryTypeValue(SqlBinaryTypeValue src) : base(src) + { + } + + // TODO : truncate if newValue is too long + public SqlBinaryTypeValue ChangeTo(HexValue newValue, SqlValueSource source) => TypeHandler.ChangeValueTo(this, newValue, source); + + public SqlBinaryTypeValue ChangeTo(int newSize, SqlValueSource source) => TypeHandler.ChangeValueTo(this, newSize, source); + + public override SqlBinaryTypeValue DeepClone() + { + if (IsPreciseValue && !IsNull) + { + return new SqlBinaryTypeValue(this, new HexValue(Value.AsString, Value.MinBytes)); + } + + return new SqlBinaryTypeValue(this); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Binary/SqlBinaryTypeValueFactory.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Binary/SqlBinaryTypeValueFactory.cs new file mode 100644 index 00000000..762a6ec6 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Binary/SqlBinaryTypeValueFactory.cs @@ -0,0 +1,164 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.TSQL.ExpressionEvaluator.Interfaces; +using TeamTools.TSQL.ExpressionEvaluator.Routines; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + public class SqlBinaryTypeValueFactory : SqlGenericValueFactory, ISqlValueFactory + { + private static readonly string BinaryFallbackTypeName = TSqlDomainAttributes.Types.Binary; + private static readonly Dictionary DefaultTypeSizes = new Dictionary(StringComparer.OrdinalIgnoreCase); + private static readonly HashSet Types; + + static SqlBinaryTypeValueFactory() + { + DefaultTypeSizes.Add(TSqlDomainAttributes.Types.Binary, 1); + // FIXME : column or variable will be 1 symbol long + // only CAST/CONVERT to VARBINARY without length results with 30-symbol long value + DefaultTypeSizes.Add(TSqlDomainAttributes.Types.VarBinary, 30); + + Types = new HashSet(DefaultTypeSizes.Keys, StringComparer.OrdinalIgnoreCase); + } + + public SqlBinaryTypeValueFactory() : base(BinaryFallbackTypeName) + { + } + + // TODO : it should be readonly + public SqlBinaryTypeHandler TypeHandler { get; internal set; } + + public override SqlBinaryTypeValue MakePreciseValue(string typeName, HexValue value, SqlValueSource source) + { + if (!IsTypeSupported(typeName)) + { + // TODO : or throw? + return default; + } + + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + return new SqlBinaryTypeValue(TypeHandler, new SqlBinaryTypeReference(typeName, value.AsString.Length / 2, this), value, source); + } + + public override SqlBinaryTypeValue MakeApproximateValue(string typeName, int estimatedLength, SqlValueSource source) + { + if (!IsTypeSupported(typeName)) + { + // TODO : or throw? + return default; + } + + // TODO : value can be empty (LEN(@var) = 0) + // but minimun string storage size is 1 char + return new SqlBinaryTypeValue( + TypeHandler, + MakeSqlDataTypeReference(typeName, estimatedLength), + SqlValueKind.Unknown, + source); + } + + public override SqlBinaryTypeValue MakeNullValue(string typeName, SqlValueSource source) + { + if (!IsTypeSupported(typeName)) + { + // TODO : or throw? + return default; + } + + return new SqlBinaryTypeValue( + TypeHandler, + MakeSqlDataTypeReference(typeName, GetDefaultTypeSize(typeName)), + SqlValueKind.Null, + source); + } + + public override SqlBinaryTypeValue MakeUnknownValue(string typeName) => DoMakeValue(typeName, SqlValueKind.Unknown); + + public SqlValue NewValue(SqlTypeReference typeRef, SqlValueKind valueKind) + { + // TODO : refactor this magic transition + if (!(typeRef is SqlBinaryTypeReference binRef)) + { + return default; + } + + // TODO : pass source + return new SqlBinaryTypeValue( + TypeHandler, + binRef, + valueKind, + null); + } + + public SqlValue NewNull(TSqlFragment source) => MakeNull(source); + + public SqlValue NewLiteral(string typeName, string value, TSqlFragment source) + { + if (!IsTypeSupported(typeName)) + { + return default; + } + + if (string.IsNullOrEmpty(value)) + { + return new SqlBinaryTypeValue( + TypeHandler, + MakeSqlDataTypeReference(typeName, GetDefaultTypeSize(typeName)), + SqlValueKind.Unknown, + new SqlValueSource(SqlValueSourceKind.Literal, source)); + } + + // TODO : HexValue.TryParse + create unknown if false? + // value starts with 0x, division by 2 is truncated so this formula for fixed length in bytes is fine + return MakeLiteral(typeName, new HexValue(value, (value.Length - 1) / 2), source); + } + + public SqlBinaryTypeReference MakeSqlDataTypeReference(string typeName, int length) + { + if (!IsTypeSupported(typeName)) + { + return default; + } + + return new SqlBinaryTypeReference(typeName, length, this); + } + + public override int GetDefaultTypeSize(string typeName) + { + DefaultTypeSizes.TryGetValue(typeName, out var typeSize); + return typeSize; + } + + // TODO : provide source + protected SqlBinaryTypeValue DoMakeValue(string typeName, SqlValueKind valueKind, SqlValueSource source = null) + { + if (!IsTypeSupported(typeName)) + { + // TODO : or throw? + return default; + } + + // FIXME : get rid of this -1 magic + // if it is supposed to mean MAX - use static field instead of hardcoded value + int size = -1; + if (valueKind != SqlValueKind.Unknown) + { + size = GetDefaultTypeSize(typeName); + } + + return new SqlBinaryTypeValue( + TypeHandler, + MakeSqlDataTypeReference(typeName, size), + valueKind, + source); + } + + protected override ICollection GetSupportedTypes() => Types; + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Date/SqlDateOnlyValue.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Date/SqlDateOnlyValue.cs new file mode 100644 index 00000000..f761b957 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Date/SqlDateOnlyValue.cs @@ -0,0 +1,43 @@ +using System; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + // TODO : Switch to System.DateOnly after dropping netstandard2.0 support + public class SqlDateOnlyValue : SqlGenericValueWithHandler + { + public SqlDateOnlyValue(SqlDateTypeHandler typeHandler, SqlDateTimeTypeReference typeReference, SqlValueKind valueKind, SqlValueSource source) + : base(typeHandler, typeReference, valueKind, source) + { + } + + public SqlDateOnlyValue(SqlDateTypeHandler typeHandler, SqlDateTimeTypeReference typeReference, DateTime value, SqlValueSource source) + : base(typeHandler, typeReference, value, source) + { + } + + // For cloning + protected SqlDateOnlyValue(SqlDateOnlyValue src, DateTime value) : base(src, value) + { + } + + // For cloning + protected SqlDateOnlyValue(SqlDateOnlyValue src) : base(src) + { + } + + public override SqlDateOnlyValue DeepClone() + { + if (IsPreciseValue && !IsNull) + { + return new SqlDateOnlyValue(this, Value); + } + + return new SqlDateOnlyValue(this); + } + + public SqlDateOnlyValue ChangeTo(DateTime newValue, SqlValueSource source) => TypeHandler.ChangeValueTo(this, newValue, source); + + public SqlDateOnlyValue ChangeTo(SqlDateTimeValueRange newSize, SqlValueSource source) => TypeHandler.ChangeValueTo(this, newSize, source); + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Date/SqlDateTypeConverter.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Date/SqlDateTypeConverter.cs new file mode 100644 index 00000000..428b4390 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Date/SqlDateTypeConverter.cs @@ -0,0 +1,114 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + // TODO : refactor first then cover with tests + // FIXME : fix Source for new values. maybe pass from outside + [ExcludeFromCodeCoverage] + public static class SqlDateTypeConverter + { + public static SqlDateOnlyValue ConvertValueFrom(this SqlDateTypeHandler typeHandler, SqlValue src, string targetType) + { + if (src is null) + { + return default; + } + + if (src is SqlDateOnlyValue dateSrc + && string.Equals(dateSrc.TypeName, targetType, StringComparison.OrdinalIgnoreCase)) + { + return dateSrc; + } + + var targetTypeRef = typeHandler.DateValueFactory.MakeSqlDataTypeReference(targetType); + + return typeHandler.ConvertValueFrom(src, targetTypeRef, false); + } + + public static SqlDateOnlyValue ConvertValueFrom(this SqlDateTypeHandler typeHandler, SqlValue src, SqlDateTimeTypeReference targetType, bool forceTargetType) + { + return typeHandler.DoConvertValueFrom(src, targetType, true, forceTargetType); + } + + private static SqlDateOnlyValue DoConvertValueFrom( + this SqlDateTypeHandler typeHandler, + SqlValue src, + SqlDateTimeTypeReference targetType, + bool strictTypeSize, + bool forceTargetType) + { + if (src is null) + { + return default; + } + + if (src.IsNull) + { + return typeHandler.DateValueFactory.MakeNullValue(targetType.TypeName, src.Source); + } + + // converting from string + if (src is SqlStrTypeValue strSrc) + { + if (!strSrc.IsPreciseValue) + { + return typeHandler.DateValueFactory.MakeUnknownValue(targetType.TypeName); + } + + return (SqlDateOnlyValue)typeHandler.DateValueFactory.NewLiteral(targetType.TypeName, strSrc.Value, src.Source.Node); + } + + // converting from int + if (src is SqlIntTypeValue intSrc) + { + if (!intSrc.IsPreciseValue) + { + // TODO : use range if provided + return typeHandler.DateValueFactory.MakeUnknownValue(targetType.TypeName); + } + + return typeHandler.DateValueFactory.MakePreciseValue(targetType.TypeName, SqlDateTimeTypeReference.MinSqlValue.AddDays(intSrc.Value).Date, src.Source); + } + + // converting from bigint + if (src is SqlBigIntTypeValue bigintSrc) + { + if (!bigintSrc.IsPreciseValue || bigintSrc.Value > int.MaxValue || bigintSrc.Value < int.MinValue) + { + // TODO : use range if provided + return typeHandler.DateValueFactory.MakeUnknownValue(targetType.TypeName); + } + + return typeHandler.DateValueFactory.MakePreciseValue(targetType.TypeName, SqlDateTimeTypeReference.MinSqlValue.AddDays((int)bigintSrc.Value).Date, src.Source); + } + + // converting from datetime + if (src is SqlDateTimeValue datetimeSrc) + { + if (!datetimeSrc.IsPreciseValue) + { + // TODO : use range if provided + return typeHandler.DateValueFactory.MakeUnknownValue(targetType.TypeName); + } + + return typeHandler.DateValueFactory.MakePreciseValue(targetType.TypeName, datetimeSrc.Value.Date, src.Source); + } + + // converting from time + if (src is SqlTimeOnlyValue timeSrc) + { + if (!timeSrc.IsPreciseValue) + { + // TODO : use range if provided + return typeHandler.DateValueFactory.MakeUnknownValue(targetType.TypeName); + } + + return typeHandler.DateValueFactory.MakePreciseValue(targetType.TypeName, new DateTime(timeSrc.Value.Ticks).Date, src.Source); + } + + return default; + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Date/SqlDateTypeHandler.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Date/SqlDateTypeHandler.cs new file mode 100644 index 00000000..19a40b05 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Date/SqlDateTypeHandler.cs @@ -0,0 +1,39 @@ +using System; +using TeamTools.TSQL.ExpressionEvaluator.Interfaces; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + public sealed class SqlDateTypeHandler : SqlGenericDateTimeTypeHandler + { + private readonly SqlDateTypeValueFactory typedValueFactory; + + public SqlDateTypeHandler(ISqlTypeConverter typeConverter, IViolationRegistrar violations) + : this(new SqlDateTypeValueFactory(), typeConverter, violations) + { + } + + private SqlDateTypeHandler(SqlDateTypeValueFactory valueFactory, ISqlTypeConverter typeConverter, IViolationRegistrar violations) + : base(valueFactory, typeConverter, violations) + { + valueFactory.TypeHandler = this; + typedValueFactory = valueFactory; + } + + public SqlDateTypeValueFactory DateValueFactory => typedValueFactory; + + public override SqlValue ConvertFrom(SqlValue from, SqlTypeReference to, bool forceTargetType = false) + => to is SqlDateTimeTypeReference datetimeType ? ConvertFrom(from, datetimeType, forceTargetType) : default; + + public SqlDateOnlyValue ConvertFrom(SqlValue from, SqlDateTimeTypeReference to, bool forceTargetType) + => this.ConvertValueFrom(from, to, forceTargetType); + + public override SqlValue ConvertFrom(SqlValue from, string to) + => IsTypeSupported(to) ? this.ConvertValueFrom(from, to) : default; + + public override ISqlValueFactory GetValueFactory() => typedValueFactory; + + public override SqlTypeReference MakeSqlDataTypeReference(string typeName) + => typedValueFactory.MakeSqlDataTypeReference(typeName); + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Date/SqlDateTypeValueFactory.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Date/SqlDateTypeValueFactory.cs new file mode 100644 index 00000000..47839c97 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Date/SqlDateTypeValueFactory.cs @@ -0,0 +1,126 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using System.Globalization; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + public sealed class SqlDateTypeValueFactory : SqlGenericDateTimeTypeValueFactory + { + private static readonly HashSet DateTypes = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "DATE", + }; + + private static readonly DateTimeFormatInfo DateTimeSqlAnsiFormat; + + static SqlDateTypeValueFactory() + { + DateTimeSqlAnsiFormat = (DateTimeFormatInfo)CultureInfo.GetCultureInfo("en-us").DateTimeFormat.Clone(); + DateTimeSqlAnsiFormat.FullDateTimePattern = "yyyyMMdd'T'HH':'mm':'ss"; + DateTimeSqlAnsiFormat.LongDatePattern = "yyyy'-'MM'-'dd"; + DateTimeSqlAnsiFormat.ShortDatePattern = "yyyyMMdd"; + DateTimeSqlAnsiFormat.ShortTimePattern = "HH':'mm"; + DateTimeSqlAnsiFormat.LongTimePattern = "HH':'mm':'ss"; + } + + // TODO : it should be readonly + public SqlDateTypeHandler TypeHandler { get; internal set; } + + public override SqlDateOnlyValue MakeApproximateValue(string typeName, SqlDateTimeValueRange estimatedSize, SqlValueSource source) + { + if (!IsTypeSupported(typeName)) + { + return default; + } + + return new SqlDateOnlyValue( + TypeHandler, + new SqlDateTimeTypeReference(typeName, estimatedSize, this), + SqlValueKind.Unknown, + source); + } + + public override SqlDateOnlyValue MakePreciseValue(string typeName, DateTime value, SqlValueSource source) + { + if (!IsTypeSupported(typeName)) + { + return default; + } + + return new SqlDateOnlyValue( + TypeHandler, + new SqlDateTimeTypeReference(typeName, new SqlDateTimeValueRange(value), this), + value, + source); + } + + public override SqlDateOnlyValue MakeNullValue(string typeName, SqlValueSource source) + { + if (!IsTypeSupported(typeName)) + { + return default; + } + + return new SqlDateOnlyValue( + TypeHandler, + MakeSqlDataTypeReference(typeName), + SqlValueKind.Null, + source); + } + + public override SqlValue NewLiteral(string typeName, string value, TSqlFragment source) + { + if (!IsTypeSupported(typeName)) + { + return default; + } + + if (!DateTime.TryParse(value, DateTimeSqlAnsiFormat, DateTimeStyles.None, out var datetimeValue) + && !DateTime.TryParse(value, CultureInfo.CurrentCulture.DateTimeFormat, DateTimeStyles.None, out datetimeValue)) + { + return DoMakeValue( + typeName, + SqlValueKind.Unknown, + new SqlValueSource(SqlValueSourceKind.Literal, source)); + } + + return MakeLiteral(typeName, datetimeValue, source); + } + + public override SqlValue NewNull(TSqlFragment source) => MakeNull(source); + + public override SqlValue NewValue(SqlTypeReference typeRef, SqlValueKind valueKind) => DoMakeValue(typeRef.TypeName, valueKind); + + public override SqlDateOnlyValue MakeUnknownValue(string typeName) => DoMakeValue(typeName, SqlValueKind.Unknown); + + public SqlDateTimeTypeReference MakeSqlDataTypeReference(string typeName) + { + var typeSize = GetDefaultTypeSize(typeName); + + if (typeSize is null) + { + return default; + } + + return new SqlDateTimeTypeReference(typeName, typeSize, this); + } + + protected override ICollection GetSupportedTypes() => DateTypes; + + private SqlDateOnlyValue DoMakeValue(string typeName, SqlValueKind valueKind, SqlValueSource source = null) + { + if (!IsTypeSupported(typeName)) + { + return default; + } + + return new SqlDateOnlyValue( + TypeHandler, + MakeSqlDataTypeReference(typeName), + valueKind, + source); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/DateTime/SqlDateTimeTypeConverter.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/DateTime/SqlDateTimeTypeConverter.cs new file mode 100644 index 00000000..cee3d62e --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/DateTime/SqlDateTimeTypeConverter.cs @@ -0,0 +1,114 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + // TODO : refactor first then cover with tests + // FIXME : fix Source for new values. maybe pass from outside + [ExcludeFromCodeCoverage] + public static class SqlDateTimeTypeConverter + { + public static SqlDateTimeValue ConvertValueFrom(this SqlDateTimeTypeHandler typeHandler, SqlValue src, string targetType) + { + if (src is null) + { + return default; + } + + if (src is SqlDateTimeValue datetimeSrc + && string.Equals(datetimeSrc.TypeName, targetType, StringComparison.OrdinalIgnoreCase)) + { + return datetimeSrc; + } + + var targetTypeRef = typeHandler.DateTimeValueFactory.MakeSqlDataTypeReference(targetType); + + return typeHandler.ConvertValueFrom(src, targetTypeRef, false); + } + + public static SqlDateTimeValue ConvertValueFrom(this SqlDateTimeTypeHandler typeHandler, SqlValue src, SqlDateTimeTypeReference targetType, bool forceTargetType) + { + return typeHandler.DoConvertValueFrom(src, targetType, true, forceTargetType); + } + + private static SqlDateTimeValue DoConvertValueFrom( + this SqlDateTimeTypeHandler typeHandler, + SqlValue src, + SqlDateTimeTypeReference targetType, + bool strictTypeSize, + bool forceTargetType) + { + if (src is null) + { + return default; + } + + if (src.IsNull) + { + return typeHandler.DateTimeValueFactory.MakeNullValue(targetType.TypeName, src.Source); + } + + // converting from string + if (src is SqlStrTypeValue strSrc) + { + if (!strSrc.IsPreciseValue) + { + return typeHandler.DateTimeValueFactory.MakeUnknownValue(targetType.TypeName); + } + + return (SqlDateTimeValue)typeHandler.DateTimeValueFactory.NewLiteral(targetType.TypeName, strSrc.Value, src.Source.Node); + } + + // converting from int + if (src is SqlIntTypeValue intSrc) + { + if (!intSrc.IsPreciseValue) + { + // TODO : use range if provided + return typeHandler.DateTimeValueFactory.MakeUnknownValue(targetType.TypeName); + } + + return typeHandler.DateTimeValueFactory.MakePreciseValue(targetType.TypeName, SqlDateTimeTypeReference.MinSqlValue.AddDays(intSrc.Value).Date, src.Source); + } + + // converting from bigint + if (src is SqlBigIntTypeValue bigintSrc) + { + if (!bigintSrc.IsPreciseValue || bigintSrc.Value > int.MaxValue || bigintSrc.Value < int.MinValue) + { + // TODO : use range if provided + return typeHandler.DateTimeValueFactory.MakeUnknownValue(targetType.TypeName); + } + + return typeHandler.DateTimeValueFactory.MakePreciseValue(targetType.TypeName, SqlDateTimeTypeReference.MinSqlValue.AddDays((int)bigintSrc.Value).Date, src.Source); + } + + // converting from date + if (src is SqlDateOnlyValue dateSrc) + { + if (!dateSrc.IsPreciseValue) + { + // TODO : use range if provided + return typeHandler.DateTimeValueFactory.MakeUnknownValue(targetType.TypeName); + } + + return typeHandler.DateTimeValueFactory.MakePreciseValue(targetType.TypeName, dateSrc.Value, src.Source); + } + + // converting from time + if (src is SqlTimeOnlyValue timeSrc) + { + if (!timeSrc.IsPreciseValue) + { + // TODO : use range if provided + return typeHandler.DateTimeValueFactory.MakeUnknownValue(targetType.TypeName); + } + + return typeHandler.DateTimeValueFactory.MakePreciseValue(targetType.TypeName, new DateTime(timeSrc.Value.Ticks), src.Source); + } + + return default; + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/DateTime/SqlDateTimeTypeHandler.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/DateTime/SqlDateTimeTypeHandler.cs new file mode 100644 index 00000000..03c09e2f --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/DateTime/SqlDateTimeTypeHandler.cs @@ -0,0 +1,70 @@ +using System; +using TeamTools.TSQL.ExpressionEvaluator.Interfaces; +using TeamTools.TSQL.ExpressionEvaluator.Interfaces.OperatorHandlers; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + public sealed class SqlDateTimeTypeHandler : SqlGenericDateTimeTypeHandler, + IPlusOperatorHandler, IMinusOperatorHandler + { + private readonly SqlDateTimeTypeValueFactory typedValueFactory; + private readonly Func getSum; + private readonly Func getSubtract; + private readonly Func makeApproxSum; + + public SqlDateTimeTypeHandler(ISqlTypeConverter typeConverter, IViolationRegistrar violations) + : this(new SqlDateTimeTypeValueFactory(), typeConverter, violations) + { + } + + private SqlDateTimeTypeHandler(SqlDateTimeTypeValueFactory valueFactory, ISqlTypeConverter typeConverter, IViolationRegistrar violations) + : base(valueFactory, typeConverter, violations) + { + valueFactory.TypeHandler = this; + typedValueFactory = valueFactory; + + // FIXME : но если в конвертере целое число переводить в дату с прибавкой 1900-01-01, то здесь же нужно тогда это учитывать + getSum = new Func((a, b) => a.Add(new TimeSpan(b.Ticks - SqlDateTimeTypeReference.MinSqlValue.Ticks))); + getSubtract = new Func((a, b) => a.Subtract(new TimeSpan(b.Ticks - SqlDateTimeTypeReference.MinSqlValue.Ticks))); + // TODO : does this actually make sense? + makeApproxSum = new Func((sizeA, sizeB) => new SqlDateTimeValueRange( + sizeA.Low < sizeB.Low ? sizeA.Low : sizeB.Low, + sizeA.High > sizeB.High ? sizeA.High : sizeB.High)); + } + + public SqlDateTimeTypeValueFactory DateTimeValueFactory => typedValueFactory; + + public override ISqlValueFactory GetValueFactory() => typedValueFactory; + + public override SqlTypeReference MakeSqlDataTypeReference(string typeName) + => typedValueFactory.MakeSqlDataTypeReference(typeName); + + public override SqlValue ConvertFrom(SqlValue from, SqlTypeReference to, bool forceTargetType = false) + => to is SqlDateTimeTypeReference datetimeType ? ConvertFrom(from, datetimeType, forceTargetType) : default; + + public SqlDateTimeValue ConvertFrom(SqlValue from, SqlDateTimeTypeReference to, bool forceTargetType) + => this.ConvertValueFrom(from, to, forceTargetType); + + public override SqlValue ConvertFrom(SqlValue from, string to) + => IsTypeSupported(to) ? this.ConvertValueFrom(from, to) : default; + + public SqlValue Sum(SqlValue augend, SqlValue addend) + { + return Compute( + augend, + addend, + getSum, + makeApproxSum); + } + + public SqlValue Subtract(SqlValue minuend, SqlValue subtrahend) + { + return Compute( + minuend, + subtrahend, + getSubtract, + makeApproxSum); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/DateTime/SqlDateTimeTypeValueFactory.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/DateTime/SqlDateTimeTypeValueFactory.cs new file mode 100644 index 00000000..48a08f20 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/DateTime/SqlDateTimeTypeValueFactory.cs @@ -0,0 +1,132 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading; +using TeamTools.TSQL.ExpressionEvaluator.Routines; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + public class SqlDateTimeTypeValueFactory : SqlGenericDateTimeTypeValueFactory + { + private static readonly HashSet DateTimeTypes = new HashSet(StringComparer.OrdinalIgnoreCase) + { + TSqlDomainAttributes.Types.SmallDateTime, + TSqlDomainAttributes.Types.DateTime, + TSqlDomainAttributes.Types.DateTime2, + }; + + private static readonly string[] DateTimeFormats = new string[] + { + "yyyyMMdd'T'HH':'mm':'ss", + "yyyyMMdd HH':'mm':'ss", + "yyyyMMdd HH':'mm", + "yyyyMMdd", + "yyyy-MM-dd'T'HH':'mm':'ss", + "yyyy-MM-dd HH':'mm':'ss", + "yyyy-MM-dd HH':'mm", + "yyyy-MM-dd", + }; + + // TODO : or always en-us? + private readonly CultureInfo defaultCulture = Thread.CurrentThread.CurrentCulture; + + // TODO : it should be readonly + public SqlDateTimeTypeHandler TypeHandler { get; internal set; } + + public override SqlDateTimeValue MakeApproximateValue(string typeName, SqlDateTimeValueRange estimatedSize, SqlValueSource source) + { + if (!IsTypeSupported(typeName)) + { + return default; + } + + return new SqlDateTimeValue( + TypeHandler, + new SqlDateTimeTypeReference(typeName, estimatedSize, this), + SqlValueKind.Unknown, + source); + } + + public override SqlDateTimeValue MakePreciseValue(string typeName, DateTime value, SqlValueSource source) + { + if (!IsTypeSupported(typeName)) + { + return default; + } + + return new SqlDateTimeValue( + TypeHandler, + new SqlDateTimeTypeReference(typeName, new SqlDateTimeValueRange(value), this), + value, + source); + } + + public override SqlDateTimeValue MakeNullValue(string typeName, SqlValueSource source) + { + if (!IsTypeSupported(typeName)) + { + return default; + } + + return new SqlDateTimeValue( + TypeHandler, + MakeSqlDataTypeReference(typeName), + SqlValueKind.Null, + source); + } + + public override SqlValue NewLiteral(string typeName, string value, TSqlFragment source) + { + if (!IsTypeSupported(typeName)) + { + return default; + } + + if (!DateTime.TryParseExact(value, DateTimeFormats, defaultCulture, DateTimeStyles.None, out var datetimeValue)) + { + return DoMakeValue( + typeName, + SqlValueKind.Unknown, + new SqlValueSource(SqlValueSourceKind.Literal, source)); + } + + return MakeLiteral(typeName, datetimeValue, source); + } + + public override SqlValue NewNull(TSqlFragment source) => MakeNull(source); + + public override SqlValue NewValue(SqlTypeReference typeRef, SqlValueKind valueKind) => DoMakeValue(typeRef.TypeName, valueKind); + + public override SqlDateTimeValue MakeUnknownValue(string typeName) => DoMakeValue(typeName, SqlValueKind.Unknown); + + public SqlDateTimeTypeReference MakeSqlDataTypeReference(string typeName) + { + var typeSize = GetDefaultTypeSize(typeName); + + if (typeSize is null) + { + return default; + } + + return new SqlDateTimeTypeReference(typeName, typeSize, this); + } + + protected override ICollection GetSupportedTypes() => DateTimeTypes; + + private SqlDateTimeValue DoMakeValue(string typeName, SqlValueKind valueKind, SqlValueSource source = null) + { + if (!IsTypeSupported(typeName)) + { + return default; + } + + return new SqlDateTimeValue( + TypeHandler, + MakeSqlDataTypeReference(typeName), + valueKind, + source); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/DateTime/SqlDateTimeValue.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/DateTime/SqlDateTimeValue.cs new file mode 100644 index 00000000..44280d85 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/DateTime/SqlDateTimeValue.cs @@ -0,0 +1,42 @@ +using System; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + public class SqlDateTimeValue : SqlGenericValueWithHandler + { + public SqlDateTimeValue(SqlDateTimeTypeHandler typeHandler, SqlDateTimeTypeReference typeReference, SqlValueKind valueKind, SqlValueSource source) + : base(typeHandler, typeReference, valueKind, source) + { + } + + public SqlDateTimeValue(SqlDateTimeTypeHandler typeHandler, SqlDateTimeTypeReference typeReference, DateTime value, SqlValueSource source) + : base(typeHandler, typeReference, value, source) + { + } + + // For cloning + protected SqlDateTimeValue(SqlDateTimeValue src, DateTime value) : base(src, value) + { + } + + // For cloning + protected SqlDateTimeValue(SqlDateTimeValue src) : base(src) + { + } + + public override SqlDateTimeValue DeepClone() + { + if (IsPreciseValue && !IsNull) + { + return new SqlDateTimeValue(this, Value); + } + + return new SqlDateTimeValue(this); + } + + public SqlDateTimeValue ChangeTo(DateTime newValue, SqlValueSource source) => TypeHandler.ChangeValueTo(this, newValue, source); + + public SqlDateTimeValue ChangeTo(SqlDateTimeValueRange newSize, SqlValueSource source) => TypeHandler.ChangeValueTo(this, newSize, source); + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Decimal/SqlDecimalTypeConverter.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Decimal/SqlDecimalTypeConverter.cs new file mode 100644 index 00000000..00d3039a --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Decimal/SqlDecimalTypeConverter.cs @@ -0,0 +1,113 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + [ExcludeFromCodeCoverage] + public static class SqlDecimalTypeConverter + { + public static SqlDecimalTypeValue ConvertValueFrom(this SqlDecimalTypeHandler typeHandler, SqlValue src, string targetType) + { + if (src is null) + { + return default; + } + + if (src is SqlDecimalTypeValue decSrc + && string.Equals(decSrc.TypeName, targetType, StringComparison.OrdinalIgnoreCase)) + { + return decSrc; + } + + var targetTypeRef = typeHandler.DecimalValueFactory.MakeSqlDataTypeReference(targetType); + + return typeHandler.ConvertValueFrom(src, targetTypeRef, false); + } + + public static SqlDecimalTypeValue ConvertValueFrom(this SqlDecimalTypeHandler typeHandler, SqlValue src, SqlDecimalTypeReference targetType, bool forceTargetType) + { + return typeHandler.DoConvertValueFrom(src, targetType, true, forceTargetType); + } + + // TODO : implement strictTypeSize and forceTargetType support or get rid of them + private static SqlDecimalTypeValue DoConvertValueFrom( + this SqlDecimalTypeHandler typeHandler, + SqlValue src, + SqlDecimalTypeReference targetType, + bool strictTypeSize, + bool forceTargetType) + { + if (src is null) + { + return default; + } + + if (src.IsNull) + { + // TODO : not sure about passing src source through + return typeHandler.DecimalValueFactory.MakeNullValue(targetType.TypeName, src.Source); + } + + // converting from int + if (src is SqlIntTypeValue intSrc) + { + if (!intSrc.IsPreciseValue) + { + return typeHandler.DecimalValueFactory.MakeApproximateValue(targetType.TypeName, targetType.Size, src.Source); + } + + // TODO : detect Scale loss + // TODO : check precision and out of range error + return typeHandler.DecimalValueFactory.MakePreciseValue(targetType.TypeName, intSrc.Value, src.Source); + } + + // converting from bigint + if (src is SqlBigIntTypeValue bigintSrc) + { + if (!bigintSrc.IsPreciseValue) + { + return typeHandler.DecimalValueFactory.MakeApproximateValue(targetType.TypeName, targetType.Size, src.Source); + } + + // TODO : detect Scale loss + // TODO : check precision and out of range error + return typeHandler.DecimalValueFactory.MakePreciseValue(targetType.TypeName, (decimal)bigintSrc.Value, src.Source); + } + + // converting from another decimal + if (src is SqlDecimalTypeValue decSrc) + { + if (!decSrc.IsPreciseValue) + { + return typeHandler.DecimalValueFactory.MakeApproximateValue(targetType.TypeName, targetType.Size, src.Source); + } + + // TODO : detect actual Scale loss + decimal number = decSrc.Value; + if (targetType.Size.Scale < decSrc.EstimatedSize.Scale) + { + number = Math.Round(number, targetType.Size.Scale); + } + + // TODO : check precision and out of range error + return typeHandler.DecimalValueFactory.MakePreciseValue(targetType.TypeName, number, src.Source); + } + + // converting from another string + if (src is SqlStrTypeValue strSrc) + { + if (!strSrc.IsPreciseValue || !decimal.TryParse(strSrc.Value, NumberStyles.Number, CultureInfo.InvariantCulture, out decimal decValue)) + { + return typeHandler.DecimalValueFactory.MakeApproximateValue(targetType.TypeName, targetType.Size, src.Source); + } + + // TODO : check Precision and out of range error + return typeHandler.DecimalValueFactory.MakePreciseValue(targetType.TypeName, decValue, src.Source); + } + + return default; + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Decimal/SqlDecimalTypeHandler.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Decimal/SqlDecimalTypeHandler.cs new file mode 100644 index 00000000..e995a06d --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Decimal/SqlDecimalTypeHandler.cs @@ -0,0 +1,213 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using TeamTools.TSQL.ExpressionEvaluator.Interfaces; +using TeamTools.TSQL.ExpressionEvaluator.Interfaces.OperatorHandlers; +using TeamTools.TSQL.ExpressionEvaluator.Properties; +using TeamTools.TSQL.ExpressionEvaluator.Routines; +using TeamTools.TSQL.ExpressionEvaluator.Values; +using TeamTools.TSQL.ExpressionEvaluator.Violations; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + public class SqlDecimalTypeHandler : SqlGenericTypeHandler, + IPlusOperatorHandler, IMinusOperatorHandler, IMultiplyOperatorHandler, IDivideOperatorHandler, + IReverseValueSignHandler + { + private readonly SqlDecimalTypeValueFactory typedValueFactory; + private readonly Func getSum; + private readonly Func getMultiply; + private readonly Func getSubtract; + private readonly Func makeApproxSum; + + static SqlDecimalTypeHandler() + { + } + + public SqlDecimalTypeHandler(ISqlTypeConverter typeConverter, IViolationRegistrar violations) + : this(new SqlDecimalTypeValueFactory(), typeConverter, violations) + { + } + + protected SqlDecimalTypeHandler(SqlDecimalTypeValueFactory valueFactory, ISqlTypeConverter typeConverter, IViolationRegistrar violations) + : base(valueFactory, typeConverter, violations) + { + valueFactory.TypeHandler = this; + typedValueFactory = valueFactory; + + getSum = new Func((a, b) => a + b); + getMultiply = new Func((a, b) => a * b); + getSubtract = new Func((a, b) => a - b); + + // TODO : move Precision and Scale calculations to the range class implementation? + // FIXME : prevent range bounds from falling out of valid Int range + // FIXME : take max precision and scale + makeApproxSum = new Func((sizeA, sizeB) => new SqlDecimalValueRange( + sizeA.Low == decimal.MinValue || sizeB.Low == decimal.MinValue ? decimal.MinValue : sizeA.Low + sizeB.Low, + sizeA.High == decimal.MaxValue || sizeB.High == decimal.MaxValue ? decimal.MaxValue : sizeA.High + sizeB.High, + Math.Max(sizeA.Scale, sizeB.Scale) + Math.Max(sizeA.Precision - sizeA.Scale, sizeB.Precision - sizeB.Scale) + 1, + Math.Max(sizeA.Scale, sizeB.Scale))); + } + + public SqlDecimalTypeValueFactory DecimalValueFactory => typedValueFactory; + + public override ISqlValueFactory GetValueFactory() => typedValueFactory; + + public override SqlDecimalValueRange CombineSize(SqlDecimalValueRange a, SqlDecimalValueRange b) + { + return new SqlDecimalValueRange( + Math.Min(a.Low, b.Low), + Math.Max(a.High, b.High), + Math.Max(a.Precision, b.Precision), + Math.Max(a.Scale, b.Scale)); + } + + public SqlValue Sum(SqlValue augend, SqlValue addend) + { + return Compute( + augend, + addend, + getSum, + makeApproxSum); + } + + // TODO : register redundant minus 0 + public SqlValue Subtract(SqlValue minuend, SqlValue subtrahend) + { + return Compute( + minuend, + subtrahend, + getSubtract, + // FIXME : prevent range bounds from falling out of valid Decimal range + (sizeA, sizeB) => new SqlDecimalValueRange( + sizeA.Low == decimal.MinValue || sizeB.High == decimal.MaxValue ? decimal.MinValue : sizeA.Low - sizeB.High, + sizeA.High == decimal.MaxValue || sizeB.Low == decimal.MinValue ? decimal.MaxValue : sizeA.High - sizeB.Low, + Math.Max(sizeA.Scale, sizeB.Scale) + Math.Max(sizeA.Precision - sizeA.Scale, sizeB.Precision - sizeB.Scale) + 1, + Math.Max(sizeA.Scale, sizeB.Scale))); + } + + // TODO : register redundant multiply by 1 + public SqlValue Multiply(SqlValue multiplicand, SqlValue multiplier) + { + return Compute( + multiplicand, + multiplier, + getMultiply, + (sizeA, sizeB) => new SqlDecimalValueRange( + sizeA.Low == decimal.MinValue || sizeB.Low == decimal.MinValue ? decimal.MinValue : sizeA.Low + sizeB.Low, + sizeA.High == decimal.MaxValue || sizeB.High == decimal.MaxValue ? decimal.MaxValue : sizeA.High + sizeB.High, + sizeA.Precision + sizeB.Precision + 1, + sizeA.Scale + sizeB.Scale)); + } + + // TODO : respect expression resulting type. the result must still fit the defined scale + // TODO : register redundant divide by 1 + public SqlValue Divide(SqlValue dividend, SqlValue divisor) + { + return Compute( + dividend, + divisor, + (a, b) => + { + if (b == 0) + { + // Expressions like 1/0 are sometimes used to raise error intentionally + if (dividend.SourceKind == SqlValueSourceKind.Literal && divisor.SourceKind == SqlValueSourceKind.Literal + && (a == 0 || a == 1)) + { + Violations.RegisterViolation(new IntentionalExceptionViolation(Strings.ViolationDetails_IntentionalException_DivisionByZero, divisor.Source)); + } + else + { + Violations.RegisterViolation(new DivideByZeroViolation(divisor.Source)); + } + + return 0; + } + + return a / b; + }, + (sizeA, sizeB) => new SqlDecimalValueRange( + sizeA.Low == decimal.MinValue || sizeB.High == decimal.MaxValue ? decimal.MinValue : sizeA.Low - sizeB.High, + sizeA.High == decimal.MaxValue || sizeB.Low == decimal.MinValue ? decimal.MaxValue : sizeA.High - sizeB.Low, + Math.Max(6, sizeA.Scale + sizeB.Precision + 1) + sizeA.Precision - sizeA.Scale + sizeB.Scale, + Math.Max(6, sizeA.Scale + sizeB.Precision + 1))); + } + + public override SqlTypeReference MakeSqlDataTypeReference(string typeName) + => DecimalValueFactory.MakeSqlDataTypeReference(typeName); + + public SqlTypeReference MakeSqlDataTypeReference(string typeName, int precision, int scale) + => DecimalValueFactory.MakeSqlDataTypeReference(typeName, precision, scale); + + public override SqlValue ConvertFrom(SqlValue from, SqlTypeReference to, bool forceTargetType = false) + => to is SqlDecimalTypeReference intType ? ConvertFrom(from, intType, forceTargetType) : default; + + public SqlDecimalTypeValue ConvertFrom(SqlValue from, SqlDecimalTypeReference to, bool forceTargetType) + => this.ConvertValueFrom(from, to, forceTargetType); + + public override SqlValue ConvertFrom(SqlValue from, string to) + => IsTypeSupported(to) ? this.ConvertValueFrom(from, to) : default; + + public SqlValue ReverseSign(SqlValue value) + => value is SqlDecimalTypeValue decValue ? RevertValueSign(decValue) : default; + + public override SqlTypeReference MakeSqlDataTypeReference(DataTypeReference dataType) + { + if (dataType?.Name is null) + { + return default; + } + + string typeName = dataType.GetFullName(); + + if (dataType is SqlDataTypeReference sdt && sdt.Parameters.Count == 2 + && int.TryParse(sdt.Parameters[0].Value, out int precision) + && int.TryParse(sdt.Parameters[1].Value, out int scale) + && precision >= 0 && precision <= TSqlDomainAttributes.MaxDecimalPrecision + && scale >= 0 && scale <= precision) + { + return DecimalValueFactory.MakeSqlDataTypeReference(typeName, precision, scale); + } + + // making with default precision and scale + return DecimalValueFactory.MakeSqlDataTypeReference(typeName); + } + + protected override SqlDecimalValueRange DoMergeTwoEstimatedSizes(SqlDecimalValueRange a, SqlDecimalValueRange b) + { + // TODO : verify that 0 <= Scale <= Precision and result size is <= 38 + return new SqlDecimalValueRange( + Math.Min(a.Low, b.Low), + Math.Max(a.High, b.High), + Math.Max(a.Precision, b.Precision), + Math.Max(a.Scale, b.Scale)); + } + + private SqlDecimalTypeValue RevertValueSign(SqlDecimalTypeValue value) + { + if (value is null) + { + return default; + } + + if (value.IsNull) + { + return value; + } + + if (value.IsPreciseValue && value.Value == 0) + { + return value; + } + + string resultTypeName = value.TypeName; + + if (value.IsPreciseValue) + { + return ValueFactory.MakePreciseValue(resultTypeName, -value.Value, value.Source); + } + + return ValueFactory.MakeApproximateValue(resultTypeName, SqlDecimalValueRange.RevertRange(value.EstimatedSize), value.Source); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Decimal/SqlDecimalTypeReference.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Decimal/SqlDecimalTypeReference.cs new file mode 100644 index 00000000..ce16d14c --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Decimal/SqlDecimalTypeReference.cs @@ -0,0 +1,45 @@ +using TeamTools.TSQL.ExpressionEvaluator.Interfaces; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + public class SqlDecimalTypeReference : SqlGenericTypeReference + { + public SqlDecimalTypeReference(string typeName, SqlDecimalValueRange size, ISqlValueFactory valueFactory) + : base(typeName, size, valueFactory) + { + } + + public override bool Equals(SqlTypeReference other) + { + return base.Equals(other) + && other is SqlDecimalTypeReference decRef + && decRef.Size.Equals(Size); + } + + public override string ToString() + { + return $"{TypeName}({Size.Precision}, {Size.Scale})"; + } + + // docs: https://learn.microsoft.com/en-us/sql/t-sql/data-types/decimal-and-numeric-transact-sql?view=sql-server-ver17 + protected override int GetBytes() + { + if (Size.Precision >= 29) + { + return 17; + } + + if (Size.Precision >= 20) + { + return 13; + } + + if (Size.Precision >= 10) + { + return 9; + } + + return 5; + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Decimal/SqlDecimalTypeValue.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Decimal/SqlDecimalTypeValue.cs new file mode 100644 index 00000000..de0c9ef3 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Decimal/SqlDecimalTypeValue.cs @@ -0,0 +1,43 @@ +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + public class SqlDecimalTypeValue : SqlGenericValueWithHandler + { + public SqlDecimalTypeValue(SqlDecimalTypeHandler typeHandler, SqlDecimalTypeReference typeReference, SqlValueKind valueKind, SqlValueSource source) + : base(typeHandler, typeReference, valueKind, source) + { + } + + public SqlDecimalTypeValue(SqlDecimalTypeHandler typeHandler, SqlDecimalTypeReference typeReference, decimal value, SqlValueSource source) + : base(typeHandler, typeReference, value, source) + { + } + + // For cloning + protected SqlDecimalTypeValue(SqlDecimalTypeValue src, decimal value) : base(src, value) + { + } + + // For cloning + protected SqlDecimalTypeValue(SqlDecimalTypeValue src) : base(src) + { + } + + public SqlDecimalTypeValue ChangeTo(decimal newValue, SqlValueSource source) + => TypeHandler.ChangeValueTo(this, newValue, source); + + public SqlDecimalTypeValue ChangeTo(SqlDecimalValueRange newSize, SqlValueSource source) + => TypeHandler.ChangeValueTo(this, newSize, source); + + public override SqlDecimalTypeValue DeepClone() + { + if (IsPreciseValue && !IsNull) + { + return new SqlDecimalTypeValue(this, Value); + } + + return new SqlDecimalTypeValue(this); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Decimal/SqlDecimalTypeValueFactory.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Decimal/SqlDecimalTypeValueFactory.cs new file mode 100644 index 00000000..e81aa97d --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Decimal/SqlDecimalTypeValueFactory.cs @@ -0,0 +1,157 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using System.Globalization; +using TeamTools.TSQL.ExpressionEvaluator.Interfaces; +using TeamTools.TSQL.ExpressionEvaluator.Routines; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + public class SqlDecimalTypeValueFactory : SqlGenericValueFactory, ISqlValueFactory + { + private static readonly SqlDecimalValueRange DefaultValueRange = new SqlDecimalValueRange(decimal.MinValue, decimal.MaxValue, 18, 0); + + private static readonly HashSet Types = new HashSet(StringComparer.OrdinalIgnoreCase) + { + TSqlDomainAttributes.Types.Decimal, + "NUMERIC", + }; + + public SqlDecimalTypeValueFactory() : base(TSqlDomainAttributes.Types.Decimal) + { + } + + // TODO : it should be readonly + public SqlDecimalTypeHandler TypeHandler { get; internal set; } + + public override SqlDecimalTypeValue MakePreciseValue(string typeName, decimal value, SqlValueSource source) + { + if (!IsTypeSupported(typeName)) + { + return default; + } + + // Extracting precision and scale from provided value so the value can be represented in terms of SQL SERVER + int scale = GetDecimalPlaces(value); + int precision = GetPrecision(value, scale); + + return new SqlDecimalTypeValue( + TypeHandler, + new SqlDecimalTypeReference(typeName, new SqlDecimalValueRange(value, value, precision, scale), this), + value, + source); + } + + public override SqlDecimalTypeValue MakeApproximateValue(string typeName, SqlDecimalValueRange estimatedSize, SqlValueSource source) + { + if (!IsTypeSupported(typeName)) + { + return default; + } + + return new SqlDecimalTypeValue( + TypeHandler, + new SqlDecimalTypeReference(typeName, estimatedSize, this), + SqlValueKind.Unknown, + source); + } + + public override SqlDecimalTypeValue MakeNullValue(string typeName, SqlValueSource source) + { + if (!IsTypeSupported(typeName)) + { + return default; + } + + return new SqlDecimalTypeValue( + TypeHandler, + MakeSqlDataTypeReference(typeName), + SqlValueKind.Null, + source); + } + + public override SqlDecimalTypeValue MakeUnknownValue(string typeName) => DoMakeValue(typeName, SqlValueKind.Unknown); + + // FIXME : low/high limit gets lost here if provided + public SqlValue NewValue(SqlTypeReference typeRef, SqlValueKind valueKind) => DoMakeValue(typeRef.TypeName, valueKind); + + public SqlValue NewNull(TSqlFragment source) => MakeNull(source); + + public SqlValue NewLiteral(string typeName, string value, TSqlFragment source) + { + if (!IsTypeSupported(typeName)) + { + return default; + } + + if (!decimal.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out decimal decValue)) + { + return DoMakeValue( + typeName, + SqlValueKind.Unknown, + new SqlValueSource(SqlValueSourceKind.Literal, source)); + } + + return MakeLiteral(typeName, decValue, source); + } + + // DECIMAL(18,0) is the default option set + public SqlDecimalTypeReference MakeSqlDataTypeReference(string typeName, int precision = 18, int scale = 0) + { + if (!IsTypeSupported(typeName)) + { + return default; + } + + return new SqlDecimalTypeReference(typeName, new SqlDecimalValueRange(decimal.MinValue, decimal.MaxValue, precision, scale), this); + } + + public override SqlDecimalValueRange GetDefaultTypeSize(string typeName) => DefaultValueRange; + + // TODO : provide source + protected SqlDecimalTypeValue DoMakeValue(string typeName, SqlValueKind valueKind, SqlValueSource source = null) + { + if (!IsTypeSupported(typeName)) + { + return default; + } + + return new SqlDecimalTypeValue( + TypeHandler, + MakeSqlDataTypeReference(typeName), + valueKind, + source); + } + + protected override ICollection GetSupportedTypes() => Types; + + /// + /// Gets the number of digits after the decimal point in a decimal value. + /// + /// The decimal value. + /// The number of digits after the decimal point. + private static int GetDecimalPlaces(decimal value) + { + // Get the internal representation of the decimal value. + // The fourth element of the array contains the scale factor. + int[] bits = decimal.GetBits(value); + + // The scale factor is stored in bits 16-23 of the fourth element. + // We shift the bits to the right by 16 and then mask with 0xFF (255) + // to get the scale factor. + int scale = (bits[3] >> 16) & 0xFF; + + return scale; + } + + private static int GetPrecision(decimal value, int scale) + { + // +1 for value < 10 + unchecked + { + return (int)Math.Floor(Math.Log10(Math.Abs((double)value))) + 1 + scale; + } + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Decimal/SqlDecimalValueRange.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Decimal/SqlDecimalValueRange.cs new file mode 100644 index 00000000..1ead15a9 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Decimal/SqlDecimalValueRange.cs @@ -0,0 +1,50 @@ +using TeamTools.TSQL.ExpressionEvaluator.Routines; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling.GenericNumber; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + public class SqlDecimalValueRange : SqlGenericNumberValueRange + { + public SqlDecimalValueRange(decimal low, decimal high, int precision, int scale) : base(low, high) + { + Precision = precision; + Scale = scale; + + if (Precision > TSqlDomainAttributes.MaxDecimalPrecision) + { + int delta = Precision - TSqlDomainAttributes.MaxDecimalPrecision; + Precision = TSqlDomainAttributes.MaxDecimalPrecision; + Scale -= delta; + } + + if (Scale < 0) + { + Scale = 0; + } + else if (Scale > Precision) + { + // TODO : not sure what for -1 + Scale = Precision - 1; + } + } + + public int Precision { get; set; } + + public int Scale { get; set; } + + public static SqlDecimalValueRange RevertRange(SqlDecimalValueRange range) + { + return new SqlDecimalValueRange(-range.High, -range.Low, range.Precision, range.Scale); + } + + // TODO : check match for scale and precision + public bool IsValueWithin(decimal value) => value >= Low && value <= High; + + public virtual bool Equals(SqlDecimalValueRange other) + { + return base.Equals(other) + && Precision.Equals(other.Precision) + && Scale.Equals(other.Scale); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericDateTime/DateTimeEnums.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericDateTime/DateTimeEnums.cs new file mode 100644 index 00000000..6df8f1b8 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericDateTime/DateTimeEnums.cs @@ -0,0 +1,105 @@ +using System; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + public enum DateTimeRangeKind + { + /// + /// Unknown + /// + Unknown = 0, + + /// + /// Precise date and/or time value + /// + Precise, + + /// + /// CurrentMoment - offset + /// + Past, + + /// + /// Relative date or time: current date / time + /// + CurrentMoment, + + /// + /// CurrentMoment + offset + /// + Future, + } + + [Flags] + public enum TimeDetails + { + /// + /// No time info (date only) + /// + None = 0, + + /// + /// Contains hours + /// + Hours = 1, + + /// + /// Contains minutes + /// + Minutes = 2, + + /// + /// Contains seconds + /// + Seconds = 4, + + /// + /// Contains milliseconds + /// + Milliseconds = 8, + + /// + /// Contains microseconds + /// + Microseconds = 16, + + /// + /// HH MM SS MS + /// + RegularDateTime = Hours | Minutes | Seconds | Milliseconds, + + /// + /// HH MM SS MS NS + /// + Detailed = Hours | Minutes | Seconds | Milliseconds | Microseconds, + + /// + /// HH MM + /// + DayTime = Hours | Minutes, + + /// + /// No time info (for date-only values) + /// + DateOnly = None, + } + + // TODO : shouldn't it be buils as Flags similarly to TimeDetails? + public enum DateDetails + { + /// + /// No date info (time only) + /// + None = 0, + + /// + /// Small date info with limited range + /// + Small, + + /// + /// Full date info + /// + Full, + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericDateTime/SqlDateTimeRelativeValue.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericDateTime/SqlDateTimeRelativeValue.cs new file mode 100644 index 00000000..b3f75803 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericDateTime/SqlDateTimeRelativeValue.cs @@ -0,0 +1,122 @@ +using System; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + public class SqlDateTimeRelativeValue : IComparable, IEquatable + { + public SqlDateTimeRelativeValue(TimeDetails timeAttributes, DateDetails dateAttributes) : this(DateTimeRangeKind.Unknown, timeAttributes, dateAttributes) + { } + + public SqlDateTimeRelativeValue(DateTimeRangeKind rangeKind, DateDetails dateAttributes) : this(rangeKind, TimeDetails.None, dateAttributes) + { } + + public SqlDateTimeRelativeValue(DateTimeRangeKind rangeKind, TimeDetails timeAttributes) : this(rangeKind, timeAttributes, DateDetails.None) + { } + + public SqlDateTimeRelativeValue(DateTimeRangeKind rangeKind, TimeDetails timeAttributes, DateDetails dateAttributes) + { + RangeKind = rangeKind; + TimeAttributes = timeAttributes; + DateAttributes = dateAttributes; + } + + public SqlDateTimeRelativeValue(TimeSpan value) + { + PreciseValue = new DateTime(value.Ticks); + + TimeAttributes = TimeDetails.RegularDateTime; + DateAttributes = DateDetails.None; + } + + public SqlDateTimeRelativeValue(DateTime value) + { + PreciseValue = value; + RangeKind = DateTimeRangeKind.Precise; + + /*if (value > DateTime.Today) + { + RangeKind = DateTimeRangeKind.Precise; + } + else if (value < DateTime.Today) + { + RangeKind = DateTimeRangeKind.Past; + } + else // if (value > DateTime.Today) + { + // TODO : or precise? + RangeKind = DateTimeRangeKind.Unknown; + } + */ + + if (value.TimeOfDay == TimeSpan.Zero) + { + TimeAttributes = TimeDetails.None; + } + else + { + // TODO : shouldn't it come from the very specific value sql-datatype? + TimeAttributes = TimeDetails.RegularDateTime; + } + + if (value.Date == DateTime.MinValue) + { + DateAttributes = DateDetails.None; + } + else + { + DateAttributes = DateDetails.Full; + } + } + + public DateTimeRangeKind RangeKind { get; } = DateTimeRangeKind.Unknown; + + public TimeDetails TimeAttributes { get; } = TimeDetails.None; + + public DateDetails DateAttributes { get; } = DateDetails.None; + + public DateTime? PreciseValue { get; } + + public static bool operator <(SqlDateTimeRelativeValue a, SqlDateTimeRelativeValue b) => a.CompareTo(b) < 0; + + public static bool operator >(SqlDateTimeRelativeValue a, SqlDateTimeRelativeValue b) => a.CompareTo(b) > 0; + + public static bool operator >=(SqlDateTimeRelativeValue a, SqlDateTimeRelativeValue b) => a.CompareTo(b) >= 0; + + public static bool operator <=(SqlDateTimeRelativeValue a, SqlDateTimeRelativeValue b) => a.CompareTo(b) <= 0; + + public int CompareTo(SqlDateTimeRelativeValue other) + { + if (RangeKind == DateTimeRangeKind.Precise && other.RangeKind == DateTimeRangeKind.Precise) + { + return PreciseValue.Value.CompareTo(other.PreciseValue); + } + else if (RangeKind == DateTimeRangeKind.CurrentMoment && other.RangeKind == DateTimeRangeKind.CurrentMoment) + { + return 0; + } + else if (RangeKind == DateTimeRangeKind.Future && other.RangeKind != DateTimeRangeKind.Future) + { + return 1; + } + else if (RangeKind == DateTimeRangeKind.Past && other.RangeKind != DateTimeRangeKind.Past) + { + return -1; + } + else if (other.RangeKind == DateTimeRangeKind.Future && RangeKind != DateTimeRangeKind.Future) + { + return -1; + } + else if (other.RangeKind == DateTimeRangeKind.Past && RangeKind != DateTimeRangeKind.Past) + { + return 1; + } + + return RangeKind - other.RangeKind; + } + + public bool Equals(SqlDateTimeRelativeValue other) + { + return CompareTo(other) == 0; + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericDateTime/SqlDateTimeTypeReference.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericDateTime/SqlDateTimeTypeReference.cs new file mode 100644 index 00000000..4d3920c6 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericDateTime/SqlDateTimeTypeReference.cs @@ -0,0 +1,29 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using TeamTools.TSQL.ExpressionEvaluator.Interfaces; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + public class SqlDateTimeTypeReference : SqlGenericTypeReference + { + public SqlDateTimeTypeReference(string typeName, SqlDateTimeValueRange size, ISqlValueFactory valueFactory) + : base(typeName, size, valueFactory) + { } + + public static DateTime MinSqlValue { get; } = new DateTime(1900, 1, 1); + + [ExcludeFromCodeCoverage] + protected override int GetBytes() + { + switch (TypeName) + { + case "DATE": return 3; + case "TIME": return 5; + case "SMALLDATETIME": return 4; + } + + // TODO : respect DATETIME2 precision + return 8; + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericDateTime/SqlDateTimeValueRange.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericDateTime/SqlDateTimeValueRange.cs new file mode 100644 index 00000000..df2ca563 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericDateTime/SqlDateTimeValueRange.cs @@ -0,0 +1,37 @@ +using System; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + public class SqlDateTimeValueRange : IComparable + { + public SqlDateTimeValueRange(SqlDateTimeRelativeValue lowerAndUpperRange) : this(lowerAndUpperRange, lowerAndUpperRange) + { } + + public SqlDateTimeValueRange(DateTime lowerAndUpperDateTimeValue) : this(new SqlDateTimeRelativeValue(lowerAndUpperDateTimeValue)) + { } + + public SqlDateTimeValueRange(TimeSpan lowerAndUpperTimeValue) : this(new SqlDateTimeRelativeValue(lowerAndUpperTimeValue)) + { } + + public SqlDateTimeValueRange(SqlDateTimeRelativeValue lowerRange, SqlDateTimeRelativeValue upperRange) + { + Low = lowerRange; + High = upperRange; + } + + public SqlDateTimeRelativeValue Low { get; } + + public SqlDateTimeRelativeValue High { get; } + + public int CompareTo(SqlDateTimeValueRange other) + { + if (other.High == High && other.Low == Low) + { + return 0; + } + + // TODO : shouldn't we respect Low value here? + return other.High.CompareTo(High); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericDateTime/SqlGenericDateTimeTypeHandler.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericDateTime/SqlGenericDateTimeTypeHandler.cs new file mode 100644 index 00000000..5d87dca1 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericDateTime/SqlGenericDateTimeTypeHandler.cs @@ -0,0 +1,23 @@ +using System; +using TeamTools.TSQL.ExpressionEvaluator.Interfaces; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + public abstract class SqlGenericDateTimeTypeHandler : SqlGenericTypeHandler + where TValue : IComparable + where TSqlValue : SqlGenericValue + { + protected SqlGenericDateTimeTypeHandler(SqlGenericDateTimeTypeValueFactory valueFactory, ISqlTypeConverter typeConverter, IViolationRegistrar violations) + : base(valueFactory, typeConverter, violations) + { + } + + public override SqlDateTimeValueRange CombineSize(SqlDateTimeValueRange a, SqlDateTimeValueRange b) + { + var rangeMin = a.Low < b.Low ? a.Low : b.Low; + var rangeMax = a.High > b.High ? a.High : b.High; + + return new SqlDateTimeValueRange(rangeMin, rangeMax); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericDateTime/SqlGenericDateTimeTypeValueFactory.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericDateTime/SqlGenericDateTimeTypeValueFactory.cs new file mode 100644 index 00000000..0f32c57f --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericDateTime/SqlGenericDateTimeTypeValueFactory.cs @@ -0,0 +1,45 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.TSQL.ExpressionEvaluator.Interfaces; +using TeamTools.TSQL.ExpressionEvaluator.Routines; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + public abstract class SqlGenericDateTimeTypeValueFactory : SqlGenericValueFactory, ISqlValueFactory + where TValue : IComparable + where TSqlValue : SqlValue + { + private static readonly string DateTimeFallbackTypeName = TSqlDomainAttributes.Types.DateTime; + + private static readonly Dictionary DefaultRanges = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { TSqlDomainAttributes.Types.SmallDateTime, new SqlDateTimeValueRange(new SqlDateTimeRelativeValue(TimeDetails.DayTime, DateDetails.Small)) }, + { TSqlDomainAttributes.Types.DateTime, new SqlDateTimeValueRange(new SqlDateTimeRelativeValue(TimeDetails.RegularDateTime, DateDetails.Full)) }, + { TSqlDomainAttributes.Types.DateTime2, new SqlDateTimeValueRange(new SqlDateTimeRelativeValue(TimeDetails.Detailed, DateDetails.Full)) }, + { "DATE", new SqlDateTimeValueRange(new SqlDateTimeRelativeValue(TimeDetails.None, DateDetails.Full)) }, + { "TIME", new SqlDateTimeValueRange(new SqlDateTimeRelativeValue(TimeDetails.Detailed, DateDetails.None)) }, + }; + + protected SqlGenericDateTimeTypeValueFactory() : base(DateTimeFallbackTypeName) + { + } + + public override SqlDateTimeValueRange GetDefaultTypeSize(string typeName) + { + if (!string.IsNullOrEmpty(typeName) && DefaultRanges.TryGetValue(typeName, out var range)) + { + return range; + } + + return default; + } + + public abstract SqlValue NewLiteral(string typeName, string value, TSqlFragment source); + + public abstract SqlValue NewNull(TSqlFragment source); + + public abstract SqlValue NewValue(SqlTypeReference typeRef, SqlValueKind valueKind); + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericNumber/SqlGenericNumberValueRange.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericNumber/SqlGenericNumberValueRange.cs index 5c9f7370..1a7200d6 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericNumber/SqlGenericNumberValueRange.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/GenericNumber/SqlGenericNumberValueRange.cs @@ -18,6 +18,11 @@ protected SqlGenericNumberValueRange(TNumber low, TNumber high) public static int Compare(SqlGenericNumberValueRange a, SqlGenericNumberValueRange b) { + if (b is null) + { + return 1; + } + if (a.Low.Equals(b.Low) && a.High.Equals(b.High)) { return 0; @@ -41,6 +46,6 @@ public static int Compare(SqlGenericNumberValueRange a, SqlGenericNumbe public int CompareTo(SqlGenericNumberValueRange other) => Compare(this, other); - public bool Equals(SqlGenericNumberValueRange other) => 0 == CompareTo(other); + public virtual bool Equals(SqlGenericNumberValueRange other) => CompareTo(other) == 0; } } diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Int/SqlIntTypeConverter.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Int/SqlIntTypeConverter.cs index 74530676..6503b5a7 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Int/SqlIntTypeConverter.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Int/SqlIntTypeConverter.cs @@ -141,6 +141,56 @@ private static SqlIntTypeValue DoConvertValueFrom( return typeHandler.IntValueFactory.MakePreciseValue(targetType.TypeName, intValue, src.Source); } + // converting from datetime + if (src is SqlDateTimeValue datetimeSrc) + { + // 99991231 23:59:59.997 + const int maxDateTimeAsIntValue = 2958464; + DateTime minDateTimeSqlValue = new DateTime(1900, 1, 1); + + if (!datetimeSrc.IsPreciseValue) + { + return typeHandler.IntValueFactory.MakeApproximateValue(targetType.TypeName, new SqlIntValueRange(0, maxDateTimeAsIntValue), src.Source); + } + + // FIXME : prevent out of range error + int intValue = (int)(datetimeSrc.Value - minDateTimeSqlValue).TotalDays; + + return typeHandler.IntValueFactory.MakePreciseValue(targetType.TypeName, intValue, src.Source); + } + + // converting from varbinary + if (src is SqlBinaryTypeValue binarySrc) + { + if (!binarySrc.IsPreciseValue) + { + return typeHandler.IntValueFactory.MakeUnknownValue(targetType.TypeName); + } + + int intValue = 0; + + // preventing out of range error + unchecked + { + intValue = (int)binarySrc.Value.AsNumber; + } + + return typeHandler.IntValueFactory.MakePreciseValue(targetType.TypeName, intValue, src.Source); + } + + // converting from decimal + if (src is SqlDecimalTypeValue decSrc) + { + if (!decSrc.IsPreciseValue) + { + return typeHandler.IntValueFactory.MakeUnknownValue(targetType.TypeName); + } + + // TODO : check out of range for small target int types + // TODO : register scale loss + return typeHandler.IntValueFactory.MakePreciseValue(targetType.TypeName, (int)decSrc.Value, src.Source); + } + // TODO : register violation about impossible conversion? return default; } diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/SqlGenericTypeReference.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/SqlGenericTypeReference.cs index 3b2619a7..b8950a2e 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/SqlGenericTypeReference.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/SqlGenericTypeReference.cs @@ -19,15 +19,11 @@ protected SqlGenericTypeReference(string typeName, TSize size, ISqlValueFactory public TSize Size { get; } - public int Bytes => GetBytes(); - public override int CompareTo(SqlTypeReference other) => other is SqlGenericTypeReference typeWithSize && IsOfSameType(other) ? Size.CompareTo(typeWithSize.Size) // TODO : or error? : default; - - protected abstract int GetBytes(); } } diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/SqlTypeConverter.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/SqlTypeConverter.cs index e204992e..1181ab95 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/SqlTypeConverter.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/SqlTypeConverter.cs @@ -67,6 +67,26 @@ public virtual T ImplicitlyConvert(SqlValue from) return (T)ImplicitlyConvertTo(TSqlDomainAttributes.Types.BigInt, from); } + if (typeof(T) == typeof(SqlDateOnlyValue)) + { + return (T)ImplicitlyConvertTo(TSqlDomainAttributes.Types.Date, from); + } + + if (typeof(T) == typeof(SqlTimeOnlyValue)) + { + return (T)ImplicitlyConvertTo(TSqlDomainAttributes.Types.Time, from); + } + + if (typeof(T) == typeof(SqlDateTimeValue)) + { + return (T)ImplicitlyConvertTo(TSqlDomainAttributes.Types.DateTime, from); + } + + if (typeof(T) == typeof(SqlDecimalTypeValue)) + { + return (T)ImplicitlyConvertTo(TSqlDomainAttributes.Types.Decimal, from); + } + return default; } diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/SqlTypeReference.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/SqlTypeReference.cs index cc71b7c3..16cb78d5 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/SqlTypeReference.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/SqlTypeReference.cs @@ -22,6 +22,8 @@ protected SqlTypeReference(string typeName, ISqlValueFactory valueFactory) public string TypeName { get; } + public int Bytes => GetBytes(); + public SqlValue MakeValue(SqlValueKind valueKind) { if (valueKind == SqlValueKind.Precise) @@ -43,6 +45,8 @@ public virtual bool Equals(SqlTypeReference other) public abstract int CompareTo(SqlTypeReference other); + protected abstract int GetBytes(); + protected bool IsOfSameType(SqlTypeReference other) { return other.GetType().IsAssignableFrom(GetType()) diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Str/SqlStrTypeConverter.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Str/SqlStrTypeConverter.cs index 0f9c7060..54714f92 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Str/SqlStrTypeConverter.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Str/SqlStrTypeConverter.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using TeamTools.TSQL.ExpressionEvaluator.Routines; using TeamTools.TSQL.ExpressionEvaluator.Values; @@ -11,6 +12,52 @@ namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling [ExcludeFromCodeCoverage] public static class SqlStrTypeConverter { + private static readonly Dictionary DateTimeFormats = new Dictionary + { + { 0, "mon dd yyyy hh:miAM" }, + { 100, "mon dd yyyy hh:miAM" }, + { 1, "mm/dd/yy" }, + { 101, "mm/dd/yyyy" }, + { 2, "yy.mm.dd" }, + { 102, "yyyy.mm.dd" }, + { 3, "dd/mm/yy" }, + { 103, "dd/mm/yy" }, + { 4, "dd.mm.yy" }, + { 104, "dd.mm.yyyy" }, + { 5, "dd-mm-yy" }, + { 105, "dd-mm-yyyy" }, + { 6, "dd mon yy" }, + { 106, "dd mon yyyy" }, + { 7, "Mon dd, yy" }, + { 107, "Mon dd, yyyy" }, + { 8, "hh:mi:ss" }, + { 108, "hh:mi:ss" }, + { 9, "mon dd yy hh:mi:ss:mmmAM" }, + { 109, "mon dd yyyy hh:mi:ss:mmmAM" }, + { 10, "mm-dd-yy" }, + { 110, "mm-dd-yyyy" }, + { 11, "yy/mm/dd" }, + { 111, "yyyy/mm/dd" }, + { 12, "yymmdd" }, + { 112, "yyyymmdd" }, + { 13, "dd mon yyyy hh:mi:ss:mmm" }, + { 113, "dd mon yyyy hh:mi:ss:mmm" }, + { 14, "hh:mi:ss:mmm" }, + { 114, "hh:mi:ss:mmm" }, + { 20, "yyyy-mm-dd hh:mi:ss" }, + { 120, "yyyy-mm-dd hh:mi:ss" }, + { 21, "yyyy-mm-dd hh:mi:ss.mmm" }, + { 121, "yyyy-mm-dd hh:mi:ss.mmm" }, + { 25, "yyyy-mm-dd hh:mi:ss.mmm" }, + { 22, "mm/dd/yy hh:mi:ss AM" }, + { 23, "yyyy-mm-dd" }, + { 24, "hh:mi:ss" }, + { 126, "yyyy-mm-ddThh:mi:ss.mmm" }, + { 127, "yyyy-MM-ddThh:mm:ss.fffZ" }, + { 130, "dd mon yyyy hh:mi:ss:mmmAM" }, + { 131, "dd/mm/yyyy hh:mi:ss:mmmAM" }, + }; + public static SqlStrTypeValue ConvertValueFrom(this SqlStrTypeHandler typeHandler, SqlValue src, string targetType) { int targetSize = typeHandler.StrValueFactory.GetDefaultTypeSize(targetType); @@ -83,7 +130,7 @@ private static SqlStrTypeValue DoConvertValueFrom( { // target type size is not enough to store source value // FIXME : this is the very wrong Source provided here - typeHandler.Violations.RegisterViolation(new ImplicitTruncationViolation(targetType.Size, maxLength, null, intSrc.Source)); + typeHandler.Violations.RegisterViolation(new ImplicitTruncationViolation(targetType.Size, maxLength, null, src.Source)); } else if (targetType.Size > maxLength) { @@ -108,7 +155,7 @@ private static SqlStrTypeValue DoConvertValueFrom( var resultSrc = src.Source; if (strictTargetSize && strValue.Length > targetType.Size) { - typeHandler.Violations.RegisterViolation(new ImplicitTruncationViolation(targetType.Size, strValue.Length, strValue, intSrc.Source)); + typeHandler.Violations.RegisterViolation(new ImplicitTruncationViolation(targetType.Size, strValue.Length, strValue, src.Source)); strValue = strValue.Substring(0, targetType.Size); resultSrc = new SqlValueSource(SqlValueSourceKind.Expression, src.Source.Node); } @@ -130,7 +177,7 @@ private static SqlStrTypeValue DoConvertValueFrom( if (targetType.Size < maxLength) { - typeHandler.Violations.RegisterViolation(new ImplicitTruncationViolation(targetType.Size, maxLength, null, bigintSrc.Source)); + typeHandler.Violations.RegisterViolation(new ImplicitTruncationViolation(targetType.Size, maxLength, null, src.Source)); } int targetSize = forceTargetType ? targetType.Size : maxLength; @@ -150,7 +197,7 @@ private static SqlStrTypeValue DoConvertValueFrom( var resultSrc = src.Source; if (strictTargetSize && strValue.Length > targetType.Size) { - typeHandler.Violations.RegisterViolation(new ImplicitTruncationViolation(targetType.Size, strValue.Length, strValue, bigintSrc.Source)); + typeHandler.Violations.RegisterViolation(new ImplicitTruncationViolation(targetType.Size, strValue.Length, strValue, src.Source)); strValue = strValue.Substring(0, targetType.Size); resultSrc = new SqlValueSource(SqlValueSourceKind.Expression, src.Source.Node); } @@ -179,7 +226,7 @@ private static SqlStrTypeValue DoConvertValueFrom( // FIXME : wrong source is provided here // current node is expected, not the node where another // variable was initialized - typeHandler.Violations.RegisterViolation(new ImplicitTruncationViolation(definedTargetSize, strSrc.EstimatedSize, null, strSrc.Source)); + typeHandler.Violations.RegisterViolation(new ImplicitTruncationViolation(definedTargetSize, strSrc.EstimatedSize, null, src.Source)); } targetSize = definedTargetSize; @@ -188,7 +235,7 @@ private static SqlStrTypeValue DoConvertValueFrom( if (!targetType.IsUnicode && !strSrc.IsNull && strSrc.TypeReference is SqlStrTypeReference strRef && strRef.IsUnicode) { - typeHandler.Violations.RegisterViolation(new NationalSymbolLossViolation(strSrc.TypeName, strSrc.Source)); + typeHandler.Violations.RegisterViolation(new NationalSymbolLossViolation(strSrc.TypeName, src.Source)); } return typeHandler.StrValueFactory.MakeApproximateValue(targetType.TypeName, targetSize, src.Source); @@ -221,7 +268,7 @@ private static SqlStrTypeValue DoConvertValueFrom( { // FIXME : how to distinct implicit truncation from explicit one? // in case of implicit a violation warning is needed. - typeHandler.Violations.RegisterViolation(new ImplicitTruncationViolation(definedTargetSize, strValue.Length, strValue, strSrc.Source)); + typeHandler.Violations.RegisterViolation(new ImplicitTruncationViolation(definedTargetSize, strValue.Length, strValue, src.Source)); strValue = strValue.Substring(0, definedTargetSize); resultSrc = new SqlValueSource(SqlValueSourceKind.Expression, src.Source.Node); @@ -230,6 +277,202 @@ private static SqlStrTypeValue DoConvertValueFrom( return typeHandler.StrValueFactory.MakePreciseValue(targetType.TypeName, strValue, resultSrc); } + // converting from datetime + if (src is SqlDateTimeValue datetimeSrc) + { + // yyyy-MM-ddThh:mm:ss.fffZ + const int maxDateTimeFormatLength = 24; + const int maxDateFormatLength = 10; + + if (!datetimeSrc.IsPreciseValue) + { + int approximateLength = maxDateTimeFormatLength; + + if (strictTargetSize) + { + approximateLength = targetType.Size; + } + else if (datetimeSrc.EstimatedSize.High.TimeAttributes == TimeDetails.None) + { + approximateLength = maxDateFormatLength; + } + + return typeHandler.StrValueFactory.MakeApproximateValue(targetType.TypeName, approximateLength, src.Source); + } + + // TODO : respect specific style if provided to CONVERT + // TODO : respect culture defined on server-side? or is current thread culture a better choice? + string strValue = datetimeSrc.Value.ToString(); + // little crutch for dates without time before implementing CONVERT styles + if (datetimeSrc.Value.TimeOfDay.Equals(TimeSpan.Zero)) + { + strValue = datetimeSrc.Value.Date.ToString("yyyy'-'MM'-'dd"); + } + + // FIXME : magic + if (targetType.Size < 0) + { + // TODO : or unknown? + return typeHandler.StrValueFactory.MakePreciseValue(targetType.TypeName, strValue, src.Source); + } + + var resultSrc = src.Source; + if (strictTargetSize && strValue.Length > targetType.Size) + { + // TODO : implement CONVERT style support first + // typeHandler.Violations.RegisterViolation(new ImplicitTruncationViolation(targetType.Size, strValue.Length, strValue, src.Source)); + strValue = strValue.Substring(0, targetType.Size); + resultSrc = new SqlValueSource(SqlValueSourceKind.Expression, src.Source.Node); + } + + return typeHandler.StrValueFactory.MakePreciseValue(targetType.TypeName, strValue, resultSrc); + } + + // converting from date + if (src is SqlDateOnlyValue dateSrc) + { + // yyyy-MM-dd + const int maxDateFormatLength = 10; + + if (!dateSrc.IsPreciseValue) + { + int approximateLength = strictTargetSize ? targetType.Size : maxDateFormatLength; + + return typeHandler.StrValueFactory.MakeApproximateValue(targetType.TypeName, approximateLength, src.Source); + } + + // TODO : respect specific format if provided to CAST/CONVERT + string strValue = dateSrc.Value.Date.ToString("yyyy'-'MM'-'dd"); + + // FIXME : magic + if (targetType.Size < 0) + { + // TODO : or unknown? + return typeHandler.StrValueFactory.MakePreciseValue(targetType.TypeName, strValue, src.Source); + } + + var resultSrc = src.Source; + if (strictTargetSize && strValue.Length > targetType.Size) + { + // TODO : implement CONVERT style support first + // typeHandler.Violations.RegisterViolation(new ImplicitTruncationViolation(targetType.Size, strValue.Length, strValue, src.Source)); + strValue = strValue.Substring(0, targetType.Size); + resultSrc = new SqlValueSource(SqlValueSourceKind.Expression, src.Source.Node); + } + + return typeHandler.StrValueFactory.MakePreciseValue(targetType.TypeName, strValue, resultSrc); + } + + // converting from time + if (src is SqlTimeOnlyValue timeSrc) + { + // 23:59:59.9970000 + const int maxTimeFormatLength = 16; + + if (!timeSrc.IsPreciseValue) + { + int approximateLength = strictTargetSize ? targetType.Size : maxTimeFormatLength; + return typeHandler.StrValueFactory.MakeApproximateValue(targetType.TypeName, approximateLength, src.Source); + } + + // TODO : respect specific format if provided to CONVERT + string strValue = timeSrc.Value.ToString("hh':'mm':'ss'.'ffffff"); + + // FIXME : magic + if (targetType.Size < 0) + { + // TODO : or unknown? + return typeHandler.StrValueFactory.MakePreciseValue(targetType.TypeName, strValue, src.Source); + } + + var resultSrc = src.Source; + if (strictTargetSize && strValue.Length > targetType.Size) + { + // TODO : implement CONVERT style support first + // typeHandler.Violations.RegisterViolation(new ImplicitTruncationViolation(targetType.Size, strValue.Length, strValue, src.Source)); + strValue = strValue.Substring(0, targetType.Size); + resultSrc = new SqlValueSource(SqlValueSourceKind.Expression, src.Source.Node); + } + + return typeHandler.StrValueFactory.MakePreciseValue(targetType.TypeName, strValue, resultSrc); + } + + // TODO : see also binary conversion styles + // https://learn.microsoft.com/en-us/sql/t-sql/functions/cast-and-convert-transact-sql?view=sql-server-ver17&f1url=%3FappId%3DDev15IDEF1%26l%3DEN-US%26k%3Dk(convert_TSQL)%3Bk(sql13.swb.tsqlresults.f1)%3Bk(sql13.swb.tsqlquery.f1)%3Bk(MiscellaneousFilesProject)%3Bk(DevLang-TSQL)%26rd%3Dtrue#binary-styles + // converting from varbinary + if (src is SqlBinaryTypeValue binarySrc) + { + if (!binarySrc.IsPreciseValue) + { + // Each byte from source is represented by two symbols in a string + int targetSize = binarySrc.EstimatedSize * 2; + if (strictTargetSize) + { + if (targetType.HasFixedLength) + { + targetSize = targetType.Size; + } + else if (targetType.Size < targetSize) + { + targetSize = targetType.Size; + } + } + + return typeHandler.StrValueFactory.MakeApproximateValue(targetType.TypeName, targetSize, src.Source); + } + + // TODO : if style is 1 then 0x must be prepended - use Value.ToString() in such case + // TODO : if no style provided then binary data should not be formatted as Hex and should be used as array of ASCII char codes + var strValue = binarySrc.Value.AsString; + var resultSrc = src.Source; + + if (strictTargetSize && strValue.Length > targetType.Size) + { + // TODO : implement CONVERT style support first + typeHandler.Violations.RegisterViolation(new ImplicitTruncationViolation(targetType.Size, strValue.Length, strValue, src.Source)); + strValue = strValue.Substring(0, targetType.Size); + resultSrc = new SqlValueSource(SqlValueSourceKind.Expression, src.Source.Node); + } + + return typeHandler.StrValueFactory.MakePreciseValue(targetType.TypeName, strValue, resultSrc); + } + + // converting from decimal + if (src is SqlDecimalTypeValue decSrc) + { + // TODO : strictTargetSize + HasFixedLength => targetType.Size + if (!decSrc.IsPreciseValue) + { + // +1 - the decimal part separator + int targetSize = decSrc.EstimatedSize.Precision + (decSrc.EstimatedSize.Scale > 0 ? 1 : 0); + if (strictTargetSize) + { + if (targetType.HasFixedLength) + { + targetSize = targetType.Size; + } + else if (targetType.Size < targetSize) + { + targetSize = targetType.Size; + } + } + + return typeHandler.StrValueFactory.MakeApproximateValue(targetType.TypeName, targetSize, src.Source); + } + + var strValue = decSrc.Value.ToString(); + var resultSrc = src.Source; + + if (strictTargetSize && strValue.Length > targetType.Size) + { + typeHandler.Violations.RegisterViolation(new ImplicitTruncationViolation(targetType.Size, strValue.Length, strValue, src.Source)); + strValue = strValue.Substring(0, targetType.Size); + resultSrc = new SqlValueSource(SqlValueSourceKind.Expression, src.Source.Node); + } + + return typeHandler.StrValueFactory.MakePreciseValue(targetType.TypeName, strValue, resultSrc); + } + return default; } } diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Str/SqlStrTypeHandler.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Str/SqlStrTypeHandler.cs index 7a0a8c28..ca5f6cd3 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Str/SqlStrTypeHandler.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Str/SqlStrTypeHandler.cs @@ -64,6 +64,7 @@ public override int CombineSize(int a, int b) return int.MaxValue; } + // TODO : shouldn't the 8000 limit be checked here? return a + b; } diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Str/SqlStrTypeReference.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Str/SqlStrTypeReference.cs index 1976b31b..8a5490b2 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Str/SqlStrTypeReference.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Str/SqlStrTypeReference.cs @@ -1,30 +1,13 @@ using System; -using System.Collections.Generic; using TeamTools.TSQL.ExpressionEvaluator.Interfaces; -using TeamTools.TSQL.ExpressionEvaluator.Routines; namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling { public class SqlStrTypeReference : SqlGenericTypeReference { - // TODO : put to one place - private static readonly HashSet SupportedTypes = new HashSet(StringComparer.OrdinalIgnoreCase) - { - TSqlDomainAttributes.Types.Char, - TSqlDomainAttributes.Types.NChar, - TSqlDomainAttributes.Types.Varchar, - TSqlDomainAttributes.Types.NVarchar, - TSqlDomainAttributes.Types.SysName, - }; - public SqlStrTypeReference(string typeName, int size, ISqlValueFactory valueFactory) : base(typeName, size, valueFactory) { - if (!SupportedTypes.Contains(typeName)) - { - throw new ArgumentOutOfRangeException(nameof(typeName)); - } - // TODO : currently string with unknown length will be created as size = -1 // not sure if this is a good idea /* @@ -33,10 +16,15 @@ public SqlStrTypeReference(string typeName, int size, ISqlValueFactory valueFact throw new ArgumentOutOfRangeException(nameof(size), size, "String type max length must be positive"); } */ + + HasFixedLength = !typeName.StartsWith("VAR", StringComparison.OrdinalIgnoreCase) + && !typeName.StartsWith("NVAR", StringComparison.OrdinalIgnoreCase); } public bool IsUnicode => GetIsUnicode(); + public bool HasFixedLength { get; } + public override bool Equals(SqlTypeReference other) { return base.Equals(other) diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Str/SqlStrTypeValueFactory.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Str/SqlStrTypeValueFactory.cs index 00b87012..9d4471c7 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Str/SqlStrTypeValueFactory.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Str/SqlStrTypeValueFactory.cs @@ -18,6 +18,8 @@ static SqlStrTypeValueFactory() { DefaultStringSizes.Add(TSqlDomainAttributes.Types.Char, 1); DefaultStringSizes.Add(TSqlDomainAttributes.Types.NChar, 1); + // FIXME : column or variable will be 1 symbol long + // only CAST/CONVERT to VARCHAR without length results with 30-symbol long value DefaultStringSizes.Add(TSqlDomainAttributes.Types.Varchar, 30); DefaultStringSizes.Add(TSqlDomainAttributes.Types.NVarchar, 30); DefaultStringSizes.Add(TSqlDomainAttributes.Types.SysName, 128); diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Time/SqlTimeOnlyValue.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Time/SqlTimeOnlyValue.cs new file mode 100644 index 00000000..d624ad99 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Time/SqlTimeOnlyValue.cs @@ -0,0 +1,43 @@ +using System; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + // TODO : Switch to System.TimeOnly after dropping netstandard2.0 support + public class SqlTimeOnlyValue : SqlGenericValueWithHandler + { + public SqlTimeOnlyValue(SqlTimeTypeHandler typeHandler, SqlDateTimeTypeReference typeReference, SqlValueKind valueKind, SqlValueSource source) + : base(typeHandler, typeReference, valueKind, source) + { + } + + public SqlTimeOnlyValue(SqlTimeTypeHandler typeHandler, SqlDateTimeTypeReference typeReference, TimeSpan value, SqlValueSource source) + : base(typeHandler, typeReference, value, source) + { + } + + // For cloning + protected SqlTimeOnlyValue(SqlTimeOnlyValue src, TimeSpan value) : base(src, value) + { + } + + // For cloning + protected SqlTimeOnlyValue(SqlTimeOnlyValue src) : base(src) + { + } + + public override SqlTimeOnlyValue DeepClone() + { + if (IsPreciseValue && !IsNull) + { + return new SqlTimeOnlyValue(this, Value); + } + + return new SqlTimeOnlyValue(this); + } + + public SqlTimeOnlyValue ChangeTo(TimeSpan newValue, SqlValueSource source) => TypeHandler.ChangeValueTo(this, newValue, source); + + public SqlTimeOnlyValue ChangeTo(SqlDateTimeValueRange newSize, SqlValueSource source) => TypeHandler.ChangeValueTo(this, newSize, source); + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Time/SqlTimeTypeConverter.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Time/SqlTimeTypeConverter.cs new file mode 100644 index 00000000..d5875d0a --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Time/SqlTimeTypeConverter.cs @@ -0,0 +1,114 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + // TODO : refactor first then cover with tests + // FIXME : fix Source for new values. maybe pass from outside + [ExcludeFromCodeCoverage] + public static class SqlTimeTypeConverter + { + public static SqlTimeOnlyValue ConvertValueFrom(this SqlTimeTypeHandler typeHandler, SqlValue src, string targetType) + { + if (src is null) + { + return default; + } + + if (src is SqlTimeOnlyValue timeSrc + && string.Equals(timeSrc.TypeName, targetType, StringComparison.OrdinalIgnoreCase)) + { + return timeSrc; + } + + var targetTypeRef = typeHandler.TimeValueFactory.MakeSqlDataTypeReference(targetType); + + return typeHandler.ConvertValueFrom(src, targetTypeRef, false); + } + + public static SqlTimeOnlyValue ConvertValueFrom(this SqlTimeTypeHandler typeHandler, SqlValue src, SqlDateTimeTypeReference targetType, bool forceTargetType) + { + return typeHandler.DoConvertValueFrom(src, targetType, true, forceTargetType); + } + + private static SqlTimeOnlyValue DoConvertValueFrom( + this SqlTimeTypeHandler typeHandler, + SqlValue src, + SqlDateTimeTypeReference targetType, + bool strictTypeSize, + bool forceTargetType) + { + if (src is null) + { + return default; + } + + if (src.IsNull) + { + return typeHandler.TimeValueFactory.MakeNullValue(targetType.TypeName, src.Source); + } + + // converting from string + if (src is SqlStrTypeValue strSrc) + { + if (!strSrc.IsPreciseValue) + { + return typeHandler.TimeValueFactory.MakeUnknownValue(targetType.TypeName); + } + + return (SqlTimeOnlyValue)typeHandler.TimeValueFactory.NewLiteral(targetType.TypeName, strSrc.Value, src.Source.Node); + } + + // converting from int + if (src is SqlIntTypeValue intSrc) + { + if (!intSrc.IsPreciseValue) + { + // TODO : use range if provided + return typeHandler.TimeValueFactory.MakeUnknownValue(targetType.TypeName); + } + + return typeHandler.TimeValueFactory.MakePreciseValue(targetType.TypeName, new TimeSpan(intSrc.Value), src.Source); + } + + // converting from bigint + if (src is SqlBigIntTypeValue bigintSrc) + { + if (!bigintSrc.IsPreciseValue || bigintSrc.Value > int.MaxValue || bigintSrc.Value < int.MinValue) + { + // TODO : use range if provided + return typeHandler.TimeValueFactory.MakeUnknownValue(targetType.TypeName); + } + + return typeHandler.TimeValueFactory.MakePreciseValue(targetType.TypeName, new TimeSpan((int)bigintSrc.Value), src.Source); + } + + // converting from date + if (src is SqlDateOnlyValue dateSrc) + { + if (!dateSrc.IsPreciseValue) + { + // TODO : use range if provided + return typeHandler.TimeValueFactory.MakeUnknownValue(targetType.TypeName); + } + + return typeHandler.TimeValueFactory.MakePreciseValue(targetType.TypeName, TimeSpan.Zero, src.Source); + } + + // converting from datetime + if (src is SqlDateTimeValue datetimeSrc) + { + if (!datetimeSrc.IsPreciseValue) + { + // TODO : use range if provided + return typeHandler.TimeValueFactory.MakeUnknownValue(targetType.TypeName); + } + + return typeHandler.TimeValueFactory.MakePreciseValue(targetType.TypeName, datetimeSrc.Value.TimeOfDay, src.Source); + } + + return default; + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Time/SqlTimeTypeHandler.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Time/SqlTimeTypeHandler.cs new file mode 100644 index 00000000..88c4d96b --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Time/SqlTimeTypeHandler.cs @@ -0,0 +1,39 @@ +using System; +using TeamTools.TSQL.ExpressionEvaluator.Interfaces; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + public sealed class SqlTimeTypeHandler : SqlGenericDateTimeTypeHandler + { + private readonly SqlTimeTypeValueFactory typedValueFactory; + + public SqlTimeTypeHandler(ISqlTypeConverter typeConverter, IViolationRegistrar violations) + : this(new SqlTimeTypeValueFactory(), typeConverter, violations) + { + } + + private SqlTimeTypeHandler(SqlTimeTypeValueFactory valueFactory, ISqlTypeConverter typeConverter, IViolationRegistrar violations) + : base(valueFactory, typeConverter, violations) + { + valueFactory.TypeHandler = this; + typedValueFactory = valueFactory; + } + + public SqlTimeTypeValueFactory TimeValueFactory => typedValueFactory; + + public override SqlValue ConvertFrom(SqlValue from, SqlTypeReference to, bool forceTargetType = false) + => to is SqlDateTimeTypeReference datetimeType ? ConvertFrom(from, datetimeType, forceTargetType) : default; + + public SqlTimeOnlyValue ConvertFrom(SqlValue from, SqlDateTimeTypeReference to, bool forceTargetType) + => this.ConvertValueFrom(from, to, forceTargetType); + + public override SqlValue ConvertFrom(SqlValue from, string to) + => IsTypeSupported(to) ? this.ConvertValueFrom(from, to) : default; + + public override ISqlValueFactory GetValueFactory() => typedValueFactory; + + public override SqlTypeReference MakeSqlDataTypeReference(string typeName) + => typedValueFactory.MakeSqlDataTypeReference(typeName); + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Time/SqlTimeTypeValueFactory.cs b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Time/SqlTimeTypeValueFactory.cs new file mode 100644 index 00000000..7cced47d --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluator/TypeHandling/Time/SqlTimeTypeValueFactory.cs @@ -0,0 +1,112 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.ExpressionEvaluator.TypeHandling +{ + public sealed class SqlTimeTypeValueFactory : SqlGenericDateTimeTypeValueFactory + { + private static readonly HashSet TimeTypes = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "TIME", + }; + + // TODO : it should be readonly + public SqlTimeTypeHandler TypeHandler { get; internal set; } + + public override SqlTimeOnlyValue MakeApproximateValue(string typeName, SqlDateTimeValueRange estimatedSize, SqlValueSource source) + { + if (!IsTypeSupported(typeName)) + { + return default; + } + + return new SqlTimeOnlyValue( + TypeHandler, + new SqlDateTimeTypeReference(typeName, estimatedSize, this), + SqlValueKind.Unknown, + source); + } + + public override SqlTimeOnlyValue MakePreciseValue(string typeName, TimeSpan value, SqlValueSource source) + { + if (!IsTypeSupported(typeName)) + { + return default; + } + + return new SqlTimeOnlyValue( + TypeHandler, + new SqlDateTimeTypeReference(typeName, new SqlDateTimeValueRange(value), this), + value, + source); + } + + public override SqlTimeOnlyValue MakeNullValue(string typeName, SqlValueSource source) + { + if (!IsTypeSupported(typeName)) + { + return default; + } + + return new SqlTimeOnlyValue( + TypeHandler, + MakeSqlDataTypeReference(typeName), + SqlValueKind.Null, + source); + } + + public override SqlValue NewLiteral(string typeName, string value, TSqlFragment source) + { + if (!IsTypeSupported(typeName)) + { + return default; + } + + if (!TimeSpan.TryParse(value, out var timeValue)) + { + return DoMakeValue( + typeName, + SqlValueKind.Unknown, + new SqlValueSource(SqlValueSourceKind.Literal, source)); + } + + return MakeLiteral(typeName, timeValue, source); + } + + public override SqlValue NewNull(TSqlFragment source) => MakeNull(source); + + public override SqlValue NewValue(SqlTypeReference typeRef, SqlValueKind valueKind) => DoMakeValue(typeRef.TypeName, valueKind); + + public override SqlTimeOnlyValue MakeUnknownValue(string typeName) => DoMakeValue(typeName, SqlValueKind.Unknown); + + public SqlDateTimeTypeReference MakeSqlDataTypeReference(string typeName) + { + var typeSize = GetDefaultTypeSize(typeName); + + if (typeSize is null) + { + return default; + } + + return new SqlDateTimeTypeReference(typeName, typeSize, this); + } + + protected override ICollection GetSupportedTypes() => TimeTypes; + + private SqlTimeOnlyValue DoMakeValue(string typeName, SqlValueKind valueKind, SqlValueSource source = null) + { + if (!IsTypeSupported(typeName)) + { + return default; + } + + return new SqlTimeOnlyValue( + TypeHandler, + MakeSqlDataTypeReference(typeName), + valueKind, + source); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluator/Values/SqlLiteralValueFactory.cs b/TeamTools.TSQL.ExpressionEvaluator/Values/SqlLiteralValueFactory.cs index 9fa5c866..6fb9c2a4 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/Values/SqlLiteralValueFactory.cs +++ b/TeamTools.TSQL.ExpressionEvaluator/Values/SqlLiteralValueFactory.cs @@ -1,5 +1,6 @@ using Microsoft.SqlServer.TransactSql.ScriptDom; using System; +using System.Globalization; using System.Numerics; using TeamTools.TSQL.ExpressionEvaluator.Interfaces; using TeamTools.TSQL.ExpressionEvaluator.Routines; @@ -13,6 +14,8 @@ public class SqlLiteralValueFactory : ILiteralValueFactory private static readonly string UnicodeStringType = TSqlDomainAttributes.Types.NVarchar; private static readonly string DefaultNumericType = TSqlDomainAttributes.Types.Int; private static readonly string BigIntType = TSqlDomainAttributes.Types.BigInt; + private static readonly string BinaryType = TSqlDomainAttributes.Types.Binary; + private static readonly string DecimalType = TSqlDomainAttributes.Types.Decimal; private static readonly string FallbackType = DefaultStringType; private readonly ISqlTypeResolver typeResolver; @@ -33,6 +36,10 @@ public SqlValue Make(Literal src) typeName = UnicodeStringType; } } + else if (src is BinaryLiteral) + { + typeName = BinaryType; + } else if (int.TryParse(src.Value, out int _)) { typeName = DefaultNumericType; @@ -41,6 +48,10 @@ public SqlValue Make(Literal src) { typeName = BigIntType; } + else if (src is NumericLiteral && decimal.TryParse(src.Value, NumberStyles.Number, CultureInfo.InvariantCulture, out decimal _)) + { + typeName = DecimalType; + } else { // ScriptDom treats as literals MAX keyword from VARCHAR size for example diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Evaluation/EvaluateXQueryTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Evaluation/EvaluateXQueryTests.cs new file mode 100644 index 00000000..166ffeb5 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Evaluation/EvaluateXQueryTests.cs @@ -0,0 +1,50 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using NUnit.Framework; +using System; +using TeamTools.TSQL.ExpressionEvaluator.Evaluation; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Violations; + +namespace TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator +{ + [Category("TeamTools.TSQL.ExpressionEvaluator")] + [TestOf(typeof(SqlScriptAnalyzer))] + public class EvaluateXQueryTests : BaseEvaluatorTestClass + { + private VariableDeclarationVisitor declareVisitor; + private SqlScriptAnalyzer analyzer; + + [SetUp] + public override void SetUp() + { + base.SetUp(); + + declareVisitor = new VariableDeclarationVisitor(VarReg, TypeResolver); + analyzer = new SqlScriptAnalyzer(VarReg, Eval, TypeResolver, Converter, new MockCondHandler(), Violations); + } + + [Test] + public void Test_XQueryValue_ExtractsTypeInfo() + { + var dom = ParseScript(@" + DECLARE @xvalue VARCHAR(15) + SET @xvalue = ( + SELECT foo + FROM @bar + FOR XML PATH(''), TYPE + ).value('.', 'varchar(4000)'); + "); + + dom.Accept(declareVisitor); + dom.Accept(analyzer); + + // Because 4000 is way longer than 15 + Assert.That(Violations.ViolationCount, Is.EqualTo(1)); + Assert.That(Violations.Violations[0], Is.InstanceOf(typeof(ImplicitTruncationViolation))); + + var v = (ImplicitTruncationViolation)Violations.Violations[0]; + Assert.That(v.ValueSize, Is.EqualTo(4000)); + Assert.That(v.TypeSize, Is.EqualTo(15)); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/BaseMockFunctionTest.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/BaseMockFunctionTest.cs index 7b45f145..ac426d49 100644 --- a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/BaseMockFunctionTest.cs +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/BaseMockFunctionTest.cs @@ -61,5 +61,18 @@ protected SqlIntTypeValue MakeInt(int value) return Context.Converter.ImplicitlyConvert( Factory.NewLiteral("INT", value.ToString(), default)); } + + protected SqlDecimalTypeValue MakeDecimal(decimal value) + { + return Context.Converter.ImplicitlyConvert( + Factory.NewLiteral("DECIMAL", value.ToString(), default)); + } + + protected SqlDateTimeValue MakeDateTime(string value) + { + return string.IsNullOrEmpty(value) + ? Context.Converter.ImplicitlyConvert(Factory.NewValue(new SqlDateTimeTypeReference("DATETIME", new SqlDateTimeValueRange(new SqlDateTimeRelativeValue(DateTimeRangeKind.Unknown, DateDetails.Full)), Factory), SqlValueKind.Unknown)) + : Context.Converter.ImplicitlyConvert(Factory.NewLiteral("DATETIME", value, default)); + } } } diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/DateAddTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/DateAddTests.cs new file mode 100644 index 00000000..63b4bda1 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/DateAddTests.cs @@ -0,0 +1,52 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentDto; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.DateFunctions; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator; + +namespace TeamTools.TSQL.ExpressionEvaluatorTests.Tests.Functions.DateFunctions +{ + [Category("TeamTools.TSQL.ExpressionEvaluator.EvalFunctions")] + [TestOf(typeof(DateAdd))] + public class DateAddTests : BaseMockFunctionTest + { + private DateAdd func; + private List funcArgs; + private SqlDateTimeValue dt; + + [SetUp] + public override void SetUp() + { + base.SetUp(); + + func = new DateAdd(); + funcArgs = ArgFactory.MakeList(new DatePartArgument("YEAR"), new ValueArgument(MakeInt(3)), new ValueArgument(MakeDateTime("2022-11-09"))); + } + + [Test] + public void Test_DateDiff_ComputesDiffInYears() + { + var res = func.Evaluate(funcArgs, Context); + + Assert.That(res, Is.Not.Null); + Assert.That(res.IsPreciseValue, Is.True); + Assert.That(res, Is.InstanceOf()); + + Assert.That(((SqlDateTimeValue)res).Value.Year, Is.EqualTo(2025)); + } + + [Test] + public void Test_DateDiff_ComputesDiffInDays() + { + var diffArgs = ArgFactory.MakeList(new DatePartArgument("DAY"), new ValueArgument(MakeInt(3)), new ValueArgument(MakeDateTime("2022-11-09"))); + var res = func.Evaluate(diffArgs, Context); + + Assert.That(res, Is.Not.Null); + Assert.That(res.IsPreciseValue, Is.True); + Assert.That(res, Is.InstanceOf()); + + Assert.That(((SqlDateTimeValue)res).Value.Day, Is.EqualTo(12)); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/DateDiffTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/DateDiffTests.cs new file mode 100644 index 00000000..e751e421 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/DateDiffTests.cs @@ -0,0 +1,39 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentDto; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.DateFunctions; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator; + +namespace TeamTools.TSQL.ExpressionEvaluatorTests.Tests.Functions.DateFunctions +{ + [Category("TeamTools.TSQL.ExpressionEvaluator.EvalFunctions")] + [TestOf(typeof(DateDiff))] + public class DateDiffTests : BaseMockFunctionTest + { + private DateDiff func; + private List funcArgs; + private SqlDateTimeValue dt; + + [SetUp] + public override void SetUp() + { + base.SetUp(); + + func = new DateDiff(); + funcArgs = ArgFactory.MakeList(new DatePartArgument("YEAR"), new ValueArgument(MakeDateTime("2010-04-02")), new ValueArgument(MakeDateTime("2022-11-09"))); + } + + [Test] + public void Test_DateDiff_ComputesDiffInYears() + { + var res = func.Evaluate(funcArgs, Context); + + Assert.That(res, Is.Not.Null); + Assert.That(res.IsPreciseValue, Is.True); + Assert.That(res, Is.InstanceOf()); + + Assert.That(((SqlIntTypeValue)res).Value, Is.EqualTo(12)); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/DateNameTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/DateNameTests.cs index 91a44659..2fbdb63e 100644 --- a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/DateNameTests.cs +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/DateNameTests.cs @@ -2,7 +2,6 @@ using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentDto; using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.DateFunctions; using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; -using TeamTools.TSQL.ExpressionEvaluator.Values; namespace TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator { @@ -11,7 +10,6 @@ namespace TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator public sealed class DateNameTests : BaseMockFunctionTest { private DateName func; - private SqlValue dt; private DatePartArgument datePart; [SetUp] @@ -20,15 +18,13 @@ public override void SetUp() base.SetUp(); func = new DateName(); - // TODO : support real dates - dt = MakeStr("dummy"); datePart = new DatePartArgument("HOUR"); } [Test] - public void Test_DatePart_ReturnsApproximateRange() + public void Test_DateName_ReturnsApproximateRange() { - var res = func.Evaluate(ArgFactory.MakeList(datePart, new ValueArgument(dt)), Context); + var res = func.Evaluate(ArgFactory.MakeList(datePart, new ValueArgument(MakeDateTime(null))), Context); Assert.That(res, Is.Not.Null); Assert.That(res.IsPreciseValue, Is.False); @@ -36,5 +32,16 @@ public void Test_DatePart_ReturnsApproximateRange() // Hour value 0-23 is 2 symbols max long Assert.That((res as SqlStrTypeValue).EstimatedSize, Is.EqualTo(2)); } + + [Test] + public void Test_DateName_ReturnsSpecificValue() + { + var res = func.Evaluate(ArgFactory.MakeList(datePart, new ValueArgument(MakeDateTime("2010-05-31 12:30:00"))), Context); + + Assert.That(res, Is.Not.Null); + Assert.That(res.IsPreciseValue, Is.True); + Assert.That(res, Is.InstanceOf()); + Assert.That((res as SqlStrTypeValue).Value, Is.EqualTo("12")); + } } } diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/DatePartExtractorTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/DatePartExtractorTests.cs new file mode 100644 index 00000000..1dba941d --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/DatePartExtractorTests.cs @@ -0,0 +1,59 @@ +using NUnit.Framework; +using System; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.DateFunctions; +using DatePart = TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentDto.DatePartEnum; + +namespace TeamTools.TSQL.ExpressionEvaluatorTests.Tests.Functions.DateFunctions +{ + [Category("TeamTools.TSQL.ExpressionEvaluator.EvalFunctions")] + [TestOf(typeof(DatePartExtractor))] + internal class DatePartExtractorTests + { + private readonly DateTime dateValue = new System.DateTime(2005, 7, 31, 12, 30, 55, 779); + + [Test] + public void Test_DatePartExtractor_ReturnsZeroOnZeroDate() + { + int datePart = 0; + + Assert.That(DatePartExtractor.ExtractDatePartFromSpecificDate(DateTime.MinValue, DatePart.Year, out datePart), Is.True); + Assert.That(datePart, Is.EqualTo(0)); + + Assert.That(DatePartExtractor.ExtractDatePartFromSpecificDate(DateTime.MinValue, DatePart.Day, out datePart), Is.True); + Assert.That(datePart, Is.EqualTo(0)); + } + + [Test] + public void Test_DatePartExtractor_SupportsDateParts() + { + int datePart = 0; + + Assert.That(DatePartExtractor.ExtractDatePartFromSpecificDate(dateValue, DatePart.Year, out datePart), Is.True); + Assert.That(datePart, Is.EqualTo(2005)); + + Assert.That(DatePartExtractor.ExtractDatePartFromSpecificDate(dateValue, DatePart.Quarter, out datePart), Is.True); + Assert.That(datePart, Is.EqualTo(3)); + + Assert.That(DatePartExtractor.ExtractDatePartFromSpecificDate(dateValue, DatePart.Month, out datePart), Is.True); + Assert.That(datePart, Is.EqualTo(7)); + + Assert.That(DatePartExtractor.ExtractDatePartFromSpecificDate(dateValue, DatePart.Day, out datePart), Is.True); + Assert.That(datePart, Is.EqualTo(31)); + + Assert.That(DatePartExtractor.ExtractDatePartFromSpecificDate(dateValue, DatePart.DayOfYear, out datePart), Is.True); + Assert.That(datePart, Is.EqualTo(212)); + + Assert.That(DatePartExtractor.ExtractDatePartFromSpecificDate(dateValue, DatePart.Week, out datePart), Is.True); + Assert.That(datePart, Is.EqualTo(31)); + + Assert.That(DatePartExtractor.ExtractDatePartFromSpecificDate(dateValue, DatePart.Hour, out datePart), Is.True); + Assert.That(datePart, Is.EqualTo(12)); + + Assert.That(DatePartExtractor.ExtractDatePartFromSpecificDate(dateValue, DatePart.Minute, out datePart), Is.True); + Assert.That(datePart, Is.EqualTo(30)); + + Assert.That(DatePartExtractor.ExtractDatePartFromSpecificDate(dateValue, DatePart.Second, out datePart), Is.True); + Assert.That(datePart, Is.EqualTo(55)); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/DatePartTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/DatePartTests.cs index 0431139c..9d75aa34 100644 --- a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/DatePartTests.cs +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/DatePartTests.cs @@ -2,7 +2,6 @@ using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentDto; using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.DateFunctions; using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; -using TeamTools.TSQL.ExpressionEvaluator.Values; namespace TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator { @@ -12,7 +11,6 @@ public sealed class DatePartTests : BaseMockFunctionTest { private DatePart func; private SqlFunctionArgument datePart; - private SqlValue dt; [SetUp] public override void SetUp() @@ -20,21 +18,34 @@ public override void SetUp() base.SetUp(); func = new DatePart(); - // TODO : support real dates - dt = MakeStr("dummy"); datePart = new DatePartArgument("DAY"); } [Test] public void Test_DatePart_ReturnsApproximateRange() { - var res = func.Evaluate(ArgFactory.MakeList(datePart, new ValueArgument(dt)), Context); + var res = func.Evaluate(ArgFactory.MakeList(datePart, new ValueArgument(MakeDateTime(null))), Context); Assert.That(res, Is.Not.Null); Assert.That(res.IsPreciseValue, Is.False); Assert.That(res, Is.InstanceOf()); - Assert.That((res as SqlIntTypeValue).EstimatedSize.Low, Is.EqualTo(1)); - Assert.That((res as SqlIntTypeValue).EstimatedSize.High, Is.EqualTo(31)); + + var intResRange = ((SqlIntTypeValue)res).EstimatedSize; + Assert.That(intResRange.Low, Is.EqualTo(1)); + Assert.That(intResRange.High, Is.EqualTo(31)); + } + + [Test] + public void Test_DatePart_ReturnsPreciseAnswer() + { + var res = func.Evaluate(ArgFactory.MakeList(datePart, new ValueArgument(MakeDateTime("2025-11-06"))), Context); + + Assert.That(res, Is.Not.Null); + Assert.That(res.IsPreciseValue, Is.True); + Assert.That(res, Is.InstanceOf()); + + var intRes = ((SqlIntTypeValue)res).Value; + Assert.That(intRes, Is.EqualTo(6)); } } } diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/DateTimeFromPartsTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/DateTimeFromPartsTests.cs new file mode 100644 index 00000000..d6fc0025 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/DateTimeFromPartsTests.cs @@ -0,0 +1,52 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.ArgumentDto; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.DateFunctions; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator; + +namespace TeamTools.TSQL.ExpressionEvaluatorTests.Tests.Functions.DateFunctions +{ + [Category("TeamTools.TSQL.ExpressionEvaluator.EvalFunctions")] + [TestOf(typeof(DateTimeFromParts))] + public class DateTimeFromPartsTests : BaseMockFunctionTest + { + private DateTimeFromParts func; + private List funcArgs; + private SqlDateTimeValue dt; + + [SetUp] + public override void SetUp() + { + base.SetUp(); + + func = new DateTimeFromParts(); + funcArgs = ArgFactory.MakeListOfValues( + MakeInt(2010), + MakeInt(7), + MakeInt(22), + MakeInt(12), + MakeInt(30), + MakeInt(55), + MakeInt(777)); + } + + [Test] + public void Test_DateTimeFromParts_ReturnsPreciseValue() + { + var res = func.Evaluate(funcArgs, Context); + + Assert.That(res, Is.Not.Null); + Assert.That(res.IsPreciseValue, Is.True); + Assert.That(res, Is.InstanceOf()); + + var result = ((SqlDateTimeValue)res).Value; + Assert.That(result.Year, Is.EqualTo(2010)); + Assert.That(result.Month, Is.EqualTo(7)); + Assert.That(result.Day, Is.EqualTo(22)); + Assert.That(result.Hour, Is.EqualTo(12)); + Assert.That(result.Minute, Is.EqualTo(30)); + Assert.That(result.Second, Is.EqualTo(55)); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/EndOfMonthTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/EndOfMonthTests.cs new file mode 100644 index 00000000..eb2e66c6 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/EndOfMonthTests.cs @@ -0,0 +1,56 @@ +using NUnit.Framework; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.DateFunctions; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator; + +namespace TeamTools.TSQL.ExpressionEvaluatorTests.Tests.Functions.DateFunctions +{ + [Category("TeamTools.TSQL.ExpressionEvaluator.EvalFunctions")] + [TestOf(typeof(EndOfMonth))] + public sealed class EndOfMonthTests : BaseMockFunctionTest + { + private EndOfMonth func; + + [SetUp] + public override void SetUp() + { + base.SetUp(); + + func = new EndOfMonth(); + } + + [Test] + public void Test_EndOfMonth_ReturnsApproximateRange() + { + var res = func.Evaluate(ArgFactory.MakeListOfValues(MakeDateTime(null)), Context); + + Assert.That(res, Is.Not.Null); + Assert.That(res.IsPreciseValue, Is.False); + Assert.That(res, Is.InstanceOf()); + } + + [Test] + public void Test_EndOfMonth_ReturnsSpecificValueIfSourceIsPrecise() + { + var res = func.Evaluate(ArgFactory.MakeListOfValues(MakeDateTime("2010-05-01")), Context); + + Assert.That(res, Is.Not.Null); + Assert.That(res.IsPreciseValue, Is.True); + Assert.That(res, Is.InstanceOf()); + Assert.That((res as SqlDateOnlyValue).Value.Month, Is.EqualTo(5)); + Assert.That((res as SqlDateOnlyValue).Value.Day, Is.EqualTo(31)); + } + + [Test] + public void Test_EndOfMonth_ReturnsRespectsLeapYear() + { + var res = func.Evaluate(ArgFactory.MakeListOfValues(MakeDateTime("2000-02-22")), Context); + + Assert.That(res, Is.Not.Null); + Assert.That(res.IsPreciseValue, Is.True); + Assert.That(res, Is.InstanceOf()); + Assert.That((res as SqlDateOnlyValue).Value.Month, Is.EqualTo(2)); + Assert.That((res as SqlDateOnlyValue).Value.Day, Is.EqualTo(29)); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/MonthTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/MonthTests.cs index 14951a28..1ee1b982 100644 --- a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/MonthTests.cs +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/DateFunctions/MonthTests.cs @@ -1,7 +1,6 @@ using NUnit.Framework; using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.DateFunctions; using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; -using TeamTools.TSQL.ExpressionEvaluator.Values; namespace TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator { @@ -10,7 +9,6 @@ namespace TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator public sealed class MonthTests : BaseMockFunctionTest { private Month func; - private SqlValue dt; [SetUp] public override void SetUp() @@ -18,14 +16,12 @@ public override void SetUp() base.SetUp(); func = new Month(); - // TODO : support real dates - dt = MakeStr("dummy"); } [Test] public void Test_Month_ReturnsApproximateRange() { - var res = func.Evaluate(ArgFactory.MakeListOfValues(dt), Context); + var res = func.Evaluate(ArgFactory.MakeListOfValues(MakeDateTime(null)), Context); Assert.That(res, Is.Not.Null); Assert.That(res.IsPreciseValue, Is.False); @@ -33,5 +29,16 @@ public void Test_Month_ReturnsApproximateRange() Assert.That((res as SqlIntTypeValue).EstimatedSize.Low, Is.EqualTo(1)); Assert.That((res as SqlIntTypeValue).EstimatedSize.High, Is.EqualTo(12)); } + + [Test] + public void Test_Month_ReturnsSpecificValueIfSourceIsPrecise() + { + var res = func.Evaluate(ArgFactory.MakeListOfValues(MakeDateTime("2010-05-31")), Context); + + Assert.That(res, Is.Not.Null); + Assert.That(res.IsPreciseValue, Is.True); + Assert.That(res, Is.InstanceOf()); + Assert.That((res as SqlIntTypeValue).Value, Is.EqualTo(5)); + } } } diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/MathFunctions/CeilingTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/MathFunctions/CeilingTests.cs new file mode 100644 index 00000000..bb3f2d7a --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/MathFunctions/CeilingTests.cs @@ -0,0 +1,47 @@ +using NUnit.Framework; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.MathFunctions; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; + +namespace TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator +{ + [Category("TeamTools.TSQL.ExpressionEvaluator.EvalFunctions")] + [TestOf(typeof(Round))] + public sealed class CeilingTests : BaseMockFunctionTest + { + private Ceiling func; + private SqlDecimalTypeValue value; + + [SetUp] + public override void SetUp() + { + base.SetUp(); + + func = new Ceiling(); + value = MakeDecimal(123.51629m); + } + + [Test] + public void Test_Ceiling_ForDecimal() + { + var result = func.Evaluate(ArgFactory.MakeListOfValues(value), Context); + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.InstanceOf()); + + var number = (SqlDecimalTypeValue)result; + Assert.That(number.IsPreciseValue, Is.True); + Assert.That(number.Value, Is.EqualTo(124m)); + } + + [Test] + public void Test_Ceiling_ForInt() + { + var result = func.Evaluate(ArgFactory.MakeListOfValues(MakeInt(123)), Context); + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.InstanceOf()); + + var number = (SqlDecimalTypeValue)result; + Assert.That(number.IsPreciseValue, Is.True); + Assert.That(number.Value, Is.EqualTo(123)); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/MathFunctions/RoundTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/MathFunctions/RoundTests.cs new file mode 100644 index 00000000..795ccce6 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/MathFunctions/RoundTests.cs @@ -0,0 +1,68 @@ +using NUnit.Framework; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.MathFunctions; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator +{ + [Category("TeamTools.TSQL.ExpressionEvaluator.EvalFunctions")] + [TestOf(typeof(Round))] + public sealed class RoundTests : BaseMockFunctionTest + { + private Round func; + private SqlDecimalTypeValue value; + + [SetUp] + public override void SetUp() + { + base.SetUp(); + + func = new Round(); + value = MakeDecimal(123.51629m); + } + + [Test] + public void Test_Round_Fractions() + { + var result = func.Evaluate(ArgFactory.MakeListOfValues(value, MakeInt(-2)), Context); + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.InstanceOf()); + + var number = (SqlDecimalTypeValue)result; + Assert.That(number.IsPreciseValue, Is.True); + Assert.That(number.Value, Is.EqualTo(100m)); + } + + [Test] + public void Test_Round_Natural() + { + var result = func.Evaluate(ArgFactory.MakeListOfValues(value, MakeInt(2)), Context); + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.InstanceOf()); + + var number = (SqlDecimalTypeValue)result; + Assert.That(number.IsPreciseValue, Is.True); + Assert.That(number.Value, Is.EqualTo(123.52m)); + } + + [Test] + public void Test_Round_ForInt() + { + var result = func.Evaluate(ArgFactory.MakeListOfValues(MakeInt(100), MakeInt(2)), Context); + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.InstanceOf()); + + var number = (SqlDecimalTypeValue)result; + Assert.That(number.IsPreciseValue, Is.True); + Assert.That(number.Value, Is.EqualTo(100)); + } + + [Test] + public void Test_Round_ForUnknownLength() + { + var result = func.Evaluate(ArgFactory.MakeListOfValues(value, TypeHandler.ValueFactory.NewValue(value.TypeReference, SqlValueKind.Unknown)), Context); + Assert.That(result, Is.Not.Null); + Assert.That(result.IsPreciseValue, Is.False); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/SysFunctions/CursorStatusTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/SysFunctions/CursorStatusTests.cs new file mode 100644 index 00000000..f94ec948 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/SysFunctions/CursorStatusTests.cs @@ -0,0 +1,56 @@ +using NUnit.Framework; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.SysFunctions; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; + +namespace TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator +{ + [Category("TeamTools.TSQL.ExpressionEvaluator.EvalFunctions")] + [TestOf(typeof(CursorStatus))] + public sealed class CursorStatusTests : BaseMockFunctionTest + { + private CursorStatus func; + + [SetUp] + public override void SetUp() + { + base.SetUp(); + + func = new CursorStatus(); + } + + [Test] + public void Test_CursorStatus_ReturnsNullOnNullArgs() + { + var res = func.Evaluate(ArgFactory.MakeListOfValues(Factory.NewNull(default), MakeStr("cr")), Context); + + Assert.That(res, Is.Not.Null); + Assert.That(res.IsNull, Is.False, "null scope"); + Assert.That(Violations.ViolationCount, Is.EqualTo(1)); + + res = func.Evaluate(ArgFactory.MakeListOfValues(MakeStr("local"), Factory.NewNull(default)), Context); + + Assert.That(res, Is.Not.Null); + Assert.That(res.IsNull, Is.False, "null name"); + Assert.That(Violations.ViolationCount, Is.EqualTo(2)); + } + + [Test] + public void Test_CursorStatus_BadScopeIsReported() + { + var res = func.Evaluate(ArgFactory.MakeListOfValues(MakeStr("unknown"), MakeStr("cursor_name")), Context); + + Assert.That(res, Is.Not.Null); + Assert.That(Violations.ViolationCount, Is.EqualTo(1)); + } + + [Test] + public void Test_CursorStatus_GoodScopeIsNotReported() + { + var res = func.Evaluate(ArgFactory.MakeListOfValues(MakeStr("local"), MakeStr("cursor_name")), Context); + + Assert.That(res, Is.Not.Null); + Assert.That(Violations.ViolationCount, Is.Zero); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/XQuery/XQueryValueTest.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/XQuery/XQueryValueTest.cs new file mode 100644 index 00000000..7b54c3a7 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Functions/XQuery/XQueryValueTest.cs @@ -0,0 +1,63 @@ +using NUnit.Framework; +using System; +using TeamTools.TSQL.ExpressionEvaluator.BuiltInFunctions.XQueryFunctions; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; + +namespace TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator +{ + [Category("TeamTools.TSQL.ExpressionEvaluator.EvalFunctions")] + [TestOf(typeof(XQueryValue))] + public sealed class XQueryValueTest : BaseObjNameTest + { + private XQueryValue func; + + [SetUp] + public override void SetUp() + { + base.SetUp(); + + func = new XQueryValue(); + } + + [Test] + public void Test_XQueryValue_ReturnsPreciseType() + { + var res = func.Evaluate(ArgFactory.MakeListOfValues(MakeStr("."), MakeStr("SMALLINT")), Context); + + Assert.That(res, Is.Not.Null); + Assert.That(res.IsNull, Is.False); + Assert.That(res.IsPreciseValue, Is.False); + Assert.That(res.TypeReference.TypeName, Is.EqualTo("SMALLINT").IgnoreCase); + } + + [Test] + public void Test_XQueryValue_ReturnsPreciseTypeWithLength() + { + var res = func.Evaluate(ArgFactory.MakeListOfValues(MakeStr("."), MakeStr("varchar ( 21 )")), Context); + + Assert.That(res, Is.Not.Null); + Assert.That(res.IsNull, Is.False); + Assert.That(res.IsPreciseValue, Is.False); + Assert.That(res.TypeReference.TypeName, Is.EqualTo("VARCHAR").IgnoreCase); + } + + [Test] + public void Test_XQueryValue_ReturnsPreciseTypeWithParams() + { + var res = func.Evaluate(ArgFactory.MakeListOfValues(MakeStr("."), MakeStr("DECIMAL(12,11)")), Context); + + Assert.That(res, Is.Not.Null); + Assert.That(res.IsNull, Is.False); + Assert.That(res.IsPreciseValue, Is.False); + Assert.That(res.TypeReference.TypeName, Is.EqualTo("DECIMAL").IgnoreCase); + } + + [Test] + public void Test_XQueryValue_ReturnsUnknown() + { + var res = func.Evaluate(ArgFactory.MakeListOfValues(MakeStr("asdf"), MakeStr("asdf")), Context); + + Assert.That(res, Is.Null); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Mocks/MockSqlTypeConverter.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Mocks/MockSqlTypeConverter.cs index 71daaa03..15fdc979 100644 --- a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Mocks/MockSqlTypeConverter.cs +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Mocks/MockSqlTypeConverter.cs @@ -39,6 +39,31 @@ public override T ImplicitlyConvert(SqlValue from) return (T)GenerateFromMock("INT", mv); } + + if (typeof(T) == typeof(SqlDateOnlyValue)) + { + if (from.TypeName.EndsWith("TIME") || from.TypeName.EndsWith("DATE")) + { + return (T)GenerateFromMock(from.TypeReference.TypeName, mv); + } + + return (T)GenerateFromMock("DATE", mv); + } + + if (typeof(T) == typeof(SqlDateTimeValue)) + { + if (from.TypeName.StartsWith("DATETIME")) + { + return (T)GenerateFromMock(from.TypeReference.TypeName, mv); + } + + return (T)GenerateFromMock("DATETIME", mv); + } + + if (typeof(T) == typeof(SqlDecimalTypeValue)) + { + return (T)GenerateFromMock("DECIMAL", mv); + } } return base.ImplicitlyConvert(from); @@ -129,6 +154,110 @@ private SqlValue GenerateFromMock(string typeName, MockSqlValue from) SqlValueKind.Unknown, from.Source); } + else if (typeName.Equals("DATE")) + { + if (from.IsNull) + { + return new SqlDateOnlyValue( + new SqlDateTypeHandler(this, new ViolationReporter()), + new SqlDateTimeTypeReference(typeName, new SqlDateTimeValueRange(DateTime.Today), mockTypeHandler.ValueFactory), + SqlValueKind.Null, + from.Source); + } + + if (from.IsPreciseValue) + { + return new SqlDateOnlyValue( + new SqlDateTypeHandler(this, new ViolationReporter()), + new SqlDateTimeTypeReference(typeName, new SqlDateTimeValueRange(from.DateTimeValue), mockTypeHandler.ValueFactory), + from.DateTimeValue, + from.Source); + } + + return new SqlDateOnlyValue( + new SqlDateTypeHandler(this, new ViolationReporter()), + new SqlDateTimeTypeReference(typeName, new SqlDateTimeValueRange(from.DateTimeValue), mockTypeHandler.ValueFactory), + SqlValueKind.Unknown, + from.Source); + } + else if (typeName.StartsWith("DATE") || typeName.StartsWith("SMALLDATE")) + { + if (from.IsNull) + { + return new SqlDateTimeValue( + new SqlDateTimeTypeHandler(this, new ViolationReporter()), + new SqlDateTimeTypeReference(typeName, new SqlDateTimeValueRange(DateTime.Today), mockTypeHandler.ValueFactory), + SqlValueKind.Null, + from.Source); + } + + if (from.IsPreciseValue) + { + return new SqlDateTimeValue( + new SqlDateTimeTypeHandler(this, new ViolationReporter()), + new SqlDateTimeTypeReference(typeName, new SqlDateTimeValueRange(from.DateTimeValue), mockTypeHandler.ValueFactory), + from.DateTimeValue, + from.Source); + } + + return new SqlDateTimeValue( + new SqlDateTimeTypeHandler(this, new ViolationReporter()), + new SqlDateTimeTypeReference(typeName, new SqlDateTimeValueRange(from.DateTimeValue), mockTypeHandler.ValueFactory), + SqlValueKind.Unknown, + from.Source); + } + else if (typeName.Equals("TIME")) + { + if (from.IsNull) + { + return new SqlTimeOnlyValue( + new SqlTimeTypeHandler(this, new ViolationReporter()), + new SqlDateTimeTypeReference(typeName, new SqlDateTimeValueRange(DateTime.Now), mockTypeHandler.ValueFactory), + SqlValueKind.Null, + from.Source); + } + + if (from.IsPreciseValue) + { + return new SqlTimeOnlyValue( + new SqlTimeTypeHandler(this, new ViolationReporter()), + new SqlDateTimeTypeReference(typeName, new SqlDateTimeValueRange(from.DateTimeValue), mockTypeHandler.ValueFactory), + from.DateTimeValue.TimeOfDay, + from.Source); + } + + return new SqlTimeOnlyValue( + new SqlTimeTypeHandler(this, new ViolationReporter()), + new SqlDateTimeTypeReference(typeName, new SqlDateTimeValueRange(from.DateTimeValue), mockTypeHandler.ValueFactory), + SqlValueKind.Unknown, + from.Source); + } + else if (typeName.Equals("DECIMAL")) + { + if (from.IsNull) + { + return new SqlDecimalTypeValue( + new SqlDecimalTypeHandler(this, new ViolationReporter()), + new SqlDecimalTypeReference(typeName, new SqlDecimalValueRange(decimal.MinValue, decimal.MaxValue, 18, 0), mockTypeHandler.ValueFactory), + SqlValueKind.Null, + from.Source); + } + + if (from.IsPreciseValue) + { + return new SqlDecimalTypeValue( + new SqlDecimalTypeHandler(this, new ViolationReporter()), + new SqlDecimalTypeReference(typeName, new SqlDecimalValueRange(from.DecimalValue, from.DecimalValue, 38, 18), mockTypeHandler.ValueFactory), + from.DecimalValue, + from.Source); + } + + return new SqlDecimalTypeValue( + new SqlDecimalTypeHandler(this, new ViolationReporter()), + new SqlDecimalTypeReference(typeName, new SqlDecimalValueRange(decimal.MinValue, decimal.MaxValue, 18, 0), mockTypeHandler.ValueFactory), + SqlValueKind.Unknown, + from.Source); + } return new SqlStrTypeValue( new SqlStrTypeHandler(this, new ViolationReporter()), diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Mocks/MockSqlTypeReference.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Mocks/MockSqlTypeReference.cs index 46fd565f..f7dec54e 100644 --- a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Mocks/MockSqlTypeReference.cs +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Mocks/MockSqlTypeReference.cs @@ -13,5 +13,7 @@ public MockSqlTypeReference(string typeName, ISqlValueFactory valueFactory, int public int Dummy { get; } public override int CompareTo(SqlTypeReference other) => 0; + + protected override int GetBytes() => 4; } } diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Mocks/MockSqlValue.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Mocks/MockSqlValue.cs index b93c7995..b2fc5ade 100644 --- a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Mocks/MockSqlValue.cs +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Mocks/MockSqlValue.cs @@ -27,6 +27,10 @@ public MockSqlValue(SqlTypeReference typeRef, SqlValueKind valueKind, SqlValueSo public string StrValue { get; set; } = ""; + public System.DateTime DateTimeValue { get; set; } = System.DateTime.MinValue; + + public decimal DecimalValue { get; set; } = 0; + public override ISqlTypeHandler GetTypeHandler() => typeHandler; public override SqlTypeReference GetTypeReference() => typeRef; diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Mocks/MockTypes.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Mocks/MockTypes.cs index ac289856..dabaaa40 100644 --- a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Mocks/MockTypes.cs +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Mocks/MockTypes.cs @@ -15,6 +15,10 @@ public static class MockTypes "INT", "SMALLINT", "TINYINT", + "DATETIME", + "DATE", + "TIME", + "DECIMAL", "DUMMY", }; diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Mocks/MockValueFactory.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Mocks/MockValueFactory.cs index 5a062898..0959525e 100644 --- a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Mocks/MockValueFactory.cs +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/Mocks/MockValueFactory.cs @@ -1,4 +1,5 @@ using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; using TeamTools.TSQL.ExpressionEvaluator.Interfaces; using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; using TeamTools.TSQL.ExpressionEvaluator.Values; @@ -32,6 +33,14 @@ public SqlValue NewLiteral(string typeName, string value, TSqlFragment source) { literalValue.IntValue = int.Parse(value); } + else if (typeName == "DECIMAL") + { + literalValue.DecimalValue = decimal.Parse(value); + } + else if (typeName.StartsWith("DATE") || typeName.EndsWith("TIME")) + { + literalValue.DateTimeValue = DateTime.Parse(value); + } else { literalValue.StrValue = value; diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/BaseSqlTypeHandlerTestClass.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/BaseSqlTypeHandlerTestClass.cs index 9f00f358..b69d660e 100644 --- a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/BaseSqlTypeHandlerTestClass.cs +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/BaseSqlTypeHandlerTestClass.cs @@ -1,10 +1,13 @@ using TeamTools.TSQL.ExpressionEvaluator.Core; using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; namespace TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator.TypeHandling { public abstract class BaseSqlTypeHandlerTestClass { + private SqlStrTypeHandler strTypeHandler; + protected ViolationReporter Violations { get; private set; } protected SqlTypeResolver TypeResolver { get; private set; } @@ -16,6 +19,12 @@ public virtual void SetUp() Violations = new ViolationReporter(); TypeResolver = new SqlTypeResolver(); Converter = new SqlTypeConverter(TypeResolver); + strTypeHandler = new SqlStrTypeHandler(Converter, Violations); + } + + protected SqlValue MakeStr(string str) + { + return strTypeHandler.StrValueFactory.MakePreciseValue("VARCHAR", str, new SqlValueSource(SqlValueSourceKind.Variable, null)); } } } diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlBigIntTypeHandlerTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/BigInt/SqlBigIntTypeHandlerTests.cs similarity index 100% rename from TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlBigIntTypeHandlerTests.cs rename to TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/BigInt/SqlBigIntTypeHandlerTests.cs diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlBigIntTypeOperatorsTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/BigInt/SqlBigIntTypeOperatorsTests.cs similarity index 100% rename from TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlBigIntTypeOperatorsTests.cs rename to TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/BigInt/SqlBigIntTypeOperatorsTests.cs diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlBigIntTypeReferenceTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/BigInt/SqlBigIntTypeReferenceTests.cs similarity index 100% rename from TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlBigIntTypeReferenceTests.cs rename to TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/BigInt/SqlBigIntTypeReferenceTests.cs diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlBigIntTypeValueFactoryTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/BigInt/SqlBigIntTypeValueFactoryTests.cs similarity index 100% rename from TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlBigIntTypeValueFactoryTests.cs rename to TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/BigInt/SqlBigIntTypeValueFactoryTests.cs diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlBigIntTypeValueTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/BigInt/SqlBigIntTypeValueTests.cs similarity index 81% rename from TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlBigIntTypeValueTests.cs rename to TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/BigInt/SqlBigIntTypeValueTests.cs index 3b11f7df..e88bdb02 100644 --- a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlBigIntTypeValueTests.cs +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/BigInt/SqlBigIntTypeValueTests.cs @@ -62,5 +62,19 @@ public void Test_SqlBigIntTypeValue_GetHandlerReturnsExpectedValue() Assert.That(value, Is.Not.Null); Assert.That(value.GetTypeHandler(), Is.EqualTo(typeHandler)); } + + [Test] + public void Test_SqlBigIntTypeValue_DeepCloneCopiesPreciseValue() + { + var value = ValueFactory.MakePreciseValue("BIGINT", -5, new SqlValueSource(SqlValueSourceKind.Literal, null)); + Assert.That(value, Is.Not.Null); + Assert.That(value.Value, Is.EqualTo((BigInteger)(-5))); + + var clone = value.DeepClone(); + Assert.That(clone, Is.Not.Null); + Assert.That(clone.IsPreciseValue, Is.True); + Assert.That(clone.Value, Is.EqualTo(value.Value)); + Assert.That(clone.TypeName, Is.EqualTo("BIGINT")); + } } } diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlBigIntValueRangeTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/BigInt/SqlBigIntValueRangeTests.cs similarity index 100% rename from TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlBigIntValueRangeTests.cs rename to TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/BigInt/SqlBigIntValueRangeTests.cs diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Binary/HexValueTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Binary/HexValueTests.cs new file mode 100644 index 00000000..f1acc719 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Binary/HexValueTests.cs @@ -0,0 +1,195 @@ +using NUnit.Framework; +using System.Numerics; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; + +namespace TeamTools.TSQL.ExpressionEvaluatorTests.Tests.TypeHandling.Binary +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Assertion", "NUnit2010:Use EqualConstraint for better assertion messages in case of failure", Justification = "For self comparison test")] + [Category("TeamTools.TSQL.ExpressionEvaluator")] + [TestOf(typeof(HexValue))] + internal class HexValueTests + { + [Test] + public void Test_HexValue_MakeFromString() + { + var a = new HexValue("1001"); + Assert.That(a.AsNumber, Is.EqualTo((BigInteger)4097)); + Assert.That(a.AsString, Is.EqualTo("1001")); + } + + [Test] + public void Test_HexValue_MakeFromInt() + { + var a = new HexValue(1); + Assert.That(a.AsNumber, Is.EqualTo((BigInteger)1)); + Assert.That(a.AsString, Is.EqualTo("01")); + + a = new HexValue(100); + Assert.That(a.AsNumber, Is.EqualTo((BigInteger)100)); + Assert.That(a.AsString, Is.EqualTo("64")); + + a = new HexValue(1000); + Assert.That(a.AsNumber, Is.EqualTo((BigInteger)1000)); + Assert.That(a.AsString, Is.EqualTo("03E8")); + } + + [Test] + public void Test_HexValue_MakeFromBigInt() + { + var a = new HexValue((BigInteger)33); + Assert.That(a.AsNumber, Is.EqualTo((BigInteger)33)); + Assert.That(a.AsString, Is.EqualTo("21")); + } + + [Test] + public void Test_HexValue_MakeFromBadStringDoesNotFail() + { + Assert.DoesNotThrow(() => new HexValue("0xZX new HexValue("0x")); + var v = new HexValue("0x"); + Assert.That(v, Is.Not.Null); + Assert.That(v.AsNumber, Is.EqualTo((BigInteger)0)); + } + + [Test] + public void Test_HexValue_ChangeAsNumber() + { + var a = new HexValue(31); + Assert.That(a.AsNumber, Is.EqualTo((BigInteger)31)); + Assert.That(a.AsString, Is.EqualTo("1F")); + + a.AsNumber = 242; + + Assert.That(a.AsNumber, Is.EqualTo((BigInteger)242)); + Assert.That(a.AsString, Is.EqualTo("F2")); + } + + [Test] + public void Test_HexValue_ChangeAsString() + { + var a = new HexValue(1); + Assert.That(a.AsNumber, Is.EqualTo((BigInteger)1)); + Assert.That(a.AsString, Is.EqualTo("01")); + + a.AsString = "1F1"; + + Assert.That(a.AsString, Is.EqualTo("01F1")); + Assert.That(a.AsNumber, Is.EqualTo((BigInteger)497)); + } + + [Test] + public void Test_HexValue_ConcatBasedOnStr() + { + var a = new HexValue("100"); + var b = new HexValue("200"); + var c = a + b; + + Assert.That(c, Is.Not.Null); + Assert.That(c.AsString, Is.EqualTo("01000200")); + Assert.That(c.AsNumber, Is.EqualTo((BigInteger)16777728)); + } + + [Test] + public void Test_HexValue_ConcatBasedOnInt() + { + var a = new HexValue(100); + Assert.That(a.AsString, Is.EqualTo("64")); + var b = new HexValue(200); + Assert.That(b.AsString, Is.EqualTo("C8")); + var c = a + b; + + Assert.That(c, Is.Not.Null); + Assert.That(c.AsString, Is.EqualTo("64C8")); + Assert.That(c.AsNumber, Is.EqualTo((BigInteger)25800)); + } + + [Test] + public void Test_HexValue_MinLengthPrependsZeroes() + { + var a = new HexValue(4097, 5); + Assert.That(a.AsNumber, Is.EqualTo((BigInteger)4097)); + Assert.That(a.AsString, Is.EqualTo("0000001001")); + + var b = new HexValue("0xFF", 5); + Assert.That(b.AsNumber, Is.EqualTo((BigInteger)255)); + Assert.That(b.AsString, Is.EqualTo("00000000FF")); + } + + [Test] + public void Test_HexValue_ToStringPrependsShebang() + { + var a = new HexValue(1); + Assert.That(a.ToString(), Is.EqualTo("0x01")); + } + + [Test] + public void Test_HexValue_Equals() + { + var a = new HexValue(1); + var b = new HexValue(1); + var c = new HexValue(0); + + Assert.That(a.Equals(a), Is.True); + Assert.That(a.Equals((object)b), Is.True); + Assert.That(a == b, Is.True); + Assert.That(a, Is.EqualTo(b)); + Assert.That(a, Is.Not.EqualTo(c)); + Assert.That(a == c, Is.False); + Assert.That(a != c, Is.True); + Assert.That(a == null, Is.False); + Assert.That(a != null, Is.True); + Assert.That(a == (object)"", Is.False); + Assert.That(a.Equals(123), Is.False); + } + + [Test] + public void Test_HexValue_GreaterThan() + { + var a = new HexValue(1); + var b = new HexValue(1); + var c = new HexValue(0); + + Assert.That(a, Is.Not.GreaterThan(b)); + Assert.That(a, Is.GreaterThan(c)); + + Assert.That(a > b, Is.False); + Assert.That(a >= b, Is.True); + Assert.That(a > c, Is.True); + } + + [Test] + public void Test_HexValue_LessThan() + { + var a = new HexValue(1); + var b = new HexValue(1); + var c = new HexValue(0); + + Assert.That(a, Is.Not.LessThan(b)); + Assert.That(c, Is.LessThan(a)); + + Assert.That(a < b, Is.False); + Assert.That(a <= b, Is.True); + Assert.That(c < a, Is.True); + } + + [Test] + public void Test_HexValue_CompareTo() + { + var a = new HexValue(1); + var b = new HexValue(1); + var c = new HexValue(0); + + Assert.That(a.CompareTo(b), Is.Zero); + Assert.That(a.CompareTo(c), Is.EqualTo(1)); + Assert.That(c.CompareTo(a), Is.EqualTo(-1)); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Binary/SqlBinaryTypeHandlerTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Binary/SqlBinaryTypeHandlerTests.cs new file mode 100644 index 00000000..4b2dab87 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Binary/SqlBinaryTypeHandlerTests.cs @@ -0,0 +1,225 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using NUnit.Framework; +using System; +using System.Numerics; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; +using TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator.TypeHandling; + +namespace TeamTools.TSQL.ExpressionEvaluatorTests.Tests.TypeHandling.Binary +{ + [Category("TeamTools.TSQL.ExpressionEvaluator")] + [TestOf(typeof(SqlBinaryTypeHandler))] + public sealed class SqlBinaryTypeHandlerTests : BaseSqlTypeHandlerTestClass + { + private SqlBinaryTypeHandler typeHandler; + + private SqlBinaryTypeValueFactory ValueFactory => typeHandler.BinaryValueFactory; + + [OneTimeSetUp] + public override void SetUp() + { + base.SetUp(); + typeHandler = new SqlBinaryTypeHandler(Converter, Violations); + } + + [Test] + public void Test_SqlBinaryTypeHandler_CanConvertFromValidStr() + { + var date = typeHandler.TypeConverter.ImplicitlyConvert(typeHandler.ConvertFrom(MakeStr("0x1F"), "BINARY")); + + Assert.That(date, Is.Not.Null); + Assert.That(date.IsPreciseValue, Is.True); + Assert.That(date.Value, Is.EqualTo(new HexValue(31))); + } + + [Test] + public void Test_SqlBinaryTypeHandler_ChangeToAppliesProvidedValue() + { + var bin = typeHandler.TypeConverter.ImplicitlyConvert(typeHandler.ConvertFrom(MakeStr("0x1F"), "BINARY")); + + Assert.That(bin, Is.Not.Null); + + bin = bin.ChangeTo(new HexValue(42), new SqlValueSource(SqlValueSourceKind.Expression, null)); + Assert.That(bin.Value, Is.EqualTo(new HexValue(42))); + } + + [Test] + public void Test_SqlBinaryTypeHandler_MakeSqlDataTypeReferenceFromBrokenDefinition() + { + var typeDef = new SqlDataTypeReference(); // no initialization + try + { + var typeRef = typeHandler.MakeSqlDataTypeReference(typeDef); + Assert.That(typeRef, Is.Null); + } + catch (Exception e) + { + Assert.Fail(e.Message); + } + + try + { + var typeRef = typeHandler.MakeSqlDataTypeReference((DataTypeReference)null); + Assert.That(typeRef, Is.Null); + } + catch (Exception e) + { + Assert.Fail(e.Message); + } + } + + [Test] + public void Test_SqlBinaryTypeHandler_MakeSqlDataTypeReferenceFromDefinition() + { + var typeDef = new SqlDataTypeReference + { + Name = new SchemaObjectName(), + }; + + typeDef.Name.Identifiers.Add(new Identifier { Value = "BINARY" }); + typeDef.Parameters.Add(new IntegerLiteral { Value = "700" }); + + var typeRef = typeHandler.MakeSqlDataTypeReference(typeDef); + Assert.That(typeRef, Is.Not.Null); + Assert.That(typeRef.TypeName, Is.EqualTo("BINARY")); + Assert.That(typeRef, Is.InstanceOf(typeof(SqlBinaryTypeReference))); + Assert.That(((SqlBinaryTypeReference)typeRef).Size, Is.EqualTo(700)); + } + + [Test] + public void Test_SqlBinaryTypeHandler_MakeSqlDataTypeReferenceFromName() + { + var typeRef = typeHandler.MakeSqlDataTypeReference("VARBINARY"); + Assert.That(typeRef, Is.Not.Null); + Assert.That(typeRef.TypeName, Is.EqualTo("VARBINARY")); + Assert.That(typeRef, Is.InstanceOf(typeof(SqlBinaryTypeReference))); + Assert.That(((SqlBinaryTypeReference)typeRef).Size, Is.EqualTo(30)); + } + + [Test] + public void Test_SqlBinaryTypeHandler_ConcatWorksSimilarToStrings() + { + var a = new SqlBinaryTypeValue(typeHandler, (SqlBinaryTypeReference)typeHandler.MakeSqlDataTypeReference("VARBINARY"), new HexValue("10"), new SqlValueSource(SqlValueSourceKind.Literal, default)); + var b = new SqlBinaryTypeValue(typeHandler, (SqlBinaryTypeReference)typeHandler.MakeSqlDataTypeReference("VARBINARY"), new HexValue("20"), new SqlValueSource(SqlValueSourceKind.Literal, default)); + var c = typeHandler.Sum(a, b); + + Assert.That(c, Is.Not.Null); + Assert.That(c.TypeName, Is.EqualTo("VARBINARY")); + Assert.That(c, Is.InstanceOf(typeof(SqlBinaryTypeValue))); + + var bin = (SqlBinaryTypeValue)c; + Assert.That(bin.Value.AsString, Is.EqualTo("1020")); + Assert.That(bin.Value.AsNumber, Is.EqualTo((BigInteger)4128)); + } + + [Test] + public void Test_SqlBinaryTypeHandler_CanConvertFromInt() + { + var intHandler = new SqlIntTypeHandler(Converter, Violations); + var a = new SqlIntTypeValue(intHandler, new SqlIntTypeReference("INT", new SqlIntValueRange(1, 1), intHandler.IntValueFactory), 1, new SqlValueSource(SqlValueSourceKind.Literal, default)); + var b = typeHandler.ConvertFrom(a, "BINARY"); + + Assert.That(b, Is.Not.Null); + Assert.That(b.TypeName, Is.EqualTo("BINARY")); + Assert.That(b, Is.InstanceOf(typeof(SqlBinaryTypeValue))); + + var bin = (SqlBinaryTypeValue)b; + Assert.That(bin.Value.AsString, Is.EqualTo("01")); + Assert.That(bin.Value.AsNumber, Is.EqualTo((BigInteger)1)); + } + + [Test] + public void Test_SqlBinaryTypeHandler_ConvertionFromIntUsesOriginalByteLength() + { + var intHandler = new SqlIntTypeHandler(Converter, Violations); + var a = new SqlIntTypeValue(intHandler, new SqlIntTypeReference("INT", new SqlIntValueRange(1, 1), intHandler.IntValueFactory), 1, new SqlValueSource(SqlValueSourceKind.Literal, default)); + var b = typeHandler.ConvertFrom(a, "VARBINARY"); + + Assert.That(b, Is.Not.Null); + Assert.That(b.TypeName, Is.EqualTo("VARBINARY")); + Assert.That(b, Is.InstanceOf(typeof(SqlBinaryTypeValue))); + + var bin = (SqlBinaryTypeValue)b; + Assert.That(bin.Value.AsString, Is.EqualTo("01")); + Assert.That(bin.Value.AsNumber, Is.EqualTo((BigInteger)1)); + } + + [Test] + public void Test_SqlBinaryTypeHandler_ConvertionFromVariableToFixedLengthAppendsZeroes() + { + var src = (SqlBinaryTypeValue)ValueFactory.NewLiteral("VARBINARY", "0x00FF", default); + var fixedBin = typeHandler.ConvertValueFrom(src, ValueFactory.MakeSqlDataTypeReference("BINARY", 4), true); + var varBin = typeHandler.ConvertValueFrom(src, ValueFactory.MakeSqlDataTypeReference("VARBINARY", 4), true); + + // no additional padding + Assert.That(varBin, Is.Not.Null); + Assert.That(varBin.TypeName, Is.EqualTo("VARBINARY")); + Assert.That(varBin.Value.AsString, Is.EqualTo("00FF")); + + // additional tail of zeroes + Assert.That(fixedBin, Is.Not.Null); + Assert.That(fixedBin.TypeName, Is.EqualTo("BINARY")); + Assert.That(fixedBin.Value.AsString, Is.EqualTo("00FF0000")); + } + + [Test] + public void Test_SqlBinaryTypeHandler_ConvertionFromIntToFixedLengthPrependsZeroes() + { + var intHandler = new SqlIntTypeHandler(Converter, Violations); + var number = new SqlIntTypeValue(intHandler, new SqlIntTypeReference("INT", new SqlIntValueRange(1, 1), intHandler.IntValueFactory), 1, new SqlValueSource(SqlValueSourceKind.Literal, default)); + + var fixedBin = (SqlBinaryTypeValue)typeHandler.ConvertFrom(number, new SqlBinaryTypeReference("BINARY", 5, ValueFactory), true); + + Assert.That(fixedBin, Is.Not.Null); + Assert.That(fixedBin.Value.AsNumber, Is.EqualTo((BigInteger)1)); + Assert.That(fixedBin.Value.AsString, Is.EqualTo("0000000001")); + } + + [Test] + public void Test_SqlBinaryTypeHandler_ConvertionFromIntToVariableLengthPrependsZeroesBasedOnSourceTypeSize() + { + var intHandler = new SqlIntTypeHandler(Converter, Violations); + + var number = new SqlIntTypeValue(intHandler, new SqlIntTypeReference("INT", new SqlIntValueRange(1, 1), intHandler.IntValueFactory), 1, new SqlValueSource(SqlValueSourceKind.Literal, default)); + var fixedBin = (SqlBinaryTypeValue)typeHandler.ConvertFrom(number, new SqlBinaryTypeReference("VARBINARY", 100, ValueFactory), true); + + Assert.That(fixedBin, Is.Not.Null); + Assert.That(fixedBin.Value.AsNumber, Is.EqualTo((BigInteger)1)); + Assert.That(fixedBin.Value.AsString, Is.EqualTo("00000001")); // because INT is 4 bytes + + number = new SqlIntTypeValue(intHandler, new SqlIntTypeReference("SMALLINT", new SqlIntValueRange(1, 1), intHandler.IntValueFactory), 1, new SqlValueSource(SqlValueSourceKind.Literal, default)); + fixedBin = (SqlBinaryTypeValue)typeHandler.ConvertFrom(number, new SqlBinaryTypeReference("VARBINARY", 100, ValueFactory), true); + + Assert.That(fixedBin, Is.Not.Null); + Assert.That(fixedBin.Value.AsNumber, Is.EqualTo((BigInteger)1)); + Assert.That(fixedBin.Value.AsString, Is.EqualTo("0001")); // because SMALLINT is 2 bytes + } + + [Test] + public void Test_SqlBinaryTypeHandler_ConvertionFromIntToVariableLengthTruncatesFromStart() + { + var intHandler = new SqlIntTypeHandler(Converter, Violations); + + var number = new SqlIntTypeValue(intHandler, new SqlIntTypeReference("INT", new SqlIntValueRange(1, 1), intHandler.IntValueFactory), 1, new SqlValueSource(SqlValueSourceKind.Literal, default)); + var fixedBin = (SqlBinaryTypeValue)typeHandler.ConvertFrom(number, new SqlBinaryTypeReference("VARBINARY", 1, ValueFactory), true); + + Assert.That(fixedBin, Is.Not.Null); + Assert.That(fixedBin.Value.AsNumber, Is.EqualTo((BigInteger)1)); + Assert.That(fixedBin.Value.AsString, Is.EqualTo("01")); // nevertheless INT is 4 bytes, '1' is just one and can be stored in 1 byte + } + + [Test] + public void Test_SqlBinaryTypeHandler_ConvertionFromIntToFixedLengthTruncatesFromEnd() + { + var intHandler = new SqlIntTypeHandler(Converter, Violations); + + var number = new SqlIntTypeValue(intHandler, new SqlIntTypeReference("INT", new SqlIntValueRange(1, 1), intHandler.IntValueFactory), 43981, new SqlValueSource(SqlValueSourceKind.Literal, default)); + var fixedBin = (SqlBinaryTypeValue)typeHandler.ConvertFrom(number, new SqlBinaryTypeReference("BINARY", 1, ValueFactory), true); + + // 43981 == 0xABCD + Assert.That(fixedBin, Is.Not.Null); + Assert.That(fixedBin.Value.AsString, Is.EqualTo("AB")); // regular binary truncation cuts the end (just like strings) + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Binary/SqlBinaryTypeValueFactoryTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Binary/SqlBinaryTypeValueFactoryTests.cs new file mode 100644 index 00000000..74aae2e1 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Binary/SqlBinaryTypeValueFactoryTests.cs @@ -0,0 +1,167 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using TeamTools.TSQL.ExpressionEvaluator; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; +using TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator.TypeHandling; + +namespace TeamTools.TSQL.ExpressionEvaluatorTests.Tests.TypeHandling.Binary +{ + [Category("TeamTools.TSQL.ExpressionEvaluator")] + [TestOf(typeof(SqlBinaryTypeValueFactory))] + public sealed class SqlBinaryTypeValueFactoryTests : BaseSqlTypeHandlerTestClass + { + private SqlBinaryTypeHandler typeHandler; + + private SqlBinaryTypeValueFactory ValueFactory => typeHandler.BinaryValueFactory; + + [SetUp] + public override void SetUp() + { + base.SetUp(); + typeHandler = new SqlBinaryTypeHandler(Converter, Violations); + } + + [Test] + public void Test_SqlBinaryTypeValue_DeepCloneCopiesPreciseValue() + { + var value = ValueFactory.MakePreciseValue("BINARY", new HexValue(34), new SqlValueSource(SqlValueSourceKind.Literal, null)); + Assert.That(value, Is.Not.Null); + Assert.That(value.Value.AsNumber, Is.EqualTo((BigInteger)34)); + + var clone = value.DeepClone(); + Assert.That(clone, Is.Not.Null); + Assert.That(clone.IsPreciseValue, Is.True); + Assert.That(clone.Value, Is.EqualTo(value.Value)); + Assert.That(clone.Value.ToString(), Is.EqualTo("0x22")); + Assert.That(clone.TypeName, Is.EqualTo("BINARY")); + } + + [Test] + public void Test_SqlBinaryTypeValueFactory_NewLiteral() + { + var value = ValueFactory.NewLiteral("BINARY", "0xFA0031", default); + + Assert.That(value, Is.Not.Null); + Assert.That(value.TypeName, Is.EqualTo("BINARY")); + } + + [Test] + public void Test_SqlBinaryTypeValueFactory_LiteralsParsedWithRespectToDBEnginePeculiarities() + { + const string script = @" + DECLARE @a VARBINARY(3) = CAST(1 AS BIGINT) + DECLARE @b BINARY(1) = 1 + DECLARE @c VARBINARY(10) = 1 + DECLARE @d BINARY(10) = 1 + DECLARE @e VARBINARY(10) = CAST(1 AS BIGINT) + + PRINT @a + PRINT @b + PRINT @c + PRINT @d + PRINT @e + + --- expected result: + -- 0x000001 + -- 0x01 + -- 0x00000001 + -- 0x00000000000000000001 + -- 0x0000000000000001 + "; + + Dictionary expectedResults = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "@a", "0x000001" }, + { "@b", "0x01" }, + { "@c", "0x00000001" }, + { "@d", "0x00000000000000000001" }, + { "@e", "0x0000000000000001" }, + }; + + var dom = TSqlParser.CreateParser(SqlVersion.Sql150, true); + using var reader = new StringReader(script); + var sql = (TSqlScript)dom.Parse(reader, out var errors); + + Assert.That(errors, Is.Empty); + Assert.That(sql.Batches.Count, Is.EqualTo(1)); + + var eval = new ScalarExpressionEvaluator(sql.Batches[0]); + + foreach (var v in expectedResults) + { + var value = eval.GetValueAt(v.Key, sql.LastTokenIndex); + Assert.That(value, Is.Not.Null, v.Key); + Assert.That(value, Is.InstanceOf(typeof(SqlBinaryTypeValue))); + + var bin = ((SqlBinaryTypeValue)value).Value; + Assert.That(bin, Is.Not.Null, v.Key); + Assert.That(bin.ToString(), Is.EqualTo(v.Value), v.Key); + } + } + + [Test] + public void Test_SqlBinaryTypeValueFactory_MakeMethodsDontFailForUnsupportedType() + { + CallMakeMethodsWith("dummy"); + CallMakeMethodsWith(""); + CallMakeMethodsWith(null); + } + + [Test] + public void Test_SqlBinaryTypeValueFactory_MakeThrowsForNull() + { + Assert.Throws(typeof(ArgumentNullException), () => ValueFactory.MakeLiteral("BINARY", null, null)); + } + + [Test] + public void Test_SqlBinaryTypeValueFactory_MakeReturnsUnknownForEmptyString() + { + Assert.DoesNotThrow(() => ValueFactory.NewLiteral("BINARY", "", null)); + var value = ValueFactory.NewLiteral("BINARY", "", null); + + Assert.That(value, Is.Not.Null); + Assert.That(value.ValueKind, Is.EqualTo(SqlValueKind.Unknown)); + } + + [Test] + public void Test_SqlBinaryTypeValueFactory_MakeNull() + { + var value = ValueFactory.MakeNullValue("BINARY", null); + Assert.That(value, Is.Not.Null); + Assert.That(value.IsNull, Is.True); + + var v2 = ValueFactory.NewNull(null); + Assert.That(v2, Is.Not.Null); + Assert.That(v2.IsNull, Is.True); + } + + private void CallMakeMethodsWith(string dummyType) + { + Assert.DoesNotThrow(() => ValueFactory.MakeApproximateValue(dummyType, default, default)); + Assert.That(ValueFactory.MakeApproximateValue(dummyType, default, default), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakeLiteral(dummyType, default, default)); + Assert.That(ValueFactory.MakeLiteral(dummyType, default, default), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakeNullValue(dummyType, default)); + Assert.That(ValueFactory.MakeNullValue(dummyType, default), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakePreciseValue(dummyType, default, default)); + Assert.That(ValueFactory.MakePreciseValue(dummyType, default, default), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakeUnknownValue(dummyType)); + Assert.That(ValueFactory.MakeUnknownValue(dummyType), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakeSqlDataTypeReference(dummyType, 0)); + Assert.That(ValueFactory.MakeSqlDataTypeReference(dummyType, 1), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.NewLiteral(dummyType, "123", default)); + Assert.That(ValueFactory.NewLiteral(dummyType, "123", default), Is.Null); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Date/SqlDateTypeHandlerTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Date/SqlDateTypeHandlerTests.cs new file mode 100644 index 00000000..ee6ec212 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Date/SqlDateTypeHandlerTests.cs @@ -0,0 +1,44 @@ +using NUnit.Framework; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; +using TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator.TypeHandling; + +namespace TeamTools.TSQL.ExpressionEvaluatorTests.Tests.TypeHandling.Date +{ + [Category("TeamTools.TSQL.ExpressionEvaluator")] + [TestOf(typeof(SqlDateTypeHandler))] + public sealed class SqlDateTypeHandlerTests : BaseSqlTypeHandlerTestClass + { + private SqlDateTypeHandler typeHandler; + + private SqlDateTypeValueFactory ValueFactory => typeHandler.DateValueFactory; + + [OneTimeSetUp] + public override void SetUp() + { + base.SetUp(); + typeHandler = new SqlDateTypeHandler(Converter, Violations); + } + + [Test] + public void Test_SqlDateTypeHandler_CanConvertFromValidStr() + { + var date = typeHandler.TypeConverter.ImplicitlyConvert(typeHandler.ConvertFrom(MakeStr("2007-12-30"), "DATE")); + + Assert.That(date, Is.Not.Null); + Assert.That(date.IsPreciseValue, Is.True); + Assert.That(date.Value, Is.EqualTo(new System.DateTime(2007, 12, 30))); + } + + [Test] + public void Test_SqlDateTypeHandler_ChangeToAppliesProvidedValue() + { + var date = typeHandler.TypeConverter.ImplicitlyConvert(typeHandler.ConvertFrom(MakeStr("2007-12-30"), "DATE")); + + Assert.That(date, Is.Not.Null); + + date = date.ChangeTo(new System.DateTime(2015, 11, 23), new SqlValueSource(SqlValueSourceKind.Expression, null)); + Assert.That(date.Value, Is.EqualTo(new System.DateTime(2015, 11, 23))); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Date/SqlDateTypeValueFactoryTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Date/SqlDateTypeValueFactoryTests.cs new file mode 100644 index 00000000..7361d55c --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Date/SqlDateTypeValueFactoryTests.cs @@ -0,0 +1,69 @@ +using NUnit.Framework; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; +using TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator.TypeHandling; + +namespace TeamTools.TSQL.ExpressionEvaluatorTests.Tests.TypeHandling.Date +{ + [Category("TeamTools.TSQL.ExpressionEvaluator")] + [TestOf(typeof(SqlDateTypeValueFactory))] + public sealed class SqlDateTypeValueFactoryTests : BaseSqlTypeHandlerTestClass + { + private SqlDateTypeHandler typeHandler; + + private SqlDateTypeValueFactory ValueFactory => typeHandler.DateValueFactory; + + [SetUp] + public override void SetUp() + { + base.SetUp(); + typeHandler = new SqlDateTypeHandler(Converter, Violations); + } + + [Test] + public void Test_SqlDateTypeValue_DeepCloneCopiesPreciseValue() + { + var value = ValueFactory.MakePreciseValue("DATE", new System.DateTime(2005, 07, 31), new SqlValueSource(SqlValueSourceKind.Literal, null)); + Assert.That(value, Is.Not.Null); + Assert.That(value.Value, Is.EqualTo(new System.DateTime(2005, 07, 31))); + + var clone = value.DeepClone(); + Assert.That(clone, Is.Not.Null); + Assert.That(clone.IsPreciseValue, Is.True); + Assert.That(clone.Value, Is.EqualTo(value.Value)); + Assert.That(clone.TypeName, Is.EqualTo("DATE")); + } + + [Test] + public void Test_SqlDateTypeValueFactory_MakeMethodsDontFailForUnsupportedType() + { + CallMakeMethodsWith("dummy"); + CallMakeMethodsWith(""); + CallMakeMethodsWith(null); + } + + private void CallMakeMethodsWith(string dummyType) + { + Assert.DoesNotThrow(() => ValueFactory.MakeApproximateValue(dummyType, default, default)); + Assert.That(ValueFactory.MakeApproximateValue(dummyType, default, default), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakeLiteral(dummyType, default, default)); + Assert.That(ValueFactory.MakeLiteral(dummyType, default, default), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakeNullValue(dummyType, default)); + Assert.That(ValueFactory.MakeNullValue(dummyType, default), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakePreciseValue(dummyType, default, default)); + Assert.That(ValueFactory.MakePreciseValue(dummyType, default, default), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakeUnknownValue(dummyType)); + Assert.That(ValueFactory.MakeUnknownValue(dummyType), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakeSqlDataTypeReference(dummyType)); + Assert.That(ValueFactory.MakeSqlDataTypeReference(dummyType), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.NewLiteral(dummyType, "123", default)); + Assert.That(ValueFactory.NewLiteral(dummyType, "123", default), Is.Null); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/DateTime/SqlDateTimeRelativeValueTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/DateTime/SqlDateTimeRelativeValueTests.cs new file mode 100644 index 00000000..3f19961b --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/DateTime/SqlDateTimeRelativeValueTests.cs @@ -0,0 +1,31 @@ +using NUnit.Framework; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; + +namespace TeamTools.TSQL.ExpressionEvaluatorTests.Tests.TypeHandling.DateTime +{ + [Category("TeamTools.TSQL.ExpressionEvaluator")] + [TestOf(typeof(SqlDateTimeRelativeValue))] + public class SqlDateTimeRelativeValueTests + { + [Test] + public void Test_SqlDateTimeRelativeValue_ValueComparison() + { + var a = new SqlDateTimeRelativeValue(DateTimeRangeKind.Past, DateDetails.Full); + var b = new SqlDateTimeRelativeValue(DateTimeRangeKind.Future, DateDetails.Full); + var c = new SqlDateTimeRelativeValue(DateTimeRangeKind.CurrentMoment, DateDetails.Full); + + Assert.That(a.CompareTo(b), Is.EqualTo(-1)); + Assert.That(a.CompareTo(c), Is.EqualTo(-1)); + + Assert.That(b.CompareTo(a), Is.EqualTo(1)); + Assert.That(b.CompareTo(c), Is.EqualTo(1)); + + Assert.That(c.CompareTo(a), Is.EqualTo(1)); + Assert.That(c.CompareTo(b), Is.EqualTo(-1)); + + Assert.That(a.CompareTo(a), Is.Zero); + Assert.That(b.CompareTo(b), Is.Zero); + Assert.That(c.CompareTo(c), Is.Zero); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/DateTime/SqlDateTimeTypeHandlerTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/DateTime/SqlDateTimeTypeHandlerTests.cs new file mode 100644 index 00000000..916c15ad --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/DateTime/SqlDateTimeTypeHandlerTests.cs @@ -0,0 +1,44 @@ +using NUnit.Framework; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; +using TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator.TypeHandling; + +namespace TeamTools.TSQL.ExpressionEvaluatorTests.Tests.TypeHandling.DateTime +{ + [Category("TeamTools.TSQL.ExpressionEvaluator")] + [TestOf(typeof(SqlDateTimeTypeHandler))] + internal class SqlDateTimeTypeHandlerTests : BaseSqlTypeHandlerTestClass + { + private SqlDateTimeTypeHandler typeHandler; + + private SqlDateTimeTypeValueFactory ValueFactory => typeHandler.DateTimeValueFactory; + + [OneTimeSetUp] + public override void SetUp() + { + base.SetUp(); + typeHandler = new SqlDateTimeTypeHandler(Converter, Violations); + } + + [Test] + public void Test_SqlDateTimeTypeHandler_CanConvertFromValidStr() + { + var date = typeHandler.TypeConverter.ImplicitlyConvert(typeHandler.ConvertFrom(MakeStr("2007-12-30 12:30:55"), "DATETIME")); + + Assert.That(date, Is.Not.Null); + Assert.That(date.IsPreciseValue, Is.True); + Assert.That(date.Value, Is.EqualTo(new System.DateTime(2007, 12, 30, 12, 30, 55))); + } + + [Test] + public void Test_SqlDateTimeTypeHandler_ChangeToAppliesProvidedValue() + { + var date = typeHandler.TypeConverter.ImplicitlyConvert(typeHandler.ConvertFrom(MakeStr("2007-12-30 12:30:55"), "DATETIME")); + + Assert.That(date, Is.Not.Null); + + date = date.ChangeTo(new System.DateTime(2015, 11, 23, 15, 44, 23), new SqlValueSource(SqlValueSourceKind.Expression, null)); + Assert.That(date.Value, Is.EqualTo(new System.DateTime(2015, 11, 23, 15, 44, 23))); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/DateTime/SqlDateTimeTypeValueFactoryTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/DateTime/SqlDateTimeTypeValueFactoryTests.cs new file mode 100644 index 00000000..96fb1523 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/DateTime/SqlDateTimeTypeValueFactoryTests.cs @@ -0,0 +1,69 @@ +using NUnit.Framework; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; +using TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator.TypeHandling; + +namespace TeamTools.TSQL.ExpressionEvaluatorTests.Tests.TypeHandling.DateTime +{ + [Category("TeamTools.TSQL.ExpressionEvaluator")] + [TestOf(typeof(SqlDateTimeTypeValueFactory))] + internal class SqlDateTimeTypeValueFactoryTests : BaseSqlTypeHandlerTestClass + { + private SqlDateTimeTypeHandler typeHandler; + + private SqlDateTimeTypeValueFactory ValueFactory => typeHandler.DateTimeValueFactory; + + [SetUp] + public override void SetUp() + { + base.SetUp(); + typeHandler = new SqlDateTimeTypeHandler(Converter, Violations); + } + + [Test] + public void Test_SqlDateTimeTypeValue_DeepCloneCopiesPreciseValue() + { + var value = ValueFactory.MakePreciseValue("DATETIME2", new System.DateTime(2005, 07, 31, 12, 30, 55), new SqlValueSource(SqlValueSourceKind.Literal, null)); + Assert.That(value, Is.Not.Null); + Assert.That(value.Value, Is.EqualTo(new System.DateTime(2005, 07, 31, 12, 30, 55))); + + var clone = value.DeepClone(); + Assert.That(clone, Is.Not.Null); + Assert.That(clone.IsPreciseValue, Is.True); + Assert.That(clone.Value, Is.EqualTo(value.Value)); + Assert.That(clone.TypeName, Is.EqualTo("DATETIME2")); + } + + [Test] + public void Test_SqlDateTimeTypeValueFactory_MakeMethodsDontFailForUnsupportedType() + { + CallMakeMethodsWith("dummy"); + CallMakeMethodsWith(""); + CallMakeMethodsWith(null); + } + + private void CallMakeMethodsWith(string dummyType) + { + Assert.DoesNotThrow(() => ValueFactory.MakeApproximateValue(dummyType, default, default)); + Assert.That(ValueFactory.MakeApproximateValue(dummyType, default, default), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakeLiteral(dummyType, default, default)); + Assert.That(ValueFactory.MakeLiteral(dummyType, default, default), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakeNullValue(dummyType, default)); + Assert.That(ValueFactory.MakeNullValue(dummyType, default), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakePreciseValue(dummyType, default, default)); + Assert.That(ValueFactory.MakePreciseValue(dummyType, default, default), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakeUnknownValue(dummyType)); + Assert.That(ValueFactory.MakeUnknownValue(dummyType), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakeSqlDataTypeReference(dummyType)); + Assert.That(ValueFactory.MakeSqlDataTypeReference(dummyType), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.NewLiteral(dummyType, "123", default)); + Assert.That(ValueFactory.NewLiteral(dummyType, "123", default), Is.Null); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Decimal/SqlDecimalTypeHandlerTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Decimal/SqlDecimalTypeHandlerTests.cs new file mode 100644 index 00000000..e0a7c1ea --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Decimal/SqlDecimalTypeHandlerTests.cs @@ -0,0 +1,286 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using NUnit.Framework; +using System; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; +using TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator.TypeHandling; + +namespace TeamTools.TSQL.ExpressionEvaluatorTests.Tests.TypeHandling.Decimal +{ + [Category("TeamTools.TSQL.ExpressionEvaluator")] + [TestOf(typeof(SqlDecimalTypeHandler))] + public sealed class SqlDecimalTypeHandlerTests : BaseSqlTypeHandlerTestClass + { + private SqlDecimalTypeHandler typeHandler; + + private SqlDecimalTypeValueFactory ValueFactory => typeHandler.DecimalValueFactory; + + [OneTimeSetUp] + public override void SetUp() + { + base.SetUp(); + typeHandler = new SqlDecimalTypeHandler(Converter, Violations); + } + + [Test] + public void Test_SqlDecimalTypeHandler_CanConvertFromValidStr() + { + var date = typeHandler.TypeConverter.ImplicitlyConvert(typeHandler.ConvertFrom(MakeStr("123.4567"), "DECIMAL")); + + Assert.That(date, Is.Not.Null); + Assert.That(date.IsPreciseValue, Is.True); + Assert.That(date.Value, Is.EqualTo(123.4567m)); + } + + [Test] + public void Test_SqlDecimalTypeHandler_ChangeToAppliesProvidedValue() + { + var dec = typeHandler.TypeConverter.ImplicitlyConvert(typeHandler.ConvertFrom(MakeStr("123.4567"), "DECIMAL")); + + Assert.That(dec, Is.Not.Null); + + dec = dec.ChangeTo(200.002m, new SqlValueSource(SqlValueSourceKind.Expression, null)); + Assert.That(dec.Value, Is.EqualTo(200.002m)); + } + + [Test] + public void Test_SqlDecimalTypeHandler_MakeSqlDataTypeReferenceFromBrokenDefinition() + { + var typeDef = new SqlDataTypeReference(); // no initialization + try + { + var typeRef = typeHandler.MakeSqlDataTypeReference(typeDef); + Assert.That(typeRef, Is.Null); + } + catch (Exception e) + { + Assert.Fail(e.Message); + } + + try + { + var typeRef = typeHandler.MakeSqlDataTypeReference((DataTypeReference)null); + Assert.That(typeRef, Is.Null); + } + catch (Exception e) + { + Assert.Fail(e.Message); + } + } + + [Test] + public void Test_SqlDecimalTypeHandler_MakeSqlDataTypeReferenceFromDefinition() + { + var typeDef = new SqlDataTypeReference + { + Name = new SchemaObjectName(), + }; + + typeDef.Name.Identifiers.Add(new Identifier { Value = "DECIMAL" }); + typeDef.Parameters.Add(new IntegerLiteral { Value = "12" }); + typeDef.Parameters.Add(new IntegerLiteral { Value = "5" }); + + var typeRef = typeHandler.MakeSqlDataTypeReference(typeDef); + Assert.That(typeRef, Is.Not.Null); + Assert.That(typeRef.TypeName, Is.EqualTo("DECIMAL")); + Assert.That(typeRef, Is.InstanceOf(typeof(SqlDecimalTypeReference))); + Assert.That(((SqlDecimalTypeReference)typeRef).Size.Precision, Is.EqualTo(12)); + Assert.That(((SqlDecimalTypeReference)typeRef).Size.Scale, Is.EqualTo(5)); + } + + [Test] + public void Test_SqlDecimalTypeHandler_MakeSqlDataTypeReferenceFromName() + { + var typeRef = typeHandler.MakeSqlDataTypeReference("NUMERIC"); + Assert.That(typeRef, Is.Not.Null); + Assert.That(typeRef.TypeName, Is.EqualTo("NUMERIC")); + Assert.That(typeRef, Is.InstanceOf(typeof(SqlDecimalTypeReference))); + Assert.That(((SqlDecimalTypeReference)typeRef).Size.Precision, Is.EqualTo(18)); + Assert.That(((SqlDecimalTypeReference)typeRef).Size.Scale, Is.EqualTo(0)); + } + + [Test] + public void Test_SqlDecimalTypeHandler_Sum() + { + var a = new SqlDecimalTypeValue(typeHandler, (SqlDecimalTypeReference)typeHandler.MakeSqlDataTypeReference("NUMERIC"), 1.9m, new SqlValueSource(SqlValueSourceKind.Literal, default)); + var b = new SqlDecimalTypeValue(typeHandler, (SqlDecimalTypeReference)typeHandler.MakeSqlDataTypeReference("NUMERIC"), 2.5m, new SqlValueSource(SqlValueSourceKind.Literal, default)); + var c = typeHandler.Sum(a, b); + + Assert.That(c, Is.Not.Null); + Assert.That(c.TypeName, Is.EqualTo("NUMERIC")); + Assert.That(c, Is.InstanceOf(typeof(SqlDecimalTypeValue))); + + var dec = (SqlDecimalTypeValue)c; + Assert.That(dec.Value, Is.EqualTo(4.4m)); + } + + [Test] + public void Test_SqlDecimalTypeHandler_Sum_OfNullReturnsNull() + { + var a = ValueFactory.NewNull(default); + Assert.That(a, Is.Not.Null); + + var b = new SqlDecimalTypeValue(typeHandler, (SqlDecimalTypeReference)typeHandler.MakeSqlDataTypeReference("NUMERIC"), 2.5m, new SqlValueSource(SqlValueSourceKind.Literal, default)); + var c = typeHandler.Sum(a, b); + + Assert.That(c, Is.Not.Null); + Assert.That(c.IsNull, Is.True); + } + + [Test] + public void Test_SqlDecimalTypeHandler_Divide() + { + var a = new SqlDecimalTypeValue(typeHandler, (SqlDecimalTypeReference)typeHandler.MakeSqlDataTypeReference("NUMERIC"), 121.121m, new SqlValueSource(SqlValueSourceKind.Literal, default)); + var b = new SqlDecimalTypeValue(typeHandler, (SqlDecimalTypeReference)typeHandler.MakeSqlDataTypeReference("NUMERIC"), 11m, new SqlValueSource(SqlValueSourceKind.Literal, default)); + var c = typeHandler.Divide(a, b); + + Assert.That(c, Is.Not.Null); + Assert.That(c.TypeName, Is.EqualTo("NUMERIC")); + Assert.That(c, Is.InstanceOf(typeof(SqlDecimalTypeValue))); + + var dec = (SqlDecimalTypeValue)c; + Assert.That(dec.Value, Is.EqualTo(11.011m)); + } + + [Test] + public void Test_SqlDecimalTypeHandler_Multiply() + { + var a = new SqlDecimalTypeValue(typeHandler, (SqlDecimalTypeReference)typeHandler.MakeSqlDataTypeReference("NUMERIC"), 5.5m, new SqlValueSource(SqlValueSourceKind.Literal, default)); + var b = new SqlDecimalTypeValue(typeHandler, (SqlDecimalTypeReference)typeHandler.MakeSqlDataTypeReference("NUMERIC"), 2.2m, new SqlValueSource(SqlValueSourceKind.Literal, default)); + var c = typeHandler.Multiply(a, b); + + Assert.That(c, Is.Not.Null); + Assert.That(c.TypeName, Is.EqualTo("NUMERIC")); + Assert.That(c, Is.InstanceOf(typeof(SqlDecimalTypeValue))); + + var dec = (SqlDecimalTypeValue)c; + Assert.That(dec.Value, Is.EqualTo(12.1m)); + } + + [Test] + public void Test_SqlDecimalTypeHandler_Subtract() + { + var a = new SqlDecimalTypeValue(typeHandler, (SqlDecimalTypeReference)typeHandler.MakeSqlDataTypeReference("NUMERIC"), 5.5m, new SqlValueSource(SqlValueSourceKind.Literal, default)); + var b = new SqlDecimalTypeValue(typeHandler, (SqlDecimalTypeReference)typeHandler.MakeSqlDataTypeReference("NUMERIC"), 2.2m, new SqlValueSource(SqlValueSourceKind.Literal, default)); + var c = typeHandler.Subtract(a, b); + + Assert.That(c, Is.Not.Null); + Assert.That(c.TypeName, Is.EqualTo("NUMERIC")); + Assert.That(c, Is.InstanceOf(typeof(SqlDecimalTypeValue))); + + var dec = (SqlDecimalTypeValue)c; + Assert.That(dec.Value, Is.EqualTo(3.3m)); + } + + [Test] + public void Test_SqlDecimalTypeHandler_CombineSize() + { + var a = new SqlDecimalValueRange(-100, 100, 5, 3); + var b = new SqlDecimalValueRange(-200, 200, 15, 10); + var c = typeHandler.CombineSize(a, b); + + Assert.That(c, Is.Not.Null); + Assert.That(c.Low, Is.EqualTo(-200m)); + Assert.That(c.High, Is.EqualTo(200m)); + Assert.That(c.Precision, Is.EqualTo(15)); + Assert.That(c.Scale, Is.EqualTo(10)); + } + + [Test] + public void Test_SqlDecimalTypeHandler_RevertSign_Precise() + { + var a = new SqlDecimalTypeValue(typeHandler, (SqlDecimalTypeReference)typeHandler.MakeSqlDataTypeReference("NUMERIC"), 123.456m, new SqlValueSource(SqlValueSourceKind.Literal, default)); + var c = typeHandler.ReverseSign(a); + + Assert.That(c, Is.Not.Null); + Assert.That(c.TypeName, Is.EqualTo("NUMERIC")); + Assert.That(c, Is.InstanceOf(typeof(SqlDecimalTypeValue))); + + var dec = (SqlDecimalTypeValue)c; + Assert.That(dec.IsPreciseValue, Is.True); + Assert.That(dec.Value, Is.EqualTo(-123.456m)); + } + + [Test] + public void Test_SqlDecimalTypeHandler_RevertSign_Aproximate() + { + var a = ValueFactory.MakeApproximateValue("DECIMAL", new SqlDecimalValueRange(-222, 111, 18, 2), new SqlValueSource(SqlValueSourceKind.Expression, default)); + Assert.That(a.EstimatedSize.Low, Is.EqualTo(-222m)); + Assert.That(a.EstimatedSize.High, Is.EqualTo(111m)); + Assert.That(a.EstimatedSize.Precision, Is.EqualTo(18)); + Assert.That(a.EstimatedSize.Scale, Is.EqualTo(2)); + + var c = typeHandler.ReverseSign(a); + + Assert.That(c, Is.Not.Null); + Assert.That(c.TypeName, Is.EqualTo("DECIMAL")); + Assert.That(c, Is.InstanceOf(typeof(SqlDecimalTypeValue))); + + var dec = (SqlDecimalTypeValue)c; + Assert.That(dec.IsPreciseValue, Is.False); + Assert.That(dec.EstimatedSize.Low, Is.EqualTo(-111m)); + Assert.That(dec.EstimatedSize.High, Is.EqualTo(222m)); + Assert.That(dec.EstimatedSize.Precision, Is.EqualTo(18)); + Assert.That(dec.EstimatedSize.Scale, Is.EqualTo(2)); + } + + [Test] + public void Test_SqlDecimalTypeHandler_ReverseSign_Null() + { + var a = ValueFactory.NewNull(default); + Assert.That(a, Is.Not.Null); + + var c = typeHandler.ReverseSign(a); + + Assert.That(c, Is.Not.Null); + Assert.That(c.IsNull, Is.True); + } + + [Test] + public void Test_SqlDecimalTypeHandler_CanConvertFromInt() + { + var intHandler = new SqlIntTypeHandler(Converter, Violations); + var a = new SqlIntTypeValue(intHandler, new SqlIntTypeReference("INT", new SqlIntValueRange(1, 1), intHandler.IntValueFactory), 1, new SqlValueSource(SqlValueSourceKind.Literal, default)); + var b = typeHandler.ConvertFrom(a, "DECIMAL"); + + Assert.That(b, Is.Not.Null); + Assert.That(b.TypeName, Is.EqualTo("DECIMAL")); + Assert.That(b, Is.InstanceOf(typeof(SqlDecimalTypeValue))); + + var dec = (SqlDecimalTypeValue)b; + Assert.That(dec.Value, Is.EqualTo(1m)); + } + + [Test] + public void Test_SqlDecimalTypeHandler_ConvertionFromVariableToLessScaleTruncates() + { + var src = (SqlDecimalTypeValue)ValueFactory.NewLiteral("DECIMAL", "654.321", default); + Assert.That(src.Value, Is.EqualTo(654.321m)); + + // no scale + var dec = typeHandler.ConvertValueFrom(src, ValueFactory.MakeSqlDataTypeReference("DECIMAL", 18, 0), true); + + Assert.That(dec, Is.Not.Null); + Assert.That(dec.TypeName, Is.EqualTo("DECIMAL")); + Assert.That(dec.Value, Is.EqualTo(654m)); + + // scale = 1 + dec = typeHandler.ConvertValueFrom(src, ValueFactory.MakeSqlDataTypeReference("DECIMAL", 18, 1), true); + + Assert.That(dec, Is.Not.Null); + Assert.That(dec.Value, Is.EqualTo(654.3m)); + + // scale = 2 + dec = typeHandler.ConvertValueFrom(src, ValueFactory.MakeSqlDataTypeReference("DECIMAL", 18, 2), true); + + Assert.That(dec, Is.Not.Null); + Assert.That(dec.Value, Is.EqualTo(654.32m)); + + // scale = 3 + dec = typeHandler.ConvertValueFrom(src, ValueFactory.MakeSqlDataTypeReference("DECIMAL", 18, 3), true); + + Assert.That(dec, Is.Not.Null); + Assert.That(dec.Value, Is.EqualTo(654.321m)); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Decimal/SqlDecimalTypeValueFactoryTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Decimal/SqlDecimalTypeValueFactoryTests.cs new file mode 100644 index 00000000..73ac72c3 --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Decimal/SqlDecimalTypeValueFactoryTests.cs @@ -0,0 +1,100 @@ +using NUnit.Framework; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; +using TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator.TypeHandling; + +namespace TeamTools.TSQL.ExpressionEvaluatorTests.Tests.TypeHandling.Decimal +{ + [Category("TeamTools.TSQL.ExpressionEvaluator")] + [TestOf(typeof(SqlDecimalTypeValueFactory))] + public sealed class SqlDecimalTypeValueFactoryTests : BaseSqlTypeHandlerTestClass + { + private SqlDecimalTypeHandler typeHandler; + + private SqlDecimalTypeValueFactory ValueFactory => typeHandler.DecimalValueFactory; + + [SetUp] + public override void SetUp() + { + base.SetUp(); + typeHandler = new SqlDecimalTypeHandler(Converter, Violations); + } + + [Test] + public void Test_SqlDecimalTypeValue_DeepCloneCopiesPreciseValue() + { + var value = ValueFactory.MakePreciseValue("DECIMAL", 123.4567m, new SqlValueSource(SqlValueSourceKind.Literal, null)); + Assert.That(value, Is.Not.Null); + Assert.That(value.Value, Is.EqualTo(123.4567m)); + + var clone = value.DeepClone(); + Assert.That(clone, Is.Not.Null); + Assert.That(clone.IsPreciseValue, Is.True); + Assert.That(clone.Value, Is.EqualTo(value.Value)); + Assert.That(clone.TypeName, Is.EqualTo("DECIMAL")); + } + + [Test] + public void Test_SqlDecimalTypeValueFactory_NewLiteral() + { + var value = ValueFactory.NewLiteral("DECIMAL", "123.4567", default); + + Assert.That(value, Is.Not.Null); + Assert.That(value.TypeName, Is.EqualTo("DECIMAL")); + } + + [Test] + public void Test_SqlDecimalTypeValueFactory_MakeMethodsDontFailForUnsupportedType() + { + CallMakeMethodsWith("dummy"); + CallMakeMethodsWith(""); + CallMakeMethodsWith(null); + } + + [Test] + public void Test_SqlDecimalTypeValueFactory_MakeReturnsUnknownForEmptyString() + { + Assert.DoesNotThrow(() => ValueFactory.NewLiteral("DECIMAL", "", null)); + var value = ValueFactory.NewLiteral("DECIMAL", "", null); + + Assert.That(value, Is.Not.Null); + Assert.That(value.ValueKind, Is.EqualTo(SqlValueKind.Unknown)); + } + + [Test] + public void Test_SqlDecimalTypeValueFactory_MakeNull() + { + var value = ValueFactory.MakeNullValue("DECIMAL", null); + Assert.That(value, Is.Not.Null); + Assert.That(value.IsNull, Is.True); + + var v2 = ValueFactory.NewNull(null); + Assert.That(v2, Is.Not.Null); + Assert.That(v2.IsNull, Is.True); + } + + private void CallMakeMethodsWith(string dummyType) + { + Assert.DoesNotThrow(() => ValueFactory.MakeApproximateValue(dummyType, default, default)); + Assert.That(ValueFactory.MakeApproximateValue(dummyType, default, default), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakeLiteral(dummyType, default, default)); + Assert.That(ValueFactory.MakeLiteral(dummyType, default, default), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakeNullValue(dummyType, default)); + Assert.That(ValueFactory.MakeNullValue(dummyType, default), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakePreciseValue(dummyType, default, default)); + Assert.That(ValueFactory.MakePreciseValue(dummyType, default, default), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakeUnknownValue(dummyType)); + Assert.That(ValueFactory.MakeUnknownValue(dummyType), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakeSqlDataTypeReference(dummyType, 0)); + Assert.That(ValueFactory.MakeSqlDataTypeReference(dummyType, 1), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.NewLiteral(dummyType, "123", default)); + Assert.That(ValueFactory.NewLiteral(dummyType, "123", default), Is.Null); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Decimal/SqlDecimalValueRangeTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Decimal/SqlDecimalValueRangeTests.cs new file mode 100644 index 00000000..35a6d59e --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Decimal/SqlDecimalValueRangeTests.cs @@ -0,0 +1,95 @@ +using NUnit.Framework; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator.TypeHandling; + +namespace TeamTools.TSQL.ExpressionEvaluatorTests.Tests.TypeHandling.Decimal +{ + [Category("TeamTools.TSQL.ExpressionEvaluator")] + [TestOf(typeof(SqlDecimalValueRange))] + public class SqlDecimalValueRangeTests : BaseSqlTypeHandlerTestClass + { + private SqlDecimalTypeHandler typeHandler; + + private SqlDecimalTypeValueFactory ValueFactory => typeHandler.DecimalValueFactory; + + [OneTimeSetUp] + public override void SetUp() + { + base.SetUp(); + typeHandler = new SqlDecimalTypeHandler(Converter, Violations); + } + + [Test] + public void Test_SqlDecimalValueRange_EqualityRespectsPrecisionAndScale() + { + var a = new SqlDecimalValueRange(0, 100, 18, 3); + var b = new SqlDecimalValueRange(0, 100, 18, 3); + + // all the same + Assert.That(a.Equals(b), Is.True); + + // something is different + b = new SqlDecimalValueRange(0, 100, 18, 10); + Assert.That(a.Equals(b), Is.False); + + b = new SqlDecimalValueRange(0, 100, 12, 3); + Assert.That(a.Equals(b), Is.False); + + b = new SqlDecimalValueRange(100, 100, 18, 3); + Assert.That(a.Equals(b), Is.False); + + b = new SqlDecimalValueRange(0, 0, 18, 3); + Assert.That(a.Equals(b), Is.False); + + b = new SqlDecimalValueRange(1000, 10000, 18, 3); + Assert.That(a.Equals(b), Is.False); + } + + [Test] + public void Test_SqlDecimalValueRange_RevertRange() + { + var a = new SqlDecimalValueRange(99, 100, 18, 3); + var b = SqlDecimalValueRange.RevertRange(a); + + Assert.That(b, Is.Not.Null); + Assert.That(b.Low, Is.EqualTo(-100)); + Assert.That(b.High, Is.EqualTo(-99)); + + // nothing happened to precision and scale + Assert.That(b.Precision, Is.EqualTo(18)); + Assert.That(b.Scale, Is.EqualTo(3)); + } + + [TestCase(18, 9)] + [TestCase(22, 13)] + [TestCase(38, 17)] + public void Test_SqlDecimalTypeReference_GetBytesRespectsPrecision(int precision, int bytes) + { + var typeRef = new SqlDecimalTypeReference("DECIMAL", new SqlDecimalValueRange(0, 1, precision, 3), ValueFactory); + Assert.That(typeRef.Bytes, Is.EqualTo(bytes), precision.ToString()); + } + + [Test] + public void Test_SqlDecimalValueRange_FixesIllegalPrecision() + { + var range = new SqlDecimalValueRange(1, 2, 200, 0); + + Assert.That(range.Precision, Is.EqualTo(38)); + Assert.That(range.Scale, Is.EqualTo(0)); + } + + [Test] + public void Test_SqlDecimalValueRange_FixesIllegalScale() + { + var range = new SqlDecimalValueRange(1, 2, 15, 38); + + Assert.That(range.Precision, Is.EqualTo(15)); + Assert.That(range.Scale, Is.EqualTo(14)); + + range = new SqlDecimalValueRange(1, 2, 15, -1); + + Assert.That(range.Precision, Is.EqualTo(15)); + Assert.That(range.Scale, Is.EqualTo(0)); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlIntTypeHandlerTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Int/SqlIntTypeHandlerTests.cs similarity index 100% rename from TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlIntTypeHandlerTests.cs rename to TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Int/SqlIntTypeHandlerTests.cs diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlIntTypeOperatorsTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Int/SqlIntTypeOperatorsTests.cs similarity index 100% rename from TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlIntTypeOperatorsTests.cs rename to TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Int/SqlIntTypeOperatorsTests.cs diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlIntTypeReferenceTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Int/SqlIntTypeReferenceTests.cs similarity index 100% rename from TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlIntTypeReferenceTests.cs rename to TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Int/SqlIntTypeReferenceTests.cs diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlIntTypeValueFactoryTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Int/SqlIntTypeValueFactoryTests.cs similarity index 100% rename from TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlIntTypeValueFactoryTests.cs rename to TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Int/SqlIntTypeValueFactoryTests.cs diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlIntTypeValueTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Int/SqlIntTypeValueTests.cs similarity index 81% rename from TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlIntTypeValueTests.cs rename to TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Int/SqlIntTypeValueTests.cs index 55ec6982..3bbd3339 100644 --- a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlIntTypeValueTests.cs +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Int/SqlIntTypeValueTests.cs @@ -60,5 +60,19 @@ public void Test_SqlIntTypeValue_GetHandlerReturnsExpectedValue() Assert.That(value, Is.Not.Null); Assert.That(value.GetTypeHandler(), Is.EqualTo(typeHandler)); } + + [Test] + public void Test_SqlIntTypeValue_DeepCloneCopiesPreciseValue() + { + var value = ValueFactory.MakePreciseValue("SMALLINT", 42, new SqlValueSource(SqlValueSourceKind.Literal, null)); + Assert.That(value, Is.Not.Null); + Assert.That(value.Value, Is.EqualTo(42)); + + var clone = value.DeepClone(); + Assert.That(clone, Is.Not.Null); + Assert.That(clone.IsPreciseValue, Is.True); + Assert.That(clone.Value, Is.EqualTo(value.Value)); + Assert.That(clone.TypeName, Is.EqualTo("SMALLINT")); + } } } diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlIntValueRangeTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Int/SqlIntValueRangeTests.cs similarity index 100% rename from TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlIntValueRangeTests.cs rename to TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Int/SqlIntValueRangeTests.cs diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlStrTypeDefinitionParserTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/String/SqlStrTypeDefinitionParserTests.cs similarity index 100% rename from TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlStrTypeDefinitionParserTests.cs rename to TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/String/SqlStrTypeDefinitionParserTests.cs diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlStrTypeHandlerTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/String/SqlStrTypeHandlerTests.cs similarity index 100% rename from TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlStrTypeHandlerTests.cs rename to TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/String/SqlStrTypeHandlerTests.cs diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlStrTypeReferenceTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/String/SqlStrTypeReferenceTests.cs similarity index 93% rename from TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlStrTypeReferenceTests.cs rename to TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/String/SqlStrTypeReferenceTests.cs index ccddedeb..ffb507e6 100644 --- a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlStrTypeReferenceTests.cs +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/String/SqlStrTypeReferenceTests.cs @@ -18,12 +18,6 @@ public void SetUp() size = 4000; } - [Test] - public void Test_SqlStrTypeReference_ConstructorFailsIfTypeIsWrong() - { - Assert.Throws(typeof(ArgumentOutOfRangeException), () => new SqlStrTypeReference("unknown type", size, factory)); - } - [Test] public void Test_SqlStrTypeReference_ConstructorFailsIfTypeIsWrongForUnicode() { diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlStrTypeValueFactoryTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/String/SqlStrTypeValueFactoryTests.cs similarity index 100% rename from TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlStrTypeValueFactoryTests.cs rename to TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/String/SqlStrTypeValueFactoryTests.cs diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlStrTypeValueTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/String/SqlStrTypeValueTests.cs similarity index 100% rename from TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/SqlStrTypeValueTests.cs rename to TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/String/SqlStrTypeValueTests.cs diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Time/SqlTimeTypeHandlerTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Time/SqlTimeTypeHandlerTests.cs new file mode 100644 index 00000000..1f27474d --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Time/SqlTimeTypeHandlerTests.cs @@ -0,0 +1,45 @@ +using NUnit.Framework; +using System; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; +using TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator.TypeHandling; + +namespace TeamTools.TSQL.ExpressionEvaluatorTests.Tests.TypeHandling.Time +{ + [Category("TeamTools.TSQL.ExpressionEvaluator")] + [TestOf(typeof(SqlTimeTypeHandler))] + internal class SqlTimeTypeHandlerTests : BaseSqlTypeHandlerTestClass + { + private SqlTimeTypeHandler typeHandler; + + private SqlTimeTypeValueFactory ValueFactory => typeHandler.TimeValueFactory; + + [OneTimeSetUp] + public override void SetUp() + { + base.SetUp(); + typeHandler = new SqlTimeTypeHandler(Converter, Violations); + } + + [Test] + public void Test_SqlTimeTypeHandler_CanConvertFromValidStr() + { + var time = typeHandler.TypeConverter.ImplicitlyConvert(typeHandler.ConvertFrom(MakeStr("12:30:55"), "TIME")); + + Assert.That(time, Is.Not.Null); + Assert.That(time.IsPreciseValue, Is.True); + Assert.That(time.Value, Is.EqualTo(new TimeSpan(12, 30, 55))); + } + + [Test] + public void Test_SqlTimeTypeHandler_ChangeToAppliesProvidedValue() + { + var time = typeHandler.TypeConverter.ImplicitlyConvert(typeHandler.ConvertFrom(MakeStr("12:30:55"), "TIME")); + + Assert.That(time, Is.Not.Null); + + time = time.ChangeTo(new TimeSpan(15, 44, 21), new SqlValueSource(SqlValueSourceKind.Expression, null)); + Assert.That(time.Value, Is.EqualTo(new TimeSpan(15, 44, 21))); + } + } +} diff --git a/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Time/SqlTimeTypeValueFactoryTests.cs b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Time/SqlTimeTypeValueFactoryTests.cs new file mode 100644 index 00000000..6004772d --- /dev/null +++ b/TeamTools.TSQL.ExpressionEvaluatorTests/Tests/TypeHandling/Time/SqlTimeTypeValueFactoryTests.cs @@ -0,0 +1,70 @@ +using NUnit.Framework; +using System; +using TeamTools.TSQL.ExpressionEvaluator.TypeHandling; +using TeamTools.TSQL.ExpressionEvaluator.Values; +using TeamTools.TSQL.LinterTests.Routines.ExpressionEvaluator.TypeHandling; + +namespace TeamTools.TSQL.ExpressionEvaluatorTests.Tests.TypeHandling.Time +{ + [Category("TeamTools.TSQL.ExpressionEvaluator")] + [TestOf(typeof(SqlTimeTypeValueFactory))] + internal class SqlTimeTypeValueFactoryTests : BaseSqlTypeHandlerTestClass + { + private SqlTimeTypeHandler typeHandler; + + private SqlTimeTypeValueFactory ValueFactory => typeHandler.TimeValueFactory; + + [SetUp] + public override void SetUp() + { + base.SetUp(); + typeHandler = new SqlTimeTypeHandler(Converter, Violations); + } + + [Test] + public void Test_SqlTimeTypeValue_DeepCloneCopiesPreciseValue() + { + var value = ValueFactory.MakePreciseValue("TIME", new TimeSpan(12, 30, 55), new SqlValueSource(SqlValueSourceKind.Literal, null)); + Assert.That(value, Is.Not.Null); + Assert.That(value.Value, Is.EqualTo(new TimeSpan(12, 30, 55))); + + var clone = value.DeepClone(); + Assert.That(clone, Is.Not.Null); + Assert.That(clone.IsPreciseValue, Is.True); + Assert.That(clone.Value, Is.EqualTo(value.Value)); + Assert.That(clone.TypeName, Is.EqualTo("TIME")); + } + + [Test] + public void Test_SqlTimeTypeValueFactory_MakeMethodsDontFailForUnsupportedType() + { + CallMakeMethodsWith("dummy"); + CallMakeMethodsWith(""); + CallMakeMethodsWith(null); + } + + private void CallMakeMethodsWith(string dummyType) + { + Assert.DoesNotThrow(() => ValueFactory.MakeApproximateValue(dummyType, default, default)); + Assert.That(ValueFactory.MakeApproximateValue(dummyType, default, default), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakeLiteral(dummyType, default, default)); + Assert.That(ValueFactory.MakeLiteral(dummyType, default, default), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakeNullValue(dummyType, default)); + Assert.That(ValueFactory.MakeNullValue(dummyType, default), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakePreciseValue(dummyType, default, default)); + Assert.That(ValueFactory.MakePreciseValue(dummyType, default, default), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakeUnknownValue(dummyType)); + Assert.That(ValueFactory.MakeUnknownValue(dummyType), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.MakeSqlDataTypeReference(dummyType)); + Assert.That(ValueFactory.MakeSqlDataTypeReference(dummyType), Is.Null); + + Assert.DoesNotThrow(() => ValueFactory.NewLiteral(dummyType, "123", default)); + Assert.That(ValueFactory.NewLiteral(dummyType, "123", default), Is.Null); + } + } +} diff --git a/TeamTools.TSQL.Linter/DefaultConfig.json b/TeamTools.TSQL.Linter/DefaultConfig.json index eae93a09..d480f6de 100644 --- a/TeamTools.TSQL.Linter/DefaultConfig.json +++ b/TeamTools.TSQL.Linter/DefaultConfig.json @@ -152,6 +152,8 @@ "RD0798:REDUNDANT_INIT_NULL": "hint", "RD0811:EXECUTE_AS_CALLER": "hint", "RD0814:IN_DUP_VAR": "warning", + "RD0849:REDUNDANT_INDEX_FILTER": "hint", + "RD0850:EXTRA_WHERE_PREDICATE": "hint", "RD0925:REDUNDANT_LIKE": "warning", "RD0926:REDUNDANT_NOT_FOR_REPLICATION": "warning", "RD0927:REDUNDANT_COL_NULLABILITY_CHECK": "warning", @@ -212,6 +214,7 @@ "NM0271:MAGIC_@@_##_NAME": "warning", "NM0712:NON_TEMP_OBJECT_LIKE_TEMP": "warning", "NM0714:ALIAS_IS_KEYWORD": "hint", + "NM0854:IDENTIFIER_LOOK_ALIKE_CHAR": "hint", "NM0961:INDEX_NAME_PATTERN": "warning", "NM0962:TRIGGER_NAME_PATTERN": "warning", "NM0963:TABLE_NAME_LOWER_SNAKE_CASE": "off", @@ -307,6 +310,10 @@ "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", "DD0153:FK_MULTIPLE_COL": "hint", "DD0158:TABLE_ALL_COL_NULL": "hint", @@ -457,6 +464,13 @@ "CS0834:LITERAL_LOOK_ALIKE_CHAR": "hint", "CS0835:COMMENT_LOOK_ALIKE_CHAR": "hint", "CS0840:CHAR_REMOVED_AND_SEARCHED": "warning", + "CS0841:INVISIBLE_CHAR_IN_IDENTIFIER": "hint", + "CS0842:INVISIBLE_CHAR_IN_COMMENT": "hint", + "CS0843:OUTPUT_MISMATCHES_ACTION": "warning", + "CS0844:CONDITIONS_SAME_DECISIONS": "hint", + "CS0851:FAKE_OUTER_JOIN": "hint", + "CS0852:NON_CORRELATED_JOIN_PREDICATE": "hint", + "CS0853:COMPARISON_LEFT_EQUALS_RIGHT": "hint", "CS0905:VAR_LACKS_PRECISION": "warning", "CS0914:INTERSECT_EXCEPT_BROKEN_BY_LITERAL": "warning", "CS0917:FORBIDDEN_INSERT_HINTS": "warning", diff --git a/TeamTools.TSQL.Linter/Routines/BooleanExpressionHandling/BooleanComparisonConverter.cs b/TeamTools.TSQL.Linter/Routines/BooleanExpressionHandling/BooleanComparisonConverter.cs new file mode 100644 index 00000000..afc5d14c --- /dev/null +++ b/TeamTools.TSQL.Linter/Routines/BooleanExpressionHandling/BooleanComparisonConverter.cs @@ -0,0 +1,88 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Diagnostics.CodeAnalysis; + +namespace TeamTools.TSQL.Linter.Routines +{ + [ExcludeFromCodeCoverage] + internal static class BooleanComparisonConverter + { + public static BooleanComparisonType RevertComparison(BooleanComparisonType cmp) + { + switch (cmp) + { + case BooleanComparisonType.Equals: + // Aligning not equality checks to "<>" format + return BooleanComparisonType.NotEqualToBrackets; + + case BooleanComparisonType.NotEqualToBrackets: + case BooleanComparisonType.NotEqualToExclamation: + return BooleanComparisonType.Equals; + + case BooleanComparisonType.GreaterThan: + return BooleanComparisonType.LessThanOrEqualTo; + + case BooleanComparisonType.GreaterThanOrEqualTo: + return BooleanComparisonType.LessThan; + + case BooleanComparisonType.LessThanOrEqualTo: + return BooleanComparisonType.GreaterThan; + + case BooleanComparisonType.LessThan: + return BooleanComparisonType.GreaterThanOrEqualTo; + + case BooleanComparisonType.NotGreaterThan: + return BooleanComparisonType.GreaterThan; + + case BooleanComparisonType.NotLessThan: + return BooleanComparisonType.LessThan; + + // TODO : or fail? + default: + return cmp; + } + } + + public static string ComparisonToString(BooleanComparisonType cmp) + { + switch (cmp) + { + case BooleanComparisonType.Equals: + return "="; + + case BooleanComparisonType.NotEqualToBrackets: + return "<>"; + + case BooleanComparisonType.NotEqualToExclamation: + return "!="; + + case BooleanComparisonType.GreaterThan: + return ">"; + + case BooleanComparisonType.GreaterThanOrEqualTo: + return ">="; + + case BooleanComparisonType.LessThanOrEqualTo: + return "<="; + + case BooleanComparisonType.LessThan: + return "<"; + + case BooleanComparisonType.NotGreaterThan: + return "!>"; + + case BooleanComparisonType.NotLessThan: + return "!<"; + + // TODO : or fail? + default: + return cmp.ToString(); + } + } + + public static BooleanComparisonType ToEqualityComparison(bool isEqual) + { + return isEqual ? BooleanComparisonType.Equals : BooleanComparisonType.NotEqualToBrackets; + } + } +} diff --git a/TeamTools.TSQL.Linter/Routines/BooleanExpressionHandling/BooleanExpressionComparer.cs b/TeamTools.TSQL.Linter/Routines/BooleanExpressionHandling/BooleanExpressionComparer.cs new file mode 100644 index 00000000..a32df24b --- /dev/null +++ b/TeamTools.TSQL.Linter/Routines/BooleanExpressionHandling/BooleanExpressionComparer.cs @@ -0,0 +1,60 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; + +namespace TeamTools.TSQL.Linter.Routines +{ + internal static class BooleanExpressionComparer + { + public static bool AreEqualExpressions(TSqlFragment exprA, TSqlFragment exprB) + { + if (exprA is null && exprB is null) + { + // both are nulls + return true; + } + + if (exprA is null || exprB is null) + { + // only one is null + return false; + } + + if (exprA is Literal literA && exprB is Literal literB) + { + // this is faster than building script fragment text + return literA.Value.Equals(literB.Value, StringComparison.OrdinalIgnoreCase); + } + + if (exprA is Literal || exprB is Literal) + { + // one is a literal another is not + return false; + } + + if (exprA is VariableReference varA && exprB is VariableReference varB) + { + // this is faster than building script fragment text + return varA.Name.Equals(varB.Name, StringComparison.OrdinalIgnoreCase); + } + + if (exprA is VariableReference || exprB is VariableReference) + { + // one is a variable reference another is not + return false; + } + + // comparing expressions lexicographically since there is no smarter option for now + // TODO : something better and faster needed + return string.Equals(exprA.GetFragmentCleanedText(), exprB.GetFragmentCleanedText(), StringComparison.OrdinalIgnoreCase); + } + + public static bool AreEqualExpressions(BooleanExpressionParts exprA, BooleanExpressionParts exprB) + { + return exprA.ComparisonType == exprB.ComparisonType + && AreEqualExpressions(exprA.FirstExpression, exprB.FirstExpression) + && AreEqualExpressions(exprA.SecondExpression, exprB.SecondExpression) + && ((exprA.FirstExpression != null && exprA.SecondExpression != null) + || AreEqualExpressions(exprA.OriginalExpression, exprB.OriginalExpression)); + } + } +} diff --git a/TeamTools.TSQL.Linter/Routines/BooleanExpressionHandling/BooleanExpressionNormalizer.cs b/TeamTools.TSQL.Linter/Routines/BooleanExpressionHandling/BooleanExpressionNormalizer.cs new file mode 100644 index 00000000..ac52dfa4 --- /dev/null +++ b/TeamTools.TSQL.Linter/Routines/BooleanExpressionHandling/BooleanExpressionNormalizer.cs @@ -0,0 +1,102 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TeamTools.TSQL.Linter.Routines +{ + internal static class BooleanExpressionNormalizer + { + // Normalizing boolean predicates to be able to detect equal expression even + // if they are written in reversed or negated way + // e.g. (@a < @b) is the same as (@b > @a) and NOT (@a >= @b) + public static BooleanExpressionParts Normalize(BooleanExpression expr) + { + var result = new BooleanExpressionParts + { + OriginalExpression = expr, + }; + + // TODO : BooleanBinaryExpression? at least sort + if (expr is BooleanComparisonExpression cmp) + { + // TODO : handle NotGreaterThan as negation + result.ComparisonType = GetNormalizedComparisonType(cmp.ComparisonType); + result.FirstExpression = ExtractExpression(cmp.FirstExpression); + result.SecondExpression = ExtractExpression(cmp.SecondExpression); + + if (result.ComparisonType != cmp.ComparisonType) + { + // switching sides of expression if needed for normalization + (result.FirstExpression, result.SecondExpression) = (result.SecondExpression, result.FirstExpression); + } + else if (result.ComparisonType == BooleanComparisonType.Equals) + { + // normalizing by sorting alphabetically if comparison has no direction (< or >) + string firstExpression = result.FirstExpression.GetFragmentCleanedText(); + string secondExpression = result.SecondExpression.GetFragmentCleanedText(); + if (string.Compare(firstExpression, secondExpression) > 0) + { + // switching sides of expression if needed for normalization + (result.FirstExpression, result.SecondExpression) = (result.SecondExpression, result.FirstExpression); + } + } + else if (result.ComparisonType == BooleanComparisonType.NotEqualToExclamation) + { + // replacing with equal comparison type for similarity + // != -> <> + result.ComparisonType = BooleanComparisonType.NotEqualToBrackets; + } + } + else if (expr is BooleanIsNullExpression isnull) + { + result.ComparisonType = isnull.IsNot ? BooleanComparisonType.NotEqualToBrackets : BooleanComparisonType.Equals; + result.FirstExpression = ExtractExpression(isnull.Expression); + result.SecondExpression = null; + } + else if (expr is BooleanNotExpression not) + { + // expanding NOT (@a >= @b) to @a < @b and normalizing to @b > @a + result = Normalize(ExtractExpression(not.Expression)); + var comparisonType = GetNormalizedComparisonType(BooleanComparisonConverter.RevertComparison(result.ComparisonType)); + if (comparisonType != result.ComparisonType) + { + result.ComparisonType = comparisonType; + if (result.SecondExpression != null) + { + // switching sides of expression if needed for normalization + // unless this is IS NULL / IS NOT NULL expression + (result.FirstExpression, result.SecondExpression) = (result.SecondExpression, result.FirstExpression); + } + } + } + + return result; + } + + private static ScalarExpression ExtractExpression(ScalarExpression expr) + => BooleanExpressionPartsExtractor.ExtractExpression(expr); + + private static BooleanExpression ExtractExpression(BooleanExpression expr) + => BooleanExpressionPartsExtractor.ExtractExpression(expr); + + // Normalizing comparison types: all left-sided (<, <=, !<) to right-sided (>, >=, !>) + private static BooleanComparisonType GetNormalizedComparisonType(BooleanComparisonType cmp) + { + switch (cmp) + { + case BooleanComparisonType.LessThan: + return BooleanComparisonType.GreaterThan; + + case BooleanComparisonType.LessThanOrEqualTo: + return BooleanComparisonType.GreaterThanOrEqualTo; + + case BooleanComparisonType.NotLessThan: + return BooleanComparisonType.NotGreaterThan; + + default: + return cmp; + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Routines/BooleanExpressionHandling/BooleanExpressionParts.cs b/TeamTools.TSQL.Linter/Routines/BooleanExpressionHandling/BooleanExpressionParts.cs new file mode 100644 index 00000000..db3962db --- /dev/null +++ b/TeamTools.TSQL.Linter/Routines/BooleanExpressionHandling/BooleanExpressionParts.cs @@ -0,0 +1,145 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; + +namespace TeamTools.TSQL.Linter.Routines +{ + // TODO : Support: BETWEEN, [NOT] IN, [NOT] LIKE, IS [NOT] NULL + internal sealed class BooleanExpressionParts : IEquatable + { + public ScalarExpression FirstExpression { get; set; } + + public ScalarExpression SecondExpression { get; set; } + + public BooleanExpression OriginalExpression { get; set; } + + public BooleanComparisonType ComparisonType { get; set; } = BooleanComparisonType.Equals; + + public override string ToString() + { + return string.Format( + "{0} {1} {2}", + FirstExpression.GetFragmentCleanedText(), + ComparisonToString(ComparisonType, SecondExpression is null), + SecondExpression?.GetFragmentCleanedText() ?? ""); + } + + public override bool Equals(object other) => Equals(other as BooleanExpressionParts); + + public bool Equals(BooleanExpressionParts other) + { + if (other is null) + { + return false; + } + + if (ComparisonType != other.ComparisonType) + { + return false; + } + + if ((SecondExpression is null) != (other.SecondExpression is null)) + { + return false; + } + + string firstName = ExtractExpressionName(FirstExpression); + string otherFirstName = ExtractExpressionName(other.FirstExpression); + + if (string.IsNullOrEmpty(firstName) != string.IsNullOrEmpty(otherFirstName)) + { + return false; + } + + string secondName = ExtractExpressionName(SecondExpression); + string otherSecondName = ExtractExpressionName(other.SecondExpression); + + if (string.IsNullOrEmpty(secondName) != string.IsNullOrEmpty(otherSecondName)) + { + return false; + } + + if (!string.IsNullOrEmpty(firstName) && (SecondExpression is null || string.IsNullOrEmpty(secondName))) + { + return string.Equals(firstName, otherFirstName, StringComparison.OrdinalIgnoreCase) + && (SecondExpression is null || string.Equals(secondName, otherSecondName, StringComparison.OrdinalIgnoreCase)); + } + + if (string.IsNullOrEmpty(firstName)) + { + firstName = FirstExpression.GetFragmentCleanedText(); + } + + if (string.IsNullOrEmpty(otherFirstName)) + { + otherFirstName = other.FirstExpression.GetFragmentCleanedText(); + } + + if (SecondExpression != null) + { + if (string.IsNullOrEmpty(secondName)) + { + secondName = SecondExpression.GetFragmentCleanedText(); + } + + if (string.IsNullOrEmpty(otherSecondName)) + { + otherSecondName = other.SecondExpression.GetFragmentCleanedText(); + } + } + + return string.Equals(firstName, otherFirstName, StringComparison.OrdinalIgnoreCase) + && (SecondExpression is null || string.Equals(secondName, otherSecondName, StringComparison.OrdinalIgnoreCase)); + } + + public BooleanExpressionParts Mirror() + { + if (SecondExpression is null) + { + // NOT NULL expression cannot be mirrored + return this; + } + + return new BooleanExpressionParts + { + FirstExpression = SecondExpression, + SecondExpression = FirstExpression, + OriginalExpression = OriginalExpression, + ComparisonType = BooleanComparisonConverter.RevertComparison(ComparisonType), + }; + } + + private static string ComparisonToString(BooleanComparisonType comparison, bool isComparisonToNull) + { + if (isComparisonToNull) + { + return comparison == BooleanComparisonType.Equals ? "IS NULL" : "IS NOT NULL"; + } + + return BooleanComparisonConverter.ComparisonToString(comparison); + } + + private static string ExtractExpressionName(ScalarExpression node) + { + if (node is VariableReference varRef) + { + return varRef.Name; + } + else if (node is ColumnReferenceExpression colRef) + { + return colRef?.MultiPartIdentifier.Identifiers.GetFullName(); + } + else if (node is StringLiteral str) + { + // String literals are stored in ScriptDom without quotes + // escaping is not related here since we're not going to generate legal code from it + return string.Format("'{0}'", str.Value); + } + else if (node is Literal literal) + { + return literal.Value; + } + + return default; + } + } +} diff --git a/TeamTools.TSQL.Linter/Routines/BooleanExpressionHandling/BooleanExpressionPartsExtractor.cs b/TeamTools.TSQL.Linter/Routines/BooleanExpressionHandling/BooleanExpressionPartsExtractor.cs new file mode 100644 index 00000000..710bea63 --- /dev/null +++ b/TeamTools.TSQL.Linter/Routines/BooleanExpressionHandling/BooleanExpressionPartsExtractor.cs @@ -0,0 +1,43 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; + +namespace TeamTools.TSQL.Linter.Routines +{ + internal static class BooleanExpressionPartsExtractor + { + public static BooleanExpression ExtractExpression(BooleanExpression expr) + { + while (expr is BooleanParenthesisExpression pe) + { + expr = pe.Expression; + } + + return expr; + } + + public static ScalarExpression ExtractExpression(ScalarExpression expr) + { + while (expr is ParenthesisExpression pe) + { + expr = pe.Expression; + } + + if (expr is ScalarSubquery q) + { + var spec = q.QueryExpression.GetQuerySpecification(); + if (spec != null + && spec.FromClause is null + && spec.WhereClause is null + && spec.SelectElements.Count == 1 + && spec.SelectElements[0] is SelectScalarExpression sel) + { + // if subquery is selecting single column with no WHERE and FROM + // then we can grab selected scalar expression itself + return ExtractExpression(sel.Expression); + } + } + + return expr; + } + } +} diff --git a/TeamTools.TSQL.Linter/Routines/CollapsibleInExtractor.cs b/TeamTools.TSQL.Linter/Routines/CollapsibleInExtractor.cs new file mode 100644 index 00000000..97783c39 --- /dev/null +++ b/TeamTools.TSQL.Linter/Routines/CollapsibleInExtractor.cs @@ -0,0 +1,111 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TeamTools.TSQL.Linter.Routines +{ + /// + /// It detects IN/NOT IN predicates that can be combined into single one + /// 1) foo IN (1, 2, 3) OR foo IN (4, 5) => foo IN (1, 2, 3, 4, 5) + /// 2) foo NOT IN (1, 2, 3) AND foo NOT IN (4, 5) => foo NOT IN (1, 2, 3, 4, 5) + /// + internal class CollapsibleInExtractor + { + private readonly bool notPresence; + private readonly BooleanBinaryExpressionType binaryOperator; + + public CollapsibleInExtractor(bool notPresence) + { + this.notPresence = notPresence; + binaryOperator = notPresence ? BooleanBinaryExpressionType.And : BooleanBinaryExpressionType.Or; + } + + public void Process(BooleanBinaryExpression node, Action callback) + { + if (node.BinaryExpressionType != binaryOperator) + { + return; + } + + var predicates = ExpandPredicates(node.FirstExpression) + .Union(ExpandPredicates(node.SecondExpression)) + .ToList(); + + var alreadyMentionedLeftSide = new HashSet(StringComparer.OrdinalIgnoreCase); + + for (int i = predicates.Count - 1; i >= 0; i--) + { + var predicate = predicates[i]; + + if (!alreadyMentionedLeftSide.Add(predicate.FilteredItemName)) + { + callback(predicate.Node, predicate.FilteredItemName); + } + } + } + + private IEnumerable ExpandPredicates(BooleanExpression node) + { + if (node is BooleanBinaryExpression bin && bin.BinaryExpressionType == binaryOperator) + { + return ExpandPredicates(bin.FirstExpression). + Union(ExpandPredicates(bin.SecondExpression)); + } + + return ExpandExpression(node); + } + + private IEnumerable ExpandExpression(BooleanExpression node) + { + while (node is BooleanParenthesisExpression pe) + { + node = pe.Expression; + } + + if (!(node is InPredicate inPredicate && inPredicate.Values != null)) + { + // ... IN (subquery) cannot be easily combined + // only IN (values,..) supported + yield break; + } + + if (inPredicate.NotDefined != notPresence) + { + yield break; + } + + var leftSide = inPredicate.Expression.ExtractScalarExpression(); + string filteredItemName; + + // On the left side there must be a variable or a column reference + if (leftSide is VariableReference filteredVarRef) + { + filteredItemName = filteredVarRef.Name; + } + else if (leftSide is ColumnReferenceExpression filteredColRef + && filteredColRef.MultiPartIdentifier != null) + { + // filteredColRef.MultiPartIdentifier can be null for sys columns like $action + filteredItemName = filteredColRef.MultiPartIdentifier.Identifiers.GetFullName(); + } + else + { + yield break; + } + + yield return new InPredicateInfo + { + Node = leftSide, + FilteredItemName = filteredItemName, + }; + } + + private sealed class InPredicateInfo + { + public TSqlFragment Node { get; set; } + + public string FilteredItemName { get; set; } + } + } +} diff --git a/TeamTools.TSQL.Linter/Routines/CombinablePredicateExtractor.cs b/TeamTools.TSQL.Linter/Routines/CombinablePredicateExtractor.cs new file mode 100644 index 00000000..e1ae3b2b --- /dev/null +++ b/TeamTools.TSQL.Linter/Routines/CombinablePredicateExtractor.cs @@ -0,0 +1,251 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TeamTools.TSQL.Linter.Routines +{ + /// + /// It detects predicates that can be combined into single IN/NOT IN expression + /// 1) A != B AND A != C => A NOT IN (B, C) + /// 2) A = B OR A = C => A IN (B, C) + /// + internal class CombinablePredicateExtractor + { + private static readonly string ExampleTemplateWithAllValues = "{0} IN ({1})"; + private static readonly string ExampleTemplateWithSomeValues = "{0} IN ({1},..)"; + private static readonly string ExampleTemplateWithAllValuesNegated = "{0} NOT IN ({1})"; + private static readonly string ExampleTemplateWithSomeValuesNegated = "{0} NOT IN ({1},..)"; + + private static readonly int MinValuesToCollapse = 3; + private static readonly int MaxValuesToShow = 5; + + private readonly bool notPresence; + private readonly BooleanBinaryExpressionType binaryOperator; + private readonly BooleanComparisonType equalityOperator; + private readonly Func getMsgTemplate; + + public CombinablePredicateExtractor(bool notPresence) + { + if (notPresence) + { + this.notPresence = true; + binaryOperator = BooleanBinaryExpressionType.And; + // Note, there is also NotEqualToExclamation + // and a separate rule which forces to use <> instead of != + equalityOperator = BooleanComparisonType.NotEqualToBrackets; + getMsgTemplate = GetMsgTemplateNegated; + } + else + { + this.notPresence = false; + binaryOperator = BooleanBinaryExpressionType.Or; + equalityOperator = BooleanComparisonType.Equals; + getMsgTemplate = GetMsgTemplate; + } + } + + public void Process(BooleanBinaryExpression node, Action callback) + { + if (node.BinaryExpressionType != binaryOperator) + { + return; + } + + // TODO : simplification and optimization required + // Extracting nested simple predicates: + // - "A = B" combined with 'OR' operator + // - "A != B" combined with 'AND' operator + // and composing full list of options for filtered elements. + // In the output dictionary the Key will be the filtered element name + // and the Value - list of options which can be put into (NOT) IN predicate values. + var equalityComparisons = ExpandPredicates(node.FirstExpression) + .Union(ExpandPredicates(node.SecondExpression)) + .GroupBy(cmp => cmp.FilteredItem, StringComparer.OrdinalIgnoreCase) + .Where(grp => grp.Count() >= MinValuesToCollapse && grp.Max(v => v.ShouldBeReported)) + .ToDictionary( + grp => grp.Key, + grp => grp + .Where(s => !string.IsNullOrEmpty(s.FilterValueText)) + .DistinctBy(value => value.FilterValueText, StringComparer.OrdinalIgnoreCase) + .OrderBy(value => value.FilterValueText, StringComparer.OrdinalIgnoreCase) + .ToList(), + StringComparer.OrdinalIgnoreCase); + + // Composing messages with recommended IN/NOT IN expression + foreach (var collapse in equalityComparisons) + { + string values = string.Join( + ", ", + collapse.Value + .Select(v => v.FilterValueText) + .Take(MaxValuesToShow)); + + string exampleTemplate = getMsgTemplate(collapse.Value.Count > MaxValuesToShow); + + callback.Invoke( + collapse.Value[0].FilterValue, + string.Format(exampleTemplate, collapse.Key, values)); + } + } + + private static string GetMsgTemplate(bool limitedNumberOfelements) + { + return limitedNumberOfelements + ? ExampleTemplateWithSomeValues + : ExampleTemplateWithAllValues; + } + + private static string GetMsgTemplateNegated(bool limitedNumberOfelements) + { + return limitedNumberOfelements + ? ExampleTemplateWithSomeValuesNegated + : ExampleTemplateWithAllValuesNegated; + } + + /// + /// It will return only equality comparisons combined with OR operator. + /// On one of the sides there must be a column reference or variable + /// and on the other side either literal or variable. + /// + private IEnumerable ExpandPredicates(BooleanExpression node) + { + if (node is BooleanBinaryExpression bin) + { + if (bin.BinaryExpressionType != binaryOperator) + { + return Enumerable.Empty(); + } + + return ExpandPredicates(bin.FirstExpression). + Union(ExpandPredicates(bin.SecondExpression)); + } + + return ExpandExpression(node); + } + + private IEnumerable ExpandExpression(BooleanExpression node) + { + while (node is BooleanParenthesisExpression pe) + { + node = pe.Expression; + } + + if (node is BooleanComparisonExpression cmp && cmp.ComparisonType == equalityOperator) + { + var leftSide = cmp.FirstExpression.ExtractScalarExpression(); + var rightSide = cmp.SecondExpression.ExtractScalarExpression(); + + var predicate = CombinablePredicate.Make(leftSide, rightSide); + if (predicate != null) + { + yield return predicate; + } + + // Trying both directions of comparison + predicate = CombinablePredicate.Make(rightSide, leftSide); + if (predicate != null) + { + yield return predicate; + } + } + else if (node is InPredicate inPredicate && inPredicate.Values != null + && inPredicate.NotDefined == notPresence) + { + var leftSide = inPredicate.Expression.ExtractScalarExpression(); + + // TODO : not sure if exposing IN back to separate predicates is a good idea + for (int i = inPredicate.Values.Count - 1; i >= 0; i--) + { + var rightSide = inPredicate.Values[i].ExtractScalarExpression(); + + var predicate = CombinablePredicate.Make(leftSide, rightSide); + if (predicate != null) + { + // IN values are no evil + predicate.ShouldBeReported = false; + yield return predicate; + } + } + } + } + + /// + /// It represents (filtered item, filter value) pair. + /// + private sealed class CombinablePredicate + { + public CombinablePredicate(string filteredItem, VariableReference filterValue) : this(filteredItem, (TSqlFragment)filterValue) + { + FilterValueText = filterValue.Name; + } + + public CombinablePredicate(string filteredItem, Literal filterValue) : this(filteredItem, (TSqlFragment)filterValue) + { + if (filterValue is StringLiteral) + { + // String literals are stored in ScriptDom deserialized, without quotation. + // To print it with an IN predicate example quotation is needed back. Nested quotes should be escaped. + FilterValueText = "'" + filterValue.Value.Replace("'", "''") + "'"; + } + else + { + FilterValueText = filterValue.Value; + } + } + + public CombinablePredicate(string filteredItem, string filterValue, TSqlFragment filterNode) : this(filteredItem, filterNode) + { + FilterValueText = filterValue; + } + + private CombinablePredicate(string filteredItem, TSqlFragment filterValue) + { + FilteredItem = filteredItem ?? throw new ArgumentNullException(nameof(filteredItem)); + FilterValue = filterValue ?? throw new ArgumentNullException(nameof(filterValue)); + } + + public string FilteredItem { get; } + + public TSqlFragment FilterValue { get; } + + public string FilterValueText { get; } + + public bool ShouldBeReported { get; set; } = true; + + public static CombinablePredicate Make(ScalarExpression filteredItem, ScalarExpression filterValue) + { + string filteredItemName; + + // On the left side there must be a variable or a column reference + if (filteredItem is VariableReference filteredVarRef) + { + filteredItemName = filteredVarRef.Name; + } + else if (filteredItem is ColumnReferenceExpression filteredColRef + && filteredColRef.MultiPartIdentifier != null) + { + // filteredColRef.MultiPartIdentifier can be null for sys columns like $action + filteredItemName = filteredColRef.MultiPartIdentifier.Identifiers.GetFullName(); + } + else + { + return default; + } + + // On the right side there must be something simple: literal or variable + if (filterValue is VariableReference filterValueAsVar) + { + return new CombinablePredicate(filteredItemName, filterValueAsVar); + } + else if (filterValue is Literal filterValueAsLiteral) + { + return new CombinablePredicate(filteredItemName, filterValueAsLiteral); + } + + // Any other case is not supported. Complex IN predicate values are not welcome. + return default; + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Routines/DatabaseObjectIdentifierDetector.cs b/TeamTools.TSQL.Linter/Routines/DatabaseObjectIdentifierDetector.cs index ad8b7184..7403d4d1 100644 --- a/TeamTools.TSQL.Linter/Routines/DatabaseObjectIdentifierDetector.cs +++ b/TeamTools.TSQL.Linter/Routines/DatabaseObjectIdentifierDetector.cs @@ -1,5 +1,6 @@ using Microsoft.SqlServer.TransactSql.ScriptDom; using System; +using System.Collections.Generic; namespace TeamTools.TSQL.Linter.Routines { @@ -77,17 +78,12 @@ public override void Visit(DropObjectsStatement node) public override void Visit(TableReferenceWithAliasAndColumns node) { - // Generic SchemaObjectName would catch - if (definitionOnly) + if (ignoreAliases) { return; } - int n = node.Columns.Count; - for (int i = 0; i < n; i++) - { - IdentifierDetected(node.Columns[i]); - } + ListOfIdentifiersDetected(node.Columns); } public override void Visit(CreateTableStatement node) @@ -215,7 +211,7 @@ public override void Visit(DeclareTableVariableBody node) public override void Visit(IndexStatement node) => IdentifierDetected(node.Name); - public override void Visit(ColumnDefinition node) => IdentifierDetected(node.ColumnIdentifier); + public override void Visit(ColumnDefinitionBase node) => IdentifierDetected(node.ColumnIdentifier); public override void Visit(CreateSchemaStatement node) => IdentifierDetected(node.Name); @@ -239,7 +235,7 @@ public override void Visit(TableReferenceWithAlias node) public override void Visit(SelectScalarExpression node) { - if (definitionOnly || ignoreAliases) + if (ignoreAliases) { return; } @@ -255,6 +251,7 @@ public override void Visit(CommonTableExpression node) } IdentifierDetected(node.ExpressionName); + ListOfIdentifiersDetected(node.Columns); } public override void Visit(SecurityStatement node) @@ -264,17 +261,19 @@ public override void Visit(SecurityStatement node) return; } - var ids = node.SecurityTargetObject.ObjectName.MultiPartIdentifier.Identifiers; - int n = ids.Count; + ListOfIdentifiersDetected(node.SecurityTargetObject.ObjectName.MultiPartIdentifier.Identifiers); + } - for (int i = 0; i < n; i++) + private static string RemoveVarPrefix(string varName) => varName.Substring(1); + + private void ListOfIdentifiersDetected(IList columns) + { + for (int i = 0, n = columns.Count; i < n; i++) { - IdentifierDetected(ids[i]); + IdentifierDetected(columns[i]); } } - private static string RemoveVarPrefix(string varName) => varName.Substring(1); - private void IdentifierDetected(Identifier node, string cleanedName = null) { if (node is null) diff --git a/TeamTools.TSQL.Linter/Routines/InvisibleCharDetector.cs b/TeamTools.TSQL.Linter/Routines/InvisibleCharDetector.cs new file mode 100644 index 00000000..499b54a3 --- /dev/null +++ b/TeamTools.TSQL.Linter/Routines/InvisibleCharDetector.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TeamTools.TSQL.Linter.Routines +{ + internal static class InvisibleCharDetector + { + private static readonly char ZeroCodeChar = '\u007f'; + private static readonly Dictionary InvisibleChars = new Dictionary + { + { '\u000B', "Line tabulation" }, + { '\u000C', "Form feed" }, + { '\u070F', "Syriac Abbreviation Mark" }, + { '\u180B', "Mongolian Free Variation Selector One" }, + { '\u180C', "Mongolian Free Variation Selector Two" }, + { '\u180D', "Mongolian Free Variation Selector Three" }, + { '\u180E', "Mongolian Vowel Separator" }, + { '\u2000', "En Quad" }, + { '\u2001', "Em Quad" }, + { '\u2002', "En Space" }, + { '\u2003', "Em Space" }, + { '\u2004', "Three-Per-Em Space" }, + { '\u2005', "Four-Per-Em Space" }, + { '\u2006', "Six-Per-Em Space" }, + { '\u2007', "Figure Space" }, + { '\u2008', "Punctuation Space" }, + { '\u2009', "Thin Space" }, + { '\u200A', "Hair Space" }, + { '\u200B', "Zero Width Space" }, + { '\u200C', "Zero Width Non-Joiner" }, + { '\u200D', "Zero Width Joiner" }, + { '\u200E', "Left To Right Mark" }, + { '\u200F', "Right To Left Mark" }, + { '\u202A', "Left To Right Embedding" }, + { '\u202B', "Right To Left Embedding" }, + { '\u202C', "Pop Directional Formatting" }, + { '\u202D', "Left To Right Override" }, + { '\u202E', "Right To Left Override" }, + { '\u202F', "Narrow No-Break Space" }, + { '\u205F', "Medium Mathematical Space" }, + { '\u2060', "Word Joiner" }, + { '\u2062', "Invisible Times" }, + { '\u2063', "Invisible Separator" }, + { '\u2064', "Invisible Plus" }, + { '\u2065', "Invisible Operators" }, + { '\u2800', "Braille Pattern Blank" }, + { '\uFEFF', "Zero Width No-break Space" }, + }; + + public static int LocateInvisibleChar(string value, out string charDescription) + { + charDescription = null; + + if (string.IsNullOrEmpty(value)) + { + return -1; + } + + int n = value.Length; + for (int i = 0; i < n; i++) + { + var c = value[i]; + if (c != ZeroCodeChar && InvisibleChars.TryGetValue(c, out var symbolName)) + { + charDescription = symbolName; + return i; + } + } + + return -1; + } + } +} diff --git a/TeamTools.TSQL.Linter/Routines/NetStandardExtensions.cs b/TeamTools.TSQL.Linter/Routines/NetStandardExtensions.cs index d86ea27a..04fef7c2 100644 --- a/TeamTools.TSQL.Linter/Routines/NetStandardExtensions.cs +++ b/TeamTools.TSQL.Linter/Routines/NetStandardExtensions.cs @@ -46,12 +46,36 @@ public static TVal GetValueOrDefault(this IDictionary di return def; } + + public static IEnumerable DistinctBy(this IEnumerable source, Func keySelector) + { + HashSet seenKeys = new HashSet(); + foreach (TSource element in source) + { + if (seenKeys.Add(keySelector(element))) + { + yield return element; + } + } + } + + public static IEnumerable DistinctBy(this IEnumerable source, Func keySelector, IEqualityComparer comparer) + { + HashSet seenKeys = new HashSet(comparer); + foreach (TSource element in source) + { + if (seenKeys.Add(keySelector(element))) + { + yield return element; + } + } + } #endif public static int LineCount(this string str) { // just \n is fine here - we are not doing split, just counting - // no matter whether \r preceides it or not + // no matter whether \r precedes it or not const char LineBreakChar = '\n'; // the string itself takes at least 1 line for sure // TODO : shouldn't it return 0 for empty string or null? diff --git a/TeamTools.TSQL.Linter/Routines/ScriptDomExtensions/ScriptDomExtension.cs b/TeamTools.TSQL.Linter/Routines/ScriptDomExtensions/ScriptDomExtension.cs index 04694dae..ad4ad612 100644 --- a/TeamTools.TSQL.Linter/Routines/ScriptDomExtensions/ScriptDomExtension.cs +++ b/TeamTools.TSQL.Linter/Routines/ScriptDomExtensions/ScriptDomExtension.cs @@ -1,6 +1,7 @@ using Microsoft.SqlServer.TransactSql.ScriptDom; using System; using System.Collections.Generic; +using System.Diagnostics; using TeamTools.Common.Linting; namespace TeamTools.TSQL.Linter.Routines @@ -35,6 +36,7 @@ public static bool IsSkippableTokens(TSqlTokenType token) public static bool IsTokensNotNeedingSpaceAround(TSqlTokenType token) { return token == TSqlTokenType.Comma + || token == TSqlTokenType.WhiteSpace || token == TSqlTokenType.LeftParenthesis || token == TSqlTokenType.RightParenthesis || token == TSqlTokenType.Plus @@ -289,16 +291,35 @@ public static string GetFragmentCleanedText(this TSqlFragment node) var sb = ObjectPools.StringBuilderPool.Get(); bool lastWasSpace = false; int start = node.FirstTokenIndex; - int end = node.LastTokenIndex + 1; + int end = node.LastTokenIndex + 1; // +1 - to be able to use i < end & [i + 1] in the loop below + + if (node is StatementList block && start == -1 && block.Statements.Count > 0) + { + // Crutch for StatementList which has both FirstTokenIndex and LastTokenIndex == -1 + start = block.Statements[0].FirstTokenIndex; + end = block.Statements[block.Statements.Count - 1].LastTokenIndex + 1; + } + + if (start < 0) + { + Debug.Fail("Broken TSqlFragment boundaries! " + node.GetType().Name); + // Unable to run the loop + return default; + } + + bool noSpaceNeededAfter; for (int i = start; i < end; i++) { var token = node.ScriptTokenStream[i]; + noSpaceNeededAfter = false; + if (IsTokensNotNeedingSpaceAround(token.TokenType) || (i == end - 1) || IsTokensNotNeedingSpaceAround(node.ScriptTokenStream[i + 1].TokenType)) { lastWasSpace = true; + noSpaceNeededAfter = true; } if (IsSkippableTokens(token.TokenType)) @@ -308,8 +329,8 @@ public static string GetFragmentCleanedText(this TSqlFragment node) continue; } - // Not sure why, but this space started breaking some code (comparison fails) - // result.Append(' '); + sb.Append(' '); + lastWasSpace = true; } else @@ -328,7 +349,7 @@ public static string GetFragmentCleanedText(this TSqlFragment node) sb.Append(token.Text); } - lastWasSpace = false; + lastWasSpace = noSpaceNeededAfter; } } diff --git a/TeamTools.TSQL.Linter/Rules/Ambiguity/SchemaQualifiedProcCallRule.cs b/TeamTools.TSQL.Linter/Rules/Ambiguity/SchemaQualifiedProcCallRule.cs index 833e338b..fc9e167d 100644 --- a/TeamTools.TSQL.Linter/Rules/Ambiguity/SchemaQualifiedProcCallRule.cs +++ b/TeamTools.TSQL.Linter/Rules/Ambiguity/SchemaQualifiedProcCallRule.cs @@ -34,6 +34,14 @@ public override void Visit(ExecutableProcedureReference node) return; } + // TODO : shouldn't it detect longer sequences of such symbols? + if (name.BaseIdentifier.Value.Length == 1 + && InvisibleCharDetector.LocateInvisibleChar(name.BaseIdentifier.Value, out var _) == 0) + { + // this is an invisible unicode symbol which is supposed to be detected by a separate rule + return; + } + HandleNodeError(name); } } diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/CommentHasInvisibleCharRule.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/CommentHasInvisibleCharRule.cs new file mode 100644 index 00000000..b0fbaf24 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/CommentHasInvisibleCharRule.cs @@ -0,0 +1,37 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + // See also IdentifierHasInvisibleCharRule, LiteralHasInvisibleCharRule + [RuleIdentity("CS0842", "INVISIBLE_CHAR_IN_COMMENT")] + internal sealed class CommentHasInvisibleCharRule : AbstractRule + { + public CommentHasInvisibleCharRule() : base() + { + } + + protected override void ValidateScript(TSqlScript node) + { + for (int i = 0, n = node.ScriptTokenStream.Count; i < n; i++) + { + var token = node.ScriptTokenStream[i]; + if (token.TokenType == TSqlTokenType.SingleLineComment || token.TokenType == TSqlTokenType.MultilineComment) + { + ValidateChars(token.Text, token.Line, token.Column); + } + } + } + + private void ValidateChars(string text, int line, int col) + { + int badCharPos = InvisibleCharDetector.LocateInvisibleChar(text, out string symbolName); + if (badCharPos >= 0) + { + // TODO: point to exact line and col of multiline comment + HandleLineError(line, col, symbolName); + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/ComparedExpressionsEqualRule.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/ComparedExpressionsEqualRule.cs new file mode 100644 index 00000000..b3b0ae98 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/ComparedExpressionsEqualRule.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 +{ + [RuleIdentity("CS0853", "COMPARISON_LEFT_EQUALS_RIGHT")] + internal sealed class ComparedExpressionsEqualRule : AbstractRule + { + public ComparedExpressionsEqualRule() : base() + { + } + + public override void Visit(BooleanComparisonExpression node) + => ValidateComparisonSides(node.FirstExpression, node.SecondExpression); + + public override void Visit(LikePredicate node) + => ValidateComparisonSides(node.FirstExpression, node.SecondExpression); + + public override void Visit(BooleanTernaryExpression node) + { + ValidateComparisonSides(node.FirstExpression, node.SecondExpression); + ValidateComparisonSides(node.FirstExpression, node.ThirdExpression); + } + + private static bool AreEqualExpressions(ScalarExpression left, ScalarExpression right) + { + while (left is ParenthesisExpression pe) + { + left = pe.Expression; + } + + while (right is ParenthesisExpression pe) + { + right = pe.Expression; + } + + if (left is Literal l && int.TryParse(l.Value, out int literalValue) + && (literalValue == 0 || literalValue == 1)) + { + // 1 = 1, 0 != 0 are used sometimes on purpose + return false; + } + + return BooleanExpressionComparer.AreEqualExpressions(left, right); + } + + private void ValidateComparisonSides(ScalarExpression left, ScalarExpression right) + { + if (AreEqualExpressions(left, right)) + { + HandleNodeError(right); + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/ConditionsLeadToSimilarBehaviorRule.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/ConditionsLeadToSimilarBehaviorRule.cs new file mode 100644 index 00000000..1067f530 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/ConditionsLeadToSimilarBehaviorRule.cs @@ -0,0 +1,135 @@ +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("CS0844", "CONDITIONS_SAME_DECISIONS")] + internal sealed class ConditionsLeadToSimilarBehaviorRule : AbstractRule + { + public ConditionsLeadToSimilarBehaviorRule() : base() + { + } + + public override void Visit(SimpleCaseExpression node) + { + var flows = new List(node.WhenClauses.Count + 1); + for (int i = node.WhenClauses.Count - 1; i >= 0; i--) + { + flows.Add(node.WhenClauses[i].ThenExpression); + } + + if (node.ElseExpression != null) + { + flows.Add(node.ElseExpression); + } + + DetectDups(flows); + } + + // Searched Case and Simple Case are very similar however don't have common ancestor + public override void Visit(SearchedCaseExpression node) + { + var flows = new List(node.WhenClauses.Count + 1); + for (int i = node.WhenClauses.Count - 1; i >= 0; i--) + { + flows.Add(node.WhenClauses[i].ThenExpression); + } + + if (node.ElseExpression != null) + { + flows.Add(node.ElseExpression); + } + + DetectDups(flows); + } + + public override void Visit(IfStatement node) + { + var flows = new List(); + + while (node != null) + { + flows.Add(node.ThenStatement); + if (node.ElseStatement is IfStatement elseIf) + { + node = elseIf; + } + else + { + if (node.ElseStatement != null) + { + flows.Add(node.ElseStatement); + } + + node = null; + } + } + + DetectDups(flows); + } + + public override void Visit(IIfCall node) + { + var flows = new List(2); + flows.Add(node.ThenExpression); + flows.Add(node.ElseExpression); + + DetectDups(flows); + } + + // Expanding flow expression/statement list + private static TSqlFragment ExpandFragment(TSqlFragment node) + { + while (node is ParenthesisExpression pe) + { + node = pe.Expression; + } + + while (node is BeginEndBlockStatement be) + { + if (be.StatementList.Statements.Count == 1) + { + node = be.StatementList.Statements[0]; + } + else + { + node = be.StatementList; + } + } + + return node; + } + + private void DetectDups(IList flows) + where T : TSqlFragment + { + if (flows is null || flows.Count < 2) + { + return; + } + + string priorFlowCode = null; + + for (int i = flows.Count - 1; i >= 0; i--) + { + TSqlFragment flow = ExpandFragment(flows[i]); + + string flowCode = flow.GetFragmentCleanedText(); + if (priorFlowCode is null) + { + priorFlowCode = flowCode; + } + else if (!string.Equals(priorFlowCode, flowCode, StringComparison.OrdinalIgnoreCase)) + { + // found something different - no violation + return; + } + } + + HandleNodeError(flows[0]); + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/DuplicateConditionRule.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/DuplicateConditionRule.cs index 166b6b1f..4e8bd6ae 100644 --- a/TeamTools.TSQL.Linter/Rules/CodeSmell/DuplicateConditionRule.cs +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/DuplicateConditionRule.cs @@ -162,54 +162,10 @@ public override void Visit(BooleanBinaryExpression node) } } - // Extracting the expression from parenthesis or fake scalar select - private static ScalarExpression ExtractExpression(ScalarExpression expr) - { - while (expr is ParenthesisExpression pe) - { - expr = pe.Expression; - } - - if (expr is ScalarSubquery q) - { - var spec = q.QueryExpression.GetQuerySpecification(); - if (spec != null - && spec.FromClause is null - && spec.WhereClause is null - && spec.SelectElements.Count == 1 - && spec.SelectElements[0] is SelectScalarExpression sel) - { - // if subquery is selecting single column with no WHERE and FROM - // then we can grab selected scalar expression itself - return ExtractExpression(sel.Expression); - } - } - - return expr; - } - - private static BooleanExpression ExtractExpression(BooleanExpression expr) - { - while (expr is BooleanParenthesisExpression pe) - { - expr = pe.Expression; - } - - return expr; - } - // Extracting nested boolean expression if any private static IEnumerable ExtractNestedExpressions(BooleanExpression node) { - if (node is BooleanParenthesisExpression pe) - { - foreach (var e in ExtractNestedExpressions(pe.Expression)) - { - yield return e; - } - - yield break; - } + node = ExtractExpression(node); if (node is BooleanBinaryExpression bin) { @@ -244,118 +200,15 @@ private static IEnumerable ExtractNestedExpressions(BooleanEx yield return node; } - // Normalizing boolean predicates to be able to detect equal expression even - // if they are written in reversed or negated way - // e.g. (@a < @b) is the same as (@b > @a) and NOT (@a >= @b) private static BooleanExpressionParts NormalizeBooleanExpression(BooleanExpression expr) - { - var result = new BooleanExpressionParts - { - OriginalExpression = expr, - }; - - // TODO : BooleanBinaryExpression? at least sort - if (expr is BooleanComparisonExpression cmp) - { - // TODO : handle NotGreaterThan as negation - result.ComparisonType = GetNormalizedComparisonType(cmp.ComparisonType); - result.FirstExpression = ExtractExpression(cmp.FirstExpression); - result.SecondExpression = ExtractExpression(cmp.SecondExpression); - - if (result.ComparisonType != cmp.ComparisonType) - { - // switching sides of expression if needed for normalization - (result.FirstExpression, result.SecondExpression) = (result.SecondExpression, result.FirstExpression); - } - else if (result.ComparisonType == BooleanComparisonType.Equals) - { - // normalizing by sorting alphabetically if comparison has no direction (< or >) - string firstExpression = result.FirstExpression.GetFragmentCleanedText(); - string secondExpression = result.SecondExpression.GetFragmentCleanedText(); - if (string.Compare(firstExpression, secondExpression) > 0) - { - // switching sides of expression if needed for normalization - (result.FirstExpression, result.SecondExpression) = (result.SecondExpression, result.FirstExpression); - } - } - else if (result.ComparisonType == BooleanComparisonType.NotEqualToExclamation) - { - // replacing with equal comparison type for similarity - // != -> <> - result.ComparisonType = BooleanComparisonType.NotEqualToBrackets; - } - } + => BooleanExpressionNormalizer.Normalize(expr); - if (expr is BooleanNotExpression not) - { - // expanding NOT (@a >= @b) to @a < @b and normalizing to @b > @a - result = NormalizeBooleanExpression(ExtractExpression(not.Expression)); - var comparisonType = GetNormalizedComparisonType(GetNegatedComparisonType(result.ComparisonType)); - if (comparisonType != result.ComparisonType) - { - result.ComparisonType = comparisonType; - // switching sides of expression if needed for normalization - (result.FirstExpression, result.SecondExpression) = (result.SecondExpression, result.FirstExpression); - } - } - - return result; - } - - // Normalizing comparison types: all left-sided (<, <=, !<) to right-sided (>, >=, !>) - private static BooleanComparisonType GetNormalizedComparisonType(BooleanComparisonType cmp) - { - switch (cmp) - { - case BooleanComparisonType.LessThan: - return BooleanComparisonType.GreaterThan; - - case BooleanComparisonType.LessThanOrEqualTo: - return BooleanComparisonType.GreaterThanOrEqualTo; - - case BooleanComparisonType.NotLessThan: - return BooleanComparisonType.NotGreaterThan; - - default: - return cmp; - } - } - - private static BooleanComparisonType GetNegatedComparisonType(BooleanComparisonType cmp) - { - switch (cmp) - { - case BooleanComparisonType.LessThan: - return BooleanComparisonType.GreaterThanOrEqualTo; - - case BooleanComparisonType.GreaterThan: - return BooleanComparisonType.LessThanOrEqualTo; - - case BooleanComparisonType.LessThanOrEqualTo: - return BooleanComparisonType.GreaterThan; - - case BooleanComparisonType.GreaterThanOrEqualTo: - return BooleanComparisonType.LessThan; - - case BooleanComparisonType.NotGreaterThan: - return BooleanComparisonType.LessThanOrEqualTo; - - case BooleanComparisonType.NotLessThan: - return BooleanComparisonType.GreaterThanOrEqualTo; - - case BooleanComparisonType.Equals: - return BooleanComparisonType.NotEqualToBrackets; - - case BooleanComparisonType.NotEqualToBrackets: - return BooleanComparisonType.Equals; - - case BooleanComparisonType.NotEqualToExclamation: - return BooleanComparisonType.Equals; + // Extracting the expression from parenthesis or fake scalar select + private static ScalarExpression ExtractExpression(ScalarExpression expr) + => BooleanExpressionPartsExtractor.ExtractExpression(expr); - default: - return cmp; - } - } + private static BooleanExpression ExtractExpression(BooleanExpression expr) + => BooleanExpressionPartsExtractor.ExtractExpression(expr); // Going deeper to extract all the IF-ELSE-IF-ELSE-IF private static IEnumerable GetAllIfFlows(IfStatement node) @@ -374,17 +227,7 @@ private static IEnumerable GetAllIfFlows(IfStatement node) private static IfStatement GetElseIf(IfStatement node) { - if (node is null) - { - return default; - } - - if (node.ElseStatement is null) - { - return default; - } - - if (node.ElseStatement is IfStatement ifstmt) + if (node?.ElseStatement is IfStatement ifstmt) { return ifstmt; } @@ -392,58 +235,6 @@ private static IfStatement GetElseIf(IfStatement node) return default; } - private static bool AreEqualExpressions(TSqlFragment exprA, TSqlFragment exprB) - { - if (exprA is null && exprB is null) - { - // both are nulls - return true; - } - - if (exprA is null || exprB is null) - { - // only one is null - return false; - } - - if (exprA is Literal literA && exprB is Literal literB) - { - // this is faster than building script fragment text - return literA.Value.Equals(literB.Value, StringComparison.OrdinalIgnoreCase); - } - - if (exprA is Literal || exprB is Literal) - { - // one is a literal another is not - return false; - } - - if (exprA is VariableReference varA && exprB is VariableReference varB) - { - // this is faster than building script fragment text - return varA.Name.Equals(varB.Name, StringComparison.OrdinalIgnoreCase); - } - - if (exprA is VariableReference || exprB is VariableReference) - { - // one is a variable reference another is not - return false; - } - - // comparing expressions lexicographically since there is no smarter option for now - // TODO : something better and faster needed - return string.Equals(exprA.GetFragmentCleanedText(), exprB.GetFragmentCleanedText(), StringComparison.OrdinalIgnoreCase); - } - - private static bool AreEqualExpressions(BooleanExpressionParts exprA, BooleanExpressionParts exprB) - { - return exprA.ComparisonType == exprB.ComparisonType - && AreEqualExpressions(exprA.FirstExpression, exprB.FirstExpression) - && AreEqualExpressions(exprA.SecondExpression, exprB.SecondExpression) - && ((exprA.FirstExpression != null && exprA.SecondExpression != null) - || AreEqualExpressions(exprA.OriginalExpression, exprB.OriginalExpression)); - } - private void AddOrRaiseViolation(IList registeredItems, ScalarExpression newItem) { if (newItem is null) @@ -457,7 +248,7 @@ private void AddOrRaiseViolation(IList registeredItems, Scalar for (int i = 0; i < n; i++) { var oldItem = registeredItems[i]; - if (AreEqualExpressions(oldItem, newItem)) + if (BooleanExpressionComparer.AreEqualExpressions(oldItem, newItem)) { // if we already registered similar expression // then newItem is the duplicate we are looking for @@ -491,7 +282,7 @@ private void AddOrRaiseViolation(IList registeredItems, for (int i = 0; i < n; i++) { var oldItem = registeredItems[i]; - if (AreEqualExpressions(oldItem, newItem)) + if (BooleanExpressionComparer.AreEqualExpressions(oldItem, newItem)) { firstInstance = oldItem; break; @@ -508,16 +299,5 @@ private void AddOrRaiseViolation(IList registeredItems, newItem.FirstExpression as TSqlFragment ?? newItem.OriginalExpression, string.Format(Strings.ViolationDetails_DuplicateConditionRule_SeeDupAtLine, (firstInstance.FirstExpression as TSqlFragment ?? firstInstance.OriginalExpression).StartLine)); } - - private class BooleanExpressionParts - { - public ScalarExpression FirstExpression { get; set; } - - public ScalarExpression SecondExpression { get; set; } - - public BooleanExpression OriginalExpression { get; set; } - - public BooleanComparisonType ComparisonType { get; set; } = BooleanComparisonType.Equals; - } } } diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/FakeOuterJoinRule.OuterSourceExtraction.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/FakeOuterJoinRule.OuterSourceExtraction.cs new file mode 100644 index 00000000..07155f1f --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/FakeOuterJoinRule.OuterSourceExtraction.cs @@ -0,0 +1,107 @@ +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 +{ + internal partial class FakeOuterJoinRule + { + private static ICollection ExtractOuterSources(IList sources) + { + var outerSourceNames = new HashSet(StringComparer.OrdinalIgnoreCase); + + for (int i = sources.Count - 1; i >= 0; i--) + { + var source = sources[i]; + + // If there are no joins then nothing to extract + if (source is JoinTableReference) + { + foreach (var outerSource in ExtractOuterSources(source)) + { + outerSourceNames.Add(outerSource); + } + } + } + + return outerSourceNames; + } + + private static IEnumerable ExtractOuterSources(TableReference source) + { + if (source is QualifiedJoin qj) + { + if (qj.QualifiedJoinType == QualifiedJoinType.LeftOuter) + { + if (qj.SecondTableReference is QualifiedJoin nestedJoin) + { + // LEFT INNER ON ON - both parts of this construction are OUTER sources + return ExtractOuterSources(nestedJoin.FirstTableReference) + .Union(ExtractOuterSources(nestedJoin.SecondTableReference)); + } + else + { + return ExtractOuterSourceNames(qj.SecondTableReference); + } + } + else if (qj.QualifiedJoinType == QualifiedJoinType.RightOuter) + { + return ExtractOuterSourceNames(qj.FirstTableReference); + } + } + + if (source is JoinTableReference uj) + { + // extracting joins recursively + if (uj.FirstTableReference is JoinTableReference join1) + { + return ExtractOuterSources(join1); + } + else if (uj.SecondTableReference is JoinTableReference join2) + { + return ExtractOuterSources(join2); + } + } + else + { + // Not a join but a table reference + return ExtractOuterSourceNames(source); + } + + return Enumerable.Empty(); + } + + private static IEnumerable ExtractOuterSourceNames(TableReference sourceReference) + { + if (sourceReference is null) + { + yield break; + } + + if (sourceReference is TableReferenceWithAlias aliased && aliased.Alias != null) + { + // Outer source may be referenced either via full name or an alias if provided + yield return aliased.Alias.Value; + } + + if (sourceReference is NamedTableReference nm) + { + yield return nm.SchemaObject.GetFullName(); + + if (nm.SchemaObject.SchemaIdentifier is null + || string.Equals(nm.SchemaObject.SchemaIdentifier.Value, TSqlDomainAttributes.DefaultSchemaName, StringComparison.OrdinalIgnoreCase)) + { + // 'dbo.tbl' can be referenced as 'tbl' with schema name omitted + yield return nm.SchemaObject.BaseIdentifier.Value; + } + } + else if (sourceReference is VariableTableReference vr) + { + yield return vr.Variable.Name; + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/FakeOuterJoinRule.WherePredicateExpansion.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/FakeOuterJoinRule.WherePredicateExpansion.cs new file mode 100644 index 00000000..14bdb31e --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/FakeOuterJoinRule.WherePredicateExpansion.cs @@ -0,0 +1,79 @@ +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 FakeOuterJoinRule + { + private static IEnumerable ExpandPredicate(BooleanExpression predicate) + { + while (predicate is BooleanParenthesisExpression pe) + { + predicate = pe.Expression; + } + + // OR predicate referencing OUTER JOIN source may be absolutely valid + if (predicate is BooleanBinaryExpression bin && bin.BinaryExpressionType == BooleanBinaryExpressionType.And) + { + foreach (var e in ExpandPredicate(bin.FirstExpression)) + { + yield return e; + } + + foreach (var e in ExpandPredicate(bin.SecondExpression)) + { + yield return e; + } + } + else + { + yield return predicate; + } + } + + private static ScalarExpression ExpandExpression(ScalarExpression expr) + { + while (expr is ParenthesisExpression pe) + { + expr = pe.Expression; + } + + if (expr is UnaryExpression un) + { + // sign has no meaning in our case + return ExpandExpression(un.Expression); + } + + return expr; + } + + private static string GetReferencedSourceFullName(IList id) + { + if (id.Count == 2) + { + // 'src_alias.col' => 'src_alias' + return id[0].Value; + } + + var sb = ObjectPools.StringBuilderPool.Get(); + + // All name parts except the last one (column name) are needed + for (int i = 0, n = id.Count - 1; i < n; i++) + { + if (i > 0) + { + sb.Append(TSqlDomainAttributes.NamePartSeparator); + } + + sb.Append(id[i].Value); + } + + var fullName = sb.ToString(); + ObjectPools.StringBuilderPool.Return(sb); + return fullName; + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/FakeOuterJoinRule.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/FakeOuterJoinRule.cs new file mode 100644 index 00000000..57e44c41 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/FakeOuterJoinRule.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("CS0851", "FAKE_OUTER_JOIN")] + internal sealed partial class FakeOuterJoinRule : AbstractRule + { + public FakeOuterJoinRule() : base() + { + } + + // TODO : FROM a LEFT JOIN b ON ... INNER JOIN c ON c.id = b.id + public override void Visit(QuerySpecification node) + { + if (node.WhereClause?.SearchCondition is null) + { + // no WHERE or it is a WHERE CURRENT OF + return; + } + + if ((node.FromClause?.TableReferences?.Count ?? 0) == 0) + { + // no FROM + return; + } + + var outerSources = ExtractOuterSources(node.FromClause.TableReferences); + + if (outerSources.Count == 0) + { + // no outer joins + return; + } + + DetectPredicatesReferencingOuterSources(node.WhereClause.SearchCondition, outerSources); + } + + private void DetectOuterSourceReference(ScalarExpression possibleColReference, ICollection outerSources) + { + if (!(ExpandExpression(possibleColReference) is ColumnReferenceExpression colRef)) + { + return; + } + + var referencedIdentifiers = colRef.MultiPartIdentifier.Identifiers; + + if (referencedIdentifiers.Count <= 1) + { + // Single name means column name with no link to a specific source. + // Not possible to make outer source matching. + return; + } + + string referencedSourceName = GetReferencedSourceFullName(referencedIdentifiers); + + if (outerSources.Contains(referencedSourceName)) + { + HandleNodeError(possibleColReference, referencedSourceName); + } + else if (referencedIdentifiers.Count == 3 && string.Equals(referencedIdentifiers[0].Value, TSqlDomainAttributes.DefaultSchemaName, StringComparison.OrdinalIgnoreCase)) + { + referencedSourceName = referencedIdentifiers[1].Value; + + // Reference 'dbo.tbl.col' is the same as 'tbl.col' thus checking without schema as well + if (outerSources.Contains(referencedSourceName)) + { + HandleNodeError(possibleColReference, referencedSourceName); + } + } + } + + private void DetectPredicatesReferencingOuterSources(BooleanExpression wherePredicate, ICollection outerSources) + { + foreach (BooleanExpression predicate in ExpandPredicate(wherePredicate)) + { + if (predicate is BooleanComparisonExpression cmp) + { + DetectOuterSourceReference(cmp.FirstExpression, outerSources); + DetectOuterSourceReference(cmp.SecondExpression, outerSources); + } + else if (predicate is BooleanIsNullExpression isnull && isnull.IsNot) + { + // col IS NOT NULL + DetectOuterSourceReference(isnull.Expression, outerSources); + } + else if (predicate is InPredicate inpred) + { + DetectOuterSourceReference(inpred.Expression, outerSources); + } + else if (predicate is LikePredicate likepred) + { + DetectOuterSourceReference(likepred.FirstExpression, outerSources); + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/IdentifierHasInvisibleCharRule.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/IdentifierHasInvisibleCharRule.cs new file mode 100644 index 00000000..f69bccab --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/IdentifierHasInvisibleCharRule.cs @@ -0,0 +1,24 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("CS0841", "INVISIBLE_CHAR_IN_IDENTIFIER")] + internal class IdentifierHasInvisibleCharRule : AbstractRule + { + public IdentifierHasInvisibleCharRule() : base() + { + } + + public override void Visit(Identifier node) + { + int badCharPos = InvisibleCharDetector.LocateInvisibleChar(node.Value, out string symbolName); + if (badCharPos >= 0) + { + HandleLineError(node.StartLine, node.StartColumn + badCharPos, symbolName); + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/InsertedDeletedIrrelevanceRule.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/InsertedDeletedIrrelevanceRule.cs new file mode 100644 index 00000000..d331f6ed --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/InsertedDeletedIrrelevanceRule.cs @@ -0,0 +1,102 @@ +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("CS0843", "OUTPUT_MISMATCHES_ACTION")] + internal sealed class InsertedDeletedIrrelevanceRule : AbstractRule + { + public InsertedDeletedIrrelevanceRule() : base() + { + } + + public override void ExplicitVisit(InsertSpecification node) + { + var cols = node.OutputClause?.SelectColumns ?? node.OutputIntoClause?.SelectColumns; + Detect(TSqlDomainAttributes.TriggerSystemTables.Deleted, cols); + } + + public override void ExplicitVisit(DeleteSpecification node) + { + var cols = node.OutputClause?.SelectColumns ?? node.OutputIntoClause?.SelectColumns; + Detect(TSqlDomainAttributes.TriggerSystemTables.Inserted, cols); + } + + public override void ExplicitVisit(MergeSpecification node) + { + bool hasInsert = false; + bool hasDelete = false; + + for (int i = 0, n = node.ActionClauses.Count; i < n; i++) + { + var act = node.ActionClauses[i].Action; + + if (act is InsertMergeAction) + { + hasInsert = true; + } + else if (act is DeleteMergeAction) + { + hasDelete = true; + } + else if (act is UpdateMergeAction) + { + hasInsert = true; + hasDelete = true; + } + } + + if (hasInsert && hasDelete) + { + // Both INSERTED and DELETED may contain data + return; + } + + var irrelevantTbl = hasInsert ? TSqlDomainAttributes.TriggerSystemTables.Deleted : TSqlDomainAttributes.TriggerSystemTables.Inserted; + var cols = node.OutputClause?.SelectColumns ?? node.OutputIntoClause?.SelectColumns; + Detect(irrelevantTbl, cols); + } + + private static Identifier GetColumnIdentifier(SelectElement element) + { + if (element is SelectStarExpression star) + { + if (star.Qualifier.Count == 1) + { + // .* + return star.Qualifier[0]; + } + } + else if (element is SelectScalarExpression expr && expr.Expression is ColumnReferenceExpression col) + { + if (col.MultiPartIdentifier?.Count == 2) + { + // . + return col.MultiPartIdentifier[0]; + } + } + + return default; + } + + private void Detect(string irrelevantEventTable, IList selectedCols) + { + if (selectedCols is null || selectedCols.Count == 0) + { + return; + } + + for (int i = selectedCols.Count - 1; i >= 0; i--) + { + var colSource = GetColumnIdentifier(selectedCols[i]); + if (colSource != null && string.Equals(colSource.Value, irrelevantEventTable, StringComparison.OrdinalIgnoreCase)) + { + HandleNodeError(colSource, irrelevantEventTable); + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/LiteralHasInvisibleCharRule.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/LiteralHasInvisibleCharRule.cs index 8f5dc352..55968d6b 100644 --- a/TeamTools.TSQL.Linter/Rules/CodeSmell/LiteralHasInvisibleCharRule.cs +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/LiteralHasInvisibleCharRule.cs @@ -2,66 +2,23 @@ using System.Collections.Generic; using TeamTools.Common.Linting; using TeamTools.TSQL.Linter.Properties; +using TeamTools.TSQL.Linter.Routines; namespace TeamTools.TSQL.Linter.Rules { [RuleIdentity("CS0793", "INVISIBLE_CHAR")] internal sealed class LiteralHasInvisibleCharRule : AbstractRule { - private static readonly Dictionary InvisibleChars; - - static LiteralHasInvisibleCharRule() - { - // TODO : add some more - InvisibleChars = new Dictionary - { - { '\u070F', "Syriac Abbreviation Mark" }, - { '\u2000', "En Quad" }, - { '\u2001', "Em Quad" }, - { '\u2002', "En Space" }, - { '\u2003', "Em Space" }, - { '\u2004', "Three-Per-Em Space" }, - { '\u2005', "Four-Per-Em Space" }, - { '\u2006', "Six-Per-Em Space" }, - { '\u2007', "Figure Space" }, - { '\u2008', "Punctuation Space" }, - { '\u2009', "Thin Space" }, - { '\u200A', "Hair Space" }, - { '\u200B', "Zero-Width Space" }, - { '\u200C', "Zero Width Non-Joiner" }, - { '\u200D', "Zero Width Joiner" }, - { '\u200E', "Left-To-Right Mark" }, - { '\u200F', "Right-To-Left Mark" }, - { '\u202F', "Narrow No-Break Space" }, - { '\u205F', "Medium Mathematical Space" }, - { '\u2060', "Word Joiner" }, - { '\u2800', "Braille Pattern Blank" }, - { '\uFEFF', "Zero Width No-break Space" }, - }; - } - public LiteralHasInvisibleCharRule() : base() { } public override void Visit(StringLiteral node) { - if (string.IsNullOrEmpty(node.Value)) - { - return; - } - - const char zeroCodeChar = '\0'; - - int n = node.Value.Length; - for (int i = 0; i < n; i++) + int badCharPos = InvisibleCharDetector.LocateInvisibleChar(node.Value, out string symbolName); + if (badCharPos >= 0) { - var c = node.Value[i]; - if (c != zeroCodeChar && InvisibleChars.TryGetValue(c, out var symbolName)) - { - HandleNodeError(node, string.Format(Strings.ViolationDetails_LiteralHasInvisibleCharRule_SymbolAtPos, symbolName, i.ToString())); - return; - } + HandleNodeError(node, string.Format(Strings.ViolationDetails_LiteralHasInvisibleCharRule_SymbolAtPos, symbolName, badCharPos.ToString())); } } } diff --git a/TeamTools.TSQL.Linter/Rules/CodeSmell/NonCorrelatedJoinPredicateRule.cs b/TeamTools.TSQL.Linter/Rules/CodeSmell/NonCorrelatedJoinPredicateRule.cs new file mode 100644 index 00000000..4c2ef796 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/CodeSmell/NonCorrelatedJoinPredicateRule.cs @@ -0,0 +1,281 @@ +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("CS0852", "NON_CORRELATED_JOIN_PREDICATE")] + internal sealed class NonCorrelatedJoinPredicateRule : AbstractRule + { + public NonCorrelatedJoinPredicateRule() : base() + { + } + + public override void Visit(QuerySpecification node) + { + if ((node.FromClause?.TableReferences?.Count ?? 0) == 0) + { + // no FROM + return; + } + + if (node.FromClause.TableReferences.Count == 1 + && !(node.FromClause.TableReferences[0] is JoinTableReference)) + { + // simple select with single source + return; + } + + ValidateJoins(node.FromClause.TableReferences); + } + + // It extracts names of a source possible for use as addressing: alias if provided, + // fully qualified name, name with "dbo" schema omitted + private static ICollection ExtractSourceNames(TableReference source, bool takePrior = false) + { + var names = new HashSet(StringComparer.OrdinalIgnoreCase); + + while (source is QualifiedJoin qj) + { + source = takePrior ? qj.SecondTableReference : qj.FirstTableReference; + } + + if (source is TableReferenceWithAlias aliased && aliased.Alias != null) + { + names.Add(aliased.Alias.Value); + } + + if (source is NamedTableReference named) + { + names.Add(named.SchemaObject.GetFullName()); + + if (named.SchemaObject.SchemaIdentifier is null + || string.Equals(named.SchemaObject.SchemaIdentifier.Value, TSqlDomainAttributes.DefaultSchemaName, StringComparison.OrdinalIgnoreCase)) + { + // default schema can be omitted in references + names.Add(named.SchemaObject.BaseIdentifier.Value); + } + } + + return names; + } + + // At least one condition in a predicate must refer to then given table source and some other table source + private static bool IsValidJoin(QualifiedJoin join) + { + var leftNames = ExtractSourceNames(join.FirstTableReference, true); + var rightNames = ExtractSourceNames(join.SecondTableReference); + + return IsJoinPredicateCorrelated(join.SearchCondition, leftNames, rightNames); + } + + // Both parts of a boolean comparison expression must be linked to at least 2 sources + private static bool IsJoinPredicateCorrelated(BooleanExpression predicate, ICollection leftNames, ICollection rightNames) + { + while (predicate is BooleanParenthesisExpression pe) + { + predicate = pe.Expression; + } + + if (predicate is BooleanBinaryExpression bin) + { + if (bin.BinaryExpressionType == BooleanBinaryExpressionType.And) + { + // for AND either of expression must be fine + return IsJoinPredicateCorrelated(bin.FirstExpression, leftNames, rightNames) + || IsJoinPredicateCorrelated(bin.SecondExpression, leftNames, rightNames); + } + else + { + // for OR all of the expressions must be fine + return IsJoinPredicateCorrelated(bin.FirstExpression, leftNames, rightNames) + && IsJoinPredicateCorrelated(bin.SecondExpression, leftNames, rightNames); + } + } + + if (predicate is BooleanComparisonExpression cmp) + { + return CorrelatedColumnReferenceDetector.HasCorrelatedRefs(cmp.FirstExpression, cmp.SecondExpression, leftNames, rightNames); + } + + if (predicate is LikePredicate like) + { + return CorrelatedColumnReferenceDetector.HasCorrelatedRefs(like.FirstExpression, like.SecondExpression, leftNames, rightNames); + } + + if (predicate is BooleanTernaryExpression between) + { + return CorrelatedColumnReferenceDetector.HasCorrelatedRefs(between.FirstExpression, between.SecondExpression, leftNames, rightNames) + && CorrelatedColumnReferenceDetector.HasCorrelatedRefs(between.FirstExpression, between.ThirdExpression, leftNames, rightNames); + } + + // IS [NOT] NULL and such cannot be correlated + return false; + } + + // Recursivly expanding nested/linked joins to find all joins and join predicates + private static IEnumerable ExpandJoins(JoinTableReference join) + { + if (join is QualifiedJoin qj) + { + yield return qj; + } + + // Expanding nested/linked joins recursively + if (join.FirstTableReference is JoinTableReference firstSrc) + { + foreach (var j in ExpandJoins(firstSrc)) + { + yield return j; + } + } + + if (join.SecondTableReference is JoinTableReference secondSrc) + { + foreach (var j in ExpandJoins(secondSrc)) + { + yield return j; + } + } + } + + private void ValidateJoins(IList sources) + { + for (int i = sources.Count - 1; i >= 0; i--) + { + var src = sources[i]; + if (src is JoinTableReference jtr) + { + foreach (var join in ExpandJoins(jtr)) + { + if (!IsValidJoin(join)) + { + HandleNodeError(join.SearchCondition); + } + } + } + } + } + + // It finds all the column references and tries to realize if it is linked to either + // of two joined sources. If both parts of a given boolean comparison expression (join predicate) + // are linked to something and at least one of the parts is linked particularly to left or right source + // used in the JOIN then everything is fine. + private sealed class CorrelatedColumnReferenceDetector : TSqlFragmentVisitor + { + private readonly ICollection leftNames; + private readonly ICollection rightNames; + + public CorrelatedColumnReferenceDetector(ICollection leftNames, ICollection rightNames) + { + this.leftNames = leftNames; + this.rightNames = rightNames; + } + + [Flags] + private enum ColumnSourceFlags + { + None = 0, + Left = 1, + Right = 2, + Another = 4, + Both = Left | Right, + External = Right | Another, + } + + private bool ColumnsDontHaveTableAlias { get; set; } + + private ColumnSourceFlags CurrentDetections { get; set; } = ColumnSourceFlags.None; + + private ColumnSourceFlags LeftDetections { get; set; } = ColumnSourceFlags.None; + + private ColumnSourceFlags RightDetections { get; set; } = ColumnSourceFlags.None; + + public static bool HasCorrelatedRefs(ScalarExpression first, ScalarExpression second, ICollection leftNames, ICollection rightNames) + { + var instance = new CorrelatedColumnReferenceDetector(leftNames, rightNames); + + instance.LeftDetections = instance.DetectSourceRefs(first); + instance.RightDetections = instance.DetectSourceRefs(second); + + // Both "left" and "right" sources must be mentioned on one or two sides of the predicate. + // Also some other source from the query may be mentioned with either "left" or "right" source. + // If there are columns referenced without explicitly defined source alias then + // violation should not be reported because everything may be fine in fact. + // Another rule should report such unqualified references as violations. + return (instance.LeftDetections | instance.RightDetections) == ColumnSourceFlags.Both + || (instance.LeftDetections | instance.RightDetections) > ColumnSourceFlags.Another + || instance.ColumnsDontHaveTableAlias; + } + + // Catching all column references + public override void Visit(ColumnReferenceExpression node) + { + var ids = node.MultiPartIdentifier.Identifiers; + if (ids.Count < 2) + { + ColumnsDontHaveTableAlias = true; + return; + } + + DetectCurrentSourceRef(BuildTableReference(ids)); + } + + // It builds full source name written before column ref + private static string BuildTableReference(IList nameParts) + { + if (nameParts.Count == 2) + { + // . => + return nameParts[0].Value; + } + + var sb = ObjectPools.StringBuilderPool.Get(); + + // All name parts except the last one (column name) are needed + for (int i = 0, n = nameParts.Count - 1; i < n; i++) + { + if (i > 0) + { + sb.Append(TSqlDomainAttributes.NamePartSeparator); + } + + sb.Append(nameParts[i].Value); + } + + var fullName = sb.ToString(); + ObjectPools.StringBuilderPool.Return(sb); + return fullName; + } + + // It compares given column source name to known names extracted + // from 2 sources defined in a JOIN. + private void DetectCurrentSourceRef(string sourceRef) + { + if (leftNames.Contains(sourceRef)) + { + CurrentDetections |= ColumnSourceFlags.Left; + } + else if (rightNames.Contains(sourceRef)) + { + CurrentDetections |= ColumnSourceFlags.Right; + } + else + { + // TODO : Validate that alias/table name exists in the query? Or let another rule do this. + CurrentDetections |= ColumnSourceFlags.Another; + } + } + + private ColumnSourceFlags DetectSourceRefs(TSqlFragment node) + { + // Result intermediate result holder and run the visitor + CurrentDetections = ColumnSourceFlags.None; + node.Accept(this); + return CurrentDetections; + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/CodingConvention/ExecWithoutExecRule.cs b/TeamTools.TSQL.Linter/Rules/CodingConvention/ExecWithoutExecRule.cs index 7c278d82..196720e1 100644 --- a/TeamTools.TSQL.Linter/Rules/CodingConvention/ExecWithoutExecRule.cs +++ b/TeamTools.TSQL.Linter/Rules/CodingConvention/ExecWithoutExecRule.cs @@ -16,6 +16,19 @@ public override void Visit(ExecuteStatement node) int i = node.FirstTokenIndex; int n = node.LastTokenIndex; + if (i == n && i >= 0) + { + var statementText = node.ScriptTokenStream[i].Text; + + // TODO : shouldn't it detect longer sequences of such symbols? + if (statementText.Length == 1 + && InvisibleCharDetector.LocateInvisibleChar(statementText, out var _) == 0) + { + // this is an invisible unicode symbol which is supposed to be detected by a separate rule + return; + } + } + while (i < n && ScriptDomExtension.IsSkippableTokens(node.ScriptTokenStream[i].TokenType)) { i++; diff --git a/TeamTools.TSQL.Linter/Rules/Naming/AlphabetMixInIdentifierRule.cs b/TeamTools.TSQL.Linter/Rules/Naming/AlphabetMixInIdentifierRule.cs index 541d9acc..5386f141 100644 --- a/TeamTools.TSQL.Linter/Rules/Naming/AlphabetMixInIdentifierRule.cs +++ b/TeamTools.TSQL.Linter/Rules/Naming/AlphabetMixInIdentifierRule.cs @@ -9,11 +9,12 @@ namespace TeamTools.TSQL.Linter.Rules [RuleIdentity("NM0259", "ALPHABET_MIX_IDENTIFIER")] internal sealed class AlphabetMixInIdentifierRule : AbstractRule { + // TODO : support many languages, use codepage detection private static readonly Regex DigitsOnly = MakeRegex(@"^[$@$\s0-9_+)(-]+$"); private static readonly Regex LatinSymbols = MakeRegex(@"[a-zA-Z$]+"); - private static readonly Regex NonLatinSymbols = MakeRegex(@"[^a-zA-Z0-9_@#&$\/\\\s.,?:-]+"); - private static readonly Regex CyrillicSymbols = MakeRegex(@"[а-яА-Я]+"); - private static readonly Regex NonCyrillicSymbols = MakeRegex(@"[^а-яА-Я0-9_@#&$\/\\\s.,?:-]+"); + private static readonly Regex NonLatinSymbols = MakeRegex(@"[^a-zA-Z0-9_@#&$\/\\\s.,?:=)(-]+"); + private static readonly Regex CyrillicSymbols = MakeRegex(@"[а-яА-ЯёЁ]+"); + private static readonly Regex NonCyrillicSymbols = MakeRegex(@"[^а-яА-ЯёЁ0-9_@#&$\/\\\s.,?:=)(-]+"); private readonly Action validator; @@ -40,6 +41,15 @@ private void ValidateIdentifier(Identifier node, string name) return; } + if (node.ScriptTokenStream[node.FirstTokenIndex].TokenType == TSqlTokenType.QuotedIdentifier) + { + // Ignoring quoted identifiers - they may be intentionally quoted so they may contain + // fancy names with symbols from different charsets. + // There is another rule which prevents unnecessary name quoting. + // And another rule should prevent fancy name usage. + return; + } + if (DigitsOnly.IsMatch(ident)) { // digit-only or unreadable identifier which is handled by separate rule diff --git a/TeamTools.TSQL.Linter/Rules/Naming/IdentifierContainsLookAlikeCharRule.cs b/TeamTools.TSQL.Linter/Rules/Naming/IdentifierContainsLookAlikeCharRule.cs new file mode 100644 index 00000000..083bb3f4 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Naming/IdentifierContainsLookAlikeCharRule.cs @@ -0,0 +1,40 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + // See also LiteralContainsLookAlikeCharRule, AlphabetMixInIdentifierRule + [RuleIdentity("NM0854", "IDENTIFIER_LOOK_ALIKE_CHAR")] + internal sealed class IdentifierContainsLookAlikeCharRule : AbstractRule + { + private const int MinIdentifierLength = 2; + + private readonly Action validator; + + public IdentifierContainsLookAlikeCharRule() : base() + { + validator = new Action(ValidateIdentifier); + } + + protected override void ValidateScript(TSqlScript node) + { + var ident = new DatabaseObjectIdentifierDetector(validator, true); + node.AcceptChildren(ident); + } + + private void ValidateIdentifier(Identifier node, string name) + { + if (string.IsNullOrEmpty(name) + || name.Length < MinIdentifierLength) + { + return; + } + + ValidateChars(name, node.StartLine, node.StartColumn); + } + + private void ValidateChars(string text, int line, int col) => LookAlikeCharDetector.ValidateChars(text, line, col, ViolationHandlerPerLine); + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Performance/NonSargablePredicateRule.cs b/TeamTools.TSQL.Linter/Rules/Performance/NonSargablePredicateRule.cs index 27824c26..ca3cca7d 100644 --- a/TeamTools.TSQL.Linter/Rules/Performance/NonSargablePredicateRule.cs +++ b/TeamTools.TSQL.Linter/Rules/Performance/NonSargablePredicateRule.cs @@ -31,35 +31,25 @@ public void LoadMetadata(SqlServerMetadata data) private static void ExtractNonSargablePredicates(BooleanExpression node, List predicates, HashSet builtInFunctions) { - if (node is BooleanBinaryExpression bin) + while (node is BooleanParenthesisExpression pe) { - ExtractNonSargablePredicates(bin.FirstExpression, predicates, builtInFunctions); - ExtractNonSargablePredicates(bin.SecondExpression, predicates, builtInFunctions); - - return; + node = pe.Expression; } - if (node is BooleanParenthesisExpression pexpr) + if (node is BooleanBinaryExpression bin) { - ExtractNonSargablePredicates(pexpr.Expression, predicates, builtInFunctions); - - return; + ExtractNonSargablePredicates(bin.FirstExpression, predicates, builtInFunctions); + ExtractNonSargablePredicates(bin.SecondExpression, predicates, builtInFunctions); } - - if (node is BooleanComparisonExpression cmp) + else if (node is BooleanComparisonExpression cmp) { predicates.AddRange(PredicateClassifier.GetNonSargablePredicates(cmp.FirstExpression, cmp.SecondExpression, builtInFunctions)); - - return; } - - if (node is BooleanTernaryExpression trn) + else if (node is BooleanTernaryExpression trn) { predicates.AddRange(PredicateClassifier.GetNonSargablePredicates(trn.FirstExpression, trn.SecondExpression, builtInFunctions)); // note, this might produce dup items in target 'predicates' list predicates.AddRange(PredicateClassifier.GetNonSargablePredicates(trn.FirstExpression, trn.ThirdExpression, builtInFunctions)); - - return; } } diff --git a/TeamTools.TSQL.Linter/Rules/Performance/SubstringPredicateToLikeRule.cs b/TeamTools.TSQL.Linter/Rules/Performance/SubstringPredicateToLikeRule.cs index 40839ef3..1a66b69b 100644 --- a/TeamTools.TSQL.Linter/Rules/Performance/SubstringPredicateToLikeRule.cs +++ b/TeamTools.TSQL.Linter/Rules/Performance/SubstringPredicateToLikeRule.cs @@ -1,5 +1,6 @@ using Microsoft.SqlServer.TransactSql.ScriptDom; using System; +using System.Diagnostics; using TeamTools.Common.Linting; namespace TeamTools.TSQL.Linter.Rules @@ -7,15 +8,16 @@ namespace TeamTools.TSQL.Linter.Rules [RuleIdentity("PF0956", "SUBSTRING_TO_LIKE")] internal sealed class SubstringPredicateToLikeRule : AbstractRule { + private readonly LikeCandidateVisitor visitor; + public SubstringPredicateToLikeRule() : base() { + visitor = new LikeCandidateVisitor(ViolationHandlerWithMessage); } - public override void Visit(WhereClause node) - { - var visitor = new LikeCandidateVisitor(ViolationHandlerWithMessage); - node.AcceptChildren(visitor); - } + public override void Visit(WhereClause node) => node.SearchCondition.AcceptChildren(visitor); + + public override void Visit(QualifiedJoin node) => node.SearchCondition.AcceptChildren(visitor); private class LikeCandidateVisitor : TSqlFragmentVisitor { @@ -42,14 +44,7 @@ public override void Visit(BooleanComparisonExpression node) arg = node.FirstExpression; } - if (fn is null) - { - return; - } - - arg = GetScalarVarOrStringLiteral(arg); - - if (arg is null) + if (fn is null || arg is null) { return; } @@ -59,35 +54,30 @@ public override void Visit(BooleanComparisonExpression node) { functionName = funcCall.FunctionName.Value; - if (!functionName.Equals("SUBSTRING", StringComparison.OrdinalIgnoreCase)) + if (!IsFunctionCallALikeCandidate(funcCall, functionName, ref arg)) { return; } + } + else if (fn is LeftFunctionCall lft) + { + functionName = "LEFT"; - // LEFT function always works from the start - if (funcCall != null - && functionName.Equals("SUBSTRING", StringComparison.OrdinalIgnoreCase) - && funcCall.Parameters.Count == 3) + if (lft.Parameters.Count != 2 + || GetScalarVarOrLiteral(lft.Parameters[1]) is null + || GetScalarVarOrLiteral(arg) is null) { - var stringStart = GetScalarVarOrStringLiteral(funcCall.Parameters[1]); - if (stringStart is null - || !(stringStart is IntegerLiteral stringStartPos) - || !int.TryParse(stringStartPos.Value, out int stringStartPosValue)) - { - // unknown position in a string - return; - } - - if (stringStartPosValue > 1) - { - // not the beginning of a string - return; - } + // if length is unknown or dependent on column value + // or filter value is similarly unpredictable at compile time + // then it might be hard or impossible to rewrite it into + // understandable and performant LIKE predicate + return; } } else { - functionName = "LEFT"; + Debug.Fail("We should never get here"); + return; } callback(fn, string.Format(MsgTemplate, functionName, GetLikePredicate(arg))); @@ -95,9 +85,9 @@ public override void Visit(BooleanComparisonExpression node) private static ScalarExpression GetFunctionCallExpression(ScalarExpression node) { - if (node is ParenthesisExpression pe) + while (node is ParenthesisExpression pe) { - return GetFunctionCallExpression(pe.Expression); + node = pe.Expression; } if (node is FunctionCall fn) @@ -113,11 +103,11 @@ private static ScalarExpression GetFunctionCallExpression(ScalarExpression node) return default; } - private static ScalarExpression GetScalarVarOrStringLiteral(ScalarExpression node) + private static ScalarExpression GetScalarVarOrLiteral(ScalarExpression node) { - if (node is ParenthesisExpression pe) + while (node is ParenthesisExpression pe) { - return GetScalarVarOrStringLiteral(pe.Expression); + node = pe.Expression; } if (node is Literal) @@ -154,6 +144,45 @@ private static string GetEscapedValue(string searchString) .Replace("_", "[_]") .Replace("%", "[%]"); } + + private static bool IsFunctionCallALikeCandidate(FunctionCall funcCall, string functionName, ref ScalarExpression searchValue) + { + if (functionName.Equals("SUBSTRING", StringComparison.OrdinalIgnoreCase) + && funcCall.Parameters.Count == 3) + { + var stringStart = GetScalarVarOrLiteral(funcCall.Parameters[1]); + if (!(stringStart is IntegerLiteral stringStartPos) + || !int.TryParse(stringStartPos.Value, out int stringStartPosValue)) + { + // unknown position in a string + return false; + } + + searchValue = GetScalarVarOrLiteral(searchValue); + + // Only the beginning of a string can be converted to LIKE '%' + return stringStartPosValue == 1 && searchValue != null; + } + + if (functionName.Equals("CHARINDEX", StringComparison.OrdinalIgnoreCase) + && funcCall.Parameters.Count == 2) + { + var charPos = GetScalarVarOrLiteral(searchValue); + searchValue = GetScalarVarOrLiteral(funcCall.Parameters[0]); + + if (!(charPos is IntegerLiteral stringStartPos) + || !int.TryParse(stringStartPos.Value, out int expectedCharPosValue)) + { + // unknown position in a string + return false; + } + + // Only the beginning of a string can be converted to LIKE '%' + return expectedCharPosValue == 1 && searchValue != null; + } + + return false; + } } } } diff --git a/TeamTools.TSQL.Linter/Rules/Redundancy/RedundantIndexFilterRule.cs b/TeamTools.TSQL.Linter/Rules/Redundancy/RedundantIndexFilterRule.cs new file mode 100644 index 00000000..db7eccb3 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Redundancy/RedundantIndexFilterRule.cs @@ -0,0 +1,185 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; +using TeamTools.TSQL.Linter.Routines.TableDefinitionExtractor; + +namespace TeamTools.TSQL.Linter.Rules +{ + // TODO : Support IN / NOT IN predicates + [RuleIdentity("RD0849", "REDUNDANT_INDEX_FILTER")] + [IndexRule] + internal sealed class RedundantIndexFilterRule : ScriptAnalysisServiceConsumingRule + { + public RedundantIndexFilterRule() : base() + { + } + + protected override void ValidateScript(TSqlScript script) + { + var info = GetService(script); + + if (info.Tables.Count == 0) + { + return; + } + + foreach (var tbl in info.Tables) + { + // Extracting defined column-related constraints in format + var columnConstraints = ExtractColumnConstraints(tbl.Value); + if (columnConstraints.Count == 0) + { + continue; + } + + foreach (var idx in info.Indices(tbl.Key).OfType()) + { + // Extracting index filter definition in format + if (idx.Definition is CreateIndexStatement index && index.FilterPredicate != null) + { + // CREATE INDEX statement + ValidateIndexPredicate(ExtractColumnFilters(index.FilterPredicate), columnConstraints); + } + else if (idx.Definition is IndexDefinition ix && ix.FilterPredicate != null) + { + // Inline index definition + ValidateIndexPredicate(ExtractColumnFilters(ix.FilterPredicate), columnConstraints); + } + } + } + } + + private static IDictionary> ExtractColumnConstraints(SqlTableDefinition tbl) + { + var constraints = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + // NOT NULL constraints + foreach (var col in tbl.Columns) + { + // ContainsKey for protection agains broken table definition with col name dups + if (!col.Value.IsNullable && !constraints.ContainsKey(col.Key)) + { + var colConstraints = new List(); + colConstraints.Add(MakeNotNullConstraint(col.Value.Node.ColumnIdentifier)); + constraints.Add(col.Key, colConstraints); + } + } + + // CHECK constraints + for (int i = tbl.Node.TableConstraints.Count - 1; i >= 0; i--) + { + var cstr = tbl.Node.TableConstraints[i]; + if (cstr is CheckConstraintDefinition chk) + { + // Extract simple expressions, take those who reference a column + foreach (var expr in ExtractNestedExpressions(chk.CheckCondition)) + { + var colFilter = expr; + if (!(colFilter.FirstExpression is ColumnReferenceExpression)) + { + // let the column reference be on the left side + colFilter = colFilter.Mirror(); + } + + if (colFilter.FirstExpression is ColumnReferenceExpression colRef) + { + var colName = colRef.MultiPartIdentifier.GetLastIdentifier().Value; + if (!constraints.TryGetValue(colName, out var colConstraints)) + { + colConstraints = new List(); + constraints.Add(colName, colConstraints); + } + + colConstraints.Add(colFilter); + } + } + } + } + + return constraints; + } + + private static IEnumerable> ExtractColumnFilters(BooleanExpression indexPredicate) + { + foreach (var expr in ExtractNestedExpressions(indexPredicate)) + { + var colFilter = expr; + if (!(colFilter.FirstExpression is ColumnReferenceExpression)) + { + // let the column reference be on the left side + colFilter = colFilter.Mirror(); + } + + if (colFilter.FirstExpression is ColumnReferenceExpression colRef) + { + string colName = colRef.MultiPartIdentifier.Identifiers.GetFullName(); + yield return new KeyValuePair(colName, colFilter); + } + } + } + + private static IEnumerable ExtractNestedExpressions(BooleanExpression node) + { + node = BooleanExpressionPartsExtractor.ExtractExpression(node); + + // OR's are harder to understand and filtered index syntax does not currently support OR + if (node is BooleanBinaryExpression bin && bin.BinaryExpressionType == BooleanBinaryExpressionType.And) + { + foreach (var e in ExtractNestedExpressions(bin.FirstExpression)) + { + yield return e; + } + + foreach (var e in ExtractNestedExpressions(bin.SecondExpression)) + { + yield return e; + } + } + + yield return BooleanExpressionNormalizer.Normalize(node); + } + + private static BooleanExpressionParts MakeNotNullConstraint(Identifier column) + { + var colReference = new ColumnReferenceExpression + { + MultiPartIdentifier = new MultiPartIdentifier(), + // Things below are required for GetFragmentCleanedText() + FirstTokenIndex = column.FirstTokenIndex, + LastTokenIndex = column.LastTokenIndex, + ScriptTokenStream = column.ScriptTokenStream, + }; + colReference.MultiPartIdentifier.Identifiers.Add(column); + + return new BooleanExpressionParts + { + FirstExpression = colReference, + ComparisonType = BooleanComparisonConverter.ToEqualityComparison(false), + }; + } + + // Matching column constraint predicates with index filter predicates via column name + private void ValidateIndexPredicate(IEnumerable> columnPredicates, IDictionary> columnConstraints) + { + foreach (var columnPredicate in columnPredicates) + { + string columnName = columnPredicate.Key; + + if (columnConstraints.TryGetValue(columnName, out var constraints)) + { + for (int i = constraints.Count - 1; i >= 0; i--) + { + if (columnPredicate.Value.Equals(constraints[i])) + { + HandleNodeError(columnPredicate.Value.OriginalExpression, columnName); + } + } + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Redundancy/SignedZeroRule.cs b/TeamTools.TSQL.Linter/Rules/Redundancy/SignedZeroRule.cs index 7075def0..b3385576 100644 --- a/TeamTools.TSQL.Linter/Rules/Redundancy/SignedZeroRule.cs +++ b/TeamTools.TSQL.Linter/Rules/Redundancy/SignedZeroRule.cs @@ -40,6 +40,6 @@ private static ScalarExpression ExtractExpression(ScalarExpression node) private static bool IsNull(ScalarExpression node) => node is NullLiteral; private bool IsZero(ScalarExpression node) - => node is Literal l && decimal.TryParse(l.Value, NumberStyles.AllowDecimalPoint, sqlCulture, out decimal value) && value == 0; + => node is Literal l && decimal.TryParse(l.Value, NumberStyles.Number, sqlCulture, out decimal value) && value == 0; } } diff --git a/TeamTools.TSQL.Linter/Rules/Redundancy/WherePredicateSameAsJoinPredicateRule.cs b/TeamTools.TSQL.Linter/Rules/Redundancy/WherePredicateSameAsJoinPredicateRule.cs new file mode 100644 index 00000000..1e8ac2b7 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Redundancy/WherePredicateSameAsJoinPredicateRule.cs @@ -0,0 +1,135 @@ +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("RD0850", "EXTRA_WHERE_PREDICATE")] + internal class WherePredicateSameAsJoinPredicateRule : AbstractRule + { + public WherePredicateSameAsJoinPredicateRule() : base() + { + } + + public override void Visit(QuerySpecification node) + { + if (node.WhereClause?.SearchCondition is null) + { + // no WHERE or it is a WHERE CURRENT OF + return; + } + + if ((node.FromClause?.TableReferences?.Count ?? 0) == 0) + { + // no FROM + return; + } + + var innerJoinPredicates = ExtractInnerJoinPredicates(node.FromClause.TableReferences); + + if (innerJoinPredicates.Count == 0) + { + // no outer joins + return; + } + + DetectDupPredicates(node.WhereClause.SearchCondition, innerJoinPredicates); + } + + // Extracting all predicates from INNER JOINS defined in a query + private static ICollection ExtractInnerJoinPredicates(IList joins) + { + var joinPredicates = new List(); + + for (int i = joins.Count - 1; i >= 0; i--) + { + var join = joins[i]; + if (join is JoinTableReference jtr) + { + foreach (var predicate in ExtractInnerJoinPredicates(jtr)) + { + joinPredicates.Add(predicate); + } + } + } + + return joinPredicates; + } + + // Extracting join predicates recursively + private static IEnumerable ExtractInnerJoinPredicates(JoinTableReference join) + { + if (join is QualifiedJoin qj && qj.QualifiedJoinType == QualifiedJoinType.Inner) + { + foreach (var predicate in ExtractNestedPredicates(qj.SearchCondition)) + { + yield return predicate; + } + } + + // Expanding linked joins recursively + if (join.FirstTableReference is JoinTableReference fjt) + { + foreach (var predicate in ExtractInnerJoinPredicates(fjt)) + { + yield return predicate; + } + } + + if (join.SecondTableReference is JoinTableReference sjt) + { + foreach (var predicate in ExtractInnerJoinPredicates(sjt)) + { + yield return predicate; + } + } + } + + // Splitting complex predicate into atomic boolean expressions with normalization + private static IEnumerable ExtractNestedPredicates(BooleanExpression expr) + { + while (expr is BooleanParenthesisExpression pe) + { + expr = pe.Expression; + } + + // AND means that all predicates are supposed to be applied. OR is not supported. + if (expr is BooleanBinaryExpression bin && bin.BinaryExpressionType == BooleanBinaryExpressionType.And) + { + foreach (var predicate in ExtractNestedPredicates(bin.FirstExpression)) + { + yield return predicate; + } + + foreach (var predicate in ExtractNestedPredicates(bin.SecondExpression)) + { + yield return predicate; + } + } + else + { + var predicate = BooleanExpressionNormalizer.Normalize(expr); + + if (predicate != null && predicate.FirstExpression != null && predicate.SecondExpression != null) + { + yield return predicate; + } + } + } + + // Splitting WHERE predicates into atomic expressions and comparing them to previously extracted + // predicates from INNER JOINs + private void DetectDupPredicates(BooleanExpression wherePredicate, ICollection joinPredicates) + { + foreach (var where in ExtractNestedPredicates(wherePredicate)) + { + if (joinPredicates.Contains(where)) + { + HandleNodeError(where.FirstExpression); + } + } + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/ScriptAnalysisServiceConsumingRule.cs b/TeamTools.TSQL.Linter/Rules/ScriptAnalysisServiceConsumingRule.cs index 0804f5d8..d515655b 100644 --- a/TeamTools.TSQL.Linter/Rules/ScriptAnalysisServiceConsumingRule.cs +++ b/TeamTools.TSQL.Linter/Rules/ScriptAnalysisServiceConsumingRule.cs @@ -14,10 +14,10 @@ public void InjectServiceProvider(IScriptAnalysisServiceProvider provider) serviceProvider = provider; } - protected SVC GetService(TSqlFragment node) - where SVC : class + protected TSVC GetService(TSqlFragment node) + where TSVC : class { - return serviceProvider.GetService(node); + return serviceProvider.GetService(node); } } } diff --git a/TeamTools.TSQL.Linter/Rules/Simplification/MultipleAndToNotInRule.cs b/TeamTools.TSQL.Linter/Rules/Simplification/MultipleAndToNotInRule.cs new file mode 100644 index 00000000..dac79398 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Simplification/MultipleAndToNotInRule.cs @@ -0,0 +1,40 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("SI0846", "MULTIPLE_AND_TO_NOT_IN")] + internal sealed class MultipleAndToNotInRule : AbstractRule + { + private readonly CombinablePredicateExtractor predicateExtractor = new CombinablePredicateExtractor(true); + + public MultipleAndToNotInRule() : base() + { + } + + // A constraint and computed column definition must not use IN expression + // unless DacFx fixes permanent diff generated by it. + // See ConstraintWithoutListsRule + public override void ExplicitVisit(CheckConstraintDefinition node) + { } + + public override void ExplicitVisit(ColumnDefinition node) + { } + + // Explicit - to avoid double visiting parts of complex AND-OR-AND-OR expression. + public override void ExplicitVisit(BooleanBinaryExpression node) + { + if (node.BinaryExpressionType != BooleanBinaryExpressionType.And) + { + // Forwarding to base visitor so the nested binary expressions will be processed + Visit(node); + + // Only AND is supported by the rule + return; + } + + predicateExtractor.Process(node, ViolationHandlerWithMessage); + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Simplification/MultipleInPredicateIntoOneRule.cs b/TeamTools.TSQL.Linter/Rules/Simplification/MultipleInPredicateIntoOneRule.cs new file mode 100644 index 00000000..8a537fe5 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Simplification/MultipleInPredicateIntoOneRule.cs @@ -0,0 +1,31 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("SI0847", "MULTIPLE_IN_TO_SINGLE")] + internal sealed class MultipleInPredicateIntoOneRule : AbstractRule + { + private readonly CollapsibleInExtractor extractor = new CollapsibleInExtractor(false); + + public MultipleInPredicateIntoOneRule() : base() + { + } + + // Explicit - to avoid double visiting parts of complex AND-OR-AND-OR expression. + public override void ExplicitVisit(BooleanBinaryExpression node) + { + if (node.BinaryExpressionType != BooleanBinaryExpressionType.Or) + { + // Forwarding to base visitor so the nested binary expressions will be processed + Visit(node); + + // Only OR is supported by the rule + return; + } + + extractor.Process(node, ViolationHandlerWithMessage); + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Simplification/MultipleNotInPredicateIntoOneRule.cs b/TeamTools.TSQL.Linter/Rules/Simplification/MultipleNotInPredicateIntoOneRule.cs new file mode 100644 index 00000000..845b5baf --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Simplification/MultipleNotInPredicateIntoOneRule.cs @@ -0,0 +1,31 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("SI0848", "MULTIPLE_NOT_IN_TO_SINGLE")] + internal sealed class MultipleNotInPredicateIntoOneRule : AbstractRule + { + private readonly CollapsibleInExtractor extractor = new CollapsibleInExtractor(true); + + public MultipleNotInPredicateIntoOneRule() : base() + { + } + + // Explicit - to avoid double visiting parts of complex AND-OR-AND-OR expression. + public override void ExplicitVisit(BooleanBinaryExpression node) + { + if (node.BinaryExpressionType != BooleanBinaryExpressionType.And) + { + // Forwarding to base visitor so the nested binary expressions will be processed + Visit(node); + + // Only AND is supported by the rule + return; + } + + extractor.Process(node, ViolationHandlerWithMessage); + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Simplification/MultipleOrToInRule.cs b/TeamTools.TSQL.Linter/Rules/Simplification/MultipleOrToInRule.cs new file mode 100644 index 00000000..14859962 --- /dev/null +++ b/TeamTools.TSQL.Linter/Rules/Simplification/MultipleOrToInRule.cs @@ -0,0 +1,40 @@ +using Microsoft.SqlServer.TransactSql.ScriptDom; +using TeamTools.Common.Linting; +using TeamTools.TSQL.Linter.Routines; + +namespace TeamTools.TSQL.Linter.Rules +{ + [RuleIdentity("SI0845", "MULTIPLE_OR_TO_IN")] + internal sealed class MultipleOrToInRule : AbstractRule + { + private readonly CombinablePredicateExtractor predicateExtractor = new CombinablePredicateExtractor(false); + + public MultipleOrToInRule() : base() + { + } + + // A constraint and computed column definition must not use IN expression + // unless DacFx fixes permanent diff generated by it. + // See ConstraintWithoutListsRule + public override void ExplicitVisit(CheckConstraintDefinition node) + { } + + public override void ExplicitVisit(ColumnDefinition node) + { } + + // Explicit - to avoid double visiting parts of complex AND-OR-AND-OR expression. + public override void ExplicitVisit(BooleanBinaryExpression node) + { + if (node.BinaryExpressionType != BooleanBinaryExpressionType.Or) + { + // Forwarding to base visitor so the nested binary expressions will be processed + Visit(node); + + // Only OR is supported by the rule + return; + } + + predicateExtractor.Process(node, ViolationHandlerWithMessage); + } + } +} diff --git a/TeamTools.TSQL.Linter/Rules/Simplification/SimplePredicateNegationRule.cs b/TeamTools.TSQL.Linter/Rules/Simplification/SimplePredicateNegationRule.cs index 5685826a..bafbb787 100644 --- a/TeamTools.TSQL.Linter/Rules/Simplification/SimplePredicateNegationRule.cs +++ b/TeamTools.TSQL.Linter/Rules/Simplification/SimplePredicateNegationRule.cs @@ -1,7 +1,7 @@ using Microsoft.SqlServer.TransactSql.ScriptDom; -using System.Diagnostics.CodeAnalysis; using TeamTools.Common.Linting; using TeamTools.TSQL.Linter.Properties; +using TeamTools.TSQL.Linter.Routines; namespace TeamTools.TSQL.Linter.Rules { @@ -22,85 +22,11 @@ public override void Visit(BooleanNotExpression node) if (expr is BooleanComparisonExpression cmp) { - var revertedComparison = ComparisonToString(RevertComparison(cmp.ComparisonType)); + var revertedComparison = BooleanComparisonConverter.ComparisonToString( + BooleanComparisonConverter.RevertComparison(cmp.ComparisonType)); HandleNodeError(cmp, string.Format(Strings.ViolationDetails_SimplePredicateNegationRule_UseReversedComparison, revertedComparison)); } } - - [ExcludeFromCodeCoverage] - private static BooleanComparisonType RevertComparison(BooleanComparisonType cmp) - { - switch (cmp) - { - case BooleanComparisonType.Equals: - return BooleanComparisonType.NotEqualToBrackets; - - case BooleanComparisonType.NotEqualToBrackets: - case BooleanComparisonType.NotEqualToExclamation: - return BooleanComparisonType.Equals; - - case BooleanComparisonType.GreaterThan: - return BooleanComparisonType.LessThanOrEqualTo; - - case BooleanComparisonType.GreaterThanOrEqualTo: - return BooleanComparisonType.LessThan; - - case BooleanComparisonType.LessThanOrEqualTo: - return BooleanComparisonType.GreaterThan; - - case BooleanComparisonType.LessThan: - return BooleanComparisonType.GreaterThanOrEqualTo; - - case BooleanComparisonType.NotGreaterThan: - return BooleanComparisonType.GreaterThan; - - case BooleanComparisonType.NotLessThan: - return BooleanComparisonType.LessThan; - - // TODO : or fail? - default: - return cmp; - } - } - - // TODO : extract to something more reusable - [ExcludeFromCodeCoverage] - private static string ComparisonToString(BooleanComparisonType cmp) - { - switch (cmp) - { - case BooleanComparisonType.Equals: - return "="; - - case BooleanComparisonType.NotEqualToBrackets: - return "<>"; - - case BooleanComparisonType.NotEqualToExclamation: - return "!="; - - case BooleanComparisonType.GreaterThan: - return ">"; - - case BooleanComparisonType.GreaterThanOrEqualTo: - return ">="; - - case BooleanComparisonType.LessThanOrEqualTo: - return "<="; - - case BooleanComparisonType.LessThan: - return "<"; - - case BooleanComparisonType.NotGreaterThan: - return "!>"; - - case BooleanComparisonType.NotLessThan: - return "!<"; - - // TODO : or fail? - default: - return default; - } - } } } diff --git a/TeamTools.TSQL.LinterTests/TestingInfrastructure/MockLinter.cs b/TeamTools.TSQL.LinterTests/TestingInfrastructure/MockLinter.cs index f21b8e9c..968e7e77 100644 --- a/TeamTools.TSQL.LinterTests/TestingInfrastructure/MockLinter.cs +++ b/TeamTools.TSQL.LinterTests/TestingInfrastructure/MockLinter.cs @@ -256,14 +256,22 @@ private void InitTypeInfo() Meta.Types.Add("VARCHAR", new SqlServerMetaTypeDescription { Name = "VARCHAR" }); Meta.Types.Add("NATIONAL VARYING CHARACTER", new SqlServerMetaTypeDescription { Name = "NATIONAL VARYING CHARACTER", AlsoKnownAs = "NVARCHAR", ForceToOriginalName = true }); Meta.Types.Add("CHARACTER", new SqlServerMetaTypeDescription { Name = "CHARACTER", AlsoKnownAs = "CHAR", ForceToOriginalName = true }); + Meta.Types.Add("XML", new SqlServerMetaTypeDescription { Name = "XML", CanBeNativelyCompiled = false }); Meta.Types.Add("TIMESTAMP", new SqlServerMetaTypeDescription { Name = "TIMESTAMP", CanBeNativelyCompiled = false, AlsoKnownAs = "ROWVERSION", ForceToOriginalName = true }); Meta.Types.Add("ROWVERSION", new SqlServerMetaTypeDescription { Name = "ROWVERSION", CanBeNativelyCompiled = false }); Meta.Types.Add("HIERARCHYID", new SqlServerMetaTypeDescription { Name = "HIERARCHYID", CanBeNativelyCompiled = false }); + Meta.Types.Add("INTEGER", new SqlServerMetaTypeDescription { Name = "INTEGER", AlsoKnownAs = "INT", ForceToOriginalName = true }); Meta.Types.Add("INT", new SqlServerMetaTypeDescription { Name = "INT" }); Meta.Types.Add("DEC", new SqlServerMetaTypeDescription { Name = "DEC", AlsoKnownAs = "DECIMAL", ForceToOriginalName = true }); + Meta.Types.Add("DATETIME", new SqlServerMetaTypeDescription { Name = "DATETIME" }); + Meta.Types.Add("DATE", new SqlServerMetaTypeDescription { Name = "DATE" }); + Meta.Types.Add("TIME", new SqlServerMetaTypeDescription { Name = "TIME" }); + + Meta.Types.Add("BINARY", new SqlServerMetaTypeDescription { Name = "BINARY" }); + Meta.Types.Add("VARBINARY", new SqlServerMetaTypeDescription { Name = "VARBINARY" }); } } } diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SchemaQualifiedProcCallRule/TestSources/invisible_char_identifier_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SchemaQualifiedProcCallRule/TestSources/invisible_char_identifier_raise_0_violations.sql new file mode 100644 index 00000000..7397ac1b --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Ambiguity/SchemaQualifiedProcCallRule/TestSources/invisible_char_identifier_raise_0_violations.sql @@ -0,0 +1,4 @@ +/* there is an invisible ZWNBSP unicode symbol in the line below +which is treated by ScriptDom as identifier +but the rule should ignore it */ + --<< here diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/CommentHasInvisibleCharRule/CommentHasInvisibleCharRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/CommentHasInvisibleCharRule/CommentHasInvisibleCharRuleTests.cs new file mode 100644 index 00000000..4a905c4e --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/CommentHasInvisibleCharRule/CommentHasInvisibleCharRuleTests.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(CommentHasInvisibleCharRule))] + public sealed class CommentHasInvisibleCharRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(CommentHasInvisibleCharRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/CommentHasInvisibleCharRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/CommentHasInvisibleCharRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..d3ebd343 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/CommentHasInvisibleCharRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,7 @@ +-- empty comment below +-- + +/* +No bad chars +here +*/ diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/CommentHasInvisibleCharRule/TestSources/bad_chars_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/CommentHasInvisibleCharRule/TestSources/bad_chars_raise_2_violations.sql new file mode 100644 index 00000000..cc7c4aa4 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/CommentHasInvisibleCharRule/TestSources/bad_chars_raise_2_violations.sql @@ -0,0 +1,5 @@ +-- ZWSP here:​ + +/* + <-- Hair space here +*/ diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ComparedExpressionsEqualRule/ComparedExpressionsEqualRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ComparedExpressionsEqualRule/ComparedExpressionsEqualRuleTests.cs new file mode 100644 index 00000000..c0fea8b0 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ComparedExpressionsEqualRule/ComparedExpressionsEqualRuleTests.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(ComparedExpressionsEqualRule))] + public class ComparedExpressionsEqualRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(ComparedExpressionsEqualRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ComparedExpressionsEqualRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ComparedExpressionsEqualRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..2dc933ed --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ComparedExpressionsEqualRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,7 @@ +SELECT 1 +FROM foo f +join bar b +on f.id = b.id +where group_id <> @group_id +and title like 'start%' +and dt between @start and @end diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ComparedExpressionsEqualRule/TestSources/magic_literals_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ComparedExpressionsEqualRule/TestSources/magic_literals_raise_0_violations.sql new file mode 100644 index 00000000..ea4398c6 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ComparedExpressionsEqualRule/TestSources/magic_literals_raise_0_violations.sql @@ -0,0 +1,7 @@ +while 1 = 1 +begin + select 1 + from foo + join numbers + on 0 = 0 +end diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ComparedExpressionsEqualRule/TestSources/same_sides_raise_4_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ComparedExpressionsEqualRule/TestSources/same_sides_raise_4_violations.sql new file mode 100644 index 00000000..a5484019 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ComparedExpressionsEqualRule/TestSources/same_sides_raise_4_violations.sql @@ -0,0 +1,7 @@ +SELECT 1 +FROM foo f +join bar b +on f.id = ((f.id)) -- 1 +where group_id <> group_id -- 2 +and title like title -- 3 +and dt between dt and @end -- 4 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/ConditionsLeadToSimilarBehaviorRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/ConditionsLeadToSimilarBehaviorRuleTests.cs new file mode 100644 index 00000000..5e893478 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/ConditionsLeadToSimilarBehaviorRuleTests.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(ConditionsLeadToSimilarBehaviorRule))] + public class ConditionsLeadToSimilarBehaviorRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(ConditionsLeadToSimilarBehaviorRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/TestSources/all_different_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/TestSources/all_different_raise_0_violations.sql new file mode 100644 index 00000000..3b488e62 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/TestSources/all_different_raise_0_violations.sql @@ -0,0 +1,18 @@ +SET @a = CASE WHEN @b = @c + THEN CAST(GETDATE() as DATE)+(1) + WHEN @c > 100 + THEN @d END + +SET @a = CASE @b WHEN @c THEN 1 ELSE NULL END + +IF @dt > GETDATE() +BEGIN + PRINT 'FUTURE' +END +ELSE +BEGIN + BEGIN + SET @dt = NULL + PRINT 'NOT FUTURE' + END +END diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/TestSources/case_similar_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/TestSources/case_similar_raise_2_violations.sql new file mode 100644 index 00000000..7f64fdb0 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/TestSources/case_similar_raise_2_violations.sql @@ -0,0 +1,6 @@ +SET @a = CASE WHEN @b = @c + THEN (CAST(GETDATE() as DATE)+1) + WHEN @c > 100 + THEN CAST(GETDATE() as DATE) +1 END + +SET @a = CASE @b WHEN @c THEN 1 ELSE 1 END diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/TestSources/if_else_similar_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/TestSources/if_else_similar_raise_0_violations.sql new file mode 100644 index 00000000..fef0c92d --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/TestSources/if_else_similar_raise_0_violations.sql @@ -0,0 +1,15 @@ + +IF @dt > GETDATE() +BEGIN + PRINT 'FUTURE' + RETURN; +END +ELSE +BEGIN + BEGIN + PRINT 'FUTURE' + + -- comment + RETURN + END +END diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/TestSources/iif_different_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/TestSources/iif_different_raise_0_violations.sql new file mode 100644 index 00000000..a5ead918 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/TestSources/iif_different_raise_0_violations.sql @@ -0,0 +1,2 @@ +-- compatibility level min: 110 +SELECT IIF(@a > 1, 100, @b) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/TestSources/iif_similar_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/TestSources/iif_similar_raise_1_violations.sql new file mode 100644 index 00000000..f520a099 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/TestSources/iif_similar_raise_1_violations.sql @@ -0,0 +1,5 @@ +-- compatibility level min: 110 +SELECT IIF(@a > 1, (@b + + + 42), + @b+42) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/TestSources/single_option_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/TestSources/single_option_raise_0_violations.sql new file mode 100644 index 00000000..567bcbf9 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/ConditionsLeadToSimilarBehaviorRule/TestSources/single_option_raise_0_violations.sql @@ -0,0 +1,8 @@ +SET @a = CASE WHEN @b = @c THEN 1 END + +SET @a = CASE @b WHEN @c THEN 1 END + +IF @dt > GETDATE() +BEGIN + PRINT 'FUTURE' +END diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/FakeOuterJoinRule/FakeOuterJoinRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/FakeOuterJoinRule/FakeOuterJoinRuleTests.cs new file mode 100644 index 00000000..4ac670a8 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/FakeOuterJoinRule/FakeOuterJoinRuleTests.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(FakeOuterJoinRule))] + public class FakeOuterJoinRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(FakeOuterJoinRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/FakeOuterJoinRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/FakeOuterJoinRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..b786a179 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/FakeOuterJoinRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,92 @@ +-- bare select +SELECT 1 + +-- no FROM +SELECT 1 +WHERE 1=1 + +-- no WHERE +SELECT * +FROM foo +LEFT JOIN bar +ON child_id = parent_id + +-- no JOIN +SELECT * +FROM foo +WHERE foo.group_id = 123 + +-- WHERE CURRENT OF +UPDATE foo +SET flag = 1 +FROM foo +LEFT JOIN bar +ON child_id = parent_id +WHERE CURRENT OF cr + +-- no OUTER JOIN +SELECT * +FROM foo +INNER JOIN bar +ON child_id = parent_id +INNER JOIN far +ON child_id = parent_id +INNER JOIN jar +ON child_id = parent_id +WHERE bar.x = foo.y + +-- WHERE is not related to join +SELECT * +FROM foo +LEFT JOIN bar +ON id = parent_id +INNER JOIN far +ON far.id = foo.id +WHERE far.x = 1 +OR foo.y = 1 + +-- it's still OUTER +SELECT * +FROM foo +LEFT JOIN bar +ON child_id = parent_id +WHERE bar.id IS NULL + +-- ON ON +SELECT * +FROM foo + LEFT JOIN bar + INNER JOIN far + ON far.id = bar.id + ON bar.parent_id = foo.id +WHERE far.value IS NULL + +-- OR +SELECT * +FROM foo f +LEFT OUTER JOIN bar b +ON child_id = parent_id +WHERE b.value IS NOT NULL +OR f.parent_id IS NULL + +-- nested WHERE +select * +from foo +inner join +( + select * + from bar + where bar.flag > 1 +) b +on b.id = foo.id + +select * +from foo +left join +( + select * + from bar + where bar.flag > 1 +) b +on b.id = foo.id +where foo.group_id = 123 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/FakeOuterJoinRule/TestSources/on_on_where_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/FakeOuterJoinRule/TestSources/on_on_where_raise_1_violations.sql new file mode 100644 index 00000000..45af5176 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/FakeOuterJoinRule/TestSources/on_on_where_raise_1_violations.sql @@ -0,0 +1,17 @@ +SELECT * +FROM foo + LEFT JOIN bar + INNER JOIN far + ON far.id = bar.id + ON bar.parent_id = foo.id +INNER JOIN jar +on foo.name = jar.name +WHERE far.value > 0 -- here + +SELECT * +FROM foo + LEFT JOIN @bar as bar + ON bar.parent_id = foo.id +INNER JOIN far + ON far.id = bar.id +WHERE far.value > 0 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/FakeOuterJoinRule/TestSources/where_by_alias_raise_3_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/FakeOuterJoinRule/TestSources/where_by_alias_raise_3_violations.sql new file mode 100644 index 00000000..91c4cbc1 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/FakeOuterJoinRule/TestSources/where_by_alias_raise_3_violations.sql @@ -0,0 +1,20 @@ +SELECT * +FROM foo f +LEFT OUTER JOIN bar b +ON child_id = parent_id +WHERE b.title LIKE 'asdf%' -- 1 + +SELECT * +FROM foo f +LEFT OUTER JOIN bar b +ON child_id = parent_id +WHERE b.value IN (1, 2, 3) -- 2 + +SELECT * +FROM foo f +INNER JOIN jar j +ON j.id = f.id +CROSS APPLY dbo.fn(j.some_id) +LEFT JOIN bar b +ON child_id = parent_id +WHERE ((-b.value)) = 1 -- 3 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/FakeOuterJoinRule/TestSources/where_by_name_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/FakeOuterJoinRule/TestSources/where_by_name_raise_2_violations.sql new file mode 100644 index 00000000..6027ca67 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/FakeOuterJoinRule/TestSources/where_by_name_raise_2_violations.sql @@ -0,0 +1,16 @@ +SELECT * +FROM foo AS f +LEFT JOIN dbo.bar AS b +ON child_id = parent_id +-- make this query a little bit complicated +INNER JOIN far +ON far.something = f.src_something +CROSS APPLY dbo.my_fn(f.x) as y +WHERE 1 = bar.value + AND dbo.fn() IS NOT NULL + +SELECT * +FROM dbo.bar +RIGHT JOIN schm.foo +ON child_id = parent_id +WHERE bar.value < schm.foo.value_limit diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/FakeOuterJoinRule/TestSources/where_outer_is_not_null_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/FakeOuterJoinRule/TestSources/where_outer_is_not_null_raise_1_violations.sql new file mode 100644 index 00000000..54638cbf --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/FakeOuterJoinRule/TestSources/where_outer_is_not_null_raise_1_violations.sql @@ -0,0 +1,5 @@ +SELECT * +FROM foo f +LEFT OUTER JOIN bar b +ON child_id = parent_id +WHERE b.value IS NOT NULL diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IdentifierHasInvisibleCharRule/IdentifierHasInvisibleCharRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IdentifierHasInvisibleCharRule/IdentifierHasInvisibleCharRuleTests.cs new file mode 100644 index 00000000..2defb0d8 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IdentifierHasInvisibleCharRule/IdentifierHasInvisibleCharRuleTests.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(IdentifierHasInvisibleCharRule))] + public sealed class IdentifierHasInvisibleCharRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(IdentifierHasInvisibleCharRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IdentifierHasInvisibleCharRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IdentifierHasInvisibleCharRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..d3ebd343 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IdentifierHasInvisibleCharRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,7 @@ +-- empty comment below +-- + +/* +No bad chars +here +*/ diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IdentifierHasInvisibleCharRule/TestSources/bad_chars_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IdentifierHasInvisibleCharRule/TestSources/bad_chars_raise_2_violations.sql new file mode 100644 index 00000000..c8556797 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/IdentifierHasInvisibleCharRule/TestSources/bad_chars_raise_2_violations.sql @@ -0,0 +1,5 @@ +/*<<-- ZWNBSP here is treated as [EXEC] ZWNBSP proc */ +GO + +create table [ZWSP here:​] +(id int) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/InsertedDeletedIrrelevanceRule/InsertedDeletedIrrelevanceRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/InsertedDeletedIrrelevanceRule/InsertedDeletedIrrelevanceRuleTests.cs new file mode 100644 index 00000000..3a0bdbbf --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/InsertedDeletedIrrelevanceRule/InsertedDeletedIrrelevanceRuleTests.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(InsertedDeletedIrrelevanceRule))] + public sealed class InsertedDeletedIrrelevanceRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(InsertedDeletedIrrelevanceRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/InsertedDeletedIrrelevanceRule/TestSources/insert_delete_bad_raise_4_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/InsertedDeletedIrrelevanceRule/TestSources/insert_delete_bad_raise_4_violations.sql new file mode 100644 index 00000000..f5655e70 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/InsertedDeletedIrrelevanceRule/TestSources/insert_delete_bad_raise_4_violations.sql @@ -0,0 +1,23 @@ +-- INSERT +insert into a(title) + output DELETED.* -- 1 +select title +from src + +insert into a(title) + output DELETED.id -- 2 + into del.hist(id) +select title +from src + +-- DELETE +delete d + output INSERTED.id -- 3 +from tbl as d +where category_id = 1 + +delete from d + output INSERTED.* -- 4 + into history.del_category +from tbl as d +where category_id = 1 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/InsertedDeletedIrrelevanceRule/TestSources/insert_update_delete_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/InsertedDeletedIrrelevanceRule/TestSources/insert_update_delete_good_raise_0_violations.sql new file mode 100644 index 00000000..f6230829 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/InsertedDeletedIrrelevanceRule/TestSources/insert_update_delete_good_raise_0_violations.sql @@ -0,0 +1,57 @@ +-- INSERT +-- no output +insert into a(title) +select title +from src + +insert into a(title) + output inserted.lastmod +select title +from src + +insert into a(title) + output inserted.id, inserted.lastmod + into #mod (id, lastmod) +select title +from src + +-- DELETE +-- no output +delete src +where category_id = 1 + +delete from d + output deleted.id +from tbl as d +where category_id = 1 + +delete from d + output deleted.id, GETDATE() + into history.del_category(id, del_dt) +from tbl as d +where category_id = 1 + +-- UPDATE +-- deleted +update d set + lastmod = GETDATE() + output deleted.id + into history.upd_category(id) +from tbl as d +where category_id = 1 + +-- inserted +update d set + lastmod = GETDATE() + output inserted.lastmod + into history.upd_category(lastmod) +from tbl as d +where category_id = 1 + +-- both +update d set + title = @new_title + output inserted.title, deleted.title + into history.upd_category(new_title, old_title) +from tbl as d +where category_id = 1 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/InsertedDeletedIrrelevanceRule/TestSources/merge_bad_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/InsertedDeletedIrrelevanceRule/TestSources/merge_bad_raise_2_violations.sql new file mode 100644 index 00000000..43c9d5e8 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/InsertedDeletedIrrelevanceRule/TestSources/merge_bad_raise_2_violations.sql @@ -0,0 +1,17 @@ +merge foo +using bar +on a = b +when matched then + delete +output inserted.* -- 1 +; + +merge foo +using bar +on a = b +when not matched by target then + insert (x, y) + values (z, w) +output deleted.a -- 2 +into #tmp (deleted_a) +; diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/InsertedDeletedIrrelevanceRule/TestSources/merge_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/InsertedDeletedIrrelevanceRule/TestSources/merge_good_raise_0_violations.sql new file mode 100644 index 00000000..59f0cc4e --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/InsertedDeletedIrrelevanceRule/TestSources/merge_good_raise_0_violations.sql @@ -0,0 +1,42 @@ +merge foo +using bar +on a = b +when matched then + delete +-- no output +; + +merge foo +using bar +on a = b +when matched then + delete +output deleted.*; + +merge foo +using bar +on a = b +when not matched by target then + insert (x, y) + values (z, w) +output $action, inserted.a +into #tmp (act, inserted_a); + +merge foo +using bar +on a = b +when matched then + delete +when not matched by target then + insert (x, y) + values (z, w) +output inserted.a, deleted.w +into #tmp (inserted_a, deleted_w); + +merge foo +using bar +on a = b +when matched then + update set + title = new_title +output inserted.title as new_title, deleted.title as old_title; diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/LiteralOversizeRule/TestSources/binary_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/LiteralOversizeRule/TestSources/binary_raise_2_violations.sql new file mode 100644 index 00000000..226d33ce --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/LiteralOversizeRule/TestSources/binary_raise_2_violations.sql @@ -0,0 +1,2 @@ +DECLARE @f BINARY(1) = 4096 -- 0x1000 +DECLARE @f VARBINARY(10) = 0x010203040506070809101112131415 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/LiteralOversizeRule/TestSources/good_size_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/LiteralOversizeRule/TestSources/good_size_raise_0_violations.sql index de1adf3d..44d1802e 100644 --- a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/LiteralOversizeRule/TestSources/good_size_raise_0_violations.sql +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/LiteralOversizeRule/TestSources/good_size_raise_0_violations.sql @@ -20,3 +20,7 @@ BEGIN SET @ErrorMsg = '. ' + ISNULL(@ErrorMsg, 'foo.bar failed'); END; GO + +DECLARE @d BINARY(10) = 1 +DECLARE @e BINARY(1) = 0x01 +DECLARE @f VARBINARY(10) = 0x01020304050607080910 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/NonCorrelatedJoinPredicateRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/NonCorrelatedJoinPredicateRuleTests.cs new file mode 100644 index 00000000..609fb4d5 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/NonCorrelatedJoinPredicateRuleTests.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(NonCorrelatedJoinPredicateRule))] + public sealed class NonCorrelatedJoinPredicateRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(NonCorrelatedJoinPredicateRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..36abcb72 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,67 @@ +-- bare select +SELECT 1 + +-- no FROM +SELECT 1 +WHERE 1=1 + +-- no JOIN +SELECT * +FROM foo +WHERE foo.group_id = 123 + +SELECT 1 +FROM dbo.foo AS f +INNER JOIN @bar AS b + ON b.group_id = f.group_id +INNER JOIN dbo.car AS c + ON c.id = b.id + +SELECT 1 +FROM dbo.foo AS f +INNER JOIN scm.bar AS b + ON scm.bar.id > f.id +LEFT JOIN asdf.far + ON bar.title != '' + AND asdf.far.something = b.something + +-- both OR parts are fine +SELECT 1 +FROM dbo.foo AS f +INNER JOIN scm.bar AS b + ON scm.bar.id > f.id + OR foo.group_id = ISNULL(b.group_id, -1) + +-- left inner +select * +from foo + left join bar + inner join jar + on bar.another_id = jar.id + on bar.foo_id = foo.id + +-- bunch of joins to dictionaries +select 1 +from trades as t +inner join products as p + on p.id = t.product_id +inner join locations as l + on l.id = t.location_id +left join defects as d + on d.trade_id = t.id +outer apply +( + select top 1 c.descr + from complaints c + where c.trade_id = t.id + order by c.dt desc +) as c +left join +( + select top 10 tt.id + from trades tt + where tt.product_id = t.product_id + order by tt.dt desc +) tt +on tt.merchant_id = t.merchant_id + diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/bad_join_raise_3_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/bad_join_raise_3_violations.sql new file mode 100644 index 00000000..51418642 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/bad_join_raise_3_violations.sql @@ -0,0 +1,21 @@ +SELECT 1 +FROM dbo.foo AS f +INNER JOIN dbo.car AS c + ON b.group_id > 1 -- 1 +WHERE c.id = b.id + +SELECT 1 +FROM dbo.foo AS f +INNER JOIN dbo.bar AS b + ON b.enabled = @enabled -- 2 +LEFT JOIN dbo.far + ON b.title != '' -- 3 + +/* TODO : this should be reported as well +SELECT 1 +FROM dbo.foo AS f +INNER JOIN dbo.bar AS b + ON b.some_id = f.some_id +INNER JOIN dbo.car AS c + ON b.group_id = f.group_id -- 4 "c" not mentioned +*/ diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/bad_left_inner_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/bad_left_inner_raise_2_violations.sql new file mode 100644 index 00000000..3a885231 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/bad_left_inner_raise_2_violations.sql @@ -0,0 +1,6 @@ +select * +from foo + left join bar + inner join jar + on jar.id = @x -- 1 + on bar.foo_id > 0 -- 2 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 new file mode 100644 index 00000000..20b52153 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/between_raise_0_violations.sql @@ -0,0 +1,4 @@ +SELECT 1 +FROM dbo.foo AS f +INNER JOIN scm.bar AS b + ON scm.bar.dt BETWEEN f.period_start AND dbo.foo.period_end diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/like_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/like_raise_0_violations.sql new file mode 100644 index 00000000..ae5bf87a --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodeSmell/NonCorrelatedJoinPredicateRule/TestSources/like_raise_0_violations.sql @@ -0,0 +1,4 @@ +SELECT 1 +FROM dbo.foo AS f +INNER JOIN scm.bar AS b + ON scm.bar.title LIKE f.pattern diff --git a/TeamTools.TSQL.LinterTests/UnitTests/CodingConvention/ExecWithoutExecRule/TestSources/invisible_char_identifier_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/CodingConvention/ExecWithoutExecRule/TestSources/invisible_char_identifier_raise_0_violations.sql new file mode 100644 index 00000000..7397ac1b --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/CodingConvention/ExecWithoutExecRule/TestSources/invisible_char_identifier_raise_0_violations.sql @@ -0,0 +1,4 @@ +/* there is an invisible ZWNBSP unicode symbol in the line below +which is treated by ScriptDom as identifier +but the rule should ignore it */ + --<< here diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/all_good_raise_0_violations.sql index cb038831..7d104db0 100644 --- a/TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/all_good_raise_0_violations.sql +++ b/TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/all_good_raise_0_violations.sql @@ -22,3 +22,6 @@ CREATE SERVICE MainEntities_TargetService ON QUEUE dbo._TargetQueue_MainEntities ([//my_company/SQL/backend_MainEntities_contract], [//my_company/SQL/backend_MainEntities_contract_a76t]); GO + +select 1 as col_name +from src as s diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/alphabet_mix_in_definitions_raise_3_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/alphabet_mix_in_definitions_raise_3_violations.sql index 86b12e7c..7259045b 100644 --- a/TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/alphabet_mix_in_definitions_raise_3_violations.sql +++ b/TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/alphabet_mix_in_definitions_raise_3_violations.sql @@ -1,4 +1,4 @@ -CREATE PROC dbo.[новая test хранимка] -- 1 +CREATE PROC dbo.новая_test_хранимка -- 1 AS BEGIN DECLARE diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/bad_col_not_invented_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/bad_col_not_invented_raise_0_violations.sql new file mode 100644 index 00000000..c2d4fa3d --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/bad_col_not_invented_raise_0_violations.sql @@ -0,0 +1,3 @@ +-- if table was defined like this then there is no other option then using given name +select s.ыi +from src as s diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/column_alias_mix_raise_3_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/column_alias_mix_raise_3_violations.sql new file mode 100644 index 00000000..d85dcb72 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/column_alias_mix_raise_3_violations.sql @@ -0,0 +1,17 @@ + +select 1 as ыi -- 1 +from src as s + +select * +from +( + values (1), (2) +) as t (ыi) -- 2 + +;with cte (ыi) -- 3 +as +( + select foo + from bar +) +select * from cte diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/complex_names_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/complex_names_raise_0_violations.sql new file mode 100644 index 00000000..44b891eb --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/complex_names_raise_0_violations.sql @@ -0,0 +1,3 @@ +select 1 as [TD align=right] + , '' [Сумма счёта (без учета НДС)] + , NULL as [Некоторый Quoted Identifier 123] diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/open_json_column_alias_mix_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/open_json_column_alias_mix_raise_1_violations.sql new file mode 100644 index 00000000..dedde4b2 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/open_json_column_alias_mix_raise_1_violations.sql @@ -0,0 +1,4 @@ +-- compatibility level min: 130 +SELECT * +FROM OPENJSON(@data) +WITH (ыi INT); -- here diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/quoted_identifier_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/quoted_identifier_raise_0_violations.sql new file mode 100644 index 00000000..85978c87 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Naming/AlphabetMixInIdentifierRule/TestSources/quoted_identifier_raise_0_violations.sql @@ -0,0 +1,3 @@ +CREATE PROC dbo.[новая test хранимка] +as; + diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Naming/IdentifierContainsLookAlikeCharRule/IdentifierContainsLookAlikeCharRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Naming/IdentifierContainsLookAlikeCharRule/IdentifierContainsLookAlikeCharRuleTests.cs new file mode 100644 index 00000000..52edb465 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Naming/IdentifierContainsLookAlikeCharRule/IdentifierContainsLookAlikeCharRuleTests.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.Collections.Generic; +using TeamTools.TSQL.Linter.Rules; + +namespace TeamTools.TSQL.LinterTests +{ + [Category("Linter.TSQL.Naming")] + [TestOfRule(typeof(IdentifierContainsLookAlikeCharRule))] + public class IdentifierContainsLookAlikeCharRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(IdentifierContainsLookAlikeCharRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Naming/IdentifierContainsLookAlikeCharRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Naming/IdentifierContainsLookAlikeCharRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..f5851af5 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Naming/IdentifierContainsLookAlikeCharRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,5 @@ +select 1 as [Master] -- all latin +GO + +create type [средний] -- all cyrillic +from int; diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Naming/IdentifierContainsLookAlikeCharRule/TestSources/bad_char_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Naming/IdentifierContainsLookAlikeCharRule/TestSources/bad_char_raise_2_violations.sql new file mode 100644 index 00000000..2b918ad0 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Naming/IdentifierContainsLookAlikeCharRule/TestSources/bad_char_raise_2_violations.sql @@ -0,0 +1,5 @@ +select 1 as [Маst] -- cyrillic "Ма" +GO + +create type [cледний] -- latin "c" +from int diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/NonSargablePredicateRule/TestSources/computations_raise_3_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/NonSargablePredicateRule/TestSources/computations_raise_3_violations.sql index d61f0ef4..5fcde4aa 100644 --- a/TeamTools.TSQL.LinterTests/UnitTests/Performance/NonSargablePredicateRule/TestSources/computations_raise_3_violations.sql +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/NonSargablePredicateRule/TestSources/computations_raise_3_violations.sql @@ -1,8 +1,8 @@ SELECT * FROM dbo.foo f INNER JOIN bdo.bar b -ON b.id = f.id+1 -WHERE (f.num - b.num) = 0 +ON ((b.id = f.id+1)) +WHERE ((f.num - b.num) = 0) DELETE b FROM dbo.foo f diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SubstringPredicateToLikeRule/TestSources/charindex_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SubstringPredicateToLikeRule/TestSources/charindex_raise_2_violations.sql new file mode 100644 index 00000000..480ca6b3 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SubstringPredicateToLikeRule/TestSources/charindex_raise_2_violations.sql @@ -0,0 +1,4 @@ +SELECT f.id +FROM dbo.foo f +WHERE (CHARINDEX('A', f.title)) = 1 + OR (1) = CHARINDEX('B', f.title) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SubstringPredicateToLikeRule/TestSources/join_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SubstringPredicateToLikeRule/TestSources/join_raise_2_violations.sql new file mode 100644 index 00000000..91cb8a36 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SubstringPredicateToLikeRule/TestSources/join_raise_2_violations.sql @@ -0,0 +1,5 @@ +SELECT f.id +FROM dbo.foo f +JOIN dbo.bar b +ON ((CHARINDEX('A', b.title)) = 1 -- 1 +AND ('A' = LEFT(f.name, 1))) -- 2 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SubstringPredicateToLikeRule/TestSources/left_as_like_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SubstringPredicateToLikeRule/TestSources/left_as_like_raise_2_violations.sql index becb7ea9..27bd70e5 100644 --- a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SubstringPredicateToLikeRule/TestSources/left_as_like_raise_2_violations.sql +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SubstringPredicateToLikeRule/TestSources/left_as_like_raise_2_violations.sql @@ -1,4 +1,4 @@ SELECT 1 FROM dbo.foo f WHERE LEFT(f.title, 2) = 'asdf' - OR LEFT(f.title, 2) = @str + OR @str = LEFT(f.title, 2) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SubstringPredicateToLikeRule/TestSources/other_function_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SubstringPredicateToLikeRule/TestSources/other_function_raise_0_violations.sql index e37d274c..18e9d435 100644 --- a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SubstringPredicateToLikeRule/TestSources/other_function_raise_0_violations.sql +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SubstringPredicateToLikeRule/TestSources/other_function_raise_0_violations.sql @@ -1,3 +1,4 @@ SELECT f.id FROM dbo.foo f WHERE REPLACE(f.title, 1, 1) = 'asdf' + OR dbo.MyFn(f.title) = 'X' diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Performance/SubstringPredicateToLikeRule/TestSources/unknown_value_or_start_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SubstringPredicateToLikeRule/TestSources/unknown_value_or_start_raise_0_violations.sql new file mode 100644 index 00000000..31b0a550 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Performance/SubstringPredicateToLikeRule/TestSources/unknown_value_or_start_raise_0_violations.sql @@ -0,0 +1,7 @@ +-- col value and function results are unpredictable +SELECT f.id +FROM dbo.foo f +WHERE SUBSTRING(f.title, dbo.my_fn(), 1) = 'A' + AND SUBSTRING(f.title, LEN(title_prefix), 1) = 'A' + AND LEFT(f.descr, substr_length) = f.some_col + AND LEFT(f.descr, LEN(descr_length_for_match)) = 'asdf' diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/RedundantIndexFilterRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/RedundantIndexFilterRuleTests.cs new file mode 100644 index 00000000..716f6d1c --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/RedundantIndexFilterRuleTests.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(RedundantIndexFilterRule))] + public sealed class RedundantIndexFilterRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(RedundantIndexFilterRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..0de3e817 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,34 @@ +CREATE TABLE dbo.foo +( + category_id INT NOT NULL + , CONSTRAINT ck CHECK (category_id <> 1 AND category_id < 10) +) +GO + +-- table name is different +CREATE INDEX ix ON dbo.bar(category_id) +WHERE category_id <> 1 AND category_id < 10 +GO + +-- column name is different +CREATE INDEX ix ON dbo.foo(category_id) +WHERE some_id <> 1 AND some_id < 10 +GO + +-- no filter +CREATE INDEX ix ON dbo.foo(category_id) +GO + +-- no constraints +CREATE TABLE dbo.my_table +( + some_id INT +) +GO + +CREATE INDEX ix ON dbo.my_table(some_id) +WHERE some_id IS NOT NULL +GO + +CREATE INDEX ix ON dbo.my_table(some_id) +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/check_filter_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/check_filter_raise_2_violations.sql new file mode 100644 index 00000000..8d1afc53 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/check_filter_raise_2_violations.sql @@ -0,0 +1,11 @@ +CREATE TABLE dbo.foo +( + category_id INT + , CONSTRAINT ck CHECK (100 < category_id AND NOT (category_id >= (1000))) +) +GO + +CREATE INDEX ix ON dbo.foo(category_id) +WHERE category_id < 1000 -- 1 + AND category_id > 100 -- 2 +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/does_not_fail_on_filetable_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/does_not_fail_on_filetable_raise_0_violations.sql new file mode 100644 index 00000000..8623ad74 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/does_not_fail_on_filetable_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/Redundancy/RedundantIndexFilterRule/TestSources/inline_idx_bad_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/inline_idx_bad_raise_2_violations.sql new file mode 100644 index 00000000..5ceb704a --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/inline_idx_bad_raise_2_violations.sql @@ -0,0 +1,10 @@ +-- compatibility level min: 130 +CREATE TABLE dbo.foo +( + category_id INT NOT NULL + , CONSTRAINT ck CHECK (category_id > 0) + , INDEX ix (category_id) WHERE + category_id IS NOT NULL -- 1 + AND category_id > 0 -- 2 +) +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/inline_idx_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/inline_idx_good_raise_0_violations.sql new file mode 100644 index 00000000..362fc3d4 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/inline_idx_good_raise_0_violations.sql @@ -0,0 +1,9 @@ +-- compatibility level min: 130 +CREATE TABLE dbo.foo +( + category_id INT NOT NULL + , CONSTRAINT ck CHECK (category_id <> 1 AND category_id < 10) + , INDEX ix1 (category_id) + , INDEX ix2 (category_id) WHERE category_id > 100 +) +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/not_null_col_filter_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/not_null_col_filter_raise_1_violations.sql new file mode 100644 index 00000000..113120c4 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/not_null_col_filter_raise_1_violations.sql @@ -0,0 +1,9 @@ +CREATE TABLE dbo.foo +( + category_id INT NOT NULL +) +GO + +CREATE INDEX ix ON dbo.foo(category_id) +WHERE category_id IS NOT NULL -- here +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/or_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/or_raise_0_violations.sql new file mode 100644 index 00000000..c8c62588 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/RedundantIndexFilterRule/TestSources/or_raise_0_violations.sql @@ -0,0 +1,14 @@ +CREATE TABLE dbo.foo +( + category_id INT NOT NULL + , CONSTRAINT ck CHECK (category_id = 1 OR category_id < 10) +) +GO + +CREATE INDEX ix ON dbo.foo(category_id) +WHERE category_id = 1 +GO + +CREATE INDEX ix ON dbo.foo(category_id) +WHERE category_id < 10 +GO diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/WherePredicateSameAsJoinPredicateRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/WherePredicateSameAsJoinPredicateRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..2d40de18 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/WherePredicateSameAsJoinPredicateRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,33 @@ +-- no from +select 1 +where 1=0 + +-- no where +select 1 +from a + +update t set + x = y +from a +WHERE CURRENT OF cr + +-- no join +select 1 +from a +where x > y + +-- no inner join +select 1 +from a +left join b on id = parent_id +outer apply dbo.my_fn(arg) +right join c on foo = bar +where x > y + +-- no dups +select 1 +from a +inner join b on b.id = a.parent_id and b.x > b.y +where a.group_id = 123 +and b.value is not null + diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/WherePredicateSameAsJoinPredicateRule/TestSources/extra_predicate_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/WherePredicateSameAsJoinPredicateRule/TestSources/extra_predicate_raise_2_violations.sql new file mode 100644 index 00000000..f8113c2f --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/WherePredicateSameAsJoinPredicateRule/TestSources/extra_predicate_raise_2_violations.sql @@ -0,0 +1,14 @@ +select 1 +from a +inner join @bar as b on b.id = a.parent_id and b.x > b.y +inner join far as f on f.some_id = a.other_id +left join car as c on c.some_id = a.another_id and @var > 100 +where a.group_id = 123 + and (((b.x)) > b.y) -- here + +select 1 +from a +inner join bar on bar.id = a.parent_id + and ((123)) = group_id +where (group_id = 123) -- here + and bar.y < bar.x diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/WherePredicateSameAsJoinPredicateRule/TestSources/left_inner_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/WherePredicateSameAsJoinPredicateRule/TestSources/left_inner_raise_0_violations.sql new file mode 100644 index 00000000..c608bebe --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/WherePredicateSameAsJoinPredicateRule/TestSources/left_inner_raise_0_violations.sql @@ -0,0 +1,8 @@ +select * +from foo +left join bar + inner join far + on far.group_id = bar.group_id + on bar.id = foo.id + and @var > 100 +where @var > 100 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/WherePredicateSameAsJoinPredicateRule/WherePredicateSameAsJoinPredicateRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/WherePredicateSameAsJoinPredicateRule/WherePredicateSameAsJoinPredicateRuleTests.cs new file mode 100644 index 00000000..bf537735 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Redundancy/WherePredicateSameAsJoinPredicateRule/WherePredicateSameAsJoinPredicateRuleTests.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(WherePredicateSameAsJoinPredicateRule))] + public sealed class WherePredicateSameAsJoinPredicateRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(WherePredicateSameAsJoinPredicateRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleAndToNotInRule/MultipleAndToNotInRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleAndToNotInRule/MultipleAndToNotInRuleTests.cs new file mode 100644 index 00000000..5ee009cf --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleAndToNotInRule/MultipleAndToNotInRuleTests.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(MultipleAndToNotInRule))] + public sealed class MultipleAndToNotInRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(MultipleAndToNotInRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleAndToNotInRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleAndToNotInRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..3f9fb6c3 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleAndToNotInRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,20 @@ +SELECT * +FROM dbo.foo +WHERE category_id NOT IN (1, 2, 3) + +SELECT * +FROM dbo.foo +WHERE category_id NOT IN (1, 2, 3) + AND is_root <> 1 + +SELECT * +FROM dbo.foo +WHERE category_id = 1 + AND category_id <> @excluded_id + +IF (@category_id IS NOT NULL + AND (@category_id > 100 OR @category_id < 30)) + OR @has_no_category = 1 +BEGIN + SET @bar = 'far' +END diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleAndToNotInRule/TestSources/not_equals_to_in_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleAndToNotInRule/TestSources/not_equals_to_in_raise_2_violations.sql new file mode 100644 index 00000000..1a8117e3 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleAndToNotInRule/TestSources/not_equals_to_in_raise_2_violations.sql @@ -0,0 +1,14 @@ +-- ... => category_id NOT IN (1, 2, 3, 4, 5) +SELECT * +FROM dbo.foo +WHERE category_id NOT IN (1, 2, 3) + AND category_id <> 4 + AND category_id <> 5 + +-- ... => @category_id NOT IN (100, 200, 300) +IF (@category_id <> 100 + AND 200 <> @category_id + AND @category_id <> (300)) +BEGIN + SET @bar = 'far' +END diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInPredicateIntoOneRule/MultipleInPredicateIntoOneRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInPredicateIntoOneRule/MultipleInPredicateIntoOneRuleTests.cs new file mode 100644 index 00000000..34acdb3f --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInPredicateIntoOneRule/MultipleInPredicateIntoOneRuleTests.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(MultipleInPredicateIntoOneRule))] + public sealed class MultipleInPredicateIntoOneRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(MultipleInPredicateIntoOneRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInPredicateIntoOneRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInPredicateIntoOneRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..0107be46 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInPredicateIntoOneRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,23 @@ +SELECT * +FROM dbo.foo +WHERE category_id NOT IN (1, 2, 3) + +SELECT * +FROM dbo.foo +WHERE category_id IN (1, 2, 3) + + +SELECT * +FROM dbo.foo +WHERE category_id NOT IN (1, 2, 3) + AND title_id NOT IN (100, 200) + +SELECT * +FROM dbo.foo +WHERE category_id NOT IN (1, 2, 3) + OR category_id <> 200 -- OR cannot be collapsed to NOT IN + +SELECT * +FROM dbo.foo +WHERE category_id IN (1, 2, 3) + AND category_id <> 100 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInPredicateIntoOneRule/TestSources/multiple_in_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInPredicateIntoOneRule/TestSources/multiple_in_raise_1_violations.sql new file mode 100644 index 00000000..c87f8fac --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInPredicateIntoOneRule/TestSources/multiple_in_raise_1_violations.sql @@ -0,0 +1,5 @@ +-- ... => category_id IN (1, 2, 3, 4, 5) +SELECT * +FROM dbo.foo +WHERE category_id IN (1, 2, 3) + OR category_id IN (4, 5) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInPredicateIntoOneRule/TestSources/multiple_not_in_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInPredicateIntoOneRule/TestSources/multiple_not_in_raise_0_violations.sql new file mode 100644 index 00000000..ccf16044 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleInPredicateIntoOneRule/TestSources/multiple_not_in_raise_0_violations.sql @@ -0,0 +1,5 @@ +-- ... => category_id NOT IN (1, 2, 3, 4, 5) +SELECT * +FROM dbo.foo +WHERE category_id NOT IN (1, 2, 3) + AND category_id NOT IN (4, 5) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleNotInPredicateIntoOneRule/MultipleNotInPredicateIntoOneRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleNotInPredicateIntoOneRule/MultipleNotInPredicateIntoOneRuleTests.cs new file mode 100644 index 00000000..6ddabc8e --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleNotInPredicateIntoOneRule/MultipleNotInPredicateIntoOneRuleTests.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(MultipleNotInPredicateIntoOneRule))] + public sealed class MultipleNotInPredicateIntoOneRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(MultipleNotInPredicateIntoOneRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleNotInPredicateIntoOneRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleNotInPredicateIntoOneRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..0107be46 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleNotInPredicateIntoOneRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,23 @@ +SELECT * +FROM dbo.foo +WHERE category_id NOT IN (1, 2, 3) + +SELECT * +FROM dbo.foo +WHERE category_id IN (1, 2, 3) + + +SELECT * +FROM dbo.foo +WHERE category_id NOT IN (1, 2, 3) + AND title_id NOT IN (100, 200) + +SELECT * +FROM dbo.foo +WHERE category_id NOT IN (1, 2, 3) + OR category_id <> 200 -- OR cannot be collapsed to NOT IN + +SELECT * +FROM dbo.foo +WHERE category_id IN (1, 2, 3) + AND category_id <> 100 diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleNotInPredicateIntoOneRule/TestSources/multiple_in_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleNotInPredicateIntoOneRule/TestSources/multiple_in_raise_0_violations.sql new file mode 100644 index 00000000..c87f8fac --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleNotInPredicateIntoOneRule/TestSources/multiple_in_raise_0_violations.sql @@ -0,0 +1,5 @@ +-- ... => category_id IN (1, 2, 3, 4, 5) +SELECT * +FROM dbo.foo +WHERE category_id IN (1, 2, 3) + OR category_id IN (4, 5) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleNotInPredicateIntoOneRule/TestSources/multiple_not_in_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleNotInPredicateIntoOneRule/TestSources/multiple_not_in_raise_1_violations.sql new file mode 100644 index 00000000..ccf16044 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleNotInPredicateIntoOneRule/TestSources/multiple_not_in_raise_1_violations.sql @@ -0,0 +1,5 @@ +-- ... => category_id NOT IN (1, 2, 3, 4, 5) +SELECT * +FROM dbo.foo +WHERE category_id NOT IN (1, 2, 3) + AND category_id NOT IN (4, 5) diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleOrToInRule/MultipleOrToInRuleTests.cs b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleOrToInRule/MultipleOrToInRuleTests.cs new file mode 100644 index 00000000..322ec133 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleOrToInRule/MultipleOrToInRuleTests.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(MultipleOrToInRule))] + public sealed class MultipleOrToInRuleTests : BaseRuleTest + { + [TestCaseSource(nameof(TestCasePresets))] + public override void TestRule(string scriptPath, int expectedViolationCount) + { + CheckRuleViolations(scriptPath, expectedViolationCount); + } + + private static IEnumerable TestCasePresets() + { + return GetTestSources(typeof(MultipleOrToInRuleTests)); + } + } +} diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleOrToInRule/TestSources/all_good_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleOrToInRule/TestSources/all_good_raise_0_violations.sql new file mode 100644 index 00000000..2ddc2f6c --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleOrToInRule/TestSources/all_good_raise_0_violations.sql @@ -0,0 +1,20 @@ +SELECT * +FROM dbo.foo +WHERE category_id IN (1, 2, 3) + +SELECT * +FROM dbo.foo +WHERE category_id IN (1, 2, 3) + OR is_root = 1 + +SELECT * +FROM dbo.foo +WHERE category_id = 1 + AND category_id <> @excluded_id + +IF (@category_id IS NOT NULL + AND (@category_id > 100 OR @category_id < 30)) + OR @has_no_category = 1 +BEGIN + SET @bar = 'far' +END diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleOrToInRule/TestSources/already_has_in_raise_1_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleOrToInRule/TestSources/already_has_in_raise_1_violations.sql new file mode 100644 index 00000000..951aa261 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleOrToInRule/TestSources/already_has_in_raise_1_violations.sql @@ -0,0 +1,6 @@ +-- ... => category_id IN (1, 2, 3, 4, 5) +SELECT * +FROM dbo.foo +WHERE category_id IN (1, 2, 3) + OR category_id = 4 + or 5 = category_id diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleOrToInRule/TestSources/many_items_raise_2_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleOrToInRule/TestSources/many_items_raise_2_violations.sql new file mode 100644 index 00000000..61d2c70b --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleOrToInRule/TestSources/many_items_raise_2_violations.sql @@ -0,0 +1,17 @@ +-- ... => category_id IN (1, 2, 3) +SELECT * +FROM dbo.foo +WHERE category_id = 1 + OR (2 = category_id) + OR title <> 'asfd' + OR (CATEGORY_ID = (3)) + OR id > 100 + OR 0 = @take_all + +-- ... => @category_id IN (100, 200, 300) +IF (@category_id = 100 + OR 200 = @category_id + OR @category_id = (300)) +BEGIN + SET @bar = 'far' +END diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleOrToInRule/TestSources/not_in_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleOrToInRule/TestSources/not_in_raise_0_violations.sql new file mode 100644 index 00000000..94e57972 --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleOrToInRule/TestSources/not_in_raise_0_violations.sql @@ -0,0 +1,4 @@ +SELECT * +FROM dbo.foo +WHERE category_id NOT IN (1, 2, 3) + OR category_id = @included_id diff --git a/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleOrToInRule/TestSources/table_definition_raise_0_violations.sql b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleOrToInRule/TestSources/table_definition_raise_0_violations.sql new file mode 100644 index 00000000..4f1e37ae --- /dev/null +++ b/TeamTools.TSQL.LinterTests/UnitTests/Simplification/MultipleOrToInRule/TestSources/table_definition_raise_0_violations.sql @@ -0,0 +1,6 @@ +CREATE TABLE dbo.foo +( + bar CHAR(10) + , computed_col AS (CASE WHEN bar = 'B' OR bar = 'A' OR bar = 'R' THEN 1 ELSE 0 END) + , CONSTRAINT CK_BAR_FILTER CHECK (bar = 'B' OR bar = 'A' OR bar = 'R') +) From 0a01fc050ef9a722e175b6261117261ae1015db7 Mon Sep 17 00:00:00 2001 From: Ivan Starostin Date: Fri, 13 Feb 2026 18:17:17 +0300 Subject: [PATCH 2/3] docs upd --- .../Resources/Docs/en-us/CS0840.md | 43 ++++++++++++++ .../Resources/Docs/en-us/CS0841.md | 16 ++++++ .../Resources/Docs/en-us/CS0842.md | 16 ++++++ .../Resources/Docs/en-us/CS0843.md | 44 +++++++++++++++ .../Resources/Docs/en-us/CS0844.md | 40 +++++++++++++ .../Resources/Docs/en-us/CV0838.md | 41 ++++++++++++++ .../Resources/Docs/en-us/CV0839.md | 35 ++++++++++++ .../Docs/en-us/Category_CodeSmell.md | 9 ++- .../Docs/en-us/Category_CodingConvention.md | 2 + .../Docs/en-us/Category_Redundancy.md | 1 + .../Docs/en-us/Category_Simplification.md | 4 ++ .../Resources/Docs/en-us/Group_Indices.md | 1 + .../Resources/Docs/en-us/PF0956.md | 4 +- .../Resources/Docs/en-us/RD0849.md | 56 +++++++++++++++++++ .../Resources/Docs/en-us/SI0845.md | 35 ++++++++++++ .../Resources/Docs/en-us/SI0846.md | 34 +++++++++++ .../Resources/Docs/en-us/SI0847.md | 34 +++++++++++ .../Resources/Docs/en-us/SI0848.md | 34 +++++++++++ .../Resources/Docs/ru-ru/CS0840.md | 43 ++++++++++++++ .../Resources/Docs/ru-ru/CS0841.md | 16 ++++++ .../Resources/Docs/ru-ru/CS0842.md | 16 ++++++ .../Resources/Docs/ru-ru/CS0843.md | 44 +++++++++++++++ .../Resources/Docs/ru-ru/CS0844.md | 40 +++++++++++++ .../Resources/Docs/ru-ru/CV0838.md | 41 ++++++++++++++ .../Resources/Docs/ru-ru/CV0839.md | 35 ++++++++++++ .../Resources/Docs/ru-ru/PF0956.md | 2 +- .../Resources/Docs/ru-ru/RD0849.md | 56 +++++++++++++++++++ .../Resources/Docs/ru-ru/SI0845.md | 35 ++++++++++++ .../Resources/Docs/ru-ru/SI0846.md | 34 +++++++++++ .../Resources/Docs/ru-ru/SI0847.md | 34 +++++++++++ .../Resources/Docs/ru-ru/SI0848.md | 34 +++++++++++ ...20\264\320\265\320\272\321\201\321\213.md" | 1 + ...20\275\320\276\321\201\321\202\321\214.md" | 1 + ...20\262\320\260\320\275\320\270\321\216.md" | 2 + ...20\262\320\260\320\275\320\270\320\265.md" | 9 ++- ...21\211\320\265\320\275\320\270\320\265.md" | 4 ++ .../Resources/ViolationMessages.json | 14 +++++ .../Resources/ViolationMessages.ru-ru.json | 14 +++++ TeamTools.TSQL.Linter/readme.md | 4 ++ 39 files changed, 921 insertions(+), 7 deletions(-) create mode 100644 TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0840.md create mode 100644 TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0841.md create mode 100644 TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0842.md create mode 100644 TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0843.md create mode 100644 TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0844.md create mode 100644 TeamTools.TSQL.Linter/Resources/Docs/en-us/CV0838.md create mode 100644 TeamTools.TSQL.Linter/Resources/Docs/en-us/CV0839.md create mode 100644 TeamTools.TSQL.Linter/Resources/Docs/en-us/RD0849.md create mode 100644 TeamTools.TSQL.Linter/Resources/Docs/en-us/SI0845.md create mode 100644 TeamTools.TSQL.Linter/Resources/Docs/en-us/SI0846.md create mode 100644 TeamTools.TSQL.Linter/Resources/Docs/en-us/SI0847.md create mode 100644 TeamTools.TSQL.Linter/Resources/Docs/en-us/SI0848.md create mode 100644 TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0840.md create mode 100644 TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0841.md create mode 100644 TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0842.md create mode 100644 TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0843.md create mode 100644 TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0844.md create mode 100644 TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CV0838.md create mode 100644 TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CV0839.md create mode 100644 TeamTools.TSQL.Linter/Resources/Docs/ru-ru/RD0849.md create mode 100644 TeamTools.TSQL.Linter/Resources/Docs/ru-ru/SI0845.md create mode 100644 TeamTools.TSQL.Linter/Resources/Docs/ru-ru/SI0846.md create mode 100644 TeamTools.TSQL.Linter/Resources/Docs/ru-ru/SI0847.md create mode 100644 TeamTools.TSQL.Linter/Resources/Docs/ru-ru/SI0848.md create mode 100644 TeamTools.TSQL.Linter/readme.md diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0840.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0840.md new file mode 100644 index 00000000..3565d04b --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0840.md @@ -0,0 +1,43 @@ + +# Symbols removed and compared with string containing them + +||| +|-|-| +| Id | **CS0840** +| Mnemo | CHAR_REMOVED_AND_SEARCHED +| Severity | ⚠ Warning +| Category | [CodeSmell](./Category_CodeSmell.md) +| Source code | [CharRemovedAndThenSearchedRule.cs](../../../Rules/CodeSmell/CharRemovedAndThenSearchedRule.cs) + +## Cause + +This rule is triggered when the result of a `REPLACE`, `TRIM`, `LTRIM`, or `RTRIM` function, which removes specific characters, is compared to a string that contains those same characters in its middle, beginning, or/and end, respectively. +

Verify the comparison string excludes the characters being removed.

+ +## Examples + +Bad + +```sql +IF REPLACE(@s, 'X', 'Y') <> 'XXX asdf' +OR REPLACE(@s, 'X', 'Y') = 'as X df' +BEGIN + PRINT '?' +END +``` + +Good + +```sql +IF REPLACE(@s, 'X', 'Y') <> 'ZZZ asdf' +OR REPLACE(@s, 'X', 'Y') = 'as Z df' +BEGIN + PRINT '?' +END +``` + +## Tips + +💡 The value with which the result of the `REPLACE`, `TRIM`, `LTRIM` or `RTRIM` function is compared must be specific: either a literal or a variable that has a predictable value. + +💡 The rule takes into account the fact that `TRIM` functions can accept an explicit character or a substring to be removed. diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0841.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0841.md new file mode 100644 index 00000000..df125425 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0841.md @@ -0,0 +1,16 @@ + + +# Identifier contains non-printable character + +||| +|-|-| +| Id | **CS0841** +| Mnemo | INVISIBLE_CHAR_IN_IDENTIFIER +| Severity | ℹ Hint +| Category | [CodeSmell](./Category_CodeSmell.md) +| Source code | [IdentifierHasInvisibleCharRule.cs](../../../Rules/CodeSmell/IdentifierHasInvisibleCharRule.cs) + +## Cause + +This rule is triggered when identifier contains non-printable character. +

Remove non-printable character.

diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0842.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0842.md new file mode 100644 index 00000000..4fc6ff08 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0842.md @@ -0,0 +1,16 @@ + + +# Comment contains non-printable character + +||| +|-|-| +| Id | **CS0842** +| Mnemo | INVISIBLE_CHAR_IN_COMMENT +| Severity | ℹ Hint +| Category | [CodeSmell](./Category_CodeSmell.md) +| Source code | [CommentHasInvisibleCharRule.cs](../../../Rules/CodeSmell/CommentHasInvisibleCharRule.cs) + +## Cause + +This rule is triggered when comment contains non-printable character. +

Remove non-printable character.

diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0843.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0843.md new file mode 100644 index 00000000..78930698 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0843.md @@ -0,0 +1,44 @@ + + +# The expression doesn't fill the used system table + +||| +|-|-| +| Id | **CS0843** +| Mnemo | OUTPUT_MISMATCHES_ACTION +| Severity | ⚠ Warning +| Category | [CodeSmell](./Category_CodeSmell.md) +| Source code | [InsertedDeletedIrrelevanceRule.cs](../../../Rules/CodeSmell/InsertedDeletedIrrelevanceRule.cs) + +## Cause + +This rule is triggered when the expression doesn't fill the used in `OUTPUT [INTO]` system table. +

Fix OUTPUT [INTO] construction.

+ +## Examples + +Bad + +```sql +MERGE foo AS trg +USING bar AS src +ON trg.id = src.id +WHEN NOT MATCHED BY TARGET THEN + INSERT (name, position) + VALUES (src.name, src.position) +OUTPUT DELETED.id +INTO #tmp (id) +``` + +Good + +```sql +MERGE foo AS trg +USING bar AS src +ON trg.id = src.id +WHEN NOT MATCHED BY TARGET THEN + INSERT (name, position) + VALUES (src.name, src.position) +OUTPUT INSERTED.id +INTO #tmp (id) +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0844.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0844.md new file mode 100644 index 00000000..db8b7db5 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CS0844.md @@ -0,0 +1,40 @@ + + +# All flow branches lead to the same behavior + +||| +|-|-| +| Id | **CS0844** +| Mnemo | CONDITIONS_SAME_DECISIONS +| Severity | ℹ Hint +| Category | [CodeSmell](./Category_CodeSmell.md) +| Source code | [ConditionsLeadToSimilarBehaviorRule.cs](../../../Rules/CodeSmell/ConditionsLeadToSimilarBehaviorRule.cs) + +## Cause + +This rule is triggered when all flow branches lead to the same behavior. +

Fix flow branches.

+ +## Examples + +Bad + +```sql +SET @a = CASE + WHEN @b = @c THEN + (CAST(GETDATE() AS DATE) + 1) + WHEN @c > 100 THEN + CAST(GETDATE() AS DATE) + 1 + END; +``` + +Good + +```sql +SET @a = CASE + WHEN @b = @c THEN + CAST(GETDATE() AS DATE) + 1 + WHEN @c > 100 THEN + CAST(GETDATE() AS DATE) + 5 + END; +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/CV0838.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CV0838.md new file mode 100644 index 00000000..595c1623 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CV0838.md @@ -0,0 +1,41 @@ + +# System type is not in UPPERCASE + +||| +|-|-| +| Id | **CV0838** +| Mnemo | SYSTEM_TYPE_UPPERCASE +| Severity | ⚠ Warning +| Category | [CodingConvention](./Category_CodingConvention.md) +| Source code | [SystemTypeUppercaseRule.cs](../../../Rules/CodingConvention/SystemTypeUppercaseRule.cs) + +## Cause + +This rule is triggered when a system datatype is written in lower or mixed case. +

Write system datatype in UPPERCASE.

+ +## Examples + +Bad + +```sql +CREATE PROCEDURE dbo.foo + @arg Bit -- 1 +AS +BEGIN + DECLARE @var int -- 2 +END +GO +``` + +Good + +```sql +CREATE PROCEDURE dbo.foo + @arg BIT +AS +BEGIN + DECLARE @var INT +END +GO +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/CV0839.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CV0839.md new file mode 100644 index 00000000..996bc88b --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/CV0839.md @@ -0,0 +1,35 @@ + +# Global variable is not in UPPERCASE + +||| +|-|-| +| Id | **CV0839** +| Mnemo | GLOBAL_VAR_UPPERCASE +| Severity | ⚠ Warning +| Category | [CodingConvention](./Category_CodingConvention.md) +| Source code | [GlobalVarUppercaseRule.cs](../../../Rules/CodingConvention/GlobalVarUppercaseRule.cs) + +## Cause + +This rule is triggered when a global variable name is written in lower or mixed case. +

Write global variable name in UPPERCASE.

+ +## Examples + +Bad + +```sql +IF @@rowcount > 0 + PRINT @@spid + +SELECT @@datefirst +``` + +Good + +```sql +IF @@ROWCOUNT > 0 + PRINT @@SPID + +SELECT @@DATEFIRST +``` 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 bd1bea2c..060c2df2 100644 --- a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_CodeSmell.md +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_CodeSmell.md @@ -86,8 +86,13 @@ | [CS0821](./CS0821.md) | DECIMAL without scale | [CS0830](./CS0830.md) | Same temporary table names | [CS0832](./CS0832.md) | Table variable inside a function -| [CS0790](./CS0834.md) | Look-alike char mix in string literal -| [CS0790](./CS0835.md) | Look-alike char mix in comment +| [CS0834](./CS0834.md) | Look-alike char mix in string literal +| [CS0835](./CS0835.md) | Look-alike char mix in comment +| [CS0840](./CS0840.md) | Symbols removed and compared with string containing them +| [CS0841](./CS0841.md) | Identifier contains non-printable character +| [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 | [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 b7b03b62..ef38bbca 100644 --- a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_CodingConvention.md +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_CodingConvention.md @@ -40,5 +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 +| [CV0838](./CV0838.md) | System type is not in UPPERCASE +| [CV0839](./CV0839.md) | Global variable is not in UPPERCASE [To docs homepage](./readme.md) 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 693821f2..ea7d744d 100644 --- a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Redundancy.md +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Redundancy.md @@ -43,6 +43,7 @@ | [RD0798](./RD0798.md) | Redundant variable initialization with NULL | [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 | [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 58f5c9e7..7adc481d 100644 --- a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Simplification.md +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Category_Simplification.md @@ -8,5 +8,9 @@ | [SI0735](./SI0735.md) | Variable assignment can be simplified - set value in DECLARE | [SI0753](./SI0753.md) | Multiple DROP statements can be collapsed into one | [SI0754](./SI0754.md) | Multiple ALTER statements can be collapsed into one +| [SI0845](./SI0845.md) | Multiple equality checks can be combined into single IN predicate +| [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 [To docs homepage](./readme.md) diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Group_Indices.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Group_Indices.md index 7dc491d0..02b18e24 100644 --- a/TeamTools.TSQL.Linter/Resources/Docs/en-us/Group_Indices.md +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/Group_Indices.md @@ -29,5 +29,6 @@ | [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 | [RD0724](./RD0724.md) | Redundant index option +| [RD0849](./RD0849.md) | The index constraint is already defined at the table level [To docs homepage](./readme.md) diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0956.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0956.md index 48f36ef4..fe64fa40 100644 --- a/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0956.md +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/PF0956.md @@ -11,7 +11,7 @@ ## Cause -This rule is triggered when a column value is truncated using `LEFT` or `SUBSTRING` (when extracting the first character) and compared to a scalar value. +This rule is triggered when a column value is truncated using `LEFT`, `SUBSTRING` or `CHARINDEX` (when extracting the first character) and compared to a scalar value.

Rewrite using LIKE.

## Examples @@ -34,6 +34,6 @@ WHERE env.var_name LIKE 'GF[_]%' ## Tips -💡 The rule responds to the use of string functions `LEFT` and `SUBSTRING` (when extracting from the first character) in the `WHERE` clause. These functions are non-sargable. However, a `LIKE` clause with a known string prefix, such as `LIKE 'a%'`, is sargable. If there is an index on the filtered field, it will perform a `SEEK` instead of a `SCAN`. +💡 The rule responds to the use of string functions `LEFT`, `SUBSTRING` and `CHARINDEX` (when extracting from the first character) in the `WHERE` clause. These functions are non-sargable. However, a `LIKE` clause with a known string prefix, such as `LIKE 'a%'`, is sargable. If there is an index on the filtered field, it will perform a `SEEK` instead of a `SCAN`. 💡 If the scalar value being compared includes characters that `LIKE` interprets as special characters (`[`, `]`, `%`, `_`), these characters must be escaped when converting to a `LIKE` clause. diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/RD0849.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/RD0849.md new file mode 100644 index 00000000..e109c9d3 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/RD0849.md @@ -0,0 +1,56 @@ + + +# The index constraint is already defined at the table level + +||| +|-|-| +| Id | **RD0849** +| Mnemo | REDUNDANT_INDEX_FILTER +| Severity | ℹ Hint +| Category | [Redundancy](./Category_Redundancy.md), [Indices](./Group_Indices.md) +| Source code | [RedundantIndexFilterRule.cs](../../../Rules/Redundancy/RedundantIndexFilterRule.cs) + +## Cause + +This rule is triggered when the index constraint is already defined at the table level. +

Remove index constraint.

+ +## Examples + +Bad + +```sql +CREATE TABLE dbo.foo +( + category_id INT NOT NULL + , product_id INT NOT NULL + , CONSTRAINT CK_category_id CHECK (100 < category_id) +); +GO + +CREATE INDEX IX_category_id ON dbo.foo(category_id) +WHERE category_id > 100; +GO + +CREATE INDEX IX_product_id ON dbo.foo(product_id) +WHERE product_id IS NOT NULL; +GO +``` + +Good + +```sql +CREATE TABLE dbo.foo +( + category_id INT NOT NULL + , product_id INT NOT NULL + , CONSTRAINT CK_category_id CHECK (100 < category_id) +); +GO + +CREATE INDEX IX_category_id ON dbo.foo(category_id); +GO + +CREATE INDEX IX_product_id ON dbo.foo(product_id); +GO +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/SI0845.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/SI0845.md new file mode 100644 index 00000000..357f7762 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/SI0845.md @@ -0,0 +1,35 @@ + +# Multiple equality checks can be combined into single IN predicate + +||| +|-|-| +| Id | **SI0845** +| Mnemo | MULTIPLE_OR_TO_IN +| Severity | ℹ Hint +| Category | [Simplification](./Category_Simplification.md) +| Source code | [MultipleOrToInRule.cs](../../../Rules/Simplification/MultipleOrToInRule.cs) + +## Cause + +This rule is triggered when multiple equality checks can be combined into single `IN` predicate. +

Combine the checks into single IN predicate.

+ +## Examples + +Bad + +```sql +SELECT * +FROM dbo.foo +WHERE category_id IN (1, 2, 3) + OR category_id = 4 + OR 5 = category_id; +``` + +Good + +```sql +SELECT * +FROM dbo.foo +WHERE category_id IN (1, 2, 3, 4, 5); +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/SI0846.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/SI0846.md new file mode 100644 index 00000000..1073726c --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/SI0846.md @@ -0,0 +1,34 @@ + +# Multiple inequality checks can be combined into single NOT IN predicate + +||| +|-|-| +| Id | **SI0846** +| Mnemo | MULTIPLE_AND_TO_NOT_IN +| Severity | ℹ Hint +| Category | [Simplification](./Category_Simplification.md) +| Source code | [MultipleAndToNotInRule.cs](../../../Rules/Simplification/MultipleAndToNotInRule.cs) + +## Cause + +This rule is triggered when multiple inequality checks can be combined into single `NOT IN` predicate. +

Combine the checks into single NOT IN predicate.

+ +## Examples + +Bad + +```sql +SELECT * +FROM dbo.foo +WHERE category_id <> 1 + AND category_id <> 2; +``` + +Good + +```sql +SELECT * +FROM dbo.foo +WHERE category_id NOT IN (1, 2); +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/SI0847.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/SI0847.md new file mode 100644 index 00000000..838bebb5 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/SI0847.md @@ -0,0 +1,34 @@ + +# Multiple similar IN predicates can be combined into single one + +||| +|-|-| +| Id | **SI0847** +| Mnemo | MULTIPLE_IN_TO_SINGLE +| Severity | ℹ Hint +| Category | [Simplification](./Category_Simplification.md) +| Source code | [MultipleInPredicateIntoOneRule.cs](../../../Rules/Simplification/MultipleInPredicateIntoOneRule.cs) + +## Cause + +This rule is triggered when multiple similar `IN` predicates can be combined into single one. +

Combine IN predicates.

+ +## Examples + +Bad + +```sql +SELECT * +FROM dbo.foo +WHERE category_id IN (1, 2, 3) + OR category_id IN (4, 5); +``` + +Good + +```sql +SELECT * +FROM dbo.foo +WHERE category_id IN (1, 2, 3, 4, 5); +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/en-us/SI0848.md b/TeamTools.TSQL.Linter/Resources/Docs/en-us/SI0848.md new file mode 100644 index 00000000..da19d0a9 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/en-us/SI0848.md @@ -0,0 +1,34 @@ + +# Multiple similar NOT IN predicates can be combined into single one + +||| +|-|-| +| Id | **SI0848** +| Mnemo | MULTIPLE_NOT_IN_TO_SINGLE +| Severity | ℹ Hint +| Category | [Simplification](./Category_Simplification.md) +| Source code | [MultipleNotInPredicateIntoOneRule.cs](../../../Rules/Simplification/MultipleNotInPredicateIntoOneRule.cs) + +## Cause + +This rule is triggered when multiple similar `NOT IN` predicates can be combined into single one. +

Combine NOT IN predicates.

+ +## Examples + +Bad + +```sql +SELECT * +FROM dbo.foo +WHERE category_id NOT IN (1, 2, 3) + AND category_id NOT IN (4, 5); +``` + +Good + +```sql +SELECT * +FROM dbo.foo +WHERE category_id NOT IN (1, 2, 3, 4, 5); +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0840.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0840.md new file mode 100644 index 00000000..a748281b --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0840.md @@ -0,0 +1,43 @@ + +# Строка после удаления символов сравнивается со строкой с такими символами + +||| +|-|-| +| Id | **CS0840** +| Мнемо | CHAR_REMOVED_AND_SEARCHED +| Серьёзность | ⚠ Предупреждение +| Категория | [СомнительноеКодирование](./Категория_СомнительноеКодирование.md) +| Исходный код | [CharRemovedAndThenSearchedRule.cs](../../../Rules/CodeSmell/CharRemovedAndThenSearchedRule.cs) + +## Причина + +Правило срабатывает, если результат функции `REPLACE`, `TRIM`, `LTRIM` или `RTRIM`, удаляющей определённые символы, сравнивается со строкой, которая содержит эти же символы, начинается и/или заканчивается ими (в зависимости от примененной функции). +

Убедитесь, что строка, с которой идёт сравнение, не содержит символы, удаляемые функцией.

+ +## Примеры + +Некорректно + +```sql +IF REPLACE(@s, 'X', 'Y') <> 'XXX asdf' +OR REPLACE(@s, 'X', 'Y') = 'as X df' +BEGIN + PRINT '?' +END +``` + +Корректно + +```sql +IF REPLACE(@s, 'X', 'Y') <> 'ZZZ asdf' +OR REPLACE(@s, 'X', 'Y') = 'as Z df' +BEGIN + PRINT '?' +END +``` + +## Подсказки + +💡 Значение, с которым сравнивается результат выполнения функции `REPLACE`, `TRIM`, `LTRIM` или `RTRIM`, должно быть конкретным: либо литерал, либо переменная с предсказуемым значением. + +💡 Правило учитывает возможность `TRIM-функций` принимать явное указание символа или подстроки, которые нужно отсечь. diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0841.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0841.md new file mode 100644 index 00000000..07bf80ac --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0841.md @@ -0,0 +1,16 @@ + + +# В идентификаторе присутствуют непечатные символы + +||| +|-|-| +| Id | **CS0841** +| Мнемо | INVISIBLE_CHAR_IN_IDENTIFIER +| Серьёзность | ℹ Подсказка +| Категория | [СомнительноеКодирование](./Категория_СомнительноеКодирование.md) +| Исходный код | [IdentifierHasInvisibleCharRule.cs](../../../Rules/CodeSmell/IdentifierHasInvisibleCharRule.cs) + +## Причина + +Правило срабатывает, если в идентификаторе присутствуют непечатные символы. +

Удалите непечатные символы.

diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0842.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0842.md new file mode 100644 index 00000000..2dcb4a17 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0842.md @@ -0,0 +1,16 @@ + + +# В комментарии присутствуют непечатные символы + +||| +|-|-| +| Id | **CS0842** +| Мнемо | INVISIBLE_CHAR_IN_COMMENT +| Серьёзность | ℹ Подсказка +| Категория | [СомнительноеКодирование](./Категория_СомнительноеКодирование.md) +| Исходный код | [CommentHasInvisibleCharRule.cs](../../../Rules/CodeSmell/CommentHasInvisibleCharRule.cs) + +## Причина + +Правило срабатывает, если в комментарии присутствуют непечатные символы. +

Удалите непечатные символы.

diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0843.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0843.md new file mode 100644 index 00000000..eb1a3cd3 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0843.md @@ -0,0 +1,44 @@ + + +# Выражение не наполняет используемую служебную таблицу + +||| +|-|-| +| Id | **CS0843** +| Мнемо | OUTPUT_MISMATCHES_ACTION +| Серьёзность | ⚠ Предупреждение +| Категория | [СомнительноеКодирование](./Категория_СомнительноеКодирование.md) +| Исходный код | [InsertedDeletedIrrelevanceRule.cs](../../../Rules/CodeSmell/InsertedDeletedIrrelevanceRule.cs) + +## Причина + +Правило срабатывает, если выражение не наполняет используемую в `OUTPUT [INTO]` служебную таблицу. +

Исправьте конструкцию OUTPUT [INTO].

+ +## Примеры + +Некорректно + +```sql +MERGE foo AS trg +USING bar AS src +ON trg.id = src.id +WHEN NOT MATCHED BY TARGET THEN + INSERT (name, position) + VALUES (src.name, src.position) +OUTPUT DELETED.id +INTO #tmp (id) +``` + +Корректно + +```sql +MERGE foo AS trg +USING bar AS src +ON trg.id = src.id +WHEN NOT MATCHED BY TARGET THEN + INSERT (name, position) + VALUES (src.name, src.position) +OUTPUT INSERTED.id +INTO #tmp (id) +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0844.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0844.md new file mode 100644 index 00000000..22c160cd --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CS0844.md @@ -0,0 +1,40 @@ + + +# Все ветки условного поведения приводят к одинаковому результату + +||| +|-|-| +| Id | **CS0844** +| Мнемо | CONDITIONS_SAME_DECISIONS +| Серьёзность | ℹ Подсказка +| Категория | [СомнительноеКодирование](./Категория_СомнительноеКодирование.md) +| Исходный код | [ConditionsLeadToSimilarBehaviorRule.cs](../../../Rules/CodeSmell/ConditionsLeadToSimilarBehaviorRule.cs) + +## Причина + +Правило срабатывает, если все ветки условного поведения приводят к одинаковому результату. +

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

+ +## Примеры + +Некорректно + +```sql +SET @a = CASE + WHEN @b = @c THEN + (CAST(GETDATE() AS DATE) + 1) + WHEN @c > 100 THEN + CAST(GETDATE() AS DATE) + 1 + END; +``` + +Корректно + +```sql +SET @a = CASE + WHEN @b = @c THEN + CAST(GETDATE() AS DATE) + 1 + WHEN @c > 100 THEN + CAST(GETDATE() AS DATE) + 5 + END; +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CV0838.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CV0838.md new file mode 100644 index 00000000..5283c865 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CV0838.md @@ -0,0 +1,41 @@ + +# Регистр при написании имён системных типов + +||| +|-|-| +| Id | **CV0838** +| Мнемо | SYSTEM_TYPE_UPPERCASE +| Серьёзность | ⚠ Предупреждение +| Категория | [СоглашенияПоКодированию](./Категория_СоглашенияПоКодированию.md) +| Исходный код | [SystemTypeUppercaseRule.cs](../../../Rules/CodingConvention/SystemTypeUppercaseRule.cs) + +## Причина + +Правило срабатывает, если имя системного типа данных написано в нижнем или смешанном регистре. +

Напишите имя системного типа данных в верхнем регистре.

+ +## Примеры + +Некорректно + +```sql +CREATE PROCEDURE dbo.foo + @arg Bit -- 1 +AS +BEGIN + DECLARE @var int -- 2 +END +GO +``` + +Корректно + +```sql +CREATE PROCEDURE dbo.foo + @arg BIT +AS +BEGIN + DECLARE @var INT +END +GO +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CV0839.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CV0839.md new file mode 100644 index 00000000..046e2b3e --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/CV0839.md @@ -0,0 +1,35 @@ + +# Регистр в упоминаниях глобальных переменных + +||| +|-|-| +| Id | **CV0839** +| Мнемо | GLOBAL_VAR_UPPERCASE +| Серьёзность | ⚠ Предупреждение +| Категория | [СоглашенияПоКодированию](./Категория_СоглашенияПоКодированию.md) +| Исходный код | [GlobalVarUppercaseRule.cs](../../../Rules/CodingConvention/GlobalVarUppercaseRule.cs) + +## Причина + +Правило срабатывает, если имя глобальной переменной написано в нижнем или смешанном регистре. +

Напишите имя глобальной переменной в верхнем регистре.

+ +## Примеры + +Некорректно + +```sql +IF @@rowcount > 0 + PRINT @@spid + +SELECT @@datefirst +``` + +Корректно + +```sql +IF @@ROWCOUNT > 0 + PRINT @@SPID + +SELECT @@DATEFIRST +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0956.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0956.md index 32ba5330..8c1299fe 100644 --- a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0956.md +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/PF0956.md @@ -34,6 +34,6 @@ WHERE env.var_name LIKE 'GF[_]%' ## Подсказки -💡 Срабатывает на применение строковых функций `LEFT` и `SUBSTRING` (если извлечение идет с первого символа) в `WHERE`. Эти функции не саргабельны. А вот `LIKE` с известным началом строки, например `LIKE 'a%'` - саргабелен. И если есть индекс по фильтруемому столбцу, то будет `SEEK` вместо `SCAN`. +💡 Срабатывает на применение строковых функций `LEFT`, `SUBSTRING` и `CHARINDEX` (если извлечение идет с первого символа) в `WHERE`. Эти функции не саргабельны. А вот `LIKE` с известным началом строки, например `LIKE 'a%'` - саргабелен. И если есть индекс по фильтруемому столбцу, то будет `SEEK` вместо `SCAN`. 💡 Если в скалярном значении, с которым выполняется сравнение, есть символы, которые `LIKE` воспринимает как спецсимволы (`[`, `]`, `%`, `_`), то при переписывании с использованием `LIKE` их нужно экранировать. diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/RD0849.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/RD0849.md new file mode 100644 index 00000000..88c834af --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/RD0849.md @@ -0,0 +1,56 @@ + + +# Ограничение индекса уже задано на уровне таблицы + +||| +|-|-| +| Id | **RD0849** +| Мнемо | REDUNDANT_INDEX_FILTER +| Серьёзность | ℹ Подсказка +| Категория | [Избыточность](./Категория_Избыточность.md), [Индексы](./Группа_Индексы.md) +| Исходный код | [RedundantIndexFilterRule.cs](../../../Rules/Redundancy/RedundantIndexFilterRule.cs) + +## Причина + +Правило срабатывает, если ограничение индекса уже задано на уровне таблицы. +

Удалите ограничение индекса.

+ +## Примеры + +Некорректно + +```sql +CREATE TABLE dbo.foo +( + category_id INT NOT NULL + , product_id INT NOT NULL + , CONSTRAINT CK_category_id CHECK (100 < category_id) +); +GO + +CREATE INDEX IX_category_id ON dbo.foo(category_id) +WHERE category_id > 100; +GO + +CREATE INDEX IX_product_id ON dbo.foo(product_id) +WHERE product_id IS NOT NULL; +GO +``` + +Корректно + +```sql +CREATE TABLE dbo.foo +( + category_id INT NOT NULL + , product_id INT NOT NULL + , CONSTRAINT CK_category_id CHECK (100 < category_id) +); +GO + +CREATE INDEX IX_category_id ON dbo.foo(category_id); +GO + +CREATE INDEX IX_product_id ON dbo.foo(product_id); +GO +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/SI0845.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/SI0845.md new file mode 100644 index 00000000..c80af63d --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/SI0845.md @@ -0,0 +1,35 @@ + +# Проверки на равенство могут быть объединены в предикат IN + +||| +|-|-| +| Id | **SI0845** +| Мнемо | MULTIPLE_OR_TO_IN +| Серьёзность | ℹ Подсказка +| Категория | [Упрощение](./Категория_Упрощение.md) +| Исходный код | [MultipleOrToInRule.cs](../../../Rules/Simplification/MultipleOrToInRule.cs) + +## Причина + +Правило срабатывает, если проверки на равенство могут быть объединены в предикат `IN`. +

Объедините проверки в один IN предикат.

+ +## Примеры + +Некорректно + +```sql +SELECT * +FROM dbo.foo +WHERE category_id IN (1, 2, 3) + OR category_id = 4 + OR 5 = category_id; +``` + +Корректно + +```sql +SELECT * +FROM dbo.foo +WHERE category_id IN (1, 2, 3, 4, 5); +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/SI0846.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/SI0846.md new file mode 100644 index 00000000..d9f83fbf --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/SI0846.md @@ -0,0 +1,34 @@ + +# Проверки на неравенство могут быть объединены в предикат NOT IN + +||| +|-|-| +| Id | **SI0846** +| Мнемо | MULTIPLE_AND_TO_NOT_IN +| Серьёзность | ℹ Подсказка +| Категория | [Упрощение](./Категория_Упрощение.md) +| Исходный код | [MultipleAndToNotInRule.cs](../../../Rules/Simplification/MultipleAndToNotInRule.cs) + +## Причина + +Правило срабатывает, если проверки на неравенство могут быть объединены в предикат `NOT IN`. +

Объедините проверки в один NOT IN предикат.

+ +## Примеры + +Некорректно + +```sql +SELECT * +FROM dbo.foo +WHERE category_id <> 1 + AND category_id <> 2; +``` + +Корректно + +```sql +SELECT * +FROM dbo.foo +WHERE category_id NOT IN (1, 2); +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/SI0847.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/SI0847.md new file mode 100644 index 00000000..c41c7b11 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/SI0847.md @@ -0,0 +1,34 @@ + +# Схожие предикаты IN могут быть объединены + +||| +|-|-| +| Id | **SI0847** +| Мнемо | MULTIPLE_IN_TO_SINGLE +| Серьёзность | ℹ Подсказка +| Категория | [Упрощение](./Категория_Упрощение.md) +| Исходный код | [MultipleInPredicateIntoOneRule.cs](../../../Rules/Simplification/MultipleInPredicateIntoOneRule.cs) + +## Причина + +Правило срабатывает, если схожие предикаты `IN` могут быть объединены. +

Объедините предикаты IN.

+ +## Примеры + +Некорректно + +```sql +SELECT * +FROM dbo.foo +WHERE category_id IN (1, 2, 3) + OR category_id IN (4, 5); +``` + +Корректно + +```sql +SELECT * +FROM dbo.foo +WHERE category_id IN (1, 2, 3, 4, 5); +``` diff --git a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/SI0848.md b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/SI0848.md new file mode 100644 index 00000000..ccc9c0f0 --- /dev/null +++ b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/SI0848.md @@ -0,0 +1,34 @@ + +# Схожие предикаты NOT IN могут быть объединены + +||| +|-|-| +| Id | **SI0848** +| Мнемо | MULTIPLE_NOT_IN_TO_SINGLE +| Серьёзность | ℹ Подсказка +| Категория | [Упрощение](./Категория_Упрощение.md) +| Исходный код | [MultipleNotInPredicateIntoOneRule.cs](../../../Rules/Simplification/MultipleNotInPredicateIntoOneRule.cs) + +## Причина + +Правило срабатывает, если схожие предикаты `NOT IN` могут быть объединены. +

Объедините предикаты NOT IN.

+ +## Примеры + +Некорректно + +```sql +SELECT * +FROM dbo.foo +WHERE category_id NOT IN (1, 2, 3) + AND category_id NOT IN (4, 5); +``` + +Корректно + +```sql +SELECT * +FROM dbo.foo +WHERE category_id NOT IN (1, 2, 3, 4, 5); +``` diff --git "a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\223\321\200\321\203\320\277\320\277\320\260_\320\230\320\275\320\264\320\265\320\272\321\201\321\213.md" "b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\223\321\200\321\203\320\277\320\277\320\260_\320\230\320\275\320\264\320\265\320\272\321\201\321\213.md" index 944218b7..81e4cb7d 100644 --- "a/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\223\321\200\321\203\320\277\320\277\320\260_\320\230\320\275\320\264\320\265\320\272\321\201\321\213.md" +++ "b/TeamTools.TSQL.Linter/Resources/Docs/ru-ru/\320\223\321\200\321\203\320\277\320\277\320\260_\320\230\320\275\320\264\320\265\320\272\321\201\321\213.md" @@ -28,5 +28,6 @@ | [PF0910](./PF0910.md) | Индексация столбца, допускающего NULL или имеющего значение по умолчанию | [PF0928](./PF0928.md) | Индекс имеет фильтр по NULL для столбца, который не включён в состав индекса | [RD0724](./RD0724.md) | Избыточное определение опций при создании индекса +| [RD0849](./RD0849.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\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 12e0a3f3..65d1e150 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" @@ -42,6 +42,7 @@ | [RD0798](./RD0798.md) | Избыточная инициализация переменной значением NULL | [RD0811](./RD0811.md) | Избыточное указание EXECUTE AS CALLER | [RD0814](./RD0814.md) | Переменная указана более одного раза в предикате IN +| [RD0849](./RD0849.md) | Ограничение индекса уже задано на уровне таблицы | [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\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 fe9e7c10..26500bb9 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,5 +40,7 @@ | [CV0803](./CV0803.md) | Системные типы нужно использовать без схемы | [CV0805](./CV0805.md) | PRINT в бизнес-логике | [CV0810](./CV0810.md) | Вызов хранимой процедуры нужно предварять словом EXEC +| [CV0838](./CV0838.md) | Регистр при написании имён системных типов +| [CV0839](./CV0839.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\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 edf99b66..4724d3fa 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" @@ -87,8 +87,13 @@ | [CS0821](./CS0821.md) | DECIMAL без дробной части | [CS0830](./CS0830.md) | Одинаковые имена временных таблиц | [CS0832](./CS0832.md) | Табличная переменная внутри функции -| [CS0832](./CS0834.md) | Символ другого алфавита в слове строкового литерала -| [CS0832](./CS0835.md) | Символ другого алфавита в слове текста комментария +| [CS0834](./CS0834.md) | Символ другого алфавита в слове строкового литерала +| [CS0835](./CS0835.md) | Символ другого алфавита в слове текста комментария +| [CS0840](./CS0840.md) | Строка после удаления символов сравнивается со строкой с такими символами +| [CS0841](./CS0841.md) | В идентификаторе присутствуют непечатные символы +| [CS0842](./CS0842.md) | В комментарии присутствуют непечатные символы +| [CS0843](./CS0843.md) | Выражение не наполняет используемую служебную таблицу +| [CS0844](./CS0844.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 3afaf758..70afa765 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" @@ -7,5 +7,9 @@ | [SI0735](./SI0735.md) | Значение переменной можно указать в DECLARE | [SI0753](./SI0753.md) | Несколько выражений DROP можно объединить в одно | [SI0754](./SI0754.md) | Несколько выражений ALTER можно объединить в одно +| [SI0845](./SI0845.md) | Проверки на равенство могут быть объединены в предикат IN +| [SI0846](./SI0846.md) | Проверки на неравенство могут быть объединены в предикат NOT IN +| [SI0847](./SI0847.md) | Схожие предикаты IN могут быть объединены +| [SI0848](./SI0848.md) | Схожие предикаты NOT IN могут быть объединены [На основную страницу документации](./readme.md) diff --git a/TeamTools.TSQL.Linter/Resources/ViolationMessages.json b/TeamTools.TSQL.Linter/Resources/ViolationMessages.json index cfe5038f..f6f486ab 100644 --- a/TeamTools.TSQL.Linter/Resources/ViolationMessages.json +++ b/TeamTools.TSQL.Linter/Resources/ViolationMessages.json @@ -145,6 +145,8 @@ "RD0798:REDUNDANT_INIT_NULL": "Redundant variable initialization with NULL", "RD0811:EXECUTE_AS_CALLER": "Redundant EXECUTE AS CALLER directive", "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", "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", @@ -205,6 +207,7 @@ "NM0271:MAGIC_@@_##_NAME": "Forbidden name @@, ## or similar", "NM0712:NON_TEMP_OBJECT_LIKE_TEMP": "Object of this type cannot be temporary", "NM0714:ALIAS_IS_KEYWORD": "Keyword is used for alias", + "NM0854:IDENTIFIER_LOOK_ALIKE_CHAR": "Look-alike char mix in indentifier", "NM0961:INDEX_NAME_PATTERN": "Index name violates naming pattern", "NM0962:TRIGGER_NAME_PATTERN": "Trigger name violates naming pattern", "NM0963:TABLE_NAME_LOWER_SNAKE_CASE": "Table naming convention violation - lower_snake_case expected", @@ -300,6 +303,10 @@ "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", "SI0754:ALTER_STATEMENTS_INTO_ONE": "Multiple ALTER statements can be collapsed into one", + "SI0845:MULTIPLE_OR_TO_IN": "Multiple equality checks can be combined into single IN predicate", + "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", "DD0153:FK_MULTIPLE_COL": "Composite foreign key", "DD0158:TABLE_ALL_COL_NULL": "All table columns can contain a NULL value", @@ -450,6 +457,13 @@ "CS0834:LITERAL_LOOK_ALIKE_CHAR": "Look-alike char mix in string literal", "CS0835:COMMENT_LOOK_ALIKE_CHAR": "Look-alike char mix in comment", "CS0840:CHAR_REMOVED_AND_SEARCHED": "Symbols removed and compared with string containing them", + "CS0841:INVISIBLE_CHAR_IN_IDENTIFIER": "Identifier contains non-printable character", + "CS0842:INVISIBLE_CHAR_IN_COMMENT": "Comment contains non-printable character", + "CS0843:OUTPUT_MISMATCHES_ACTION": "Output table is not effected by this statement", + "CS0844:CONDITIONS_SAME_DECISIONS": "All flow branches lead to the same behavior", + "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", "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 d73e2e15..1de49274 100644 --- a/TeamTools.TSQL.Linter/Resources/ViolationMessages.ru-ru.json +++ b/TeamTools.TSQL.Linter/Resources/ViolationMessages.ru-ru.json @@ -145,6 +145,8 @@ "RD0798:REDUNDANT_INIT_NULL": "Избыточная инициализация переменной значением NULL", "RD0811:EXECUTE_AS_CALLER": "Избыточное указание EXECUTE AS CALLER", "RD0814:IN_DUP_VAR": "Переменная указана более одного раза в предикате IN", + "RD0849:REDUNDANT_INDEX_FILTER": "Ограничение индекса уже задано на уровне таблицы", + "RD0850:EXTRA_WHERE_PREDICATE": "Фильтр WHERE уже применён на уровне INNER JOIN", "RD0925:REDUNDANT_LIKE": "Использование LIKE без спецсимволов", "RD0926:REDUNDANT_NOT_FOR_REPLICATION": "Избыточная опция NOT FOR REPLICATION", "RD0927:REDUNDANT_COL_NULLABILITY_CHECK": "Использование CHECK CONSTRAINT вместо атрибута столбца", @@ -205,6 +207,7 @@ "NM0271:MAGIC_@@_##_NAME": "Запрещены наименования @@, ## или подобные", "NM0712:NON_TEMP_OBJECT_LIKE_TEMP": "Объект данного типа не может быть временным", "NM0714:ALIAS_IS_KEYWORD": "Ключевое слово использовано для алиаса", + "NM0854:IDENTIFIER_LOOK_ALIKE_CHAR": "Символ другого алфавита в идентификаторе", "NM0961:INDEX_NAME_PATTERN": "Наименование индекса нарушает установленный шаблон", "NM0962:TRIGGER_NAME_PATTERN": "Наименование триггера нарушает установленный шаблон", "NM0963:TABLE_NAME_LOWER_SNAKE_CASE": "Нарушение соглашения об именовании таблиц - ожидается lower_snake_case", @@ -300,6 +303,10 @@ "SI0735:SET_TO_DECLARE": "Значение переменной можно указать в DECLARE", "SI0753:DROP_STATEMENTS_INTO_ONE": "Несколько выражений DROP можно объединить в одно", "SI0754:ALTER_STATEMENTS_INTO_ONE": "Несколько выражений ALTER можно объединить в одно", + "SI0845:MULTIPLE_OR_TO_IN": "Проверки на равенство могут быть объединены в предикат IN", + "SI0846:MULTIPLE_AND_TO_NOT_IN": "Проверки на неравенство могут быть объединены в предикат NOT IN", + "SI0847:MULTIPLE_IN_TO_SINGLE": "Схожие предикаты IN могут быть объединены", + "SI0848:MULTIPLE_NOT_IN_TO_SINGLE": "Схожие предикаты NOT IN могут быть объединены", "DD0153:FK_MULTIPLE_COL": "Составной внешний ключ", "DD0158:TABLE_ALL_COL_NULL": "Все столбцы таблицы допускают NULL", @@ -450,6 +457,13 @@ "CS0834:LITERAL_LOOK_ALIKE_CHAR": "Символ другого алфавита в слове строкового литерала", "CS0835:COMMENT_LOOK_ALIKE_CHAR": "Символ другого алфавита в слове текста комментария", "CS0840:CHAR_REMOVED_AND_SEARCHED": "Строка после удаления символов сравнивается со строкой с такими символами", + "CS0841:INVISIBLE_CHAR_IN_IDENTIFIER": "В идентификаторе присутствуют непечатные символы", + "CS0842:INVISIBLE_CHAR_IN_COMMENT": "В комментарии присутствуют непечатные символы", + "CS0843:OUTPUT_MISMATCHES_ACTION": "Выражение не наполняет используемую служебную таблицу", + "CS0844:CONDITIONS_SAME_DECISIONS": "Все ветки условного поведения приводят к одинаковому результату", + "CS0851:FAKE_OUTER_JOIN": "Соединение заявлено как внешнее (OUTER), но работает как внутреннее (INNER)", + "CS0852:NON_CORRELATED_JOIN_PREDICATE": "Предикат соединения не связан с присоединяемыми источниками", + "CS0853:COMPARISON_LEFT_EQUALS_RIGHT": "Левая часть выражения сравнения совпадает с правой", "CS0905:VAR_LACKS_PRECISION": "Аргумент даты или времени не имеет запрошенной детализации", "CS0914:INTERSECT_EXCEPT_BROKEN_BY_LITERAL": "Различающиеся литералы в конструкциях INTERSECT/EXCEPT", "CS0917:FORBIDDEN_INSERT_HINTS": " Недопустимый hint в INSERT", diff --git a/TeamTools.TSQL.Linter/readme.md b/TeamTools.TSQL.Linter/readme.md new file mode 100644 index 00000000..a24cffbd --- /dev/null +++ b/TeamTools.TSQL.Linter/readme.md @@ -0,0 +1,4 @@ + +# T-SQL linter + +[Rules Documentation](./Resources/Docs) From 4e57af125227999663200168466bcbdee0b9d48b Mon Sep 17 00:00:00 2001 From: Ivan Starostin Date: Fri, 13 Feb 2026 18:17:39 +0300 Subject: [PATCH 3/3] add pack nupkg step to workflow --- .github/workflows/ci.yml | 52 ++++++++++++++++++- .../TeamTools.Common.Linting.csproj | 10 ++-- .../TeamTools.TSQL.ExpressionEvaluator.csproj | 10 ++-- .../TeamTools.TSQL.Linter.csproj | 16 +++--- 4 files changed, 66 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 687576e9..521c3eee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,8 @@ env: TEST_PROJECT: TeamTools.TSQL.LinterTests PUBLISH_PROJECT_PATH: TeamTools.TSQL.Linter/TeamTools.TSQL.Linter.csproj OUTPUT_PATH: ${{ github.workspace }}/.bin/ - PUBLISH_PATH: ${{ github.workspace }}/.pub/ + PUBLISH_PATH: ${{ github.workspace }}/.pub + NUPKG_PATH: ${{ github.workspace }}\.nupkgs jobs: semver: @@ -89,6 +90,7 @@ jobs: run: dotnet --info - name: Cache NuGet packages + id: cache-nugets uses: actions/cache@v4 with: path: ${{ github.workspace }}/packages @@ -143,12 +145,43 @@ jobs: -p:BaseOutputPath="${{ env.OUTPUT_PATH }}" -p:PublishDir="${{ env.PUBLISH_PATH }}/Release/net8.0" + - name: Pack + if: ${{ matrix.publish }} + run: > + dotnet pack "${{ github.workspace}}\TeamTools.TSQL.Common\TeamTools.Common.Linting.csproj" + --no-build + --output "${{ env.NUPKG_PATH }}" + -p:Configuration=Release + -p:BaseOutputPath="${{ env.OUTPUT_PATH }}" + -p:VersionPrefix=${{ needs.semver.outputs.next-version }} + && + dotnet pack "${{ github.workspace}}\TeamTools.TSQL.ExpressionEvaluator\TeamTools.TSQL.ExpressionEvaluator.csproj" + --no-build + --output "${{ env.NUPKG_PATH }}" + -p:Configuration=Release + -p:BaseOutputPath="${{ env.OUTPUT_PATH }}" + -p:VersionPrefix=${{ needs.semver.outputs.next-version }} + && + dotnet pack "${{ env.PUBLISH_PROJECT_PATH }}" + --no-build + --output "${{ env.NUPKG_PATH }}" + -p:Configuration=Release + -p:BaseOutputPath="${{ env.OUTPUT_PATH }}" + -p:VersionPrefix=${{ needs.semver.outputs.next-version }} + -p:Authors="Ivan Starostin et al" + -p:Copyright="2019- (c) Ivan Starostin" + -p:RepositoryCommit=${{ github.sha }} + -p:RepositoryUrl="${{ github.repositoryUrl }}" + -p:RepositoryType=git + -p:RepositoryBranch="${{ github.ref_name }}" + - name: Upload build artifacts 2.0 uses: actions/upload-artifact@v4 if: ${{ matrix.publish }} with: name: ${{ env.PRODUCT_NAME }}-${{ needs.semver.outputs.next-version }}-${{ matrix.configuration }}-netstandard2.0 path: ${{ env.PUBLISH_PATH }}/${{ matrix.configuration }}/netstandard2.0 + if-no-files-found: error - name: Upload build artifacts 6.0 uses: actions/upload-artifact@v4 @@ -156,6 +189,7 @@ jobs: with: name: ${{ env.PRODUCT_NAME }}-${{ needs.semver.outputs.next-version }}-${{ matrix.configuration }}-net6.0 path: ${{ env.PUBLISH_PATH }}/${{ matrix.configuration }}/net6.0 + if-no-files-found: error - name: Upload build artifacts 8.0 uses: actions/upload-artifact@v4 @@ -163,13 +197,25 @@ jobs: with: name: ${{ env.PRODUCT_NAME }}-${{ needs.semver.outputs.next-version }}-${{ matrix.configuration }}-net8.0 path: ${{ env.PUBLISH_PATH }}/${{ matrix.configuration }}/net8.0 + if-no-files-found: error + + - name: Upload nugets + uses: actions/upload-artifact@v4 + if: ${{ matrix.publish }} + with: + name: nuget-packages + include-hidden-files: true + if-no-files-found: error + path: | + ${{ env.NUPKG_PATH }}\*.nupkg - name: Upload test bundle uses: actions/upload-artifact@v4 if: ${{ matrix.configuration == 'Debug' && matrix.os == 'windows-latest' && steps.build.conclusion == 'success' && !cancelled() }} with: name: test-bundle - path: ${{ env.OUTPUT_PATH }}/${{ matrix.configuration }}/net8.0 + path: ${{ env.OUTPUT_PATH }}${{ matrix.configuration }}/net8.0 + if-no-files-found: error - name: Test if: ${{ !matrix.coverage }} @@ -194,6 +240,7 @@ jobs: with: name: test-results path: TestResults + if-no-files-found: error - name: Test Report uses: dorny/test-reporter@v2 @@ -202,6 +249,7 @@ jobs: name: NUnit testing ${{ matrix.configuration }} build on ${{ matrix.os }} path: "**/TestResults/*.trx,*.trx" reporter: dotnet-trx + if-no-files-found: error validate-markdown: runs-on: ubuntu-latest diff --git a/TeamTools.TSQL.Common/TeamTools.Common.Linting.csproj b/TeamTools.TSQL.Common/TeamTools.Common.Linting.csproj index 1a4d99d1..1c5a5ff2 100644 --- a/TeamTools.TSQL.Common/TeamTools.Common.Linting.csproj +++ b/TeamTools.TSQL.Common/TeamTools.Common.Linting.csproj @@ -9,10 +9,9 @@ true - ../.nupkg - MIT - ./readme.md - StaticCodeAnalysis;CodeQuality;TeamTools + ../.nupkgs + readme.md + LICENSE @@ -22,7 +21,8 @@ - + + diff --git a/TeamTools.TSQL.ExpressionEvaluator/TeamTools.TSQL.ExpressionEvaluator.csproj b/TeamTools.TSQL.ExpressionEvaluator/TeamTools.TSQL.ExpressionEvaluator.csproj index 42704b3e..9df742d4 100644 --- a/TeamTools.TSQL.ExpressionEvaluator/TeamTools.TSQL.ExpressionEvaluator.csproj +++ b/TeamTools.TSQL.ExpressionEvaluator/TeamTools.TSQL.ExpressionEvaluator.csproj @@ -14,10 +14,9 @@ true - ../.nupkg - MIT - ./readme.md - T-SQL;tsql;TeamTools + ../.nupkgs + readme.md + LICENSE T-SQL scalar expression evaluator @@ -38,7 +37,8 @@ - + + diff --git a/TeamTools.TSQL.Linter/TeamTools.TSQL.Linter.csproj b/TeamTools.TSQL.Linter/TeamTools.TSQL.Linter.csproj index 0f2f2833..bbe7add3 100644 --- a/TeamTools.TSQL.Linter/TeamTools.TSQL.Linter.csproj +++ b/TeamTools.TSQL.Linter/TeamTools.TSQL.Linter.csproj @@ -6,18 +6,13 @@ Debug;Release - - - $(MSBuildProjectDirectory)\.runsettings - - true - ../.nupkg - MIT - ./readme.md - T-SQL;tsql;sqlproj;StaticCodeAnalysis;CodeQuality;TeamTools + ../.nupkgs + readme.md + LICENSE + T-SQL;tsql;sqlproj;StaticCodeAnalysis;CodeQuality;TeamTools;static-code-analysis;sql-linter Pluggable linter library for T-SQL files organized in SSDT-project manner @@ -34,7 +29,7 @@ - + @@ -45,6 +40,7 @@ true true +