web-dev-qa-db-fra.com

Correspondance d'un] (crochet de fermeture) avec PATINDEX à l'aide du caractère générique "[]"

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?

Information additionnelle

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.

9
Andriy M

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.

6
Andriy M

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.

4
Erik Darling

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)
;
4
George.Palacios

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

4
hvd