0

I need to update two attributes in a XML content that is stored in a ntext column called data

I tried using the xml.modify XQuery method using a CAST or CONVERT on the data column but did not manage to do it:

When trying to use CAST

UPDATE [dbo].[CodeSystemCodes_data] 
    SET
        (CAST([data] as xml)).modify('replace value of (/Utilities.CodeSystems.CodeSystemCodes/@Description)[1] with sql:variable("@NEW_EXAM_NAME")')
    WHERE [data] like '%' + @CURRENT_EXAM_CODE + '%'

I get this error

Incorrect syntax near '(' 

...before the CAST, removing the '(' also fails

I ended up creating a temporary table with single XML type column called xmlData used the modify method on that table and then returned the data to the original table, but this seems like and overkill

How do I use the modify method using the data column of type ntext without using a any kind of intermediary table ? maybe with a CAST or CONVERT or something else. I tried using and XML variable with no success either.

Keep in mind that changing the column type at this moment is not possible since I am not the DBA or the person with the role to decide that.

I am using XML modify method instead of REPLACE since I don't want a wrong replace along the XML data.

There are other questions that are similar but both are unanswered at this moment

Code:

DECLARE @NEW_EXAM_NAME NVARCHAR(10) = 'BCC'
DECLARE @CODE_DESC NVARCHAR(50)

SET @CODE_DESC = @CURRENT_EXAM_CODE + ' - ' + @NEW_EXAM_NAME

CREATE TABLE #temp_xml (xmlData XML)

INSERT INTO #temp_xml
    SELECT CAST([data] AS XML)
    FROM [dbo].[CodeSystemCodes_data]   
    WHERE [data] LIKE '%' + @CURRENT_EXAM_CODE + '%'

UPDATE #temp_xml
SET xmlData.modify('replace value of (/Utilities.CodeSystems.CodeSystemCodes/@Description)[1] with sql:variable("@NEW_EXAM_NAME")')

UPDATE #temp_xml
SET xmlData.modify('replace value of (/Utilities.CodeSystems.CodeSystemCodes/@CodeAndDescription)[1] with sql:variable("@CODE_DESC")')
        
SELECT * FROM #temp_xml

UPDATE [dbo].[CodeSystemCodes_data] 
SET [data] = CAST(CAST((SELECT xmlData FROM #temp_xml) as nvarchar(max)) as ntext)
WHERE [data] LIKE '%' + @CURRENT_EXAM_CODE + '%'

DROP TABLE #temp_xml
Mauricio Gracia Gutierrez
  • 10,288
  • 6
  • 68
  • 99
  • I mention in my question that I am not the DBA or have the role to decide on that, I just asked yesterday if there are any plans to migrate away from 'ntext' and they said 'no'. Nothing I can do I about that. – Mauricio Gracia Gutierrez Oct 01 '20 at 14:47

1 Answers1

2

Unfortunately, you're pretty much stuck doing something similar to what you have when your XML is stored as text.

You can try something like this if you want to get rid of the use of temp tables. You can run this in SSMS.

/* Base table mock-up */
DECLARE @Data TABLE ( [data] NTEXT, [id] INT IDENTITY (1,1) );
INSERT INTO @Data ( [data] ) VALUES 
    ( '<root><values><val>Value 1</val><val>to_be_changed</val></values></root>' ),
    ( '<root><values><val>All is well here.</val><val>All is well here, too.</val></values></root>' ),
    ( '<root><values><val>Another value.</val><val>to_be_changed</val></values></root>' );

/* Find/Replace variables */
DECLARE 
    @find_value VARCHAR(50) = 'to_be_changed',
    @replace_value VARCHAR(50) = 'Value 2';

/* Create a table variable to temporarily house the ntext data as xml so the XML may be modified */
DECLARE @Temp TABLE ( DataXml XML, id INT );

/* Insert [data] into the XML column */
INSERT INTO @Temp ( DataXml, [id] )
SELECT CAST ( [data] AS XML ), [id] FROM @Data WHERE [data] LIKE '%' + @find_value + '%';

/* Show the @Data resultset before modifying */
SELECT * FROM @Data;

/* The WHILE is to make sure every node that requires updating gets updated */
/* Modify each instance matching the @find_value criteria */
WHILE EXISTS ( SELECT * FROM @Temp WHERE DataXml.exist( '//root/values/val/text()[.=sql:variable("@find_value")]' ) = 1 )
UPDATE @Temp
SET
    DataXml.modify ('
        replace value of (/root/values/val/text()[.=sql:variable("@find_value")])[1]
        with sql:variable("@replace_value")
    ');

/* Update the results back to the ntext column */
UPDATE @Data
SET
    [data] = CAST ( t.DataXml AS NVARCHAR(MAX) )
FROM @Data d
INNER JOIN @Temp t
    ON d.id = t.id;

/* Show the updated @Data resultset */
SELECT * FROM @Data;

The initial select of @Data:

/* Show the @Data resultset before modifying */
SELECT * FROM @Data;

Returns

+---------------------------------------------------------------------------------------------+----+
|                                            data                                             | id |
+---------------------------------------------------------------------------------------------+----+
| <root><values><val>Value 1</val><val>to_be_changed</val></values></root>                    |  1 |
| <root><values><val>All is well here.</val><val>All is well here, too.</val></values></root> |  2 |
| <root><values><val>Another value.</val><val>to_be_changed</val></values></root>             |  3 |
+---------------------------------------------------------------------------------------------+----+

And the final resultset of @Data:

/* Show the updated @Data resultset */
SELECT * FROM @Data;

Returns

+---------------------------------------------------------------------------------------------+----+
|                                            data                                             | id |
+---------------------------------------------------------------------------------------------+----+
| <root><values><val>Value 1</val><val>Value 2</val></values></root>                          |  1 |
| <root><values><val>All is well here.</val><val>All is well here, too.</val></values></root> |  2 |
| <root><values><val>Another value.</val><val>Value 2</val></values></root>                   |  3 |
+---------------------------------------------------------------------------------------------+----+

Possible alternative method: Perhaps a simple REPLACE on your text.

UPDATE @Data
SET
    [data] = REPLACE ( CAST ( [data] AS NVARCHAR(MAX) ), @find_value, @replace_value )
FROM @Data d
WHERE
    d.[data] LIKE '%' + @find_value + '%';

UPDATE:

I should have been more clear by saying "I don't want to use any kind of intermediary tables"

/* For-each find/replace instance found... */
WHILE EXISTS ( SELECT * FROM @Data WHERE CAST ( [data] AS XML ).exist( '//root/values/val/text()[.=sql:variable("@find_value")]' ) = 1 )
BEGIN

    DECLARE @id INT, @xml XML;
    SELECT TOP 1
        @id = id,
        @xml = CAST ( [data] AS XML )
    FROM @Data
    WHERE CAST ( [data] AS XML ).exist( '//root/values/val/text()[.=sql:variable("@find_value")]' ) = 1;

    -- Modify the XML --
    SET @xml.modify('
        replace value of (/root/values/val/text()[.=sql:variable("@find_value")])[1]
        with sql:variable("@replace_value")
    ');

    -- Update the modified XML --
    UPDATE @Data
    SET
        [data] = CAST ( @xml AS NVARCHAR(MAX) )
    WHERE id = @id;

END

/* Show the updated resultset */
SELECT * FROM @Data ORDER BY id;

Updated Resultset:

+---------------------------------------------------------------------------------------------+----+
|                                            data                                             | id |
+---------------------------------------------------------------------------------------------+----+
| <root><values><val>Value 1</val><val>Value 2</val><val>Value 2</val></values></root>        |  1 |
| <root><values><val>All is well here.</val><val>All is well here, too.</val></values></root> |  2 |
| <root><values><val>Another value.</val><val>Value 2</val></values></root>                   |  3 |
+---------------------------------------------------------------------------------------------+----+

UPDATE BY OP

Thanks for your last solution, I ended up doing this, no need for a while loop

DECLARE @CURRENT_EXAM_CODE NVARCHAR(10) = 'BXC_14B'
DECLARE @NEW_EXAM_NAME NVARCHAR(10) = 'BCC'
DECLARE @CODE_DESC NVARCHAR(50)
DECLARE @XML_DATA XML


-- convert existing NTEXT data into XML
SELECT @XML_DATA =
    CAST([data] as xml)
    FROM [dbo].[CodeSystemCodes_data]   
    WHERE [data] like '%' + @CURRENT_EXAM_CODE + '%'

-- update the xml data
SET @XML_DATA.modify('replace value of (/Utilities.CodeSystems.CodeSystemCodes/@Description)[1] with sql:variable("@NEW_EXAM_NAME")')

SET @CODE_DESC = @CURRENT_EXAM_CODE + ' - ' + @NEW_EXAM_NAME

SET @XML_DATA.modify('replace value of (/Utilities.CodeSystems.CodeSystemCodes/@CodeAndDescription)[1] with sql:variable("@CODE_DESC")')

-- convert xml data back to ntext type
UPDATE [dbo].[CodeSystemCodes_data] 
    SET 
        [data] = CAST(CAST(@XML_DATA as nvarchar(max)) as ntext)
    WHERE [data] like '%' + @CURRENT_EXAM_CODE + '%'
Mauricio Gracia Gutierrez
  • 10,288
  • 6
  • 68
  • 99
critical_error
  • 6,306
  • 3
  • 14
  • 16
  • thanks for that, maybe I should have been more clear by saying "I don't want to use any kind of intermediary tables" ;-) – Mauricio Gracia Gutierrez Oct 01 '20 at 14:13
  • also I wanted to use XML modify method since it avoids replacing an occurrence somewhere else in the XML data – Mauricio Gracia Gutierrez Oct 01 '20 at 14:15
  • 1
    @MauricioGraciaGutierrez That's going to be a tough one, avoiding any intermediary tables. I tried several ways, using a CTE, CROSS APPLY, CAST/CONVERT, but none of them worked. I'll give it some thought when I have a moment, though. – critical_error Oct 01 '20 at 16:06
  • Lets say that your approach or even my current approach will be used, but for learning purposes it will be great to find a better way to do it. – Mauricio Gracia Gutierrez Oct 01 '20 at 16:17
  • 1
    @MauricioGraciaGutierrez I think I have an answer for you! Check out the `UPDATE` to my answer. – critical_error Oct 01 '20 at 17:37
  • 1
    @MauricioGraciaGutierrez Nice on the update! The WHILE was to make sure every node that requires updating gets updated as it only does [1] at a time, but if you don't need it, even better! – critical_error Oct 02 '20 at 17:42