J'écris un analyseur JSON personnalisé en T-SQL†.
Aux fins de mon analyseur, j'utilise la fonction PATINDEX
qui calcule la position d'un jeton à partir d'une liste de jetons. Les jetons dans mon cas sont tous des caractères uniques et ils comprennent ceux-ci:
{} []:,
Habituellement, lorsque j'ai besoin de trouver la (première) position de l'un des nombreux caractères donnés, j'utilise la fonction PATINDEX
comme ceci:
PATINDEX('%[abc]%', SourceString)
La fonction me donnera alors la première position de a
ou b
ou c
- selon ce qui se trouve en premier - dans SourceString
.
Maintenant, le problème dans mon cas semble être lié au ]
personnage. Dès que je le précise dans la liste des personnages, par ex. comme ça:
PATINDEX('%[[]{}:,]%', SourceString)
mon modèle prévu devient apparemment cassé, car la fonction ne trouve jamais de correspondance. On dirait que j'ai besoin d'un moyen d'échapper au premier ]
pour que PATINDEX
le traite comme l'un des caractères de recherche plutôt que comme un symbole spécial.
J'ai trouvé cette question posant sur un problème similaire:
Cependant, dans ce cas, le ]
n'a tout simplement pas besoin d'être spécifié entre crochets, car il s'agit d'un seul caractère et il peut être spécifié sans crochets autour d'eux. La solution alternative, qui utilise l'échappement, ne fonctionne que pour LIKE
et non pour PATINDEX
, car elle utilise un sous-paragraphe ESCAPE
, pris en charge par le premier et non par le second.
Donc, ma question est, existe-t-il un moyen de rechercher un ]
avec PATINDEX
en utilisant le [ ]
caractère générique? Ou existe-t-il un moyen d'émuler cette fonctionnalité à l'aide d'autres outils Transact-SQL?
Voici un exemple de requête où je dois utiliser PATINDEX
avec le […]
modèle comme ci-dessus. Le modèle fonctionne ici (bien que quelque peu) car il n'inclut pas le ]
caractère. J'en ai besoin pour travailler avec ]
ainsi que:
WITH
data AS (SELECT CAST('{"f1":["v1","v2"],"f2":"v3"}' AS varchar(max)) AS ResponseJSON),
parser AS
(
SELECT
Level = 1,
OpenClose = 1,
P = p.P,
S = SUBSTRING(d.ResponseJSON, 1, NULLIF(p.P, 0) - 1),
C = SUBSTRING(d.ResponseJSON, NULLIF(p.P, 0), 1),
ResponseJSON = SUBSTRING(d.ResponseJSON, NULLIF(p.P, 0) + 1, 999999)
FROM
data AS d
CROSS APPLY (SELECT PATINDEX('%[[{]%', d.ResponseJSON)) AS p (P)
UNION ALL
SELECT
Level = ISNULL(d.OpenClose - 1, 0) + d.Level + ISNULL(oc.OpenClose, 0),
OpenClose = oc.OpenClose,
P = d.P + p.P,
S = SUBSTRING(d.ResponseJSON, 1, NULLIF(p.P, 0) - 1),
C = c.C,
ResponseJSON = SUBSTRING(d.ResponseJSON, NULLIF(p.P, 0) + 1, 999999)
FROM
parser AS d
CROSS APPLY (SELECT PATINDEX('%[[{}:,]%' COLLATE Latin1_General_BIN2, d.ResponseJSON)) AS p (P)
CROSS APPLY (SELECT SUBSTRING(d.ResponseJSON, NULLIF(p.P, 0), 1)) AS c (C)
CROSS APPLY (SELECT CASE WHEN c.C IN ('[', '{') THEN 1 WHEN c.C IN (']', '}') THEN 0 END) AS oc (OpenClose)
WHERE 1=1
AND p.P <> 0
)
SELECT
*
FROM
parser
OPTION
(MAXRECURSION 0)
;
La sortie que j'obtiens est:
Level OpenClose P S C ResponseJSON
----- --------- -- ----- -- ---------------------------
1 1 1 { "f1":["v1","v2"],"f2":"v3"}
1 null 6 "f1" : ["v1","v2"],"f2":"v3"}
2 1 7 [ "v1","v2"],"f2":"v3"}
2 null 12 "v1" , "v2"],"f2":"v3"}
2 null 18 "v2"] , "f2":"v3"}
2 null 23 "f2" : "v3"}
2 0 28 "v3" }
Vous pouvez voir que le ]
est inclus dans le cadre de S
dans l'une des lignes. La colonne Level
indique le niveau d'imbrication, c'est-à-dire l'imbrication des crochets et des accolades. Comme vous pouvez le voir, une fois que le niveau devient 2, il ne revient jamais à 1. Il l'aurait fait si je pouvais faire reconnaître PATINDEX
]
comme un jeton.
La sortie attendue pour l'exemple ci-dessus est:
Level OpenClose P S C ResponseJSON
----- --------- -- ---- -- ---------------------------
1 1 1 { "f1":["v1","v2"],"f2":"v3"}
1 NULL 6 "f1" : ["v1","v2"],"f2":"v3"}
2 1 7 [ "v1","v2"],"f2":"v3"}
2 NULL 12 "v1" , "v2"],"f2":"v3"}
2 0 17 "v2" ] ,"f2":"v3"}
1 NULL 18 , "f2":"v3"}
1 NULL 23 "f2" : "v3"}
1 0 28 "v3" }
Vous pouvez jouer avec cette requête à db <> violon .
† Nous utilisons SQL Server 2014 et il est peu probable que nous mettions à niveau prochainement vers une version qui prend en charge l'analyse JSON en mode natif. Je pourrais écrire une application pour faire le travail, mais les résultats de l'analyse doivent être traités davantage, ce qui implique plus de travail dans l'application que simplement l'analyse - le type de travail qui serait beaucoup plus facile, et probablement plus efficace, effectué avec un script T-SQL, si seulement je pouvais l'appliquer directement aux résultats.
Il est très peu probable que je puisse utiliser SQLCLR comme solution à ce problème. Cependant, cela ne me dérange pas si quelqu'un décide de publier une solution SQLCLR, car cela pourrait être utile pour d'autres.
Ma propre solution, qui est plus une solution de contournement, consistait à spécifier une plage de caractères qui comprenait le ]
et en utilisant cette plage avec les autres caractères du [ ]
caractère générique. J'ai utilisé une plage basée sur la table ASCII. Selon cette table, le ]
le personnage se trouve dans le quartier suivant:
Hex Dec Char --- --- ---- … 5A 90 Z 5B 91 5C 92\ 5D 93] 5E 94 ^ 5F 95 _ …
Ma gamme a donc pris la forme de [-^
, c'est-à-dire qu'il comprenait quatre caractères: [
, \
, ]
, ^
. J'ai également spécifié que le modèle utilise un classement binaire, pour correspondre exactement à la plage ASCII. L'expression PATINDEX
résultante a fini par ressembler à ceci:
PATINDEX('%[[-^{}:,]%' COLLATE Latin1_General_BIN2, MyJSONString)
Le problème évident avec cette approche est que la plage au début du modèle comprend deux caractères indésirables, \
et ^
. La solution a fonctionné pour moi simplement parce que les caractères supplémentaires ne pouvaient jamais apparaître dans les chaînes JSON spécifiques que j'avais besoin d'analyser. Naturellement, cela ne peut pas être vrai en général, donc je suis toujours intéressé par d'autres méthodes, espérons-le plus universelles que les miennes.
J'ai une prise de position probablement terrible de l'arrière quand j'ai dû faire beaucoup de fractionnement de cordes.
Si vous avez un jeu de caractères connu, faites-en un tableau.
CREATE TABLE dbo.characters ( character CHAR(1) NOT NULL PRIMARY KEY CLUSTERED );
INSERT dbo.characters ( character )
SELECT *
FROM (
SELECT '[' UNION ALL
SELECT ']' UNION ALL
SELECT '{' UNION ALL
SELECT '}' UNION ALL
SELECT ','
) AS x (v)
Ensuite, utilisez ce magique CROSS APPLY
avec CHARINDEX
:
SELECT TOP 1000 p.Id, p.Body, ca.*
FROM dbo.Posts AS p
CROSS APPLY (
SELECT TOP 1 CHARINDEX(c.character, p.Body) AS first_things_first
FROM dbo.characters AS c
ORDER BY CHARINDEX(c.character, p.Body) ASC
) AS ca
WHERE ca.first_things_first > 0
Si je manque quelque chose d'évident sur ce que vous devez faire, laissez-moi le savoir.
J'ai vu des approches dans le passé pour remplacer le personnage incriminé avant de chercher et de le remettre après.
Dans ce cas, nous pourrions faire quelque chose comme:
DECLARE @test NVARCHAR(MAX);
DECLARE @replacementcharacter CHAR(1) = CHAR(174);
SET @test = 'Test[]@String'
SELECT PATINDEX('%[[' + @replacementcharacter + '@]%', REPLACE(@test,']',@Replacementcharacter))
Ce code renvoie correctement 5. J'utilise le caractère ¬ car il est peu probable qu'il apparaisse - s'il n'y a pas de caractères ASCII que vous n'utiliserez pas, cette solution ne fonctionnera pas.
Curieusement, la réponse directe à votre question serait non - je ne peux pas non plus demander à PATINDEX de rechercher "]", mais si vous le remplacez, vous n'en avez pas besoin.
Même exemple mais sans l'utilisation variable:
DECLARE @test NVARCHAR(MAX);
SET @test = 'Test[]@String'
SELECT PATINDEX('%[[' + CHAR(174) + '@]%', REPLACE(@test,']',CHAR(174)))
L'utilisation de la solution ci-dessus dans votre code donne les résultats requis:
WITH
data AS (SELECT CAST('{"f1":["v1","v2"],"f2":"v3"}' AS varchar(max)) AS ResponseJSON),
parser AS
(
SELECT
Level = 1,
OpenClose = 1,
P = p.P,
S = SUBSTRING(d.ResponseJSON, 1, NULLIF(p.P, 0) - 1),
C = SUBSTRING(d.ResponseJSON, NULLIF(p.P, 0), 1),
ResponseJSON = SUBSTRING(d.ResponseJSON, NULLIF(p.P, 0) + 1, 999999)
FROM
data AS d
CROSS APPLY (SELECT PATINDEX('%[[{'+ CHAR(174) + ']%', REPLACE(d.ResponseJSON,']',CHAR(174)))) AS p (P)
UNION ALL
SELECT
Level = ISNULL(d.OpenClose - 1, 0) + d.Level + ISNULL(oc.OpenClose, 0),
OpenClose = oc.OpenClose,
P = d.P + p.P,
S = SUBSTRING(d.ResponseJSON, 1, NULLIF(p.P, 0) - 1),
C = c.C,
ResponseJSON = SUBSTRING(d.ResponseJSON, NULLIF(p.P, 0) + 1, 999999)
FROM
parser AS d
CROSS APPLY (SELECT PATINDEX('%[[{}:,'+ CHAR(174) + ']%' COLLATE Latin1_General_BIN2, REPLACE(d.ResponseJSON,']',CHAR(174)))) AS p (P)
CROSS APPLY (SELECT SUBSTRING(d.ResponseJSON, NULLIF(p.P, 0), 1)) AS c (C)
CROSS APPLY (SELECT CASE WHEN c.C IN ('[', '{') THEN 1 WHEN c.C IN (']', '}') THEN 0 END) AS oc (OpenClose)
WHERE 1=1
AND p.P <> 0
)
SELECT
*
FROM
parser
OPTION
(MAXRECURSION 0)
;
Comme ]
N'est spécial que dans [...]
, Vous pouvez utiliser PATINDEX
deux fois, déplaçant ]
En dehors de [...]
. Évaluez à la fois PATINDEX('%[[{}:,]%', SourceString)
et PATINDEX('%]%', SourceString)
. Si un résultat est nul, prenez l'autre. Sinon, prenez la moindre des deux valeurs.
Dans votre exemple:
WITH
data AS (SELECT CAST('{"f1":["v1","v2"],"f2":"v3"}' AS varchar(max)) AS ResponseJSON),
parser AS
(
SELECT
Level = 1,
OpenClose = 1,
P = p.P,
S = SUBSTRING(d.ResponseJSON, 1, NULLIF(p.P, 0) - 1),
C = SUBSTRING(d.ResponseJSON, NULLIF(p.P, 0), 1),
ResponseJSON = SUBSTRING(d.ResponseJSON, NULLIF(p.P, 0) + 1, 999999)
FROM
data AS d
CROSS APPLY (SELECT PATINDEX('%[[{]%', d.ResponseJSON)) AS p (P)
UNION ALL
SELECT
Level = ISNULL(d.OpenClose - 1, 0) + d.Level + ISNULL(oc.OpenClose, 0),
OpenClose = oc.OpenClose,
P = d.P + ISNULL(p.P, 0),
S = SUBSTRING(d.ResponseJSON, 1, p.P - 1),
C = c.C,
ResponseJSON = SUBSTRING(d.ResponseJSON, p.P + 1, 999999)
FROM
parser AS d
CROSS APPLY (VALUES (NULLIF(PATINDEX('%[[{}:,]%', d.ResponseJSON), 0), NULLIF(PATINDEX('%]%', d.ResponseJSON), 0))) AS p_ (a, b)
CROSS APPLY (VALUES (CASE WHEN p_.a < p_.b OR p_.b IS NULL THEN p_.a ELSE p_.b END)) AS p (P)
CROSS APPLY (SELECT SUBSTRING(d.ResponseJSON, p.P, 1)) AS c (C)
CROSS APPLY (SELECT CASE WHEN c.C IN ('[', '{') THEN 1 WHEN c.C IN (']', '}') THEN 0 END) AS oc (OpenClose)
WHERE 1=1
AND p.P <> 0
)
SELECT
*
FROM
parser
OPTION
(MAXRECURSION 0)
;
https://dbfiddle.uk/?rdbms=sqlserver_2014&fiddle=66fba2218d8d7d310d5a682be143f6eb