2025-08-06 11:09:14 -04:00
/**
2026-01-28 17:09:06 -08:00
* Copyright IBM Corp. 2021, 2026
2025-08-06 11:09:14 -04:00
* SPDX-License-Identifier: BUSL-1.1
*/
import { generateSQLExpressions } from 'api/utils/sqlite-query' ;
import { module , test } from 'qunit' ;
module ( 'Unit | Utility | sqlite-query' , function ( hooks ) {
const date = new Date ( 2025 , 1 , 1 );
const isoDateString = date . toISOString ();
hooks . beforeEach ( function () {
String . prototype . removeExtraWhiteSpace = function () {
return this . replace ( /^\s+/gm , '' ). trim ();
};
});
hooks . afterEach ( function () {
delete String . prototype . removeExtraWhiteSpace ;
});
test ( 'it grabs resources not in the model mapping' , function ( assert ) {
const query = {
filters : { id : [{ equals : 'tokenKey' }] },
};
const { sql , parameters } = generateSQLExpressions ( 'token' , query );
assert . strictEqual (
sql ,
`
2025-08-18 13:39:43 -07:00
SELECT * FROM "token"
2025-11-03 17:28:11 -05:00
WHERE ("token".id = ?)` . removeExtraWhiteSpace (),
2025-08-06 11:09:14 -04:00
);
assert . deepEqual ( parameters , [ 'tokenKey' ]);
});
test ( 'it executes count queries correctly' , function ( assert ) {
const select = {
2025-10-31 16:58:05 -04:00
select : [{ field : '*' , isCount : true , alias : 'total' }],
2025-08-06 11:09:14 -04:00
};
const { sql , parameters } = generateSQLExpressions ( 'target' , {}, select );
assert . strictEqual (
sql ,
`
2025-08-18 13:39:43 -07:00
SELECT count(*) as total FROM "target"
2025-11-03 17:28:11 -05:00
ORDER BY "target".created_time DESC` . removeExtraWhiteSpace (),
2025-08-06 11:09:14 -04:00
);
assert . deepEqual ( parameters , []);
});
2025-10-31 16:58:05 -04:00
test . each (
'it generates select clause with various options' ,
{
'single field' : {
select : [{ field : 'name' }],
2025-11-03 17:28:11 -05:00
expectedSelect : '"target".name' ,
2025-10-31 16:58:05 -04:00
},
'multiple fields' : {
select : [{ field : 'name' }, { field : 'type' }, { field : 'id' }],
2025-11-03 17:28:11 -05:00
expectedSelect : '"target".name, "target".type, "target".id' ,
2025-10-31 16:58:05 -04:00
},
'field with alias' : {
select : [{ field : 'name' , alias : 'resource_name' }],
2025-11-03 17:28:11 -05:00
expectedSelect : '"target".name as resource_name' ,
2025-10-31 16:58:05 -04:00
},
'multiple fields with aliases' : {
select : [
{ field : 'name' , alias : 'resource_name' },
{ field : 'type' , alias : 'resource_type' },
],
2026-02-02 18:52:19 -06:00
expectedSelect :
'"target".name as resource_name, "target".type as resource_type' ,
2025-10-31 16:58:05 -04:00
},
'count with alias' : {
select : [{ field : 'id' , isCount : true , alias : 'total_count' }],
2025-11-03 17:28:11 -05:00
expectedSelect : 'count("target".id) as total_count' ,
2025-10-31 16:58:05 -04:00
},
'count without alias' : {
select : [{ field : '*' , isCount : true }],
expectedSelect : 'count(*)' ,
},
'count distinct' : {
select : [{ field : 'status' , isCount : true , isDistinct : true }],
2025-11-03 17:28:11 -05:00
expectedSelect : 'count(DISTINCT "target".status)' ,
2025-10-31 16:58:05 -04:00
},
'count distinct with alias' : {
select : [
{
field : 'type' ,
isCount : true ,
isDistinct : true ,
alias : 'unique_types' ,
},
],
2025-11-03 17:28:11 -05:00
expectedSelect : 'count(DISTINCT "target".type) as unique_types' ,
2025-10-31 16:58:05 -04:00
},
'mixed regular and count fields' : {
select : [
{ field : 'name' },
{ field : 'id' , isCount : true , alias : 'count' },
],
2025-11-03 17:28:11 -05:00
expectedSelect : '"target".name, count("target".id) as count' ,
2025-10-31 16:58:05 -04:00
},
'field with distinct' : {
select : [{ field : 'type' , isDistinct : true }],
2025-11-03 17:28:11 -05:00
expectedSelect : 'DISTINCT "target".type' ,
2025-10-31 16:58:05 -04:00
},
'multiple distinct fields' : {
select : [
{ field : 'type' , isDistinct : true },
{ field : 'status' , isDistinct : true },
],
2025-11-03 17:28:11 -05:00
expectedSelect : 'DISTINCT "target".type, "target".status' ,
2025-10-31 16:58:05 -04:00
},
'multiple distinct with count fields' : {
select : [
{ field : 'type' , isDistinct : true , isCount : true },
{ field : 'status' , isDistinct : true , isCount : true },
],
2026-02-02 18:52:19 -06:00
expectedSelect :
'count(DISTINCT "target".type), count(DISTINCT "target".status)' ,
2025-10-31 16:58:05 -04:00
},
'distinct with alias' : {
select : [{ field : 'type' , isDistinct : true , alias : 'unique_type' }],
2025-11-03 17:28:11 -05:00
expectedSelect : 'DISTINCT "target".type as unique_type' ,
2025-10-31 16:58:05 -04:00
},
'multiple distinct fields with aliases' : {
select : [
{ field : 'type' , isDistinct : true , alias : 'unique_type' },
{ field : 'status' , isDistinct : true , alias : 'unique_status' },
],
2026-02-02 18:52:19 -06:00
expectedSelect :
'DISTINCT "target".type as unique_type, "target".status as unique_status' ,
2025-10-31 16:58:05 -04:00
},
'count distinct multiple fields' : {
select : [
{
field : 'type' ,
isCount : true ,
isDistinct : true ,
alias : 'type_count' ,
},
{
field : 'status' ,
isCount : true ,
isDistinct : true ,
alias : 'status_count' ,
},
],
expectedSelect :
2025-11-03 17:28:11 -05:00
'count(DISTINCT "target".type) as type_count, count(DISTINCT "target".status) as status_count' ,
2025-10-31 16:58:05 -04:00
},
},
function ( assert , { select , expectedSelect }) {
const { sql , parameters } = generateSQLExpressions (
'target' ,
{},
{ select },
);
assert . strictEqual (
sql ,
`
SELECT ${ expectedSelect } FROM "target"
2025-11-03 17:28:11 -05:00
ORDER BY "target".created_time DESC` . removeExtraWhiteSpace (),
2025-10-31 16:58:05 -04:00
);
assert . deepEqual ( parameters , []);
},
);
2025-08-06 11:09:14 -04:00
test . each (
'it generates filters correctly' ,
{
equals : {
query : {
filters : {
type : [{ equals : 'ssh' }],
},
},
2025-11-03 17:28:11 -05:00
expectedWhereClause : 'WHERE ("target".type = ?)' ,
2025-08-06 11:09:14 -04:00
expectedParams : [ 'ssh' ],
},
notEquals : {
query : {
filters : {
type : [{ notEquals : 'ssh' }],
},
},
2025-11-03 17:28:11 -05:00
expectedWhereClause : 'WHERE ("target".type != ?)' ,
2025-08-06 11:09:14 -04:00
expectedParams : [ 'ssh' ],
},
contains : {
query : {
filters : {
type : [{ contains : 'ssh' }],
},
},
2025-11-03 17:28:11 -05:00
expectedWhereClause : 'WHERE ("target".type LIKE ?)' ,
2025-08-06 11:09:14 -04:00
expectedParams : [ '%ssh%' ],
},
greaterThan : {
query : {
filters : {
created_time : [{ gt : date }],
numberField : [{ gt : 10 }],
},
},
2025-11-03 17:28:11 -05:00
expectedWhereClause :
'WHERE ("target".created_time > ?) AND ("target".numberField > ?)' ,
2025-08-06 11:09:14 -04:00
expectedParams : [ isoDateString , 10 ],
},
lessThan : {
query : {
filters : {
created_time : [{ lt : date }],
numberField : [{ lt : 10 }],
},
},
2025-11-03 17:28:11 -05:00
expectedWhereClause :
'WHERE ("target".created_time < ?) AND ("target".numberField < ?)' ,
2025-08-06 11:09:14 -04:00
expectedParams : [ isoDateString , 10 ],
},
greaterThanOrEqual : {
query : {
filters : {
created_time : [{ gte : date }],
numberField : [{ gte : 10 }],
},
},
2025-11-03 17:28:11 -05:00
expectedWhereClause :
'WHERE ("target".created_time >= ?) AND ("target".numberField >= ?)' ,
2025-08-06 11:09:14 -04:00
expectedParams : [ isoDateString , 10 ],
},
lessThanOrEqual : {
query : {
filters : {
created_time : [{ lte : date }],
numberField : [{ lte : 10 }],
},
},
2025-11-03 17:28:11 -05:00
expectedWhereClause :
'WHERE ("target".created_time <= ?) AND ("target".numberField <= ?)' ,
2025-08-06 11:09:14 -04:00
expectedParams : [ isoDateString , 10 ],
},
logicalOperators : {
query : {
filters : {
id : {
logicalOperator : 'and' ,
values : [{ notEquals : 'id1' }, { notEquals : 'id2' }],
},
status : {
logicalOperator : 'or' ,
values : [{ equals : 'active' }, { equals : 'pending' }],
},
type : [{ equals : 'ssh' }],
},
},
expectedWhereClause :
2025-11-03 17:28:11 -05:00
'WHERE ("target".id NOT IN (?, ?)) AND ("target".status IN (?, ?)) AND ("target".type = ?)' ,
2025-08-06 11:09:14 -04:00
expectedParams : [ 'id1' , 'id2' , 'active' , 'pending' , 'ssh' ],
},
2025-11-03 17:28:11 -05:00
equalsNull : {
query : {
filters : {
description : [{ equals : null }],
},
},
expectedWhereClause : 'WHERE ("target".description IS NULL)' ,
expectedParams : [],
},
notEqualsNull : {
query : {
filters : {
description : [{ notEquals : null }],
},
},
expectedWhereClause : 'WHERE ("target".description IS NOT NULL)' ,
expectedParams : [],
},
mixedNullAndValues : {
query : {
filters : {
status : [
{ equals : 'active' },
{ equals : 'pending' },
{ equals : null },
],
type : [{ notEquals : null }, { notEquals : 'ssh' }],
},
},
expectedWhereClause :
'WHERE ("target".status IN (?, ?) OR "target".status IS NULL) AND ("target".type NOT IN (?) OR "target".type IS NOT NULL)' ,
expectedParams : [ 'active' , 'pending' , 'ssh' ],
},
2025-08-06 11:09:14 -04:00
},
function ( assert , { query , expectedWhereClause , expectedParams }) {
const { sql , parameters } = generateSQLExpressions ( 'target' , query );
assert . strictEqual (
sql ,
`
2025-08-18 13:39:43 -07:00
SELECT * FROM "target"
2025-08-06 11:09:14 -04:00
${ expectedWhereClause }
2025-11-03 17:28:11 -05:00
ORDER BY "target".created_time DESC` . removeExtraWhiteSpace (),
2025-08-06 11:09:14 -04:00
);
assert . deepEqual ( parameters , expectedParams );
},
);
test . each (
'it generates sort direction correctly' ,
[
[ 'asc' , 'ASC' ],
[ 'desc' , 'DESC' ],
],
function ( assert , [ direction , expectedDirection ]) {
const query = {
2025-08-18 13:39:43 -07:00
sort : { attributes : [ 'name' ], direction },
};
const { sql , parameters } = generateSQLExpressions ( 'target' , query );
assert . strictEqual (
sql ,
`
SELECT * FROM "target"
2025-11-03 17:28:11 -05:00
ORDER BY "target".name COLLATE NOCASE ${ expectedDirection } , "target".name ${ expectedDirection } ` . removeExtraWhiteSpace (),
2025-08-18 13:39:43 -07:00
);
assert . deepEqual ( parameters , []);
},
);
test . each (
'it generates sort order clause correctly' ,
{
'sort on multiple attributes and coalesced' : {
sort : {
attributes : [ 'name' , 'id' ],
isCoalesced : true ,
},
2025-11-03 17:28:11 -05:00
expectedOrderByClause : `ORDER BY COALESCE("target".name, "target".id) COLLATE NOCASE DESC, COALESCE("target".name, "target".id) DESC` ,
2025-08-18 13:39:43 -07:00
},
'sort on multiple attributes' : {
sort : {
attributes : [ 'name' , 'id' ],
},
2025-11-03 17:28:11 -05:00
expectedOrderByClause : `ORDER BY "target".name COLLATE NOCASE DESC, "target".id COLLATE NOCASE DESC, "target".name DESC, "target".id DESC` ,
2025-08-18 13:39:43 -07:00
},
'sort on mapped attributes' : {
sort : {
attributes : [ 'type' ],
customSort : { attributeMap : { ssh : 'SSH' , tcp : 'Generic TCP' } },
},
2025-11-03 17:28:11 -05:00
expectedOrderByClause : `ORDER BY CASE "target".type WHEN 'ssh' THEN 'SSH' WHEN 'tcp' THEN 'Generic TCP' END DESC` ,
2025-08-18 13:39:43 -07:00
},
},
function ( assert , { sort , expectedOrderByClause }) {
const query = {
sort : { ... sort , direction : 'desc' },
2025-08-06 11:09:14 -04:00
};
const { sql , parameters } = generateSQLExpressions ( 'target' , query );
assert . strictEqual (
sql ,
`
2025-08-18 13:39:43 -07:00
SELECT * FROM "target"
${ expectedOrderByClause } ` . removeExtraWhiteSpace (),
2025-08-06 11:09:14 -04:00
);
assert . deepEqual ( parameters , []);
},
);
test . each (
'it generates pagination clause' ,
[
{ page : 1 , pageSize : 10 , expectedParams : [ 10 , 0 ] },
{ page : 3 , pageSize : 10 , expectedParams : [ 10 , 20 ] },
{ page : 7 , pageSize : 50 , expectedParams : [ 50 , 300 ] },
],
function ( assert , { page , pageSize , expectedParams }) {
const { sql , parameters } = generateSQLExpressions (
'target' ,
{},
{ page , pageSize },
);
assert . strictEqual (
sql ,
`
2025-08-18 13:39:43 -07:00
SELECT * FROM "target"
2025-11-03 17:28:11 -05:00
ORDER BY "target".created_time DESC
2025-08-06 11:09:14 -04:00
LIMIT ? OFFSET ?` . removeExtraWhiteSpace (),
);
assert . deepEqual ( parameters , expectedParams );
},
);
2025-11-03 17:28:11 -05:00
test . each (
'it generates FTS5 search correctly' ,
{
'string search with filters' : {
query : {
search : 'favorite' ,
2025-08-06 11:09:14 -04:00
},
2025-11-03 17:28:11 -05:00
expectedSql : `
SELECT * FROM "target"
WHERE "target".rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?)
ORDER BY "target".created_time DESC` ,
expectedParams : [ '"favorite"*' ],
2025-10-31 16:58:05 -04:00
},
2025-11-03 17:28:11 -05:00
'object search with field-specific searches' : {
query : {
search : {
text : 'favorite' ,
fields : [ 'name' , 'description' ],
},
},
expectedSql : `
SELECT * FROM "target"
WHERE "target".rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?)
ORDER BY "target".created_time DESC` ,
expectedParams : [ 'name:"favorite"* OR description:"favorite"*' ],
2025-10-31 16:58:05 -04:00
},
2025-11-05 17:57:45 -05:00
'search with related searches' : {
query : {
search : {
text : 'dev' ,
select : 'id' ,
relatedSearches : [
{
resource : 'alias' ,
fields : [ 'name' , 'description' ],
join : {
joinOn : 'destination_id' ,
joinFrom : 'id' ,
},
},
],
},
},
expectedSql : `
SELECT * FROM "target"
WHERE "target".id IN (SELECT id FROM target_fts WHERE target_fts MATCH ?
UNION SELECT "target".id FROM alias_fts JOIN "target" ON "target".id = alias_fts.destination_id WHERE alias_fts MATCH ?)
ORDER BY "target".created_time DESC` ,
expectedParams : [ '"dev"*' , 'name:"dev"* OR description:"dev"*' ],
},
'search with multiple related searches' : {
query : {
search : {
text : 'dev' ,
relatedSearches : [
{
resource : 'alias' ,
fields : [ 'name' , 'description' ],
join : {
joinOn : 'destination_id' ,
},
},
{
resource : 'session' ,
fields : [ 'name' ],
join : {
joinOn : 'target_id' ,
},
},
],
},
},
expectedSql : `
SELECT * FROM "target"
WHERE "target".rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?
UNION SELECT "target".rowid FROM alias_fts JOIN "target" ON "target".id = alias_fts.destination_id WHERE alias_fts MATCH ?
UNION SELECT "target".rowid FROM session_fts JOIN "target" ON "target".id = session_fts.target_id WHERE session_fts MATCH ?)
ORDER BY "target".created_time DESC` ,
expectedParams : [
'"dev"*' ,
'name:"dev"* OR description:"dev"*' ,
'name:"dev"*' ,
],
},
2025-11-03 17:28:11 -05:00
},
function ( assert , { query , expectedSql , expectedParams }) {
const { sql , parameters } = generateSQLExpressions ( 'target' , query );
assert . strictEqual ( sql , expectedSql . removeExtraWhiteSpace ());
assert . deepEqual ( parameters , expectedParams );
},
);
2025-10-31 16:58:05 -04:00
2025-11-03 17:28:11 -05:00
test . each (
'it generates joins with filters' ,
{
'basic joins with filters' : {
query : {
filters : {
type : [{ equals : 'ssh' }],
joins : [
{
resource : 'session' ,
query : {
filters : {
status : [{ equals : 'active' }],
},
},
joinOn : 'target_id' ,
joinType : 'INNER' ,
},
{
resource : 'session-recording' ,
query : {
filters : {
state : [{ equals : 'available' }],
},
},
joinFrom : 'idx' ,
joinOn : 'target_id' ,
joinType : 'LEFT' ,
},
],
},
},
select : [{ field : 'data' }],
expectedSql : `
SELECT "target".data FROM "target"
INNER JOIN "session" session1 ON "target".id = session1.target_id LEFT JOIN "session_recording" session_recording1 ON "target".idx = session_recording1.target_id
WHERE ("target".type = ?) AND ("session1".status = ?) AND ("session_recording1".state = ?)
ORDER BY "target".created_time DESC` ,
expectedParams : [ 'ssh' , 'active' , 'available' ],
},
'multiple joins same table' : {
query : {
filters : {
joins : [
{
resource : 'session' ,
query : {
filters : {
status : [{ equals : 'active' }],
},
},
joinOn : 'target_id' ,
joinType : 'INNER' ,
},
{
resource : 'session' ,
query : {
filters : {
status : [{ equals : 'pending' }],
},
},
joinOn : 'host_id' ,
joinType : 'LEFT' ,
},
],
},
},
select : [{ field : 'id' }],
expectedSql : `
SELECT "target".id FROM "target"
INNER JOIN "session" session1 ON "target".id = session1.target_id LEFT JOIN "session" session2 ON "target".id = session2.host_id
WHERE ("session1".status = ?) AND ("session2".status = ?)
ORDER BY "target".created_time DESC` ,
expectedParams : [ 'active' , 'pending' ],
},
},
function ( assert , { query , select , expectedSql , expectedParams }) {
const { sql , parameters } = generateSQLExpressions ( 'target' , query , {
select ,
});
assert . strictEqual ( sql , expectedSql . removeExtraWhiteSpace ());
assert . deepEqual ( parameters , expectedParams );
},
);
2025-10-31 16:58:05 -04:00
2025-08-06 11:09:14 -04:00
test ( 'it generates SQL with all clauses combined' , function ( assert ) {
const query = {
search : 'favorite' ,
filters : {
type : [{ equals : 'ssh' }],
status : {
logicalOperator : 'or' ,
values : [{ equals : 'active' }, { equals : 'pending' }],
},
created_time : [{ gte : date }],
},
2025-08-18 13:39:43 -07:00
sort : { attributes : [ 'name' ], direction : 'desc' },
2025-08-06 11:09:14 -04:00
};
const { sql , parameters } = generateSQLExpressions ( 'target' , query , {
page : 2 ,
pageSize : 15 ,
2025-10-31 16:58:05 -04:00
select : [{ field : 'data' }],
2025-08-06 11:09:14 -04:00
});
assert . strictEqual (
sql ,
`
2025-11-03 17:28:11 -05:00
SELECT "target".data FROM "target"
WHERE ("target".type = ?) AND ("target".status IN (?, ?)) AND ("target".created_time >= ?) AND "target".rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?)
ORDER BY "target".name COLLATE NOCASE DESC, "target".name DESC
2025-08-06 11:09:14 -04:00
LIMIT ? OFFSET ?` . removeExtraWhiteSpace (),
);
assert . deepEqual ( parameters , [
'ssh' ,
'active' ,
'pending' ,
isoDateString ,
'"favorite"*' ,
15 ,
15 ,
]);
});
2025-10-31 16:58:05 -04:00
test . each (
'it throws assertion error for invalid select combinations' ,
{
'mixing distinct with non-distinct columns' : {
select : [
{ field : 'type' , isDistinct : true },
{ field : 'status' , isDistinct : true },
{ field : 'name' },
],
expectedError :
/Can not combine non-distincts with multi column distincts/ ,
},
'mixing distinct with aggregate non-distinct columns' : {
select : [
{ field : 'type' , isDistinct : true },
{ field : 'status' , isDistinct : true },
{ field : 'name' , isCount : true },
],
expectedError :
/Can not combine non-distincts with multi column distincts/ ,
},
},
function ( assert , { select , expectedError }) {
assert . throws (() => {
generateSQLExpressions ( 'target' , {}, { select });
}, expectedError );
},
);
2025-08-06 11:09:14 -04:00
test . each (
'it handles empty and invalid fields' ,
{
emptyObject : {},
emptySearch : { search : '' },
emptyFilter : { filters : {} },
emptyFilterAttributes : { filters : { type : [] } },
2025-08-18 13:39:43 -07:00
invalidSortAttribute : {
sort : { attributes : [ 'test' ], direction : 'asc' },
},
invalidCoalescedSortAttribute : {
sort : {
attributes : [ 'id' , 'test' ],
direction : 'asc' ,
isCoalesced : true ,
},
},
2025-08-06 11:09:14 -04:00
onlyPage : { page : 10 },
onlyPageSize : { pageSize : 5 },
},
function ( assert , query ) {
2025-08-18 13:39:43 -07:00
const { sql : targetSql , parameters : targetParameters } =
generateSQLExpressions ( 'target' , query );
2025-08-06 11:09:14 -04:00
assert . strictEqual (
2025-08-18 13:39:43 -07:00
targetSql ,
2025-08-06 11:09:14 -04:00
`
2025-08-18 13:39:43 -07:00
SELECT * FROM "target"
2025-11-03 17:28:11 -05:00
ORDER BY "target".created_time DESC` . removeExtraWhiteSpace (),
2025-08-06 11:09:14 -04:00
);
2025-08-18 13:39:43 -07:00
assert . deepEqual ( targetParameters , []);
const { sql : resourceSql , parameters : resourceParameters } =
generateSQLExpressions ( 'resource' , query );
assert . strictEqual (
resourceSql ,
`
SELECT * FROM "resource"` . removeExtraWhiteSpace (),
);
assert . deepEqual ( resourceParameters , []);
2025-08-06 11:09:14 -04:00
},
);
});