Follow-up to #611. stringSort: 'locale' with localeOptions covers collations that Intl.Collator can express, but a common backend collation is not one of them, and there is no way to plug in the backend's own comparator.
The problem
Postgres databases on glibc's en_US.UTF-8 (the default for many hosted Postgres, including the one behind InstantDB) order text in multiple levels:
- Letters and digits only, with spaces, punctuation and symbols ignored.
- Accents, then case.
- The ignored characters by position.
So +3 power sorts under "3", and pillowfort sorts before pillow fort.
Intl.Collator can ignore punctuation (ignorePunctuation: true), but not symbols. The Unicode extension that would do it (-u-ka-shifted-kv-symbol) is dropped by V8 and JavaScriptCore. On 5,519 real rows, the closest Intl setting misplaced a run of rows starting with symbols. A synced collection whose local order differs from the server's gets wrong windows: a pushed ORDER BY … LIMIT n returns the server's first n, and the local query sorts them differently.
Proposal: a custom string collation
defaultStringCollation: { stringSort: 'custom', compare: (a: string, b: string) => number }
// and per clause
q.orderBy(({ s }) => s.name, { direction: 'asc', stringSort: 'custom', compare })
We're running this as a patch on 0.9.2. It's small:
ascComparator calls opts.compare(a, b) for strings when stringSort === 'custom'.
buildCompareOptionsFromConfig and the builder's orderBy pass compare through, and expression-helpers copies it.
BaseIndex.matchesCompareOptions matches a custom index only to the same compare function.
usesLocaleStringSort in index-optimization becomes stringSort !== 'lexical', so custom collations aren't range-optimized.
canExpressCursorOrder already only allows lexical, so custom collations fall back as locale does.
Happy to open a PR.
A related gap: comparisons ignore the collation
gt/gte/lt/lte evaluate strings with compareValues (code units) whatever the collection's collation, while SQL compares text in the column's collation. That's why cursor-based loading is limited to lexical strings. It also means a keyset predicate like or(gt(name, v), and(eq(name, v), gt(id, k))) can't be evaluated locally in the order the rows are sorted in. Deep pages over a non-lexical string sort have to load their prefix.
Would you consider collation-aware string comparisons, for example comparison operators that use the collection's collation (or an expression-level collate)? With that, string sorts could use cursor loading under any collation.
Follow-up to #611.
stringSort: 'locale'withlocaleOptionscovers collations thatIntl.Collatorcan express, but a common backend collation is not one of them, and there is no way to plug in the backend's own comparator.The problem
Postgres databases on glibc's
en_US.UTF-8(the default for many hosted Postgres, including the one behind InstantDB) order text in multiple levels:So
+3 powersorts under "3", andpillowfortsorts beforepillow fort.Intl.Collatorcan ignore punctuation (ignorePunctuation: true), but not symbols. The Unicode extension that would do it (-u-ka-shifted-kv-symbol) is dropped by V8 and JavaScriptCore. On 5,519 real rows, the closestIntlsetting misplaced a run of rows starting with symbols. A synced collection whose local order differs from the server's gets wrong windows: a pushedORDER BY … LIMIT nreturns the server's first n, and the local query sorts them differently.Proposal: a custom string collation
We're running this as a patch on 0.9.2. It's small:
ascComparatorcallsopts.compare(a, b)for strings whenstringSort === 'custom'.buildCompareOptionsFromConfigand the builder'sorderBypasscomparethrough, andexpression-helperscopies it.BaseIndex.matchesCompareOptionsmatches a custom index only to the samecomparefunction.usesLocaleStringSortinindex-optimizationbecomesstringSort !== 'lexical', so custom collations aren't range-optimized.canExpressCursorOrderalready only allowslexical, so custom collations fall back aslocaledoes.Happy to open a PR.
A related gap: comparisons ignore the collation
gt/gte/lt/lteevaluate strings withcompareValues(code units) whatever the collection's collation, while SQL compares text in the column's collation. That's why cursor-based loading is limited tolexicalstrings. It also means a keyset predicate likeor(gt(name, v), and(eq(name, v), gt(id, k)))can't be evaluated locally in the order the rows are sorted in. Deep pages over a non-lexical string sort have to load their prefix.Would you consider collation-aware string comparisons, for example comparison operators that use the collection's collation (or an expression-level
collate)? With that, string sorts could use cursor loading under any collation.