n8n/packages/@n8n/db
Garrit Franke d24fa54996
perf(editor): harden node search data layer with index, EXISTS sharing and keyset scan
Ports the measured DB-layer hardening from #30294 onto the node content
search, and closes two correctness gaps:

1. Sharing as EXISTS, not `workflow.id IN (subquery)`. The IN form makes
   SQLite drive from shared_workflow and sort every LIKE match in a temp
   B-tree, discarding the updatedAt index (measured on a 20k corpus in
   #30294; plan-asserted there in node-search-variants.perf.ts).

2. Index on workflow_entity.updatedAt (migration
   AddUpdatedAtIndexToWorkflowEntity1786525332822 + entity mirror). The
   query orders by updatedAt and stops at the limit; without the index
   every keystroke sorts the whole table (149ms -> 0.9ms at 20k). The
   migration was already registered in the generated migration index on
   this branch but the file itself was never committed.

3. Keyset scan instead of a fixed 500-candidate cap. The SQL pre-filter
   matches the raw JSON blob - node ids, webhook ids, positions - which
   the in-memory matcher deliberately skips, so a fixed cap let blob-only
   false positives displace real hits and silently return nothing. The
   service now batches 100 candidates on an (updatedAt, id) cursor (id
   tiebreak: updatedAt is not unique) until the result list fills,
   candidates run out, or a 2000-workflow safety rail stops pathological
   queries. The cursor is a row-value comparison, NOT the equivalent
   'a < x OR (a = x AND b < y)': the OR form makes SQLite pick a
   MULTI-INDEX OR plan that re-materialises per batch (measured 3.5s vs
   96ms per search at 20k in #30294).

   Credential names are matched too (new 'credentials' field, ranked
   last): they appear in the serialized workflow, so the pre-filter
   matches them, and without an in-memory counterpart a credential-name
   query only ever produced false positives - the most realistic trigger
   for the empty-results bug.

Also ports the Postgres planner hint (SET LOCAL enable_seqscan = off)
from #30294: Postgres cannot estimate '%...%' selectivity and otherwise
seq-scans + top-N sorts instead of walking the index (75ms -> 1ms).

Deliberately not ported from #30294 (yet): the pLimit(1) concurrency
gate + per-user rate limiter, and the performance benchmark suite.

Verified: matcher 40/40, service unit 19/19, integration 22/22 on both
SQLite and Postgres 16; typecheck and lint clean across db, cli,
workflow, api-types and editor-ui; editor-ui composable tests 24/24.
2026-08-12 16:24:01 +02:00
..
scripts refactor(core): Generate DB migration index files instead of hand-maintaining them (no-changelog) (#35581) 2026-08-06 11:53:27 +00:00
src perf(editor): harden node search data layer with index, EXISTS sharing and keyset scan 2026-08-12 16:24:01 +02:00
AGENTS.md chore(core): Lock the @n8n/typeorm import ratchet and document the boundary (no-changelog) (#34370) 2026-07-21 08:26:21 +00:00
eslint.config.mjs fix(core): Prevent sub-workflow failures after transferring a deleted user's workflows (#33772) 2026-07-14 07:33:01 +00:00
package.json refactor(core): Generate DB migration index files instead of hand-maintaining them (no-changelog) (#35581) 2026-08-06 11:53:27 +00:00
tsconfig.build.json chore: Migrate the rest of packages/@n8n to typescript 7 (#34332) 2026-07-17 10:45:58 +03:00
tsconfig.json chore: Migrate the rest of packages/@n8n to typescript 7 (#34332) 2026-07-17 10:45:58 +03:00
vite.config.ts chore: Consolidate ts tooling catalog (#34310) 2026-07-16 10:10:12 +00:00