Remove extra blank lines left behind when the stitcher field was
removed from engine test constructors. Extract getStitchTicket helper
to assert the queue entry exists instead of using optional chaining
that could silently pass undefined.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
Now that immediate mode is removed, the stitch ticket is always
provided by the deferred worker. Remove the optional marker and all
the conditional guards that checked for its presence.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
A second call with mode='immediate' would incorrectly log "Unknown
strategy mode" because the deprecation flag was already set.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
INSERT return values differ across database engines (row IDs, row
counts, or empty arrays), making them unsuitable for detecting blocked
upserts. Use a post-write hash verification instead, which works
reliably on all engines.
Adds test coverage for stale ticket rejection across all supported
database engines (MySQL 8, PostgreSQL 14/18, SQLite 3).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
On PostgreSQL and SQLite, the stitch ticket check is now part of the
upsert WHERE clause, eliminating the TOCTOU window between the check
and the write. MySQL does not support ON CONFLICT ... DO UPDATE ... WHERE,
so it retains the separate SELECT with a negligible race window.
SQLite misreports affected rows for blocked upserts, so a post-write
hash verification is used there instead of the row count.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
The initial SELECT entity_id FROM refresh_state check is redundant —
the processedResult query in the Promise.all already handles the
missing-entity case. This saves one database round trip per stitch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
Now that immediate mode is removed, the two-step pattern of inserting a
placeholder row and then updating it is unnecessary. Replace with a
single upsert that creates or updates the final_entities row in one
operation, removing a round-trip and simplifying the flow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
The conflict resolution replaced unicode escapes with literal control
character bytes, causing GitHub to render the file as binary.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
Remove the immediate stitching strategy from the catalog-backend plugin.
All stitching now uses the deferred mode exclusively, which processes
entities asynchronously via a worker queue. This simplifies the stitching
subsystem by removing dead-weight code paths and the strategy parameter
threading, and fixes a bug where deleteWithEagerPruningOfChildren
hardcoded immediate-mode semantics regardless of configured mode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
Move the adjustedFilters useMemo above refresh and reuse it in both
the fetch callback and the frontend filtering memo.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
The lastFetchParamsRef starts as undefined, so the initial debounce
would have fetched with an empty filter (returning every entity in
the catalog). Guard against this by skipping the fetch when no filter
components have registered yet.
Also skip the URL sync effect until at least one filter has been set,
to avoid briefly clearing filter query params from the URL bar before
filter components initialize.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
The label is read by CatalogTable for the table title (e.g. "All
Components (42)"), so suppressing the label update would cause a
visible regression. The provider's ref-based fetch dedup already
prevents the redundant API call when only the label changes, so
the second updateFilters call is harmless.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
Replace useAsyncFn + outputState comparison with ref-based fetch dedup
and generation counter. Split entity filtering into a synchronous
useMemo so frontend-only filter changes are instant.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
The list CTE's INNER JOIN on search already excludes entities whose
sort-field value is NULL (truncated long values stored as NULL by
buildEntitySearch). Add the same whereNotNull to the count query's
EXISTS so totalItems matches the reachable set. Matters for fields
like metadata.description where ~128K entities have NULL values.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
The list CTE and count query both need the same filter, query, and
full-text-search predicates. Extract them into a shared
applyPredicates closure so they can't drift apart. Move the count
query construction next to the CTE for readability.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
Remove pr-33721, pr-34001, pr-34004 — all shipped in v1.51.0.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
The filtered CTE was referenced twice (count + data), preventing
Postgres 12+ from inlining it. This forced full materialization of
the filtered set before LIMIT could short-circuit. Split into two
separate queries run via Promise.all so the list CTE is only
referenced once and the planner can short-circuit on LIMIT.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
Only wrap the SELECT+UPDATE in a transaction for MySQL/PostgreSQL
where FOR UPDATE SKIP LOCKED is actually used. For sqlite3 the
transaction adds unnecessary BEGIN/COMMIT overhead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
Same FOR UPDATE SKIP LOCKED race: the processing loop's claim query
had the SELECT and UPDATE as separate auto-committed statements.
The existing caller already wraps in a transaction, but this makes
the function self-protecting if called without one.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
The env vars used lowercase postgres18/postgres14 instead of uppercase
POSTGRES18/POSTGRES14, which didn't match the names expected by
TestDatabases. This caused all postgres-only test suites to see no
available databases and crash.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
When eachSupportedId() returned an empty array, describe.each([]) would
throw and crash the entire test suite. Return a placeholder ID instead,
so individual tests fail with a clear error rather than preventing the
entire suite from running.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>