Hacer coincidir a] (corchete de cierre) con PATINDEX utilizando el comodín "[]"

9

Estoy escribiendo un analizador JSON personalizado en T-SQL .

Para el propósito de mi analizador, estoy usando la PATINDEXfunción que calcula la posición de un token de una lista de tokens. Los tokens en mi caso son caracteres únicos e incluyen estos:

{} []:,

Por lo general, cuando necesito encontrar la (primera) posición de cualquiera de varios caracteres, uso la PATINDEXfunción de esta manera:

PATINDEX('%[abc]%', SourceString)

La función me dará la primera posición de aor bo c, lo que ocurra primero, en SourceString.

Ahora el problema en mi caso parece estar relacionado con el ]personaje. Tan pronto como lo especifique en la lista de caracteres, por ejemplo, así:

PATINDEX('%[[]{}:,]%', SourceString)

mi patrón deseado aparentemente se rompe, porque la función nunca encuentra una coincidencia. Parece que necesito una forma de escapar del primero ]para que lo PATINDEXtrate como uno de los caracteres de búsqueda en lugar de un símbolo especial.

He encontrado esta pregunta preguntando sobre un problema similar:

Sin embargo, en ese caso, ]simplemente no es necesario especificarlo entre paréntesis, ya que es solo un carácter y se puede especificar sin paréntesis. La solución alternativa, que usa escape, funciona solo para LIKEy no para PATINDEX, porque usa una ESCAPEsubcláusula, respaldada por la primera y no por la segunda.

Por lo tanto, mi pregunta es, ¿hay alguna manera de buscar una ]con PATINDEXel uso del [ ]comodín? ¿O hay una manera de emular esa funcionalidad usando otras herramientas de Transact-SQL?

Información Adicional

Aquí hay un ejemplo de una consulta en la que necesito usar PATINDEXcon el […]patrón como el anterior. El patrón aquí funciona (aunque de alguna manera ) porque no incluye el ]carácter. También lo necesito para trabajar ]:

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)
;

El resultado que obtengo es:

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"   }   

Puede ver que ]se incluye como parte de Suna de las filas. La Levelcolumna indica el nivel de anidamiento, lo que significa anidamiento entre paréntesis y llaves. Como puede ver, una vez que el nivel se convierte en 2, nunca vuelve a 1. Lo habría hecho si pudiera hacer que PATINDEXreconozca ]como una ficha.

El resultado esperado para el ejemplo anterior es:

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"  }

Se puede jugar con esta consulta en db <> violín .


Estamos utilizando SQL Server 2014 y es poco probable que pronto se actualice a una versión que admita el análisis JSON de forma nativa. Podría escribir una solicitud para hacer el trabajo, pero los resultados del análisis deben procesarse aún más, lo que implica más trabajo en la aplicación que solo el análisis, el tipo de trabajo que sería mucho más fácil y probablemente más eficiente. un script T-SQL, si solo pudiera aplicarlo directamente a los resultados.

Es muy poco probable que pueda usar SQLCLR como solución para este problema. Sin embargo, no me importa si alguien decide publicar una solución SQLCLR, ya que eso podría ser útil para otros.

Andriy M
fuente
¿Qué hay de json que se parece ["foo]bar”]?
Salman A
@SalmanA: tales escenarios se pueden ignorar de forma segura.
Andriy M

Respuestas:

6

Mi propia solución, que es más una solución alternativa, consistió en especificar un rango de caracteres que incluía el ]uso de ese rango junto con los otros caracteres en el [ ]comodín. Usé un rango basado en la tabla ASCII. Según esa tabla, el ]personaje se encuentra en el siguiente barrio:

Hex Dec Char
--- --- ----
...
5A 90 Z
5B 91 [
5C 92 \
5D 93]
5E 94 ^
5F 95 _
...

Mi rango, por lo tanto, tomó la forma de [-^, es decir, que incluyó cuatro caracteres: [, \, ], ^. También especifiqué que el patrón usa una intercalación binaria, para que coincida exactamente con el rango ASCII. La PATINDEXexpresión resultante terminó así:

PATINDEX('%[[-^{}:,]%' COLLATE Latin1_General_BIN2, MyJSONString)

El problema obvio con este enfoque es que el rango al comienzo del patrón incluye dos caracteres no deseados, \y ^. La solución funcionó para mí simplemente porque los caracteres adicionales nunca podrían aparecer en las cadenas JSON específicas que necesitaba analizar. Naturalmente, esto no puede ser cierto en general, por lo que todavía estoy interesado en otros métodos, espero que sean más universales que los míos.

Andriy M
fuente
4

Tengo una opinión probablemente terrible de esto cuando tuve que dividir muchas cuerdas.

Si tiene un conjunto de caracteres conocido, haga una tabla con ellos.

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)

Luego usa ese mágico CROSS APPLYjunto con 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 me falta algo obvio sobre lo que debe hacer, déjeme saber.

Erik Darling
fuente
4

He visto enfoques en el pasado para reemplazar el personaje ofensivo antes de buscar y volver a colocarlo después.

En este caso podríamos hacer algo como:

DECLARE @test NVARCHAR(MAX);
DECLARE @replacementcharacter CHAR(1) = CHAR(174);

SET @test = 'Test[]@String'

SELECT PATINDEX('%[[' + @replacementcharacter + '@]%', REPLACE(@test,']',@Replacementcharacter))

Este código devuelve correctamente 5. Estoy usando el carácter ¬ ya que es poco probable que aparezca; si no hay caracteres ASCII que no usará, esta solución no funcionará.

Sin embargo, por extraño que parezca, la respuesta directa a su pregunta sería no: tampoco puedo hacer que PATINDEX busque ']', pero si lo reemplaza no es necesario.

Mismo ejemplo pero sin el uso variable:

DECLARE @test NVARCHAR(MAX);

SET @test = 'Test[]@String'

SELECT PATINDEX('%[[' + CHAR(174) + '@]%', REPLACE(@test,']',CHAR(174)))

Usar la solución anterior en su código produce los resultados requeridos:

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

Como ]solo es especial [...], puedes usar PATINDEXdos veces, moviéndote ]fuera del [...]. Evaluar ambos PATINDEX('%[[{}:,]%', SourceString)y PATINDEX('%]%', SourceString). Si un resultado es cero, tome el otro. De lo contrario, tome el menor de los dos valores.

En tu ejemplo:

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

hvd
fuente
-4

Para una izquierda '[':

PATINDEX('%[[]%',expression)

Por un derecho ']':

PATINDEX('%]%',expression)
Arte
fuente
1
Esto especifica cómo buscar un corchete de apertura o uno de cierre; el OP está buscando uno de varios caracteres (señalado encerrando los caracteres en cuestión entre corchetes), incluido un corchete de cierre.
RDFozz