Marklogic cts:or query in Loop - xquery

I want to create a cts:or-query in a for loop. How can I do this?
An example of my logic:
let $query := for $tag in (1,2,3,4,5)
return myquery
I would like to get final queries such as:
let $query := cts:or-query(
(
cts:element-query(xs:QName("ts:tag"),'1'),
cts:element-query(xs:QName("ts:tag"),'2'),
cts:element-query(xs:QName("ts:tag"),'3'),
cts:element-query(xs:QName("ts:tag"),'4'),
cts:element-query(xs:QName("ts:tag"),'5')
)
)

For this particular example it would be better to write a shotgun-OR:
cts:element-value-query(xs:QName("ts:tag"), xs:string(1 to 5))
This will behave like an or-query, but will be a little more efficient. Note that I changed your cts:element-query to an element-value query. That may or may not be what you want, but each query term should be as precise as possible.
You can also use a FLWOR expression to generate queries. This is useful for and-query semantics, where the previous technique doesn't help.
let $query := cts:and-query(
for $i in ('dog', 'cat', 'rat')
return cts:word-query($i))
return cts:search(collection(), $query)[1 to 20]

This will work:
let $query := cts:or-query(
for $val in ('1', '2', '3', '4', '5')
return cts:element-query(xs:QName("ts:tag"), $val)
)
The FLWOR loop returns a sequence of cts:element-query's.

Related

How to sort by dynamically with ascending or descending in Marklogic?

let $sortelement := 'Salary'
let $sortby := 'ascending'
for $doc in collection('employee')
order by $doc/*[local-name() eq $sortelement] $sortby
return $doc
This code throws and error, what is the correct way to do this?
If you are just looking to build the order by dynamically within a FLWOR statement, you can't. As Michael Kay points out in the comments, you could use a conditional statement and decide whether or not to reverse() the ascending (default) sorted sequence.
let $sortelement := 'Salary'
let $sortby := 'ascending'
let $results :=
for $doc in collection('employee')
order by $doc/*[local-name() eq $sortelement]
return $doc
return
if ($sortby eq 'descending')
then reverse($results)
else $results
Depending upon how many documents are in your collection, retrieving every document and sorting them won't scale. It can take a long time, and can exceed memory limits for expanded tree cache.
If you have indexes on those elements, then you can dynamically build a cts:index-order() and specify as the third parameter for cts:search() in order to get them returned in the specified order:
let $sortelement := 'Salary'
let $sortby := 'ascending'
return
cts:search(doc(),
cts:collection-query("employee"),
cts:index-order(cts:element-reference(xs:QName($sortelement)), $sortby)
)

How can I cts query on two values?

I'm trying to work out a nice cts query that matches two nodes, rather than one. For example, I have records from two sources, both with an ID value and a dateTime value. I'd like to find records in the first source that has a matching ID in the second source and a newer dateTime value.
Something like this (does not work):
cts:uris(
(),
(),
cts:and-query((
cts:collection-query("source1"),
cts:path-range-query(
"/record/ID",
"=",
cts:values(
cts:path-reference("/record/ID"),
(),
(),
cts:collection-query("source2")
)
),
cts:path-range-query(
"/record/dateTimeValue",
">",
cts:values(
cts:path-reference("/record/dateTimeValue"),
(),
(),
cts:collection-query("source2")
)
)
))
)
This wont work because it returns records that have an equal ID value and where there also exists a record with a greater dateTimeValue
How do I make the cts query match on two values? Can I only do this through a FLWOR?
If I understand the requirement correctly, this query could be implemented efficiently with a join:
Create a TDE view scoped to the collection with a context of /record that projects id and datetime columns
Use an Optic query to join the view with itself on the id with a condition of a greater datetime and then join the matching documents (or project all of the needed columns from the record)
Something like the following:
const docid = op.fragmentIdCol('docid');
const v1 = op.fromView(null,"source2", "v1", docid);
const v2 = op.fromView(null,"source2", "v2");
v1.joinInner(v2, op.on(v1.col("id"), v2.col("id"),
op.gt(v1.col("datetime"), v2.col("datetime")))
.select(docid)
.joinDoc('doc', 'docid')
.result();
For more detail, see:
https://docs.marklogic.com/ModifyPlan.prototype.joinInner
Hoping that helps,
You can do this with a cts query by searching source1 for every combination found in source2 (or vice versa). I don't know how performant it would be... it doesn't seem like it should be worse than getting two co-occurrence maps and doing it manually.
cts:uris(
(),
(),
cts:and-query((
cts:collection-query("source1"),
cts:or-query((
for $tuple in cts:value-co-occurrences(
cts:path-reference("/record/ID"),
cts:path-reference("/record/dateTimeValue"),
(),
cts:collection-query("source2")
)
return cts:and-query((
cts:path-range-query("/record/ID", "=", $tuple/cts:value[1]),
cts:path-range-query("/record/dateTimeValue", ">", $tuple/cts:value[2])
))
))
)
)
If the ID overlap between source1 and source2 is small, then it's probably better to find the overlap first and plug those IDs into the co-occurrence query, so it isn't scatter-querying so widely.
This does the work:
let $local:cL := function($dt as xs:dateTime, $id as xs:string)
{
let $query :=
cts:and-query((
cts:collection-query("source1"),
cts:path-range-query("/record/dateTimeValue", ">", $dt),
cts:path-range-query("/record/ID", "=", $id)
))
for $uri in cts:uris("", "document", $query)
return
<uri>{$uri}</uri>
}
let $docs := cts:search(doc(), cts:collection-query("source2"))
for $doc in $docs
return
xdmp:apply( $local:cL, $doc/record/dateTimeValue, $doc/record/ID )

Compare a count of subquery with scalar

I'm using Symfony 4.3 and I have a problem in my DQL query.
The purpose of my query is to select email of user how have list of right for this is my DQL query:
$qb = $this->createQueryBuilder('u')
->select('u.email,u.id')
->leftJoin('u.profile', 'profile')
->leftJoin('u.country', 'country')
->leftJoin('profile.privileges', 'pri')
->leftJoin('pri.ressource', 'resource')
$checkRightQuery = $this->em->createQueryBuilder()
->select('count(rc)')
->from(Ressource::class, 'rc')
->leftJoin('rc.privileges', 'privil')
->leftJoin('privil.profile', 'prof')
->leftJoin('prof.user', 'user')
->where( $this->em->getExpressionBuilder()->in('rc.actionFront', ':rights'))
->andWhere('user.id =u.id');
$qb->andWhere(
$this->em->getExpressionBuilder()->eq(count($rights),
$checkRightQuery
)
);
$qb->setParameters(['rights' => $rights]);
The problem is when I take the result of the count it's not a scalar and it can't compare it to scalar.
Any help please?
Try using parenthesis on your condition with the subquery:
$qb->andWhere(
$this->em->getExpressionBuilder()->eq(count($rights),
'(' . $checkRightQuery . ')'
)
);
References
Doctrine return error with “eq”, no with “in”

symfony2 doctrine query negation of where

i wanna query for all of my categories like this:
$othercategories = $this->getDoctrine()->getRepository('Bundle:Category')->findBy(
array('language' => $language, 'active' => 1),
array('sorting' => 'ASC')
);
what i wanna do is to add another parameter to my query, i want all categories EXCEPT one with a specific id. so like:
WHERE id NOT IN ( 2 )
or
WHERE id <> 2
how can i achieve that?
You can use DQL queries like this
$em = $this->getDoctrine()->getEntityManager();
$query = $em->createQuery( 'SELECT c FROM Bundle:Category c WHERE c.language = :language AND c.active = 1 AND c.id NOT IN ( 2 ) ORDER BY c.language ASC' )
->setParameter('language', $language);
$category= $query->getResult();
Sorry I couldn't test this because I am using my phone to answer this question and I don't know your entity variables. Let me know what changes you did to make it work, it will help others.
For more info check http://symfony.com/doc/master/book/doctrine.html
You can add these queries in repository and reuse them. Refer the Cook book on http://symfony.com/doc/master/cookbook/index.html
Hope this helped.
You can use this syntax if you prefer
$repository = $this->getDoctrine()->getRepository('Bundle:Category');
$queryBuilder = $repository->createQueryBuilder();
$notInCategoryIds = array(2); // Category ids that will be excluded
$queryBuilder->select('c')
->from('Bundle:Category', 'c')
->where('c.language = :language')->setParameter('language', $language)
->andWhere('c.active = :active')->setParameter('active', 1)
->andWhere($queryBuilder->expr()->notIn('c.id', $notInCategoryIds)
->orderBy('c.sorting', 'ASC');
$results = $queryBuilder->getQuery()->getResult();
It's probably going to be more useful for other developers that prefers this syntax

How can I order by NULL in DQL?

I'm building an app using Symfony2 framework and using Doctrine ORM. I have a table with airlines for which some IATA codes are missing. I'm outputting a list, ordered by this IATA code, but I'm getting the undesirable result that the records with null IATA codes are sorted at the top.
In MySQL this is simple enough to do, with ORDER BY ISNULL(code_iata), code_iata but I'm clueless as to what the equivalent would be for DQL. I tried
$er->createQueryBuilder('airline')->orderBy('ISNULL(airline.codeIata), airline.codeIata', 'ASC')
but this gives me a syntax error.
The Doctrine docs give me no answer either. Is there a way?
You can use the following trick in DQL to order NULL values last
$em->createQuery("SELECT c, -c.weight AS HIDDEN inverseWeight FROM Entity\Car c ORDER BY inverseWeight DESC");
The HIDDEN keyword (available since Doctrine 2.2) will result in omitting the inverseWeight field from the result set and thus preventing undesirable mixed results.
(The sort fields value is inverted therefore the order has to be inverted too, that's why the query uses DESC order, not ASC.)
Credits belong to this answer.
The most unobtrusive generic solution would be to use the CASE expression in combination with the HIDDEN keyword.
SELECT e,
CASE WHEN e.field IS NULL THEN 1 ELSE 0 END HIDDEN _isFieldNull
FROM FooBundle:Entity e
ORDER BY _isFieldNull ASC
Works with both numeric as well as other field types and doesn't require extending Doctrine.
If you want to do something similar to "NULLS LAST" in SQL (with PostgreSQL in my case):
ORDER BY freq DESC NULLS LAST
You can use the COALESCE function with the Doctrine Query Builder
(HIDDEN will hide the field "freq" on your query result set).
$qb = $this->createQueryBuilder('d')
->addSelect('COALESCE(d.freq, 0) AS HIDDEN freq')
->orderBy('freq', 'DESC')
->setMaxResults(20);
Here it is an example for a custom walker to get exactly what you want. I have taken it from Doctrine in its github issues:
https://github.com/doctrine/doctrine2/pull/100
But the code as it is there didn't work for me in MySQL. I have modified it to work in MySQL, but I haven't test at all for other engines.
Put following walker class for example in YourNS\Doctrine\Waler\ directory;
<?php
namespace YourNS\Doctrine\Walker;
use Doctrine\ORM\Query\SqlWalker;
class SortableNullsWalker extends SqlWalker
{
const NULLS_FIRST = 'NULLS FIRST';
const NULLS_LAST = 'NULLS LAST';
public function walkOrderByClause($orderByClause)
{
$sql = parent::walkOrderByClause($orderByClause);
if ($nullFields = $this->getQuery()->getHint('SortableNullsWalker.fields'))
{
if (is_array($nullFields))
{
$platform = $this->getConnection()->getDatabasePlatform()->getName();
switch ($platform)
{
case 'mysql':
// for mysql the nulls last is represented with - before the field name
foreach ($nullFields as $field => $sorting)
{
/**
* NULLs are considered lower than any non-NULL value,
* except if a – (minus) character is added before
* the column name and ASC is changed to DESC, or DESC to ASC;
* this minus-before-column-name feature seems undocumented.
*/
if ('NULLS LAST' === $sorting)
{
$sql = preg_replace_callback('/ORDER BY (.+)'.'('.$field.') (ASC|DESC)/i', function($matches) {
if ($matches[3] === 'ASC') {
$order = 'DESC';
} elseif ($matches[3] === 'DESC') {
$order = 'ASC';
}
return ('ORDER BY -'.$matches[1].$matches[2].' '.$order);
}, $sql);
}
}
break;
case 'oracle':
case 'postgresql':
foreach ($nullFields as $field => $sorting)
{
$sql = preg_replace('/(\.' . $field . ') (ASC|DESC)?\s*/i', "$1 $2 " . $sorting, $sql);
}
break;
default:
// I don't know for other supported platforms.
break;
}
}
}
return $sql;
}
}
Then:
use YourNS\Doctrine\Walker\SortableNullsWalker;
use Doctrine\ORM\Query;
[...]
$qb = $em->getRepository('YourNS:YourEntity')->createQueryBuilder('e');
$qb
->orderBy('e.orderField')
;
$entities = $qb->getQuery()
->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, '\YourNS\Doctrine\Walker\SortableNullsWalker')
->setHint('SortableNullsWalker.fields', array(
'sortOrder' => SortableNullsWalker::NULLS_LAST
))
->getResult();
DQL does not contain every function of plain SQL. Fortunately you can define your custom DQL method to accomplish this.
Some resources:
http://punkave.com/window/2012/07/24/for-the-php-crowd-adding-custom-functions-to-doctrine-2-dql
http://docs.doctrine-project.org/en/2.1/cookbook/dql-user-defined-functions.html
http://symfony.com/doc/2.0/cookbook/doctrine/custom_dql_functions.html
By default, MySQL will still sort a NULL value; it will just place it at the beginning of the result set if it was sorted ASC, and at the end if it was sorted DESC. Here, you're looking to sort ASC, but you want the NULL values to be at the bottom.
Unfortunately, as powerful as it is, Doctrine isn't going to offer much support here, since function support is limited, and most of it is limited to SELECT, WHERE, and HAVING clauses. You actually wouldn't have a problem at all if any of the following were true about the QueryBuilder:
select() accepted ISNULL()
orderBy() or addOrderBy() supported ISNULL()
the class supported the concept of UNIONs (with this, you could run two queries: one where the codeIata was NULL, and one where it wasn't, and you could sort each independently)
So that said, you can go with the user-defined functions that ArtWorkAD mentioned already, or you could replicate that last point with two different Doctrine queries:
$airlinesWithCode = $er->createQueryBuilder("airline")
->where("airline.iataCode IS NULL")
->getQuery()
->getResult();
$airlinesWithoutCode = $er->createQueryBuilder("airline")
->where("airline.iataCode IS NOT NULL")
->getQuery()
->getResult();
Then you can combine these into a single array, or treat them independently in your templates.
Another idea is to have DQL return everything in one data set, and let PHP do the heavy lifting. Something like:
$airlines = $er->findAll();
$sortedAirlines = array();
// Add non-NULL values to the end if the sorted array
foreach ($airlines as $airline)
if ($airline->getCodeIata())
$sortedAirlines[] = $airline;
// Add NULL values to the end of the sorted array
foreach ($airlines as $airline)
if (!$airline->getCodeIata())
$sortedAirlines[] = $airline;
The downside to both of these is that you won't be able to do LIMITs in MySQL, so it might only work well for relatively small data sets.
Anyway, hope this gets you on your way!

Resources