// Row ordering shared by the dataserver (DATA_LOGON ORDER_BY, an index name; // without one, RECORD_ID) and request servers (REQ_ DETAILS.ORDER_BY, fields // with a direction). import type { Row } from '../types.ts'; import { RECORD_ID } from './identity.ts'; // Nulls sort low (first ascending, last descending), as H2's default null // ordering does; numbers numerically, booleans false before true, anything // else by its string form (UTF-16 order, as Java's String.compareTo). export function compareValues(a: unknown, b: unknown): number { if (a === b) return 0; if (a === undefined || a === null) return -1; if (b === undefined || b === null) return 1; if (typeof a === 'number' && typeof b === 'number') return a - b; if (typeof a === 'boolean' && typeof b === 'boolean') return a ? 1 : -1; const [left, right] = [String(a), String(b)]; return left < right ? -1 : left > right ? 1 : 0; } export interface OrderedField { field: string; descending: boolean; } const INTEGER_TEXT = /^-?\d+$/; // RECORD_IDs compared as the integers they are: the engine's own stamps are // numbers, but a seed may bring the real server's ~7.5e18 ids as strings, // which neither a string nor a float comparison orders correctly. Anything // that isn't an integer falls back to compareValues. export function compareRecordIds(a: unknown, b: unknown): number { const asInteger = (value: unknown) => typeof value === 'number' && Number.isSafeInteger(value) ? BigInt(value) : typeof value === 'string' && INTEGER_TEXT.test(value) ? BigInt(value) : undefined; const [left, right] = [asInteger(a), asInteger(b)]; if (left === undefined || right === undefined) return compareValues(a, b); return left < right ? -1 : left > right ? 1 : 0; } // Oldest first: RECORD_ID ascending, the order rows were written in. export function byRecordId(a: Row, b: Row): number { return compareRecordIds(a[RECORD_ID], b[RECORD_ID]); } // Sorts by the fields in turn, then by RECORD_ID ascending: the tie-breaker // column GSF's SQL ordering always appends (GetByCriteriaOperation.kt, // tieBreakerColumn = "RECORD_ID", v8.15.29), whatever the direction. export function rowComparator(fields: OrderedField[]): (a: Row, b: Row) => number { return (a, b) => { for (const { field, descending } of fields) { const result = compareValues(a[field], b[field]); if (result !== 0) return descending ? -result : result; } return byRecordId(a, b); }; } const ORDER_SEPARATOR = /\s*,\s*/; const ORDER_FIELD = /^([A-Za-z]\w+)(?:\s+(ASC|DESC))?$/i; // DETAILS.ORDER_BY as a request server reads it (OrderSpec.kt OrderedString, // GSF v8.15.29): comma-separated fields, each with an optional ASC or DESC in // any case. Throws GSF's IllegalArgumentException text for anything else, // which the request server answers as REQUEST_FAILED. export function parseOrderBy(orderBy: string): OrderedField[] { return orderBy.split(ORDER_SEPARATOR).map((part) => { const match = ORDER_FIELD.exec(part); if (!match) { throw new Error( `${orderBy} is not a valid ORDER string, Fields should be comma separated, with followed ` + "by an optional ASC or DESC. E.g. 'TRADER, TRADE_DATE DESC'", ); } return { field: match[1], descending: match[2]?.toUpperCase() === 'DESC' }; }); }