diff --git a/src/Runtime/XSharp.Data/RDD/DbDataTable.prg b/src/Runtime/XSharp.Data/RDD/DbDataTable.prg
index 35e28fd592..a4af8487c0 100644
--- a/src/Runtime/XSharp.Data/RDD/DbDataTable.prg
+++ b/src/Runtime/XSharp.Data/RDD/DbDataTable.prg
@@ -77,6 +77,13 @@ PROTECT _nArea AS LONG
oData[nI] := oRDD:GetValue(nI)
IF oData[nI] IS STRING VAR strValue
oData[nI] := strValue:TrimEnd()
+ ELSEIF oData[nI] IS XSharp.IDate VAR dValue
+ // DBF data fields hand out a DbDate, which a DateTime column cannot store
+ IF dValue:Month == 0
+ oData[nI] := DBNull.Value
+ ELSE
+ oData[nI] := dValue:Value
+ ENDIF
ENDIF
NEXT
SELF:_AddRow(oData,oRDD:RecNo)
diff --git a/src/Runtime/XSharp.VFP.Tests/CursorToXmlTests.prg b/src/Runtime/XSharp.VFP.Tests/CursorToXmlTests.prg
new file mode 100644
index 0000000000..bf6cd6bd3b
--- /dev/null
+++ b/src/Runtime/XSharp.VFP.Tests/CursorToXmlTests.prg
@@ -0,0 +1,278 @@
+//
+// Copyright (c) XSharp B.V. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0.
+// See License.txt in the project root for license information.
+//
+USING System
+USING System.IO
+USING System.Text
+USING XUnit
+
+// The expected values in these tests were captured from Visual FoxPro 9
+// running the same cursors through CURSORTOXML().
+BEGIN NAMESPACE XSharp.VFP.Tests
+
+ CLASS CursorToXmlTests
+
+ STATIC CONSTRUCTOR
+ XSharp.RuntimeState.Dialect := XSharpDialect.FoxPro
+ END CONSTRUCTOR
+
+ #region helpers
+
+ PRIVATE METHOD CreateTestCursor() AS VOID
+ CREATE CURSOR curxml (id I, nombre C(10), precio N(8,2), fecha D, activo L)
+ INSERT INTO curxml VALUES (1, "uno", 10.50, {^2024-01-15}, .T.)
+ INSERT INTO curxml VALUES (2, "dos", 20.75, {^2024-02-20}, .F.)
+ INSERT INTO curxml VALUES (3, "tres", 30.00, {^2024-03-25}, .T.)
+ GO TOP
+ END METHOD
+
+ PRIVATE METHOD CreateTypesCursor() AS VOID
+ LOCAL tStamp, tEmpty, dEmpty AS USUAL
+ // A datetime literal containing a space cannot be written inside
+ // INSERT INTO, so the values are prepared first.
+ tStamp := CToT("2024-01-15 10:30:45")
+ tEmpty := CToT("")
+ dEmpty := SToD("")
+ CREATE CURSOR curtipos (cchar C(8), nnum N(10,3), ycur Y, ddate D, tstamp T, llog L)
+ INSERT INTO curtipos VALUES ("abc", 12.5, 99.95, {^2024-01-15}, tStamp, .T.)
+ INSERT INTO curtipos VALUES ("", 0, 0, dEmpty, tEmpty, .F.)
+ GO TOP
+ END METHOD
+
+ PRIVATE METHOD TempFile() AS STRING
+ RETURN Path.Combine(Path.GetTempPath(), "CursorToXml_" + Guid.NewGuid():ToString("N") + ".xml")
+ END METHOD
+
+ // Writes the given alias to a temporary file and returns the XML
+ PRIVATE METHOD ToXml(cAlias AS STRING) AS STRING
+ RETURN SELF:ToXml(cAlias, 512, 0, "")
+ END METHOD
+
+ PRIVATE METHOD ToXml(cAlias AS STRING, nFlags AS LONG, nRecords AS LONG, cSchema AS STRING) AS STRING
+ LOCAL cXml AS STRING
+ VAR cFile := SELF:TempFile()
+ CursorToXml(cAlias, cFile, 1, nFlags, nRecords, cSchema)
+ cXml := File.ReadAllText(cFile)
+ File.Delete(cFile)
+ RETURN cXml
+ END METHOD
+
+ PRIVATE METHOD CountOf(cText AS STRING, cNeedle AS STRING) AS LONG
+ LOCAL nCount := 0 AS LONG
+ LOCAL nPos := 0 AS LONG
+ DO WHILE TRUE
+ nPos := cText:IndexOf(cNeedle, nPos, StringComparison.Ordinal)
+ IF nPos < 0
+ EXIT
+ ENDIF
+ nCount++
+ nPos += cNeedle:Length
+ ENDDO
+ RETURN nCount
+ END METHOD
+
+ #endregion
+
+ #region structure
+
+ [Fact, Trait("Category", "CursorToXml")];
+ METHOD RootIsVfpDataAndRowElementIsLowercaseAlias AS VOID
+ SELF:CreateTestCursor()
+ VAR cXml := SELF:ToXml("curxml")
+ Assert.Contains("", cXml)
+ Assert.Contains("", cXml)
+ // VFP writes the alias in lower case, one element per record
+ Assert.Equal(3, SELF:CountOf(cXml, ""))
+ END METHOD
+
+ [Fact, Trait("Category", "CursorToXml")];
+ METHOD FieldNamesAreLowercase AS VOID
+ SELF:CreateTestCursor()
+ VAR cXml := SELF:ToXml("curxml")
+ Assert.Contains("", cXml)
+ Assert.DoesNotContain("", cXml)
+ END METHOD
+
+ #endregion
+
+ #region value formatting
+
+ [Fact, Trait("Category", "CursorToXml")];
+ METHOD NumericKeepsTheFieldScale AS VOID
+ SELF:CreateTestCursor()
+ VAR cXml := SELF:ToXml("curxml")
+ // N(8,2): VFP keeps the trailing zeros of the declared scale
+ Assert.Contains("10.50", cXml)
+ Assert.Contains("30.00", cXml)
+ END METHOD
+
+ [Fact, Trait("Category", "CursorToXml")];
+ METHOD CharacterIsTrimmed AS VOID
+ SELF:CreateTestCursor()
+ VAR cXml := SELF:ToXml("curxml")
+ Assert.Contains("uno", cXml)
+ END METHOD
+
+ [Fact, Trait("Category", "CursorToXml")];
+ METHOD LogicalIsWrittenInLowercase AS VOID
+ SELF:CreateTestCursor()
+ VAR cXml := SELF:ToXml("curxml")
+ Assert.Contains("true", cXml)
+ Assert.Contains("false", cXml)
+ END METHOD
+
+ [Fact, Trait("Category", "CursorToXml")];
+ METHOD DateUsesIsoFormat AS VOID
+ SELF:CreateTestCursor()
+ VAR cXml := SELF:ToXml("curxml")
+ Assert.Contains("2024-01-15", cXml)
+ END METHOD
+
+ [Fact, Trait("Category", "CursorToXml")];
+ METHOD DateTimeUsesIsoFormat AS VOID
+ SELF:CreateTypesCursor()
+ VAR cXml := SELF:ToXml("curtipos")
+ Assert.Contains("2024-01-15T10:30:45", cXml)
+ END METHOD
+
+ [Fact, Trait("Category", "CursorToXml")];
+ METHOD CurrencyAlwaysHasFourDecimals AS VOID
+ SELF:CreateTypesCursor()
+ VAR cXml := SELF:ToXml("curtipos")
+ Assert.Contains("99.9500", cXml)
+ Assert.Contains("0.0000", cXml)
+ END METHOD
+
+ [Fact, Trait("Category", "CursorToXml")];
+ METHOD EmptyValuesProduceAnEmptyElement AS VOID
+ SELF:CreateTypesCursor()
+ VAR cXml := SELF:ToXml("curtipos")
+ // empty, but not NULL: the element is present and carries no text
+ Assert.True(cXml:Contains("") .OR. cXml:Contains(""))
+ Assert.True(cXml:Contains("") .OR. cXml:Contains(""))
+ END METHOD
+
+ [Fact, Trait("Category", "CursorToXml")];
+ METHOD NullFieldsAreOmitted AS VOID
+ LOCAL cXml AS STRING
+ CREATE CURSOR curnul (id I, nombre C(10) NULL, fecha D NULL)
+ INSERT INTO curnul VALUES (1, "ok", {^2024-01-15})
+ INSERT INTO curnul VALUES (2, NULL, NULL)
+ GO TOP
+ cXml := SELF:ToXml("curnul")
+ // VFP leaves the element out completely when the field is NULL
+ Assert.Equal(1, SELF:CountOf(cXml, ""))
+ Assert.Equal(1, SELF:CountOf(cXml, ""))
+ Assert.Equal(2, SELF:CountOf(cXml, ""))
+ END METHOD
+
+ #endregion
+
+ #region record selection and cursor position
+
+ [Fact, Trait("Category", "CursorToXml")];
+ METHOD NRecordsLimitsTheNumberOfRows AS VOID
+ SELF:CreateTestCursor()
+ VAR cXml := SELF:ToXml("curxml", 512, 2, "")
+ Assert.Equal(2, SELF:CountOf(cXml, ""))
+ Assert.DoesNotContain("tres", cXml)
+ END METHOD
+
+ [Fact, Trait("Category", "CursorToXml")];
+ METHOD WritingEveryRecordLeavesThePointerAtEof AS VOID
+ SELF:CreateTestCursor()
+ SELF:ToXml("curxml")
+ // VFP does not restore the record pointer
+ Assert.True(EOF())
+ END METHOD
+
+ [Fact, Trait("Category", "CursorToXml")];
+ METHOD NRecordsLeavesThePointerOnTheLastRecordWritten AS VOID
+ SELF:CreateTestCursor()
+ SELF:ToXml("curxml", 512, 2, "")
+ Assert.False(EOF())
+ Assert.Equal((DWORD) 2, RECNO())
+ END METHOD
+
+ #endregion
+
+ #region output targets
+
+ [Fact, Trait("Category", "CursorToXml")];
+ METHOD FileOutputReturnsTheNumberOfBytesWritten AS VOID
+ LOCAL nBytes AS LONG
+ SELF:CreateTestCursor()
+ VAR cFile := SELF:TempFile()
+ nBytes := (LONG) CursorToXml("curxml", cFile, 1, 512, 0, "")
+ Assert.True(File.Exists(cFile))
+ Assert.Equal(nBytes, (LONG) FileInfo{cFile}:Length)
+ File.Delete(cFile)
+ END METHOD
+
+ [Fact, Trait("Category", "CursorToXml")];
+ METHOD MemVarOutputCreatesTheVariable AS VOID
+ PRIVATE cXmlOut
+ LOCAL nBytes AS LONG
+ SELF:CreateTestCursor()
+ // without the file flag cOutput names a memory variable
+ nBytes := (LONG) CursorToXml("curxml", "cXmlOut", 1, 0, 0, "")
+ Assert.True(nBytes > 0)
+ Assert.Contains("", cXmlOut)
+ END METHOD
+
+ [Fact, Trait("Category", "CursorToXml")];
+ METHOD CurrentWorkAreaIsUsedWhenNoAreaIsGiven AS VOID
+ LOCAL cXml AS STRING
+ SELF:CreateTestCursor()
+ VAR cFile := SELF:TempFile()
+ CursorToXml(0, cFile, 1, 512, 0, "")
+ cXml := File.ReadAllText(cFile)
+ File.Delete(cFile)
+ Assert.Equal(3, SELF:CountOf(cXml, ""))
+ END METHOD
+
+ #endregion
+
+ #region flags and schema
+
+ [Fact, Trait("Category", "CursorToXml")];
+ METHOD ContinuousFlagRemovesTheLineBreaks AS VOID
+ SELF:CreateTestCursor()
+ // 512 = to file, 1 = one continuous string
+ VAR cXml := SELF:ToXml("curxml", 513, 0, "")
+ Assert.DoesNotContain(e"\n ", cXml)
+ Assert.Contains("", cXml)
+ END METHOD
+
+ [Fact, Trait("Category", "CursorToXml")];
+ METHOD NoSchemaIsWrittenByDefault AS VOID
+ SELF:CreateTestCursor()
+ VAR cXml := SELF:ToXml("curxml")
+ Assert.DoesNotContain("xs:schema", cXml)
+ END METHOD
+
+ [Fact, Trait("Category", "CursorToXml")];
+ METHOD InlineSchemaIsEmittedWhenSchemaNameIsOne AS VOID
+ SELF:CreateTestCursor()
+ VAR cXml := SELF:ToXml("curxml", 512, 0, "1")
+ Assert.Contains("schema", cXml)
+ END METHOD
+
+ [Fact, Trait("Category", "CursorToXml")];
+ METHOD ExternalSchemaFileIsCreated AS VOID
+ SELF:CreateTestCursor()
+ VAR cXsd := Path.Combine(Path.GetTempPath(), "CursorToXml_" + Guid.NewGuid():ToString("N") + ".xsd")
+ VAR cFile := SELF:TempFile()
+ CursorToXml("curxml", cFile, 1, 512, 0, cXsd)
+ Assert.True(File.Exists(cXsd))
+ File.Delete(cXsd)
+ File.Delete(cFile)
+ END METHOD
+
+ #endregion
+
+ END CLASS
+
+END NAMESPACE
diff --git a/src/Runtime/XSharp.VFP.Tests/XSharp.VFP.Tests.xsproj b/src/Runtime/XSharp.VFP.Tests/XSharp.VFP.Tests.xsproj
index 816e140bb1..5ed2fddd0c 100644
--- a/src/Runtime/XSharp.VFP.Tests/XSharp.VFP.Tests.xsproj
+++ b/src/Runtime/XSharp.VFP.Tests/XSharp.VFP.Tests.xsproj
@@ -97,6 +97,7 @@
+
diff --git a/src/Runtime/XSharp.VFP/Cursors/Xml.prg b/src/Runtime/XSharp.VFP/Cursors/Xml.prg
new file mode 100644
index 0000000000..08f277695d
--- /dev/null
+++ b/src/Runtime/XSharp.VFP/Cursors/Xml.prg
@@ -0,0 +1,222 @@
+//
+// Copyright (c) XSharp B.V. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0.
+// See License.txt in the project root for license information.
+//
+
+using System
+using System.IO
+using System.Data
+using System.Text
+using System.Xml
+using XSharp.RDD
+using XSharp.RDD.Support
+using System.Globalization
+
+// nFlags values for CursorToXml(). Only the ones implemented are listed here:
+DEFINE C2X_FLAG_CONTINUOUS := 1 // Produce unformatted XML as one continuous string
+DEFINE C2X_FLAG_TOFILE := 512 // Send the output to the file name in cOutput
+
+
+///
+[FoxProFunction("CURSORTOXML", FoxFunctionCategory.General, FoxEngine.RuntimeCore, FoxFunctionStatus.Partial, FoxCriticality.Medium)];
+FUNCTION CursorToXML (uArea, cOutput, nOutputFormat, nFlags, nRecords, cSchemaName, cSchemaLocation, cNameSpace ) AS USUAL CLIPPER
+ local nArea as dword
+ local cTarget as string
+ local nFlagsInt as long
+ local nRecs as long
+ local cSchema as string
+ local cNs as string
+ local cXml as string
+
+ if !IsString(cOutput)
+ throw Error.VoDbError(EG_ARG, EDB_PARAM, __FUNCTION__, nameof(cOutput), 2,