From d0bff87318f0b80632f0fc74442790002d8993d5 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Mon, 10 Aug 2026 11:36:13 +0530 Subject: [PATCH 1/3] fix(databases): pin the grid sort tie breaker to the sort direction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The API appends `$sequence` to any sort that has no unique attribute, and always ascending. A descending grid sort therefore reached the engine as `column DESC, _id ASC` — a direction mix neither scan direction of the index can serve, so it filesorted the whole table. On large tables that turned a 25-row page into seconds of work, up to a query timeout. Send `$sequence` ourselves, in the same direction as the sort, so the index stays usable. Ascending sorts already lined up with the appended tie breaker and are left alone, as are sorts already on `$id` or `$sequence`. --- .../spreadsheet.svelte | 21 +++++++++++-------- .../databases/database-[database]/store.ts | 18 ++++++++++++++-- .../table-[table]/spreadsheet.svelte | 20 +++++++++++------- 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte index dd695f859f..18a3af93d9 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte @@ -37,7 +37,7 @@ import { formatNumberWithCommas } from '$lib/helpers/numbers'; import { chunks } from '$lib/helpers/array'; import { mapToQueryParams } from '$lib/components/filters/store'; - import { expandTabs, buildWildcardEntitiesQuery } from '$database/store'; + import { expandTabs, buildWildcardEntitiesQuery, orderTieBreaker } from '$database/store'; import { setupUnsavedChangesGuard } from '$lib/helpers/unsavedChanges'; import { mockSuggestions } from '$database/(suggestions)'; import { @@ -491,12 +491,15 @@ } } - function getCorrectOrderQuery() { - return $sortState?.column && $sortState?.direction !== 'default' - ? $sortState.direction === 'asc' - ? Query.orderAsc($sortState.column) - : Query.orderDesc($sortState.column) - : Query.orderDesc(''); + function getCorrectOrderQueries() { + const order = + $sortState?.column && $sortState?.direction !== 'default' + ? $sortState.direction === 'asc' + ? Query.orderAsc($sortState.column) + : Query.orderDesc($sortState.column) + : Query.orderDesc(''); + + return [order, ...orderTieBreaker(order)]; } async function loadPage(pageNumber: number): Promise { @@ -512,7 +515,7 @@ databaseId, collectionId, queries: [ - getCorrectOrderQuery(), + ...getCorrectOrderQueries(), Query.limit(SPREADSHEET_PAGE_LIMIT), Query.offset(pageToOffset(pageNumber, SPREADSHEET_PAGE_LIMIT)), ...filterQueries /* filter queries */, @@ -538,7 +541,7 @@ databaseId, collectionId, queries: [ - getCorrectOrderQuery(), + ...getCorrectOrderQueries(), Query.limit(SPREADSHEET_PAGE_LIMIT), Query.offset(pageToOffset(targetPageNum, SPREADSHEET_PAGE_LIMIT)), ...buildWildcardEntitiesQuery(collection) diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/store.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/store.ts index c8c6ba7e33..b06e15dc01 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/store.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/store.ts @@ -175,6 +175,19 @@ export function extractSortFromQueries(parsedQueries: Map) { return { column: null, direction: 'default' }; } +/** + * The API appends `$sequence` to any sort without a unique attribute, but always + * ascending. A lone descending sort therefore becomes `column DESC, _id ASC`, a + * direction mix neither scan direction of the index can serve, so the engine + * filesorts the whole table. Pin the tie breaker to the sort's own direction. + */ +export function orderTieBreaker(order: string): string[] { + const { method, attribute } = JSON.parse(order); + const needsTieBreaker = attribute && attribute !== '$id' && attribute !== '$sequence'; + + return needsTieBreaker && method === 'orderDesc' ? [Query.orderDesc('$sequence')] : []; +} + export function buildGridQueries( limit: number, offset: number, @@ -182,19 +195,20 @@ export function buildGridQueries( table: Entity, includeRelationships: boolean = true ) { - const hasOrderQuery = Array.from(parsedQueries.values()).some( + const orderQuery = Array.from(parsedQueries.values()).find( (q) => q.includes('orderAsc') || q.includes('orderDesc') ); const queryArray = [Query.limit(limit), Query.offset(offset)]; // don't override if there's a user created sort! - if (!hasOrderQuery) { + if (!orderQuery) { queryArray.push(Query.orderDesc('')); } queryArray.push( ...parsedQueries.values(), + ...(orderQuery ? orderTieBreaker(orderQuery) : []), ...(includeRelationships ? buildWildcardEntitiesQuery(table) : [Query.select(['*'])]) ); diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte index c6523114bb..fd2db2a114 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte @@ -98,6 +98,7 @@ expandTabs, type Columns, buildWildcardEntitiesQuery, + orderTieBreaker, loadGridRows, type SortState, randomDataModalState, @@ -709,12 +710,15 @@ } } - function getCorrectOrderQuery() { - return $sortState?.column && $sortState?.direction !== 'default' - ? $sortState.direction === 'asc' - ? Query.orderAsc($sortState.column) - : Query.orderDesc($sortState.column) - : Query.orderDesc(''); + function getCorrectOrderQueries() { + const order = + $sortState?.column && $sortState?.direction !== 'default' + ? $sortState.direction === 'asc' + ? Query.orderAsc($sortState.column) + : Query.orderDesc($sortState.column) + : Query.orderDesc(''); + + return [order, ...orderTieBreaker(order)]; } async function loadPage(pageNumber: number): Promise { @@ -730,7 +734,7 @@ const loadedRows = await loadGridRows( table, (includeRelationships) => [ - getCorrectOrderQuery(), + ...getCorrectOrderQueries(), Query.limit(SPREADSHEET_PAGE_LIMIT), Query.offset(pageToOffset(pageNumber, SPREADSHEET_PAGE_LIMIT)), ...filterQueries /* filter queries */, @@ -761,7 +765,7 @@ const loadedRows = await loadGridRows( table, (includeRelationships) => [ - getCorrectOrderQuery(), + ...getCorrectOrderQueries(), Query.limit(SPREADSHEET_PAGE_LIMIT), Query.offset(pageToOffset(targetPageNum, SPREADSHEET_PAGE_LIMIT)), ...(includeRelationships From ca562447bc84963bd6bf345f566177071544e2e0 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Mon, 10 Aug 2026 11:53:57 +0530 Subject: [PATCH 2/3] fix(databases): match order queries on their method, not a substring `orderAsc`/`orderDesc` can appear inside a filter's value, so searching the serialized query for that text can pick a filter instead of the sort. When a filter like that was added before a descending sort, the tie breaker was skipped and the sort fell back to the mixed-direction filesort. Parse the query and compare its method. Applies to the same substring check in `extractSortFromQueries` and in the grid's `sort()`, where a matching filter would have been silently dropped when the user changed the sort. --- .../spreadsheet.svelte | 9 +++++++-- .../databases/database-[database]/store.ts | 19 +++++++++++++++---- .../table-[table]/spreadsheet.svelte | 3 ++- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte index 18a3af93d9..3db21dc92e 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte @@ -37,7 +37,12 @@ import { formatNumberWithCommas } from '$lib/helpers/numbers'; import { chunks } from '$lib/helpers/array'; import { mapToQueryParams } from '$lib/components/filters/store'; - import { expandTabs, buildWildcardEntitiesQuery, orderTieBreaker } from '$database/store'; + import { + expandTabs, + buildWildcardEntitiesQuery, + orderMethod, + orderTieBreaker + } from '$database/store'; import { setupUnsavedChangesGuard } from '$lib/helpers/unsavedChanges'; import { mockSuggestions } from '$database/(suggestions)'; import { @@ -265,7 +270,7 @@ if (parsedQueries.size > 0) { for (const [tagValue, queryString] of parsedQueries.entries()) { - if (queryString.includes('orderAsc') || queryString.includes('orderDesc')) { + if (orderMethod(queryString)) { parsedQueries.delete(tagValue); } } diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/store.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/store.ts index b06e15dc01..92e757ff60 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/store.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/store.ts @@ -161,13 +161,24 @@ export function buildWildcardEntitiesQuery(entity: Entity | null = null): string ]; } +/** + * A filter can carry `orderAsc` or `orderDesc` inside its value, so match on the + * query's method rather than searching the serialized query for the substring. + */ +export function orderMethod(query: string): string | null { + const { method } = JSON.parse(query); + + return method === 'orderAsc' || method === 'orderDesc' ? method : null; +} + export function extractSortFromQueries(parsedQueries: Map) { for (const [tagValue, queryString] of parsedQueries.entries()) { - if (queryString.includes('orderAsc') || queryString.includes('orderDesc')) { - const isAsc = queryString.includes('orderAsc'); + const method = orderMethod(queryString); + + if (method) { return { column: tagValue.value, - direction: isAsc ? 'asc' : 'desc' + direction: method === 'orderAsc' ? 'asc' : 'desc' }; } } @@ -196,7 +207,7 @@ export function buildGridQueries( includeRelationships: boolean = true ) { const orderQuery = Array.from(parsedQueries.values()).find( - (q) => q.includes('orderAsc') || q.includes('orderDesc') + (query) => orderMethod(query) !== null ); const queryArray = [Query.limit(limit), Query.offset(offset)]; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte index fd2db2a114..7a226d1e96 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte @@ -98,6 +98,7 @@ expandTabs, type Columns, buildWildcardEntitiesQuery, + orderMethod, orderTieBreaker, loadGridRows, type SortState, @@ -395,7 +396,7 @@ if (parsedQueries.size > 0) { for (const [tagValue, queryString] of parsedQueries.entries()) { - if (queryString.includes('orderAsc') || queryString.includes('orderDesc')) { + if (orderMethod(queryString)) { parsedQueries.delete(tagValue); } } From 8043a14091918855aaeb8fe5a631ce66169842e0 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Mon, 10 Aug 2026 12:15:13 +0530 Subject: [PATCH 3/3] chore(deps): raise the nanoid floor to clear the high-severity audit GHSA-28wg-ghj8-5hjv and GHSA-2v37-7h3g-55p8 patch the 3.x line at 3.3.17 and the 5.x line at 5.1.16, so no single override covers both: forcing 5.x onto postcss would hand a CJS consumer an ESM-only package. Every consumer's declared range already permits a patched release, so only the pinned floors and the resolved versions move: nanoid (direct) ^5.1.11 -> ^5.1.16 postcss (override) ^8.5.18 -> ^8.5.26, which floors nanoid at ^3.3.17 @ai-sdk/provider-utils 3.3.11 -> 3.3.18 (declares ^3.3.8) @melt-ui/svelte 5.1.7 -> 5.1.16 (declares ^5.0.4) postcss 3.3.16 -> 3.3.18 (declares ^3.3.17) Integrity hashes come from the registry and no other package is touched. `bun install --frozen-lockfile` leaves the lockfile byte-identical. --- bun.lock | 14 +++++++------- package.json | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/bun.lock b/bun.lock index 263bb6ac76..179e3b1ee0 100644 --- a/bun.lock +++ b/bun.lock @@ -37,7 +37,7 @@ "flatted": "^3.4.2", "ignore": "^6.0.2", "json5": "^2.2.3", - "nanoid": "^5.1.11", + "nanoid": "^5.1.16", "nanotar": "^0.3.0", "pretty-bytes": "^6.1.1", "remarkable": "^2.0.1", @@ -98,7 +98,7 @@ "js-yaml": "^4.3.1", "minimatch": "10.2.3", "picomatch": "^4.0.4", - "postcss": "^8.5.18", + "postcss": "^8.5.26", "vite": "npm:rolldown-vite@latest", "ws": "^8.21.0", "yaml": "^1.10.3", @@ -1128,7 +1128,7 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nanoid": ["nanoid@5.1.11", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg=="], + "nanoid": ["nanoid@5.1.16", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="], "nanotar": ["nanotar@0.3.0", "", {}, "sha512-Kv2JYYiCzt16Kt5QwAc9BFG89xfPNBx+oQL4GQXD9nLqPkZBiNaqaCWtwnbk/q7UVsTYevvM1b0UF8zmEI4pCg=="], @@ -1194,7 +1194,7 @@ "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - "postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="], + "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], "postcss-load-config": ["postcss-load-config@3.1.4", "", { "dependencies": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg=="], @@ -1500,7 +1500,7 @@ "@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@1.0.11", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-CPyImHGiT3svyfmvPvAFTianZzWFtm0qK82XjwlQIA1C3IQ2iku/PMQXi7aFyrX0TyMh3VTkJPB03tjU2VXVrw=="], - "@ai-sdk/provider-utils/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "@ai-sdk/provider-utils/nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], "@ai-sdk/ui-utils/@ai-sdk/provider": ["@ai-sdk/provider@1.0.11", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-CPyImHGiT3svyfmvPvAFTianZzWFtm0qK82XjwlQIA1C3IQ2iku/PMQXi7aFyrX0TyMh3VTkJPB03tjU2VXVrw=="], @@ -1524,7 +1524,7 @@ "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - "@melt-ui/svelte/nanoid": ["nanoid@5.1.7", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ=="], + "@melt-ui/svelte/nanoid": ["nanoid@5.1.16", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="], "@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], @@ -1574,7 +1574,7 @@ "popmotion/tslib": ["tslib@2.4.0", "", {}, "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="], - "postcss/nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], + "postcss/nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], "style-value-types/tslib": ["tslib@2.4.0", "", {}, "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="], diff --git a/package.json b/package.json index d8a7e51e64..d6d809420b 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "flatted": "^3.4.2", "ignore": "^6.0.2", "json5": "^2.2.3", - "nanoid": "^5.1.11", + "nanoid": "^5.1.16", "nanotar": "^0.3.0", "pretty-bytes": "^6.1.1", "remarkable": "^2.0.1", @@ -113,6 +113,6 @@ "picomatch": "^4.0.4", "cookie": "^0.7.0", "ws": "^8.21.0", - "postcss": "^8.5.18" + "postcss": "^8.5.26" } }