Skip to main content

Additional tQL features

Data Transformation with the Painless Script (instead of Format-function)

select converted statusId as "Status", priorityId as priority,  creationDate,
script( ' def month;
if (doc["creationDate"].size()!=0)
{
Instant instant = Instant.ofEpochMilli(Long.parseLong(doc["creationDate"].value.millis.toString()));
ZonedDateTime zdt = ZonedDateTime.ofInstant(instant, ZoneId.of("Z"));
month = zdt.getMonthValue();
}
else month = 0;
return month ') as month
from ticket
where creationDate between '2022-02-01T00:00:00.000+0000' and '2022-02-28T00:00:00.000+0000'
order by statusId desc

Ordering collection of JSON-doc by fields

The ORDER BY clause is used to sort the results of SELECT by one or more expressions ORDER BY expression [ ASC | DESC ] [, ...], where the expression represents an input column or an output column**.**

select typeCode as "PEW type"
, id as "PEW count"
, chars.project
from ticket
where creationDate between '2022-03-01T00:00:00.000+0000' and '2022-03-05T00:00:00.000+0000'
order by chars.project asc
Limit 5
PEW typePEW countchars.project
Storye0b3ba37-cccd-4ee9-8e3e-1598bdec6a73ADAPSCET
Storyae1e9265-cda2-4d70-ba47-41c921e51fd7ADAPSCET
Subtaskca9ace47-28c2-4881-b8dc-c71820f0b9d9CET
Subtask15aa1cd5-aa3b-4961-8426-74eb82565971CET
Story1318e692-d78e-43cc-8ebe-81345e4ad155CET

Using Group by and Painless for Calculation in Nested Objects

More complex example:

select custom.worklogs.workerId,
round(sum(custom.worklogs.duration)/60/8, 1) AS "Sum duration MD"
from ticket
group by custom.worklogs[startDate > '2022-01-01T00:00:00.000+0000'].workerId
custom.worklogs.workerIdSum duration MD
6f30f0d3-1074-45a9-bfe9-93f0c802a10a618.9
d8769f98-9758-4fcd-80a2-410c9d915b5292.5
e7f5865c-7719-4a14-beef-4bce2aae34f930.6
bd4cabfe-17b5-4d43-9020-7b8d0418756774.5

or even:

select custom.worklogs[].workerId,
format(histogram(custom.worklogs[startDate > '2022-01-01T00:00:00.000+0000'].startDate, 'month'), 'MM') as "Month",
round(sum(custom.worklogs.duration)/60/8, 1) AS "Sum duration MD"
from ticket
group by custom.worklogs[].workerId
, histogram(custom.worklogs[startDate > '2022-01-01T00:00:00.000+0000'].startDate, 'month')
custom.worklogs.workerIdMonthSum duration MD
6f30f0d3-1074-45a9-bfe9-93f0c802a10a01118
e7f5865c-7719-4a14-beef-4bce2aae34f90111.3
6c570ee6-139e-47b0-ac8f-8c9ae2d203ee0116.6
d8769f98-9758-4fcd-80a2-410c9d915b520116.4

Post-Processing

All tQL queries run against the ElasticSearch index, and query results are then translated to tabular form. They are post-processed before returning to the client.

Converted

The general expression for post-processing is “converted”.

select converted key, name, priorityId, statusId from ticket
TSMTTS-1430Display modificationBlockerClosed
TSMTTS-1429Another issueMajorClosed

In this case, each column is consulted with tSM Listing Column Definition to display “converter” and found values. The value is then translated (typical converters are “register value id” -> register value name”, boolean to “Yes”/”No” etc.).

Please note that the result is the same as if you use the convert method for all fields.

select convert(key), convert(name), convert(priorityId), convert(statusId) from ticket

The conversion optional parameters:

select converted(withOrigins) *  from ticket
  • withOrigins - add original values as a new column at the end with _origin prefix. It is useful for aggregation reports, where we need information about the original value before conversion

select converted(withOrigins) statusId from ticket group by statusId

statusId origin_statusId
Nový 17da3ee8-ac27-0633-61f5-b03bec1d79e4
Uzavřen e9d53fcd-f867-439e-c1ed-8667cc48038e
Zpracování f4961557-06c0-4b5c-bdc1-2a76efc1c2e2
  • useConvertOnBackend - consult column definition for the flag convertOnBackend and do the conversion only if the flag is set. We use this settings from tSM UI application to select which fields should be converted on backend (e.g. ID to name translation) and which on frontend only (e.g. user icon)

  • useExportFields - if exportField is defined in the listing column definition, use this column instead of the requested field. We use it for export to excel.

  • skipConvert - switch off conversion. It can be used in combination with other parameters.

select converted(skipConvert) statusId
from ticket
group by statusId

statusId
17da3ee8-ac27-0633-61f5-b03bec1d79e4
e9d53fcd-f867-439e-c1ed-8667cc48038e
f4961557-06c0-4b5c-bdc1-2a76efc1c2e2

Internals

If you execute a query like this:

select priorityCode, round(count(id) / 100, 1) from ticket group by priorityCode
priorityCodecalc4
0.8
blocker3.4
critical6.2
major33.6

What happens in the background is:

  • internal column “__internalCalc1” is created for count(id)
  • internal column “__internalCalc2” is created for “__internalCalc1” / 100
  • internal column “__internalCalc3” is created for “round(__internalCalc2)”
  • Only “__internalCalc1” makes it to the ElasticSearch query
  • For each ElasticSearch result row, the internal calculation for __internalCalc2 and __internalCalc3 is evaluated
  • The result is returned as calc4 (from __internalCalc3)

Functions

A list of functions supported in tQL.

Select transformation

script

Script functions support painless scripts for extended data transformation.

For additional information about painless see https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting-painless.html

For example, it could be used simultaneously to check null values and transform data:

select typeCode as "PEW type"
, script('if (doc["closedDate"].size() !=0)
doc["closedDate"].value.millis / (60.0 * 10.0*60);
else "2022-03-04T01:16:43.046+0000"
') as closedDate
from ticket
limit 2
PEW typeclosedDateEW type
2022-03-04T01:16:43.046+0000
Subtask45648841.277777776Subtask

script_array

Elasticsearch always returns script results as an array. The default behavior in tQL is that if the array contains only one field, it will return the extracted value. But if you know that the result should be an array, you might force it with this function instead of the standard script.

select typeCode as "PEW type"
,script_array(
'def array = new def[] {2, 3, 4, 6};
return array ') as closedDate
from ticket
where closedDate between '2022-03-04T01:16:43.046+0000' and
'2022-03-14T01:16:43.046+0000'
limit 30
PEW typeclosedDate
Ukol2,3,4,6
Subtask2,3,4,6
Subtask2,3,4,6
Subtask2,3,4,6
Subtask2,3,4,6

searchHit

searchHit() returns the whole object from ElasticSearch. This function is mainly used for internal tSM purposes, but you can use it also to consult the whole JSON document content.

select searchHit() from ticket
{
"_index": "dataliteticket_20220516_064313",
"_type": "_doc",
"_id": "null",
"_score": 1.0,
"_source": {
"custom": {
"worklogs": [
{
"_index": "dataliteticket_20220516_064313",
"_type": "_doc",
"duration": 60,
"workerId": "04f316cd-4897-45e3-8145-7bf9e951c9a4",
"endDate": "2021-09-16T12:16:0"
}
]
}
}
}

JSON

Constructs JSON objects as a result, and it can be joined from multiple columns.

select
-- property names are generated automatically if not specified
json(key, name),
-- key can be identifier or double quoted string
json(myKey: key, "My complex key": name),
-- example of constant and complex value in json function call
json(name: 'myScript', scriptValue:
script('return params._source["tasks"]?.stream()?.collect(Collectors.toList())')
)
from ticket

Datediff

Returns the difference between two dates. The first parameter is the result units.

select converted key as "ID"
, tasks[].id
, tasks.history.userGroupCode
, dateDiff('ms', tasks[].startTime, first(tasks[].history[changes = 'USER_GROUP' and userId is not null].startTime)) as Date_Diff
from ticket
where tasks[].history[changes = 'USER_GROUP'] and key = 'PEW-3973'
group by key
, tasks[].id
, tasks[].history[changes = 'USER_GROUP'].userGroupCode
, tasks[].startTime
IDhistory - user groupDate_Diff
pew-3972ZPEW192
pew-3973ZPEW1596
pew-3974ZPEW911
pew-3975ZPEW1525
pew-3976ZPEW192

You can use other values - mcs', 'ms', 'ss, 'mi', 'hh', 'dd', 'wi', 'mo', 'qq', and 'yyyy’.

Notice. The converted result is a truncated integer value of a time interval (for example 22 h -> 0 d).

select datediff('mcs','2019-12-31T23:59:59.00+0000', '2020-01-01T00:00:00.00+0000') as Interval_mcs
, datediff('ms','2019-12-31T23:59:59.00+0000', '2020-01-01T00:00:00.00+0000') as Interval_ms
, datediff('ss','2019-12-31T23:59:59.00+0000', '2020-01-01T00:00:00.00+0000') as Interval_ss
, datediff('mi','2019-12-31T23:59:59.00+0000', '2020-01-01T00:00:00.00+0000') as Interval_mi
, datediff('hh','2019-12-31T23:59:59.00+0000', '2020-01-01T00:00:00.00+0000') as Interval_hh
, datediff('dd','2019-12-31T23:59:59.00+0000', '2020-01-01T00:00:00.00+0000') as Interval_dd
, datediff('wi','2019-12-31T23:59:59.00+0000', '2020-01-01T00:00:00.00+0000') as Interval_wi
, datediff('mo','2019-12-31T23:59:59.00+0000', '2020-01-01T00:00:00.00+0000') as Interval_mo
, datediff('qq','2019-12-31T23:59:59.00+0000', '2020-01-01T00:00:00.00+0000') as Interval_qq
, datediff('yyyy','2019-12-31T23:59:59.00+0000', '2020-01-01T00:00:00.00+0000') as Interval_yyyy
from ticket
limit 1
Interval_mcsInterval_msInterval_ssInterval_miInterval_hhInterval_ddInterval_wiInterval_moInterval_qqInterval_yyyy
1000000100010000000

If you need an approximate result, you can transform milliseconds (‘ms’) by arithmetic transformation like

dateDiff('ms', tasks[].startTime, tasks[].history[].startTime) /1000.0 / 60.0 /60.0

and use the format function.

select round(datediff('ms',tasks.history.startTime,tasks.history.endTime)/1000.0/60.0 , 1)
from ticket
group by tasks[].history[].startTime, tasks[].history[].endTime
Duration
13.1
0.7
0
91.2
200.1
8
1213.4
13.4

Note that if datediff function has been used with nested values, you have to use group by clause to return single values of dates (since nested values could contain an array of dates) otherwise the result would be null.

Wrong result

select round(datediff('ms',tasks.history.startTime,tasks.history.endTime)/1000.0/60.0 , 1)
from ticket
Duration
null
null
null

Post-Processing

round

select round(avg(custom.worklogs.duration), 2) as rand_avg
from nested(ticket, custom.worklogs)
group by custom.worklogs[]
rand_avg
160.95

format

format(whenInserted, 'dd.MM.yyyy')

select format(histogram(creationDate, 'month'), "YYYY-MM") as month
from ticket
month
2022-02
2022-03
2022-04
2022-05
  • "dd" represents the day of the month
  • "MM" represents the month
  • "yyyy" represents the year
  • "HH" represents the hour in 24-hour format
  • "mm" represents the minutes
  • "ss" represents the seconds

convert

Use keyword converted for po st-processing, and use a converter to translate one value for another:

  • select
  • convert(priority), // default converter configured for column priority
  • convert(myBooleanCol, 'booleanTranslateValueConvertor'), // convert with boolean converter

For example:

Converted rules definition is allocated in Listeners->Fields configuration

Select convert(priorityId), priorityName
from ticket
group by priorityId, priorityName
PrioritypriorityName
Majormajor
Trivialtrivial
Blockerblocker
Minorminor
Criticalcritical

Without conversion, the result looks a little bit different

Select priorityId, priorityName
from ticket
group by priorityId, priorityName
priorityIdpriorityName
6e71a823-806e-3d03-11ee-0f0e2d55061dmajor
6fd9ef55-bcec-f460-1244-0b43b3605b34trivial
795c96a5-4b5b-7b8a-9f57-428b33aa0534blocker
910c2db8-77da-2e2b-590b-b00f929492feminor
c9c96175-67b8-e8c0-6f4b-59a67a53f808critical

Notice. Converted is not applied to the WHEN clause because it’s post-processing operation

select convert(priorityId) as priority, convert(statusId) as Status custom.worklogs.workerIdfrom ticket
where priorityName like 'major'
order by statusId desc

Additional Samples of the Complex Usage of tQL Clauses for Data Retrieval and Transformation

Support of sum, count, max, min, and avg realized in tQL could also be applied with complex painless scripts:

select count(id)
, max(
'def month, d;
if (doc["creationDate"].size()!=0)
{ Instant instant = Instant.ofEpochMilli(Long
.parseLong(doc["creationDate"]
.value.millis.toString()));
ZonedDateTime zdt = ZonedDateTime.ofInstant(instant, ZoneId.of("Z"));
month = zdt.getMonthValue();
}
return month') as max_month
, round(avg(
'def month, d;
if (doc["creationDate"].size()!=0)
{ Instant instant = Instant.ofEpochMilli(Long
.parseLong(doc["creationDate"]
.value.millis.toString()));
ZonedDateTime zdt = ZonedDateTime.ofInstant(instant, ZoneId.of("Z"));
month = zdt.getMonthValue();
}
return month'
) , 2) as avg_duration
from ticket
where statusName like 'Closed'

Date Transformation with the Painless Script (instead of Format-function)

select converted statusId as "Status", priorityId as priority,  creationDate,
script( ' def month;
if (doc["creationDate"].size()!=0)
{
Instant instant = Instant.ofEpochMilli(Long.parseLong(doc["creationDate"].value.millis.toString()));
ZonedDateTime zdt = ZonedDateTime.ofInstant(instant, ZoneId.of("Z"));
month = zdt.getMonthValue();
}
else month = 0;
return month ') as month
from ticket
where creationDate between '2022-02-01T00:00:00.000+0000' and '2022-02-28T00:00:00.000+0000'
order by statusId desc

Convert

All tQL queries run against the ElasticSearch index, and query results are then translated to tabular form. They are post-processed before returning to the client. The general expression for post-processing is “converted”.

select converted key, name, priorityId, statusId from ticket
TSMTTS-1430Display modificationBlockerClosed
TSMTTS-1429Another issueMajorClosed

In this case, each column is consulted with tSM Listing Column Definition to display “converter” and found values. The value is then translated (typical converters are “register value id” -> register value name”, boolean to “Yes”/”No” etc.).

Please note that the result is the same as if you use the convert method for all fields.

select convert(key), convert(name), convert(priorityId), convert(statusId) from ticket

Internals

If you execute a query like this:

select priorityCode, round(count(id) / 100, 1) from ticket group by priorityCode
priorityCodecalc4
0.8
blocker3.4
critical6.2
major33.6

What happens in the background is:

  • internal column “__internalCalc1” is created for count(id)
  • internal column “__internalCalc2” is created for “__internalCalc1” / 100
  • internal column “__internalCalc3” is created for “round(__internalCalc2)”
  • Only “__internalCalc1” makes it to the ElasticSearch query
  • For each ElasticSearch result row, the internal calculation for __internalCalc2 and __internalCalc3 is evaluated
  • The result is returned as calc4 (from __internalCalc3)

Aggregation Functions and Painlesss Scripts

Support of sum, count, max, min, and avg realized in tQL could also be applied with complex painless scripts:

select count(id)
, max(
'def month, d;
if (doc["creationDate"].size()!=0)
{ Instant instant = Instant.ofEpochMilli(Long
.parseLong(doc["creationDate"]
.value.millis.toString()));
ZonedDateTime zdt = ZonedDateTime.ofInstant(instant, ZoneId.of("Z"));
month = zdt.getMonthValue();
}
return month') as max_month
, round(avg(
'def month, d;
if (doc["creationDate"].size()!=0)
{ Instant instant = Instant.ofEpochMilli(Long
.parseLong(doc["creationDate"]
.value.millis.toString()));
ZonedDateTime zdt = ZonedDateTime.ofInstant(instant, ZoneId.of("Z"));
month = zdt.getMonthValue();
}
return month'
) , 2) as avg_duration
from ticket
where statusName like 'Closed'