feat: 🎸 Add custom sort support to sql parser (#2959)

* feat: 🎸 Add custom sort support to sql parser

* test: 💍 fix column numner

* refactor: 💡 apply pr feedback

* fix: 🐛 use every method for checking attrs

* refactor: 💡 PR feedback + ensure default sort

* refactor: 💡 Consolidate attributes field when sorting

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-17441

---------

Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>
This commit is contained in:
Lisbet AlvarezandZhihe Li authored and Zhihe Li committed 2025-09-03 11:41:41 -04:00
1 parent eab93cf6c5
commit d414df4ca4
29 files changed
+333 -260

No files matched your search

+46 -13
View File
@@ -33,18 +33,7 @@ export function generateSQLExpressions(
addFilterConditions(filters, parameters, conditions);
addSearchConditions(search, resource, parameters, conditions);
const { attribute, direction } = sort;
let orderByClause = '';
if (attribute) {
// We have to check if the attribute is valid for the resource
// as we can't use parameterized queries for ORDER BY
const validAttributes = Object.keys(modelMapping[resource]);
if (validAttributes.includes(attribute)) {
orderByClause = `ORDER BY ${attribute} ${direction === 'desc' ? 'DESC' : 'ASC'}`;
}
} else if (modelMapping[resource]?.created_time) {
orderByClause = `ORDER BY created_time DESC`;
}
const orderByClause = constructOrderByClause(resource, sort);
const whereClause = conditions.length ? `WHERE ${and(conditions)}` : '';
@@ -53,7 +42,7 @@ export function generateSQLExpressions(
parameters.push(pageSize, (page - 1) * pageSize);
}
const selectClause = `SELECT ${select ? select.join(', ') : '*'} FROM ${resource}`;
const selectClause = `SELECT ${select ? select.join(', ') : '*'} FROM "${resource}"`;
return {
// Replace any empty newlines or leading whitespace on each line to be consistent with formatting
@@ -145,6 +134,50 @@ function addSearchConditions(search, resource, parameters, conditions) {
conditions.push(searchConditions);
}
function constructOrderByClause(resource, sort) {
const defaultOrderByClause = 'ORDER BY created_time DESC';
const { attributes, customSort, direction, isCoalesced } = sort;
const sortDirection = direction === 'desc' ? 'DESC' : 'ASC';
// We have to check if the attributes are valid for the resource
// as we can't use parameterized queries for ORDER BY
const validAttributes = Object.keys(modelMapping[resource] ?? {});
if (attributes?.some((attr) => !validAttributes.includes(attr))) {
return modelMapping[resource]?.created_time ? defaultOrderByClause : '';
}
if (customSort?.attributeMap) {
const whenClauses = Object.keys(customSort.attributeMap).reduce(
(acc, key) => {
return acc + `WHEN '${key}' THEN '${customSort.attributeMap[key]}' `;
},
'',
);
return `ORDER BY CASE ${attributes.join(', ')} ${whenClauses}END ${sortDirection}`;
} else if (attributes?.length > 0) {
const commaSeparatedVals = attributes.join(', ');
// In places where `collate nocase` is used, it is to ensure case is ignored on the initial sort.
// Then, a sort on the same condition is performed to ensure upper-case strings are given preference in a tie.
if (isCoalesced) {
return `ORDER BY COALESCE(${attributes.join(', ')}) COLLATE NOCASE ${sortDirection}, COALESCE(${commaSeparatedVals}) ${sortDirection}`;
}
const attributesWithNoCollate = attributes
.map((attr) => `${attr} COLLATE NOCASE ${sortDirection}`)
.join(', ');
const attributesWithDirection = attributes
.map((attr) => `${attr} ${sortDirection}`)
.join(', ');
return `ORDER BY ${attributesWithNoCollate}, ${attributesWithDirection}`;
} else if (modelMapping[resource]?.created_time) {
return defaultOrderByClause;
} else {
return '';
}
}
// Comparison Operators
const OPERATORS = {
equals: ' = ?',