Friday 16 July 2010

testing migration from 0.9-ml to 1.0-ml

Chances are that ever so often you'll come across some legacy XQuery code in 0.9-ml that should really be upgraded to 1.0-ml for the obvious reasons, and everyone seams to be ever so scared of touching the code, because "it works".

So ultimately your duty is to make sure the code operates exactly as it did before. Ideally you should run each function in each module on the legacy code, and on the refactured code, and then compare the output. At the very least you should run tests on entry point functions in your modules.

Now that's easier said than done. If you have followed a TDD methodology from the outset, great, all you have to do is to run the tests against the refactured code, and bob is your oncle, voila . . . ready to go. But as we all know, often the case is that no tests are available for the legacy code, or tests are targeted at abstracted java DAO interfaces, and really don't dive deep enough into the fragments returned by your xquery.

Some of the queries may even return large xml fragments and making it difficult to compare the output visually, and very expensive to use fn:deep-compare().

If you come across one or more of the above scenarios, then here's how I have dealt with it: First I create a sub-folder containing a clone of the original modules in ML, containing the refactured code. Then you need to import both modules into a single test module.

Note that both sets of modules will need different namespaces even if just temporarily. Only then the test module can import both modules also using different namespace prefixes.

You can then run the same functions simultaneously on both the legacy code, and the refactured code. Assign a variable for the outcome of each and wrap the function call in a xdmp:quote() and then in a xdmp:md5(). Now you can do a simple string comparison of the md5 of both results, and they should match. If they don't your test has failed.

Here's an example:

xquery version "1.0-ml";

import module namespace old = "ns:old-module" at "/folder/module.xqy";
import module namespace new = "ns:new-module" at "/folder/1.0-ml/module.xqy";

declare variable $item1 := old:function1("param 1");
declare variable $item2 := new:function1("param 1");

xdmp:md5(xdmp:quote($item1)) eq xdmp:md5(xdmp:quote($item2))
=> true


You can make a function of it and bang it in a test module that can be re-used in the rest of your tests . . .

xquery version "1.0-ml";

module namespace t = "ns:migration-test-suite";

declare function t:assertEquals($item1 as item()*, $item2 as item()) as xs:string
{if(xdmp:md5(xdmp:quote($item1)) eq xdmp:md5(xdmp:quote($item2))) then "passed" else "failed"};

Tuesday 13 July 2010

MarkLogic cts:query serialization

If you work with MarkLogic you've surely come across cts functions (Built-In: Search), cts:query, and its closest counterparts.

One useful feature available in recent releases is the serialization of cts queries, which allows for the conversion of cts:query types to xml fragments, and the other way round.

If you take a look at the code below, you'll notice the local:mySearch() function which takes a cts:query as it's only parameter. You'll also note cts:search() is used inside this function and references the $query parameter abstracting the query executed by cts:search(). Most interestingly you'll also find a conditional statement invoking an xpath on the $query variable. "But the parameter is strongly typed to cts:query!" . . . you ask. Well that's because it is possible to serialize what would otherwise be "cts:element-value-query(xs:QName("filename"), "myFile", ("lang=en"), 1)" to xml, just by embedding the query in a parent element.

let $query as element(query) := element query {$query}


It is equally possible to do the reverse operation, simply by invoking an xpath returning cts elements in your fragment, wrapped in a cts:query() function.

cts:query($query/*)


This feature also allows for cts queries to be built dynamically as xml fragments rather than concatenated strings.


xquery version '1.0-ml';

declare function local:mySearch($query as cts:query) as element(response)
{
let $query as element(query) := element query {$query}
let $searchResults as element(record)* := cts:search(/record, cts:query($query/*))

return
element response
{
$query,
element results {
if($query/cts:element-value-query/cts:element/text() eq "filename")
then $searchResults//element1
else $searchResults//element2
}
}
};

declare function local:searchByFilename($filename as xs:string) as element(response)
{local:mySearch(cts:element-value-query(xs:QName("filename"), $filename))};

declare function local:searchByVolume($volume as xs:string) as element(response)
{local:mySearch(cts:element-value-query(xs:QName("volume"), $volume))};


local:searchByFilename("myFile")



returns

<response>
<query>
<cts:element-value-query xmlns:cts="http://marklogic.com/cts">
<cts:element>filename</cts:element>
<cts:text xml:lang="en">myfile</cts:text>
</cts:element-value-query>
</query>
<query>cts:element-value-query(xs:QName("filename"), "myfile", ("lang=en"), 1)</query>
<results>
<element1>some content</element1>
<element1>some other content</element1>
</results>
</response>