├── .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]
4 | 
5 |
6 | [](https://github.com/lowlydba/dba-multitool/actions/workflows/sqlserver-unit.yml)
7 | [](https://github.com/lowlydba/dba-multitool/actions/workflows/azuresql-unit.yml)
8 | [](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 |
--------------------------------------------------------------------------------