├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── dependabot.yml ├── stale.yml └── workflows │ ├── merge-dependabot.yml │ └── on-push-do-docs.yml ├── .gitignore ├── code_of_conduct.md ├── license.txt ├── readme.md └── src ├── .editorconfig ├── Directory.Build.props ├── Directory.Packages.props ├── Shared.sln.DotSettings ├── Tests ├── GlobalUsings.cs ├── ModuleInit.cs ├── Tests.CommandEmpty.verified.txt ├── Tests.CommandEscaped.verified.txt ├── Tests.CommandFull.verified.txt ├── Tests.Error.verified.txt ├── Tests.Exception.verified.txt ├── Tests.ParameterCollectionFull.verified.txt ├── Tests.ParameterEmpty.verified.txt ├── Tests.ParameterFull.verified.txt ├── Tests.RecordingError.verified.txt ├── Tests.RecordingReadingResults.verified.txt ├── Tests.RecordingSpecific.verified.txt ├── Tests.RecordingTest.verified.txt ├── Tests.RecordingUsage.verified.txt ├── Tests.RecordingWithParameter.verified.txt ├── Tests.Schema.verified.md ├── Tests.SchemaAsSql.verified.sql ├── Tests.SchemaFilter.verified.md ├── Tests.SchemaInDynamic.verified.txt ├── Tests.SchemaInclude.verified.md ├── Tests.SchemaIncludeAll.verified.md ├── Tests.cs └── Tests.csproj ├── Verify.SqlServer.sln ├── Verify.SqlServer.sln.DotSettings ├── Verify.SqlServer ├── Converters │ ├── CommandConverter.cs │ ├── ConnectionConverter.cs │ ├── ErrorConverter.cs │ ├── ExceptionConverter.cs │ ├── ParameterCollectionConverter.cs │ └── ParameterConverter.cs ├── Extensions.cs ├── GlobalUsings.cs ├── Recording │ ├── ErrorEntry.cs │ └── Listener.cs ├── RemoveSquareBracketVisitor.cs ├── SchemaValidation │ ├── DbObjects.cs │ ├── Format.cs │ ├── Obsoletes.cs │ ├── SchemaSettings.cs │ ├── SqlScriptBuilder.cs │ └── VerifySettingsExtensions.cs ├── SqlFormatter.cs ├── Verify.SqlServer.csproj └── VerifySqlServer.cs ├── appveyor.yml ├── global.json ├── icon.png ├── key.snk ├── mdsnippets.json └── nuget.config /.gitattributes: -------------------------------------------------------------------------------- 1 | * text 2 | *.png binary 3 | *.snk binary 4 | 5 | *.verified.* text eol=lf working-tree-encoding=UTF-8 6 | 7 | .editorconfig text eol=lf working-tree-encoding=UTF-8 8 | Shared.sln.DotSettings text eol=lf working-tree-encoding=UTF-8 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: VerifyTests 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug fix 3 | about: Create a bug fix to help us improve 4 | --- 5 | 6 | Note: New issues raised, where it is clear the submitter has not read the issue template, are likely to be closed with "please read the issue template". Please don't take offense at this. It is simply a time management decision. If someone raises an issue, and can't be bothered to spend the time to read the issue template, then the project maintainers should not be expected to spend the time to read the submitted issue. Often too much time is spent going back and forth in issue comments asking for information that is outlined in the issue template. 7 | 8 | 9 | #### Preamble 10 | 11 | General questions may be better placed [StackOveflow](https://stackoverflow.com/). 12 | 13 | Where relevant, ensure you are using the current stable versions on your development stack. For example: 14 | 15 | * Visual Studio 16 | * [.NET SDK or .NET Core SDK](https://www.microsoft.com/net/download) 17 | * Any related NuGet packages 18 | 19 | Any code or stack traces must be properly formatted with [GitHub markdown](https://guides.github.com/features/mastering-markdown/). 20 | 21 | 22 | #### Describe the bug 23 | 24 | A clear and concise description of what the bug is. Include any relevant version information. 25 | 26 | A clear and concise description of what you expected to happen. 27 | 28 | Add any other context about the problem here. 29 | 30 | 31 | #### Minimal Repro 32 | 33 | Ensure you have replicated the bug in a minimal solution with the fewest moving parts. Often this will help point to the true cause of the problem. Upload this repro as part of the issue, preferably a public GitHub repository or a downloadable zip. The repro will allow the maintainers of this project to smoke test the any fix. 34 | 35 | #### Submit a PR that fixes the bug 36 | 37 | Submit a [Pull Request (PR)](https://help.github.com/articles/about-pull-requests/) that fixes the bug. Include in this PR a test that verifies the fix. If you were not able to fix the bug, a PR that illustrates your partial progress will suffice. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: How to raise feature requests 4 | --- 5 | 6 | 7 | Note: New issues raised, where it is clear the submitter has not read the issue template, are likely to be closed with "please read the issue template". Please don't take offense at this. It is simply a time management decision. If someone raises an issue, and can't be bothered to spend the time to read the issue template, then the project maintainers should not be expected to spend the time to read the submitted issue. Often too much time is spent going back and forth in issue comments asking for information that is outlined in the issue template. 8 | 9 | If you are certain the feature will be accepted, it is better to raise a [Pull Request (PR)](https://help.github.com/articles/about-pull-requests/). 10 | 11 | If you are uncertain if the feature will be accepted, outline the proposal below to confirm it is viable, prior to raising a PR that implements the feature. 12 | 13 | Note that even if the feature is a good idea and viable, it may not be accepted since the ongoing effort in maintaining the feature may outweigh the benefit it delivers. 14 | 15 | 16 | #### Is the feature request related to a problem 17 | 18 | A clear and concise description of what the problem is. 19 | 20 | 21 | #### Describe the solution 22 | 23 | A clear and concise proposal of how you intend to implement the feature. 24 | 25 | 26 | #### Describe alternatives considered 27 | 28 | A clear and concise description of any alternative solutions or features you've considered. 29 | 30 | 31 | #### Additional context 32 | 33 | Add any other context about the feature request here. 34 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: nuget 4 | directory: "/src" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 7 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Set to true to ignore issues in a milestone (defaults to false) 6 | exemptMilestones: true 7 | # Comment to post when marking an issue as stale. Set to `false` to disable 8 | markComment: > 9 | This issue has been automatically marked as stale because it has not had 10 | recent activity. It will be closed if no further activity occurs. Thank you 11 | for your contributions. 12 | # Comment to post when closing a stale issue. Set to `false` to disable 13 | closeComment: false 14 | # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': 15 | pulls: 16 | daysUntilStale: 30 17 | exemptLabels: 18 | - Question 19 | - Bug 20 | - Feature 21 | - Improvement -------------------------------------------------------------------------------- /.github/workflows/merge-dependabot.yml: -------------------------------------------------------------------------------- 1 | name: merge-dependabot 2 | on: 3 | pull_request: 4 | jobs: 5 | automerge: 6 | runs-on: ubuntu-latest 7 | if: github.actor == 'dependabot[bot]' 8 | steps: 9 | - name: Dependabot Auto Merge 10 | uses: ahmadnassri/action-dependabot-auto-merge@v2.6.6 11 | with: 12 | target: minor 13 | github-token: ${{ secrets.dependabot }} 14 | command: squash and merge -------------------------------------------------------------------------------- /.github/workflows/on-push-do-docs.yml: -------------------------------------------------------------------------------- 1 | name: on-push-do-docs 2 | on: 3 | push: 4 | jobs: 5 | docs: 6 | runs-on: windows-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - name: Run MarkdownSnippets 10 | run: | 11 | dotnet tool install --global MarkdownSnippets.Tool 12 | mdsnippets ${GITHUB_WORKSPACE} 13 | shell: bash 14 | - name: Push changes 15 | run: | 16 | git config --local user.email "action@github.com" 17 | git config --local user.name "GitHub Action" 18 | git commit -m "Docs changes" -a || echo "nothing to commit" 19 | remote="https://${GITHUB_ACTOR}:${{secrets.GITHUB_TOKEN}}@github.com/${GITHUB_REPOSITORY}.git" 20 | branch="${GITHUB_REF:11}" 21 | git push "${remote}" ${branch} || echo "nothing to push" 22 | shell: bash -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.suo 2 | *.user 3 | bin/ 4 | obj/ 5 | .vs/ 6 | *.DotSettings.user 7 | .idea/ 8 | *.received.* 9 | nugets/ -------------------------------------------------------------------------------- /code_of_conduct.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This project has adopted the code of conduct defined by the Contributor Covenant 4 | to clarify expected behavior in our community. 5 | For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/about/code-of-conduct). 6 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) .NET Foundation and Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Verify.SqlServer 2 | 3 | [![Discussions](https://img.shields.io/badge/Verify-Discussions-yellow?svg=true&label=)](https://github.com/orgs/VerifyTests/discussions) 4 | [![Build status](https://ci.appveyor.com/api/projects/status/enh6mjugcbmoun0e?svg=true)](https://ci.appveyor.com/project/SimonCropp/verify-sqlserver) 5 | [![NuGet Status](https://img.shields.io/nuget/v/Verify.SqlServer.svg)](https://www.nuget.org/packages/Verify.SqlServer/) 6 | 7 | Extends [Verify](https://github.com/VerifyTests/Verify) to allow verification of SqlServer bits. 8 | 9 | **See [Milestones](../../milestones?state=closed) for release notes.** 10 | 11 | 12 | ## NuGet package 13 | 14 | https://nuget.org/packages/Verify.SqlServer/ 15 | 16 | 17 | ## Usage 18 | 19 | 20 | 21 | ```cs 22 | [ModuleInitializer] 23 | public static void Init() => 24 | VerifySqlServer.Initialize(); 25 | ``` 26 | snippet source | anchor 27 | 28 | 29 | 30 | ### SqlServer Schema 31 | 32 | This test: 33 | 34 | 35 | 36 | ```cs 37 | await Verify(connection); 38 | ``` 39 | snippet source | anchor 40 | 41 | 42 | Will result in the following verified file: 43 | 44 |
 45 | 
 46 | 
 47 | 
48 | 49 | #### Object types to include 50 | 51 | 52 | 53 | ```cs 54 | await Verify(connection) 55 | // include only tables and views 56 | .SchemaIncludes(DbObjects.Tables | DbObjects.Views); 57 | ``` 58 | snippet source | anchor 59 | 60 | 61 | Available values: 62 | 63 | 64 | 65 | ```cs 66 | namespace VerifyTests.SqlServer; 67 | 68 | [Flags] 69 | public enum DbObjects 70 | { 71 | StoredProcedures = 1, 72 | Synonyms = 2, 73 | Tables = 4, 74 | UserDefinedFunctions = 8, 75 | Views = 16, 76 | All = StoredProcedures | Synonyms | Tables | UserDefinedFunctions | Views 77 | } 78 | ``` 79 | snippet source | anchor 80 | 81 | 82 | 83 | #### Filtering 84 | 85 | Objects can be dynamically filtered: 86 | 87 | 88 | 89 | ```cs 90 | await Verify(connection) 91 | // include tables & views, or named MyTrigger 92 | .SchemaFilter( 93 | _ => _ is TableViewBase || 94 | _.Name == "MyTrigger"); 95 | ``` 96 | snippet source | anchor 97 | 98 | 99 | 100 | 101 | ### Recording 102 | 103 | Recording allows all commands executed to be captured and then (optionally) verified. 104 | 105 | Call `SqlRecording.StartRecording()`: 106 | 107 | 108 | 109 | ```cs 110 | await using var connection = new SqlConnection(connectionString); 111 | await connection.OpenAsync(); 112 | Recording.Start(); 113 | await using var command = connection.CreateCommand(); 114 | command.CommandText = "select Value from MyTable"; 115 | var value = await command.ExecuteScalarAsync(); 116 | await Verify(value!); 117 | ``` 118 | snippet source | anchor 119 | 120 | 121 | Will result in the following verified file: 122 | 123 | 124 | 125 | ```txt 126 | { 127 | target: 42, 128 | sql: { 129 | Text: 130 | select Value 131 | from MyTable, 132 | HasTransaction: false 133 | } 134 | } 135 | ``` 136 | snippet source | anchor 137 | 138 | 139 | 140 | Sql entries can be explicitly read using `SqlRecording.FinishRecording`, optionally filtered, and passed to Verify: 141 | 142 | 143 | 144 | ```cs 145 | await using var connection = new SqlConnection(connectionString); 146 | await connection.OpenAsync(); 147 | Recording.Start(); 148 | await using var command = connection.CreateCommand(); 149 | command.CommandText = "select Value from MyTable"; 150 | var value = await command.ExecuteScalarAsync(); 151 | 152 | await using var errorCommand = connection.CreateCommand(); 153 | errorCommand.CommandText = "select Value from BadTable"; 154 | try 155 | { 156 | await errorCommand.ExecuteScalarAsync(); 157 | } 158 | catch 159 | { 160 | } 161 | 162 | var entries = Recording 163 | .Stop() 164 | .Select(_ => _.Data); 165 | //Optionally filter results 166 | await Verify( 167 | new 168 | { 169 | value, 170 | sqlEntries = entries 171 | }); 172 | ``` 173 | snippet source | anchor 174 | 175 | 176 | 177 | #### Interpreting recording results 178 | 179 | Recording results can be interpreted in a a variety of ways: 180 | 181 | 182 | 183 | ```cs 184 | var entries = Recording.Stop(); 185 | 186 | // all sql entries via key 187 | var sqlEntries = entries 188 | .Where(_ => _.Name == "sql") 189 | .Select(_ => _.Data); 190 | 191 | // successful Commands via Type 192 | var sqlCommandsViaType = entries 193 | .Select(_ => _.Data) 194 | .OfType(); 195 | 196 | // failed Commands via Type 197 | var sqlErrorsViaType = entries 198 | .Select(_ => _.Data) 199 | .OfType(); 200 | ``` 201 | snippet source | anchor 202 | 203 | 204 | 205 | ## Icon 206 | 207 | [Database](https://thenounproject.com/term/database/310841/) designed by [Creative Stall](https://thenounproject.com/creativestall/) from [The Noun Project](https://thenounproject.com). 208 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | 6 | [*.cs] 7 | indent_size = 4 8 | charset = utf-8 9 | 10 | # Redundant accessor body 11 | resharper_redundant_accessor_body_highlighting = error 12 | 13 | # Replace with field keyword 14 | resharper_replace_with_field_keyword_highlighting = error 15 | 16 | # Replace with single call to Single(..) 17 | resharper_replace_with_single_call_to_single_highlighting = error 18 | 19 | # Replace with single call to SingleOrDefault(..) 20 | resharper_replace_with_single_call_to_single_or_default_highlighting = error 21 | 22 | # Replace with single call to LastOrDefault(..) 23 | resharper_replace_with_single_call_to_last_or_default_highlighting = error 24 | 25 | # Replace with single call to Last(..) 26 | resharper_replace_with_single_call_to_last_highlighting = error 27 | 28 | # Replace with single call to First(..) 29 | resharper_replace_with_single_call_to_first_highlighting = error 30 | 31 | # Replace with single call to FirstOrDefault(..) 32 | resharper_replace_with_single_call_to_first_or_default_highlighting = error 33 | 34 | # Replace with single call to Any(..) 35 | resharper_replace_with_single_call_to_any_highlighting = error 36 | 37 | # Replace with single call to Count(..) 38 | resharper_replace_with_single_call_to_count_highlighting = error 39 | 40 | # Declare types in namespaces 41 | dotnet_diagnostic.CA1050.severity = none 42 | 43 | # Use Literals Where Appropriate 44 | dotnet_diagnostic.CA1802.severity = error 45 | 46 | # Template should be a static expression 47 | dotnet_diagnostic.CA2254.severity = error 48 | 49 | # Potentially misleading parameter name in lambda or local function 50 | resharper_all_underscore_local_parameter_name_highlighting = none 51 | 52 | # Redundant explicit collection creation in argument of 'params' parameter 53 | resharper_redundant_explicit_params_array_creation_highlighting = error 54 | 55 | # Do not initialize unnecessarily 56 | dotnet_diagnostic.CA1805.severity = error 57 | 58 | # Avoid unsealed attributes 59 | dotnet_diagnostic.CA1813.severity = error 60 | 61 | # Test for empty strings using string length 62 | dotnet_diagnostic.CA1820.severity = none 63 | 64 | # Remove empty finalizers 65 | dotnet_diagnostic.CA1821.severity = error 66 | 67 | # Mark members as static 68 | dotnet_diagnostic.CA1822.severity = error 69 | 70 | # Avoid unused private fields 71 | dotnet_diagnostic.CA1823.severity = error 72 | 73 | # Avoid zero-length array allocations 74 | dotnet_diagnostic.CA1825.severity = error 75 | 76 | # Use property instead of Linq Enumerable method 77 | dotnet_diagnostic.CA1826.severity = error 78 | 79 | # Do not use Count()/LongCount() when Any() can be used 80 | dotnet_diagnostic.CA1827.severity = error 81 | dotnet_diagnostic.CA1828.severity = error 82 | 83 | # Use Length/Count property instead of Enumerable.Count method 84 | dotnet_diagnostic.CA1829.severity = error 85 | 86 | # Prefer strongly-typed Append and Insert method overloads on StringBuilder 87 | dotnet_diagnostic.CA1830.severity = error 88 | 89 | # Use AsSpan instead of Range-based indexers for string when appropriate 90 | dotnet_diagnostic.CA1831.severity = error 91 | 92 | # Use AsSpan instead of Range-based indexers for string when appropriate 93 | dotnet_diagnostic.CA1831.severity = error 94 | dotnet_diagnostic.CA1832.severity = error 95 | dotnet_diagnostic.CA1833.severity = error 96 | 97 | # Use StringBuilder.Append(char) for single character strings 98 | dotnet_diagnostic.CA1834.severity = error 99 | 100 | # Prefer IsEmpty over Count when available 101 | dotnet_diagnostic.CA1836.severity = error 102 | 103 | # Prefer IsEmpty over Count when available 104 | dotnet_diagnostic.CA1836.severity = error 105 | 106 | # Use Environment.ProcessId instead of Process.GetCurrentProcess().Id 107 | dotnet_diagnostic.CA1837.severity = error 108 | 109 | # Use Environment.ProcessPath instead of Process.GetCurrentProcess().MainModule.FileName 110 | dotnet_diagnostic.CA1839.severity = error 111 | 112 | # Use Environment.CurrentManagedThreadId instead of Thread.CurrentThread.ManagedThreadId 113 | dotnet_diagnostic.CA1840.severity = error 114 | 115 | # Prefer Dictionary Contains methods 116 | dotnet_diagnostic.CA1841.severity = error 117 | 118 | # Do not use WhenAll with a single task 119 | dotnet_diagnostic.CA1842.severity = error 120 | 121 | # Do not use WhenAll/WaitAll with a single task 122 | dotnet_diagnostic.CA1842.severity = error 123 | dotnet_diagnostic.CA1843.severity = error 124 | 125 | # Use span-based 'string.Concat' 126 | dotnet_diagnostic.CA1845.severity = error 127 | 128 | # Prefer AsSpan over Substring 129 | dotnet_diagnostic.CA1846.severity = error 130 | 131 | # Use string.Contains(char) instead of string.Contains(string) with single characters 132 | dotnet_diagnostic.CA1847.severity = error 133 | 134 | # Prefer static HashData method over ComputeHash 135 | dotnet_diagnostic.CA1850.severity = error 136 | 137 | # Possible multiple enumerations of IEnumerable collection 138 | dotnet_diagnostic.CA1851.severity = error 139 | 140 | # Unnecessary call to Dictionary.ContainsKey(key) 141 | dotnet_diagnostic.CA1853.severity = error 142 | 143 | # Prefer the IDictionary.TryGetValue(TKey, out TValue) method 144 | dotnet_diagnostic.CA1854.severity = error 145 | 146 | # Use Span.Clear() instead of Span.Fill() 147 | dotnet_diagnostic.CA1855.severity = error 148 | 149 | # Incorrect usage of ConstantExpected attribute 150 | dotnet_diagnostic.CA1856.severity = error 151 | 152 | # The parameter expects a constant for optimal performance 153 | dotnet_diagnostic.CA1857.severity = error 154 | 155 | # Use StartsWith instead of IndexOf 156 | dotnet_diagnostic.CA1858.severity = error 157 | 158 | # Avoid using Enumerable.Any() extension method 159 | dotnet_diagnostic.CA1860.severity = error 160 | 161 | # Avoid constant arrays as arguments 162 | dotnet_diagnostic.CA1861.severity = error 163 | 164 | # Use the StringComparison method overloads to perform case-insensitive string comparisons 165 | dotnet_diagnostic.CA1862.severity = error 166 | 167 | # Prefer the IDictionary.TryAdd(TKey, TValue) method 168 | dotnet_diagnostic.CA1864.severity = error 169 | 170 | # Use string.Method(char) instead of string.Method(string) for string with single char 171 | dotnet_diagnostic.CA1865.severity = error 172 | dotnet_diagnostic.CA1866.severity = error 173 | dotnet_diagnostic.CA1867.severity = error 174 | 175 | # Unnecessary call to 'Contains' for sets 176 | dotnet_diagnostic.CA1868.severity = error 177 | 178 | # Cache and reuse 'JsonSerializerOptions' instances 179 | dotnet_diagnostic.CA1869.severity = error 180 | 181 | # Use a cached 'SearchValues' instance 182 | dotnet_diagnostic.CA1870.severity = error 183 | 184 | # Microsoft .NET properties 185 | trim_trailing_whitespace = true 186 | csharp_preferred_modifier_order = public, private, protected, internal, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async:suggestion 187 | resharper_namespace_body = file_scoped 188 | dotnet_naming_rule.private_constants_rule.severity = warning 189 | dotnet_naming_rule.private_constants_rule.style = lower_camel_case_style 190 | dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols 191 | dotnet_naming_rule.private_instance_fields_rule.severity = warning 192 | dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style 193 | dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols 194 | dotnet_naming_rule.private_static_fields_rule.severity = warning 195 | dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style 196 | dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols 197 | dotnet_naming_rule.private_static_readonly_rule.severity = warning 198 | dotnet_naming_rule.private_static_readonly_rule.style = lower_camel_case_style 199 | dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols 200 | dotnet_naming_style.lower_camel_case_style.capitalization = camel_case 201 | dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case 202 | dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private 203 | dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field 204 | dotnet_naming_symbols.private_constants_symbols.required_modifiers = const 205 | dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private 206 | dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field 207 | dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private 208 | dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field 209 | dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static 210 | dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private 211 | dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field 212 | dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static, readonly 213 | dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none 214 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none 215 | dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none 216 | 217 | # ReSharper properties 218 | resharper_object_creation_when_type_not_evident = target_typed 219 | 220 | # ReSharper inspection severities 221 | resharper_arrange_object_creation_when_type_evident_highlighting = error 222 | resharper_arrange_object_creation_when_type_not_evident_highlighting = error 223 | resharper_arrange_redundant_parentheses_highlighting = error 224 | resharper_arrange_static_member_qualifier_highlighting = error 225 | resharper_arrange_this_qualifier_highlighting = error 226 | resharper_arrange_type_member_modifiers_highlighting = none 227 | resharper_built_in_type_reference_style_for_member_access_highlighting = hint 228 | resharper_built_in_type_reference_style_highlighting = hint 229 | resharper_check_namespace_highlighting = none 230 | resharper_convert_to_using_declaration_highlighting = error 231 | resharper_css_not_resolved_highlighting = warning 232 | resharper_field_can_be_made_read_only_local_highlighting = none 233 | resharper_merge_into_logical_pattern_highlighting = warning 234 | resharper_merge_into_pattern_highlighting = error 235 | resharper_method_has_async_overload_highlighting = warning 236 | # because stop rider giving errors before source generators have run 237 | resharper_partial_type_with_single_part_highlighting = warning 238 | resharper_redundant_base_qualifier_highlighting = warning 239 | resharper_redundant_cast_highlighting = error 240 | resharper_redundant_empty_object_creation_argument_list_highlighting = error 241 | resharper_redundant_empty_object_or_collection_initializer_highlighting = error 242 | resharper_redundant_name_qualifier_highlighting = error 243 | resharper_redundant_suppress_nullable_warning_expression_highlighting = error 244 | resharper_redundant_using_directive_highlighting = error 245 | resharper_redundant_verbatim_string_prefix_highlighting = error 246 | resharper_redundant_lambda_signature_parentheses_highlighting = error 247 | resharper_replace_substring_with_range_indexer_highlighting = warning 248 | resharper_suggest_var_or_type_built_in_types_highlighting = error 249 | resharper_suggest_var_or_type_elsewhere_highlighting = error 250 | resharper_suggest_var_or_type_simple_types_highlighting = error 251 | resharper_unnecessary_whitespace_highlighting = error 252 | resharper_use_await_using_highlighting = warning 253 | resharper_use_deconstruction_highlighting = warning 254 | 255 | # Sort using and Import directives with System.* appearing first 256 | dotnet_sort_system_directives_first = true 257 | 258 | # Avoid "this." and "Me." if not necessary 259 | dotnet_style_qualification_for_field = false:error 260 | dotnet_style_qualification_for_property = false:error 261 | dotnet_style_qualification_for_method = false:error 262 | dotnet_style_qualification_for_event = false:error 263 | 264 | # Use language keywords instead of framework type names for type references 265 | dotnet_style_predefined_type_for_locals_parameters_members = true:error 266 | dotnet_style_predefined_type_for_member_access = true:error 267 | 268 | # Suggest more modern language features when available 269 | dotnet_style_object_initializer = true:error 270 | dotnet_style_collection_initializer = true:error 271 | dotnet_style_coalesce_expression = false:error 272 | dotnet_style_null_propagation = true:error 273 | dotnet_style_explicit_tuple_names = true:error 274 | 275 | # Use collection expression syntax 276 | resharper_use_collection_expression_highlighting = error 277 | 278 | # Prefer "var" everywhere 279 | csharp_style_var_for_built_in_types = true:error 280 | csharp_style_var_when_type_is_apparent = true:error 281 | csharp_style_var_elsewhere = true:error 282 | 283 | # Prefer method-like constructs to have a block body 284 | csharp_style_expression_bodied_methods = true:error 285 | csharp_style_expression_bodied_local_functions = true:error 286 | csharp_style_expression_bodied_constructors = true:error 287 | csharp_style_expression_bodied_operators = true:error 288 | resharper_place_expr_method_on_single_line = false 289 | 290 | # Prefer property-like constructs to have an expression-body 291 | csharp_style_expression_bodied_properties = true:error 292 | csharp_style_expression_bodied_indexers = true:error 293 | csharp_style_expression_bodied_accessors = true:error 294 | 295 | # Suggest more modern language features when available 296 | csharp_style_pattern_matching_over_is_with_cast_check = true:error 297 | csharp_style_pattern_matching_over_as_with_null_check = true:error 298 | csharp_style_inlined_variable_declaration = true:suggestion 299 | csharp_style_throw_expression = true:suggestion 300 | csharp_style_conditional_delegate_call = true:suggestion 301 | 302 | # Newline settings 303 | #csharp_new_line_before_open_brace = all:error 304 | resharper_max_array_initializer_elements_on_line = 1 305 | csharp_new_line_before_else = true 306 | csharp_new_line_before_catch = true 307 | csharp_new_line_before_finally = true 308 | csharp_new_line_before_members_in_object_initializers = true 309 | csharp_new_line_before_members_in_anonymous_types = true 310 | resharper_wrap_before_first_type_parameter_constraint = true 311 | resharper_wrap_extends_list_style = chop_always 312 | resharper_wrap_after_dot_in_method_calls = false 313 | resharper_wrap_before_binary_pattern_op = false 314 | resharper_wrap_object_and_collection_initializer_style = chop_always 315 | resharper_place_simple_initializer_on_single_line = false 316 | 317 | # space 318 | resharper_space_around_lambda_arrow = true 319 | 320 | dotnet_style_require_accessibility_modifiers = never:error 321 | resharper_place_type_constraints_on_same_line = false 322 | resharper_blank_lines_inside_namespace = 0 323 | resharper_blank_lines_after_file_scoped_namespace_directive = 1 324 | resharper_blank_lines_inside_type = 0 325 | 326 | insert_final_newline = false 327 | resharper_place_attribute_on_same_line = false 328 | resharper_space_around_lambda_arrow = true 329 | resharper_place_constructor_initializer_on_same_line = false 330 | 331 | #braces https://www.jetbrains.com/help/resharper/EditorConfig_CSHARP_CSharpCodeStylePageImplSchema.html#Braces 332 | resharper_braces_for_ifelse = required 333 | resharper_braces_for_foreach = required 334 | resharper_braces_for_while = required 335 | resharper_braces_for_dowhile = required 336 | resharper_braces_for_lock = required 337 | resharper_braces_for_fixed = required 338 | resharper_braces_for_for = required 339 | 340 | resharper_return_value_of_pure_method_is_not_used_highlighting = error 341 | 342 | resharper_all_underscore_local_parameter_name_highlighting = none 343 | 344 | resharper_misleading_body_like_statement_highlighting = error 345 | 346 | resharper_redundant_record_class_keyword_highlighting = error 347 | 348 | resharper_redundant_extends_list_entry_highlighting = error 349 | 350 | # Xml files 351 | [*.{xml,config,nuspec,resx,vsixmanifest,csproj,targets,props,fsproj}] 352 | indent_size = 2 353 | # https://www.jetbrains.com/help/resharper/EditorConfig_XML_XmlCodeStylePageSchema.html#resharper_xml_blank_line_after_pi 354 | resharper_blank_line_after_pi = false 355 | resharper_space_before_self_closing = true 356 | ij_xml_space_inside_empty_tag = true 357 | 358 | [*.json] 359 | indent_size = 2 360 | 361 | # Verify settings 362 | [*.{received,verified}.{txt,xml,json,md,sql,csv,html,htm,md}] 363 | charset = utf-8-bom 364 | end_of_line = lf 365 | indent_size = unset 366 | indent_style = unset 367 | insert_final_newline = false 368 | tab_width = unset 369 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CS1591;CS0649;CS8632;NU1608;NU1109 5 | 11.1.1 6 | preview 7 | 1.0.0 8 | SqlServer, Verify 9 | Extends Verify (https://github.com/VerifyTests/Verify) to allow verification of SqlServer bits. 10 | true 11 | true 12 | true 13 | true 14 | 15 | -------------------------------------------------------------------------------- /src/Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | true 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Shared.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | False 3 | Quiet 4 | True 5 | True 6 | True 7 | DO_NOT_SHOW 8 | ERROR 9 | ERROR 10 | ERROR 11 | WARNING 12 | ERROR 13 | ERROR 14 | ERROR 15 | ERROR 16 | ERROR 17 | ERROR 18 | ERROR 19 | ERROR 20 | ERROR 21 | ERROR 22 | ERROR 23 | ERROR 24 | ERROR 25 | ERROR 26 | ERROR 27 | ERROR 28 | ERROR 29 | ERROR 30 | ERROR 31 | ERROR 32 | ERROR 33 | ERROR 34 | ERROR 35 | ERROR 36 | ERROR 37 | ERROR 38 | ERROR 39 | ERROR 40 | ERROR 41 | ERROR 42 | ERROR 43 | DO_NOT_SHOW 44 | DO_NOT_SHOW 45 | ERROR 46 | ERROR 47 | ERROR 48 | ERROR 49 | ERROR 50 | ERROR 51 | ERROR 52 | ERROR 53 | ERROR 54 | ERROR 55 | ERROR 56 | ERROR 57 | C90+,E79+,S14+ 58 | ERROR 59 | ERROR 60 | ERROR 61 | ERROR 62 | ERROR 63 | ERROR 64 | ERROR 65 | ERROR 66 | ERROR 67 | ERROR 68 | ERROR 69 | ERROR 70 | ERROR 71 | ERROR 72 | ERROR 73 | ERROR 74 | ERROR 75 | ERROR 76 | ERROR 77 | ERROR 78 | ERROR 79 | ERROR 80 | ERROR 81 | ERROR 82 | ERROR 83 | ERROR 84 | ERROR 85 | ERROR 86 | ERROR 87 | ERROR 88 | ERROR 89 | ERROR 90 | ERROR 91 | ERROR 92 | ERROR 93 | ERROR 94 | ERROR 95 | ERROR 96 | ERROR 97 | ERROR 98 | ERROR 99 | ERROR 100 | ERROR 101 | ERROR 102 | ERROR 103 | ERROR 104 | ERROR 105 | ERROR 106 | ERROR 107 | ERROR 108 | ERROR 109 | ERROR 110 | ERROR 111 | ERROR 112 | ERROR 113 | ERROR 114 | ERROR 115 | ERROR 116 | ERROR 117 | ERROR 118 | ERROR 119 | ERROR 120 | ERROR 121 | ERROR 122 | DO_NOT_SHOW 123 | *.received.* 124 | *.verified.* 125 | ERROR 126 | ERROR 127 | DO_NOT_SHOW 128 | ECMAScript 2016 129 | <?xml version="1.0" encoding="utf-16"?><Profile name="c# Cleanup"><AspOptimizeRegisterDirectives>True</AspOptimizeRegisterDirectives><CSCodeStyleAttributes ArrangeVarStyle="True" ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="True" ArrangeAttributes="True" ArrangeCodeBodyStyle="True" ArrangeTrailingCommas="True" ArrangeObjectCreation="True" ArrangeDefaultValue="True" ArrangeNamespaces="True" /><CssAlphabetizeProperties>True</CssAlphabetizeProperties><JSStringLiteralQuotesDescriptor>True</JSStringLiteralQuotesDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><JsInsertSemicolon>True</JsInsertSemicolon><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs><OptimizeReferenceCommentsTs>True</OptimizeReferenceCommentsTs><PublicModifierStyleTs>True</PublicModifierStyleTs><ExplicitAnyTs>True</ExplicitAnyTs><TypeAnnotationStyleTs>True</TypeAnnotationStyleTs><RelativePathStyleTs>True</RelativePathStyleTs><AsInsteadOfCastTs>True</AsInsteadOfCastTs><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CssReformatCode>True</CssReformatCode><JsReformatCode>True</JsReformatCode><JsFormatDocComments>True</JsFormatDocComments><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSharpFormatDocComments>True</CSharpFormatDocComments><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><HtmlReformatCode>True</HtmlReformatCode><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><IDEA_SETTINGS>&lt;profile version="1.0"&gt; 130 | &lt;option name="myName" value="c# Cleanup" /&gt; 131 | &lt;/profile&gt;</IDEA_SETTINGS><RIDER_SETTINGS>&lt;profile&gt; 132 | &lt;Language id="EditorConfig"&gt; 133 | &lt;Reformat&gt;false&lt;/Reformat&gt; 134 | &lt;/Language&gt; 135 | &lt;Language id="HTML"&gt; 136 | &lt;OptimizeImports&gt;false&lt;/OptimizeImports&gt; 137 | &lt;Reformat&gt;false&lt;/Reformat&gt; 138 | &lt;Rearrange&gt;false&lt;/Rearrange&gt; 139 | &lt;/Language&gt; 140 | &lt;Language id="JSON"&gt; 141 | &lt;Reformat&gt;false&lt;/Reformat&gt; 142 | &lt;/Language&gt; 143 | &lt;Language id="RELAX-NG"&gt; 144 | &lt;Reformat&gt;false&lt;/Reformat&gt; 145 | &lt;/Language&gt; 146 | &lt;Language id="XML"&gt; 147 | &lt;OptimizeImports&gt;false&lt;/OptimizeImports&gt; 148 | &lt;Reformat&gt;false&lt;/Reformat&gt; 149 | &lt;Rearrange&gt;false&lt;/Rearrange&gt; 150 | &lt;/Language&gt; 151 | &lt;/profile&gt;</RIDER_SETTINGS></Profile> 152 | ExpressionBody 153 | ExpressionBody 154 | ExpressionBody 155 | False 156 | NEVER 157 | NEVER 158 | False 159 | False 160 | False 161 | True 162 | False 163 | CHOP_ALWAYS 164 | False 165 | False 166 | RemoveIndent 167 | RemoveIndent 168 | False 169 | True 170 | True 171 | True 172 | True 173 | True 174 | ERROR 175 | DoNothing 176 | -------------------------------------------------------------------------------- /src/Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Global using directives 2 | 3 | global using System.Data; 4 | global using System.Data.SqlTypes; 5 | global using Microsoft.Data.SqlClient; 6 | global using Microsoft.SqlServer.Management.Common; 7 | global using Microsoft.SqlServer.Management.Smo; 8 | global using VerifyTests.SqlServer; -------------------------------------------------------------------------------- /src/Tests/ModuleInit.cs: -------------------------------------------------------------------------------- 1 | public static class ModuleInit 2 | { 3 | #region Enable 4 | 5 | [ModuleInitializer] 6 | public static void Init() => 7 | VerifySqlServer.Initialize(); 8 | 9 | #endregion 10 | 11 | [ModuleInitializer] 12 | public static void InitOther() => 13 | VerifierSettings.InitializePlugins(); 14 | } -------------------------------------------------------------------------------- /src/Tests/Tests.CommandEmpty.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | Text: 3 | select * 4 | from MyTable, 5 | HasTransaction: false 6 | } -------------------------------------------------------------------------------- /src/Tests/Tests.CommandEscaped.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | Text: 3 | select * 4 | from MyTable, 5 | HasTransaction: false 6 | } -------------------------------------------------------------------------------- /src/Tests/Tests.CommandFull.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | Text: 3 | select * 4 | from MyTable, 5 | Parameters: { 6 | name: 10 7 | }, 8 | HasTransaction: false, 9 | Timeout: 10, 10 | Type: StoredProcedure, 11 | Notification: { 12 | Options: options, 13 | Timeout: 10, 14 | UserData: user data 15 | }, 16 | UpdatedRowSource: FirstReturnedRecord, 17 | EnableOptimizedParameterBinding: true 18 | } -------------------------------------------------------------------------------- /src/Tests/Tests.Error.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | Message: Invalid object name 'MyTabl2e'., 3 | Number: 208, 4 | Line: 1 5 | } -------------------------------------------------------------------------------- /src/Tests/Tests.Exception.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | Message: Invalid object name 'MyTabl2e'., 3 | Number: 208, 4 | Line: 1 5 | } -------------------------------------------------------------------------------- /src/Tests/Tests.ParameterCollectionFull.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | name: { 3 | Value: DateTime_1, 4 | Direction: InputOutput, 5 | Offset: 5, 6 | Precision: 2, 7 | Scale: 3, 8 | Size: 4, 9 | LocaleId: 10, 10 | SourceColumn: sourceColumn, 11 | SourceVersion: Proposed, 12 | ForceColumnEncryption: true 13 | } 14 | } -------------------------------------------------------------------------------- /src/Tests/Tests.ParameterEmpty.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | Name: name, 3 | DbType: Date 4 | } -------------------------------------------------------------------------------- /src/Tests/Tests.ParameterFull.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | Name: name, 3 | Value: DateTime_1, 4 | Direction: InputOutput, 5 | Offset: 5, 6 | Precision: 2, 7 | Scale: 3, 8 | Size: 4, 9 | LocaleId: 10, 10 | SourceColumn: sourceColumn, 11 | SourceVersion: Proposed, 12 | ForceColumnEncryption: true 13 | } -------------------------------------------------------------------------------- /src/Tests/Tests.RecordingError.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | sql: { 3 | Exception: { 4 | Message: Invalid object name 'MyTabl2e'., 5 | Number: 208, 6 | Line: 1 7 | }, 8 | Command: { 9 | Text: 10 | select * 11 | from MyTabl2e, 12 | HasTransaction: false 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Tests/Tests.RecordingReadingResults.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | sqlEntries: [ 3 | { 4 | Text: 5 | select Value 6 | from MyTable, 7 | HasTransaction: false 8 | }, 9 | { 10 | Exception: { 11 | Message: Invalid object name 'BadTable'., 12 | Number: 208, 13 | Line: 1 14 | }, 15 | Command: { 16 | Text: 17 | select Value 18 | from BadTable, 19 | HasTransaction: false 20 | } 21 | } 22 | ], 23 | sqlCommandsViaType: [ 24 | { 25 | Text: 26 | select Value 27 | from MyTable, 28 | HasTransaction: false 29 | } 30 | ], 31 | sqlErrorsViaType: [ 32 | { 33 | Exception: { 34 | Message: Invalid object name 'BadTable'., 35 | Number: 208, 36 | Line: 1 37 | }, 38 | Command: { 39 | Text: 40 | select Value 41 | from BadTable, 42 | HasTransaction: false 43 | } 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /src/Tests/Tests.RecordingSpecific.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | value: 42, 3 | sqlEntries: [ 4 | { 5 | Text: 6 | select Value 7 | from MyTable, 8 | HasTransaction: false 9 | }, 10 | { 11 | Exception: { 12 | Message: Invalid object name 'BadTable'., 13 | Number: 208, 14 | Line: 1 15 | }, 16 | Command: { 17 | Text: 18 | select Value 19 | from BadTable, 20 | HasTransaction: false 21 | } 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /src/Tests/Tests.RecordingTest.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | sql: [ 3 | { 4 | Text: 5 | select * 6 | from MyTable, 7 | HasTransaction: false 8 | }, 9 | { 10 | Text: 11 | select * 12 | from MyTable, 13 | HasTransaction: false 14 | }, 15 | { 16 | Text: 17 | select * 18 | from MyTable, 19 | HasTransaction: false 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /src/Tests/Tests.RecordingUsage.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | target: 42, 3 | sql: { 4 | Text: 5 | select Value 6 | from MyTable, 7 | HasTransaction: false 8 | } 9 | } -------------------------------------------------------------------------------- /src/Tests/Tests.RecordingWithParameter.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | target: null, 3 | sql: { 4 | Text: 5 | select Value 6 | from MyTable 7 | where Value = @param, 8 | Parameters: { 9 | param: 10 10 | }, 11 | HasTransaction: false 12 | } 13 | } -------------------------------------------------------------------------------- /src/Tests/Tests.Schema.verified.md: -------------------------------------------------------------------------------- 1 | ## Tables 2 | 3 | ### MyOtherTable 4 | 5 | ```sql 6 | CREATE TABLE [dbo].[MyOtherTable]( 7 | [Value] [int] NULL 8 | ) ON [PRIMARY] 9 | ``` 10 | 11 | ### MyTable 12 | 13 | ```sql 14 | CREATE TABLE [dbo].[MyTable]( 15 | [Value] [int] NULL 16 | ) ON [PRIMARY] 17 | 18 | CREATE NONCLUSTERED INDEX [MyIndex] ON [dbo].[MyTable] 19 | ( 20 | [Value] ASC 21 | ) ON [PRIMARY] 22 | 23 | CREATE TRIGGER MyTrigger 24 | ON MyTable 25 | AFTER UPDATE 26 | AS RAISERROR ('Notify Customer Relations', 16, 10); 27 | 28 | ALTER TABLE [dbo].[MyTable] ENABLE TRIGGER [MyTrigger] 29 | ``` 30 | 31 | ## Views 32 | 33 | ### MyView 34 | 35 | ```sql 36 | CREATE VIEW MyView 37 | AS 38 | SELECT Value 39 | FROM MyTable 40 | WHERE (Value > 10); 41 | ``` 42 | 43 | ## StoredProcedures 44 | 45 | ### MyProcedure 46 | 47 | ```sql 48 | CREATE PROCEDURE MyProcedure 49 | AS 50 | BEGIN 51 | SET NOCOUNT ON; 52 | SELECT Value 53 | FROM MyTable 54 | WHERE (Value > 10); 55 | END; 56 | ``` 57 | 58 | ## UserDefinedFunctions 59 | 60 | ### MyFunction 61 | 62 | ```sql 63 | CREATE FUNCTION MyFunction( 64 | @quantity INT, 65 | @list_price DEC(10,2), 66 | @discount DEC(4,2) 67 | ) 68 | RETURNS DEC(10,2) 69 | AS 70 | BEGIN 71 | RETURN @quantity * @list_price * (1 - @discount); 72 | END; 73 | ``` 74 | 75 | ## Synonyms 76 | 77 | ### synonym1 78 | 79 | ```sql 80 | CREATE SYNONYM [dbo].[synonym1] FOR [MyTable] 81 | ``` 82 | -------------------------------------------------------------------------------- /src/Tests/Tests.SchemaAsSql.verified.sql: -------------------------------------------------------------------------------- 1 | -- Tables 2 | 3 | CREATE TABLE [dbo].[MyOtherTable]( 4 | [Value] [int] NULL 5 | ) ON [PRIMARY] 6 | 7 | CREATE TABLE [dbo].[MyTable]( 8 | [Value] [int] NULL 9 | ) ON [PRIMARY] 10 | 11 | CREATE NONCLUSTERED INDEX [MyIndex] ON [dbo].[MyTable] 12 | ( 13 | [Value] ASC 14 | ) ON [PRIMARY] 15 | 16 | CREATE TRIGGER MyTrigger 17 | ON MyTable 18 | AFTER UPDATE 19 | AS RAISERROR ('Notify Customer Relations', 16, 10); 20 | 21 | ALTER TABLE [dbo].[MyTable] ENABLE TRIGGER [MyTrigger] 22 | 23 | -- Views 24 | 25 | CREATE VIEW MyView 26 | AS 27 | SELECT Value 28 | FROM MyTable 29 | WHERE (Value > 10); 30 | 31 | -- StoredProcedures 32 | 33 | CREATE PROCEDURE MyProcedure 34 | AS 35 | BEGIN 36 | SET NOCOUNT ON; 37 | SELECT Value 38 | FROM MyTable 39 | WHERE (Value > 10); 40 | END; 41 | 42 | -- UserDefinedFunctions 43 | 44 | CREATE FUNCTION MyFunction( 45 | @quantity INT, 46 | @list_price DEC(10,2), 47 | @discount DEC(4,2) 48 | ) 49 | RETURNS DEC(10,2) 50 | AS 51 | BEGIN 52 | RETURN @quantity * @list_price * (1 - @discount); 53 | END; 54 | 55 | -- Synonyms 56 | 57 | CREATE SYNONYM [dbo].[synonym1] FOR [MyTable] -------------------------------------------------------------------------------- /src/Tests/Tests.SchemaFilter.verified.md: -------------------------------------------------------------------------------- 1 | ## Tables 2 | 3 | ### MyOtherTable 4 | 5 | ```sql 6 | CREATE TABLE [dbo].[MyOtherTable]( 7 | [Value] [int] NULL 8 | ) ON [PRIMARY] 9 | ``` 10 | 11 | ### MyTable 12 | 13 | ```sql 14 | CREATE TABLE [dbo].[MyTable]( 15 | [Value] [int] NULL 16 | ) ON [PRIMARY] 17 | 18 | CREATE NONCLUSTERED INDEX [MyIndex] ON [dbo].[MyTable] 19 | ( 20 | [Value] ASC 21 | ) ON [PRIMARY] 22 | 23 | CREATE TRIGGER MyTrigger 24 | ON MyTable 25 | AFTER UPDATE 26 | AS RAISERROR ('Notify Customer Relations', 16, 10); 27 | 28 | ALTER TABLE [dbo].[MyTable] ENABLE TRIGGER [MyTrigger] 29 | ``` 30 | 31 | ## Views 32 | 33 | ### MyView 34 | 35 | ```sql 36 | CREATE VIEW MyView 37 | AS 38 | SELECT Value 39 | FROM MyTable 40 | WHERE (Value > 10); 41 | ``` 42 | -------------------------------------------------------------------------------- /src/Tests/Tests.SchemaInDynamic.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | connection: 3 | ## Tables 4 | 5 | ### MyOtherTable 6 | 7 | ```sql 8 | CREATE TABLE [dbo].[MyOtherTable]( 9 | [Value] [int] NULL 10 | ) ON [PRIMARY] 11 | ``` 12 | 13 | ### MyTable 14 | 15 | ```sql 16 | CREATE TABLE [dbo].[MyTable]( 17 | [Value] [int] NULL 18 | ) ON [PRIMARY] 19 | 20 | CREATE NONCLUSTERED INDEX [MyIndex] ON [dbo].[MyTable] 21 | ( 22 | [Value] ASC 23 | ) ON [PRIMARY] 24 | 25 | CREATE TRIGGER MyTrigger 26 | ON MyTable 27 | AFTER UPDATE 28 | AS RAISERROR ('Notify Customer Relations', 16, 10); 29 | 30 | ALTER TABLE [dbo].[MyTable] ENABLE TRIGGER [MyTrigger] 31 | ``` 32 | 33 | ## Views 34 | 35 | ### MyView 36 | 37 | ```sql 38 | CREATE VIEW MyView 39 | AS 40 | SELECT Value 41 | FROM MyTable 42 | WHERE (Value > 10); 43 | ``` 44 | 45 | ## StoredProcedures 46 | 47 | ### MyProcedure 48 | 49 | ```sql 50 | CREATE PROCEDURE MyProcedure 51 | AS 52 | BEGIN 53 | SET NOCOUNT ON; 54 | SELECT Value 55 | FROM MyTable 56 | WHERE (Value > 10); 57 | END; 58 | ``` 59 | 60 | ## UserDefinedFunctions 61 | 62 | ### MyFunction 63 | 64 | ```sql 65 | CREATE FUNCTION MyFunction( 66 | @quantity INT, 67 | @list_price DEC(10,2), 68 | @discount DEC(4,2) 69 | ) 70 | RETURNS DEC(10,2) 71 | AS 72 | BEGIN 73 | RETURN @quantity * @list_price * (1 - @discount); 74 | END; 75 | ``` 76 | 77 | ## Synonyms 78 | 79 | ### synonym1 80 | 81 | ```sql 82 | CREATE SYNONYM [dbo].[synonym1] FOR [MyTable] 83 | ``` 84 | } -------------------------------------------------------------------------------- /src/Tests/Tests.SchemaInclude.verified.md: -------------------------------------------------------------------------------- 1 | ## Tables 2 | 3 | ### MyOtherTable 4 | 5 | ```sql 6 | CREATE TABLE [dbo].[MyOtherTable]( 7 | [Value] [int] NULL 8 | ) ON [PRIMARY] 9 | ``` 10 | 11 | ### MyTable 12 | 13 | ```sql 14 | CREATE TABLE [dbo].[MyTable]( 15 | [Value] [int] NULL 16 | ) ON [PRIMARY] 17 | 18 | CREATE NONCLUSTERED INDEX [MyIndex] ON [dbo].[MyTable] 19 | ( 20 | [Value] ASC 21 | ) ON [PRIMARY] 22 | 23 | CREATE TRIGGER MyTrigger 24 | ON MyTable 25 | AFTER UPDATE 26 | AS RAISERROR ('Notify Customer Relations', 16, 10); 27 | 28 | ALTER TABLE [dbo].[MyTable] ENABLE TRIGGER [MyTrigger] 29 | ``` 30 | 31 | ## Views 32 | 33 | ### MyView 34 | 35 | ```sql 36 | CREATE VIEW MyView 37 | AS 38 | SELECT Value 39 | FROM MyTable 40 | WHERE (Value > 10); 41 | ``` 42 | -------------------------------------------------------------------------------- /src/Tests/Tests.SchemaIncludeAll.verified.md: -------------------------------------------------------------------------------- 1 | ## Tables 2 | 3 | ### MyOtherTable 4 | 5 | ```sql 6 | CREATE TABLE [dbo].[MyOtherTable]( 7 | [Value] [int] NULL 8 | ) ON [PRIMARY] 9 | ``` 10 | 11 | ### MyTable 12 | 13 | ```sql 14 | CREATE TABLE [dbo].[MyTable]( 15 | [Value] [int] NULL 16 | ) ON [PRIMARY] 17 | 18 | CREATE NONCLUSTERED INDEX [MyIndex] ON [dbo].[MyTable] 19 | ( 20 | [Value] ASC 21 | ) ON [PRIMARY] 22 | 23 | CREATE TRIGGER MyTrigger 24 | ON MyTable 25 | AFTER UPDATE 26 | AS RAISERROR ('Notify Customer Relations', 16, 10); 27 | 28 | ALTER TABLE [dbo].[MyTable] ENABLE TRIGGER [MyTrigger] 29 | ``` 30 | 31 | ## Views 32 | 33 | ### MyView 34 | 35 | ```sql 36 | CREATE VIEW MyView 37 | AS 38 | SELECT Value 39 | FROM MyTable 40 | WHERE (Value > 10); 41 | ``` 42 | 43 | ## StoredProcedures 44 | 45 | ### MyProcedure 46 | 47 | ```sql 48 | CREATE PROCEDURE MyProcedure 49 | AS 50 | BEGIN 51 | SET NOCOUNT ON; 52 | SELECT Value 53 | FROM MyTable 54 | WHERE (Value > 10); 55 | END; 56 | ``` 57 | 58 | ## UserDefinedFunctions 59 | 60 | ### MyFunction 61 | 62 | ```sql 63 | CREATE FUNCTION MyFunction( 64 | @quantity INT, 65 | @list_price DEC(10,2), 66 | @discount DEC(4,2) 67 | ) 68 | RETURNS DEC(10,2) 69 | AS 70 | BEGIN 71 | RETURN @quantity * @list_price * (1 - @discount); 72 | END; 73 | ``` 74 | 75 | ## Synonyms 76 | 77 | ### synonym1 78 | 79 | ```sql 80 | CREATE SYNONYM [dbo].[synonym1] FOR [MyTable] 81 | ``` 82 | -------------------------------------------------------------------------------- /src/Tests/Tests.cs: -------------------------------------------------------------------------------- 1 | [TestFixture] 2 | public class Tests 3 | { 4 | static SqlInstance sqlInstance; 5 | 6 | static Tests() => 7 | sqlInstance = new( 8 | "VerifySqlServer", 9 | connection => 10 | { 11 | var server = new Server(new ServerConnection(connection)); 12 | server.ConnectionContext.ExecuteNonQuery( 13 | """ 14 | CREATE TABLE 15 | MyTable(Value int); 16 | GO 17 | 18 | CREATE INDEX MyIndex 19 | ON MyTable (Value); 20 | GO 21 | 22 | INSERT INTO MyTable (Value) 23 | VALUES (42); 24 | GO 25 | 26 | CREATE TRIGGER MyTrigger 27 | ON MyTable 28 | AFTER UPDATE 29 | AS RAISERROR ('Notify Customer Relations', 16, 10); 30 | GO 31 | 32 | CREATE TABLE 33 | MyOtherTable(Value int); 34 | GO 35 | 36 | CREATE VIEW MyView 37 | AS 38 | SELECT Value 39 | FROM MyTable 40 | WHERE (Value > 10); 41 | GO 42 | 43 | create synonym synonym1 44 | for MyTable; 45 | GO 46 | 47 | CREATE PROCEDURE MyProcedure 48 | AS 49 | BEGIN 50 | SET NOCOUNT ON; 51 | SELECT Value 52 | FROM MyTable 53 | WHERE (Value > 10); 54 | END; 55 | GO 56 | 57 | CREATE FUNCTION MyFunction( 58 | @quantity INT, 59 | @list_price DEC(10,2), 60 | @discount DEC(4,2) 61 | ) 62 | RETURNS DEC(10,2) 63 | AS 64 | BEGIN 65 | RETURN @quantity * @list_price * (1 - @discount); 66 | END; 67 | """); 68 | return Task.CompletedTask; 69 | }); 70 | 71 | [Test] 72 | public async Task Schema() 73 | { 74 | await using var database = await sqlInstance.Build(); 75 | var connection = database.Connection; 76 | 77 | #region SqlServerSchema 78 | 79 | await Verify(connection); 80 | 81 | #endregion 82 | } 83 | 84 | [Test] 85 | public async Task SchemaAsSql() 86 | { 87 | await using var database = await sqlInstance.Build(); 88 | var connection = database.Connection; 89 | 90 | #region SqlServerSchemaAsSql 91 | 92 | await Verify(connection) 93 | .SchemaAsSql(); 94 | 95 | #endregion 96 | } 97 | 98 | [Test] 99 | public async Task SchemaInDynamic() 100 | { 101 | await using var database = await sqlInstance.Build(); 102 | var connection = database.Connection; 103 | await Verify(new 104 | { 105 | connection 106 | }); 107 | } 108 | 109 | [Test] 110 | public async Task RecordingError() 111 | { 112 | await using var database = await sqlInstance.Build(); 113 | await using var connection = new SqlConnection(database.ConnectionString); 114 | await connection.OpenAsync(); 115 | Recording.Start(); 116 | await using var command = connection.CreateCommand(); 117 | command.CommandText = "select * from MyTabl2e"; 118 | try 119 | { 120 | await using var dataReader = await command.ExecuteReaderAsync(); 121 | } 122 | catch 123 | { 124 | } 125 | 126 | await Verify() 127 | .ScrubLinesContaining("HelpLink.ProdVer"); 128 | } 129 | 130 | [Test] 131 | public async Task CommandEscaped() 132 | { 133 | var command = new SqlCommand 134 | { 135 | CommandText = "select * from [MyTable]" 136 | }; 137 | await Verify(command); 138 | } 139 | 140 | [Test] 141 | public async Task CommandEmpty() 142 | { 143 | var command = new SqlCommand 144 | { 145 | CommandText = "select * from MyTable" 146 | }; 147 | await Verify(command); 148 | } 149 | 150 | [Test] 151 | public async Task CommandFull() 152 | { 153 | var command = new SqlCommand 154 | { 155 | CommandText = "select * from MyTable", 156 | CommandTimeout = 10, 157 | CommandType = CommandType.StoredProcedure, 158 | DesignTimeVisible = true, 159 | UpdatedRowSource = UpdateRowSource.FirstReturnedRecord, 160 | EnableOptimizedParameterBinding = true, 161 | Notification = new("user data", "options", 10) 162 | }; 163 | command.Parameters.AddWithValue("name", 10); 164 | await Verify(command); 165 | } 166 | 167 | [Test] 168 | public async Task Exception() 169 | { 170 | await using var database = await sqlInstance.Build(); 171 | await using var connection = new SqlConnection(database.ConnectionString); 172 | await connection.OpenAsync(); 173 | await using var command = connection.CreateCommand(); 174 | command.CommandText = "select * from MyTabl2e"; 175 | await ThrowsTask(() => command.ExecuteReaderAsync()); 176 | } 177 | 178 | [Test] 179 | public async Task Error() 180 | { 181 | await using var database = await sqlInstance.Build(); 182 | await using var connection = new SqlConnection(database.ConnectionString); 183 | await connection.OpenAsync(); 184 | await using var command = connection.CreateCommand(); 185 | command.CommandText = "select * from MyTabl2e"; 186 | try 187 | { 188 | await command.ExecuteReaderAsync(); 189 | } 190 | catch (SqlException exception) 191 | { 192 | await Verify(exception.Errors[0]); 193 | } 194 | } 195 | 196 | [Test] 197 | public async Task RecordingUsage() 198 | { 199 | await using var database = await sqlInstance.Build(); 200 | var connectionString = database.ConnectionString; 201 | 202 | #region Recording 203 | 204 | await using var connection = new SqlConnection(connectionString); 205 | await connection.OpenAsync(); 206 | Recording.Start(); 207 | await using var command = connection.CreateCommand(); 208 | command.CommandText = "select Value from MyTable"; 209 | var value = await command.ExecuteScalarAsync(); 210 | await Verify(value!); 211 | 212 | #endregion 213 | } 214 | 215 | [Test] 216 | public async Task RecordingWithParameter() 217 | { 218 | await using var database = await sqlInstance.Build(); 219 | var connectionString = database.ConnectionString; 220 | 221 | await using var connection = new SqlConnection(connectionString); 222 | await connection.OpenAsync(); 223 | Recording.Start(); 224 | await using var command = connection.CreateCommand(); 225 | command.Parameters.AddWithValue("param", 10); 226 | command.CommandText = "select Value from MyTable where Value = @param"; 227 | var value = await command.ExecuteScalarAsync(); 228 | await Verify(value!); 229 | } 230 | 231 | [Test] 232 | public Task ParameterEmpty() 233 | { 234 | var parameter = new SqlParameter("name", SqlDbType.Date); 235 | return Verify(parameter); 236 | } 237 | 238 | [Test] 239 | public Task ParameterFull() 240 | { 241 | var parameter = BuildFullParameter(); 242 | 243 | return Verify(parameter); 244 | } 245 | 246 | [Test] 247 | public Task ParameterCollectionFull() 248 | { 249 | var parameter = BuildFullParameter(); 250 | 251 | var parameters = new SqlCommand().Parameters; 252 | parameters.Add(parameter); 253 | return Verify(parameters); 254 | } 255 | 256 | static SqlParameter BuildFullParameter() => 257 | new("name", SqlDbType.DateTime) 258 | { 259 | Direction = ParameterDirection.InputOutput, 260 | Offset = 5, 261 | Precision = 2, 262 | Scale = 3, 263 | Value = DateTime.Now, 264 | CompareInfo = SqlCompareOptions.BinarySort2, 265 | LocaleId = 10, 266 | Size = 4, 267 | IsNullable = false, 268 | SourceVersion = DataRowVersion.Proposed, 269 | ForceColumnEncryption = true, 270 | SourceColumn = "sourceColumn" 271 | }; 272 | 273 | [Test] 274 | public async Task RecordingSpecific() 275 | { 276 | await using var database = await sqlInstance.Build(); 277 | var connectionString = database.ConnectionString; 278 | 279 | #region RecordingSpecific 280 | 281 | await using var connection = new SqlConnection(connectionString); 282 | await connection.OpenAsync(); 283 | Recording.Start(); 284 | await using var command = connection.CreateCommand(); 285 | command.CommandText = "select Value from MyTable"; 286 | var value = await command.ExecuteScalarAsync(); 287 | 288 | await using var errorCommand = connection.CreateCommand(); 289 | errorCommand.CommandText = "select Value from BadTable"; 290 | try 291 | { 292 | await errorCommand.ExecuteScalarAsync(); 293 | } 294 | catch 295 | { 296 | } 297 | 298 | var entries = Recording 299 | .Stop() 300 | .Select(_ => _.Data); 301 | //Optionally filter results 302 | await Verify( 303 | new 304 | { 305 | value, 306 | sqlEntries = entries 307 | }); 308 | 309 | #endregion 310 | } 311 | 312 | [Test] 313 | public async Task RecordingReadingResults() 314 | { 315 | await using var database = await sqlInstance.Build(); 316 | var connectionString = database.ConnectionString; 317 | 318 | await using var connection = new SqlConnection(connectionString); 319 | await connection.OpenAsync(); 320 | Recording.Start(); 321 | await using var command = connection.CreateCommand(); 322 | command.CommandText = "select Value from MyTable"; 323 | await command.ExecuteScalarAsync(); 324 | 325 | await using var errorCommand = connection.CreateCommand(); 326 | errorCommand.CommandText = "select Value from BadTable"; 327 | try 328 | { 329 | await errorCommand.ExecuteScalarAsync(); 330 | } 331 | catch 332 | { 333 | } 334 | 335 | #region RecordingReadingResults 336 | 337 | var entries = Recording.Stop(); 338 | 339 | // all sql entries via key 340 | var sqlEntries = entries 341 | .Where(_ => _.Name == "sql") 342 | .Select(_ => _.Data); 343 | 344 | // successful Commands via Type 345 | var sqlCommandsViaType = entries 346 | .Select(_ => _.Data) 347 | .OfType(); 348 | 349 | // failed Commands via Type 350 | var sqlErrorsViaType = entries 351 | .Select(_ => _.Data) 352 | .OfType(); 353 | 354 | #endregion 355 | 356 | await Verify( 357 | new 358 | { 359 | sqlEntries, 360 | sqlCommandsViaType, 361 | sqlErrorsViaType, 362 | }); 363 | 364 | } 365 | 366 | [Test] 367 | public async Task RecordingTest() 368 | { 369 | static async Task Execute(SqlConnection sqlConnection) 370 | { 371 | await using var command = sqlConnection.CreateCommand(); 372 | command.CommandText = "select * from MyTable"; 373 | await using var dataReader = await command.ExecuteReaderAsync(); 374 | } 375 | 376 | await using var database = await sqlInstance.Build(); 377 | await using var connection = new SqlConnection(database.ConnectionString); 378 | await connection.OpenAsync(); 379 | await Execute(connection); 380 | Recording.Start(); 381 | await Execute(connection); 382 | await Execute(connection); 383 | await Execute(connection); 384 | await Verify(); 385 | } 386 | 387 | [Test] 388 | public async Task SchemaInclude() 389 | { 390 | await using var database = await sqlInstance.Build(); 391 | var connection = database.Connection; 392 | 393 | #region SchemaInclude 394 | 395 | await Verify(connection) 396 | // include only tables and views 397 | .SchemaIncludes(DbObjects.Tables | DbObjects.Views); 398 | 399 | #endregion 400 | } 401 | 402 | [Test] 403 | public async Task SchemaIncludeAll() 404 | { 405 | await using var database = await sqlInstance.Build(); 406 | var connection = database.Connection; 407 | 408 | await Verify(connection) 409 | .SchemaIncludes(DbObjects.All); 410 | } 411 | 412 | [Test] 413 | public async Task SchemaFilter() 414 | { 415 | await using var database = await sqlInstance.Build(); 416 | var connection = database.Connection; 417 | 418 | #region SchemaFilter 419 | 420 | await Verify(connection) 421 | // include tables & views, or named MyTrigger 422 | .SchemaFilter( 423 | _ => _ is TableViewBase || 424 | _.Name == "MyTrigger"); 425 | 426 | #endregion 427 | } 428 | } -------------------------------------------------------------------------------- /src/Tests/Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Verify.SqlServer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31717.71 5 | MinimumVisualStudioVersion = 16.0.29201.188 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Verify.SqlServer", "Verify.SqlServer\Verify.SqlServer.csproj", "{C35E31DE-40A9-45C0-817B-41BE5B7D076E}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{76FE87D9-EF6B-4D31-99F2-7099C719AE99}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9A6C4741-C83D-4E05-A62F-8049F65B9B6C}" 11 | ProjectSection(SolutionItems) = preProject 12 | .editorconfig = .editorconfig 13 | ..\.gitignore = ..\.gitignore 14 | appveyor.yml = appveyor.yml 15 | Directory.Build.props = Directory.Build.props 16 | Directory.Packages.props = Directory.Packages.props 17 | global.json = global.json 18 | mdsnippets.json = mdsnippets.json 19 | ..\readme.md = ..\readme.md 20 | nuget.config = nuget.config 21 | ..\.gitattributes = ..\.gitattributes 22 | EndProjectSection 23 | EndProject 24 | Global 25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 26 | Debug|Any CPU = Debug|Any CPU 27 | Release|Any CPU = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {C35E31DE-40A9-45C0-817B-41BE5B7D076E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {C35E31DE-40A9-45C0-817B-41BE5B7D076E}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {C35E31DE-40A9-45C0-817B-41BE5B7D076E}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {C35E31DE-40A9-45C0-817B-41BE5B7D076E}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {76FE87D9-EF6B-4D31-99F2-7099C719AE99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {76FE87D9-EF6B-4D31-99F2-7099C719AE99}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {76FE87D9-EF6B-4D31-99F2-7099C719AE99}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {76FE87D9-EF6B-4D31-99F2-7099C719AE99}.Release|Any CPU.Build.0 = Release|Any CPU 38 | EndGlobalSection 39 | GlobalSection(SolutionProperties) = preSolution 40 | HideSolutionNode = FALSE 41 | EndGlobalSection 42 | GlobalSection(ExtensibilityGlobals) = postSolution 43 | SolutionGuid = {B0B94980-5385-4EC6-A0EE-C9A191DDFC05} 44 | EndGlobalSection 45 | EndGlobal 46 | -------------------------------------------------------------------------------- /src/Verify.SqlServer.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | ..\Shared.sln.DotSettings 3 | True 4 | True 5 | 1 6 | -------------------------------------------------------------------------------- /src/Verify.SqlServer/Converters/CommandConverter.cs: -------------------------------------------------------------------------------- 1 | class CommandConverter : 2 | WriteOnlyJsonConverter 3 | { 4 | public override void Write(VerifyJsonWriter writer, SqlCommand command) 5 | { 6 | writer.WriteStartObject(); 7 | writer.WriteMember(command, SqlFormatter.Format(command.CommandText).ToString(), "Text"); 8 | writer.WriteMember(command, command.Parameters, "Parameters"); 9 | writer.WriteMember(command, command.Transaction != null, "HasTransaction"); 10 | 11 | if (command.CommandTimeout != 30) 12 | { 13 | writer.WriteMember(command, command.CommandTimeout, "Timeout"); 14 | } 15 | 16 | if (command.CommandType != CommandType.Text) 17 | { 18 | writer.WriteMember(command, command.CommandType, "Type"); 19 | } 20 | 21 | if (!command.DesignTimeVisible) 22 | { 23 | writer.WriteMember(command, command.DesignTimeVisible, "DesignTimeVisible"); 24 | } 25 | 26 | writer.WriteMember(command, command.Notification, "Notification"); 27 | 28 | if (command.UpdatedRowSource != UpdateRowSource.Both) 29 | { 30 | writer.WriteMember(command, command.UpdatedRowSource, "UpdatedRowSource"); 31 | } 32 | 33 | if (command.EnableOptimizedParameterBinding) 34 | { 35 | writer.WriteMember(command, command.EnableOptimizedParameterBinding, "EnableOptimizedParameterBinding"); 36 | } 37 | 38 | writer.WriteEndObject(); 39 | } 40 | } -------------------------------------------------------------------------------- /src/Verify.SqlServer/Converters/ConnectionConverter.cs: -------------------------------------------------------------------------------- 1 | class ConnectionConverter : 2 | WriteOnlyJsonConverter 3 | { 4 | public override void Write(VerifyJsonWriter writer, SqlConnection connection) 5 | { 6 | var schemaSettings = writer.Context.GetSchemaSettings(); 7 | var builder = new SqlScriptBuilder(schemaSettings); 8 | var script = builder.BuildContent(connection); 9 | writer.WriteValue(script); 10 | } 11 | } -------------------------------------------------------------------------------- /src/Verify.SqlServer/Converters/ErrorConverter.cs: -------------------------------------------------------------------------------- 1 | class ErrorConverter : 2 | WriteOnlyJsonConverter 3 | { 4 | public override void Write(VerifyJsonWriter writer, SqlError error) 5 | { 6 | writer.WriteStartObject(); 7 | writer.WriteMember(error, error.Message, "Message"); 8 | writer.WriteMember(error, error.Number, "Number"); 9 | writer.WriteMember(error, error.LineNumber, "Line"); 10 | if (error.Procedure != "") 11 | { 12 | writer.WriteMember(error, error.Procedure, "Procedure"); 13 | } 14 | 15 | writer.WriteEndObject(); 16 | } 17 | } -------------------------------------------------------------------------------- /src/Verify.SqlServer/Converters/ExceptionConverter.cs: -------------------------------------------------------------------------------- 1 | class ExceptionConverter : 2 | WriteOnlyJsonConverter 3 | { 4 | public override void Write(VerifyJsonWriter writer, SqlException exception) 5 | { 6 | writer.WriteStartObject(); 7 | 8 | var errors = exception.Errors; 9 | 10 | if (errors.Count == 1) 11 | { 12 | var error = errors[0]; 13 | writer.WriteMember(error, error.Message, "Message"); 14 | writer.WriteMember(error, error.Number, "Number"); 15 | writer.WriteMember(error, error.LineNumber, "Line"); 16 | if (exception.Procedure != "") 17 | { 18 | writer.WriteMember(error, error.Procedure, "Procedure"); 19 | } 20 | } 21 | else 22 | { 23 | writer.WriteMember(exception, errors, "Errors"); 24 | } 25 | 26 | writer.WriteEndObject(); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Verify.SqlServer/Converters/ParameterCollectionConverter.cs: -------------------------------------------------------------------------------- 1 | class ParameterCollectionConverter : 2 | WriteOnlyJsonConverter 3 | { 4 | public override void Write(VerifyJsonWriter writer, SqlParameterCollection parameters) 5 | { 6 | writer.WriteStartObject(); 7 | 8 | ParameterConverter.OmitName(); 9 | foreach (SqlParameter parameter in parameters) 10 | { 11 | var name = parameter.ParameterName; 12 | object? value; 13 | if (ParameterConverter.IsOnlyValue(parameter)) 14 | { 15 | value = parameter.Value; 16 | } 17 | else 18 | { 19 | value = parameter; 20 | } 21 | 22 | writer.WriteMember(parameters, value, name); 23 | } 24 | 25 | ParameterConverter.ClearOmitName(); 26 | 27 | writer.WriteEndObject(); 28 | } 29 | } -------------------------------------------------------------------------------- /src/Verify.SqlServer/Converters/ParameterConverter.cs: -------------------------------------------------------------------------------- 1 | class ParameterConverter : 2 | WriteOnlyJsonConverter 3 | { 4 | static AsyncLocal omitName = new(); 5 | 6 | public static void OmitName() => 7 | omitName.Value = true; 8 | 9 | public static void ClearOmitName() => 10 | omitName.Value = false; 11 | 12 | public override void Write(VerifyJsonWriter writer, SqlParameter parameter) 13 | { 14 | writer.WriteStartObject(); 15 | if (!omitName.Value) 16 | { 17 | writer.WriteMember(parameter, parameter.ParameterName, "Name"); 18 | } 19 | writer.WriteMember(parameter, parameter.Value, "Value"); 20 | 21 | var (tempDbType, tempSqlDbType, tempSqlValue) = InferExpectedProperties(parameter); 22 | if (parameter.SqlValue != parameter.Value && 23 | !Equals(parameter.SqlValue, tempSqlValue)) 24 | { 25 | writer.WriteMember(parameter, parameter.SqlValue, "SqlValue"); 26 | } 27 | 28 | if (tempDbType != parameter.DbType) 29 | { 30 | writer.WriteMember(parameter, parameter.DbType, "DbType"); 31 | } 32 | 33 | if (tempSqlDbType != parameter.SqlDbType && 34 | parameter.SqlDbType != parameter.DbType.ToSqlDbType()) 35 | { 36 | writer.WriteMember(parameter, parameter.SqlDbType, "SqlDbType"); 37 | } 38 | 39 | if (parameter.Direction != ParameterDirection.Input) 40 | { 41 | writer.WriteMember(parameter, parameter.Direction, "Direction"); 42 | } 43 | 44 | if (parameter.Offset != 0) 45 | { 46 | writer.WriteMember(parameter, parameter.Offset, "Offset"); 47 | } 48 | 49 | if (parameter.Precision != 0) 50 | { 51 | writer.WriteMember(parameter, parameter.Precision, "Precision"); 52 | } 53 | 54 | if (parameter.Scale != 0) 55 | { 56 | writer.WriteMember(parameter, parameter.Scale, "Scale"); 57 | } 58 | 59 | if (parameter.Size != 0) 60 | { 61 | writer.WriteMember(parameter, parameter.Size, "Size"); 62 | } 63 | 64 | if (parameter.CompareInfo != SqlCompareOptions.None) 65 | { 66 | writer.WriteMember(parameter, parameter.CompareInfo, "CompareInfo"); 67 | } 68 | 69 | if (parameter.IsNullable) 70 | { 71 | writer.WriteMember(parameter, parameter.IsNullable, "IsNullable"); 72 | } 73 | 74 | if (parameter.LocaleId != 0) 75 | { 76 | writer.WriteMember(parameter, parameter.LocaleId, "LocaleId"); 77 | } 78 | 79 | if (parameter.SourceColumn != "") 80 | { 81 | writer.WriteMember(parameter, parameter.SourceColumn, "SourceColumn"); 82 | } 83 | 84 | if (parameter.SourceVersion != DataRowVersion.Current) 85 | { 86 | writer.WriteMember(parameter, parameter.SourceVersion, "SourceVersion"); 87 | } 88 | 89 | if (parameter.TypeName != "") 90 | { 91 | writer.WriteMember(parameter, parameter.TypeName, "TypeName"); 92 | } 93 | 94 | if (parameter.ForceColumnEncryption) 95 | { 96 | writer.WriteMember(parameter, parameter.ForceColumnEncryption, "ForceColumnEncryption"); 97 | } 98 | 99 | if (parameter.UdtTypeName != "") 100 | { 101 | writer.WriteMember(parameter, parameter.UdtTypeName, "UdtTypeName"); 102 | } 103 | 104 | if (parameter.SourceColumnNullMapping) 105 | { 106 | writer.WriteMember(parameter, parameter.SourceColumnNullMapping, "SourceColumnNullMapping"); 107 | } 108 | 109 | if (parameter.XmlSchemaCollectionDatabase != "") 110 | { 111 | writer.WriteMember(parameter, parameter.XmlSchemaCollectionDatabase, "XmlSchemaCollectionDatabase"); 112 | } 113 | 114 | if (parameter.XmlSchemaCollectionName != "") 115 | { 116 | writer.WriteMember(parameter, parameter.XmlSchemaCollectionName, "XmlSchemaCollectionName"); 117 | } 118 | 119 | if (parameter.XmlSchemaCollectionOwningSchema != "") 120 | { 121 | writer.WriteMember(parameter, parameter.XmlSchemaCollectionOwningSchema, "XmlSchemaCollectionOwningSchema"); 122 | } 123 | 124 | writer.WriteEndObject(); 125 | } 126 | 127 | internal static bool IsOnlyValue(SqlParameter parameter) 128 | { 129 | var (tempDbType, tempSqlDbType, tempSqlValue) = InferExpectedProperties(parameter); 130 | return (parameter.SqlValue == parameter.Value || 131 | Equals(parameter.SqlValue, tempSqlValue)) && 132 | tempDbType == parameter.DbType && 133 | (tempSqlDbType == parameter.SqlDbType || 134 | parameter.SqlDbType == parameter.DbType.ToSqlDbType()) && 135 | parameter is 136 | { 137 | Direction: ParameterDirection.Input, 138 | Offset: 0, 139 | Precision: 0, 140 | Scale: 0, 141 | Size: 0, 142 | CompareInfo: SqlCompareOptions.None, 143 | IsNullable: false, 144 | LocaleId: 0, 145 | SourceColumn: "", 146 | SourceVersion: DataRowVersion.Current, 147 | TypeName: "", 148 | ForceColumnEncryption: false, 149 | UdtTypeName: "", 150 | SourceColumnNullMapping: false, 151 | XmlSchemaCollectionDatabase: "", 152 | XmlSchemaCollectionName: "", 153 | XmlSchemaCollectionOwningSchema: "" 154 | }; 155 | } 156 | 157 | static (DbType? dbType, SqlDbType? sqlDbType, object? sqlValue) InferExpectedProperties(SqlParameter parameter) 158 | { 159 | if (parameter.Value == null) 160 | { 161 | return (null, null, null); 162 | } 163 | 164 | var temp = new SqlParameter("temp", parameter.Value); 165 | return (temp.DbType, temp.SqlDbType, temp.SqlValue); 166 | } 167 | } -------------------------------------------------------------------------------- /src/Verify.SqlServer/Extensions.cs: -------------------------------------------------------------------------------- 1 | static class Extensions 2 | { 3 | public static SqlDbType ToSqlDbType(this DbType dbType) => 4 | dbType switch 5 | { 6 | DbType.AnsiString => SqlDbType.VarChar, 7 | DbType.Binary => SqlDbType.Binary, 8 | DbType.Byte => SqlDbType.TinyInt, 9 | DbType.Boolean => SqlDbType.Bit, 10 | DbType.Currency => SqlDbType.Money, 11 | DbType.Date => SqlDbType.Date, 12 | DbType.DateTime => SqlDbType.DateTime, 13 | DbType.Decimal => SqlDbType.Decimal, 14 | DbType.Double => SqlDbType.Float, 15 | DbType.Guid => SqlDbType.UniqueIdentifier, 16 | DbType.Int16 => SqlDbType.SmallInt, 17 | DbType.Int32 => SqlDbType.Int, 18 | DbType.Int64 => SqlDbType.BigInt, 19 | DbType.Object => SqlDbType.Variant, 20 | DbType.String => SqlDbType.NVarChar, 21 | DbType.Time => SqlDbType.Time, 22 | DbType.AnsiStringFixedLength => SqlDbType.Char, 23 | DbType.StringFixedLength => SqlDbType.NChar, 24 | DbType.Xml => SqlDbType.Xml, 25 | DbType.DateTime2 => SqlDbType.DateTime2, 26 | DbType.DateTimeOffset => SqlDbType.DateTimeOffset, 27 | _ => throw new ArgumentOutOfRangeException(nameof(dbType), dbType, null) 28 | }; 29 | } -------------------------------------------------------------------------------- /src/Verify.SqlServer/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System.Data; 2 | global using System.Data.Common; 3 | global using System.Data.SqlTypes; 4 | global using System.Globalization; 5 | global using Microsoft.Data.SqlClient; 6 | global using Microsoft.Extensions.DiagnosticAdapter; 7 | global using Microsoft.SqlServer.Management.Common; 8 | global using Microsoft.SqlServer.TransactSql.ScriptDom; 9 | global using VerifyTests.SqlServer; -------------------------------------------------------------------------------- /src/Verify.SqlServer/Recording/ErrorEntry.cs: -------------------------------------------------------------------------------- 1 | namespace VerifyTests.SqlServer; 2 | 3 | public class ErrorEntry(DbCommand command, DbException exception) 4 | { 5 | public DbException Exception { get; } = exception; 6 | public DbCommand Command { get; } = command; 7 | } -------------------------------------------------------------------------------- /src/Verify.SqlServer/Recording/Listener.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CA1822 2 | // https://github.com/dotnet/SqlClient/blob/master/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlClientDiagnosticListenerExtensions.cs 3 | class Listener : 4 | IObserver, 5 | IDisposable 6 | { 7 | ConcurrentQueue subscriptions = []; 8 | 9 | public void OnNext(DiagnosticListener value) 10 | { 11 | if (value.Name != "SqlClientDiagnosticListener") 12 | { 13 | return; 14 | } 15 | 16 | subscriptions.Enqueue( 17 | value.SubscribeWithAdapter( 18 | this, 19 | _ => Recording.IsRecording())); 20 | } 21 | 22 | [DiagnosticName("Microsoft.Data.SqlClient.WriteCommandAfter")] 23 | public void OnMsCommandAfter(SqlCommand command) => 24 | Recording.TryAdd("sql", command.Clone()); 25 | 26 | [DiagnosticName("Microsoft.Data.SqlClient.WriteCommandError")] 27 | public void OnMsErrorExecuteCommand(SqlCommand command, SqlException exception) => 28 | Recording.TryAdd("sql", new ErrorEntry(command.Clone(), exception)); 29 | 30 | void Clear() 31 | { 32 | foreach (var subscription in subscriptions) 33 | { 34 | subscription.Dispose(); 35 | } 36 | } 37 | 38 | public void Dispose() => 39 | Clear(); 40 | 41 | public void OnCompleted() => 42 | Clear(); 43 | 44 | public void OnError(Exception error) 45 | { 46 | } 47 | } -------------------------------------------------------------------------------- /src/Verify.SqlServer/RemoveSquareBracketVisitor.cs: -------------------------------------------------------------------------------- 1 | class RemoveSquareBracketVisitor : TSqlFragmentVisitor 2 | { 3 | public override void Visit(Identifier node) 4 | { 5 | if (node.QuoteType == QuoteType.SquareBracket) 6 | { 7 | node.QuoteType = QuoteType.NotQuoted; 8 | } 9 | 10 | base.Visit(node); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Verify.SqlServer/SchemaValidation/DbObjects.cs: -------------------------------------------------------------------------------- 1 | namespace VerifyTests.SqlServer; 2 | 3 | [Flags] 4 | public enum DbObjects 5 | { 6 | StoredProcedures = 1, 7 | Synonyms = 2, 8 | Tables = 4, 9 | UserDefinedFunctions = 8, 10 | Views = 16, 11 | All = StoredProcedures | Synonyms | Tables | UserDefinedFunctions | Views 12 | } -------------------------------------------------------------------------------- /src/Verify.SqlServer/SchemaValidation/Format.cs: -------------------------------------------------------------------------------- 1 | enum Format 2 | { 3 | Md, 4 | Sql, 5 | } -------------------------------------------------------------------------------- /src/Verify.SqlServer/SchemaValidation/Obsoletes.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable UnusedParameter.Global 2 | 3 | namespace VerifyTests; 4 | 5 | public static partial class VerifySettingsSqlExtensions 6 | { 7 | [Obsolete($"Use {nameof(SchemaIncludes)} or {nameof(SchemaFilter)}", true)] 8 | public static SettingsTask SchemaSettings( 9 | this SettingsTask settings, 10 | bool storedProcedures = true, 11 | bool tables = true, 12 | bool views = true, 13 | bool userDefinedFunctions = true, 14 | bool synonyms = true, 15 | Func? includeItem = null) => 16 | throw new(); 17 | 18 | [Obsolete($"Use {nameof(SchemaIncludes)} or {nameof(SchemaFilter)}", true)] 19 | public static void SchemaSettings( 20 | this VerifySettings settings, 21 | bool storedProcedures = true, 22 | bool tables = true, 23 | bool views = true, 24 | bool userDefinedFunctions = true, 25 | bool synonyms = true, 26 | Func? includeItem = null) 27 | { 28 | } 29 | } -------------------------------------------------------------------------------- /src/Verify.SqlServer/SchemaValidation/SchemaSettings.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.SqlServer.Management.Smo; 2 | 3 | class SchemaSettings 4 | { 5 | public DbObjects Includes { get; set; } = DbObjects.All; 6 | public Format Format { get; set; } = Format.Md; 7 | public bool IsMd => Format == Format.Md; 8 | public Func IncludeItem { get; set; } = _ => true; 9 | } -------------------------------------------------------------------------------- /src/Verify.SqlServer/SchemaValidation/SqlScriptBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.SqlServer.Management.Smo; 2 | 3 | class SqlScriptBuilder(SchemaSettings settings) 4 | { 5 | static Dictionary tableSettingsToScrubLookup; 6 | 7 | static SqlScriptBuilder() 8 | { 9 | string[] defaultsToScrub = 10 | [ 11 | "PAD_INDEX = OFF", 12 | "STATISTICS_NORECOMPUTE = OFF", 13 | "SORT_IN_TEMPDB = OFF", 14 | "DROP_EXISTING = OFF", 15 | "ONLINE = OFF", 16 | "ALLOW_ROW_LOCKS = ON", 17 | "IGNORE_DUP_KEY = OFF", 18 | "ALLOW_PAGE_LOCKS = ON", 19 | "OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF", 20 | "SORT_IN_TEMPDB = OFF" 21 | ]; 22 | tableSettingsToScrubLookup = []; 23 | foreach (var toScrub in defaultsToScrub) 24 | { 25 | tableSettingsToScrubLookup[$"({toScrub}, "] = "("; 26 | tableSettingsToScrubLookup[$"({toScrub})"] = "()"; 27 | tableSettingsToScrubLookup[$", {toScrub}"] = ""; 28 | } 29 | } 30 | 31 | public string BuildContent(SqlConnection connection) 32 | { 33 | var builder = new SqlConnectionStringBuilder(connection.ConnectionString); 34 | var server = new Server(new ServerConnection(connection)); 35 | 36 | return BuildContent(server, builder); 37 | } 38 | 39 | string BuildContent(Server server, SqlConnectionStringBuilder builder) 40 | { 41 | server.SetDefaultInitFields(typeof(Table), "Name", "IsSystemObject"); 42 | server.SetDefaultInitFields(typeof(View), "Name", "IsSystemObject"); 43 | server.SetDefaultInitFields(typeof(StoredProcedure), "Name", "IsSystemObject"); 44 | server.SetDefaultInitFields(typeof(UserDefinedFunction), "Name", "IsSystemObject"); 45 | server.SetDefaultInitFields(typeof(Trigger), "Name", "IsSystemObject"); 46 | server.SetDefaultInitFields(typeof(Synonym), "Name"); 47 | var database = server.Databases[builder.InitialCatalog]; 48 | database.Tables.Refresh(); 49 | 50 | return GetScriptingObjects(database); 51 | } 52 | 53 | string GetScriptingObjects(Database database) 54 | { 55 | var options = new ScriptingOptions 56 | { 57 | ChangeTracking = true, 58 | NoCollation = true, 59 | Triggers = true, 60 | Indexes = true, 61 | }; 62 | 63 | var builder = new StringBuilder(); 64 | 65 | if (settings.Includes.HasFlag(DbObjects.Tables)) 66 | { 67 | AppendType(builder, options, database.Tables, _ => _.IsSystemObject); 68 | ScrubTableSettings(builder); 69 | } 70 | 71 | if (settings.Includes.HasFlag(DbObjects.Views)) 72 | { 73 | AppendType(builder, options, database.Views, _ => _.IsSystemObject); 74 | } 75 | 76 | if (settings.Includes.HasFlag(DbObjects.StoredProcedures)) 77 | { 78 | AppendType(builder, options, database.StoredProcedures, _ => _.IsSystemObject); 79 | } 80 | 81 | if (settings.Includes.HasFlag(DbObjects.UserDefinedFunctions)) 82 | { 83 | AppendType(builder, options, database.UserDefinedFunctions, _ => _.IsSystemObject); 84 | } 85 | 86 | if (settings.Includes.HasFlag(DbObjects.Synonyms)) 87 | { 88 | AppendType(builder, options, database.Synonyms, _ => false); 89 | } 90 | 91 | var result = builder 92 | .ToString() 93 | .TrimEnd(); 94 | 95 | if (string.IsNullOrWhiteSpace(result)) 96 | { 97 | if (settings.IsMd) 98 | { 99 | return "## No matching items found"; 100 | } 101 | 102 | return "-- No matching items found"; 103 | } 104 | 105 | return result; 106 | } 107 | 108 | static void ScrubTableSettings(StringBuilder builder) 109 | { 110 | foreach (var toScrub in tableSettingsToScrubLookup) 111 | { 112 | builder.Replace(toScrub.Key, toScrub.Value); 113 | } 114 | 115 | builder.Replace(")WITH () ", ") "); 116 | } 117 | 118 | void AppendType(StringBuilder builder, ScriptingOptions options, SmoCollectionBase items, Func isSystem) 119 | where T : NamedSmoObject, IScriptable 120 | { 121 | var filtered = items 122 | .Cast() 123 | .Where(_ => !isSystem(_) && settings.IncludeItem(_)) 124 | .ToList(); 125 | if (filtered.Count == 0) 126 | { 127 | return; 128 | } 129 | 130 | if (settings.IsMd) 131 | { 132 | builder.AppendLineN($"## {typeof(T).Name}s"); 133 | } 134 | else 135 | { 136 | builder.AppendLineN($"-- {typeof(T).Name}s"); 137 | } 138 | builder.AppendLineN(); 139 | 140 | foreach (var item in filtered) 141 | { 142 | AddItem(builder, options, item); 143 | } 144 | } 145 | 146 | void AddItem(StringBuilder builder, ScriptingOptions options, T item) 147 | where T : NamedSmoObject, IScriptable 148 | { 149 | var lines = ScriptLines(options, item); 150 | if (settings.IsMd) 151 | { 152 | builder.AppendLineN($"### {item.Name}"); 153 | builder.AppendLineN(); 154 | builder.AppendLineN("```sql"); 155 | AppendLines(builder, lines); 156 | builder.AppendLineN("```"); 157 | } 158 | else 159 | { 160 | AppendLines(builder, lines); 161 | } 162 | 163 | builder.AppendLineN(); 164 | } 165 | 166 | static void AppendLines(StringBuilder builder, List lines) 167 | { 168 | if (lines.Count == 1) 169 | { 170 | builder.AppendLineN( 171 | lines[0] 172 | .AsSpan() 173 | .Trim()); 174 | return; 175 | } 176 | 177 | for (var index = 0; index < lines.Count; index++) 178 | { 179 | var line = lines[index]; 180 | var span = line.AsSpan(); 181 | if (index == 0) 182 | { 183 | builder.AppendLineN(span.TrimStart()); 184 | continue; 185 | } 186 | 187 | if (index == lines.Count - 1) 188 | { 189 | builder.AppendLineN(span.TrimEnd()); 190 | continue; 191 | } 192 | 193 | builder.AppendLineN(span); 194 | } 195 | } 196 | 197 | static List ScriptLines(ScriptingOptions options, T item) 198 | where T : NamedSmoObject, IScriptable => 199 | item 200 | .Script(options) 201 | .Cast() 202 | .Where(_ => !IsSet(_)) 203 | .ToList(); 204 | 205 | static bool IsSet(string script) => 206 | script is 207 | "SET ANSI_NULLS ON" or 208 | "SET QUOTED_IDENTIFIER ON"; 209 | } -------------------------------------------------------------------------------- /src/Verify.SqlServer/SchemaValidation/VerifySettingsExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.SqlServer.Management.Smo; 2 | 3 | namespace VerifyTests; 4 | 5 | public static partial class VerifySettingsSqlExtensions 6 | { 7 | public static SettingsTask SchemaIncludes( 8 | this SettingsTask settings, 9 | DbObjects includes) 10 | { 11 | settings.CurrentSettings.SchemaIncludes(includes); 12 | return settings; 13 | } 14 | 15 | public static void SchemaIncludes( 16 | this VerifySettings settings, 17 | DbObjects includes) => 18 | GetOrAddSettings(settings) 19 | .Includes = includes; 20 | 21 | public static SettingsTask SchemaFilter( 22 | this SettingsTask settings, 23 | Func filter) 24 | { 25 | settings.CurrentSettings.SchemaFilter(filter); 26 | return settings; 27 | } 28 | 29 | public static void SchemaFilter( 30 | this VerifySettings settings, 31 | Func filter) => 32 | GetOrAddSettings(settings) 33 | .IncludeItem = filter; 34 | 35 | public static SettingsTask SchemaAsMarkdown( 36 | this SettingsTask settings) 37 | { 38 | settings.CurrentSettings.SchemaAsMarkdown(); 39 | return settings; 40 | } 41 | 42 | public static void SchemaAsMarkdown( 43 | this VerifySettings settings) => 44 | GetOrAddSettings(settings) 45 | .Format = Format.Md; 46 | 47 | public static SettingsTask SchemaAsSql( 48 | this SettingsTask settings) 49 | { 50 | settings.CurrentSettings.SchemaAsSql(); 51 | return settings; 52 | } 53 | 54 | public static void SchemaAsSql( 55 | this VerifySettings settings) => 56 | GetOrAddSettings(settings) 57 | .Format = Format.Sql; 58 | 59 | static SchemaSettings GetOrAddSettings(VerifySettings settings) 60 | { 61 | var context = settings.Context; 62 | if (context.TryGetValue("SqlServer", out var value)) 63 | { 64 | return (SchemaSettings) value; 65 | } 66 | 67 | var schemaSettings = new SchemaSettings(); 68 | context["SqlServer"] = schemaSettings; 69 | return schemaSettings; 70 | } 71 | 72 | internal static SchemaSettings GetSchemaSettings(this IReadOnlyDictionary context) 73 | { 74 | if (context.TryGetValue("SqlServer", out var value)) 75 | { 76 | return (SchemaSettings) value; 77 | } 78 | 79 | return defaultSettings; 80 | } 81 | 82 | static SchemaSettings defaultSettings = new(); 83 | } -------------------------------------------------------------------------------- /src/Verify.SqlServer/SqlFormatter.cs: -------------------------------------------------------------------------------- 1 | static class SqlFormatter 2 | { 3 | public static StringBuilder Format(string input) 4 | { 5 | var parser = new TSql170Parser(false); 6 | using var reader = new StringReader(input); 7 | var fragment = parser.Parse(reader, out var errors); 8 | 9 | if (errors.Count > 0) 10 | { 11 | throw new( 12 | $""" 13 | Failed to parse sql. 14 | 15 | Errors: 16 | {string.Join(Environment.NewLine, errors.Select(_ => _.Message))} 17 | 18 | Sql input: 19 | {input} 20 | """); 21 | } 22 | 23 | var visitor = new RemoveSquareBracketVisitor(); 24 | fragment.Accept(visitor); 25 | 26 | var generator = new Sql170ScriptGenerator( 27 | new() 28 | { 29 | SqlVersion = SqlVersion.Sql170, 30 | KeywordCasing = KeywordCasing.Lowercase, 31 | IndentationSize = 2, 32 | AlignClauseBodies = true 33 | }); 34 | 35 | var builder = new StringBuilder(); 36 | using (var writer = new StringWriter(builder, CultureInfo.InvariantCulture)) 37 | { 38 | generator.GenerateScript(fragment, writer); 39 | } 40 | 41 | builder.TrimEnd(); 42 | // ReSharper disable once UseIndexFromEndExpression 43 | if (builder[builder.Length -1] == ';') 44 | { 45 | builder.Length--; 46 | } 47 | 48 | return builder; 49 | } 50 | } -------------------------------------------------------------------------------- /src/Verify.SqlServer/Verify.SqlServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net48;net8.0;net9.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Verify.SqlServer/VerifySqlServer.cs: -------------------------------------------------------------------------------- 1 | namespace VerifyTests; 2 | 3 | public static class VerifySqlServer 4 | { 5 | static Listener listener = new(); 6 | 7 | public static bool Initialized { get; private set; } 8 | 9 | public static void Initialize() 10 | { 11 | if (Initialized) 12 | { 13 | throw new("Already Initialized"); 14 | } 15 | 16 | Initialized = true; 17 | 18 | InnerVerifier.ThrowIfVerifyHasBeenRun(); 19 | VerifierSettings.AddExtraSettings(settings => 20 | { 21 | var converters = settings.Converters; 22 | converters.Add(new ErrorConverter()); 23 | converters.Add(new ConnectionConverter()); 24 | converters.Add(new CommandConverter()); 25 | converters.Add(new ExceptionConverter()); 26 | converters.Add(new ParameterConverter()); 27 | converters.Add(new ParameterCollectionConverter()); 28 | }); 29 | 30 | VerifierSettings.RegisterFileConverter(ToSql); 31 | // ReSharper disable once UnusedVariable 32 | var subscription = DiagnosticListener.AllListeners.Subscribe(listener); 33 | } 34 | 35 | static ConversionResult ToSql(SqlConnection connection, IReadOnlyDictionary context) 36 | { 37 | var settings = context.GetSchemaSettings(); 38 | var builder = new SqlScriptBuilder(settings); 39 | var content = builder.BuildContent(connection); 40 | var extension = GetExtension(settings); 41 | return new(null, extension, content); 42 | } 43 | 44 | static string GetExtension(SchemaSettings settings) 45 | { 46 | var format = settings.Format; 47 | return format switch 48 | { 49 | Format.Md => "md", 50 | Format.Sql => "sql", 51 | _ => throw new ArgumentOutOfRangeException(nameof(format), format, null) 52 | }; 53 | } 54 | } -------------------------------------------------------------------------------- /src/appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2022 2 | environment: 3 | DOTNET_NOLOGO: true 4 | DOTNET_CLI_TELEMETRY_OPTOUT: true 5 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 6 | build_script: 7 | - pwsh: | 8 | Invoke-WebRequest "https://dot.net/v1/dotnet-install.ps1" -OutFile "./dotnet-install.ps1" 9 | ./dotnet-install.ps1 -JSonFile src/global.json -Architecture x64 -InstallDir 'C:\Program Files\dotnet' 10 | - dotnet build src --configuration Release 11 | - dotnet test src --configuration Release --no-build --no-restore 12 | test: off 13 | artifacts: 14 | - path: nugets\**\*.nupkg -------------------------------------------------------------------------------- /src/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.301", 4 | "allowPrerelease": true, 5 | "rollForward": "latestFeature" 6 | } 7 | } -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VerifyTests/Verify.SqlServer/72cfc602c4a86887ed1df21f8b3bbaf56ea84e17/src/icon.png -------------------------------------------------------------------------------- /src/key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VerifyTests/Verify.SqlServer/72cfc602c4a86887ed1df21f8b3bbaf56ea84e17/src/key.snk -------------------------------------------------------------------------------- /src/mdsnippets.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/schema.json", 3 | "TocExcludes": [ "NuGet package", "Release Notes", "Icon" ], 4 | "MaxWidth": 80, 5 | "ValidateContent": true, 6 | "Convention": "InPlaceOverwrite" 7 | } -------------------------------------------------------------------------------- /src/nuget.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 20 | 21 | 24 | 25 | 26 | --------------------------------------------------------------------------------