Très pratique pour l’industrialisation, Invoke-Sqlcmd est une applet de commande Powershell qui exécute des scripts T-SQL et XQuery, ainsi que les commandes prises en charge par l’utilitaire sqlcmd. Invoke-Sqlcmd est inclue depuis SQL Server 2008, autrement dit, il n’est pas utile d’ajouter un module dans Powershell pour pouvoir utiliser cette fonction. Il est important d’en connaître les principaux avantages et limitations, en sachant qu’elle peut avoir un comportement inattendu. Voici quelques éléments de réponse aux questions suivantes :
Pourquoi Invoke-Sqlcmd est-elle si lunatique ? Est-ce qu’il vaut mieux la lancer le matin, plutôt l’après-midi, les jours pairs et enfin doit-on tenir compte de la météo ?
La première étape est de parvenir à lancer la commande Invoke-Sqlcmd, autrement dit qu’elle soit reconnue. Il se peut que vous soyez confronté à ce message d’erreur :
The term ‘invoke-sqlcmd’ is not recognized as the name of a cmdlet, function, script file, or operable program.
Pas de panique ! Déployez les outils ci-dessous dans cet ordre (suivant votre version, Microsoft SQL Server 2016 Feature Pack SP1 ou autre) et relancer Powershell :
– ENU\x64\SQLSysClrTypes.msi
– ENU\x64\SharedManagementObjects.msi
– ENU\x64\PowerShellTools.msi
Les packages sont téléchargeables ici : https://www.microsoft.com/en-US/download/details.aspx?id=54279
Cette commande permet d’utiliser le mode DAC, soit le mode de connexion de secours SQL Server pour interroger une instance/base de données quand le mode d’accès classique n’est plus possible :
1 |
Invoke-Sqlcmd -ServerInstance localhost -Database master -Query "SELECT * FROM sys.sysprocesses" -DedicatedAdministratorConnection |
Il est bien évidement possible de passer des variables en entrée d’une requête (-Query, -InputFile) comme filtre(s) ou encore pour modifier le contexte.
1 2 |
$StringArray = "MYVAR1='String1'", "MYVAR2='String2'" Invoke-Sqlcmd -ServerInstance localhost -Database AdventureWorks2014 -Query "SELECT `$(MYVAR1) AS Var1, `$(MYVAR2) AS Var2;" -Variable $StringArray |
En pratique, ça n’est pas complètement fonctionnel et pour cause, il y a des caractères à échapper.On peut recevoir ce message d’erreur :
Error: The format used to define the new variable for Invoke-Sqlcmd cmdlet is invalid. Please use the ‘var=value’ format for defining a new variable.
Typiquement si votre variable contient des guillemets et que vous lui passer directement une valeur littérale, il y a deux manières d’échapper ce caractère :
1 2 3 4 5 |
$DBParam1 = "MYVAR1='toto'" $DBParam2 = "MYVAR2='un tramway nommé `"désir`"'" $StringArray = $DBParam1, $DBParam2 Invoke-Sqlcmd -ServerInstance localhost -Database AdventureWorks2014 -Query "SELECT `$(MYVAR1) AS Var1, `$(MYVAR2) AS Var2;" -Variable $StringArray |
Ou bien comme ceci :
1 2 3 4 5 |
$DBParam1 = "MYVAR1='toto'" $DBParam2 = 'MYVAR2=''un tramway nommé "désir"''' $StringArray = $DBParam1, $DBParam2 Invoke-Sqlcmd -ServerInstance localhost -Database AdventureWorks2014 -Query "SELECT `$(MYVAR1) AS Var1, `$(MYVAR2) AS Var2;" -Variable $StringArray |
Cependant, il y a un caractère qu’on ne pas peut échapper, c’est le =. Pourquoi me direz-vous ? Parce qu’il est déjà utilisé dans l’initialisation des variables. Invoke-Sqlcmd dépend de deux DLL que voici :
Microsoft.SqlServer.Management.PSProvider.dll
Microsoft.SqlServer.Management.PSSnapins.dll
C’est la seconde qui nous intéresse. Quand on lance à un Process Explorer, on observe qu’elles sont bien appelées par le processus powershell_ise.exe :
Si on ouvre la DLL Microsoft.SqlServer.Management.PSSnapins.dll avec le dé-compilateur dotPeek gratuit par ailleurs et qu’on s’attarde sur la section ExecutionProcessor en faisant une recherche sur la chaine “Variable”, voici ce qu’on peut y trouver :
En l’état, il n’y a rien qui nous permette d’échapper le caractère =. Néanmoins, ce problème peut être contourné de deux manières : soit en remplaçant le = une première fois par une chaine improbable dans votre variable et dans un second temps de procéder à son remplacement pour restituer la valeur d’origine au niveau de votre requête/script TSQL. Pas super propre…
1 2 3 4 5 6 7 |
$var2 = 'un tramway nommé "désir" = Marlon Brando' $DBParam1 = "MYVAR1='toto'" $DBParam2 = "MYVAR2='" + $var2.Replace("=", "|#$§") + "'" $StringArray = $DBParam1, $DBParam2 Invoke-Sqlcmd -ServerInstance localhost -Database AdventureWorks2014 -Query "SELECT `$(MYVAR1) AS Var1, REPLACE(`$(MYVAR2), '|#$§', '=') AS Var2;" -Variable $StringArray |
Le seconde moyen de contournement est d’utiliser la fonction Invoke-Sqlcmd2 que nous évoquerons plus tard pour d’autres raisons.
1 2 3 4 5 6 7 8 9 10 |
Clear-Host $ScriptDirectory = Split-Path $MyInvocation.MyCommand.Path . (Join-Path $ScriptDirectory Invoke-Sqlcmd2.ps1) [string]$var1 = 'toto' [string]$var2 = 'un tramway nommé "désir" = Marlon Brando' Invoke-Sqlcmd2 -ServerInstance localhost -Database AdventureWorks2014 -Query "SELECT @MYVAR1 AS Var1, @MYVAR2 AS Var2;" -SqlParameters @{ MYVAR1=$var1; MYVAR2=$var2 } |
A l’inverse, on peut au besoin désactiver l’alimentation de variables en entrée en ajoutant le paramètre -DisableVariables.
Quand on y regarde de plus près, la fonction Invoke-Sqlcmd renvoie un objet en sortie. Dans le script ci-dessous, on récupère un DataTable unique :
1 2 3 4 5 6 |
######################################## ### Exemple 1 : Get single datatable ### ######################################## $Tables = Invoke-Sqlcmd -ServerInstance localhost -Database AdventureWorks2014 -Query "SELECT TOP 2 ObjectName = name FROM sys.objects;" $Tables.ObjectName | Out-String |
Dans ce second exemple, on a la possibilité de récupérer plusieurs DataTables en sortie, en introduisant le paramètre -As DataTables :
1 2 3 4 5 6 7 8 |
########################################### ### Exemple 2 : Get multiple Datatables ### ########################################### $Tables = Invoke-Sqlcmd -ServerInstance localhost -Database AdventureWorks2014 -Query "SELECT TOP 2 ObjectName = name FROM sys.objects;SELECT TOP 2 ObjectName = name FROM sys.objects;" -As DataTables if ($Tables -ne $null) { $Tables[0].ObjectName | Out-String } if ($Tables -ne $null) { $Tables[1].ObjectName | Out-String } |
Idem pour ce troisième exemple mais avec un cas d’usage concret, à savoir récupérer du T-SQL généré dynamiquement en vue de l’exécuter sur une autre instance/base de données :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
########################################################### ### Exemple 3 : Get multiples Datatable (dynamic -TSQL) ### ########################################################## $Query = @” SELECT SQLDyn = 'ALTER TABLE ' + QUOTENAME(OBJECT_SCHEMA_NAME(parent_object_id)) + '.' + QUOTENAME(OBJECT_NAME(parent_object_id)) + ' NOCHECK CONSTRAINT ' + QUOTENAME(name) + CHAR(10) + 'GO' FROM sys.foreign_keys WHERE is_disabled = 0 UNION ALL SELECT SQLDyn = 'DISABLE TRIGGER ' + QUOTENAME(name) + ' ON ' + QUOTENAME(OBJECT_SCHEMA_NAME(parent_id)) + '.' + QUOTENAME(OBJECT_NAME(parent_id)) + CHAR(10) + 'GO' FROM sys.triggers WHERE is_disabled = 0 SELECT SQLDyn = 'ALTER TABLE ' + QUOTENAME(OBJECT_SCHEMA_NAME(parent_object_id)) + '.' + QUOTENAME(OBJECT_NAME(parent_object_id)) + ' WITH CHECK CHECK CONSTRAINT ' + QUOTENAME(name) + CHAR(10) + 'GO' FROM sys.foreign_keys WHERE is_disabled = 0 UNION ALL SELECT SQLDyn = 'ENABLE TRIGGER ' + QUOTENAME(name) + ' ON ' + QUOTENAME(OBJECT_SCHEMA_NAME(parent_id)) + '.' + QUOTENAME(OBJECT_NAME(parent_id)) + CHAR(10) + 'GO' FROM sys.triggers WHERE is_disabled = 0 “@ $Tables = Invoke-Sqlcmd -ServerInstance localhost -Database AdventureWorks2014 -Query $Query -As DataTables if ($Tables -ne $null) { $Tables[0].SQLDyn | Out-String } if ($Tables -ne $null) { $Tables[1].SQLDyn | Out-String } |
L’unicité des colonnes en sortie doit être garantie, au risque de rencontrer l’erreur suivante :
1 |
Invoke-Sqlcmd -ServerInstance localhost -Database master -Query "EXEC sp_who2" -DedicatedAdministratorConnection | Export-Csv "C:\temp\result_sp_who2.csv" |
Quand on observe la sortie de la requête EXEC sp_who2, on constate que la colonne SPID apparait à deux reprises :
Spécifiez le paramètre QueryTimeout (en secondes) pour les requêtes dont le temps d’exécution va au delà du timeout défini par défaut sur l’instance SQL Server.
1 2 3 4 5 |
# Error Invoke-Sqlcmd -ServerInstance localhost -Database AdventureWorks2014 -Query "WAITFOR DELAY '00:02:05'" # No error Invoke-Sqlcmd -ServerInstance localhost -Database AdventureWorks2014 -Query "WAITFOR DELAY '00:02:05'" -QueryTimeout 300 |
Pour consulter le timeout définis sur une instance SQL Server, exécuter ce lot d’instructions :
1 2 3 4 5 6 7 8 |
USE [master] GO sp_configure 'show advanced options', 1 GO RECONFIGURE GO sp_configure GO |
Dans le résultat, relever la valeur (run_value) du paramètre remote query timeout (s) :
Pour un temps d’exécution illimité, définir le QueryTimeout à 0. Cela étant, sur les versions SQL Server 2008 et SQL Server 2008 R2, cette valeur n’est pas prise en compte. Pour contourner le problème, procéder comme suit :
1 2 |
# No error : SQL Server 2008 & SQL Server 2008 R2 invoke-sqlcmd -ServerInstance localhost -Database AdventureWorks2014 -Query "WAITFOR DELAY '00:02:05'" -QueryTimeout ([int]::MaxValue) |
Toutes les exceptions ne sont pas détectées, typiquement :
1 |
Invoke-Sqlcmd -ServerInstance localhost -Database AdventureWorks2014 -Query “SELECT 1/0” -ErrorAction 'Stop' -AbortOnError |
Aucune erreur n’est détectée alors qu’en temps normal, quand on exécute la requête depuis SQL Management Studio, on obtient l’exception suivante :
Soyez attentif sur les résultats ramenant du type VARCHAR(MAX), il se peut que les chaînes de caractères soient tronquées à l’arrivée. Dans le cas où vous avez généré du T-SQL dynamique, vous aurez l’occasion de vous en rendre compte lors de l’exécution, votre script générera une erreur de syntaxe en sortie. Pour remédier à cette limitation, utilisez l’option -MaxCharLength.
1 |
Invoke-Sqlcmd -ServerInstance localhost -Database AdventureWorks2014 -Query “SELECT LongColumn FROM LargeTable” -MaxCharLength ([int]::MaxValue) |
Une alternative possible est d’utiliser une variante de la fonction Invoke-Sqlcmd, Invoke-Sqlcmd2 qui n’est certes pas native mais beaucoup plus riche. On peut toutefois regretter l’absence du mode DAC et le fait que l’instruction GO ne soit pas supportée. Le détail de la commande est disponible sur Github :
https://github.com/RamblingCookieMonster/PowerShell/blob/master/Invoke-Sqlcmd2.ps1
D’autres bugs ont été référencés sur Invoke-Sqlcmd : https://blogs.technet.microsoft.com/heyscriptingguy/2013/05/06/10-tips-for-the-sql-server-powershell-scripter/
L’aide est disponible en ligne : https://technet.microsoft.com/fr-fr/library/cc281720(v=sql.110).aspx