Skip to content

feat(grid): client-side sorting for primitive result columns - #171

Merged
Blankll merged 1 commit into
masterfrom
feat/grid-primitive-sort
Oct 8, 2026
Merged

Blankll merged 1 commit into
masterfrom
feat/grid-primitive-sort

Conversation

@Blankll

@Blankll Blankll commented Oct 8, 2026

Copy link
Copy Markdown
Member

Problem

Result grids could not reliably sort by a column:

  • DataGrid (query results) rendered sort UI, but a header click emitted sortChange and QueryResultPanel re-ran the user's SQL through execute_sorted_query, wrapping it as SELECT * FROM (…) ORDER BY …. That only works for plain parsable SELECTs, re-runs potentially expensive queries on every click, and fails outright on anything sqlparser cannot parse.
  • DataTableView (table data browser) had no sort support at all.

Solution

Sorting now happens in the frontend, over the rows already returned, and is limited to primitive columns (text / number / boolean / date / NULL). JSON documents, arrays and BLOBs stay unsortable — they have no meaningful client-side total ordering.

header click ─▶ useDataGridSort (SortState) ─▶ sortRowsByState(rows)
                                                  │ primitive-only comparator
                                                  ▼
                                            sortedRows (computed) ─▶ render

Sortability — a column qualifies only if its declared type is primitive (not JSON/ARRAY/BYTEA/BLOB/BINARY/…) and every returned cell is primitive. One object/array cell disqualifies the whole column. Unsortable headers render as plain labels with a tooltip, and the header context menu disables its sort entries and explains why.

Comparison — number / boolean / date / string, preferring the declared column type (so bigint returned as a string still compares numerically). Dates compare by parsed timestamp. Strings use localeCompare(…, { numeric: true, sensitivity: 'base' }). NULLs sort last ascending; descending is a pure reversal, matching PostgreSQL.

Multi-column sort — plain click cycles ASC → DESC → cleared; shift-click appends (ASC → DESC → removed). Equal rows keep their original order and the input array is never mutated.

Changes

Area Change
src/utils/gridSort.ts new — sortability detection, comparator, sortRowsByState, formatSortState
useDataGridSort adds setSort / pruneSort; removes now-unused buildOrderByClause and hasActiveSort
DataGrid.vue sortedRows is the single source of truth for rendering, row actions, copy/export and the status bar; selection is cleared when rows/columns/sort change (selection is positional)
QueryResultPanel.vue sorting no longer re-executes; only filters still round-trip, since they change the result set itself
DataTableView.vue clickable sortable headers with indicators; export and batch delete read sortedRows; sort resets on table/connection change
ColumnHeaderContextMenu.vue sortable prop disables sort entries for non-primitive columns
i18n components.dataGrid.sort.unsortable in enUS / zhCN
docs/GRID_SORT_DESIGN.md design write-up

Known limitations

  • Table browser sort is page-scoped. get_table_data paginates server-side (100/500/1000 rows per page), so only the fetched page is reordered. Cross-page sorting would need ORDER BY pushed into that command — intentionally out of scope here.
  • The Rust execute_sorted_query command keeps its sort field (the frontend now always sends an empty list), so no backend change is required and the API stays compatible.

Testing

  • src/__tests__/gridSort.test.ts — sortability detection, per-kind comparison, NULL placement, multi-column priority, stability, immutability.
  • src/__tests__/useDataGridSort.test.ts — click/shift cycling, setSort, pruneSort.
  • Full suite: 34 suites / 609 tests pass; vue-tsc --noEmit and eslint ./ clean.

Result grids delegated sorting to the backend, which re-ran the user's SQL
wrapped in `SELECT * FROM (…) ORDER BY …`. That only worked for plain
parsable SELECTs, re-ran expensive queries on every header click, and the
table browser had no sort support at all.

Sorting now happens in the frontend over the rows already returned, and is
limited to primitive columns (text/number/boolean/date/NULL) because JSON,
arrays and BLOBs have no meaningful client-side total ordering.

- add src/utils/gridSort.ts: sortability detection, type-aware comparator,
  stable multi-column sort, sort summary
- extend useDataGridSort with setSort/pruneSort; drop the now-unused
  buildOrderByClause and hasActiveSort
- DataGrid: sortedRows drives rendering, row actions, copy/export and the
  status bar; unsortable headers and menu entries explain why
- QueryResultPanel: filters still round-trip, sorting no longer re-executes
- DataTableView: header sorting with indicators, scoped to the fetched page
- cover the comparator and sort state with unit tests

Backend `execute_sorted_query` keeps its sort field; the frontend now always
sends an empty list, so no Rust change is required.
@Blankll
Blankll merged commit 6b6fb95 into master Oct 8, 2026
3 checks passed
@Blankll
Blankll deleted the feat/grid-primitive-sort branch October 8, 2026 14:44
Blankll added a commit that referenced this pull request Oct 8, 2026
Integrate the kind-precedence design into the #171 implementation.

Per-cell kind resolution (resolveSortKind on every comparison) breaks
two ways on columns holding mixed primitives — routine on SQLite:

- the comparator flips kind between passes, so order is unstable
- resolveSortKind inspected only the LEFT value, making
  compare(a,b) !== -compare(b,a) — an asymmetric comparator

Kind is now resolved once per column: the declared type wins outright;
without one, the highest-precedence kind present is used (number →
boolean → date → string; strings are never date-sniffed). NULL ordering,
non-primitive column exclusion and the coercers from #171 are unchanged.
Blankll added a commit that referenced this pull request Oct 8, 2026
Integrate the kind-precedence design into the #171 implementation.

Per-cell kind resolution (resolveSortKind on every comparison) breaks
two ways on columns holding mixed primitives — routine on SQLite:

- the comparator flips kind between passes, so order is unstable
- resolveSortKind inspected only the LEFT value, making
  compare(a,b) !== -compare(b,a) — an asymmetric comparator

Kind is now resolved once per column: the declared type wins outright;
without one, the highest-precedence kind present is used (number →
boolean → date → string; strings are never date-sniffed). NULL ordering,
non-primitive column exclusion and the coercers from #171 are unchanged.
Blankll added a commit that referenced this pull request Oct 8, 2026
Integrate the kind-precedence design into the #171 implementation.

Per-cell kind resolution (resolveSortKind on every comparison) breaks
two ways on columns holding mixed primitives — routine on SQLite:

- the comparator flips kind between passes, so order is unstable
- resolveSortKind inspected only the LEFT value, making
  compare(a,b) !== -compare(b,a) — an asymmetric comparator

Kind is now resolved once per column: the declared type wins outright;
without one, the highest-precedence kind present is used (number →
boolean → date → string; strings are never date-sniffed). NULL ordering,
non-primitive column exclusion and the coercers from #171 are unchanged.
Blankll added a commit that referenced this pull request Oct 8, 2026
Integrate the kind-precedence design into the #171 implementation.

Per-cell kind resolution (resolveSortKind on every comparison) breaks
two ways on columns holding mixed primitives — routine on SQLite:

- the comparator flips kind between passes, so order is unstable
- resolveSortKind inspected only the LEFT value, making
  compare(a,b) !== -compare(b,a) — an asymmetric comparator

Kind is now resolved once per column: the declared type wins outright;
without one, the highest-precedence kind present is used (number →
boolean → date → string; strings are never date-sniffed). NULL ordering,
non-primitive column exclusion and the coercers from #171 are unchanged.
Blankll added a commit that referenced this pull request Oct 8, 2026
Integrate the kind-precedence design into the #171 implementation.

Per-cell kind resolution (resolveSortKind on every comparison) breaks
two ways on columns holding mixed primitives — routine on SQLite:

- the comparator flips kind between passes, so order is unstable
- resolveSortKind inspected only the LEFT value, making
  compare(a,b) !== -compare(b,a) — an asymmetric comparator

Kind is now resolved once per column: the declared type wins outright;
without one, the highest-precedence kind present is used (number →
boolean → date → string; strings are never date-sniffed). NULL ordering,
non-primitive column exclusion and the coercers from #171 are unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant