Как запросить XML в SQL, чтобы найти элемент по частичному совпадению text()

Учитывая следующий XML в столбце таблицы:

<UserFields>
  <UserField>
    <Name>UserName</Name>
    <Value>test_user</Value>
  </UserField>
  <UserField>
    <Name>Age</Name>
    <Value>24</Value>
  </UserField>
  <UserField>
    <Name>Password</Name>
    <Value>test1234!</Value>
  </UserField>
  <UserField>
    <Name>AgentPassword</Name>
    <Value>1234Test!</Value>
  </UserField>
</UserFields>
<ActionAttributes>
  <Action>
   <Name>LoginPassword</Name>
   <Value>Test123!</Value>
  </Action>
</ActionAttributes>

Я хочу запросить XML, чтобы найти любой элемент, имеющий узел <Name>, содержащий text() слово password, и заменить текст узла <Value> звездочками. В примере будет три замены, и я получу следующий XML.

<UserFields>
  <UserField>
    <Name>UserName</Name>
    <Value>test_user</Value>
  </UserField>
  <UserField>
    <Name>Age</Name>
    <Value>24</Value>
  </UserField>
  <UserField>
    <Name>Password</Name>
    <Value>********</Value>
  </UserField>
  <UserField>
    <Name>AgentPassword</Name>
    <Value>********</Value>
  </UserField>
</UserFields>
<ActionAttributes>
  <Action>
   <Name>LoginPassword</Name>
   <Value>********</Value>
  </Action>
</ActionAttributes>

Я смог придумать это:

SET @MyDoc.modify('
  replace value of (//UserField[Name = "Password"]/Value/text())[1]
  with "********"
');

Проблема с этим кодом:

  1. Он жестко кодирует имя родительского элемента («UserField»).
  2. Он жестко кодирует имя значения <Name>, поэтому находит только «пароль», а не другие варианты.

По-английски я бы сказал:

«Для любого узла, у которого есть дочерние узлы <Name> и <Value>. Если дочерний узел <Name> содержит слово «пароль», замените текст дочернего узла <Value> звездочками».

Попробуйте //UserField[contains(Name,"Password")] или //UserField[Name = "Password" or Name = "AgentPassword"]

LMC 12.08.2024 17:01

Опция or противоречит цели — попытка избежать необходимости перечислять все случаи, когда пароль может быть в имени. Опция contains выдает ошибку [modify()]: 'contains()' requires a singleton (or empty sequence), found operand of type 'xdt:untypedAtomic *'

RHarris 12.08.2024 17:10

Я считаю, что это конструктивное ограничение реализации XML mssql. or, вероятно, тоже не будет работать, поскольку [1] в конце может ограничивать обновление 1 и только 1 узлом. То же самое, если бы было несколько узлов типа Password.

LMC 12.08.2024 17:19

Не думайте, что метод изменения xml сервера sql может заменить несколько узлов, вам, вероятно, придется проанализировать и воссоздать xml с заменой полей вручную. Однако, как обычно, вопрос касается не той проблемы: зачем работать над манипулированием XML, когда вы можете работать над процессом создания такого XML

siggemannen 12.08.2024 17:34

@siggemannen - Легче поймать его в одном месте, чем пытаться прочесать систему, которая была засыпана за последние 15 лет, и найти все возможные комбинации вызовов, которые можно было бы вставить. :-)

RHarris 12.08.2024 17:42

Ну, конечно, вы не храните XML, но все равно уничтожаете его

siggemannen 12.08.2024 17:44
ReactJs | Supabase | Добавление данных в базу данных
ReactJs | Supabase | Добавление данных в базу данных
Это и есть ваш редактор таблиц в supabase.👇
Понимание Python и переход к SQL
Понимание Python и переход к SQL
Перед нами лабораторная работа по BloodOath:
0
6
66
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Выражение XPath должно определять один узел

Выражение1

Идентифицирует узел, значение которого должно быть обновлено. Он должен идентифицировать только один узел. То есть Expression1 должен быть статическим синглтоном. Если XML является типизированным, тип узла должен быть простым. При выборе нескольких узлов возникает ошибка.

Использование переменных SQL может быть альтернативой, позволяющей избежать жесткого кодирования предиката в выражении.

DECLARE @Name VARCHAR(50) = 'Password';
DECLARE @NewValue VARCHAR(50) = '********';
SET @MyDoc.modify('
replace value of (//UserField[Name = sql:variable("@Name")]/Value/text())[1]
with sql:variable("@NewValue")
');

Итерация возможных значений

DECLARE @myDoc XML;
DECLARE @Name VARCHAR(50);
DECLARE @parent VARCHAR(50);
DECLARE @NewValue VARCHAR(50) = '********';
DECLARE @rowid int;

DECLARE @tnames TABLE(
rowid iNT,
names VARCHAR(50),
parent VARCHAR(50)
);

SET @myDoc = '<root>
<UserFields>
  <UserField>
    <Name>UserName</Name>
    <Value>test_user</Value>
  </UserField>
  <UserField>
    <Name>Age</Name>
    <Value>24</Value>
  </UserField>
  <UserField>
    <Name>Password</Name>
    <Value>test1234!</Value>
  </UserField>
  <UserField>
    <Name>AgentPassword</Name>
    <Value>1234Test!</Value>
  </UserField>
</UserFields>
<ActionAttributes>
  <Action>
   <Name>LoginPassword</Name>
   <Value>Test123!</Value>
  </Action>
</ActionAttributes>
</root>
';

--SELECT @myDoc;

INSERT INTO @tnames (rowid, names, parent)
SELECT ROW_NUMBER() OVER (ORDER BY t.xname) AS ROWID, T.xname.value('(Name/text())[1]', 'varchar(max)') as names, T.xname.value('(local-name(.))[1]', 'varchar(max)') as parent
FROM @myDoc.nodes('//*[Name[contains(., "Password")]]') T(xname); 

SELECT * FROM @tnames;

WHILE (SELECT COUNT(1) FROM @tnames) > 0
BEGIN
    SELECT @rowid = rowid, @Name = names, @parent= parent FROM @tnames;
    
    SET @MyDoc.modify('
    replace value of (//*[local-name() = sql:variable("@parent")][Name = sql:variable("@Name")]/Value/text())[1]
    with sql:variable("@NewValue")
    ');
    
    DELETE FROM @tnames where rowid = @rowid;

    --SELECT ROW_NUMBER() OVER (ORDER BY t.xname) AS ROWID, T.xname.value('(Name/text())[1]', 'varchar(max)') as names, T.xname.value('(Value/text())[1]', 'varchar(max)') as value 
    --FROM @myDoc.nodes('//*[local-name() = sql:variable("@parent")][Name = sql:variable("@Name")]') T(xname);
    
END
Ответ принят как подходящий

Как многие уже упоминали, SQL Server не поддерживает обновление нескольких элементов XML за один раз.

Попробуйте следующее решение, основанное на XQuery SQL Server и его выражении FLWOR.

SQL №1

-- DDL and sample data population, start
DECLARE @tbl TABLE (id INT IDENTITY PRIMARY KEY, xml_data XML);
INSERT INTO @tbl (xml_data) VALUES
(N'<UserFields>
    <UserField>
        <Name>UserName</Name>
        <Value>test_user</Value>
    </UserField>
    <UserField>
        <Name>Age</Name>
        <Value>24</Value>
    </UserField>
    <UserField>
        <Name>Password</Name>
        <Value>test1234!</Value>
    </UserField>
    <UserField>
        <Name>AgentPassword</Name>
        <Value>1234Test!</Value>
    </UserField>
</UserFields>');
-- DDL and sample data population, end

-- just to see
SELECT * 
    , xml_data.query('<UserFields>{
        for $x in /UserFields/*
        return element UserField {
            if (contains(($x/Name/text())[1],"Password")) then
                ($x/*[local-name() != "Value"], element Value {"********"})
            else $x/*
        }
    }</UserFields>') AS modified_xml
FROM @tbl;

-- real deal
UPDATE @tbl
SET xml_data = xml_data.query('<UserFields>{
    for $x in /UserFields/*
    return element UserField {
        if (contains(($x/Name/text())[1],"Password")) then
            ($x/*[local-name() != "Value"], element Value {"********"})
        else $x/*
    }
}</UserFields>');

-- test
SELECT * FROM @tbl;

SQL № 2

Для измененного XML в вопросе мы можем использовать другой подход.

-- DDL and sample data population, start
DECLARE @tbl TABLE (id INT IDENTITY PRIMARY KEY, xml_data XML);
INSERT INTO @tbl (xml_data) VALUES
(N'<UserFields>
  <UserField>
    <Name>UserName</Name>
    <Value>test_user</Value>
  </UserField>
  <UserField>
    <Name>Age</Name>
    <Value>24</Value>
  </UserField>
  <UserField>
    <Name>Password</Name>
    <Value>test1234!</Value>
  </UserField>
  <UserField>
    <Name>AgentPassword</Name>
    <Value>1234Test!</Value>
  </UserField>
</UserFields>
<ActionAttributes>
  <Action>
   <Name>LoginPassword</Name>
   <Value>Test123!</Value>
  </Action>
</ActionAttributes>');
-- DDL and sample data population, end

-- before
SELECT * FROM @tbl;

DECLARE @ReplaceWith VARCHAR(10) = '*********';
DECLARE @UPDATE_STATUS BIT = 1;

WHILE @UPDATE_STATUS > 0 
BEGIN
    UPDATE t
    SET xml_data.modify('replace value of 
        (/*/*[contains((Name/text())[1], "Password") and Value/text()!=sql:variable("@ReplaceWith")]/Value/text())[1] 
        with sql:variable("@ReplaceWith")')
    FROM @tbl AS t
    WHERE xml_data.exist('/*/*[contains((Name/text())[1], "Password") and Value/text()!=sql:variable("@ReplaceWith")]')=1;
        
    SET @UPDATE_STATUS = @@ROWCOUNT;
    PRINT @UPDATE_STATUS;
END;

-- after
SELECT * FROM @tbl;

SQL №2 сделал свою работу! Спасибо тонны. Чего бы это ни стоило, я немного изменил код, чтобы включить замену значений независимо от регистра слова «пароль». ...[contains(upper-case(Name/text())[1]), "PASSWORD") ...

RHarris 13.08.2024 14:45

@RHarris, приятно слышать, что предложенное решение работает для вас!

Yitzhak Khabinsky 13.08.2024 14:47

Есть ли способ заставить его проверять на любом уровне? Прямо сейчас синтаксис (/*/*[contains... заставляет его искать на два уровня глубже. Иногда я нахожу XML, в котором мы можем иметь глубину 3 или 1 уровень.

RHarris 13.08.2024 14:52

Да, это возможно, переключившись на следующее выражение XPath: (//*[contains... в обоих местах.

Yitzhak Khabinsky 13.08.2024 15:10

Также, пожалуйста, проголосуйте за мое предложение в Microsoft: Feedback.azure.com/d365community/idea/… И не пропустите мои многочисленные комментарии.

Yitzhak Khabinsky 13.08.2024 15:13

Другие вопросы по теме