Repository: adamchainz/django-mysql

Issue:
XML ExtractValue function
It does come in useful sometimes (unfortunately).

https://dev.mysql.com/doc/refman/5.5/en/xml-functions.html#function_extractvalue



Discussion:

Resolving patch:
diff --git a/django_mysql/models/functions.py b/django_mysql/models/functions.py
--- a/django_mysql/models/functions.py
+++ b/django_mysql/models/functions.py
@@ -118,6 +118,33 @@ def __init__(self, field, values, **kwargs):
         super(Field, self).__init__(field, *values_exprs)
 
 
+# XML Functions
+
+
+class UpdateXML(Func):
+    function = 'UPDATEXML'
+
+    def __init__(self, xml_target, xpath_expr, new_xml):
+        if not hasattr(xpath_expr, 'resolve_expression'):
+            xpath_expr = Value(xpath_expr)
+        if not hasattr(new_xml, 'resolve_expression'):
+            new_xml = Value(new_xml)
+
+        return super(UpdateXML, self).__init__(xml_target, xpath_expr, new_xml,
+                                               output_field=TextField())
+
+
+class XMLExtractValue(Func):
+    function = 'EXTRACTVALUE'
+
+    def __init__(self, xml_frag, xpath_expr):
+        if not hasattr(xpath_expr, 'resolve_expression'):
+            xpath_expr = Value(xpath_expr)
+
+        return super(XMLExtractValue, self).__init__(xml_frag, xpath_expr,
+                                                     output_field=TextField())
+
+
 # Encryption Functions
 
 


Test patch:
diff --git a/tests/testapp/test_functions.py b/tests/testapp/test_functions.py
--- a/tests/testapp/test_functions.py
+++ b/tests/testapp/test_functions.py
@@ -12,7 +12,8 @@
 from django_mysql.models.functions import (
     CRC32, ELT, MD5, SHA1, SHA2, Abs, AsType, Ceiling, ColumnAdd, ColumnDelete,
     ColumnGet, ConcatWS, Field, Floor, Greatest, LastInsertId, Least,
-    RegexpInstr, RegexpReplace, RegexpSubstr, Round, Sign
+    RegexpInstr, RegexpReplace, RegexpSubstr, Round, Sign, UpdateXML,
+    XMLExtractValue
 )
 from testapp.models import Alphabet, Author, DynamicModel
 from testapp.test_dynamicfield import DynColTestCase
@@ -193,6 +194,43 @@ def test_order_by(self):
         assert avalues == [2, 1, 3, 4]
 
 
+@requiresDatabaseFunctions
+class XMLFunctionTests(TestCase):
+
+    def test_updatexml_simple(self):
+        Alphabet.objects.create(d='<value>123</value>')
+        Alphabet.objects.update(
+            d=UpdateXML('d', '/value', '<value>456</value>')
+        )
+        d = Alphabet.objects.get().d
+        assert d == '<value>456</value>'
+
+    def test_updatexml_annotate(self):
+        Alphabet.objects.create(d='<value>123</value>')
+        d2 = Alphabet.objects.annotate(
+            d2=UpdateXML('d', '/value', '<value>456</value>')
+        ).get().d2
+        assert d2 == '<value>456</value>'
+
+    def test_xmlextractvalue_simple(self):
+        Alphabet.objects.create(d='<some><xml /></some>')
+        Alphabet.objects.create(d='<someother><xml /></someother>')
+        Alphabet.objects.create(d='<some></some><some></some>')
+        evs = list(
+            Alphabet.objects.annotate(ev=XMLExtractValue('d', 'count(/some)'))
+                            .values_list('ev', flat=True)
+        )
+        assert evs == ['1', '0', '2']
+
+    def test_xmlextractvalue_invalid_xml(self):
+        Alphabet.objects.create(d='{"this": "isNotXML"}')
+        ab = (
+            Alphabet.objects.annotate(ev=XMLExtractValue('d', '/some'))
+                            .first()
+        )
+        assert ab.ev == ''
+
+
 @requiresDatabaseFunctions
 class EncryptionFunctionTests(TestCase):