Devuelve secuencias xml donde un atributo no contiene un carácter específico

10

Considere el siguiente XML simple:

<xml>
  <customer name="Max">
    <email address="[email protected]" />
  </customer>
  <customer name="Erik">
    <email address="[email protected]" />
  </customer>
  <customer name="Brent">
    <email address="brentcom" />
  </customer>
</xml>

Quiero obtener una lista de <Customer>secuencias donde el addressatributo del <email>elemento no contiene un @.

Entonces, quiero una salida que se vea así:

<customer name="Brent">
  <email address="brentcom" />
</customer>

mcve :

DECLARE @x XML = '<xml>
<customer name="Max"><email address="[email protected]" /></customer>
<customer name="Erik"><email address="[email protected]" /></customer>
<customer name="Brent"><email address="brentcom" /></customer>
</xml>';

Esta consulta:

SELECT WithValidEmail = @x.query('/xml/customer/email[contains(@address, "@")]')
    , WithInvalidEmail = @x.query('/xml/customer/email[contains(@address, "@")] = False');

Devoluciones:

╔═══════════════════════════════════════╦══════════════════╗
            WithValidEmail              WithInvalidEmail 
╠═══════════════════════════════════════╬══════════════════╣
 <email address="[email protected]" />                          
 <email address="[email protected]" />  false            
╚═══════════════════════════════════════╩══════════════════╝

Esta consulta:

SELECT WithInValidEmail = @x.query('/xml/customer/email')
WHERE @x.exist('/xml/customer/email[contains(@address, "@")]') = 0;

Devoluciones:

╔══════════════════╗
 WithInValidEmail 
╚══════════════════╝
    (no results)

La WHEREcláusula en la consulta anterior está eliminando todo el conjunto de XML porque existe al menos una secuencia donde la dirección de correo electrónico contiene un signo "@".

Max Vernon
fuente

Respuestas:

11

Una manera fácil de hacer esto es usar el nodes método para ir directamente al addressatributo y verificar su @signo.

El problema con la forma en que te ves ahora es que solo está verificando que cualquier dirección de correo electrónico tenga una @. Analizar los nodos XML le permite revisar correos electrónicos individuales para ello.

DECLARE @x XML
    = '<xml>
<customer name="Max"><email address="[email protected]" /></customer>
<customer name="Erik"><email address="[email protected]" /></customer>
<customer name="Brent"><email address="brentcom" /></customer>
</xml>';


SELECT x.c.value('@address', 'VARCHAR(100)') AS [email]
FROM   @x.nodes('/xml/customer/email') AS x(c)
WHERE  x.c.exist('@address[contains(., "@")]') = 0;

Si necesita consultar una tabla real con una columna XML como esta, simplemente CROSS APPLYusaría el método de nodos de la siguiente manera:

SELECT x.c.value('@address', 'VARCHAR(100)') AS [email]
FROM @x_table AS xt
CROSS APPLY xt.x.nodes('/xml/customer/email') AS x(c)
WHERE  x.c.exist('@address[contains(., "@")]') = 0;

Si desea recuperar todo el <customer>...</customer>XML de esa "fila", puede recorrer el eje hacia atrás. Solo tenga en cuenta que caminar de regreso puede hacer que el rendimiento sea un poco inestable para grandes bloques XML.

SELECT x.c.query('..')
FROM @x_table AS xt
CROSS APPLY xt.x.nodes('/xml/customer/email') AS x(c)
WHERE  x.c.exist('@address[contains(., "@")]') = 0;

Otra forma de hacerlo es:

SELECT @x.query('/xml/customer[email/@address[not(contains(., "@"))]]') answer

Mover los corchetes para envolver el nodo de correo electrónico efectivamente hace que la WHEREcláusula se aplique al customernodo. La traducción de este XQuery al inglés se ve así:

Consígueme todos los xml/customernodos con un emailnodo que tenga un addressatributo que no contenga el @símbolo

Erik Darling
fuente
4

Estabas tan cerca. Definitivamente estabas en el camino correcto al usar la .query()función y usar la containsfunción XQuery. Lo que te equivocaste fue:

  1. Poniendo el = False exterior de la [...](es decir, no era parte de la contains()expresión)
  2. Usando la palabra en Falselugar de la funciónfalse()
  3. No se especifica el nodo padre al agregarlo /..al final de la ruta (para que el resultado incluya el <customer>elemento y no solo el <email>elemento)

Corregir esas tres cosas da como resultado la siguiente expresión XQuery que te da lo que deseas:

'/xml/customer/email[contains(@address, "@") = false()]/..'

Poner eso en su ejemplo original de la pregunta le da:

DECLARE @x XML = '<xml>
<customer name="Max"><email address="[email protected]" /></customer>
<customer name="Erik"><email address="[email protected]" /></customer>
<customer name="Brent"><email address="brentcom" /></customer>
</xml>';

SELECT
@x.query('/xml/customer/email[contains(@address, "@")]/..') AS [WithValidEmail],
@x.query('/xml/customer/email[contains(@address, "@")=false()]/..') AS [WithInvalidEmail;

Esa consulta devuelve el siguiente conjunto de resultados de una sola fila con dos campos XML:

WithValidEmail                            |     WithInvalidEmail
<customer name="Max">                     |     <customer name="Brent">
  <email address="[email protected]" />          |       <email address="brentcom" />
</customer>                               |     </customer>
<customer name="Erik">                    |
  <email address="[email protected]" />   |
</customer>                               |

Esto es probablemente más eficiente que dividir el documento con la .nodes()función, ya que puede analizar el XML de una sola vez y no es necesario iniciar y detener el analizador por cada nodo.

El otro beneficio de mantenerlo dentro .query()es que obtiene un solo documento XML devuelto. Por lo tanto, si recibe un documento / valor XML que contiene múltiples nodos, puede mantener el enfoque de valor escalar de ser una sola entidad sin tener que reconstruir los nodos resultantes nuevamente en un documento. Esto también le permite usarlo en una subconsulta / CTE sin cambiar el número de filas esperadas que se devuelven.

Solomon Rutzky
fuente