Muhammad Tahir
Muhammad Tahir

Reputation: 156

is it possible to alter stored procedure via query in SQL Server?

I need to change the profile names from all stored procedures having email alerts. Is it possible to alter stored procedure via query in SQL Server.

It will help me to update all stored procedures having email alerts.

SELECT object_definition(object_id) as [Proc Definition]
  , OBJECT_NAME(object_id) [Stored Proc Name]
FROM sys.objects 
WHERE type='P' and object_definition(object_id) like '%sp_send_dbmail%'

Via above query I need to alter the body of stored procedure.

Upvotes: 0

Views: 1996

Answers (3)

Petr C pika
Petr C pika

Reputation: 1

Use it very carefully:

CREATE PROCEDURE dbo._ReplaceTextInStoredProcedures (@textToReplace nvarchar(MAX), @replacement nvarchar(MAX))
AS 
BEGIN

SET NoCount ON;

SELECT OBJECT_DEFINITION(object_id) AS def INTO #proc_defs FROM sys.procedures WHERE OBJECT_DEFINITION(object_id) LIKE ('%'+@textToReplace+'%');

UPDATE #proc_defs SET def = REPLACE(REPLACE(def, @textToReplace, @replacement), 'CREATE PROCEDURE', 'ALTER PROCEDURE');

DECLARE @def nvarchar(MAX);
DECLARE defs_cursor CURSOR
FOR SELECT def FROM #proc_defs;

OPEN defs_cursor;

FETCH NEXT FROM defs_cursor INTO @def;

WHILE @@FETCH_STATUS = 0
    BEGIN
        EXEC (@def);
        FETCH NEXT FROM defs_cursor INTO @def;
    END;

CLOSE defs_cursor;

DEALLOCATE defs_cursor;

END

Upvotes: 0

Dan Guzman
Dan Guzman

Reputation: 46203

It is possible to make mass changes to stored procedure code but the task can be fairly complex due to the many variations of T-SQL constructs. Simple string replacements may work in some cases but is fragile because the text to be replaced can occur it other contexts where it should not be changed. For example, if your original profile is named "mail", a dumb text replace would also change the name of the "sp_send_dbmail" proc.

Below is a PowerShell example that uses the Microsoft.SqlServer.TransactSql.ScriptDom assembly to more intelligently parse T-SQL code and make targeted changes. This isn't a complete solution that can change all occurrences of the profile name (e.g. local variable assignments, positional sp_send_db_mail parameters, etc.) but will change the value specified in sp_send_dbmail named parameter syntax and is more robust than replacing text. You can test and tweak the code in a dev environment to suit your needs.

Also, as a disclaimer, changing the parser token text in this way is undocumented so use at your own risk.

Function Replace-DatabaseMailProfileNames($script, $originalProfileName, $newProfileName) {

    # use the appropriate TSqlParser version for the target SQL Server version
    $parser = New-Object Microsoft.SqlServer.TransactSql.ScriptDom.TSql140Parser($true)

    $parseErrors = New-Object System.Collections.Generic.List[Microsoft.SqlServer.TransactSql.ScriptDom.ParseError]
    $scriptReader = New-Object System.IO.StringReader($script)
    $fragment = $parser.Parse($scriptReader, [ref]$parseErrors)
    $scriptChanged = $false
    if($parseErrors.Count -eq 0) {
        $fragment.Batches[0].Statements[0].ScriptTokenStream[$fragment.Batches[0].Statements[0].FirstTokenIndex].Text = "ALTER"
        foreach($statement in $fragment.Batches[0].Statements[0].StatementList.Statements) {
            switch($statement.GetType().ToString())
            {
                "Microsoft.SqlServer.TransactSql.ScriptDom.ExecuteStatement" {
                    if($statement.ExecuteSpecification.ExecutableEntity.ProcedureReference.ProcedureReference.Name.BaseIdentifier.Value.ToLower() -eq "sp_send_dbmail") {
                        foreach($parameter in $statement.ExecuteSpecification.ExecutableEntity.Parameters) {
                            if(($parameter.Variable.Name.ToLower() -eq "@profile_name") -and ($parameter.ParameterValue.Value.ToLower() -eq $originalProfileName.ToLower())) {
                                $parameter.ParameterValue.ScriptTokenStream[$parameter.ParameterValue.FirstTokenIndex].Text = $newProfileName
                                $scriptChanged = $true
                            }
                        }
                    }
                    break
                }
            }
        }

        if($scriptChanged) {
            $fragmentText = New-Object System.Text.StringBuilder
            # reconstrunct script from tokens containing new values
            for($i = $fragment.FirstTokenIndex; $i -le $fragment.LastTokenIndex; ++$i) {
                [void]$fragmentText.Append($fragment.ScriptTokenStream[$i].Text)
            }
            return $fragmentText.ToString()
        }
        else {
            # return null to indicate script was not changed
            return $null
        }

    }
    else {
        throw "Error(s) parsing script"
    }

}

############
### main ###
############

# Specify path to Microsoft.SqlServer.TransactSql.ScriptDom.dll.
# This assembly is available from the Microsoft DacFx NuGet package: https://www.nuget.org/packages/Microsoft.SqlServer.DacFx.x64/
Add-Type -Path "C:\Temp\Microsoft.SqlServer.TransactSql.ScriptDom.dll"

$originalProfileName = "Original mail profile name"
$newProfileName = "'New mail profile name'"

try {

    $connectionString = "Data Source=.;Initial Catalog=YourDatabase;Integrated Security=SSPI;MultipleActiveResultSets=True";
    $connection = New-Object System.Data.SqlClient.SqlConnection($connectionString);
    $query = @"
SELECT QUOTENAME(OBJECT_SCHEMA_NAME(p.object_id)) + '.' + QUOTENAME(p.name) AS procedure_name, sm.definition
FROM sys.procedures AS p
JOIN sys.sql_modules AS sm ON sm.object_id = p.object_id
WHERE sm.definition LIKE N'%sp_send_dbmail%';
"@
    $selectCommand = New-Object System.Data.SqlClient.SqlCommand($query, $connection)
    $connection.Open();
    $reader = $selectCommand.ExecuteReader()
    while($reader.Read()) {
        $newScript = Replace-DatabaseMailProfileNames -script "$($reader["definition"])" -originalProfileName $originalProfileName -newProfileName $newProfileName
        if($newScript -ne $null) {
            $alterCommand = New-Object System.Data.SqlClient.SqlCommand($newScript, $connection)
            [void]$alterCommand.ExecuteNonQuery()
            "Stored procedure $($reader["procedure_name"]) changed"
        }
    }

}
catch {
    throw
}

Upvotes: 0

Gurdev Singh
Gurdev Singh

Reputation: 2165

you need to run a loop to update all the procedures and get the procedure code using sp_helptext and store it in a string variable. Replace string value you need in your variable and then use the updated string variable to run the alter command and update your procs.

Upvotes: 1

Related Questions