├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── actions │ ├── mssql-sample-db │ │ ├── action.yml │ │ └── main.ps1 │ └── sqlcover │ │ ├── action.yml │ │ └── main.ps1 ├── codecov.yml ├── dependabot.yml ├── in-solidarity.yml └── workflows │ ├── artifacts.yml │ ├── azuresql-unit.yml │ ├── lint.yml │ └── sqlserver-unit.yml ├── .vscode └── settings.json ├── ADOPTERS.md ├── LICENSE ├── SECURITY.md ├── docs ├── README.md └── assets │ └── dba-multitool-logo.png ├── install_dba-multitool.sql ├── sp_doc.sql ├── sp_estindex.sql ├── sp_help_revlogin.sql ├── sp_helpme.sql ├── sp_sizeoptimiser.sql ├── tests ├── README.md ├── sp_doc.Tests.ps1 ├── sp_doc.Tests.sql ├── sp_estindex.Tests.ps1 ├── sp_estindex.Tests.sql ├── sp_helpme.Tests.ps1 ├── sp_helpme.Tests.sql ├── sp_sizeoptimiser.Tests.ps1 └── sp_sizeoptimiser.Tests.sql └── uninstall_dba-multitool.sql /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at john@lowlydba.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 71 | [version 1.4](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html). 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to DBA MultiTool 2 | 3 | :wave: Hi there! Thanks for using and contributing to the DBA MultiTool! 4 | 5 | Below are guidelines and helpful approaches for participating in the project. 6 | 7 | * [How to Help](#how-to-help) 8 | * [Testing](#testing-locally) 9 | * [Style Guide](#style-guide) 10 | 11 | ## How to Help 12 | 13 | You can help contribute by: 14 | 15 | * Opening a feature request 16 | * Opening a :bug: 17 | * Increasing unit test coverage 18 | * Making a pull request to address any of the above to the `development` branch 19 | 20 | ## Testing Locally 21 | 22 | See the testing readme in the [tests directory README](../tests/README.md) 23 | 24 | ## Style Guide 25 | 26 | Styles (or lack thereof) that are particular to this project. 27 | Think :tshirt:, not :necktie: 28 | 29 | ### T-SQL 30 | 31 | T-SQL is linted against this [configuration](../appveyor/tsqllint) 32 | of TSQLLint via a Pester test. 33 | 34 | ### Markdown 35 | 36 | All markdown, whether manually or automatically generated, should adhere to standard 37 | markdown rules. This project utilize's David Anson's [markdown lint][mdlint] 38 | with a slightly customized 39 | [configuration][mdconfig]. 40 | 41 | You can use our config and markdown lint plugins in your IDE of choice, or just wait 42 | for your commits to be automatically linted using Github Actions. 43 | 44 | ### PowerShell 45 | 46 | PowerShell is only used in the automation piece of this project, 47 | but [PSScriptAnalyzer](https://github.com/PowerShell/PSScriptAnalyzer) 48 | is used to lint it for general best practice adherement. 49 | 50 | [mdconfig]: https://github.com/LowlyDBA/dba-multitool/blob/master/.github/linters/.markdown-lint.yml 51 | [mdlint]: https://github.com/DavidAnson/markdownlint 52 | [tsqlt]: https://tsqlt.org/ 53 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # lowlydba # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | buy_me_a_coffee: johnmcc 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Versions(please complete the following information):** 20 | 21 | * OS: [e.g. iOS] 22 | * SQL Server: [e.g. 2017, 2019] 23 | * SSMS: 24 | 25 | **Additional context** 26 | Add any other context about the problem here. 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Request]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ## Description 5 | 6 | 7 | 8 | 9 | **Issue:** Fixes 10 | 11 | ## How Has This Been Tested? 12 | 13 | 14 | ## Types of changes 15 | 16 | - [ ] Bug fix (non-breaking change which fixes an issue) 17 | - [ ] New feature (non-breaking change which adds functionality) 18 | 19 | ## Checklist: 20 | 21 | 22 | - [ ] I have updated the Version number of any scripts modified. 23 | - [ ] I have updated the documentation accordingly. 24 | - [ ] I have read the [**CONTRIBUTING**](https://github.com/LowlyDBA/dba-multitool/blob/master/.github/CONTRIBUTING.md) document. 25 | - [ ] I have added tests to cover my changes. 26 | - [ ] All new and existing tests passed. 27 | -------------------------------------------------------------------------------- /.github/actions/mssql-sample-db/action.yml: -------------------------------------------------------------------------------- 1 | name: "MSSQL Sample Database" 2 | author: "lowlydba" 3 | branding: 4 | icon: "database" 5 | color: "red" 6 | description: "Restores a sample database to an instance." 7 | inputs: 8 | sql-instance: 9 | description: "Target SQL instance." 10 | required: false 11 | default: "localhost" 12 | database: 13 | description: "Sample database to download and restore." 14 | required: false 15 | default: "WideWorldImporters" 16 | runs: 17 | using: "composite" 18 | steps: 19 | - shell: pwsh 20 | id: "mssql-sample-database" 21 | run: | 22 | $params = @{ 23 | Database = "${{ inputs.database }}" 24 | SqlInstance = "${{ inputs.sql-instance }}" 25 | } 26 | 27 | ${{ github.action_path }}\main.ps1 @params 28 | -------------------------------------------------------------------------------- /.github/actions/mssql-sample-db/main.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [Parameter(Mandatory = $true)] 3 | [ValidateSet("WideWorldImporters")] 4 | [string]$Database, 5 | [Parameter(Mandatory = $true)] 6 | [string]$SqlInstance 7 | ) 8 | 9 | if ($Env:RUNNER_OS -ne "Windows") { 10 | Write-Error "This action only supported on Windows runners." -ErrorAction "Stop" 11 | } 12 | 13 | if ($Database -eq "WideWorldImporters") { 14 | $BackupPath = Join-Path -Path $Env:RUNNER_TEMP -ChildPath "$Database.bak" 15 | $Uri = "https://github.com/Microsoft/sql-server-samples/releases/download/wide-world-importers-v1.0/WideWorldImporters-Full.bak" 16 | $Documentation = "https://docs.microsoft.com/en-us/sql/samples/wide-world-importers-what-is" 17 | } 18 | 19 | Write-Output "Thanks for using MSSQL Sample Database!" 20 | Write-Output "📃: $Documentation" 21 | Write-Output "-----" 22 | Write-Output "Downloading '$Database' to '$BackupPath' ..." 23 | Invoke-WebRequest -Uri $Uri -OutFile $BackupPath 24 | Write-Output "Restoring '$Database' ..." 25 | Restore-SqlDatabase -ServerInstance $SqlInstance -Database $Database -BackupFile $BackupPath -AutoRelocateFile -Verbose 26 | #Get-DbaDatabase -SqlInstance $SqlInstance -Database $Database | Select-Object -Property Name, Status, SizeMB, SqlInstance 27 | -------------------------------------------------------------------------------- /.github/actions/sqlcover/action.yml: -------------------------------------------------------------------------------- 1 | name: "SQLCover" 2 | author: "lowlydba" 3 | branding: 4 | icon: "database" 5 | color: "purple" 6 | description: "Installs, starts, and stops SQLCover by GoEddie." 7 | inputs: 8 | sql-instance: 9 | description: "Target SQL instance." 10 | required: false 11 | default: "localhost" 12 | database: 13 | description: "Target database." 14 | required: false 15 | default: "master" 16 | output-file: 17 | description: "File to output results to." 18 | required: false 19 | default: "cobertura.xml" 20 | runs: 21 | using: "composite" 22 | steps: 23 | - id: output 24 | shell: pwsh 25 | run: | 26 | $params = @{ 27 | Database = "${{ inputs.database }}" 28 | SqlInstance = "${{ inputs.sql-instance }}" 29 | OutputFile = "${{ inputs.output-file }}" 30 | } 31 | 32 | ${{github.action_path}}\main.ps1 @params 33 | -------------------------------------------------------------------------------- /.github/actions/sqlcover/main.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [Parameter(Mandatory = $true)] 3 | [string]$Database, 4 | [Parameter(Mandatory = $true)] 5 | [string]$SqlInstance, 6 | [string]$Package = "GOEddie.SQLCover", 7 | [Parameter(Mandatory = $true)] 8 | [string]$OutputFile 9 | ) 10 | 11 | if ($Env:RUNNER_OS -ne "Windows") { 12 | Write-Error "This action only supported on Windows runners." -ErrorAction "Stop" 13 | } 14 | 15 | # Install SQLCover 16 | Write-Output "Installing SQLCover." 17 | $null = Install-Package $Package -Force -Scope "CurrentUser" 18 | $NugetPath = (Get-Package $Package).Source | Convert-Path 19 | $SQLCoverRoot = Split-Path $NugetPath 20 | $SQLCoverDllPath = Join-Path $SQLCoverRoot "lib\SQLCover.dll" 21 | Add-Type -Path $SQLCoverDllPath 22 | 23 | $connString = "server=$SqlInstance;Trusted_Connection=yes" 24 | $sqlCover = New-Object SQLCover.CodeCoverage($connString, $Database) 25 | $null = $sqlCover.Start() 26 | 27 | # Run tests 28 | Invoke-Pester -Path ".\tests\*" 29 | 30 | # Stop 31 | Write-Output "Stopping SQL Cover." 32 | $coverageResults = $sqlCover.Stop() 33 | 34 | # Save results 35 | [xml]$coberturaXml = $coverageResults.Cobertura() 36 | $OutputFullPath = Join-Path -Path "." -ChildPath $OutputFile 37 | Write-Output "Saving Cobertura report to $OutputFullPath" 38 | 39 | # Fix missing filename with best-effort value 40 | # https://github.com/GoEddie/SQLCover/issues/79 41 | foreach ($class in $coberturaXml.coverage.packages.package.classes.class) { 42 | $class.filename = $class.Name 43 | } 44 | $coberturaXml.Save($OutputFullPath) 45 | 46 | # HTML artifact 47 | $HtmlFullPath = Join-Path -Path "." -ChildPath "coverage.html" 48 | Write-Output "Saving HTML report to $HtmlFullPath" 49 | Set-Content -Path $HtmlFullPath -Value $coverageResults.Html2() -Force 50 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: 70..100 3 | round: down 4 | precision: 2 5 | status: 6 | project: 7 | default: 8 | # basic 9 | target: auto 10 | threshold: 1% 11 | patch: off 12 | 13 | fixes: 14 | - "::" -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Set update schedule for GitHub Actions 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | # Check for updates to GitHub Actions every weekday 9 | interval: "daily" 10 | -------------------------------------------------------------------------------- /.github/in-solidarity.yml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | # https://github.com/jpoehnelt/in-solidarity-bot 4 | #rules: 5 | # master: 6 | # level: off 7 | ignore: 8 | - ".github/in-solidarity.yml" # default 9 | - "**/*.yml" 10 | -------------------------------------------------------------------------------- /.github/workflows/artifacts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Generate Scripts 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | paths-ignore: 9 | - .vscode/* 10 | pull_request: 11 | paths-ignore: 12 | - .vscode/* 13 | 14 | # Cancel existing runs on new commits to a branch 15 | concurrency: 16 | group: ${{ github.workflow }}-${{ github.ref }} 17 | cancel-in-progress: true 18 | 19 | env: 20 | INSTALLER_FILE: "install_dba-multitool.sql" 21 | 22 | jobs: 23 | bundle: 24 | runs-on: ubuntu-latest 25 | name: Generate installer 26 | defaults: 27 | run: 28 | shell: pwsh 29 | 30 | steps: 31 | - name: Check out code 32 | uses: actions/checkout@v4.2.2 33 | with: 34 | repository: ${{ github.event.pull_request.head.repo.full_name }} 35 | ref: ${{ github.event.pull_request.head.ref }} 36 | 37 | - name: Create bundled installer script 38 | run: | 39 | Get-ChildItem -Path $Env:GITHUB_WORKSPACE -Filter "sp_*.sql" | Get-Content | Out-File $Env:INSTALLER_FILE -Encoding ascii 40 | Get-ChildItem -Path $Env:GITHUB_WORKSPACE -Filter $Env:INSTALLER_FILE | Select-Object -Property Name, Size, LastWriteTime, User 41 | 42 | - name: Commit bundled installer script 43 | uses: stefanzweifel/git-auto-commit-action@v5.2.0 44 | with: 45 | commit_message: Updated bundled installer [skip ci] 46 | file_pattern: ${{ env.INSTALLER_FILE }} 47 | -------------------------------------------------------------------------------- /.github/workflows/azuresql-unit.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Unit Test (AzureSQL) 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | paths-ignore: 9 | - .vscode/* 10 | pull_request: 11 | paths-ignore: 12 | - .vscode/* 13 | 14 | # Run CI once per day (at 06:00 UTC) 15 | schedule: 16 | - cron: "0 6 * * *" 17 | 18 | # Cancel existing runs on new commits to a branch 19 | concurrency: 20 | group: ${{ github.workflow }}-${{ github.ref }} 21 | cancel-in-progress: true 22 | 23 | env: 24 | SQLINSTANCE: "expresssql.database.windows.net" 25 | DATABASE: "expresssql" 26 | INSTALLER_FILE: "install_dba-multitool.sql" 27 | TSQLT_FILE: "run_tsqlt.sql" 28 | 29 | jobs: 30 | integration: 31 | runs-on: ubuntu-latest 32 | name: azuresql 33 | defaults: 34 | run: 35 | shell: pwsh 36 | 37 | steps: 38 | - name: Check out code 39 | uses: actions/checkout@v4.2.2 40 | with: 41 | path: "" 42 | 43 | - name: Create installer script 44 | run: | 45 | Get-ChildItem -Path $Env:GITHUB_WORKSPACE -Filter "sp_*.sql" | Get-Content | Out-File $Env:INSTALLER_FILE -Encoding ascii 46 | Get-ChildItem -Path $Env:GITHUB_WORKSPACE -Filter $Env:INSTALLER_FILE 47 | 48 | - name: Create tsqlt runner script 49 | run: | 50 | New-Item -Path $Env:TSQLT_FILE -ItemType "File" -Value "EXEC tsqlt.RunAll;" -Force 51 | 52 | - name: Install tSQLt 53 | uses: lowlydba/tsqlt-installer@v1.2.1 54 | with: 55 | sql-instance: ${{ env.SQLINSTANCE }} 56 | database: ${{ env.DATABASE }} 57 | user: ${{ secrets.AZURE_USERNAME }} 58 | password: ${{ secrets.AZURE_PASSWORD}} 59 | version: "latest" 60 | update: true 61 | 62 | - name: Install multitool 63 | uses: azure/sql-action@v2 64 | with: 65 | connection-string: ${{ secrets.AZURE_SQL_CONNECTION_STRING }} 66 | path: ${{ env.INSTALLER_FILE }} 67 | 68 | - name: Run tSQLt tests 69 | uses: azure/sql-action@v2 70 | with: 71 | connection-string: ${{ secrets.AZURE_SQL_CONNECTION_STRING }} 72 | path: ${{ env.TSQLT_FILE }} 73 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://help.github.com/en/articles/workflow-syntax-for-github-actions 3 | 4 | name: Lint Code 5 | on: 6 | workflow_dispatch: 7 | pull_request: 8 | paths-ignore: 9 | - install_dba-multitool.sql 10 | 11 | jobs: 12 | build: 13 | name: Lint Code 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout Code 18 | uses: actions/checkout@v4.2.2 19 | with: 20 | # Full git history is needed to get a proper list of changed files within `super-linter` 21 | fetch-depth: 0 22 | 23 | - name: Super Linter 24 | uses: github/super-linter@v7 25 | env: 26 | LINTER_RULES_PATH: .github/linter-conf 27 | VALIDATE_MARKDOWN: true 28 | VALIDATE_POWERSHELL: true 29 | FILTER_REGEX_EXCLUDE: /github/workspace/* 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | 32 | - name: TSQLLint 33 | uses: lowlydba/tsqllint-action@main 34 | with: 35 | path: "*.sql" 36 | config: "./.github/linter-conf/.tsqllintrc_150" 37 | comment: true 38 | only-changed-files: true 39 | -------------------------------------------------------------------------------- /.github/workflows/sqlserver-unit.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Unit Test (Win SQL Server) 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | paths-ignore: 9 | - .vscode/* 10 | - "**.md" 11 | pull_request: 12 | paths-ignore: 13 | - .vscode/* 14 | - "**.md" 15 | 16 | # Run CI once per day (at 06:00 UTC) 17 | schedule: 18 | - cron: "0 6 * * *" 19 | 20 | # Cancel existing runs on new commits to a branch 21 | concurrency: 22 | group: ${{ github.workflow }}-${{ github.ref }} 23 | cancel-in-progress: true 24 | 25 | env: 26 | SQLINSTANCE: "localhost" 27 | DATABASE: "tsqlt" 28 | COVERAGE_HTML_FILE: "coverage.html" 29 | COBERTURA_FILE: "cobertura.xml" 30 | SAMPLE_DATABASE: "WideWorldImporters" 31 | 32 | jobs: 33 | integration: 34 | runs-on: ${{ matrix.os }} 35 | name: sqlserver-${{ matrix.sql_server }} x ${{ matrix.os }} 36 | defaults: 37 | run: 38 | shell: pwsh 39 | strategy: 40 | fail-fast: false 41 | matrix: 42 | os: 43 | - windows-latest 44 | sql_server: 45 | - 2022 46 | - 2019 47 | 48 | steps: 49 | - name: Check out code 50 | uses: actions/checkout@v4.2.2 51 | with: 52 | path: "" 53 | ref: ${{ github.head_ref }} 54 | 55 | - name: Install SQL Server 56 | continue-on-error: true 57 | id: mssqlsuite 58 | uses: potatoqualitee/mssqlsuite@v1.8 59 | with: 60 | install: sqlengine 61 | sa-password: L0wlydb4 62 | version: ${{ matrix.sql_server }} 63 | 64 | - name: Retry SQL Server install 65 | if: steps.mssqlsuite.outcome == 'failure' 66 | uses: potatoqualitee/mssqlsuite@v1.8 67 | with: 68 | install: sqlengine 69 | sa-password: L0wlydb4 70 | 71 | - name: Install tSQLt 72 | uses: lowlydba/tsqlt-installer@v1.2.1 73 | with: 74 | sql-instance: ${{ env.SQLINSTANCE }} 75 | database: ${{ env.DATABASE }} 76 | version: "latest" 77 | create-database: true 78 | 79 | - name: Update SqlServer Module 80 | run: | 81 | Install-Module SqlServer -AllowClobber -AllowPreRelease -Force 82 | 83 | - name: Install multitool 84 | run: | 85 | foreach ($script in (Get-ChildItem -Path "." -Filter "sp_*.sql").Name) { 86 | Invoke-Sqlcmd -InputFile $script -ConnectionString "Data Source=$Env:SQLINSTANCE;Initial Catalog=$Env:DATABASE;Integrated Security=True;TrustServerCertificate=true" 87 | } 88 | 89 | - name: Run Pester tests with SQLCover 90 | uses: ./.github\actions\sqlcover 91 | with: 92 | sql-instance: ${{ env.SQLINSTANCE }} 93 | database: ${{ env.DATABASE }} 94 | 95 | - name: Restore sample database 96 | uses: ./.github\actions\mssql-sample-db 97 | with: 98 | sql-instance: ${{ env.SQLINSTANCE }} 99 | database: ${{ env.SAMPLE_DATABASE }} 100 | 101 | - name: Generate sp_doc sample 102 | env: 103 | SQL_VERSION: ${{ matrix.sql_server }} 104 | run: | 105 | Write-Output "Generating '$Env:SAMPLE_DATABASE' markdown sample." 106 | $Query = "EXEC sp_doc @DatabaseName = '$Env:SAMPLE_DATABASE';" 107 | Invoke-SqlCmd -Query $Query -As DataRows -ConnectionString "Data Source=$Env:SQLINSTANCE;Initial Catalog=$Env:DATABASE;Integrated Security=True;TrustServerCertificate=true" | Select-Object -ExpandProperty 'value' | Out-File "$($Env:SAMPLE_DATABASE)-$($Env:SQL_VERSION).md" 108 | 109 | - name: Upload sp_doc sample artifact 110 | uses: actions/upload-artifact@v4 111 | with: 112 | name: sp_doc-sample-${{ matrix.sql_server }} 113 | path: "${{ env.SAMPLE_DATABASE }}-${{ matrix.sql_server }}.md" 114 | 115 | # Only do cov report on latest SQL Server version 116 | - name: Produce the coverage report 117 | uses: insightsengineering/coverage-action@v2.7.2 118 | id: cov-report 119 | if: ${{ matrix.sql_server == 2022 }} 120 | with: 121 | path: ${{ env.COBERTURA_FILE }} 122 | threshold: 90 123 | fail: false 124 | publish: true 125 | 126 | - name: Upload HTML coverage artifact 127 | uses: actions/upload-artifact@v4 128 | if: steps.cov-report.outcome == 'success' 129 | with: 130 | name: html-coverage 131 | path: ${{ env.COVERAGE_HTML_FILE }} 132 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "markdownlint.config": { 3 | "MD024": false, 4 | "MD010": { 5 | "code_blocks": false 6 | }, 7 | "MD033": { 8 | "allowed_elements": ["details", "summary", "img", "sub", "br"] 9 | }, 10 | "MD013": { 11 | "code_blocks": false, 12 | "tables": false 13 | } 14 | }, 15 | "powershell.codeFormatting.trimWhitespaceAroundPipe": true, 16 | "powershell.codeFormatting.useCorrectCasing": true, 17 | 18 | // The number of spaces a tab is equal to. 19 | "editor.tabSize": 4, 20 | 21 | // Insert spaces when pressing Tab. 22 | "editor.insertSpaces": true, 23 | 24 | // When opening a file, `editor.tabSize` and `editor.insertSpaces` will be detected based on the file contents. 25 | "editor.detectIndentation": true 26 | } -------------------------------------------------------------------------------- /ADOPTERS.md: -------------------------------------------------------------------------------- 1 | # Adopters 2 | 3 | 4 | 5 | 6 | This is a list of organizations that have spoken publicly about their adoption or 7 | production users that have added themselves: 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2023 John McCall 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 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | If you find a vulnerability, please open a bug issue for it. 6 | 7 | Thanks! 8 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # DBA MultiTool 2 | 3 | [![License](https://img.shields.io/github/license/LowlyDBA/dba-multitool?color=blue)][license] 4 | ![Code Coverage](https://raw.githubusercontent.com/lowlydba/dba-multitool/_xml_coverage_reports/data/main/badge.svg) 5 | 6 | [![Unit Test (Win SQL Server)](https://github.com/lowlydba/dba-multitool/actions/workflows/sqlserver-unit.yml/badge.svg)](https://github.com/lowlydba/dba-multitool/actions/workflows/sqlserver-unit.yml) 7 | [![Unit Test (AzureSQL)](https://github.com/lowlydba/dba-multitool/actions/workflows/azuresql-unit.yml/badge.svg)](https://github.com/lowlydba/dba-multitool/actions/workflows/azuresql-unit.yml) 8 | [![Lint Code](https://github.com/lowlydba/dba-multitool/actions/workflows/lint.yml/badge.svg)](https://github.com/lowlydba/dba-multitool/actions/workflows/lint.yml) 9 | 10 | 11 | 12 |
The DBA MultiTool is a suite of scripts for the long haul: 13 | optimizing storage, on-the-fly documentation, general administrative needs, 14 | and more. Each script relies solely on T-SQL to ensure it is secure, 15 | requires no third-party software, and can be installed in seconds. 16 | 17 | ## Scripts 18 | 19 | To install/update all the scripts, use install_dba-multitool.sql. 20 | 21 | Looking for ways to automate the scripts? Try `Install-DbaMultiTool` from [dbatools :rocket:][dbatools], 22 | or the Ansible collection [lowlydba.sqlserver][lowlydba.sqlserver]. 23 | 24 | For detailed instructions and documentation, see [dba-multitool.org](https://dba-multitool.org) 25 | 26 | | Name | Description | 27 | | ---- | ----------- | 28 | | [sp_doc][sp_doc] | Always have current documentation by generating it on the fly in markdown. | 29 | | [sp_estindex][sp_estindex] | Estimate a new index's size and statistics without having to create it. | 30 | | [sp_help_revlogin][sp_help_revlogin] | Stored procedures that will help generate necessary scripts to transfer logins and their passwords. | 31 | | [sp_helpme][sp_helpme] | A drop-in modern alternative to `sp_help`. | 32 | | [sp_sizeoptimiser][sp_sizeoptimiser] | Recommends space saving measures for data footprints, with special checks for SQL Server Express. | 33 | 34 | ## Compatibility 35 | 36 | Only support for versions that are still in [mainstream][mainstream] support is guaranteed. 37 | 38 | | Version | Tested | 39 | | ------- | :----: | 40 | | Azure SQL | :heavy_check_mark: | 41 | | AWS RDS SQL Server * | :question: | 42 | | SQL Server 2022 | :heavy_check_mark: | 43 | | SQL Server 2019 | :heavy_check_mark: | 44 | | SQL Server 2014-2017 | :shrug: | 45 | | <= SQL Server 2012 | :x: | 46 | 47 | \* AWS RDS SQL Server is not tested, but should work *in theory*. YMMV. 48 | 49 | ## Contributing 50 | 51 | * Want to help :construction_worker:? Check out the [contributing][contrib] doc 52 | * Missing a feature? Found a :bug:? Open an [issue][issue] to get some :heart: 53 | * Something else? Say hi in the SQL Server Community Slack [#multitool][slack] channel 54 | 55 | *Icon made by [mangsaabguru](https://www.flaticon.com/authors/mangsaabguru) 56 | from [www.flaticon.com](https://www.flaticon.com/)* 57 | 58 | [contrib]: ../.github/CONTRIBUTING.md 59 | [dbatools]: https://dbatools.io 60 | [issue]: https://github.com/LowlyDBA/dba-multitool/issues 61 | [license]: ../LICENSE 62 | [lowlydba.sqlserver]: https://docs.ansible.com/ansible/latest/collections/lowlydba/sqlserver/index.html 63 | [mainstream]: https://learn.microsoft.com/en-us/sql/sql-server/end-of-support/sql-server-end-of-support-overview?view=sql-server-ver16#lifecycle-dates 64 | [slack]: https://sqlcommunity.slack.com/archives/C026Y2YCM9N 65 | [sp_doc]: https://dba-multitool.org/sp_doc 66 | [sp_estindex]: https://dba-multitool.org/sp_estindex 67 | [sp_help_revlogin]: https://learn.microsoft.com/en-us/troubleshoot/sql/database-engine/security/transfer-logins-passwords-between-instances 68 | [sp_helpme]: https://dba-multitool.org/sp_helpme 69 | [sp_sizeoptimiser]: https://dba-multitool.org/sp_sizeoptimiser 70 | -------------------------------------------------------------------------------- /docs/assets/dba-multitool-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlydba/dba-multitool/b128471aa5c3b5c59b1de849792d3ce88ee5935c/docs/assets/dba-multitool-logo.png -------------------------------------------------------------------------------- /sp_estindex.sql: -------------------------------------------------------------------------------- 1 | SET ANSI_NULLS ON; 2 | GO 3 | 4 | SET QUOTED_IDENTIFIER ON; 5 | GO 6 | 7 | IF EXISTS (SELECT * FROM sys.fn_listextendedproperty(N'Description' , N'SCHEMA',N'dbo', N'PROCEDURE',N'sp_estindex', NULL,NULL)) 8 | BEGIN; 9 | EXEC sys.sp_dropextendedproperty @name=N'Description' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_estindex'; 10 | END; 11 | GO 12 | 13 | IF EXISTS (SELECT * FROM sys.fn_listextendedproperty(N'@TableName' , N'SCHEMA',N'dbo', N'PROCEDURE',N'sp_estindex', NULL,NULL)) 14 | BEGIN; 15 | EXEC sys.sp_dropextendedproperty @name=N'@TableName' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_estindex'; 16 | END; 17 | GO 18 | 19 | IF EXISTS (SELECT * FROM sys.fn_listextendedproperty(N'@SqlMajorVersion' , N'SCHEMA',N'dbo', N'PROCEDURE',N'sp_estindex', NULL,NULL)) 20 | BEGIN; 21 | EXEC sys.sp_dropextendedproperty @name=N'@SqlMajorVersion' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_estindex'; 22 | END; 23 | GO 24 | 25 | IF EXISTS (SELECT * FROM sys.fn_listextendedproperty(N'@SchemaName' , N'SCHEMA',N'dbo', N'PROCEDURE',N'sp_estindex', NULL,NULL)) 26 | BEGIN; 27 | EXEC sys.sp_dropextendedproperty @name=N'@SchemaName' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_estindex'; 28 | END; 29 | GO 30 | 31 | IF EXISTS (SELECT * FROM sys.fn_listextendedproperty(N'@IsUnique' , N'SCHEMA',N'dbo', N'PROCEDURE',N'sp_estindex', NULL,NULL)) 32 | BEGIN; 33 | EXEC sys.sp_dropextendedproperty @name=N'@IsUnique' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_estindex'; 34 | END; 35 | GO 36 | 37 | IF EXISTS (SELECT * FROM sys.fn_listextendedproperty(N'@IndexColumns' , N'SCHEMA',N'dbo', N'PROCEDURE',N'sp_estindex', NULL,NULL)) 38 | BEGIN; 39 | EXEC sys.sp_dropextendedproperty @name=N'@IndexColumns' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_estindex'; 40 | END; 41 | GO 42 | 43 | IF EXISTS (SELECT * FROM sys.fn_listextendedproperty(N'@IncludeColumns' , N'SCHEMA',N'dbo', N'PROCEDURE',N'sp_estindex', NULL,NULL)) 44 | BEGIN; 45 | EXEC sys.sp_dropextendedproperty @name=N'@IncludeColumns' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_estindex'; 46 | END; 47 | GO 48 | 49 | IF EXISTS (SELECT * FROM sys.fn_listextendedproperty(N'@Filter' , N'SCHEMA',N'dbo', N'PROCEDURE',N'sp_estindex', NULL,NULL)) 50 | BEGIN; 51 | EXEC sys.sp_dropextendedproperty @name=N'@Filter' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_estindex'; 52 | END; 53 | GO 54 | 55 | IF EXISTS (SELECT * FROM sys.fn_listextendedproperty(N'@FillFactor' , N'SCHEMA',N'dbo', N'PROCEDURE',N'sp_estindex', NULL,NULL)) 56 | BEGIN; 57 | EXEC sys.sp_dropextendedproperty @name=N'@FillFactor' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_estindex'; 58 | END; 59 | GO 60 | 61 | IF EXISTS (SELECT * FROM sys.fn_listextendedproperty(N'@VarcharFillPercent' , N'SCHEMA',N'dbo', N'PROCEDURE',N'sp_estindex', NULL,NULL)) 62 | BEGIN; 63 | EXEC sys.sp_dropextendedproperty @name=N'@VarcharFillPercent' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_estindex'; 64 | END; 65 | GO 66 | 67 | IF EXISTS (SELECT * FROM sys.fn_listextendedproperty(N'@DatabaseName' , N'SCHEMA',N'dbo', N'PROCEDURE',N'sp_estindex', NULL,NULL)) 68 | BEGIN; 69 | EXEC sys.sp_dropextendedproperty @name=N'@DatabaseName' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_estindex'; 70 | END; 71 | GO 72 | 73 | IF EXISTS (SELECT * FROM sys.fn_listextendedproperty(N'@Verbose' , N'SCHEMA',N'dbo', N'PROCEDURE',N'sp_estindex', NULL,NULL)) 74 | BEGIN; 75 | EXEC sys.sp_dropextendedproperty @name=N'@Verbose' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_estindex'; 76 | END; 77 | GO 78 | 79 | /***************************/ 80 | /* Create stored procedure */ 81 | /***************************/ 82 | IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[sp_estindex]') AND [type] IN (N'P', N'PC')) 83 | BEGIN; 84 | EXEC dbo.sp_executesql @statement = N'CREATE PROCEDURE [dbo].[sp_estindex] AS'; 85 | END 86 | GO 87 | 88 | ALTER PROCEDURE [dbo].[sp_estindex] 89 | @SchemaName SYSNAME = NULL 90 | ,@TableName SYSNAME 91 | ,@DatabaseName SYSNAME = NULL 92 | ,@IndexColumns NVARCHAR(2048) 93 | ,@IncludeColumns NVARCHAR(2048) = NULL 94 | ,@IsUnique BIT = 0 95 | ,@Filter NVARCHAR(2048) = '' 96 | ,@FillFactor TINYINT = 100 97 | ,@VarcharFillPercent TINYINT = 100 98 | ,@Verbose BIT = 0 99 | -- Unit testing only 100 | ,@SqlMajorVersion TINYINT = 0 101 | AS 102 | BEGIN 103 | 104 | SET NOCOUNT ON; 105 | 106 | /* 107 | sp_estindex - Estimate a new index's size and statistics. 108 | 109 | Part of the DBA MultiTool http://dba-multitool.org 110 | 111 | Version: 20220124 112 | 113 | MIT License 114 | 115 | Copyright (c) 2023 John McCall 116 | 117 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 118 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 119 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 120 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 121 | 122 | The above copyright notice and this permission notice shall be included in all copies or substantial 123 | portions of the Software. 124 | 125 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 126 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 127 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 128 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 129 | DEALINGS IN THE SOFTWARE. 130 | 131 | -- TODO: 132 | -- Handle clustered indexes - https://docs.microsoft.com/en-us/sql/relational-databases/databases/estimate-the-size-of-a-clustered-index?view=sql-server-ver15 133 | 134 | ========= 135 | 136 | Example: 137 | 138 | EXEC dbo.sp_estindex @SchemaName = 'dbo', @tableName = 'Marathon', @IndexColumns = 'racer_id, finish_time, is_disqualified'; 139 | 140 | EXEC dbo.sp_estindex @tableName = 'Marathon', @IndexColumns = 'racer_id, finish_time, is_disqualified', @Filter = 'WHERE racer_id IS NOT NULL', @FillFactor = 90; 141 | 142 | */ 143 | 144 | DECLARE @Sql NVARCHAR(MAX) = N'' 145 | ,@QualifiedTable NVARCHAR(257) 146 | ,@IndexName SYSNAME = CONCAT('sp_estindex_hypothetical_idx_', DATEDIFF(SECOND,'1970-01-01 00:08:46', GETUTCDATE())) 147 | ,@DropIndexSql NVARCHAR(MAX) 148 | ,@Msg NVARCHAR(MAX) = N'' 149 | ,@IndexType SYSNAME = 'NONCLUSTERED' 150 | ,@IsHeap BIT 151 | ,@IsClusterUnique BIT 152 | ,@ObjectID INT 153 | ,@IndexID INT 154 | ,@ParmDefinition NVARCHAR(MAX) = N'' 155 | ,@NumRows BIGINT 156 | ,@UseDatabase NVARCHAR(200) 157 | ,@UniqueSql VARCHAR(10) 158 | ,@IncludeSql VARCHAR(2048) 159 | ,@PageSize BIGINT = 8192 160 | ,@FreeBytesPerPage BIGINT = 8096; 161 | 162 | BEGIN TRY 163 | -- Find Version 164 | IF (@SqlMajorVersion = 0) 165 | BEGIN; 166 | SET @SqlMajorVersion = CAST(SERVERPROPERTY('ProductMajorVersion') AS TINYINT); 167 | END; 168 | 169 | /* Validate Version */ 170 | IF (@SqlMajorVersion < 11) 171 | BEGIN; 172 | SET @Msg = 'SQL Server versions below 2012 are not supported, sorry!'; 173 | RAISERROR(@Msg, 16, 1); 174 | END; 175 | 176 | /* Validate Fill Factor */ 177 | IF (@FillFactor > 100 OR @FillFactor < 1) 178 | BEGIN; 179 | SET @Msg = 'Fill factor must be between 1 and 100.'; 180 | THROW 51000, @Msg, 1; 181 | END; 182 | 183 | /* Validate Varchar Fill Percent */ 184 | IF (@VarcharFillPercent > 100 OR @VarcharFillPercent < 1) 185 | BEGIN; 186 | SET @Msg = 'Varchar fill percent must be between 1 and 100.'; 187 | THROW 51000, @Msg, 1; 188 | END; 189 | 190 | /* Validate Filter */ 191 | IF (@Filter <> '' AND LEFT(@Filter, 5) <> 'WHERE') 192 | BEGIN; 193 | SET @Msg = 'Filter must start with ''WHERE''.'; 194 | THROW 51000, @Msg, 1; 195 | END; 196 | 197 | /* Validate Database */ 198 | IF (@DatabaseName IS NULL) 199 | BEGIN; 200 | SET @DatabaseName = DB_NAME(); 201 | IF (@Verbose = 1) 202 | BEGIN; 203 | SET @Msg = 'No database provided, assuming current database.'; 204 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 205 | END; 206 | END; 207 | ELSE IF (DB_ID(@DatabaseName) IS NULL) 208 | BEGIN; 209 | SET @DatabaseName = DB_NAME(); 210 | SET @Msg = 'Database does not exist.'; 211 | RAISERROR(@Msg, 16, 1); 212 | END; 213 | 214 | /* Validate Schema */ 215 | IF (@SchemaName IS NULL) 216 | BEGIN; 217 | SET @SchemaName = 'dbo'; 218 | IF (@Verbose = 1) 219 | BEGIN; 220 | SET @Msg = 'No schema provided, assuming dbo.'; 221 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 222 | END; 223 | END; 224 | 225 | -- Set variables with validated params 226 | SET @QualifiedTable = CONCAT(QUOTENAME(@SchemaName), '.', QUOTENAME(@TableName)); 227 | SET @UseDatabase = N'USE ' + QUOTENAME(@DatabaseName) + '; '; 228 | IF (@IsUnique = 1) 229 | BEGIN; 230 | SET @UniqueSql = ' UNIQUE '; 231 | END; 232 | IF (@IncludeColumns IS NOT NULL) 233 | BEGIN; 234 | SET @IncludeSql = CONCAT(' INCLUDE(', @IncludeColumns, ') '); 235 | END; 236 | 237 | -- Find object id 238 | SET @Sql = CONCAT(@UseDatabase, 239 | N'SELECT @ObjectID = [object_id] 240 | FROM [sys].[all_objects] 241 | WHERE [object_id] = OBJECT_ID(@QualifiedTable)'); 242 | SET @ParmDefinition = N'@QualifiedTable NVARCHAR(257) 243 | ,@ObjectID INT OUTPUT'; 244 | EXEC sp_executesql @Sql 245 | ,@ParmDefinition 246 | ,@QualifiedTable 247 | ,@ObjectID OUTPUT; 248 | 249 | -- Determine Heap or Clustered 250 | SET @Sql = CONCAT(@UseDatabase, 251 | N'SELECT @IsHeap = CASE [type] WHEN 0 THEN 1 ELSE 0 END 252 | ,@IsClusterUnique = [is_unique] 253 | FROM [sys].[indexes] 254 | WHERE [object_id] = OBJECT_ID(@QualifiedTable) 255 | AND [type] IN (1, 0)'); 256 | SET @ParmDefinition = N'@QualifiedTable NVARCHAR(257), @IsHeap BIT OUTPUT, @IsClusterUnique BIT OUTPUT'; 257 | EXEC sp_executesql @Sql 258 | ,@ParmDefinition 259 | ,@QualifiedTable 260 | ,@IsHeap OUTPUT 261 | ,@IsClusterUnique OUTPUT; 262 | 263 | -- Safety check for leftover index from previous run 264 | SET @DropIndexSql = CONCAT(@UseDatabase, 265 | N'IF EXISTS (SELECT 1 FROM [sys].[indexes] WHERE [object_id] = OBJECT_ID(''',@QualifiedTable,''') AND [name] = ''',@IndexName,''') 266 | DROP INDEX ', QUOTENAME(@IndexName), ' ON ', @QualifiedTable); 267 | EXEC sp_executesql @DropIndexSql; 268 | 269 | -- Fetch missing index stats before creation 270 | IF OBJECT_ID('tempdb..##TempMissingIndex') IS NOT NULL 271 | BEGIN; 272 | DROP TABLE ##TempMissingIndex; 273 | END; 274 | 275 | SET @Sql = CONCAT(@UseDatabase, 276 | N'SELECT [id].[statement] 277 | ,[id].[equality_columns] 278 | ,[id].[inequality_columns] 279 | ,[id].[included_columns] 280 | ,[gs].[unique_compiles] 281 | ,[gs].[user_seeks] 282 | ,[gs].[user_scans] 283 | ,[gs].[avg_total_user_cost] -- Average cost of the user queries that could be reduced 284 | ,[gs].[avg_user_impact] -- % 285 | INTO ##TempMissingIndex 286 | FROM [sys].[dm_db_missing_index_group_stats] [gs] 287 | INNER JOIN [sys].[dm_db_missing_index_groups] [ig] ON [gs].[group_handle] = [ig].[index_group_handle] 288 | INNER JOIN [sys].[dm_db_missing_index_details] [id] ON [ig].[index_handle] = [id].[index_handle] 289 | WHERE [id].[database_id] = DB_ID() 290 | AND [id].[object_id] = @ObjectID 291 | OPTION (RECOMPILE);'); 292 | SET @ParmDefinition = N'@ObjectID INT'; 293 | EXEC sp_executesql @Sql 294 | ,@ParmDefinition 295 | ,@ObjectID; 296 | 297 | -- Create the hypothetical index 298 | SET @Sql = CONCAT(@UseDatabase, 'CREATE ', @UniqueSql, @IndexType, ' INDEX ', QUOTENAME(@IndexName), ' ON ', @QualifiedTable, ' (', @IndexColumns, ') ',@IncludeSql, @Filter, ' WITH (STATISTICS_ONLY = -1)'); 299 | EXEC sp_executesql @Sql; 300 | 301 | /*******************/ 302 | /* Get index stats */ 303 | /*******************/ 304 | -- Use DBCC to avoid various inconsistencies 305 | -- in equivalent DMVs between 2012-2016 306 | SET @Sql = CONCAT(@UseDatabase, 'DBCC SHOW_STATISTICS ("', @QualifiedTable,'", ', QUOTENAME(@IndexName), ')'); 307 | EXEC sp_executesql @Sql; 308 | 309 | /***************************/ 310 | /* Get missing index stats */ 311 | /***************************/ 312 | DECLARE @QuotedKeyColumns NVARCHAR(2048) 313 | ,@QuotedInclColumns NVARCHAR(2048); 314 | 315 | --Get index columns in same format as dmv table 316 | SET @Sql = CONCAT(@UseDatabase, 317 | N'SELECT @QuotedKeyColumns = CASE WHEN [ic].[is_included_column] = 0 318 | THEN CONCAT(COALESCE(@QuotedKeyColumns COLLATE DATABASE_DEFAULT + '', '', ''''), QUOTENAME([ac].[name])) 319 | ELSE @QuotedKeyColumns 320 | END, 321 | @QuotedInclColumns = CASE WHEN [ic].[is_included_column] = 1 322 | THEN CONCAT(COALESCE(@QuotedInclColumns COLLATE DATABASE_DEFAULT + '', '', ''''), QUOTENAME([ac].[name])) 323 | ELSE @QuotedInclColumns 324 | END 325 | FROM [sys].[indexes] AS [i] 326 | INNER JOIN [sys].[index_columns] AS [ic] ON [i].[index_id] = [ic].[index_id] 327 | AND [ic].object_id = [i].object_id 328 | INNER JOIN [sys].[all_columns] AS [ac] ON [ac].[object_id] = [ic].[object_id] 329 | AND [ac].[column_id] = [ic].[column_id] 330 | WHERE [i].[name] = @IndexName 331 | AND [i].[object_id] = @ObjectID 332 | AND [i].[is_hypothetical] = 1;'); 333 | SET @ParmDefinition = N'@IndexName SYSNAME, @ObjectID INT, @QuotedKeyColumns NVARCHAR(2048) OUTPUT, @QuotedInclColumns NVARCHAR(2048) OUTPUT'; 334 | EXEC sp_executesql @Sql 335 | ,@ParmDefinition 336 | ,@IndexName 337 | ,@ObjectID 338 | ,@QuotedKeyColumns OUTPUT 339 | ,@QuotedInclColumns OUTPUT; 340 | 341 | -- Search missing index dmv for a match 342 | SELECT 'Missing index stats' AS [description] 343 | ,[statement] 344 | ,[equality_columns] 345 | ,[inequality_columns] 346 | ,[included_columns] 347 | ,[unique_compiles] 348 | ,[user_seeks] 349 | ,[user_scans] 350 | ,[avg_total_user_cost] 351 | ,[avg_user_impact] 352 | FROM ##TempMissingIndex 353 | WHERE COALESCE([equality_columns] + ', ', '') + [inequality_columns] = @QuotedKeyColumns 354 | AND ([included_columns] = @QuotedInclColumns OR [included_columns] IS NULL); 355 | 356 | IF (SELECT COUNT(1) FROM ##TempMissingIndex) = 0 AND (@Verbose = 1) 357 | BEGIN; 358 | SET @Msg = 'No matching missing index statistics found.'; 359 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 360 | END; 361 | 362 | DROP TABLE ##TempMissingIndex; 363 | 364 | /************************************************/ 365 | /* Estimate index size - does NOT consider: */ 366 | /* Partitioning, allocation pages, LOB values, */ 367 | /* compression, or sparse columns */ 368 | /************************************************/ 369 | IF (@IndexType = 'NONCLUSTERED') -- http://dba-multitool.org/est-nonclustered-index-size 370 | BEGIN; 371 | DECLARE @NumVariableKeyCols INT = 0 372 | ,@MaxVarKeySize BIGINT = 0 373 | ,@NumFixedKeyCols INT = 0 374 | ,@FixedKeySize BIGINT = 0 375 | ,@NumKeyCols INT = 0 376 | ,@NullCols INT = 0 377 | ,@IndexNullBitmap BIGINT = 0 378 | ,@VariableKeySize BIGINT = 0 379 | ,@VariableKeyFillModifier DECIMAL(3,2) = (@VarcharFillPercent / 100) 380 | ,@TotalFixedKeySize BIGINT = 0 381 | ,@IndexRowSize BIGINT = 0 382 | ,@IndexRowsPerPage BIGINT = 0 383 | ,@ClusterNumVarKeyCols INT = 0 384 | ,@MaxClusterVarKeySize BIGINT = 0 385 | ,@ClusterNumFixedKeyCols INT = 0 386 | ,@MaxClusterFixedKeySize BIGINT = 0 387 | ,@ClusterNullCols INT = 0; 388 | 389 | /**************************/ 390 | /* 1. Calculate variables */ 391 | /**************************/ 392 | -- Row count 393 | SET @Sql = CONCAT(@UseDatabase, 394 | N'SELECT @NumRows = [sp].[rows] -- Accounts for index filter if in use 395 | FROM [sys].[objects] AS [o] 396 | INNER JOIN [sys].[stats] AS [stat] ON [stat].[object_id] = [o].[object_id] 397 | CROSS APPLY [sys].[dm_db_stats_properties]([stat].[object_id], [stat].[stats_id]) AS [sp] 398 | WHERE [o].[object_id] = @ObjectID 399 | AND [stat].[name] = @IndexName;'); 400 | SET @ParmDefinition = N'@ObjectID INT, @IndexName SYSNAME, @NumRows BIGINT OUTPUT'; 401 | EXEC sp_executesql @Sql 402 | ,@ParmDefinition 403 | ,@ObjectID 404 | ,@IndexName 405 | ,@NumRows OUTPUT; 406 | 407 | IF (@Verbose = 1) 408 | BEGIN 409 | SET @Msg = CONCAT('NumRows: ', @NumRows); 410 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 411 | END; 412 | 413 | --Key types and sizes 414 | SET @Sql = CONCAT(@UseDatabase, 415 | N'SELECT @NumVariableKeyCols = ISNULL(SUM(CASE 416 | WHEN TYPE_NAME([ac].[user_type_id]) IN(''varchar'', ''nvarchar'', ''text'', ''ntext'', ''image'', ''varbinary'', ''xml'') 417 | THEN 1 418 | ELSE 0 419 | END), 0), 420 | @MaxVarKeySize = ISNULL(SUM(CASE 421 | WHEN TYPE_NAME([ac].[user_type_id]) IN(''varchar'', ''nvarchar'', ''text'', ''ntext'', ''image'', ''varbinary'', ''xml'') 422 | THEN CASE [ac].[max_length] 423 | WHEN -1 424 | THEN(4000 + 2) -- use same estimation as the query engine for max lenths 425 | ELSE COL_LENGTH(OBJECT_NAME([i].object_id), [ac].[name]) 426 | END 427 | ELSE 0 428 | END), 0), 429 | @NumFixedKeyCols = ISNULL(SUM(CASE 430 | WHEN TYPE_NAME([ac].[user_type_id]) NOT IN(''varchar'', ''nvarchar'', ''text'', ''ntext'', ''image'', ''varbinary'', ''xml'') 431 | THEN 1 432 | ELSE 0 433 | END), 0), 434 | @FixedKeySize = ISNULL(SUM(CASE 435 | WHEN TYPE_NAME([ac].[user_type_id]) NOT IN(''varchar'', ''nvarchar'', ''text'', ''ntext'', ''image'', ''varbinary'', ''xml'') 436 | THEN COL_LENGTH(OBJECT_NAME([i].object_id), [ac].[name]) 437 | ELSE 0 438 | END), 0), 439 | @NullCols = ISNULL(SUM(CAST([ac].[is_nullable] AS TINYINT)),0) 440 | FROM [sys].[indexes] AS [i] 441 | INNER JOIN [sys].[index_columns] AS [ic] ON [i].[index_id] = [ic].[index_id] 442 | AND [ic].object_id = [i].object_id 443 | INNER JOIN [sys].[all_columns] AS [ac] ON [ac].object_id = [ic].object_id 444 | AND [ac].[column_id] = [ic].[column_id] 445 | WHERE [i].[name] = @IndexName 446 | AND [i].[object_id] = @ObjectID 447 | AND [i].[is_hypothetical] = 1 448 | AND [ic].[is_included_column] = 0'); 449 | SET @ParmDefinition = N'@IndexName SYSNAME, @ObjectID INT, @NumVariableKeyCols INT OUTPUT, 450 | @MaxVarKeySize BIGINT OUTPUT, @NumFixedKeyCols INT OUTPUT, @FixedKeySize BIGINT OUTPUT, 451 | @NullCols INT OUTPUT'; 452 | EXEC sp_executesql @Sql 453 | ,@ParmDefinition 454 | ,@IndexName 455 | ,@ObjectID 456 | ,@NumVariableKeyCols OUTPUT 457 | ,@MaxVarKeySize OUTPUT 458 | ,@NumFixedKeyCols OUTPUT 459 | ,@FixedKeySize OUTPUT 460 | ,@NullCols OUTPUT; 461 | 462 | SET @NumKeyCols = @NumVariableKeyCols + @NumFixedKeyCols; 463 | 464 | IF (@Verbose = 1) 465 | BEGIN 466 | SET @Msg = CONCAT('NumVariableKeyCols: ', @NumVariableKeyCols); 467 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 468 | SET @Msg = CONCAT('MaxVarKeySize: ', @MaxVarKeySize); 469 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 470 | SET @Msg = CONCAT('NumFixedKeyCols: ', @NumFixedKeyCols); 471 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 472 | SET @Msg = CONCAT('FixedKeySize: ', @FixedKeySize); 473 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 474 | SET @Msg = CONCAT('NullCols: ', @NullCols); 475 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 476 | SET @Msg = CONCAT('NumKeyCols: ', @NumKeyCols); 477 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 478 | END; 479 | 480 | -- Account for data row locator for non-unique 481 | IF (@IsHeap = 1 AND @IsUnique = 0) 482 | BEGIN; 483 | SET @NumKeyCols = @NumKeyCols + 1; 484 | SET @NumVariableKeyCols = @NumVariableKeyCols + 1; 485 | SET @MaxVarKeySize = @MaxVarKeySize + 8; --heap RID 486 | END; 487 | ELSE IF (@IsHeap = 0 AND @IsUnique = 0) 488 | BEGIN; 489 | --Clustered keys and sizes not included in the new index 490 | SET @Sql = CONCAT(@UseDatabase, 491 | N'WITH NewIndexCol AS ( 492 | SELECT [ac].[name] 493 | FROM [sys].[indexes] AS [i] 494 | INNER JOIN [sys].[index_columns] AS [ic] ON [i].[index_id] = [ic].[index_id] 495 | AND [ic].object_id = [i].object_id 496 | INNER JOIN [sys].[all_columns] AS [ac] ON [ac].object_id = [ic].object_id 497 | AND [ac].[column_id] = [ic].[column_id] 498 | WHERE [i].[name] = @IndexName 499 | AND [i].[object_id] = @ObjectID 500 | AND [i].[is_hypothetical] = 1 501 | AND [ic].[is_included_column] = 0 502 | ) 503 | SELECT @ClusterNumVarKeyCols = ISNULL(SUM(CASE 504 | WHEN TYPE_NAME([ac].[user_type_id]) IN(''varchar'', ''nvarchar'', ''text'', ''ntext'', ''image'', ''varbinary'', ''xml'') 505 | THEN 1 506 | ELSE 0 507 | END), 0), 508 | @MaxClusterVarKeySize = ISNULL(SUM(CASE 509 | WHEN TYPE_NAME([ac].[user_type_id]) IN(''varchar'', ''nvarchar'', ''text'', ''ntext'', ''image'', ''varbinary'', ''xml'') 510 | THEN CASE [ac].[max_length] 511 | WHEN -1 512 | THEN(4000 + 2) -- use same estimation as the query engine for max lenths 513 | ELSE COL_LENGTH(OBJECT_NAME([i].object_id), [ac].[name]) 514 | END 515 | ELSE 0 516 | END), 0), 517 | @ClusterNumFixedKeyCols = ISNULL(SUM(CASE 518 | WHEN TYPE_NAME([ac].[user_type_id]) NOT IN(''varchar'', ''nvarchar'', ''text'', ''ntext'', ''image'', ''varbinary'', ''xml'') 519 | THEN 1 520 | ELSE 0 521 | END), 0), 522 | @MaxClusterFixedKeySize = ISNULL(SUM(CASE 523 | WHEN TYPE_NAME([ac].[user_type_id]) NOT IN(''varchar'', ''nvarchar'', ''text'', ''ntext'', ''image'', ''varbinary'', ''xml'') 524 | THEN COL_LENGTH(OBJECT_NAME([i].object_id), [ac].[name]) 525 | ELSE 0 526 | END), 0), 527 | @ClusterNullCols = ISNULL(SUM(CAST([ac].[is_nullable] AS TINYINT)),0) 528 | FROM [sys].[indexes] AS [i] 529 | INNER JOIN [sys].[index_columns] AS [ic] ON [i].[index_id] = [ic].[index_id] 530 | AND [ic].object_id = [i].object_id 531 | INNER JOIN [sys].[all_columns] AS [ac] ON [ac].object_id = [ic].object_id 532 | AND [ac].[column_id] = [ic].[column_id] 533 | WHERE [i].[type] = 1 --Clustered 534 | AND [i].[object_id] = @ObjectID 535 | AND [ac].[name] NOT IN (SELECT [name] FROM [NewIndexCol]);'); 536 | SET @ParmDefinition = N'@IndexName SYSNAME, @ObjectID INT, @ClusterNumVarKeyCols INT OUTPUT, 537 | @MaxClusterVarKeySize BIGINT OUTPUT, @ClusterNumFixedKeyCols INT OUTPUT, 538 | @MaxClusterFixedKeySize BIGINT OUTPUT, @ClusterNullCols INT OUTPUT'; 539 | EXEC sp_executesql @Sql 540 | ,@ParmDefinition 541 | ,@IndexName 542 | ,@ObjectID 543 | ,@ClusterNumVarKeyCols OUTPUT 544 | ,@MaxClusterVarKeySize OUTPUT 545 | ,@ClusterNumFixedKeyCols OUTPUT 546 | ,@MaxClusterFixedKeySize OUTPUT 547 | ,@ClusterNullCols OUTPUT; 548 | 549 | IF (@Verbose = 1) 550 | BEGIN 551 | SET @Msg = CONCAT('ClusterNumVarKeyCols: ', @ClusterNumVarKeyCols); 552 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 553 | SET @Msg = CONCAT('ClusterNumFixedKeyCols: ', @ClusterNumFixedKeyCols); 554 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 555 | SET @Msg = CONCAT('MaxClusterVarKeySize: ', @MaxClusterVarKeySize); 556 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 557 | SET @Msg = CONCAT('MaxClusterFixedKeySize: ', @MaxClusterFixedKeySize); 558 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 559 | SET @Msg = CONCAT('ClusterNullCols: ', @ClusterNullCols); 560 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 561 | END; 562 | 563 | -- Add counts from clustered index cols 564 | SET @NumKeyCols = @NumKeyCols + (@ClusterNumVarKeyCols + @ClusterNumFixedKeyCols); 565 | SET @FixedKeySize = @FixedKeySize + @MaxClusterFixedKeySize; 566 | SET @NumVariableKeyCols = @NumVariableKeyCols + @ClusterNumVarKeyCols; 567 | SET @MaxVarKeySize = @MaxVarKeySize + @MaxClusterVarKeySize; 568 | SET @NullCols = @NullCols + @ClusterNullCols; 569 | 570 | IF (@IsClusterUnique = 0) 571 | BEGIN; 572 | SET @MaxVarKeySize = @MaxVarKeySize + 4; 573 | SET @NumVariableKeyCols = @NumVariableKeyCols + 1; 574 | SET @NumKeyCols = @NumKeyCols + 1; 575 | END; 576 | END; 577 | 578 | IF (@Verbose = 1) 579 | BEGIN 580 | SET @Msg = CONCAT('FixedKeySize: ', @FixedKeySize); 581 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 582 | SET @Msg = CONCAT('NumVariableKeyCols: ', @NumVariableKeyCols); 583 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 584 | SET @Msg = CONCAT('NumKeyCols: ', @NumKeyCols); 585 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 586 | SET @Msg = CONCAT('MaxVarKeySize: ', @MaxVarKeySize); 587 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 588 | SET @Msg = CONCAT('NullCols: ', @NullCols); 589 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 590 | END; 591 | 592 | -- Account for index null bitmap 593 | IF (@NullCols > 0) 594 | BEGIN; 595 | SET @IndexNullBitmap = 2 + ((@NullCols + 7) / 8); 596 | END; 597 | 598 | -- Calculate variable length data size 599 | -- Assumes each col is 100% full unless 600 | -- otherwise specified 601 | IF (@NumVariableKeyCols > 0) 602 | BEGIN; 603 | --The bytes added to @MaxVarKeySize are for tracking each variable column. 604 | SET @VariableKeySize = 2 + (@NumVariableKeyCols * 2) + (@MaxVarKeySize * @VariableKeyFillModifier); 605 | END; 606 | 607 | -- Calculate index row size 608 | -- + 1 (for row header overhead of an index row) + 6 (for the child page ID pointer) 609 | SET @IndexRowSize = @FixedKeySize + @VariableKeySize + @IndexNullBitmap + 1 + 6; 610 | IF (@Verbose = 1) 611 | BEGIN 612 | SET @Msg = CONCAT('IndexRowSize: ', @IndexRowSize); 613 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 614 | END; 615 | 616 | -- Calculate number of index rows / page 617 | -- + 2 for the row's entry in the page's slot array. 618 | SET @IndexRowsPerPage = FLOOR(@FreeBytesPerPage / (@IndexRowSize + 2)); 619 | 620 | IF (@Verbose = 1) 621 | BEGIN 622 | SET @Msg = CONCAT('IndexRowsPerPage: ', @IndexRowsPerPage); 623 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 624 | END; 625 | 626 | /****************************************************************************/ 627 | /* 2. Calculate the Space Used to Store Index Information in the Leaf Level */ 628 | /****************************************************************************/ 629 | -- Specify the number of fixed-length and variable-length columns at the leaf level 630 | -- and calculate the space that is required for their storage 631 | DECLARE @NumLeafCols INT = @NumKeyCols 632 | ,@FixedLeafSize BIGINT = @FixedKeySize 633 | ,@NumVariableLeafCols INT = @NumVariableKeyCols 634 | ,@MaxVarLeafSize BIGINT = @MaxVarKeySize 635 | ,@LeafNullBitmap BIGINT = 0 636 | ,@VariableLeafSize BIGINT = 0 637 | ,@LeafRowSize BIGINT = 0 638 | ,@LeafRowsPerPage BIGINT = 0 639 | ,@FreeRowsPerPage BIGINT = 0 640 | ,@NumLeafPages BIGINT = 0 641 | ,@LeafSpaceUsed BIGINT = 0; 642 | 643 | IF (@IncludeColumns IS NOT NULL) 644 | BEGIN; 645 | DECLARE @NumVariableInclCols INT = 0 646 | ,@MaxVarInclSize BIGINT = 0 647 | ,@NumFixedInclCols INT = 0 648 | ,@FixedInclSize BIGINT = 0; 649 | 650 | --Incl types and sizes 651 | SET @Sql = CONCAT(@UseDatabase, 652 | N'SELECT @NumVariableInclCols = ISNULL(SUM(CASE 653 | WHEN TYPE_NAME([ac].[user_type_id]) IN(''varchar'', ''nvarchar'', ''text'', ''ntext'', ''image'', ''varbinary'', ''xml'') 654 | THEN 1 655 | ELSE 0 656 | END), 0), 657 | @MaxVarInclSize = ISNULL(SUM(CASE 658 | WHEN TYPE_NAME([ac].[user_type_id]) IN(''varchar'', ''nvarchar'', ''text'', ''ntext'', ''image'', ''varbinary'', ''xml'') 659 | THEN CASE [ac].[max_length] 660 | WHEN -1 661 | THEN (4000 + 2) -- use same estimation as the query engine for max lenths 662 | ELSE COL_LENGTH(OBJECT_NAME([i].object_id), [ac].[name]) 663 | END 664 | ELSE 0 665 | END), 0), 666 | @NumFixedInclCols = ISNULL(SUM(CASE 667 | WHEN TYPE_NAME([ac].[user_type_id]) NOT IN(''varchar'', ''nvarchar'', ''text'', ''ntext'', ''image'', ''varbinary'', ''xml'') 668 | THEN 1 669 | ELSE 0 670 | END), 0), 671 | @FixedInclSize = ISNULL(SUM(CASE 672 | WHEN TYPE_NAME([ac].[user_type_id]) NOT IN(''varchar'', ''nvarchar'', ''text'', ''ntext'', ''image'', ''varbinary'', ''xml'') 673 | THEN COL_LENGTH(OBJECT_NAME([i].object_id), [ac].[name]) 674 | ELSE 0 675 | END), 0) 676 | FROM [sys].[indexes] AS [i] 677 | INNER JOIN [sys].[index_columns] AS [ic] ON [i].[index_id] = [ic].[index_id] 678 | AND [ic].object_id = [i].object_id 679 | INNER JOIN [sys].[all_columns] AS [ac] ON [ac].object_id = [ic].object_id 680 | AND [ac].[column_id] = [ic].[column_id] 681 | WHERE [i].[name] = @IndexName 682 | AND [i].[object_id] = @ObjectID 683 | AND [i].[is_hypothetical] = 1 684 | AND [ic].[is_included_column] = 1;'); 685 | SET @ParmDefinition = N'@IndexName SYSNAME, @ObjectID INT, @NumVariableInclCols INT OUTPUT, 686 | @MaxVarInclSize BIGINT OUTPUT, @NumFixedInclCols INT OUTPUT, @FixedInclSize BIGINT OUTPUT'; 687 | EXEC sp_executesql @Sql 688 | ,@ParmDefinition 689 | ,@IndexName 690 | ,@ObjectID 691 | ,@NumVariableInclCols OUTPUT 692 | ,@MaxVarInclSize OUTPUT 693 | ,@NumFixedInclCols OUTPUT 694 | ,@FixedInclSize OUTPUT; 695 | 696 | -- Add included columns to rolling totals 697 | SET @NumLeafCols = @NumLeafCols + (@NumVariableInclCols + @NumFixedInclCols); 698 | SET @FixedLeafSize = @FixedLeafSize + @FixedInclSize; 699 | SET @NumVariableLeafCols = @NumVariableLeafCols + @NumVariableInclCols; 700 | SET @MaxVarLeafSize = @MaxVarLeafSize + @MaxVarInclSize; 701 | END; 702 | 703 | -- Account for data row locator for unique indexes 704 | -- If non-unique, already accounted for above 705 | IF (@IsUnique = 1) 706 | BEGIN; 707 | IF (@IsHeap = 1) 708 | BEGIN; 709 | SET @NumLeafCols = @NumLeafCols + 1; 710 | SET @NumVariableLeafCols = @NumVariableLeafCols + 1; 711 | SET @MaxVarLeafSize = @MaxVarLeafSize + 8; -- the data row locator is the heap RID (size 8 bytes). 712 | END; 713 | ELSE -- Clustered 714 | BEGIN; 715 | SET @NumLeafCols = @NumLeafCols + (@ClusterNumVarKeyCols + @ClusterNumFixedKeyCols); 716 | SET @FixedLeafSize = @FixedLeafSize + @ClusterNumFixedKeyCols; 717 | SET @NumVariableLeafCols = @NumVariableLeafCols + @ClusterNumVarKeyCols; 718 | SET @MaxVarLeafSize = @MaxVarLeafSize + @MaxClusterVarKeySize; 719 | 720 | IF (@IsClusterUnique = 0) 721 | BEGIN; 722 | SET @NumLeafCols = @NumLeafCols + 1; 723 | SET @NumVariableLeafCols = @NumVariableLeafCols + 1; 724 | SET @MaxVarLeafSize = @MaxVarLeafSize + 4; 725 | END; 726 | END; 727 | END; 728 | 729 | IF (@Verbose = 1) 730 | BEGIN 731 | SET @Msg = CONCAT('NumLeafCols: ', @NumLeafCols); 732 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 733 | SET @Msg = CONCAT('FixedLeafSize: ', @FixedLeafSize); 734 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 735 | SET @Msg = CONCAT('NumVariableLeafCols: ', @NumVariableLeafCols); 736 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 737 | SET @Msg = CONCAT('MaxVarLeafSize: ', @MaxVarLeafSize); 738 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 739 | END; 740 | 741 | -- Account for index null bitmap 742 | SET @LeafNullBitmap = 2 + ((@NumLeafCols + 7) / 8); 743 | 744 | IF (@Verbose = 1) 745 | BEGIN 746 | SET @Msg = CONCAT('LeafNullBitmap: ', @LeafNullBitmap); 747 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 748 | END; 749 | 750 | -- Calculate variable length data size 751 | -- Assumes each col is 100% full 752 | IF (@NumVariableLeafCols > 0) 753 | BEGIN; 754 | SET @VariableLeafSize = 2 + (@NumVariableLeafCols * 2) + @MaxVarLeafSize; 755 | END; 756 | 757 | IF (@Verbose = 1) 758 | BEGIN 759 | SET @Msg = CONCAT('VariableLeafSize: ', @VariableLeafSize); 760 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 761 | END; 762 | 763 | -- Calculate index row size 764 | SET @LeafRowSize = @FixedLeafSize + @VariableLeafSize + @LeafNullBitmap + 1; -- +1 for row header overhead of an index row) 765 | 766 | IF (@Verbose = 1) 767 | BEGIN 768 | SET @Msg = CONCAT('LeafRowSize: ', @LeafRowSize); 769 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 770 | END; 771 | 772 | -- Calculate number of index rows / page 773 | SET @LeafRowsPerPage = FLOOR(@FreeBytesPerPage / (@LeafRowSize + 2)); -- + 2 for the row's entry in the page's slot array. 774 | 775 | IF (@Verbose = 1) 776 | BEGIN 777 | SET @Msg = CONCAT('LeafRowsPerPage: ', @LeafRowsPerPage); 778 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 779 | END; 780 | 781 | -- Calculate free rows / page 782 | SET @FreeRowsPerPage = @FreeBytesPerPage * (( 100 - @FillFactor) / 100) / (@LeafRowSize + 2); -- + 2 for the row's entry in the page's slot array. 783 | 784 | IF (@Verbose = 1) 785 | BEGIN 786 | SET @Msg = CONCAT('FreeRowsPerPage: ', @FreeRowsPerPage); 787 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 788 | END; 789 | 790 | -- Calculate pages for all rows 791 | SET @NumLeafPages = CEILING(@NumRows / (@LeafRowsPerPage - @FreeRowsPerPage)); 792 | 793 | IF (@Verbose = 1) 794 | BEGIN 795 | SET @Msg = CONCAT('NumLeafPages: ', @NumLeafPages); 796 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 797 | END; 798 | 799 | -- Calculate size of index at leaf level 800 | SET @LeafSpaceUsed = @PageSize * @NumLeafPages; 801 | 802 | /*********************************************************************************/ 803 | /* 3. Calculate the Space Used to Store Index Information in the Non-leaf Levels */ 804 | /*********************************************************************************/ 805 | DECLARE @NonLeafLevels BIGINT = 0, 806 | @NumIndexPages BIGINT = 0, 807 | @IndexSpaceUsed BIGINT = 0; 808 | 809 | -- Calculate the number of non-leaf levels in the index 810 | SET @NonLeafLevels = CEILING(1 + LOG(@IndexRowsPerPage) * (@NumLeafPages / @IndexRowsPerPage)); 811 | 812 | IF (@Verbose = 1) 813 | BEGIN 814 | SET @Msg = CONCAT('NonLeafLevels: ', @NonLeafLevels); 815 | RAISERROR(@Msg, 10, 1) WITH NOWAIT; 816 | END; 817 | 818 | --Formula: IndexPages = Summation (Num_Leaf_Pages/Index_Rows_Per_Page^Level)where 1 <= Level <= Levels 819 | WHILE (@NonLeafLevels > 1) 820 | BEGIN 821 | DECLARE @TempIndexPages FLOAT(30); 822 | 823 | -- TempIndexPages may be exceedingly small, so catch any arith overflows and call it 0 824 | BEGIN TRY; 825 | SET @TempIndexPages = @NumLeafPages / POWER(@IndexRowsPerPage, @NonLeafLevels); 826 | SET @NumIndexPages = @NumIndexPages + @TempIndexPages; 827 | SET @NonLeafLevels = @NonLeafLevels - 1; 828 | END TRY 829 | BEGIN CATCH; 830 | SET @NonLeafLevels = @NonLeafLevels - 1; 831 | END CATCH; 832 | END; 833 | 834 | -- Calculate size of the index 835 | SET @IndexSpaceUsed = @PageSize * @NumIndexPages; 836 | 837 | /**************************************/ 838 | /* 4. Total index and leaf space used */ 839 | /**************************************/ 840 | DECLARE @Total BIGINT = 0; 841 | 842 | SET @Total = @LeafSpaceUsed + @IndexSpaceUsed; 843 | 844 | SELECT @Total/1024 AS [Est. KB] 845 | ,CAST(ROUND(@Total/1024.0/1024.0,2,1) AS DECIMAL(30,2)) AS [Est. MB] 846 | ,CAST(ROUND(@Total/1024.0/1024.0/1024.0,2,1) AS DECIMAL(30,4)) AS [Est. GB]; 847 | END; 848 | 849 | --Cleanup 850 | EXEC sp_executesql @DropIndexSql; 851 | 852 | END TRY 853 | BEGIN CATCH; 854 | BEGIN; 855 | DECLARE @ErrorNumber INT = ERROR_NUMBER(); 856 | DECLARE @ErrorLine INT = ERROR_LINE(); 857 | DECLARE @ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE(); 858 | DECLARE @ErrorSeverity INT = ERROR_SEVERITY(); 859 | DECLARE @ErrorState INT = ERROR_STATE(); 860 | 861 | EXEC sp_executesql @DropIndexSql; 862 | 863 | SET @ErrorMessage = CONCAT(QUOTENAME(OBJECT_NAME(@@PROCID)), ': ', @ErrorMessage); 864 | RAISERROR(@ErrorMessage, @ErrorSeverity, @ErrorState) WITH NOWAIT; 865 | END; 866 | END CATCH; 867 | 868 | END; 869 | GO 870 | 871 | EXEC sys.sp_addextendedproperty @name=N'@DatabaseName', @value=N'Target database of the index''s table.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_estindex'; 872 | GO 873 | 874 | EXEC sys.sp_addextendedproperty @name=N'@FillFactor', @value=N'Optional fill factor for the index. Default is 100.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_estindex'; 875 | GO 876 | 877 | EXEC sys.sp_addextendedproperty @name=N'@Filter', @value=N'Optional filter for the index.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_estindex'; 878 | GO 879 | 880 | EXEC sys.sp_addextendedproperty @name=N'@IncludeColumns', @value=N'Optional comma separated list of include columns.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_estindex'; 881 | GO 882 | 883 | EXEC sys.sp_addextendedproperty @name=N'@IndexColumns', @value=N'Comma separated list of key columns.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_estindex'; 884 | GO 885 | 886 | EXEC sys.sp_addextendedproperty @name=N'@IsUnique', @value=N'Whether or not the index is UNIQUE. Default is 0.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_estindex'; 887 | GO 888 | 889 | EXEC sys.sp_addextendedproperty @name=N'@SchemaName', @value=N'Target schema of the index''s table. Default is ''dbo''.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_estindex'; 890 | GO 891 | 892 | EXEC sys.sp_addextendedproperty @name=N'@SqlMajorVersion', @value=N'For unit testing only.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_estindex'; 893 | GO 894 | 895 | EXEC sys.sp_addextendedproperty @name=N'@TableName', @value=N'Target table for the index. Default is current database.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_estindex'; 896 | GO 897 | 898 | EXEC sys.sp_addextendedproperty @name=N'Description', @value=N'Estimate a new index''s size and statistics.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_estindex'; 899 | GO 900 | 901 | EXEC sys.sp_addextendedproperty @name=N'@VarcharFillPercent', @value=N'Optional estimated fill percent of data in variable length columns. Default is 100.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_estindex'; 902 | GO 903 | 904 | EXEC sys.sp_addextendedproperty @name=N'@Verbose', @value=N'Show intermediate variables used in size calculations. Default is 0.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_estindex'; 905 | GO 906 | -------------------------------------------------------------------------------- /sp_help_revlogin.sql: -------------------------------------------------------------------------------- 1 | /* Copyright for sp_help_revlogin is held by Microsoft. */ 2 | /* tsqllint-disable */ 3 | 4 | IF OBJECT_ID ('sp_hexadecimal') IS NOT NULL 5 | DROP PROCEDURE sp_hexadecimal 6 | GO 7 | CREATE PROCEDURE [dbo].[sp_hexadecimal] 8 | ( 9 | @binvalue varbinary(256), 10 | @hexvalue varchar (514) OUTPUT 11 | ) 12 | AS 13 | BEGIN 14 | DECLARE @charvalue varchar (514) 15 | DECLARE @i int 16 | DECLARE @length int 17 | DECLARE @hexstring char(16) 18 | SELECT @charvalue = '0x' 19 | SELECT @i = 1 20 | SELECT @length = DATALENGTH (@binvalue) 21 | SELECT @hexstring = '0123456789ABCDEF' 22 | 23 | WHILE (@i <= @length) 24 | BEGIN 25 | DECLARE @tempint int 26 | DECLARE @firstint int 27 | DECLARE @secondint int 28 | 29 | SELECT @tempint = CONVERT(int, SUBSTRING(@binvalue,@i,1)) 30 | SELECT @firstint = FLOOR(@tempint/16) 31 | SELECT @secondint = @tempint - (@firstint*16) 32 | SELECT @charvalue = @charvalue + SUBSTRING(@hexstring, @firstint+1, 1) + SUBSTRING(@hexstring, @secondint+1, 1) 33 | 34 | SELECT @i = @i + 1 35 | END 36 | SELECT @hexvalue = @charvalue 37 | END 38 | go 39 | IF OBJECT_ID ('sp_help_revlogin') IS NOT NULL 40 | DROP PROCEDURE sp_help_revlogin 41 | GO 42 | CREATE PROCEDURE [dbo].[sp_help_revlogin] 43 | ( 44 | @login_name sysname = NULL 45 | ) 46 | AS 47 | BEGIN 48 | DECLARE @name SYSNAME 49 | DECLARE @type VARCHAR (1) 50 | DECLARE @hasaccess INT 51 | DECLARE @denylogin INT 52 | DECLARE @is_disabled INT 53 | DECLARE @PWD_varbinary VARBINARY (256) 54 | DECLARE @PWD_string VARCHAR (514) 55 | DECLARE @SID_varbinary VARBINARY (85) 56 | DECLARE @SID_string VARCHAR (514) 57 | DECLARE @tmpstr VARCHAR (1024) 58 | DECLARE @is_policy_checked VARCHAR (3) 59 | DECLARE @is_expiration_checked VARCHAR (3) 60 | Declare @Prefix VARCHAR(255) 61 | DECLARE @defaultdb SYSNAME 62 | DECLARE @defaultlanguage SYSNAME 63 | DECLARE @tmpstrRole VARCHAR (1024) 64 | 65 | IF (@login_name IS NULL) 66 | BEGIN 67 | DECLARE login_curs CURSOR 68 | FOR 69 | SELECT p.sid, p.name, p.type, p.is_disabled, p.default_database_name, l.hasaccess, l.denylogin, p.default_language_name 70 | FROM sys.server_principals p 71 | LEFT JOIN sys.syslogins l ON ( l.name = p.name ) 72 | WHERE p.type IN ( 'S', 'G', 'U' ) 73 | AND p.name <> 'sa' 74 | ORDER BY p.name 75 | END 76 | ELSE 77 | DECLARE login_curs CURSOR 78 | FOR 79 | SELECT p.sid, p.name, p.type, p.is_disabled, p.default_database_name, l.hasaccess, l.denylogin, p.default_language_name 80 | FROM sys.server_principals p 81 | LEFT JOIN sys.syslogins l ON ( l.name = p.name ) 82 | WHERE p.type IN ( 'S', 'G', 'U' ) 83 | AND p.name = @login_name 84 | ORDER BY p.name 85 | 86 | OPEN login_curs 87 | FETCH NEXT FROM login_curs INTO @SID_varbinary, @name, @type, @is_disabled, @defaultdb, @hasaccess, @denylogin, @defaultlanguage 88 | IF (@@fetch_status = -1) 89 | BEGIN 90 | PRINT 'No login(s) found.' 91 | CLOSE login_curs 92 | DEALLOCATE login_curs 93 | RETURN -1 94 | END 95 | 96 | SET @tmpstr = '/* sp_help_revlogin script ' 97 | PRINT @tmpstr 98 | 99 | SET @tmpstr = '** Generated ' + CONVERT (varchar, GETDATE()) + ' on ' + @@SERVERNAME + ' */' 100 | 101 | PRINT @tmpstr 102 | PRINT '' 103 | 104 | WHILE (@@fetch_status <> -1) 105 | BEGIN 106 | IF (@@fetch_status <> -2) 107 | BEGIN 108 | PRINT '' 109 | 110 | SET @tmpstr = '-- Login: ' + @name 111 | 112 | PRINT @tmpstr 113 | 114 | SET @tmpstr='IF NOT EXISTS (SELECT * FROM sys.server_principals WHERE name = N'''+@name+''') 115 | BEGIN' 116 | Print @tmpstr 117 | 118 | IF (@type IN ( 'G', 'U')) 119 | BEGIN -- NT authenticated account/group 120 | SET @tmpstr = 'CREATE LOGIN ' + QUOTENAME( @name ) + ' FROM WINDOWS WITH DEFAULT_DATABASE = [' + @defaultdb + ']' + ', DEFAULT_LANGUAGE = [' + @defaultlanguage + ']' 121 | END 122 | ELSE 123 | BEGIN -- SQL Server authentication 124 | -- obtain password and sid 125 | SET @PWD_varbinary = CAST( LOGINPROPERTY( @name, 'PasswordHash' ) AS varbinary (256) ) 126 | 127 | EXEC sp_hexadecimal @PWD_varbinary, @PWD_string OUT 128 | EXEC sp_hexadecimal @SID_varbinary,@SID_string OUT 129 | 130 | -- obtain password policy state 131 | SELECT @is_policy_checked = CASE is_policy_checked WHEN 1 THEN 'ON' WHEN 0 THEN 'OFF' ELSE NULL END 132 | FROM sys.sql_logins 133 | WHERE name = @name 134 | 135 | SELECT @is_expiration_checked = CASE is_expiration_checked WHEN 1 THEN 'ON' WHEN 0 THEN 'OFF' ELSE NULL END 136 | FROM sys.sql_logins 137 | WHERE name = @name 138 | 139 | SET @tmpstr = 'CREATE LOGIN ' + QUOTENAME( @name ) + ' WITH PASSWORD = ' + @PWD_string + ' HASHED, SID = ' 140 | + @SID_string + ', DEFAULT_DATABASE = [' + @defaultdb + ']' + ', DEFAULT_LANGUAGE = [' + @defaultlanguage + ']' 141 | 142 | IF ( @is_policy_checked IS NOT NULL ) 143 | BEGIN 144 | SET @tmpstr = @tmpstr + ', CHECK_POLICY = ' + @is_policy_checked 145 | END 146 | 147 | IF ( @is_expiration_checked IS NOT NULL ) 148 | BEGIN 149 | SET @tmpstr = @tmpstr + ', CHECK_EXPIRATION = ' + @is_expiration_checked 150 | END 151 | END 152 | 153 | IF (@denylogin = 1) 154 | BEGIN -- login is denied access 155 | SET @tmpstr = @tmpstr + '; DENY CONNECT SQL TO ' + QUOTENAME( @name ) 156 | END 157 | ELSE IF (@hasaccess = 0) 158 | BEGIN -- login exists but does not have access 159 | SET @tmpstr = @tmpstr + '; REVOKE CONNECT SQL TO ' + QUOTENAME( @name ) 160 | END 161 | IF (@is_disabled = 1) 162 | BEGIN -- login is disabled 163 | SET @tmpstr = @tmpstr + '; ALTER LOGIN ' + QUOTENAME( @name ) + ' DISABLE' 164 | END 165 | 166 | SET @Prefix = ' 167 | EXEC master.dbo.sp_addsrvrolemember @loginame=''' 168 | 169 | SET @tmpstrRole='' 170 | 171 | SELECT @tmpstrRole = @tmpstrRole 172 | + CASE WHEN sysadmin = 1 THEN @Prefix + [LoginName] + ''', @rolename=''sysadmin''' ELSE '' END 173 | + CASE WHEN securityadmin = 1 THEN @Prefix + [LoginName] + ''', @rolename=''securityadmin''' ELSE '' END 174 | + CASE WHEN serveradmin = 1 THEN @Prefix + [LoginName] + ''', @rolename=''serveradmin''' ELSE '' END 175 | + CASE WHEN setupadmin = 1 THEN @Prefix + [LoginName] + ''', @rolename=''setupadmin''' ELSE '' END 176 | + CASE WHEN processadmin = 1 THEN @Prefix + [LoginName] + ''', @rolename=''processadmin''' ELSE '' END 177 | + CASE WHEN diskadmin = 1 THEN @Prefix + [LoginName] + ''', @rolename=''diskadmin''' ELSE '' END 178 | + CASE WHEN dbcreator = 1 THEN @Prefix + [LoginName] + ''', @rolename=''dbcreator''' ELSE '' END 179 | + CASE WHEN bulkadmin = 1 THEN @Prefix + [LoginName] + ''', @rolename=''bulkadmin''' ELSE '' END 180 | FROM ( 181 | SELECT CONVERT(VARCHAR(100),SUSER_SNAME(sid)) AS [LoginName], 182 | sysadmin, 183 | securityadmin, 184 | serveradmin, 185 | setupadmin, 186 | processadmin, 187 | diskadmin, 188 | dbcreator, 189 | bulkadmin 190 | FROM sys.syslogins 191 | WHERE ( sysadmin<>0 192 | OR securityadmin<>0 193 | OR serveradmin<>0 194 | OR setupadmin <>0 195 | OR processadmin <>0 196 | OR diskadmin<>0 197 | OR dbcreator<>0 198 | OR bulkadmin<>0 199 | ) 200 | AND name=@name 201 | ) L 202 | 203 | PRINT @tmpstr 204 | PRINT @tmpstrRole 205 | PRINT 'END' 206 | END 207 | FETCH NEXT FROM login_curs INTO @SID_varbinary, @name, @type, @is_disabled, @defaultdb, @hasaccess, @denylogin, @defaultlanguage 208 | END 209 | CLOSE login_curs 210 | DEALLOCATE login_curs 211 | RETURN 0 212 | END 213 | -------------------------------------------------------------------------------- /sp_helpme.sql: -------------------------------------------------------------------------------- 1 | SET ANSI_NULLS ON; 2 | GO 3 | 4 | SET QUOTED_IDENTIFIER ON; 5 | GO 6 | 7 | IF EXISTS (SELECT * FROM sys.fn_listextendedproperty(N'Description' , N'SCHEMA',N'dbo', N'PROCEDURE',N'sp_helpme', NULL,NULL)) 8 | BEGIN; 9 | EXEC sys.sp_dropextendedproperty @name=N'Description' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_helpme'; 10 | END 11 | GO 12 | 13 | IF EXISTS (SELECT * FROM sys.fn_listextendedproperty(N'@SqlMinorVersion' , N'SCHEMA',N'dbo', N'PROCEDURE',N'sp_helpme', NULL,NULL)) 14 | BEGIN; 15 | EXEC sys.sp_dropextendedproperty @name=N'@SqlMinorVersion' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_helpme'; 16 | END 17 | GO 18 | 19 | IF EXISTS (SELECT * FROM sys.fn_listextendedproperty(N'@SqlMajorVersion' , N'SCHEMA',N'dbo', N'PROCEDURE',N'sp_helpme', NULL,NULL)) 20 | BEGIN; 21 | EXEC sys.sp_dropextendedproperty @name=N'@SqlMajorVersion' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_helpme'; 22 | END 23 | GO 24 | 25 | IF EXISTS (SELECT * FROM sys.fn_listextendedproperty(N'@ExtendedPropertyName' , N'SCHEMA',N'dbo', N'PROCEDURE',N'sp_helpme', NULL,NULL)) 26 | BEGIN; 27 | EXEC sys.sp_dropextendedproperty @name=N'@ExtendedPropertyName' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_helpme'; 28 | END 29 | GO 30 | 31 | IF EXISTS (SELECT * FROM sys.fn_listextendedproperty(N'@ObjectName' , N'SCHEMA',N'dbo', N'PROCEDURE',N'sp_helpme', NULL,NULL)) 32 | BEGIN; 33 | EXEC sys.sp_dropextendedproperty @name=N'@ObjectName' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_helpme'; 34 | END 35 | GO 36 | 37 | /***************************/ 38 | /* Create stored procedure */ 39 | /***************************/ 40 | IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[sp_helpme]') AND [type] IN (N'P', N'PC')) 41 | BEGIN; 42 | EXEC dbo.sp_executesql @statement = N'CREATE PROCEDURE [dbo].[sp_helpme] AS'; 43 | END 44 | GO 45 | 46 | ALTER PROCEDURE [dbo].[sp_helpme] 47 | @ObjectName SYSNAME = NULL 48 | ,@ExtendedPropertyName SYSNAME = 'Description' 49 | /* Parameters defined here for testing only */ 50 | ,@SqlMajorVersion TINYINT = 0 51 | ,@SqlMinorVersion SMALLINT = 0 52 | AS 53 | 54 | /* 55 | sp_helpme - A drop-in modern alternative to sp_help. 56 | 57 | Part of the DBA MultiTool http://dba-multitool.org 58 | 59 | Version: 20230108 60 | 61 | MIT License 62 | 63 | Copyright (c) 2023 John McCall 64 | 65 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 66 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 67 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 68 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 69 | 70 | The above copyright notice and this permission notice shall be included in all copies or substantial 71 | portions of the Software. 72 | 73 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 74 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 75 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 76 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 77 | DEALINGS IN THE SOFTWARE. 78 | 79 | ========= 80 | 81 | Example: 82 | 83 | EXEC sp_helpme 'dbo.Sales'; 84 | 85 | */ 86 | 87 | BEGIN 88 | SET NOCOUNT ON; 89 | 90 | DECLARE @DbName SYSNAME 91 | ,@ObjShortName SYSNAME = N'' 92 | ,@No VARCHAR(5) = 'no' 93 | ,@Yes VARCHAR(5) = 'yes' 94 | ,@None VARCHAR(5) = 'none' 95 | ,@SysObj_Type CHAR(2) 96 | ,@ObjID INT 97 | ,@HasParam INT = 0 98 | ,@HasDepen BIT = 0 99 | ,@HasHidden BIT = 0 100 | ,@HasMasked BIT = 0 101 | ,@SQLString NVARCHAR(MAX) = N'' 102 | ,@Msg NVARCHAR(MAX) = N'' 103 | ,@ParmDefinition NVARCHAR(500); 104 | 105 | /* Find Version */ 106 | IF (@SqlMajorVersion = 0) 107 | BEGIN; 108 | SET @SqlMajorVersion = CAST(SERVERPROPERTY('ProductMajorVersion') AS TINYINT); 109 | END; 110 | IF (@SqlMinorVersion = 0) 111 | BEGIN; 112 | SET @SqlMinorVersion = CAST(SERVERPROPERTY('ProductMinorVersion') AS TINYINT); 113 | END; 114 | 115 | /* Validate Version */ 116 | IF (@SqlMajorVersion < 11) 117 | BEGIN; 118 | SET @Msg = 'SQL Server versions below 2012 are not supported, sorry!'; 119 | RAISERROR(@Msg, 16, 1); 120 | END; 121 | 122 | /* Check for Hidden Columns feature */ 123 | IF 1 = (SELECT COUNT(1) FROM sys.all_columns AS ac WHERE ac.name = 'is_hidden' AND OBJECT_NAME(ac.object_id) = 'all_columns') 124 | BEGIN 125 | SET @HasHidden = 1; 126 | END; 127 | 128 | /* Check for Masked Columns feature */ 129 | IF 1 = (SELECT COUNT(1) FROM sys.all_columns AS ac WHERE ac.name = 'is_masked' AND OBJECT_NAME(ac.object_id) = 'all_columns') 130 | BEGIN 131 | SET @HasMasked = 1; 132 | END; 133 | 134 | -- If no @ObjectName given, give a little info about all objects. 135 | IF (@ObjectName IS NULL) 136 | BEGIN; 137 | SET @SQLString = N'SELECT 138 | [Name] = [o].[name], 139 | [Owner] = USER_NAME(OBJECTPROPERTY([object_id], ''ownerid'')), 140 | [Object_type] = LOWER(REPLACE([o].[type_desc], ''_'', '' '')), 141 | [Create_datetime] = [o].[create_date], 142 | [Modify_datetime] = [o].[modify_date], 143 | [ExtendedProperty] = [ep].[value] 144 | FROM [sys].[all_objects] [o] 145 | LEFT JOIN [sys].[extended_properties] [ep] ON [ep].[major_id] = [o].[object_id] 146 | and [ep].[name] = @ExtendedPropertyName 147 | AND [ep].[minor_id] = 0 148 | AND [ep].[class] = 1 149 | ORDER BY [Owner] ASC, [Object_type] DESC, [Name] ASC;'; 150 | SET @ParmDefinition = N'@ExtendedPropertyName SYSNAME'; 151 | 152 | EXEC sp_executesql @SQLString 153 | ,@ParmDefinition 154 | ,@ExtendedPropertyName; 155 | 156 | -- Display all user types 157 | SET @SQLString = N'SELECT 158 | [User_type] = [name], 159 | [Storage_type] = TYPE_NAME(system_type_id), 160 | [Length] = max_length, 161 | [Prec] = [precision], 162 | [Scale] = [scale], 163 | [Nullable] = CASE WHEN is_nullable = 1 THEN @Yes ELSE @No END, 164 | [Default_name] = ISNULL(OBJECT_NAME(default_object_id), @None), 165 | [Rule_name] = ISNULL(OBJECT_NAME(rule_object_id), @None), 166 | [Collation] = collation_name 167 | FROM sys.types 168 | WHERE user_type_id > 256 169 | ORDER BY [name];'; 170 | SET @ParmDefinition = N'@Yes VARCHAR(5), @No VARCHAR(5), @None VARCHAR(5)'; 171 | 172 | EXEC sp_executesql @SQLString 173 | ,@ParmDefinition 174 | ,@Yes 175 | ,@No 176 | ,@None; 177 | 178 | RETURN(0); 179 | END -- End all Sysobjects 180 | 181 | -- Make sure the @ObjectName is local to the current database. 182 | SELECT @ObjShortName = PARSENAME(@ObjectName,1); 183 | SELECT @DbName = PARSENAME(@ObjectName,3); 184 | IF @DbName IS NULL 185 | SELECT @DbName = DB_NAME(); 186 | ELSE IF @DbName <> DB_NAME() 187 | BEGIN 188 | RAISERROR(15250,-1,-1); 189 | END 190 | 191 | -- @ObjectName must be either sysobjects or systypes: first look in sysobjects 192 | SET @SQLString = N'SELECT @ObjID = object_id 193 | , @SysObj_Type = type 194 | FROM sys.all_objects 195 | WHERE object_id = OBJECT_ID(@ObjectName);'; 196 | SET @ParmDefinition = N'@ObjectName SYSNAME 197 | ,@ObjID INT OUTPUT 198 | ,@SysObj_Type VARCHAR(5) OUTPUT'; 199 | 200 | EXEC sp_executesql @SQLString 201 | ,@ParmDefinition 202 | ,@ObjectName 203 | ,@ObjID OUTPUT 204 | ,@SysObj_Type OUTPUT; 205 | 206 | -- If @ObjectName not in sysobjects, try systypes 207 | IF @ObjID IS NULL 208 | BEGIN 209 | SET @SQLString = N'SELECT @ObjID = user_type_id 210 | FROM sys.types 211 | WHERE name = PARSENAME(@ObjectName,1);'; 212 | SET @ParmDefinition = N'@ObjectName SYSNAME 213 | ,@ObjID INT OUTPUT'; 214 | 215 | EXEC sp_executesql @SQLString 216 | ,@ParmDefinition 217 | ,@ObjectName 218 | ,@ObjID OUTPUT; 219 | 220 | -- If not in systypes, return 221 | IF @ObjID IS NULL 222 | BEGIN 223 | RAISERROR(15009,-1,-1,@ObjectName,@DbName); 224 | END 225 | 226 | -- Data Type help (prec/scale only valid for numerics) 227 | SET @SQLString = N'SELECT 228 | [Type_name] = t.name, 229 | [Storage_type] = type_name(system_type_id), 230 | [Length] = max_length, 231 | [Prec] = [precision], 232 | [Scale] = [scale], 233 | [Nullable] = case when is_nullable=1 then @Yes else @No end, 234 | [Default_name] = isnull(object_name(default_object_id), @None), 235 | [Rule_name] = isnull(object_name(rule_object_id), @None), 236 | [Collation] = collation_name, 237 | [ExtendedProperty] = ep.[value] 238 | FROM [sys].[types] AS [t] 239 | LEFT JOIN [sys].[extended_properties] AS [ep] ON [ep].[major_id] = [t].[user_type_id] 240 | AND [ep].[name] = @ExtendedPropertyName 241 | AND [ep].[minor_id] = 0 242 | AND [ep].[class] = 6 243 | WHERE [user_type_id] = @ObjID'; 244 | SET @ParmDefinition = N'@ObjID INT, @Yes VARCHAR(5), @No VARCHAR(5), @None VARCHAR(5), @ExtendedPropertyName SYSNAME'; 245 | 246 | EXECUTE sp_executesql @SQLString 247 | ,@ParmDefinition 248 | ,@ObjID 249 | ,@Yes 250 | ,@No 251 | ,@None 252 | ,@ExtendedPropertyName; 253 | 254 | RETURN(0); 255 | END --Systypes 256 | 257 | -- FOUND IT IN SYSOBJECT, SO GIVE OBJECT INFO 258 | SET @SQLString = N'SELECT 259 | [Name] = [o].[name], 260 | [Owner] = USER_NAME(ObjectProperty([o].[object_id], ''ownerid'')), 261 | [Type] = LOWER(REPLACE([o].[type_desc], ''_'', '' '')), 262 | [Created_datetime] = [o].[create_date], 263 | [Modify_datetime] = [o].[modify_date], 264 | [ExtendedProperty] = [ep].[value] 265 | FROM [sys].[all_objects] [o] 266 | LEFT JOIN [sys].[extended_properties] [ep] ON [ep].[major_id] = [o].[object_id] 267 | AND [ep].[name] = @ExtendedPropertyName 268 | AND [ep].[minor_id] = 0 269 | AND [ep].[class] = 1 270 | WHERE [o].[object_id] = @ObjID;'; 271 | 272 | SET @ParmDefinition = N'@ObjID INT, @ExtendedPropertyName SYSNAME'; 273 | 274 | EXEC sp_executesql @SQLString 275 | ,@ParmDefinition 276 | ,@ObjID 277 | ,@ExtendedPropertyName; 278 | 279 | -- Display column metadata if table / view 280 | SET @SQLString = N' 281 | IF EXISTS (SELECT * FROM [sys].[all_columns] WHERE [object_id] = @ObjID) 282 | BEGIN; 283 | 284 | -- SET UP NUMERIC TYPES: THESE WILL HAVE NON-BLANK PREC/SCALE 285 | -- There must be a '','' immediately after each type name (including last one), 286 | -- because that''s what we''ll search for in charindex later. 287 | DECLARE @precscaletypes NVARCHAR(150); 288 | SELECT @precscaletypes = N''tinyint,smallint,decimal,int,bigint,real,money,float,numeric,smallmoney,date,time,datetime2,datetimeoffset,'' 289 | 290 | -- INFO FOR EACH COLUMN 291 | SELECT 292 | [Column_name] = [ac].[name], 293 | [Type] = TYPE_NAME([ac].[user_type_id]), 294 | [Computed] = CASE WHEN ColumnProperty([object_id], [ac].[name], ''IsComputed'') = 0 THEN ''no'' ELSE ''yes'' END, 295 | [Length] = CONVERT(INT, [ac].[max_length]), 296 | -- for prec/scale, only show for those types that have valid precision/scale 297 | -- Search for type name + '','', because ''datetime'' is actually a substring of ''datetime2'' and ''datetimeoffset'' 298 | [Prec] = CASE WHEN CHARINDEX(type_name([ac].[system_type_id]) + '','', '''') > 0 299 | THEN CONVERT(char(5),ColumnProperty([object_id], [ac].[name], ''precision'')) 300 | ELSE '' '' END, 301 | [Scale] = CASE WHEN CHARINDEX(type_name([ac].[system_type_id]) + '','', '''') > 0 302 | THEN CONVERT(char(5),OdbcScale([ac].[system_type_id],[ac].[scale])) 303 | ELSE '' '' END, 304 | [Nullable] = CASE WHEN [ac].[is_nullable] = 0 THEN ''no'' ELSE ''yes'' END, '; 305 | 306 | --Only include if they exist on the current version 307 | IF @HasMasked = 1 308 | BEGIN 309 | SET @SQLString = @SQLString + N'[Masked] = CASE WHEN [is_masked] = 0 THEN ''no'' ELSE ''yes'' END, '; 310 | END 311 | 312 | SET @SQLString = @SQLString + N'[Sparse] = CASE WHEN [is_sparse] = 0 THEN ''no'' ELSE ''yes'' END, '; 313 | 314 | IF @HasHidden = 1 315 | BEGIN 316 | SET @SQLString = @SQLString + N'[Hidden] = CASE WHEN [is_hidden] = 0 THEN ''no'' ELSE ''yes'' END, '; 317 | END 318 | 319 | SET @SQLString = @SQLString + N' 320 | [Identity] = CASE WHEN [is_identity] = 0 THEN ''no'' ELSE ''yes'' END, 321 | [TrimTrailingBlanks] = CASE ColumnProperty([object_id], [ac].[name], ''UsesAnsiTrim'') 322 | WHEN 1 THEN ''no'' 323 | WHEN 0 THEN ''yes'' 324 | ELSE ''(n/a)'' END, 325 | [FixedLenNullInSource] = CASE 326 | WHEN type_name([ac].[system_type_id]) NOT IN (''varbinary'',''varchar'',''binary'',''char'') 327 | THEN ''(n/a)'' 328 | WHEN [ac].[is_nullable] = 0 THEN ''no'' ELSE ''yes'' END, 329 | [Collation] = [ac].[collation_name], 330 | [ExtendedProperty] = [ep].[value] 331 | FROM [sys].[all_columns] AS [ac] 332 | LEFT JOIN [sys].[extended_properties] [ep] ON [ep].[minor_id] = [ac].[column_id] 333 | AND [ep].[major_id] = [ac].[object_id] 334 | AND [ep].[name] = @ExtendedPropertyName 335 | AND [ep].[class] = 1 336 | WHERE [ac].[object_id] = @ObjID 337 | END'; 338 | SET @ParmDefinition = N'@ObjID INT, @ExtendedPropertyName SYSNAME'; 339 | EXEC sp_executesql @SQLString, @ParmDefinition, @ObjID = @ObjID, @ExtendedPropertyName = @ExtendedPropertyName; 340 | 341 | -- Identity & rowguid columns 342 | IF @SysObj_Type IN ('S ','U ','V ','TF') 343 | BEGIN 344 | DECLARE @colname SYSNAME = NULL; 345 | SET @SQLString = N'SELECT @colname = COL_NAME(@ObjID, column_id) 346 | FROM sys.identity_columns 347 | WHERE object_id = @ObjID;'; 348 | SET @ParmDefinition = N'@ObjID INT, @colname SYSNAME OUTPUT'; 349 | 350 | EXEC sp_executesql @SQLString 351 | ,@ParmDefinition 352 | ,@ObjID 353 | ,@colname OUTPUT; 354 | 355 | --Identity 356 | IF (@colname IS NOT NULL) 357 | SELECT 358 | 'Identity' = @colname, 359 | 'Seed' = IDENT_SEED(@ObjectName), 360 | 'Increment' = IDENT_INCR(@ObjectName), 361 | 'Not For Replication' = COLUMNPROPERTY(@ObjID, @colname, 'IsIDNotForRepl'); 362 | ELSE 363 | BEGIN 364 | SET @Msg = 'No identity is defined on object %ls.'; 365 | RAISERROR(@Msg, 10, 1, @ObjectName) WITH NOWAIT; 366 | END 367 | 368 | -- Rowguid 369 | SET @colname = NULL; 370 | SET @SQLString = N'SELECT @colname = [name] 371 | FROM sys.all_columns 372 | WHERE [object_id] = @ObjID AND is_rowguidcol = 1;'; 373 | SET @ParmDefinition = N'@ObjID INT, @colname SYSNAME OUTPUT'; 374 | 375 | EXEC sp_executesql @SQLString 376 | ,@ParmDefinition 377 | ,@ObjID 378 | ,@colname OUTPUT; 379 | 380 | IF (@colname IS NOT NULL) 381 | SELECT 'RowGuidCol' = @colname; 382 | ELSE 383 | BEGIN 384 | SET @Msg = 'No rowguid is defined on object %ls.'; 385 | RAISERROR(@Msg, 10, 1, @ObjectName) WITH NOWAIT; 386 | END 387 | END 388 | 389 | -- Display any procedure parameters 390 | SET @SQLString = N'SELECT TOP (1) @HasParam = 1 FROM sys.all_parameters WHERE object_id = @ObjID'; 391 | SET @ParmDefinition = N'@ObjID INT, @HasParam BIT OUTPUT'; 392 | 393 | EXEC sp_executesql @SQLString 394 | ,@ParmDefinition 395 | ,@ObjID 396 | ,@HasParam OUTPUT; 397 | 398 | --If parameters exist, show them 399 | IF @HasParam = 1 400 | BEGIN 401 | SET @SQLString = N'SELECT 402 | [Parameter_name] = [name], 403 | [Type] = TYPE_NAME(user_type_id), 404 | [Length] = max_length, 405 | [Prec] = CASE WHEN TYPE_NAME(system_type_id) = ''uniqueidentifier'' THEN [precision] 406 | ELSE OdbcPrec(system_type_id, max_length, [precision]) END, 407 | [Scale] = ODBCSCALE(system_type_id, scale), 408 | [Param_order] = parameter_id, 409 | [Collation] = CONVERT([sysname], CASE WHEN system_type_id in (35, 99, 167, 175, 231, 239) 410 | THEN SERVERPROPERTY(''collation'') END) 411 | FROM sys.all_parameters 412 | WHERE [object_id] = @ObjID;'; 413 | SET @ParmDefinition = N'@ObjID INT'; 414 | 415 | EXEC sp_executesql @SQLString 416 | ,@ParmDefinition 417 | ,@ObjID; 418 | END 419 | 420 | -- DISPLAY TABLE INDEXES & CONSTRAINTS 421 | IF @SysObj_Type IN ('S ','U ') 422 | BEGIN 423 | EXEC sys.sp_objectfilegroup @ObjID; 424 | 425 | /* Begin custom included columns for sp_helpindex */ 426 | CREATE TABLE #sp_helpindex ( 427 | index_name SYSNAME COLLATE database_default 428 | ,index_description VARCHAR(210) 429 | ,index_keys NVARCHAR(2126) COLLATE database_default --Length (16*max_identifierLength)+(15*2)+(16*3) 430 | ,index_includes NVARCHAR(MAX) --Length (1023*max_identifierLength)+(15*2)+(16*3) is > 4000 431 | ); 432 | INSERT INTO #sp_helpindex (index_name, index_description, index_keys) 433 | EXEC sys.sp_helpindex @ObjectName; 434 | 435 | IF EXISTS (SELECT 1 FROM #sp_helpindex) 436 | BEGIN 437 | SET @SQLString = N' 438 | WITH includedColumns AS ( 439 | SELECT DISTINCT i2.name AS index_name 440 | , LTRIM(STUFF(( 441 | SELECT '', '' + c.name 442 | FROM sys.indexes i 443 | INNER JOIN ' + QUOTENAME(DB_NAME()) + '.sys.index_columns ic ON i.index_id = ic.index_id 444 | INNER JOIN ' + QUOTENAME(DB_NAME()) + '.sys.columns c ON c.column_id = ic.column_id 445 | WHERE i.object_id = @ObjID 446 | AND ic.object_id = @ObjID 447 | AND c.object_id = @ObjID 448 | AND ic.is_included_column = 1 449 | AND i2.index_id = i.index_id 450 | FOR XML PATH(''''), TYPE).value(''.'', ''NVARCHAR(MAX)''), 1, 1, '''')) AS included 451 | FROM ' + QUOTENAME(DB_NAME()) + '.sys.indexes i2 452 | INNER JOIN #sp_helpindex sp ON sp.index_name COLLATE database_default = i2.name 453 | INNER JOIN ' + QUOTENAME(DB_NAME()) + '.sys.index_columns ic ON i2.index_id = ic.index_id 454 | WHERE i2.object_id = @ObjID 455 | AND ic.object_id = @ObjID 456 | AND ic.is_included_column = 1 457 | ) 458 | UPDATE sp 459 | SET sp.index_includes = ic.included 460 | FROM #sp_helpindex sp 461 | INNER JOIN includedColumns ic ON sp.index_name COLLATE database_default = ic.index_name;'; 462 | SET @ParmDefinition = N'@ObjID INT'; 463 | 464 | EXEC sp_executesql @SQLString 465 | ,@ParmDefinition 466 | ,@ObjID; 467 | END 468 | 469 | IF EXISTS (SELECT 1 FROM #sp_helpindex) 470 | BEGIN 471 | SELECT index_name, index_description, index_keys, index_includes 472 | FROM #sp_helpindex; 473 | END 474 | /* End custom included columns for sp_helpindex */ 475 | 476 | EXEC sys.sp_helpconstraint @ObjectName,'nomsg'; 477 | 478 | SET @SQLString = N'SELECT @HasDepen = COUNT(1) 479 | FROM sys.objects obj, sysdepends deps 480 | WHERE obj.[type] =''V'' 481 | AND obj.[object_id] = deps.id 482 | AND deps.depid = @ObjID 483 | AND deps.deptype = 1;'; 484 | SET @ParmDefinition = N'@ObjID INT, @HasDepen INT OUTPUT'; 485 | 486 | EXEC sp_executesql @SQLString 487 | ,@ParmDefinition 488 | ,@ObjID 489 | ,@HasDepen OUTPUT; 490 | 491 | IF @HasDepen = 0 492 | BEGIN 493 | RAISERROR(15647,-1,-1,@ObjectName); -- No views with schemabinding for reference table '%ls'. 494 | END 495 | ELSE 496 | BEGIN 497 | SET @SQLString = N'SELECT DISTINCT [Table is referenced by views] = OBJECT_SCHEMA_NAME(obj.object_id) + ''.'' + obj.[name] 498 | FROM sys.objects obj 499 | INNER JOIN sysdepends deps ON obj.object_id = deps.id 500 | WHERE obj.[type] =''V'' 501 | AND deps.depid = @ObjID 502 | AND deps.deptype = 1 503 | GROUP BY obj.[name], obj.object_id;'; 504 | SET @ParmDefinition = N'@ObjID INT'; 505 | 506 | EXEC sp_executesql @SQLString 507 | ,@ParmDefinition 508 | ,@ObjID; 509 | END 510 | END 511 | END; 512 | GO 513 | 514 | EXEC sys.sp_addextendedproperty @name=N'Description', @value=N'Drop-in alternative to sp_help. Documentation at https://expresssql.lowlydba.com' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_helpme'; 515 | GO 516 | 517 | EXEC sys.sp_addextendedproperty @name=N'@ObjectName', @value=N'Target object. Default is all objects.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_helpme'; 518 | GO 519 | 520 | EXEC sys.sp_addextendedproperty @name=N'@ExtendedPropertyName', @value=N'Key for extended properties on objects. Default is ''Description''.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_helpme'; 521 | GO 522 | 523 | EXEC sys.sp_addextendedproperty @name=N'@SqlMajorVersion', @value=N'Used for unit testing purposes only.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_helpme'; 524 | GO 525 | 526 | EXEC sys.sp_addextendedproperty @name=N'@SqlMinorVersion', @value=N'Used for unit testing purposes only.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'PROCEDURE',@level1name=N'sp_helpme'; 527 | GO 528 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | Unit tests via [tSQLt](https://tsqlt.org/) and Pester, 4 | code coverage by 5 | [SQLCover](https://github.com/GoEddie/SQLCover) and 6 | linting by [TSQLLint](https://github.com/tsqllint/tsqllint) 7 | and [super-linter](https://github.com/github/super-linter). 8 | 9 | ## How it works 10 | 11 | ### tSQLt unit tests 12 | 13 | Each stored procedure has all of its tSQLt unit tests stored in a single sql script in the `\tests\` folder and 14 | uses the naming convention of `sp_name.Tests.sql`. These should mostly adhere to the following naming conventions: 15 | 16 | - `[sp_name].[test sp fails ...]` 17 | - `[sp_name].[test sp succeeds ...]` 18 | 19 | ### Pester tests 20 | 21 | All of a stored proc's unit tests are run by a single corresponding Pester script, similarly 22 | named `sp_name.Tests.ps1`, which: 23 | 24 | - Installs the corresponding stored procedure's tSQLt tests 25 | - Runs all unit tests for the stored procedure as a single Pester invocation 26 | -------------------------------------------------------------------------------- /tests/sp_doc.Tests.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="Pester"; ModuleVersion="5.1.0" } 2 | param() 3 | 4 | BeforeDiscovery { 5 | #. "$PSScriptRoot\constants.ps1" 6 | } 7 | 8 | Describe "sp_doc" { 9 | Context "tSQLt Tests" { 10 | BeforeAll { 11 | $storedProc = "sp_doc" 12 | $testPath = "tests\" 13 | $testInstallScript = "$storedProc.Tests.sql" 14 | $runTestQuery = "EXEC tSQLt.Run '[$storedProc]'" 15 | $queryTimeout = 300 16 | 17 | $Hash = @{ 18 | ConnectionString = "Data Source=$env:SQLINSTANCE;Initial Catalog=$env:DATABASE;Integrated Security=True;TrustServerCertificate=true" 19 | Verbose = $true 20 | } 21 | 22 | # Install tests 23 | ForEach ($File in Get-ChildItem -Path $testPath -Filter $testInstallScript) { 24 | Invoke-Sqlcmd @Hash -InputFile $File.FullName 25 | } 26 | } 27 | It "All tests" { 28 | { Invoke-Sqlcmd @Hash -Query $runTestQuery -QueryTimeout $queryTimeout } | Should -Not -Throw -Because "tSQLt unit tests must pass" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/sp_doc.Tests.sql: -------------------------------------------------------------------------------- 1 | SET NOCOUNT ON; 2 | SET ANSI_NULLS ON; 3 | SET QUOTED_IDENTIFIER ON; 4 | 5 | /************************************ 6 | Begin sp_doc tests 7 | *************************************/ 8 | 9 | --Clean Class 10 | EXEC [tSQLt].[DropClass] 'sp_doc'; 11 | GO 12 | 13 | EXEC [tSQLT].[NewTestClass] 'sp_doc'; 14 | GO 15 | 16 | /* 17 | ====================== 18 | Test Prep 19 | ====================== 20 | */ 21 | 22 | /* 23 | Perform external test setup due to strange locking behavior 24 | with 1st time adds for data sensitivity classifications 25 | for [test sp returns correct Sensitivity Classification] 26 | and later tests that rely on the classification table column existing 27 | */ 28 | 29 | DECLARE @SqlMajorVersion TINYINT = CAST(SERVERPROPERTY('ProductMajorVersion') AS TINYINT); 30 | DECLARE @DatabaseName SYSNAME = DB_NAME(DB_ID()); 31 | DECLARE @Sql NVARCHAR(MAX); 32 | 33 | -- Exclude SQL 2017 since sensitivity classification is half-baked in that version 34 | IF EXISTS (SELECT 1 FROM [sys].[system_views] WHERE [name] = 'sensitivity_classifications') AND (@SqlMajorVersion <> 14) 35 | BEGIN; 36 | SET @Sql = N'ADD SENSITIVITY CLASSIFICATION TO [tsqlt].[CaptureOutputLog].[OutputText] 37 | WITH (LABEL=''Highly Confidential'', INFORMATION_TYPE=''Financial'', RANK=CRITICAL);'; 38 | EXEC sp_executesql @Sql; 39 | END; 40 | GO 41 | 42 | -- Give Azure SQL Extra time to apply classification 43 | -- IF (@@VERSION LIKE 'Microsoft SQL Azure%') 44 | -- BEGIN; 45 | -- WAITFOR DELAY '00:00:10'; 46 | -- END; 47 | -- GO 48 | 49 | /* 50 | ================= 51 | Positive Testing 52 | ================= 53 | */ 54 | 55 | /* test that sp_doc exists */ 56 | CREATE PROCEDURE [sp_doc].[test sp succeeds on create] 57 | AS 58 | BEGIN 59 | SET NOCOUNT ON; 60 | 61 | DECLARE @ObjectName NVARCHAR(1000) = N'dbo.sp_doc'; 62 | DECLARE @ErrorMessage NVARCHAR(MAX) = N'Stored procedure sp_doc does not exist.'; 63 | 64 | --Assert 65 | EXEC [tSQLt].[AssertObjectExists] @objectName = @objectName, @message = @ErrorMessage; 66 | 67 | END; 68 | GO 69 | 70 | /* test sp succeeds on valid db */ 71 | CREATE PROCEDURE [sp_doc].[test sp succeeds on valid db] 72 | AS 73 | BEGIN 74 | SET NOCOUNT ON; 75 | 76 | DECLARE @db SYSNAME = DB_NAME(DB_ID()); 77 | DECLARE @command NVARCHAR(MAX) = '[dbo].[sp_doc] @DatabaseName = ' + @db + ';'; 78 | 79 | --Assert 80 | EXEC [tSQLt].[ExpectNoException]; 81 | EXEC sp_executesql @command; 82 | --EXEC [tSQLt].[SuppressOutput] @command = @command; 83 | 84 | END; 85 | GO 86 | 87 | /* test sp_doc emoji mode doesn't error */ 88 | CREATE PROCEDURE [sp_doc].[test sp succeeds on emoji mode] 89 | AS 90 | BEGIN 91 | SET NOCOUNT ON; 92 | 93 | DECLARE @db SYSNAME = DB_NAME(DB_ID()); 94 | DECLARE @command NVARCHAR(MAX) = '[dbo].[sp_doc] @DatabaseName = ' + @db + ', @Emojis = 1;'; 95 | 96 | --Assert 97 | EXEC [tSQLt].[ExpectNoException]; 98 | EXEC sp_executesql @command; 99 | --EXEC [tSQLt].[SuppressOutput] @command = @command; 100 | 101 | END; 102 | GO 103 | 104 | /* test sp_doc unlimited stored proc length doesn't error */ 105 | CREATE PROCEDURE [sp_doc].[test sp succeeds with unlimited sp output] 106 | AS 107 | BEGIN 108 | SET NOCOUNT ON; 109 | 110 | DECLARE @db SYSNAME = DB_NAME(DB_ID()); 111 | DECLARE @command NVARCHAR(MAX) = '[dbo].[sp_doc] @DatabaseName = ' + @db + ', @LimitStoredProcLength = 1;'; 112 | 113 | --Assert 114 | EXEC [tSQLt].[ExpectNoException]; 115 | EXEC sp_executesql @command; 116 | --EXEC [tSQLt].[SuppressOutput] @command = @command; 117 | 118 | END; 119 | GO 120 | 121 | /* test sp_doc succeeds on assume current db if none given */ 122 | CREATE PROCEDURE [sp_doc].[test sp succeeds on current db if none given] 123 | AS 124 | BEGIN 125 | SET NOCOUNT ON; 126 | 127 | DECLARE @Verbose BIT = 0; 128 | DECLARE @command NVARCHAR(MAX) = CONCAT('[dbo].[sp_doc] @Verbose = ', @Verbose, ';'); 129 | 130 | --Assert 131 | EXEC [tSQLt].[ExpectNoException]; 132 | EXEC sp_executesql @command; 133 | --EXEC [tSQLt].[SuppressOutput] @command = @command; 134 | 135 | END; 136 | GO 137 | 138 | /* test sp_doc succeeds on supported SQL Server >= v12 */ 139 | CREATE PROCEDURE [sp_doc].[test sp succeeds on supported version] 140 | AS 141 | BEGIN 142 | SET NOCOUNT ON; 143 | 144 | DECLARE @version TINYINT = 13; 145 | DECLARE @Verbose BIT = 0; 146 | DECLARE @command NVARCHAR(MAX) = CONCAT('[dbo].[sp_doc] @SqlMajorVersion = ', @version, ', @Verbose = ', @Verbose, ';'); 147 | 148 | --Assert 149 | EXEC [tSQLt].[ExpectNoException]; 150 | EXEC sp_executesql @command; 151 | --EXEC [tSQLt].[SuppressOutput] @command = @command; 152 | 153 | END; 154 | GO 155 | 156 | /* test sp succeeds with @AllExtendedProperties */ 157 | CREATE PROCEDURE [sp_doc].[test sp_doc succeeds with @AllExtendedProperties] 158 | AS 159 | BEGIN 160 | SET NOCOUNT ON; 161 | 162 | DECLARE @version TINYINT = 13; 163 | DECLARE @Verbose BIT = 0; 164 | DECLARE @AllExtendedProperties BIT = 1; 165 | DECLARE @command NVARCHAR(MAX) = CONCAT('EXEC [dbo].[sp_doc] @SqlMajorVersion = ', @version, ', @Verbose = ', @Verbose, ', @AllExtendedProperties = ', @AllExtendedProperties, ' ;'); 166 | 167 | --Assert 168 | EXEC [tSQLt].[ExpectNoException]; 169 | EXEC sp_executesql @command; 170 | --EXEC [tSQLt].[SuppressOutput] @command = @command; 171 | 172 | END; 173 | GO 174 | 175 | /* test sp_doc returns correct metadata */ 176 | CREATE PROCEDURE [sp_doc].[test sp succeeds on returning desired metadata] 177 | AS 178 | BEGIN 179 | SET NOCOUNT ON; 180 | 181 | EXEC tSQLt.AssertResultSetsHaveSameMetaData 182 | 'SELECT CAST(''test'' AS NVARCHAR(MAX)) as [value]', 183 | 'EXEC [dbo].[sp_doc] @Verbose = 0'; 184 | 185 | END; 186 | GO 187 | 188 | /* test sp_doc returns correct minimum rows */ 189 | CREATE PROCEDURE [sp_doc].[test sp succeeds on returning minimum rowcount] 190 | AS 191 | BEGIN 192 | SET NOCOUNT ON; 193 | 194 | --Rows returned from empty database 195 | DECLARE @TargetRows SMALLINT = 22; 196 | DECLARE @ReturnedRows BIGINT; 197 | DECLARE @FailMessage NVARCHAR(MAX) = N'Minimum number of rows were not returned. Rows returned: '; 198 | DECLARE @Verbose BIT = 0; 199 | 200 | EXEC [dbo].[sp_doc] @Verbose = @Verbose; 201 | SET @ReturnedRows = @@ROWCOUNT; 202 | 203 | IF (@TargetRows > @ReturnedRows) 204 | BEGIN; 205 | EXEC [tSQLt].[Fail] @FailMessage, @ReturnedRows; 206 | END; 207 | 208 | END; 209 | GO 210 | 211 | 212 | /* test sp_doc returns correct Sensitivity Classification 213 | NOTE: Requires test prep at top of this file to run */ 214 | CREATE PROCEDURE [sp_doc].[test sp returns correct Sensitivity Classification] 215 | AS 216 | BEGIN 217 | SET NOCOUNT ON; 218 | 219 | DECLARE @SqlMajorVersion TINYINT = CAST(SERVERPROPERTY('ProductMajorVersion') AS TINYINT); 220 | DECLARE @Verbose BIT = 0; 221 | DECLARE @DatabaseName SYSNAME = DB_NAME(DB_ID()); 222 | DECLARE @Sql NVARCHAR(MAX); 223 | DECLARE @FailMessage NVARCHAR(MAX) = N'Did not find test sensitivity classifications in output.'; 224 | --Don't get this test value as a hit result in the output 225 | DECLARE @Expected VARCHAR(250) = CONCAT('|', ' OutputText | NVARCHAR(MAX) | yes | | | | Label: Highly Confidential
Type: Financial
Rank: CRITICAL
', '|'); 226 | 227 | -- Exclude SQL 2017 since sensitivity classification is half-baked in that version 228 | IF EXISTS (SELECT 1 FROM [sys].[system_views] WHERE [name] = 'sensitivity_classifications') AND (@SqlMajorVersion <> 14) 229 | BEGIN 230 | --Setup 231 | IF OBJECT_ID('tempdb..#result') IS NOT NULL 232 | BEGIN 233 | DROP TABLE #result; 234 | END 235 | CREATE TABLE #result ([markdown] VARCHAR(MAX)); 236 | 237 | --Get results 238 | INSERT INTO #result 239 | EXEC sp_doc @DatabaseName = @DatabaseName, @Verbose = @Verbose; 240 | 241 | --Assert 242 | IF EXISTS (SELECT 1 FROM #result WHERE [markdown] = @Expected) 243 | BEGIN 244 | RETURN; 245 | END; 246 | ELSE 247 | BEGIN 248 | EXEC [tSQLt].[Fail] @FailMessage; 249 | END; 250 | END; 251 | 252 | END; 253 | GO 254 | 255 | /* test sp_doc returns correct table index */ 256 | CREATE PROCEDURE [sp_doc].[test sp returns correct table index] 257 | AS 258 | BEGIN 259 | SET NOCOUNT ON; 260 | 261 | DECLARE @Verbose BIT = 0; 262 | DECLARE @DatabaseName SYSNAME = DB_NAME(DB_ID()); 263 | DECLARE @IndexName SYSNAME = 'idx_IndexTest'; 264 | DECLARE @TableName SYSNAME = 'IndexTest'; 265 | DECLARE @Sql NVARCHAR(MAX); 266 | DECLARE @FailMessage NVARCHAR(1000) = CONCAT('Did not find table index ', QUOTENAME(@IndexName), ' in markdown output.'); 267 | DECLARE @Expected NVARCHAR(250) = CONCAT('| ', 'idx_IndexTest | nonclustered | [id] | | |'); --Don't get this test value as a hit result in the output 268 | 269 | --Setup 270 | IF OBJECT_ID('tempdb..#result') IS NOT NULL 271 | BEGIN 272 | DROP TABLE #result; 273 | END 274 | CREATE TABLE #result ([markdown] VARCHAR(8000)); 275 | 276 | SET @Sql = N'CREATE TABLE [dbo].' + QUOTENAME(@TableName) + '([id] INT); 277 | CREATE NONCLUSTERED INDEX ' + QUOTENAME(@IndexName) + ' ON [dbo].' + QUOTENAME(@TableName) + '([id])'; 278 | EXEC sp_executesql @Sql; 279 | 280 | --Get results 281 | INSERT INTO #result 282 | EXEC sp_doc @DatabaseName = @DatabaseName, @Verbose = @Verbose; 283 | 284 | 285 | 286 | --Assert 287 | IF EXISTS (SELECT 1 FROM #result WHERE [markdown] = @Expected) 288 | BEGIN 289 | RETURN; 290 | END; 291 | ELSE 292 | EXEC [tSQLt].[Fail] @FailMessage; 293 | END; 294 | GO 295 | 296 | /* test sp_doc returns correct view index */ 297 | CREATE PROCEDURE [sp_doc].[test sp returns correct view index] 298 | AS 299 | BEGIN 300 | SET NOCOUNT ON; 301 | 302 | DECLARE @Verbose BIT = 0; 303 | DECLARE @DatabaseName SYSNAME = DB_NAME(DB_ID()); 304 | DECLARE @IndexName SYSNAME = 'idx_IndexTest'; 305 | DECLARE @ViewName SYSNAME = 'vw_IndexTest'; 306 | DECLARE @TableName SYSNAME = 'IndexTest'; 307 | DECLARE @Sql NVARCHAR(MAX); 308 | DECLARE @FailMessage NVARCHAR(1000) = CONCAT('Did not find view index ', QUOTENAME(@IndexName), ' in markdown output.'); 309 | DECLARE @Expected NVARCHAR(250) = CONCAT('| ', 'idx_IndexTest | clustered | [id] | | |'); --Don't get this test value as a hit result in the output 310 | 311 | --Setup 312 | IF OBJECT_ID('tempdb..#result') IS NOT NULL 313 | BEGIN 314 | DROP TABLE #result; 315 | END 316 | CREATE TABLE #result ([markdown] VARCHAR(8000)); 317 | 318 | SET @Sql = N'CREATE TABLE [dbo].' + QUOTENAME(@TableName) + '([id] INT);'; 319 | EXEC sp_executesql @Sql; 320 | SET @Sql = N'CREATE VIEW [dbo].' + QUOTENAME(@ViewName) + ' WITH SCHEMABINDING AS SELECT [id] FROM [dbo].' + QUOTENAME(@TableName) + ';'; 321 | EXEC sp_executesql @Sql; 322 | SET @Sql = N'CREATE UNIQUE CLUSTERED INDEX ' + QUOTENAME(@IndexName) + ' ON [dbo].' + QUOTENAME(@ViewName) + ' ([id]);'; 323 | EXEC sp_executesql @Sql; 324 | 325 | --Get results 326 | INSERT INTO #result 327 | EXEC sp_doc @DatabaseName = @DatabaseName, @Verbose = @Verbose; 328 | 329 | --Assert 330 | IF EXISTS (SELECT 1 FROM #result WHERE [markdown] = @Expected) 331 | BEGIN 332 | RETURN; 333 | END; 334 | ELSE 335 | EXEC [tSQLt].[Fail] @FailMessage; 336 | END; 337 | GO 338 | 339 | /* test sp_doc escapes md right brackets */ 340 | CREATE PROCEDURE [sp_doc].[test sp escapes md right brackets] 341 | AS 342 | BEGIN 343 | SET NOCOUNT ON; 344 | 345 | 346 | DECLARE @Verbose BIT = 0; 347 | DECLARE @DatabaseName SYSNAME = DB_NAME(DB_ID()); 348 | DECLARE @TableName SYSNAME = 'TestTable'; 349 | DECLARE @SchemaName SYSNAME = 'dbo'; 350 | DECLARE @Sql NVARCHAR(MAX); 351 | DECLARE @FailMessage NVARCHAR(1000) = N'Did not find '']'' replaced by '']'' in markdown output.'; 352 | DECLARE @Expected NVARCHAR(250) = N'| Replace | TINYINT | yes | | | this is a bracket ] %'; 353 | 354 | --Setup 355 | IF OBJECT_ID('tempdb..#result') IS NOT NULL 356 | BEGIN 357 | DROP TABLE #result; 358 | END 359 | CREATE TABLE #result ([markdown] NVARCHAR(MAX)); 360 | 361 | SET @Sql = N'CREATE TABLE ' + QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TableName) + '([Replace] TINYINT);'; 362 | EXEC sp_executesql @Sql; 363 | 364 | EXEC sp_addextendedproperty 365 | @name = N'Description', 366 | @value = 'this is a bracket ]', 367 | @level0type = N'Schema', @level0name = 'dbo', 368 | @level1type = N'Table', @level1name = @TableName, 369 | @level2type = N'Column', @level2name = 'Replace'; 370 | 371 | --Get results 372 | INSERT INTO #result 373 | EXEC sp_doc @DatabaseName = @DatabaseName, @Verbose = @Verbose; 374 | 375 | --Assert 376 | IF EXISTS (SELECT 1 FROM #result WHERE [markdown] LIKE @Expected) 377 | BEGIN 378 | RETURN; 379 | END; 380 | ELSE 381 | EXEC [tSQLt].[Fail] @FailMessage; 382 | END; 383 | GO 384 | 385 | /* test sp_doc escapes md pipes */ 386 | CREATE PROCEDURE [sp_doc].[test sp escapes md pipes] 387 | AS 388 | BEGIN 389 | SET NOCOUNT ON; 390 | 391 | SET NOCOUNT ON; 392 | 393 | DECLARE @Verbose BIT = 0; 394 | DECLARE @DatabaseName SYSNAME = DB_NAME(DB_ID()); 395 | DECLARE @TableName SYSNAME = 'TestTable'; 396 | DECLARE @Sql NVARCHAR(MAX); 397 | DECLARE @FailMessage NVARCHAR(1000) = N'Did not find ''|'' replaced by ''|'' in markdown output.'; 398 | DECLARE @Expected NVARCHAR(250) = N'| Replace | TINYINT | yes | | | mario loves |s %'; 399 | 400 | --Setup 401 | IF OBJECT_ID('tempdb..#result') IS NOT NULL 402 | BEGIN 403 | DROP TABLE #result; 404 | END 405 | CREATE TABLE #result ([markdown] VARCHAR(8000)); 406 | 407 | SET @Sql = N'CREATE TABLE [dbo].' + QUOTENAME(@TableName) + '([Replace] TINYINT);'; 408 | EXEC sp_executesql @Sql; 409 | 410 | EXEC sp_addextendedproperty 411 | @name = N'Description', 412 | @value = 'mario loves |s', 413 | @level0type = N'Schema', @level0name = 'dbo', 414 | @level1type = N'Table', @level1name = @TableName, 415 | @level2type = N'Column', @level2name = 'Replace'; 416 | 417 | --Get results 418 | INSERT INTO #result 419 | EXEC sp_doc @DatabaseName = @DatabaseName, @Verbose = @Verbose; 420 | 421 | -- Optimization for small azure sql instance 422 | DELETE FROM #result WHERE [markdown] NOT LIKE '| %'; 423 | 424 | --Assert 425 | IF EXISTS (SELECT 1 FROM #result WHERE [markdown] LIKE @Expected) 426 | BEGIN 427 | RETURN; 428 | END; 429 | ELSE 430 | EXEC [tSQLt].[Fail] @FailMessage; 431 | END; 432 | GO 433 | 434 | /* test sp_doc escapes md ticks */ 435 | CREATE PROCEDURE [sp_doc].[test sp escapes md ticks] 436 | AS 437 | BEGIN 438 | SET NOCOUNT ON; 439 | 440 | DECLARE @Verbose BIT = 0; 441 | DECLARE @DatabaseName SYSNAME = DB_NAME(DB_ID()); 442 | DECLARE @TableName SYSNAME = 'TestTable'; 443 | DECLARE @Sql NVARCHAR(MAX); 444 | DECLARE @FailMessage NVARCHAR(1000) = N'Did not find ''`'' replaced by ''`'' in markdown output.'; 445 | DECLARE @Expected NVARCHAR(250) = N'| Replace | TINYINT | yes | | | watch out for ` season %'; 446 | 447 | --Setup 448 | IF OBJECT_ID('tempdb..#result') IS NOT NULL 449 | BEGIN 450 | DROP TABLE #result; 451 | END 452 | CREATE TABLE #result ([markdown] VARCHAR(8000)); 453 | 454 | SET @Sql = N'CREATE TABLE [dbo].' + QUOTENAME(@TableName) + '([Replace] TINYINT);'; 455 | EXEC sp_executesql @Sql; 456 | 457 | EXEC sp_addextendedproperty 458 | @name = N'Description', 459 | @value = 'watch out for ` season', 460 | @level0type = N'Schema', @level0name = 'dbo', 461 | @level1type = N'Table', @level1name = @TableName, 462 | @level2type = N'Column', @level2name = 'Replace'; 463 | 464 | --Get results 465 | INSERT INTO #result 466 | EXEC sp_doc @DatabaseName = @DatabaseName, @Verbose = @Verbose; 467 | 468 | -- Optimization for small azure sql instance 469 | DELETE FROM #result WHERE [markdown] NOT LIKE '| %'; 470 | 471 | --Assert 472 | IF EXISTS (SELECT 1 FROM #result WHERE [markdown] LIKE @Expected) 473 | BEGIN 474 | RETURN; 475 | END; 476 | ELSE 477 | EXEC [tSQLt].[Fail] @FailMessage; 478 | END; 479 | GO 480 | 481 | /* test sp_doc escapes md line breaks */ 482 | CREATE PROCEDURE [sp_doc].[test sp escapes md line breaks] 483 | AS 484 | BEGIN 485 | SET NOCOUNT ON; 486 | 487 | DECLARE @Verbose BIT = 0; 488 | DECLARE @DatabaseName SYSNAME = DB_NAME(DB_ID()); 489 | DECLARE @TableName SYSNAME = 'TestTable'; 490 | DECLARE @Sql NVARCHAR(MAX); 491 | DECLARE @FailMessage NVARCHAR(1000) = N'Did not find line break replaced by ''
'' in markdown output.'; 492 | DECLARE @Expected NVARCHAR(250) = N'% i want to
break away %'; 493 | 494 | --Setup 495 | IF OBJECT_ID('tempdb..#result') IS NOT NULL 496 | BEGIN 497 | DROP TABLE #result; 498 | END 499 | CREATE TABLE #result ([markdown] VARCHAR(8000)); 500 | 501 | SET @Sql = N'CREATE TABLE [dbo].' + QUOTENAME(@TableName) + '([Replace] TINYINT);'; 502 | EXEC sp_executesql @Sql; 503 | 504 | EXEC sp_addextendedproperty 505 | @name = N'Description', 506 | @value = 'i want to 507 | break away', 508 | @level0type = N'Schema', @level0name = 'dbo', 509 | @level1type = N'Table', @level1name = @TableName, 510 | @level2type = N'Column', @level2name = 'Replace'; 511 | 512 | --Get results 513 | INSERT INTO #result 514 | EXEC sp_doc @DatabaseName = @DatabaseName, @Verbose = @Verbose; 515 | 516 | --Assert 517 | IF EXISTS (SELECT 1 FROM #result WHERE [markdown] LIKE @Expected) 518 | BEGIN 519 | RETURN; 520 | END; 521 | ELSE 522 | EXEC [tSQLt].[Fail] @FailMessage; 523 | END; 524 | GO 525 | 526 | /* 527 | ================= 528 | Negative Testing 529 | ================= 530 | */ 531 | 532 | /* test sp_doc errors on invalid db */ 533 | CREATE PROCEDURE [sp_doc].[test sp fails on invalid db] 534 | AS 535 | BEGIN 536 | SET NOCOUNT ON; 537 | 538 | DECLARE @DatabaseName SYSNAME = 'StarshipVoyager'; 539 | DECLARE @ExpectedMessage NVARCHAR(MAX) = N'Database not available.'; 540 | 541 | --Assert 542 | EXEC [tSQLt].[ExpectException] @ExpectedMessage = @ExpectedMessage; 543 | EXEC [dbo].[sp_doc] @DatabaseName = @DatabaseName; 544 | 545 | END; 546 | GO 547 | 548 | /* test sp_doc fails on unsupported SQL Server < v12 */ 549 | CREATE PROCEDURE [sp_doc].[test sp fails on unsupported version] 550 | AS 551 | BEGIN 552 | SET NOCOUNT ON; 553 | 554 | DECLARE @version TINYINT = 10; 555 | DECLARE @ExpectedMessage NVARCHAR(MAX) = N'SQL Server versions below 2012 are not supported, sorry!'; 556 | 557 | --Assert 558 | EXEC [tSQLt].[ExpectException] @ExpectedMessage = @ExpectedMessage; 559 | EXEC [dbo].[sp_doc] @SqlMajorVersion = @version; 560 | 561 | END; 562 | GO 563 | 564 | /************************************ 565 | End sp_doc tests 566 | *************************************/ 567 | -------------------------------------------------------------------------------- /tests/sp_estindex.Tests.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="Pester"; ModuleVersion="5.1.0" } 2 | param() 3 | 4 | BeforeDiscovery { 5 | #. "$PSScriptRoot\constants.ps1" 6 | } 7 | 8 | Describe "sp_estindex" { 9 | Context "tSQLt Tests" { 10 | BeforeAll { 11 | $storedProc = "sp_estindex" 12 | $testPath = "tests\" 13 | $testInstallScript = "$storedProc.Tests.sql" 14 | $runTestQuery = "EXEC tSQLt.Run '[$storedProc]'" 15 | $queryTimeout = 300 16 | 17 | $Hash = @{ 18 | ConnectionString = "Data Source=$env:SQLINSTANCE;Initial Catalog=$env:DATABASE;Integrated Security=True;TrustServerCertificate=true" 19 | Verbose = $true 20 | } 21 | 22 | # Install tests 23 | ForEach ($File in Get-ChildItem -Path $testPath -Filter $testInstallScript) { 24 | Invoke-Sqlcmd @Hash -InputFile $File.FullName 25 | } 26 | } 27 | It "All tests" { 28 | { Invoke-Sqlcmd @Hash -Query $runTestQuery -QueryTimeout $queryTimeout } | Should -Not -Throw -Because "tSQLt unit tests must pass" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/sp_estindex.Tests.sql: -------------------------------------------------------------------------------- 1 | SET NOCOUNT ON; 2 | SET ANSI_NULLS ON; 3 | SET QUOTED_IDENTIFIER ON; 4 | 5 | /************************************/ 6 | /* Begin sp_estindex tests */ 7 | /************************************/ 8 | 9 | --Clean Class 10 | EXEC tSQLt.DropClass 'sp_estindex'; 11 | GO 12 | 13 | EXEC tSQLT.NewTestClass 'sp_estindex'; 14 | GO 15 | 16 | /****************************** 17 | Success Cases 18 | ******************************/ 19 | 20 | /* 21 | test that sp_estindex exists 22 | */ 23 | CREATE PROCEDURE [sp_estindex].[test sp succeeds on create] 24 | AS 25 | BEGIN 26 | 27 | --Build 28 | DECLARE @ObjectName SYSNAME = 'dbo.sp_estindex'; 29 | DECLARE @Message NVARCHAR(MAX) = 'Stored procedure sp_estindex does not exist.'; 30 | 31 | --Assert 32 | EXEC tSQLt.AssertObjectExists @objectName = @ObjectName 33 | ,@message = @Message; 34 | 35 | END; 36 | GO 37 | 38 | /* 39 | test success on supported SQL Server >= v12 40 | */ 41 | CREATE PROCEDURE [sp_estindex].[test sp succeeds on supported version] 42 | AS 43 | BEGIN; 44 | 45 | --Build 46 | DECLARE @version TINYINT = 13; 47 | DECLARE @Verbose BIT = 0; 48 | DECLARE @command NVARCHAR(MAX) = CONCAT('EXEC [dbo].[sp_estindex] @SqlMajorVersion = ', @version, ', @TableName = ''CaptureOutputLog'', @IndexColumns = ''Id'', @SchemaName = ''tSQLt'', @Verbose =', @Verbose, ';'); 49 | 50 | --Assert 51 | EXEC [tSQLt].[ExpectNoException]; 52 | EXEC [tSQLt].[SuppressOutput] @command = @command; 53 | 54 | END; 55 | GO 56 | 57 | /* 58 | test success on unique index 59 | */ 60 | CREATE PROCEDURE [sp_estindex].[test sp succeeds on unique index] 61 | AS 62 | BEGIN; 63 | 64 | --Build 65 | DECLARE @Verbose BIT = 0; 66 | DECLARE @IsUnique BIT = 1; 67 | DECLARE @TableName SYSNAME = 'CaptureOutputLog'; 68 | DECLARE @IndexColumns NVARCHAR(MAX) = N'Id'; 69 | DECLARE @SchemaName SYSNAME = 'tSQLt'; 70 | DECLARE @command NVARCHAR(MAX) = CONCAT('EXEC [dbo].[sp_estindex] @IsUnique = ',@IsUnique, ', @TableName =''', @TableName, ''', @IndexColumns =''', @IndexColumns, ''', @SchemaName = ''', @SchemaName, ''', @Verbose =', @Verbose, ';'); 71 | 72 | --Assert 73 | EXEC [tSQLt].[ExpectNoException]; 74 | EXEC [tSQLt].[SuppressOutput] @command = @command; 75 | 76 | END; 77 | GO 78 | 79 | 80 | /* 81 | test success on filtered index 82 | */ 83 | CREATE PROCEDURE [sp_estindex].[test sp succeeds on filtered index] 84 | AS 85 | BEGIN; 86 | 87 | --Build 88 | DECLARE @Verbose BIT = 0; 89 | DECLARE @Filter VARCHAR(50) = 'WHERE ID IS NOT NULL'; 90 | DECLARE @command NVARCHAR(MAX) = CONCAT('EXEC [dbo].[sp_estindex] @Filter = ''',@Filter , 91 | ''', @TableName = ''CaptureOutputLog'', @IndexColumns = ''Id'', @SchemaName = ''tSQLt'', @Verbose =', @Verbose, ';'); 92 | 93 | --Assert 94 | EXEC [tSQLt].[ExpectNoException]; 95 | EXEC [tSQLt].[SuppressOutput] @command = @command; 96 | 97 | END; 98 | GO 99 | 100 | /* 101 | test success on non-default fill factor 102 | */ 103 | CREATE PROCEDURE [sp_estindex].[test sp succeeds on non-default fill factor] 104 | AS 105 | BEGIN; 106 | 107 | --Build 108 | DECLARE @Verbose BIT = 0; 109 | DECLARE @FillFactor TINYINT = 50; 110 | DECLARE @command NVARCHAR(MAX) = CONCAT('EXEC [dbo].[sp_estindex] @FillFactor = ',@FillFactor , 111 | ', @TableName = ''CaptureOutputLog'', @IndexColumns = ''Id'', @SchemaName = ''tSQLt'', @Verbose =', @Verbose, ';'); 112 | 113 | --Assert 114 | EXEC [tSQLt].[ExpectNoException]; 115 | EXEC [tSQLt].[SuppressOutput] @command = @command; 116 | 117 | END; 118 | GO 119 | 120 | /* 121 | test success with included columns 122 | */ 123 | CREATE PROCEDURE [sp_estindex].[test sp succeeds on included columns] 124 | AS 125 | BEGIN; 126 | 127 | --Build 128 | DECLARE @Verbose BIT = 1; 129 | DECLARE @IncludeColumns VARCHAR(50) = 'OutputText'; 130 | DECLARE @IndexColumns VARCHAR(50) = 'Id'; 131 | DECLARE @SchemaName SYSNAME = 'tSQLt'; 132 | DECLARE @TableName SYSNAME = 'CaptureOutputLog'; 133 | 134 | DECLARE @command NVARCHAR(MAX) = CONCAT('EXEC [dbo].[sp_estindex] @IncludeColumns = ''',@IncludeColumns , 135 | ''', @TableName = ''', @TableName, ''', @IndexColumns = ''',@IndexColumns, ''', @SchemaName = ''', @SchemaName, ''', @Verbose =', @Verbose, ';'); 136 | 137 | --Assert 138 | EXEC [tSQLt].[ExpectNoException]; 139 | EXEC [tSQLt].[SuppressOutput] @command = @command; 140 | 141 | END; 142 | GO 143 | 144 | /* 145 | test success with unique index on heap 146 | */ 147 | CREATE PROCEDURE [sp_estindex].[test sp succeeds on unique index on heap] 148 | AS 149 | BEGIN; 150 | 151 | --Build 152 | DECLARE @Verbose BIT = 1; 153 | DECLARE @IndexColumns VARCHAR(50) = 'ID'; 154 | DECLARE @TableName SYSNAME = 'TempHeap'; 155 | DECLARE @IsUnique BIT = 1; 156 | DECLARE @DatabaseName SYSNAME = DB_NAME(); 157 | DECLARE @command NVARCHAR(MAX) = CONCAT('EXEC [dbo].[sp_estindex] @DatabaseName =''', @DatabaseName, ''', @TableName = ''', @TableName, ''', @IndexColumns = ''',@IndexColumns, ''', @IsUnique =', @IsUnique, ', @Verbose =', @Verbose, ';'); 158 | DECLARE @ResultSetNumber TINYINT = 5; --5 = estimated index size 159 | DECLARE @FailMessage NVARCHAR(MAX) = N'Index size estimation failed - not > 0.'; 160 | 161 | --Populate table to build index for 162 | IF OBJECT_ID('tsql.dbo.TempHeap') IS NOT NULL 163 | BEGIN 164 | DROP TABLE dbo.TempHeap; 165 | END 166 | 167 | CREATE TABLE TempHeap(ID INT); 168 | 169 | WITH Nums(Number) AS 170 | (SELECT 1 AS [Number] 171 | UNION ALL 172 | SELECT Number+1 FROM [Nums] WHERE [Number] < 1000 173 | ) 174 | INSERT INTO dbo.TempHeap(ID) 175 | SELECT [Number] FROM [Nums] OPTION(MAXRECURSION 1000); 176 | 177 | --Create empty table for result set 178 | CREATE TABLE #Result ( 179 | [Est. KB] DECIMAL(10,3) 180 | ,[Est. MB] DECIMAL(10,3) 181 | ,[Est. GB] DECIMAL(10,3) 182 | ); 183 | 184 | --Assert 185 | EXEC [tSQLt].[ExpectNoException]; 186 | INSERT INTO #Result 187 | EXEC [tSQLt].[ResultSetFilter] @ResultSetNumber 188 | ,@command = @command; 189 | 190 | DECLARE @EstKB DECIMAL(10,3) = (SELECT [Est. KB] FROM #Result); 191 | 192 | IF (@EstKB IS NULL) OR (@EstKB <= 0.0) 193 | BEGIN; 194 | EXEC [tSQLt].[Fail] @FailMessage, @EstKb; 195 | END; 196 | 197 | --Teardown 198 | DROP TABLE dbo.TempHeap; 199 | DROP TABLE #Result; 200 | 201 | END; 202 | GO 203 | 204 | /* 205 | test success with non-unique index on heap 206 | */ 207 | CREATE PROCEDURE [sp_estindex].[test sp succeeds on non-unique index on heap] 208 | AS 209 | BEGIN; 210 | 211 | --Build 212 | DECLARE @Verbose BIT = 1; 213 | DECLARE @IndexColumns VARCHAR(50) = 'ID'; 214 | DECLARE @TableName SYSNAME = 'TempHeap'; 215 | DECLARE @IsUnique BIT = 0; 216 | DECLARE @DatabaseName SYSNAME = DB_NAME(); 217 | DECLARE @command NVARCHAR(MAX) = CONCAT('EXEC [dbo].[sp_estindex] @DatabaseName =''', @DatabaseName, ''', @TableName = ''', @TableName, ''', @IndexColumns = ''',@IndexColumns, ''', @IsUnique =', @IsUnique, ', @Verbose =', @Verbose, ';'); 218 | DECLARE @ResultSetNumber TINYINT = 5; --5 = estimated index size 219 | DECLARE @FailMessage NVARCHAR(MAX) = N'Index size estimation failed - not > 0.'; 220 | 221 | --Populate table to build index for 222 | IF OBJECT_ID('dbo.TempHeap') IS NOT NULL 223 | BEGIN 224 | DROP TABLE dbo.TempHeap; 225 | END 226 | 227 | CREATE TABLE dbo.TempHeap( 228 | ID INT); 229 | 230 | WITH Nums(Number) AS 231 | (SELECT 1 AS [Number] 232 | UNION ALL 233 | SELECT Number+1 FROM [Nums] WHERE [Number] < 1000 234 | ) 235 | INSERT INTO dbo.TempHeap(ID) 236 | SELECT [Number] FROM [Nums] OPTION(MAXRECURSION 1000); 237 | 238 | --Create empty table for result set 239 | CREATE TABLE #Result ( 240 | [Est. KB] DECIMAL(10,3) 241 | ,[Est. MB] DECIMAL(10,3) 242 | ,[Est. GB] DECIMAL(10,3) 243 | ); 244 | 245 | --Assert 246 | EXEC [tSQLt].[ExpectNoException]; 247 | INSERT INTO #Result 248 | EXEC [tSQLt].[ResultSetFilter] @ResultSetNumber, @command = @command; 249 | 250 | DECLARE @EstKB DECIMAL(10,3) = (SELECT [Est. KB] FROM #Result); 251 | 252 | IF (@EstKB IS NULL) OR (@EstKB <= 0.0) 253 | BEGIN; 254 | EXEC [tSQLt].[Fail] @FailMessage, @EstKb; 255 | END; 256 | 257 | --Teardown 258 | DROP TABLE dbo.TempHeap; 259 | DROP TABLE #Result; 260 | 261 | END; 262 | GO 263 | 264 | /* 265 | test success with unique index on clustered 266 | */ 267 | CREATE PROCEDURE [sp_estindex].[test sp succeeds on unique index on clustered] 268 | AS 269 | BEGIN; 270 | 271 | --Build 272 | DECLARE @Verbose BIT = 1; 273 | DECLARE @IndexColumns VARCHAR(50) = 'ID'; 274 | DECLARE @TableName SYSNAME = 'TempClustered'; 275 | DECLARE @DatabaseName SYSNAME = DB_NAME(); 276 | DECLARE @IsUnique BIT = 1; 277 | DECLARE @command NVARCHAR(MAX) = CONCAT('EXEC [dbo].[sp_estindex] @DatabaseName =''', @DatabaseName, ''', @TableName = ''', @TableName, ''', @IsUnique =', @IsUnique, ', @IndexColumns = ''',@IndexColumns, ''', @Verbose =', @Verbose, ';'); 278 | DECLARE @ResultSetNumber TINYINT = 5; --5 = estimated index size 279 | DECLARE @FailMessage NVARCHAR(MAX) = N'Index size estimation failed - not > 0.'; 280 | 281 | IF OBJECT_ID('dbo.TempClustered') IS NOT NULL 282 | BEGIN 283 | DROP TABLE dbo.TempClustered; 284 | END 285 | 286 | --Populate table to build index for 287 | CREATE TABLE dbo.TempClustered(ID INT); 288 | 289 | CREATE CLUSTERED INDEX cdx_temporary ON dbo.TempClustered(ID); 290 | 291 | 292 | ;WITH Nums(Number) AS 293 | (SELECT 1 AS [Number] 294 | UNION ALL 295 | SELECT Number+1 FROM [Nums] WHERE [Number]<1000 296 | ) 297 | INSERT INTO dbo.TempClustered(ID) 298 | SELECT [Number] FROM [Nums] OPTION(maxrecursion 1000); 299 | 300 | --Create empty table for result set 301 | CREATE TABLE #Result ( 302 | [Est. KB] DECIMAL(10,3) 303 | ,[Est. MB] DECIMAL(10,3) 304 | ,[Est. GB] DECIMAL(10,3) 305 | ); 306 | 307 | 308 | --Assert 309 | EXEC [tSQLt].[ExpectNoException]; 310 | INSERT INTO #Result 311 | EXEC [tSQLt].[ResultSetFilter] @ResultSetNumber 312 | ,@command = @command; 313 | 314 | DECLARE @EstKB DECIMAL(10,3) = (SELECT [Est. KB] FROM #Result); 315 | 316 | IF (@EstKB IS NULL) OR (@EstKB <= 0.0) 317 | BEGIN; 318 | EXEC [tSQLt].[Fail] @FailMessage, @EstKb; 319 | END; 320 | 321 | --Teardown 322 | DROP TABLE dbo.TempClustered; 323 | DROP TABLE #Result; 324 | 325 | END; 326 | GO 327 | 328 | /* 329 | test success with multi-leaf index 330 | */ 331 | CREATE PROCEDURE [sp_estindex].[test sp succeeds on multi-leaf index] 332 | AS 333 | BEGIN; 334 | 335 | --Build 336 | DECLARE @Verbose BIT = 1; 337 | DECLARE @IndexColumns VARCHAR(50) = 'ID'; 338 | DECLARE @TableName SYSNAME = 'TempHeap'; 339 | DECLARE @DatabaseName SYSNAME = DB_NAME(); 340 | DECLARE @IsUnique BIT = 1; 341 | DECLARE @command NVARCHAR(MAX) = CONCAT('EXEC [dbo].[sp_estindex] @DatabaseName =''', @DatabaseName, ''', @TableName = ''', @TableName, ''', @IsUnique =', @IsUnique, ', @IndexColumns = ''',@IndexColumns, ''', @Verbose =', @Verbose, ';'); 342 | DECLARE @ResultSetNumber TINYINT = 5; --5 = estimated index size 343 | DECLARE @FailMessage NVARCHAR(MAX) = N'Index size estimation failed - not > 0.'; 344 | 345 | IF OBJECT_ID('dbo.TempHeap') IS NOT NULL 346 | BEGIN 347 | DROP TABLE dbo.TempHeap; 348 | END 349 | 350 | --Populate table 351 | CREATE TABLE dbo.TempHeap( 352 | ID INT); 353 | 354 | ;WITH Nums(Number) AS 355 | (SELECT 1 AS [Number] 356 | UNION ALL 357 | SELECT Number+1 FROM [Nums] WHERE [Number]<10000 358 | ) 359 | INSERT INTO dbo.TempHeap(ID) 360 | SELECT [Number] FROM [Nums] OPTION(maxrecursion 10000); 361 | 362 | --Create empty table for result set 363 | CREATE TABLE #Result ( 364 | [Est. KB] DECIMAL(10,3) 365 | ,[Est. MB] DECIMAL(10,3) 366 | ,[Est. GB] DECIMAL(10,3) 367 | ); 368 | 369 | --Assert 370 | EXEC [tSQLt].[ExpectNoException]; 371 | INSERT INTO #Result 372 | EXEC [tSQLt].[ResultSetFilter] @ResultSetNumber 373 | ,@command = @command; 374 | 375 | DECLARE @EstKB DECIMAL(10,3) = (SELECT [Est. KB] FROM #Result); 376 | 377 | IF (@EstKB IS NULL) OR (@EstKB <= 0.0) 378 | BEGIN; 379 | EXEC [tSQLt].[Fail] @FailMessage, @EstKb; 380 | END; 381 | 382 | --Teardown 383 | DROP TABLE dbo.TempHeap; 384 | DROP TABLE #Result; 385 | 386 | END; 387 | GO 388 | 389 | /* 390 | test success with non-unique index on clustered 391 | */ 392 | CREATE PROCEDURE [sp_estindex].[test sp succeeds on non-unique index on clustered] 393 | AS 394 | BEGIN; 395 | 396 | --Build 397 | DECLARE @Verbose BIT = 1; 398 | DECLARE @IndexColumns VARCHAR(50) = 'ID'; 399 | DECLARE @TableName SYSNAME = 'TempClustered'; 400 | DECLARE @DatabaseName SYSNAME = DB_NAME(); 401 | DECLARE @IsUnique BIT = 0; 402 | DECLARE @command NVARCHAR(MAX) = CONCAT('EXEC [dbo].[sp_estindex] @DatabaseName =''', @DatabaseName, ''', @TableName = ''', @TableName, ''', @IsUnique =', @IsUnique, ', @IndexColumns = ''',@IndexColumns, ''', @Verbose =', @Verbose, ';'); 403 | DECLARE @ResultSetNumber TINYINT = 5; --5 = estimated index size 404 | DECLARE @FailMessage NVARCHAR(MAX) = N'Index size estimation failed - not > 0.'; 405 | 406 | IF OBJECT_ID('dbo.TempClustered') IS NOT NULL 407 | BEGIN 408 | DROP TABLE dbo.TempClustered; 409 | END 410 | 411 | --Populate table to build index for 412 | CREATE TABLE dbo.TempClustered( 413 | ID INT); 414 | 415 | 416 | ;WITH Nums(Number) AS 417 | (SELECT 1 AS [Number] 418 | UNION ALL 419 | SELECT Number+1 FROM [Nums] WHERE [Number]<1000 420 | ) 421 | INSERT INTO dbo.TempClustered(ID) 422 | SELECT [Number] FROM [Nums] OPTION(maxrecursion 1000); 423 | 424 | CREATE CLUSTERED INDEX cdx_temporary ON dbo.TempClustered(ID); 425 | 426 | --Create empty table for result set 427 | CREATE TABLE #Result ( 428 | [Est. KB] DECIMAL(10,3) 429 | ,[Est. MB] DECIMAL(10,3) 430 | ,[Est. GB] DECIMAL(10,3) 431 | ); 432 | 433 | --Assert 434 | EXEC [tSQLt].[ExpectNoException]; 435 | INSERT INTO #Result 436 | EXEC [tSQLt].[ResultSetFilter] @ResultSetNumber 437 | ,@command = @command; 438 | 439 | DECLARE @EstKB DECIMAL(10,3) = (SELECT [Est. KB] FROM #Result); 440 | 441 | IF (@EstKB IS NULL) OR (@EstKB <= 0.0) 442 | BEGIN; 443 | EXEC [tSQLt].[Fail] @FailMessage, @EstKb; 444 | END; 445 | 446 | --Teardown 447 | DROP TABLE dbo.TempClustered; 448 | DROP TABLE #Result; 449 | 450 | END; 451 | GO 452 | 453 | /* 454 | test success with existing ##TempMissingIndex 455 | */ 456 | 457 | -- CREATE PROCEDURE [sp_estindex].[test sp succeeds on existing ##TempMissingIndex] 458 | -- AS 459 | -- BEGIN; 460 | 461 | -- --Build 462 | -- DECLARE @Verbose BIT = 1; 463 | -- DECLARE @IncludeColumns VARCHAR(50) = 'OutputText'; 464 | -- DECLARE @IndexColumns VARCHAR(50) = 'Id'; 465 | -- DECLARE @SchemaName SYSNAME = 'tSQLt'; 466 | -- DECLARE @TableName SYSNAME = 'CaptureOutputLog'; 467 | 468 | -- SELECT 1 AS [one] 469 | -- INTO ##TempMissingIndex; 470 | 471 | -- DECLARE @command NVARCHAR(MAX) = CONCAT('EXEC [dbo].[sp_estindex] @TableName = ''', @TableName, ''', @IndexColumns = ''',@IndexColumns, ''', @SchemaName = ''', @SchemaName, ''', @Verbose =', @Verbose, ';'); 472 | 473 | -- --Assert 474 | -- EXEC [tSQLt].[ExpectNoException]; 475 | -- EXEC [tSQLt].[SuppressOutput] @command = @command; 476 | 477 | -- END; 478 | -- GO 479 | 480 | 481 | /* 482 | test success with nullable columns 483 | */ 484 | CREATE PROCEDURE [sp_estindex].[test sp succeeds on nullable columns] 485 | AS 486 | BEGIN; 487 | 488 | --Build 489 | DECLARE @Verbose BIT = 0; 490 | DECLARE @IndexColumns VARCHAR(50) = 'name'; 491 | DECLARE @SchemaName SYSNAME = 'dbo'; 492 | DECLARE @TableName SYSNAME = 'NullableTest'; 493 | 494 | CREATE TABLE dbo.NullableTest ( 495 | [name] varchar(50) NULL 496 | ); 497 | 498 | DECLARE @command NVARCHAR(MAX) = CONCAT('EXEC [dbo].[sp_estindex] @TableName = ''', @TableName, ''', @IndexColumns = ''',@IndexColumns, ''', @SchemaName = ''', @SchemaName, ''', @Verbose =', @Verbose, ';'); 499 | 500 | --Assert 501 | EXEC [tSQLt].[ExpectNoException]; 502 | EXEC [tSQLt].[SuppressOutput] @command = @command; 503 | 504 | END; 505 | GO 506 | 507 | /* 508 | test success with variable len columns 509 | */ 510 | CREATE PROCEDURE [sp_estindex].[test sp succeeds on variable len columns] 511 | AS 512 | BEGIN; 513 | 514 | --Build 515 | DECLARE @Verbose BIT = 1; 516 | DECLARE @IndexColumns VARCHAR(50) = 'ID'; 517 | DECLARE @TableName SYSNAME = 'TempHeap'; 518 | DECLARE @DatabaseName SYSNAME = DB_NAME(); 519 | DECLARE @command NVARCHAR(MAX) = CONCAT('EXEC [dbo].[sp_estindex] @DatabaseName =''', @DatabaseName, ''', @TableName = ''', @TableName, ''', @IndexColumns = ''',@IndexColumns, ''', @Verbose =', @Verbose, ';'); 520 | 521 | IF OBJECT_ID('dbo.TempHeap') IS NOT NULL 522 | BEGIN; 523 | DROP TABLE dbo.TempHeap; 524 | END 525 | 526 | --Create table 527 | CREATE TABLE dbo.TempHeap(ID NVARCHAR(200)); 528 | 529 | --Assert 530 | EXEC [tSQLt].[ExpectNoException]; 531 | EXEC [tSQLt].[SuppressOutput] @command = @command; 532 | 533 | --Teardown 534 | DROP TABLE dbo.TempHeap; 535 | 536 | END; 537 | GO 538 | 539 | /* 540 | test success with verbose mode 541 | */ 542 | CREATE PROCEDURE [sp_estindex].[test sp succeeds on verbose mode] 543 | AS 544 | BEGIN; 545 | 546 | --Build 547 | DECLARE @Verbose BIT = 1; 548 | DECLARE @IndexColumns VARCHAR(50) = 'name'; 549 | DECLARE @SchemaName SYSNAME = 'dbo'; 550 | DECLARE @TableName SYSNAME = 'VerboseTest'; 551 | 552 | CREATE TABLE dbo.VerboseTest ( 553 | [name] varchar(50) NULL 554 | ); 555 | 556 | DECLARE @command NVARCHAR(MAX) = CONCAT('EXEC [dbo].[sp_estindex] @TableName = ''', @TableName, ''', @IndexColumns = ''',@IndexColumns, ''', @SchemaName = ''', @SchemaName, ''', @Verbose =', @Verbose, ';'); 557 | 558 | --Assert 559 | EXEC [tSQLt].[ExpectNoException]; 560 | EXEC [tSQLt].[SuppressOutput] @command = @command; 561 | 562 | END; 563 | GO 564 | 565 | /* 566 | test success with variable len include columns 567 | */ 568 | CREATE PROCEDURE [sp_estindex].[test sp succeeds on variable len include columns] 569 | AS 570 | BEGIN; 571 | 572 | --Build 573 | DECLARE @Verbose BIT = 1; 574 | DECLARE @IndexColumns VARCHAR(50) = 'ID'; 575 | DECLARE @IncludeColumns VARCHAR(100) = 'Description'; 576 | DECLARE @TableName SYSNAME = 'TempHeap'; 577 | DECLARE @DatabaseName SYSNAME = DB_NAME(); 578 | DECLARE @command NVARCHAR(MAX) = CONCAT('EXEC [dbo].[sp_estindex] @DatabaseName =''', @DatabaseName, 579 | ''', @TableName = ''', @TableName, 580 | ''', @IndexColumns = ''',@IndexColumns, 581 | ''', @IncludeColumns = ''',@IncludeColumns, 582 | ''', @Verbose =', @Verbose, ';'); 583 | 584 | IF OBJECT_ID('dbo.TempHeap') IS NOT NULL 585 | BEGIN; 586 | DROP TABLE dbo.TempHeap; 587 | END 588 | 589 | --Create table 590 | CREATE TABLE dbo.TempHeap(ID INT, [Description] NVARCHAR(200)); 591 | 592 | --Assert 593 | EXEC [tSQLt].[ExpectNoException]; 594 | EXEC [tSQLt].[SuppressOutput] @command = @command; 595 | 596 | --Teardown 597 | DROP TABLE dbo.TempHeap; 598 | 599 | END; 600 | GO 601 | 602 | 603 | /* 604 | test success without @SchemaName 605 | */ 606 | CREATE PROCEDURE [sp_estindex].[test sp succeeds on no @SchemaName] 607 | AS 608 | BEGIN; 609 | 610 | --Build 611 | DECLARE @Verbose BIT = 1; 612 | DECLARE @IndexColumns VARCHAR(50) = 'ID'; 613 | DECLARE @TableName SYSNAME = 'TempHeap'; 614 | DECLARE @DatabaseName SYSNAME = DB_NAME(); 615 | DECLARE @command NVARCHAR(MAX) = CONCAT('EXEC [dbo].[sp_estindex] @DatabaseName =''', @DatabaseName, ''', @TableName = ''', @TableName, ''', @IndexColumns = ''',@IndexColumns, ''', @Verbose =', @Verbose, ';'); 616 | 617 | IF OBJECT_ID('dbo.TempHeap') IS NOT NULL 618 | BEGIN; 619 | DROP TABLE dbo.TempHeap; 620 | END 621 | 622 | --Create table 623 | CREATE TABLE dbo.TempHeap(ID INT); 624 | 625 | --Assert 626 | EXEC [tSQLt].[ExpectNoException]; 627 | EXEC [tSQLt].[SuppressOutput] @command = @command; 628 | 629 | --Teardown 630 | DROP TABLE dbo.TempHeap; 631 | 632 | END; 633 | GO 634 | 635 | /************************************ 636 | Failure cases 637 | *************************************/ 638 | 639 | /* 640 | test failure on unsupported SQL Server < v12 641 | */ 642 | CREATE PROCEDURE [sp_estindex].[test sp fails on unsupported version] 643 | AS 644 | BEGIN; 645 | 646 | --Build 647 | DECLARE @version TINYINT = 10; 648 | DECLARE @Verbose BIT = 0; 649 | DECLARE @TableName VARCHAR(50) = 'DoesntMatter'; 650 | DECLARE @IndexColumns VARCHAR(50) = 'AlsoDoesntMatter'; 651 | 652 | DECLARE @ExpectedMessage NVARCHAR(MAX) = '[sp_estindex]: SQL Server versions below 2012 are not supported, sorry!'; 653 | DECLARE @ExpectedSeverity TINYINT = 16; 654 | DECLARE @ExpectedState TINYINT = 1; 655 | DECLARE @ExpectedErrorNumber INT = 50000; 656 | 657 | --Assert 658 | EXEC [tSQLt].[ExpectException] @ExpectedMessage = @ExpectedMessage 659 | ,@ExpectedSeverity = @ExpectedSeverity 660 | ,@ExpectedState = @ExpectedState 661 | ,@ExpectedErrorNumber = @ExpectedErrorNumber; 662 | EXEC [dbo].[sp_estindex] @SqlMajorVersion = @version 663 | ,@TableName = @TableName 664 | ,@IndexColumns = @IndexColumns 665 | ,@Verbose = @Verbose; 666 | 667 | END; 668 | GO 669 | 670 | /* 671 | test failure with no @IndexColumns 672 | */ 673 | CREATE PROCEDURE [sp_estindex].[test sp fails on no @IndexColumns] 674 | AS 675 | BEGIN; 676 | 677 | --Build 678 | DECLARE @Verbose BIT = 0; 679 | DECLARE @TableName VARCHAR(50) = 'DoesntMatter'; 680 | DECLARE @ExpectedMessage NVARCHAR(MAX) = N'Procedure or function ''sp_estindex'' expects parameter ''@IndexColumns'', which was not supplied.'; 681 | DECLARE @ExpectedSeverity TINYINT = 16; 682 | DECLARE @ExpectedState TINYINT = 4; 683 | DECLARE @ExpectedErrorNumber INT = 201; 684 | 685 | --Assert 686 | EXEC [tSQLt].[ExpectException] @ExpectedMessage = @ExpectedMessage 687 | ,@ExpectedSeverity = @ExpectedSeverity 688 | ,@ExpectedState = @ExpectedState 689 | ,@ExpectedErrorNumber = @ExpectedErrorNumber; 690 | EXEC [dbo].[sp_estindex] @TableName = @TableName 691 | ,@Verbose = @Verbose; 692 | 693 | END; 694 | GO 695 | 696 | /* 697 | test failure with no @TableName 698 | */ 699 | CREATE PROCEDURE [sp_estindex].[test sp fails on no @TableName] 700 | AS 701 | BEGIN; 702 | 703 | --Build 704 | DECLARE @Verbose BIT = 0; 705 | DECLARE @IncludedColumns VARCHAR(50) = 'DoesntMatter'; 706 | 707 | DECLARE @ExpectedMessage NVARCHAR(MAX) = N'Procedure or function ''sp_estindex'' expects parameter ''@TableName'', which was not supplied.'; 708 | DECLARE @ExpectedSeverity TINYINT = 16; 709 | DECLARE @ExpectedState TINYINT = 4; 710 | DECLARE @ExpectedErrorNumber INT = 201; 711 | 712 | --Assert 713 | EXEC [tSQLt].[ExpectException] @ExpectedMessage = @ExpectedMessage 714 | ,@ExpectedSeverity = @ExpectedSeverity 715 | ,@ExpectedState = @ExpectedState 716 | ,@ExpectedErrorNumber = @ExpectedErrorNumber; 717 | EXEC [dbo].[sp_estindex] @IncludedColumns = @IncludedColumns 718 | ,@Verbose = @Verbose; 719 | 720 | END; 721 | GO 722 | 723 | /* 724 | test failure with invalid @IndexColumns 725 | */ 726 | CREATE PROCEDURE [sp_estindex].[test sp fails on invalid @IndexColumns] 727 | AS 728 | BEGIN; 729 | 730 | --Build 731 | DECLARE @Verbose BIT = 0; 732 | DECLARE @IndexColumns VARCHAR(50) = 'BadColumnName'; 733 | DECLARE @SchemaName SYSNAME = 'tSQLt'; 734 | DECLARE @TableName SYSNAME = 'CaptureOutputLog'; 735 | 736 | DECLARE @ExpectedMessage NVARCHAR(MAX) = CONCAT('[sp_estindex]: Column name ''', @IndexColumns, ''' does not exist in the target table or view.'); 737 | DECLARE @ExpectedSeverity TINYINT = 16; 738 | DECLARE @ExpectedState TINYINT = 1; 739 | DECLARE @ExpectedErrorNumber INT = 50000; 740 | 741 | --Assert 742 | EXEC [tSQLt].[ExpectException] @ExpectedMessage = @ExpectedMessage 743 | ,@ExpectedSeverity = @ExpectedSeverity 744 | ,@ExpectedState = @ExpectedState 745 | ,@ExpectedErrorNumber = @ExpectedErrorNumber; 746 | EXEC [dbo].[sp_estindex] @IndexColumns = @IndexColumns 747 | ,@TableName = @TableName 748 | ,@SchemaName = @SchemaName 749 | ,@Verbose = @Verbose; 750 | 751 | END; 752 | GO 753 | 754 | 755 | /* 756 | test failure with invalid @Database 757 | */ 758 | CREATE PROCEDURE [sp_estindex].[test sp fails on invalid @Database] 759 | AS 760 | BEGIN; 761 | 762 | --Build 763 | DECLARE @Verbose BIT = 0; 764 | DECLARE @IndexColumns VARCHAR(50) = 'BadColumnName'; 765 | DECLARE @SchemaName SYSNAME = 'tSQLt'; 766 | DECLARE @TableName SYSNAME = 'CaptureOutputLog'; 767 | DECLARE @DatabaseName SYSNAME = 'IDontExist'; 768 | 769 | DECLARE @ExpectedMessage NVARCHAR(MAX) = '[sp_estindex]: Database does not exist.'; 770 | DECLARE @ExpectedSeverity TINYINT = 16; 771 | DECLARE @ExpectedState TINYINT = 1; 772 | DECLARE @ExpectedErrorNumber INT = 50000; 773 | 774 | --Assert 775 | EXEC [tSQLt].[ExpectException] @ExpectedMessage = @ExpectedMessage 776 | ,@ExpectedSeverity = @ExpectedSeverity 777 | ,@ExpectedState = @ExpectedState 778 | ,@ExpectedErrorNumber = @ExpectedErrorNumber; 779 | EXEC [dbo].[sp_estindex] @IndexColumns = @IndexColumns 780 | ,@TableName = @TableName 781 | ,@SchemaName = @SchemaName 782 | ,@Verbose = @Verbose 783 | ,@DatabaseName = @DatabaseName; 784 | 785 | END; 786 | GO 787 | 788 | /* 789 | test failure with invalid @FillFactor 790 | */ 791 | CREATE PROCEDURE [sp_estindex].[test sp fails on invalid @FillFactor] 792 | AS 793 | BEGIN; 794 | 795 | --Build 796 | DECLARE @Verbose BIT = 0; 797 | DECLARE @IndexColumns VARCHAR(50) = 'BadColumnName'; 798 | DECLARE @SchemaName SYSNAME = 'tSQLt'; 799 | DECLARE @TableName SYSNAME = 'CaptureOutputLog'; 800 | DECLARE @FillFactor TINYINT = 101; 801 | 802 | DECLARE @ExpectedMessage NVARCHAR(MAX) = '[sp_estindex]: Fill factor must be between 1 and 100.'; 803 | DECLARE @ExpectedSeverity TINYINT = 16; 804 | DECLARE @ExpectedState TINYINT = 1; 805 | DECLARE @ExpectedErrorNumber INT = 50000; 806 | 807 | --Assert 808 | EXEC [tSQLt].[ExpectException] @ExpectedMessage = @ExpectedMessage 809 | ,@ExpectedSeverity = @ExpectedSeverity 810 | ,@ExpectedState = @ExpectedState 811 | ,@ExpectedErrorNumber = @ExpectedErrorNumber; 812 | EXEC [dbo].[sp_estindex] @IndexColumns = @IndexColumns 813 | ,@TableName = @TableName 814 | ,@SchemaName = @SchemaName 815 | ,@Verbose = @Verbose 816 | ,@FillFactor = @FillFactor; 817 | 818 | END; 819 | GO 820 | 821 | /* 822 | test failure with invalid @VarcharFillPercent 823 | */ 824 | CREATE PROCEDURE [sp_estindex].[test sp fails on invalid @VarcharFillPercent] 825 | AS 826 | BEGIN; 827 | 828 | --Build 829 | DECLARE @Verbose BIT = 0; 830 | DECLARE @IndexColumns VARCHAR(50) = 'BadColumnName'; 831 | DECLARE @SchemaName SYSNAME = 'tSQLt'; 832 | DECLARE @TableName SYSNAME = 'CaptureOutputLog'; 833 | DECLARE @VarcharFillPercent TINYINT = 101; 834 | 835 | DECLARE @ExpectedMessage NVARCHAR(MAX) = '[sp_estindex]: Varchar fill percent must be between 1 and 100.'; 836 | DECLARE @ExpectedSeverity TINYINT = 16; 837 | DECLARE @ExpectedState TINYINT = 1; 838 | DECLARE @ExpectedErrorNumber INT = 50000; 839 | 840 | --Assert 841 | EXEC [tSQLt].[ExpectException] @ExpectedMessage = @ExpectedMessage 842 | ,@ExpectedSeverity = @ExpectedSeverity 843 | ,@ExpectedState = @ExpectedState 844 | ,@ExpectedErrorNumber = @ExpectedErrorNumber; 845 | EXEC [dbo].[sp_estindex] @IndexColumns = @IndexColumns 846 | ,@TableName = @TableName 847 | ,@SchemaName = @SchemaName 848 | ,@Verbose = @Verbose 849 | ,@VarcharFillPercent = @VarcharFillPercent; 850 | 851 | END; 852 | GO 853 | 854 | /* 855 | test failure on @Filter missing WHERE 856 | */ 857 | CREATE PROCEDURE [sp_estindex].[test sp fails on @Filter missing WHERE] 858 | AS 859 | BEGIN; 860 | 861 | --Build 862 | DECLARE @Verbose BIT = 0; 863 | DECLARE @TableName VARCHAR(50) = 'DoesntMatter'; 864 | DECLARE @IndexColumns VARCHAR(50) = 'AlsoDoesntMatter'; 865 | DECLARE @Filter VARCHAR(50) = 'SomeCol = 2'; 866 | 867 | DECLARE @ExpectedMessage NVARCHAR(MAX) = '[sp_estindex]: Filter must start with ''WHERE''.'; 868 | DECLARE @ExpectedSeverity TINYINT = 16; 869 | DECLARE @ExpectedState TINYINT = 1; 870 | DECLARE @ExpectedErrorNumber INT = 50000; 871 | 872 | --Assert 873 | EXEC [tSQLt].[ExpectException] @ExpectedMessage = @ExpectedMessage 874 | ,@ExpectedSeverity = @ExpectedSeverity 875 | ,@ExpectedState = @ExpectedState 876 | ,@ExpectedErrorNumber = @ExpectedErrorNumber; 877 | EXEC [dbo].[sp_estindex] @TableName = @TableName 878 | ,@IndexColumns = @IndexColumns 879 | ,@Filter = @Filter 880 | ,@Verbose = @Verbose; 881 | 882 | END; 883 | GO 884 | 885 | /************************************ 886 | End sp_estindex tests 887 | *************************************/ 888 | -------------------------------------------------------------------------------- /tests/sp_helpme.Tests.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="Pester"; ModuleVersion="5.1.0" } 2 | param() 3 | 4 | BeforeDiscovery { 5 | #. "$PSScriptRoot\constants.ps1" 6 | } 7 | 8 | Describe "sp_helpme" { 9 | Context "tSQLt Tests" { 10 | BeforeAll { 11 | $storedProc = "sp_helpme" 12 | $testPath = "tests\" 13 | $testInstallScript = "$storedProc.Tests.sql" 14 | $runTestQuery = "EXEC tSQLt.Run '[$storedProc]'" 15 | $queryTimeout = 300 16 | 17 | $Hash = @{ 18 | ConnectionString = "Data Source=$env:SQLINSTANCE;Initial Catalog=$env:DATABASE;Integrated Security=True;TrustServerCertificate=true" 19 | Verbose = $true 20 | } 21 | 22 | # Install tests 23 | ForEach ($File in Get-ChildItem -Path $testPath -Filter $testInstallScript) { 24 | Invoke-Sqlcmd @Hash -InputFile $File.FullName 25 | } 26 | } 27 | It "All tests" { 28 | { Invoke-Sqlcmd @Hash -Query $runTestQuery -QueryTimeout $queryTimeout } | Should -Not -Throw -Because "tSQLt unit tests must pass" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/sp_helpme.Tests.sql: -------------------------------------------------------------------------------- 1 | SET NOCOUNT ON; 2 | SET ANSI_NULLS ON; 3 | SET QUOTED_IDENTIFIER ON; 4 | 5 | /************************************ 6 | Begin sp_helpme tests 7 | *************************************/ 8 | 9 | --Clean Class 10 | EXEC tSQLt.DropClass 'sp_helpme'; 11 | GO 12 | 13 | EXEC tSQLT.NewTestClass 'sp_helpme'; 14 | GO 15 | 16 | /* 17 | test that sp_sizeoptimiser exists 18 | */ 19 | CREATE PROCEDURE sp_helpme.[test sp succeeds on create] 20 | AS 21 | BEGIN 22 | 23 | --Assert 24 | EXEC tSQLt.AssertObjectExists @objectName = 'dbo.sp_helpme', @message = 'Stored procedure sp_helpme does not exist.'; 25 | 26 | END; 27 | GO 28 | 29 | /* 30 | test that sp_helpme errors on non-existant object 31 | */ 32 | CREATE PROCEDURE sp_helpme.[test sp fails for missing object] 33 | AS 34 | BEGIN 35 | 36 | --Build 37 | DECLARE @Table SYSNAME = 'dbo.IDontExist'; 38 | DECLARE @Database SYSNAME = DB_NAME(DB_ID()); 39 | DECLARE @ExpectedMessage NVARCHAR(MAX) = FORMATMESSAGE(N'The object ''%s'' does not exist in database ''%s'' or is invalid for this operation.', @Table, @Database); 40 | DECLARE @ExpectedSeverity TINYINT = 16; 41 | DECLARE @ExpectedState TINYINT = 1; 42 | DECLARE @ExpectedErrorNumber INT = 15009; 43 | 44 | --Assert 45 | EXEC [tSQLt].[ExpectException] @ExpectedMessage = @ExpectedMessage 46 | , @ExpectedSeverity = @ExpectedSeverity 47 | , @ExpectedState = @ExpectedState 48 | , @ExpectedErrorNumber = @ExpectedErrorNumber; 49 | EXEC [sp_helpme] @Table; 50 | 51 | END; 52 | GO 53 | 54 | /* 55 | test that sp_helpme does not fail for object that exists 56 | */ 57 | CREATE PROCEDURE sp_helpme.[test sp succeeds for object that exists] 58 | AS 59 | BEGIN 60 | 61 | --Build 62 | --Assume tSQLt's table tSQLt.CaptureOutputLog always exists 63 | DECLARE @Table SYSNAME = 'tSQLt.CaptureOutputLog'; 64 | DECLARE @cmd NVARCHAR(MAX) = N'EXEC [sp_helpme] ''' + @Table + ''';'; 65 | 66 | --Assert 67 | EXEC tSQLt.ExpectNoException; 68 | EXEC tSQLt.ResultSetFilter 0, @cmd; --Still runs but suppresses undesired output 69 | 70 | END; 71 | GO 72 | 73 | /* 74 | test first result set of sp_helpme for a table 75 | */ 76 | CREATE PROCEDURE [sp_helpme].[test sp succeeds on a table] 77 | AS 78 | BEGIN 79 | 80 | DECLARE @EngineEdition TINYINT = CAST(SERVERPROPERTY('EngineEdition') AS TINYINT); 81 | DECLARE @epname SYSNAME = 'Description'; 82 | DECLARE @schemaName SYSNAME = 'dbo'; 83 | DECLARE @TableName SYSNAME = 'sp_help_table_test'; 84 | DECLARE @qualifiedTable SYSNAME = @schemaName + '.' + @TableName; 85 | 86 | -- Build test objects 87 | -- Add table 88 | CREATE TABLE [dbo].[sp_help_table_test] ( 89 | [PersonID] [int] NOT NULL, 90 | [FullName] [nvarchar](50) NOT NULL, 91 | [PreferredName] [nvarchar](50) NOT NULL, 92 | [SearchName] [nvarchar](101) NOT NULL, 93 | [IsPermittedToLogon] [bit] NOT NULL, 94 | [LogonName] [nvarchar](50) NULL, 95 | [IsExternalLogonProvider] [bit] NOT NULL, 96 | [HashedPassword] [varbinary](max) NULL, 97 | [IsSystemUser] [bit] NOT NULL, 98 | [IsEmployee] [bit] NOT NULL, 99 | [IsSalesperson] [bit] NOT NULL, 100 | [UserPreferences] [nvarchar](max) NULL, 101 | [PhoneNumber] [nvarchar](20) NULL, 102 | [FaxNumber] [nvarchar](20) NULL, 103 | [EmailAddress] [nvarchar](256) NULL, 104 | [Photo] [varbinary](max) NULL, 105 | [CustomFields] [nvarchar](max) NULL, 106 | [OtherLanguages] [nvarchar](max) NULL, 107 | [LastEditedBy] [int] NOT NULL, 108 | [ValidFrom] [datetime2](7) NOT NULL, 109 | [ValidTo] [datetime2](7) NOT NULL 110 | ); 111 | 112 | -- Add index 113 | CREATE NONCLUSTERED INDEX [sp_help_table_test_index] ON [dbo].[sp_help_table_test] 114 | ( 115 | [PersonID] ASC, 116 | [FullName] ASC, 117 | [PreferredName] ASC, 118 | [PhoneNumber] ASC 119 | ) 120 | INCLUDE([LogonName],[IsExternalLogonProvider],[HashedPassword]); 121 | 122 | -- Add EP 123 | EXEC sys.sp_addextendedproperty @name=@epname, @value=N'People known to the application (staff, customer contacts, supplier contacts)' , @level0type=N'SCHEMA',@level0name=@schemaName, @level1type=N'TABLE',@level1name=@TableName; 124 | 125 | -- Run test 126 | DECLARE @cmd NVARCHAR(MAX) = N'EXEC [sp_helpme] ''' + @qualifiedTable + ''', ''' + @epname + ''';'; 127 | 128 | CREATE TABLE #Expected ( 129 | [name] SYSNAME NOT NULL 130 | ,[owner] NVARCHAR(20) NOT NULL 131 | ,[object_type] NVARCHAR(100) NOT NULL 132 | ,[create_datetime] DATETIME NOT NULL 133 | ,[modify_datetime] DATETIME NOT NULL 134 | ,[ExtendedProperty] SQL_VARIANT NULL 135 | ); 136 | 137 | INSERT INTO #Expected ([name], [owner], [object_type], [create_datetime], [modify_datetime], [ExtendedProperty]) 138 | SELECT 139 | [Name] = o.name, 140 | [Owner] = USER_NAME(ObjectProperty(object_id, 'ownerid')), 141 | [Type] = LOWER(REPLACE(o.type_desc, '_', ' ')), 142 | [Created_datetime] = o.create_date, 143 | [Modify_datetime] = o.modify_date, 144 | [ExtendedProperty] = ep.[value] 145 | FROM sys.all_objects o 146 | LEFT JOIN sys.extended_properties ep ON ep.major_id = o.[object_id] 147 | AND ep.[name] = @epname 148 | AND ep.minor_id = 0 149 | AND ep.class = 1 150 | WHERE o.name = @TableName; 151 | 152 | -- Actual results 153 | SELECT TOP 0 [name], [owner], [object_type], [create_datetime], [modify_datetime], [ExtendedProperty] 154 | INTO #Actual 155 | FROM #Expected; 156 | 157 | INSERT INTO #Actual ([name], [owner], [object_type], [create_datetime], [modify_datetime], [ExtendedProperty]) 158 | EXEC tSQLt.ResultSetFilter 1, @cmd; 159 | 160 | UPDATE #Actual SET ExtendedProperty = CONVERT(NVARCHAR(4000), ExtendedProperty); 161 | 162 | --Assert 163 | EXEC tSQLt.AssertEqualsTable #Expected, #Actual; 164 | 165 | END; 166 | GO 167 | 168 | /* 169 | test sp_helpme errors on unsupported SQL Server < v12 170 | */ 171 | CREATE PROCEDURE [sp_helpme].[test sp fails on unsupported version] 172 | AS 173 | BEGIN; 174 | 175 | DECLARE @version TINYINT = 10; 176 | DECLARE @ExpectedMessage NVARCHAR(MAX) = N'SQL Server versions below 2012 are not supported, sorry!'; 177 | 178 | --Assert 179 | EXEC [tSQLt].[ExpectException] @ExpectedMessage = @ExpectedMessage; 180 | EXEC [dbo].[sp_helpme] @SqlMajorVersion = @version; 181 | 182 | END; 183 | GO 184 | 185 | /* 186 | test sp_helpme works on supported SQL Server >= v12 187 | */ 188 | CREATE PROCEDURE [sp_helpme].[test sp succeeds on supported version] 189 | AS 190 | BEGIN; 191 | 192 | DECLARE @version TINYINT = 13; 193 | 194 | --Assert 195 | EXEC [tSQLt].[ExpectNoException]; 196 | EXEC [dbo].[sp_helpme] @SqlMajorVersion = @version; 197 | 198 | END; 199 | GO 200 | 201 | /* 202 | test sp_helpme fails for objects in different database 203 | */ 204 | CREATE PROCEDURE [sp_helpme].[test sp fails for obj in different db] 205 | AS 206 | BEGIN; 207 | 208 | --Build 209 | DECLARE @Table SYSNAME = 'msdb.dbo.backupset'; 210 | DECLARE @ExpectedMessage NVARCHAR(MAX) = N'The database name component of the object qualifier must be the name of the current database.'; 211 | DECLARE @ExpectedState TINYINT = 1; 212 | DECLARE @ExpectedErrorNumber INT = 15250; 213 | DECLARE @ExpectedSeverity TINYINT = 16; 214 | 215 | --Assert 216 | EXEC [tSQLt].[ExpectException] 217 | @ExpectedMessage = @ExpectedMessage 218 | ,@ExpectedSeverity = @ExpectedSeverity 219 | ,@ExpectedState = @ExpectedState 220 | ,@ExpectedErrorNumber = @ExpectedErrorNumber; 221 | 222 | EXEC [sp_helpme] @Table; 223 | 224 | END; 225 | GO 226 | 227 | /* 228 | test first result set of sp_helpme for a table 229 | */ 230 | CREATE PROCEDURE [sp_helpme].[test sp succeeds on a table sans identity col] 231 | AS 232 | BEGIN 233 | 234 | DECLARE @EngineEdition TINYINT = CAST(SERVERPROPERTY('EngineEdition') AS TINYINT); 235 | 236 | --Build 237 | --Assume tSQLt's table tSQLt.Run_LastExecution always exists 238 | DECLARE @Table SYSNAME = 'tSQLt.Run_LastExecution'; 239 | DECLARE @TableName SYSNAME = 'Run_LastExecution'; 240 | DECLARE @epname SYSNAME = 'Description'; 241 | DECLARE @cmd NVARCHAR(MAX) = N'EXEC [sp_helpme] ''' + @Table + ''', ''' + @epname + ''';'; 242 | 243 | CREATE TABLE #Expected ( 244 | [name] SYSNAME NOT NULL 245 | ,[owner] NVARCHAR(20) NOT NULL 246 | ,[object_type] NVARCHAR(100) NOT NULL 247 | ,[create_datetime] DATETIME NOT NULL 248 | ,[modify_datetime] DATETIME NOT NULL 249 | ,[ExtendedProperty] SQL_VARIANT NULL 250 | ); 251 | 252 | INSERT INTO #Expected 253 | SELECT 254 | [Name] = o.name, 255 | [Owner] = user_name(ObjectProperty(object_id, 'ownerid')), 256 | [Type] = LOWER(REPLACE(o.type_desc, '_', ' ')), 257 | [Created_datetime] = o.create_date, 258 | [Modify_datetime] = o.modify_date, 259 | [ExtendedProperty] = ep.[value] 260 | FROM sys.all_objects o 261 | LEFT JOIN sys.extended_properties ep ON ep.major_id = o.[object_id] 262 | AND ep.[name] = @epname 263 | AND ep.minor_id = 0 264 | AND ep.class = 1 265 | WHERE o.name = @TableName; 266 | 267 | CREATE TABLE #Actual ( 268 | [name] SYSNAME NOT NULL 269 | ,[owner] NVARCHAR(20) NOT NULL 270 | ,[object_type] NVARCHAR(100) NOT NULL 271 | ,[create_datetime] DATETIME NOT NULL 272 | ,[modify_datetime] DATETIME NOT NULL 273 | ,[ExtendedProperty] SQL_VARIANT NULL 274 | ); 275 | INSERT INTO #Actual 276 | EXEC tSQLt.ResultSetFilter 1, @cmd; 277 | 278 | --Assert 279 | EXEC tSQLt.AssertEqualsTable #Expected, #Actual; 280 | 281 | END; 282 | GO 283 | 284 | /* 285 | test that sp_helpme succeeds for a stored procedure 286 | */ 287 | CREATE PROCEDURE sp_helpme.[test sp succeeds for a stored procedure] 288 | AS 289 | BEGIN 290 | 291 | --Build 292 | --Assume tSQLt's stored procedure tSQLt.ApplyConstraint always exists 293 | DECLARE @StoredProc SYSNAME = 'tSQLt.ApplyConstraint'; 294 | DECLARE @cmd NVARCHAR(MAX) = N'EXEC [sp_helpme] ''' + @StoredProc + ''';'; 295 | 296 | --Assert 297 | EXEC tSQLt.ExpectNoException; 298 | EXEC tSQLt.ResultSetFilter 0, @cmd; --Still runs but suppresses undesired output 299 | 300 | END; 301 | GO 302 | 303 | /* 304 | test that sp_helpme succeeds for a type 305 | */ 306 | CREATE PROCEDURE sp_helpme.[test sp succeeds for a type] 307 | AS 308 | BEGIN 309 | 310 | --Build 311 | DECLARE @Type SYSNAME = 'dbo.SizeOptimiserTableType'; 312 | DECLARE @cmd NVARCHAR(MAX) = N'EXEC [sp_helpme] ''' + @Type + ''';'; 313 | 314 | --Assert 315 | EXEC tSQLt.ExpectNoException; 316 | EXEC tSQLt.ResultSetFilter 0, @cmd; --Still runs but suppresses undesired output 317 | 318 | END; 319 | GO 320 | 321 | /* 322 | test that sp_helpme succeeds for a type 323 | */ 324 | CREATE PROCEDURE sp_helpme.[test sp succeeds for table with schemabound view] 325 | AS 326 | BEGIN 327 | 328 | --Build 329 | DECLARE @Table SYSNAME = 'tSQLt.CaptureOutputLog'; 330 | DECLARE @cmd NVARCHAR(MAX) = N'EXEC [sp_helpme] ''' + @Table + ''';'; 331 | DECLARE @ViewName NVARCHAR(1000) = 'dbo.SchemaBoundView'; 332 | DECLARE @sql NVARCHAR(MAX) = N' 333 | CREATE VIEW dbo.SchemaBoundView 334 | WITH SCHEMABINDING 335 | AS 336 | SELECT [id] FROM tSQLt.CaptureOutputLog;'; 337 | 338 | IF EXISTS (SELECT 1 FROM [sys].[views] WHERE [object_id] = OBJECT_ID(@ViewName)) 339 | BEGIN; 340 | DROP VIEW dbo.SchemaBoundView; 341 | END; 342 | 343 | EXEC sp_executesql @sql; 344 | 345 | --Assert 346 | EXEC tSQLt.ExpectNoException; 347 | EXEC tSQLt.ResultSetFilter 0, @cmd; --Still runs but suppresses undesired output 348 | 349 | END; 350 | GO 351 | 352 | /************************************ 353 | End sp_helpme tests 354 | *************************************/ 355 | -------------------------------------------------------------------------------- /tests/sp_sizeoptimiser.Tests.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="Pester"; ModuleVersion="5.1.0" } 2 | param() 3 | 4 | BeforeDiscovery { 5 | #. "$PSScriptRoot\constants.ps1" 6 | } 7 | 8 | Describe "sp_sizeoptimiser" { 9 | Context "tSQLt Tests" { 10 | BeforeAll { 11 | $storedProc = "sp_sizeoptimiser" 12 | $testPath = "tests\" 13 | $testInstallScript = "$storedProc.Tests.sql" 14 | $runTestQuery = "EXEC tSQLt.Run '[$storedProc]'" 15 | $queryTimeout = 300 16 | 17 | $Hash = @{ 18 | ConnectionString = "Data Source=$env:SQLINSTANCE;Initial Catalog=$env:DATABASE;Integrated Security=True;TrustServerCertificate=true" 19 | Verbose = $true 20 | } 21 | 22 | # Install tests 23 | ForEach ($File in Get-ChildItem -Path $testPath -Filter $testInstallScript) { 24 | Invoke-Sqlcmd @Hash -InputFile $File.FullName 25 | } 26 | } 27 | It "All tests" { 28 | { Invoke-Sqlcmd @Hash -Query $runTestQuery -QueryTimeout $queryTimeout } | Should -Not -Throw -Because "tSQLt unit tests must pass" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/sp_sizeoptimiser.Tests.sql: -------------------------------------------------------------------------------- 1 | SET NOCOUNT ON; 2 | SET ANSI_NULLS ON; 3 | SET QUOTED_IDENTIFIER ON; 4 | 5 | /************************************ 6 | Begin sp_sizeoptimiser tests 7 | *************************************/ 8 | 9 | --Clean Class 10 | EXEC tSQLt.DropClass 'sp_sizeoptimiser'; 11 | GO 12 | 13 | EXEC tSQLT.NewTestClass 'sp_sizeoptimiser'; 14 | GO 15 | 16 | /* 17 | test that sp_sizeoptimiser exists 18 | */ 19 | CREATE PROCEDURE [sp_sizeoptimiser].[test sp succeeds on create] 20 | AS 21 | BEGIN 22 | 23 | DECLARE @ObjectName NVARCHAR(1000) = N'dbo.sp_sizeoptimiser'; 24 | DECLARE @ErrorMessage NVARCHAR(MAX) = N'Stored procedure sp_sizeoptimiser does not exist.'; 25 | 26 | --Assert 27 | EXEC tSQLt.AssertObjectExists 28 | @objectName = @objectName 29 | ,@message = @ErrorMessage; 30 | 31 | END; 32 | GO 33 | 34 | /* 35 | test that SizeOptimiserTableType exists 36 | */ 37 | CREATE PROCEDURE [sp_sizeoptimiser].[test sp succeeds on dependent table type create] 38 | AS 39 | BEGIN 40 | 41 | DECLARE @actual BIT = 0; 42 | DECLARE @expected BIT = 1; 43 | DECLARE @ErrorMessage NVARCHAR(MAX) = N'User defined table type SizeOptimiserTableType does not exist'; 44 | DECLARE @ObjectName SYSNAME = N'SizeOptimiserTableType'; 45 | 46 | --Check for table type 47 | SELECT @actual = 1 48 | FROM [sys].[table_types] 49 | WHERE [name] = @ObjectName; 50 | 51 | --Assert 52 | EXEC tSQLt.AssertEquals @expected 53 | ,@actual 54 | ,@message = @ErrorMessage; 55 | 56 | END; 57 | GO 58 | 59 | /* 60 | test that incorrect @IndexNumThreshold throws error 61 | */ 62 | CREATE PROCEDURE [sp_sizeoptimiser].[test sp fails on incorrect @IndexNumThreshold] 63 | AS 64 | BEGIN 65 | 66 | DECLARE @ExpectedMessage NVARCHAR(MAX) = N'@IndexNumThreshold must be between 1 and 999.'; 67 | DECLARE @ExpectedSeverity TINYINT = 16; 68 | DECLARE @ExpectedState TINYINT = 1; 69 | DECLARE @ExpectedErrorNumber INT = 50000; 70 | DECLARE @IndexNumThreshold TINYINT = 0; 71 | DECLARE @Verbose BIT = 0; 72 | 73 | --Assert 74 | EXEC tSQLt.ExpectException 75 | @ExpectedMessage = @ExpectedMessage 76 | ,@ExpectedSeverity = @ExpectedSeverity 77 | ,@ExpectedState = @ExpectedState 78 | ,@ExpectedErrorNumber = @ExpectedErrorNumber; 79 | EXEC dbo.sp_sizeoptimiser 80 | @IndexNumThreshold = @IndexNumThreshold 81 | ,@Verbose = @Verbose; 82 | 83 | END; 84 | GO 85 | 86 | /* test result set has correct table schema*/ 87 | CREATE PROCEDURE [sp_sizeoptimiser].[test sp succeeds with result set metadata] 88 | AS 89 | BEGIN 90 | 91 | DECLARE @version NVARCHAR(MAX) = @@VERSION; 92 | 93 | EXEC tSQLt.AssertResultSetsHaveSameMetaData 94 | @expectedCommand = N'CREATE TABLE #results 95 | ([check_num] INT NOT NULL, 96 | [check_type] NVARCHAR(50) NOT NULL, 97 | [db_name] SYSNAME NOT NULL, 98 | [obj_type] SYSNAME NOT NULL, 99 | [obj_name] NVARCHAR(400) NOT NULL, 100 | [col_name] SYSNAME NULL, 101 | [message] NVARCHAR(500) NULL, 102 | [ref_link] NVARCHAR(500) NULL); 103 | SELECT * FROM #results;', 104 | @actualCommand = N'EXEC dbo.sp_sizeoptimiser @Verbose = 0;'; 105 | 106 | END; 107 | GO 108 | 109 | /* 110 | test that passing @IncludeDatabases 111 | and @ExcludeDatabases fails 112 | */ 113 | CREATE PROCEDURE [sp_sizeoptimiser].[test sp fails using include and exclude params] 114 | AS 115 | BEGIN 116 | 117 | --Build 118 | DECLARE @IncludeDatabases [dbo].[SizeOptimiserTableType]; 119 | DECLARE @ExcludeDatabases [dbo].[SizeOptimiserTableType]; 120 | DECLARE @ExpectedMessage NVARCHAR(MAX) = 'Both @IncludeDatabases and @ExcludeDatabases cannot be specified.'; 121 | DECLARE @ExpectedSeverity TINYINT = 16; 122 | DECLARE @ExpectedState TINYINT = 1; 123 | DECLARE @ExpectedErrorNumber INT = 50000; 124 | DECLARE @Verbose BIT = 0; 125 | 126 | INSERT INTO @IncludeDatabases 127 | VALUES ('master'); 128 | 129 | INSERT INTO @ExcludeDatabases 130 | VALUES ('model'); 131 | 132 | --Assert 133 | EXEC [tSQLt].[ExpectException] @ExpectedMessage = @ExpectedMessage 134 | ,@ExpectedSeverity = @ExpectedSeverity 135 | ,@ExpectedState = @ExpectedState 136 | ,@ExpectedErrorNumber = @ExpectedErrorNumber; 137 | EXEC [dbo].[sp_sizeoptimiser] 138 | NULL 139 | ,@IncludeDatabases = @IncludeDatabases 140 | ,@ExcludeDatabases = @ExcludeDatabases 141 | ,@Verbose = @Verbose; 142 | 143 | END; 144 | GO 145 | 146 | 147 | /* 148 | test failure on unsupported SQL Server < v12 149 | */ 150 | CREATE PROCEDURE [sp_sizeoptimiser].[test sp fails on unsupported version] 151 | AS 152 | BEGIN; 153 | 154 | --Build 155 | DECLARE @version TINYINT = 10; 156 | DECLARE @Verbose BIT = 0; 157 | DECLARE @ExpectedMessage NVARCHAR(MAX) = 'SQL Server versions below 2012 are not supported, sorry!'; 158 | DECLARE @ExpectedSeverity TINYINT = 16; 159 | DECLARE @ExpectedState TINYINT = 1; 160 | DECLARE @ExpectedErrorNumber INT = 50000; 161 | 162 | --Assert 163 | EXEC [tSQLt].[ExpectException] @ExpectedMessage = @ExpectedMessage 164 | ,@ExpectedSeverity = @ExpectedSeverity 165 | ,@ExpectedState = @ExpectedState 166 | ,@ExpectedErrorNumber = @ExpectedErrorNumber; 167 | EXEC [dbo].[sp_sizeoptimiser] 168 | @SqlMajorVersion = @version 169 | ,@Verbose = @Verbose; 170 | 171 | END; 172 | GO 173 | 174 | /* 175 | test success on supported SQL Server >= v12 176 | */ 177 | CREATE PROCEDURE [sp_sizeoptimiser].[test sp succeeds on supported version] 178 | AS 179 | BEGIN; 180 | 181 | --Build 182 | DECLARE @version TINYINT = 13; 183 | DECLARE @IncludeDatabases [dbo].[SizeOptimiserTableType]; 184 | DECLARE @Verbose BIT = 0; 185 | DECLARE @command NVARCHAR(MAX) = CONCAT(N'EXEC [dbo].[sp_sizeoptimiser] @SqlMajorVersion = ',@version, ', @Verbose =', @Verbose, ';'); 186 | 187 | --Assert 188 | EXEC [tSQLt].[ExpectNoException]; 189 | EXEC [tSQLt].[SuppressOutput] @command = @command; 190 | 191 | END; 192 | GO 193 | 194 | 195 | /* 196 | test success on supported SQL Server >= v12 197 | */ 198 | CREATE PROCEDURE [sp_sizeoptimiser].[test sp succeeds with verbose mode] 199 | AS 200 | BEGIN; 201 | 202 | --Build 203 | DECLARE @version TINYINT = 13; 204 | DECLARE @IncludeDatabases [dbo].[SizeOptimiserTableType]; 205 | DECLARE @DbName SYSNAME = DB_NAME(DB_ID()); 206 | DECLARE @Verbose BIT = 1; 207 | DECLARE @command NVARCHAR(MAX) = CONCAT('EXEC [dbo].[sp_sizeoptimiser] @Verbose =', @Verbose, ';'); 208 | 209 | INSERT INTO @IncludeDatabases 210 | VALUES (@DbName); 211 | 212 | --Assert 213 | EXEC [tSQLt].[ExpectNoException]; 214 | EXEC [tSQLt].[SuppressOutput] @command = @command; 215 | 216 | END; 217 | GO 218 | 219 | /* 220 | test success on @ExcludeDatabases 221 | */ 222 | CREATE PROCEDURE [sp_sizeoptimiser].[test sp succeeds with @ExcludeDatabases] 223 | AS 224 | BEGIN; 225 | 226 | --Build 227 | DECLARE @version TINYINT = 13; 228 | DECLARE @ExcludeDatabases [dbo].[SizeOptimiserTableType]; 229 | DECLARE @Verbose BIT = 0; 230 | 231 | INSERT INTO @ExcludeDatabases 232 | VALUES ('master'); 233 | 234 | --Assert 235 | EXEC [tSQLt].[ExpectNoException]; 236 | EXEC [dbo].[sp_sizeoptimiser] 237 | @ExcludeDatabases = @ExcludeDatabases 238 | ,@Verbose = @Verbose; 239 | 240 | END; 241 | GO 242 | 243 | /* 244 | test fail on non-existant databases 245 | */ 246 | CREATE PROCEDURE [sp_sizeoptimiser].[test sp fails with imaginary database] 247 | AS 248 | BEGIN; 249 | 250 | --Build 251 | DECLARE @IncludeDatabases [dbo].[SizeOptimiserTableType]; 252 | DECLARE @DbName SYSNAME = 'BlackLivesMatter'; 253 | DECLARE @ExpectedMessage NVARCHAR(MAX) = FORMATMESSAGE('Supplied databases do not exist or are not accessible: %s.', @DbName); 254 | DECLARE @ExpectedSeverity TINYINT = 16; 255 | DECLARE @ExpectedState TINYINT = 1; 256 | DECLARE @ExpectedErrorNumber INT = 50000; 257 | 258 | INSERT INTO @IncludeDatabases 259 | VALUES (@DbName); 260 | 261 | --Assert 262 | EXEC [tSQLt].[ExpectException] @ExpectedMessage = @ExpectedMessage 263 | ,@ExpectedSeverity = @ExpectedSeverity 264 | ,@ExpectedState = @ExpectedState 265 | ,@ExpectedErrorNumber = @ExpectedErrorNumber; 266 | EXEC [dbo].[sp_sizeoptimiser] @IncludeDatabases = @IncludeDatabases; 267 | 268 | END; 269 | GO 270 | 271 | /* 272 | test success in Express Mode 273 | */ 274 | CREATE PROCEDURE [sp_sizeoptimiser].[test sp succeeds in Express Mode] 275 | AS 276 | BEGIN; 277 | 278 | --Check if testing on Azure SQL 279 | DECLARE @EngineEdition TINYINT = CAST(ServerProperty('EngineEdition') AS TINYINT); 280 | DECLARE @Verbose BIT = 0; 281 | DECLARE @AzureSQLEngine TINYINT = 5; 282 | 283 | IF (@EngineEdition <> @AzureSQLEngine) -- Not Azure SQL 284 | BEGIN 285 | --Build 286 | DECLARE @IsExpress BIT = 1; 287 | 288 | --Assert 289 | EXEC [tSQLt].[ExpectNoException]; 290 | EXEC [dbo].[sp_sizeoptimiser] 291 | @IsExpress = @IsExpress 292 | ,@Verbose = @Verbose; 293 | END; 294 | 295 | ELSE 296 | BEGIN; 297 | EXEC [tSQLt].[ExpectNoException]; 298 | EXEC [dbo].[sp_sizeoptimiser] 299 | @Verbose = @Verbose; 300 | END; 301 | END; 302 | GO 303 | 304 | /************************************ 305 | End sp_sizeoptimiser tests 306 | *************************************/ 307 | -------------------------------------------------------------------------------- /uninstall_dba-multitool.sql: -------------------------------------------------------------------------------- 1 | SET ANSI_NULLS ON; 2 | GO 3 | 4 | SET QUOTED_IDENTIFIER ON; 5 | GO 6 | 7 | SET NOCOUNT ON; 8 | GO 9 | 10 | /* Drop sp_doc */ 11 | IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[sp_doc]') AND [type] IN (N'P', N'PC')) 12 | BEGIN; 13 | DROP PROCEDURE [dbo].[sp_doc]; 14 | END 15 | GO 16 | 17 | /* Drop sp_sizeoptimiser */ 18 | IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[sp_sizeoptimiser]')) 19 | BEGIN; 20 | DROP PROCEDURE [dbo].[sp_sizeoptimiser]; 21 | END 22 | 23 | IF EXISTS (SELECT * FROM sys.types st JOIN sys.schemas ss ON st.schema_id = ss.schema_id WHERE st.name = N'SizeOptimiserTableType' AND ss.name = N'dbo') 24 | BEGIN; 25 | DROP TYPE [dbo].[SizeOptimiserTableType]; 26 | END 27 | GO 28 | 29 | /* Drop sp_estindex */ 30 | IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[sp_estindex]') AND [type] IN (N'P', N'PC')) 31 | BEGIN; 32 | DROP PROCEDURE [dbo].[sp_estindex]; 33 | END 34 | GO 35 | 36 | /* Drop sp_helpme */ 37 | IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[sp_helpme]') AND [type] IN (N'P', N'PC')) 38 | BEGIN; 39 | DROP PROCEDURE [dbo].[sp_helpme]; 40 | END 41 | GO 42 | 43 | /* Drop sp_help_revlogin & sp_hexadecimal */ 44 | IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[sp_help_revlogin]') AND [type] IN (N'P', N'PC')) 45 | BEGIN; 46 | DROP PROCEDURE [dbo].[sp_help_revlogin]; 47 | END 48 | GO 49 | 50 | IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[sp_hexadecimal]') AND [type] IN (N'P', N'PC')) 51 | BEGIN; 52 | DROP PROCEDURE [dbo].[sp_hexadecimal]; 53 | END 54 | GOs 55 | --------------------------------------------------------------------------------