From 81a7d2195e9e973de9e1dae0df4265ece03a909a Mon Sep 17 00:00:00 2001 From: rocky Date: Thu, 12 Nov 2026 17:16:43 +0800 Subject: [PATCH 1/2] fix(search): accept unknown future SearchIndexStatus values (MZ-17) Align the public SearchIndexStatus type with the runtime passthrough of unknown server statuses. Export KnownSearchIndexStatus for documented literals, keep wait defaults and READY/queryable semantics unchanged, and cover the contract with tsd, unit, and bilingual docs. --- changelogs/unreleased.md | 1 + docs/en/search-indexes.md | 8 +++-- docs/zh/search-indexes.md | 8 +++-- src/adapters/mongodb/search-indexes/index.ts | 2 +- .../mongodb/search-indexes/normalize.ts | 6 ++-- src/adapters/mongodb/search-indexes/types.ts | 1 + test/types/search-index-usage.test-d.ts | 9 +++++ .../search-indexes/search-indexes.test.ts | 34 +++++++++++++++++++ types/search-index.d.ts | 14 ++++++-- 9 files changed, 73 insertions(+), 10 deletions(-) diff --git a/changelogs/unreleased.md b/changelogs/unreleased.md index 837c6da..ac41542 100644 --- a/changelogs/unreleased.md +++ b/changelogs/unreleased.md @@ -5,6 +5,7 @@ Candidate work (not yet released): GridFS per-file ID ownership, durable append/finalize/abort state, deletion fencing, expiresAt idle-TTL migration, and recovery SHA provenance (trusted pre-verified upload() digest versus uploadStream metadata-only claims). These changes require a coordinated writer drain before upgrading and are pending full compatibility verification. No release version or publication is claimed. The GridFS and Search index fixes from the 2026-11-02 weekly scan are assigned to the [3.5.3 versioned notes](./v3.5.3.md); the vector search guidance and Search docs regrouping are in [3.5.2](./v3.5.2.md). Publication is established by the npm registry and immutable tag, not this file. - MZ-19 candidate fix: persist direct upload SHA provenance; recovery verifies physical chunks for every visible file, strictly checks only trusted upload()/resumable expected digests, and records metadata-only SHA mismatches as audit warnings. +- MZ-17: `SearchIndexStatus` is now `KnownSearchIndexStatus | (string & {})`, matching the runtime that passes unknown future server statuses through unchanged instead of rewriting them to `PENDING`. Public types export `KnownSearchIndexStatus`. Waiting defaults and READY/queryable semantics are unchanged. ## Archived preparation notes for the 3.3.2 candidate diff --git a/docs/en/search-indexes.md b/docs/en/search-indexes.md index 13a04e1..998e0ac 100644 --- a/docs/en/search-indexes.md +++ b/docs/en/search-indexes.md @@ -35,9 +35,13 @@ interface SearchIndexSpec { definition: Record; } +type KnownSearchIndexStatus = + 'PENDING' | 'BUILDING' | 'READY' | 'FAILED' | 'STALE' | 'DELETING' | 'DOES_NOT_EXIST'; +type SearchIndexStatus = KnownSearchIndexStatus | (string & {}); + interface SearchIndexInfo { name: string; - status: 'PENDING' | 'BUILDING' | 'READY' | 'FAILED' | 'STALE' | 'DELETING' | 'DOES_NOT_EXIST'; + status: SearchIndexStatus; queryable: boolean; type?: 'search' | 'vectorSearch'; typeSource: 'server' | 'inferred' | 'unknown'; @@ -106,7 +110,7 @@ When an in-place update is not available, change the definition without downtime If a short outage is acceptable, drop the index, wait for `dropped`, and create it again. Searches fail until it is queryable again. The gap grows with data size. On atlas-local, 50,000 documents with 64 dimensions took about 1–2 seconds. -`status` passes the server value through unchanged. Use `queryable` to decide whether searches can run and `status` to show progress. +`status` passes the server value through unchanged, including unknown future statuses such as `PAUSED_BY_SERVER`. Those values are not rewritten to `PENDING`. TypeScript still autocompletes the documented `KnownSearchIndexStatus` literals, and `SearchIndexStatus` also accepts other non-empty server strings. Use `queryable` to decide whether searches can run and `status` to show progress. `waitForSearchIndex` still treats only `READY` and `FAILED` as terminal for the `ready` / `queryable` waits; an unknown status keeps polling until timeout, `queryable`, or a documented terminal status. ### dropSearchIndex diff --git a/docs/zh/search-indexes.md b/docs/zh/search-indexes.md index 04ce214..3fd0faa 100644 --- a/docs/zh/search-indexes.md +++ b/docs/zh/search-indexes.md @@ -35,9 +35,13 @@ interface SearchIndexSpec { definition: Record; } +type KnownSearchIndexStatus = + 'PENDING' | 'BUILDING' | 'READY' | 'FAILED' | 'STALE' | 'DELETING' | 'DOES_NOT_EXIST'; +type SearchIndexStatus = KnownSearchIndexStatus | (string & {}); + interface SearchIndexInfo { name: string; - status: 'PENDING' | 'BUILDING' | 'READY' | 'FAILED' | 'STALE' | 'DELETING' | 'DOES_NOT_EXIST'; + status: SearchIndexStatus; queryable: boolean; type?: 'search' | 'vectorSearch'; typeSource: 'server' | 'inferred' | 'unknown'; @@ -106,7 +110,7 @@ Community 8.2.12 配合 `mongodb/mongodb-community-search` 会接受更新。20, 如果能接受短暂中断,可以先删除索引、等待 `dropped`、再重新创建。在它重新可查询之前,检索会失败。中断时长随数据量增长。atlas-local 上 50,000 篇文档、64 维约 1–2 秒。 -`status` 原样透传服务端的值。判断能否检索看 `queryable`,展示进度看 `status`。 +`status` 原样透传服务端的值,包括尚不在文档中的未来状态(例如 `PAUSED_BY_SERVER`),不会把未知状态改写成 `PENDING`。TypeScript 仍会补全已记录的 `KnownSearchIndexStatus` 字面量,同时 `SearchIndexStatus` 也接受其他非空服务端字符串。判断能否检索看 `queryable`,展示进度看 `status`。`waitForSearchIndex` 在 `ready` / `queryable` 等待中仍只把 `READY` 和 `FAILED` 视为终态;未知状态会继续轮询,直到超时、变为可查询,或出现已记录的终态。 ### dropSearchIndex diff --git a/src/adapters/mongodb/search-indexes/index.ts b/src/adapters/mongodb/search-indexes/index.ts index 19d9b83..f7c2ac9 100644 --- a/src/adapters/mongodb/search-indexes/index.ts +++ b/src/adapters/mongodb/search-indexes/index.ts @@ -9,5 +9,5 @@ export { waitForSearchIndexOp, } from './operations'; export { classifySearchIndexError, mapSearchIndexError } from './errors'; -export { normalizeSearchIndexInfo, missingSearchIndexInfo } from './normalize'; +export { normalizeSearchIndexInfo, missingSearchIndexInfo, isKnownSearchIndexStatus } from './normalize'; export type { SearchIndexCollectionLike, SearchIndexContext } from './types'; diff --git a/src/adapters/mongodb/search-indexes/normalize.ts b/src/adapters/mongodb/search-indexes/normalize.ts index 61e3808..2efcaaf 100644 --- a/src/adapters/mongodb/search-indexes/normalize.ts +++ b/src/adapters/mongodb/search-indexes/normalize.ts @@ -2,7 +2,7 @@ * Normalizes driver `$listSearchIndexes` documents into SearchIndexInfo. */ import type { Document } from 'mongodb'; -import type { SearchIndexInfo, SearchIndexStatus, SearchIndexType, SearchIndexTypeSource } from './types'; +import type { KnownSearchIndexStatus, SearchIndexInfo, SearchIndexType, SearchIndexTypeSource } from './types'; const STATUSES: ReadonlySet = new Set(['PENDING', 'BUILDING', 'READY', 'FAILED', 'STALE', 'DELETING', 'DOES_NOT_EXIST']); @@ -14,7 +14,7 @@ export function normalizeSearchIndexInfo(doc: Document): SearchIndexInfo { const info: SearchIndexInfo = { name: String(doc.name), // Unknown future statuses are passed through verbatim rather than rewritten. - status: (typeof doc.status === 'string' && doc.status.length > 0 ? doc.status : 'PENDING') as SearchIndexStatus, + status: typeof doc.status === 'string' && doc.status.length > 0 ? doc.status : 'PENDING', queryable: doc.queryable === true, typeSource: 'unknown', raw: doc as Record, @@ -32,7 +32,7 @@ export function normalizeSearchIndexInfo(doc: Document): SearchIndexInfo { return info; } -export function isKnownSearchIndexStatus(status: string): boolean { +export function isKnownSearchIndexStatus(status: string): status is KnownSearchIndexStatus { return STATUSES.has(status); } diff --git a/src/adapters/mongodb/search-indexes/types.ts b/src/adapters/mongodb/search-indexes/types.ts index 602ea74..3f0f58d 100644 --- a/src/adapters/mongodb/search-indexes/types.ts +++ b/src/adapters/mongodb/search-indexes/types.ts @@ -12,6 +12,7 @@ export type { SearchIndexInfo, SearchIndexManagement, SearchIndexSpec, + KnownSearchIndexStatus, SearchIndexStatus, SearchIndexType, SearchIndexTypeSource, diff --git a/test/types/search-index-usage.test-d.ts b/test/types/search-index-usage.test-d.ts index 9e46b91..b2529e2 100644 --- a/test/types/search-index-usage.test-d.ts +++ b/test/types/search-index-usage.test-d.ts @@ -2,8 +2,10 @@ import { expectAssignable, expectError, expectType } from 'tsd'; import MonSQLize, { ErrorCodes, type CreateSearchIndexResult, + type KnownSearchIndexStatus, type SearchIndexInfo, type SearchIndexSpec, + type SearchIndexStatus, type SearchIndexTypeSource, } from 'monsqlize'; @@ -29,6 +31,13 @@ expectAssignable<'created' | 'skipped' | 'updated'>(result.action); declare const info: SearchIndexInfo; expectType(info.typeSource); expectAssignable<'search' | 'vectorSearch' | undefined>(info.type); +expectAssignable(info.status); +expectAssignable('READY'); +expectAssignable('PAUSED_BY_SERVER'); +expectAssignable('READY'); +declare function acceptKnownStatus(status: KnownSearchIndexStatus): void; +acceptKnownStatus('READY'); +expectError(acceptKnownStatus('PAUSED_BY_SERVER')); expectError(docs.createSearchIndex({ name: 'x', definition: {} })); expectError(docs.createSearchIndex(spec, { ifExists: 'replace' })); diff --git a/test/unit/search-indexes/search-indexes.test.ts b/test/unit/search-indexes/search-indexes.test.ts index 873c40a..4f44489 100644 --- a/test/unit/search-indexes/search-indexes.test.ts +++ b/test/unit/search-indexes/search-indexes.test.ts @@ -6,6 +6,7 @@ import { createSearchIndexesOp, dropSearchIndexOp, getSearchIndexOp, + isKnownSearchIndexStatus, listSearchIndexesOp, normalizeSearchIndexInfo, updateSearchIndexOp, @@ -97,6 +98,27 @@ describe('search indexes: type resolution (A02 v1.1 §1.1)', () => { assert.equal(info.typeSource, 'unknown'); assert.equal(info.queryable, false); }); + it('passes unknown future statuses through unchanged', () => { + const info = normalizeSearchIndexInfo({ name: 'a', status: 'PAUSED_BY_SERVER', queryable: false }); + assert.equal(info.status, 'PAUSED_BY_SERVER'); + assert.equal(isKnownSearchIndexStatus(info.status), false); + }); + it('does not rewrite a known status other than PENDING', () => { + for (const status of ['BUILDING', 'READY', 'FAILED', 'STALE', 'DELETING', 'DOES_NOT_EXIST']) { + const info = normalizeSearchIndexInfo({ name: 'a', status }); + assert.equal(info.status, status); + assert.equal(isKnownSearchIndexStatus(info.status), true); + } + assert.equal(isKnownSearchIndexStatus('PENDING'), true); + assert.equal(isKnownSearchIndexStatus('paused_by_server'), false); + assert.equal(isKnownSearchIndexStatus(''), false); + }); + it('treats a missing or empty status as PENDING', () => { + assert.equal(normalizeSearchIndexInfo({ name: 'a' }).status, 'PENDING'); + assert.equal(normalizeSearchIndexInfo({ name: 'a', status: '' }).status, 'PENDING'); + assert.equal(normalizeSearchIndexInfo({ name: 'a', status: 1 }).status, 'PENDING'); + assert.equal(isKnownSearchIndexStatus(normalizeSearchIndexInfo({ name: 'a' }).status), true); + }); it('list filters by resolved type and excludes unknown', async () => { const { ctx } = fakeCollection({ lists: [[ { name: 's', type: 'search', status: 'READY' }, @@ -269,6 +291,18 @@ describe('search indexes: waitForSearchIndex', () => { const b = fakeCollection({ lists: [[{ name: 'v', status: 'FAILED', queryable: true }]] }); await rejectsCode(waitForSearchIndexOp(b.ctx, 'v', { until: 'ready', intervalMs: 100 }), 'SEARCH_INDEX_BUILD_FAILED'); }); + it('ready: an unknown future status is not terminal and times out', async () => { + const { ctx } = fakeCollection({ lists: [[{ name: 'v', status: 'PAUSED_BY_SERVER', queryable: false }]] }); + const err = await rejectsCode(waitForSearchIndexOp(ctx, 'v', { until: 'ready', intervalMs: 100, timeoutMs: 250 }), 'SEARCH_INDEX_WAIT_TIMEOUT'); + const details = (err as { details?: Array<{ last?: { status?: string } }> }).details; + assert.equal(details?.[0]?.last?.status, 'PAUSED_BY_SERVER'); + }); + it('queryable: an unknown future status still succeeds when queryable', async () => { + const { ctx } = fakeCollection({ lists: [[{ name: 'v', status: 'PAUSED_BY_SERVER', queryable: true }]] }); + const info = await waitForSearchIndexOp(ctx, 'v', { intervalMs: 100 }); + assert.equal(info.status, 'PAUSED_BY_SERVER'); + assert.equal(info.queryable, true); + }); it('dropped: resolves on DOES_NOT_EXIST, keeps waiting on FAILED', async () => { const { ctx } = fakeCollection({ lists: [[{ name: 'v', status: 'DELETING' }], [{ name: 'v', status: 'FAILED' }], []] }); assert.equal((await waitForSearchIndexOp(ctx, 'v', { until: 'dropped', intervalMs: 100 })).status, 'DOES_NOT_EXIST'); diff --git a/types/search-index.d.ts b/types/search-index.d.ts index f91b1dc..9ed6591 100644 --- a/types/search-index.d.ts +++ b/types/search-index.d.ts @@ -9,8 +9,12 @@ export type SearchIndexType = 'search' | 'vectorSearch'; /** Origin of `SearchIndexInfo.type`. */ export type SearchIndexTypeSource = 'server' | 'inferred' | 'unknown'; -/** Server-reported Search index status (passed through unchanged). */ -export type SearchIndexStatus = +/** + * Documented server-reported Search index statuses. + * Runtime values are not limited to this set: unknown future statuses are + * passed through unchanged and are never rewritten to `PENDING`. + */ +export type KnownSearchIndexStatus = | 'PENDING' | 'BUILDING' | 'READY' @@ -19,6 +23,12 @@ export type SearchIndexStatus = | 'DELETING' | 'DOES_NOT_EXIST'; +/** + * Server-reported Search index status, passed through unchanged. + * Known literals stay completable; unknown future server strings remain assignable. + */ +export type SearchIndexStatus = KnownSearchIndexStatus | (string & {}); + /** Search index definition submitted to the server. */ export interface SearchIndexSpec { /** Index name; non-empty, without leading/trailing whitespace. */ From 8eec9f1374ef89c977d1c909b22f1e627f019a64 Mon Sep 17 00:00:00 2001 From: rocky Date: Thu, 12 Nov 2026 17:33:23 +0800 Subject: [PATCH 2/2] docs(search): correct waitForSearchIndex until conditions (MZ-17) Split the queryable, ready, and dropped wait conditions so they match operations.ts. Note that SearchIndexStatus can type an empty string while normalizeSearchIndexInfo still maps empty, missing, or non-string input to PENDING. Runtime wait logic is unchanged. --- docs/en/search-indexes.md | 4 +++- docs/zh/search-indexes.md | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/en/search-indexes.md b/docs/en/search-indexes.md index 998e0ac..9de5220 100644 --- a/docs/en/search-indexes.md +++ b/docs/en/search-indexes.md @@ -110,7 +110,9 @@ When an in-place update is not available, change the definition without downtime If a short outage is acceptable, drop the index, wait for `dropped`, and create it again. Searches fail until it is queryable again. The gap grows with data size. On atlas-local, 50,000 documents with 64 dimensions took about 1–2 seconds. -`status` passes the server value through unchanged, including unknown future statuses such as `PAUSED_BY_SERVER`. Those values are not rewritten to `PENDING`. TypeScript still autocompletes the documented `KnownSearchIndexStatus` literals, and `SearchIndexStatus` also accepts other non-empty server strings. Use `queryable` to decide whether searches can run and `status` to show progress. `waitForSearchIndex` still treats only `READY` and `FAILED` as terminal for the `ready` / `queryable` waits; an unknown status keeps polling until timeout, `queryable`, or a documented terminal status. +`status` passes the server value through unchanged, including unknown future statuses such as `PAUSED_BY_SERVER`. Those values are not rewritten to `PENDING`. TypeScript still autocompletes the documented `KnownSearchIndexStatus` literals, and `SearchIndexStatus` also accepts other server strings, including `''`. At runtime, `normalizeSearchIndexInfo` normalizes an empty, missing, or non-string status to `PENDING`, so list and wait results do not surface an empty `status`. Use `queryable` to decide whether searches can run and `status` to show progress. + +The three `waitForSearchIndex` `until` values are evaluated separately (see [waitForSearchIndex](#waitforsearchindex)). `until: 'queryable'` (the default) resolves when `queryable` is true, regardless of the status literal — including `FAILED` or an unknown future value such as `PAUSED_BY_SERVER`. It rejects with `SEARCH_INDEX_BUILD_FAILED` only when `queryable` is false and `status` is `FAILED`. A `READY` status with `queryable` false is not an immediate success and keeps polling. `until: 'ready'` resolves when `status` is `READY` and rejects with `SEARCH_INDEX_BUILD_FAILED` when `status` is `FAILED`. `until: 'dropped'` resolves when `status` is `DOES_NOT_EXIST`; `FAILED` keeps waiting. An unknown status that does not meet those success or failure rules keeps polling until timeout. ### dropSearchIndex diff --git a/docs/zh/search-indexes.md b/docs/zh/search-indexes.md index 3fd0faa..932d47a 100644 --- a/docs/zh/search-indexes.md +++ b/docs/zh/search-indexes.md @@ -110,7 +110,9 @@ Community 8.2.12 配合 `mongodb/mongodb-community-search` 会接受更新。20, 如果能接受短暂中断,可以先删除索引、等待 `dropped`、再重新创建。在它重新可查询之前,检索会失败。中断时长随数据量增长。atlas-local 上 50,000 篇文档、64 维约 1–2 秒。 -`status` 原样透传服务端的值,包括尚不在文档中的未来状态(例如 `PAUSED_BY_SERVER`),不会把未知状态改写成 `PENDING`。TypeScript 仍会补全已记录的 `KnownSearchIndexStatus` 字面量,同时 `SearchIndexStatus` 也接受其他非空服务端字符串。判断能否检索看 `queryable`,展示进度看 `status`。`waitForSearchIndex` 在 `ready` / `queryable` 等待中仍只把 `READY` 和 `FAILED` 视为终态;未知状态会继续轮询,直到超时、变为可查询,或出现已记录的终态。 +`status` 原样透传服务端的值,包括尚不在文档中的未来状态(例如 `PAUSED_BY_SERVER`),不会把未知状态改写成 `PENDING`。TypeScript 仍会补全已记录的 `KnownSearchIndexStatus` 字面量,同时 `SearchIndexStatus` 也接受其他服务端字符串,包括 `''`。运行时 `normalizeSearchIndexInfo` 会把空、缺失或非字符串的 status 规范化为 `PENDING`,因此 list / wait 的结果不会出现空的 `status`。判断能否检索看 `queryable`,展示进度看 `status`。 + +`waitForSearchIndex` 的三种 `until` 条件是分开判断的(见 [waitForSearchIndex](#waitforsearchindex))。`until: 'queryable'`(默认)在 `queryable` 为 true 时返回,与 status 字面量无关,即使是 `FAILED` 或未知的未来状态(例如 `PAUSED_BY_SERVER`)也可以成功;仅在 `queryable` 为 false 且 `status` 为 `FAILED` 时以 `SEARCH_INDEX_BUILD_FAILED` 拒绝。`status` 为 `READY` 但 `queryable` 为 false 时不会立即成功,会继续轮询。`until: 'ready'` 在 `status` 为 `READY` 时返回,遇到 `FAILED` 以 `SEARCH_INDEX_BUILD_FAILED` 拒绝。`until: 'dropped'` 在 `status` 为 `DOES_NOT_EXIST` 时返回,遇到 `FAILED` 继续等待。不满足上述成功或失败规则的未知状态会继续轮询,直到超时。 ### dropSearchIndex