Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelogs/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 8 additions & 2 deletions docs/en/search-indexes.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,13 @@ interface SearchIndexSpec {
definition: Record<string, unknown>;
}

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';
Expand Down Expand Up @@ -106,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. 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 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

Expand Down
10 changes: 8 additions & 2 deletions docs/zh/search-indexes.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,13 @@ interface SearchIndexSpec {
definition: Record<string, unknown>;
}

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';
Expand Down Expand Up @@ -106,7 +110,9 @@ 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` 也接受其他服务端字符串,包括 `''`。运行时 `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

Expand Down
2 changes: 1 addition & 1 deletion src/adapters/mongodb/search-indexes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
6 changes: 3 additions & 3 deletions src/adapters/mongodb/search-indexes/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> = new Set(['PENDING', 'BUILDING', 'READY', 'FAILED', 'STALE', 'DELETING', 'DOES_NOT_EXIST']);

Expand All @@ -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<string, unknown>,
Expand All @@ -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);
}

Expand Down
1 change: 1 addition & 0 deletions src/adapters/mongodb/search-indexes/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type {
SearchIndexInfo,
SearchIndexManagement,
SearchIndexSpec,
KnownSearchIndexStatus,
SearchIndexStatus,
SearchIndexType,
SearchIndexTypeSource,
Expand Down
9 changes: 9 additions & 0 deletions test/types/search-index-usage.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -29,6 +31,13 @@ expectAssignable<'created' | 'skipped' | 'updated'>(result.action);
declare const info: SearchIndexInfo;
expectType<SearchIndexTypeSource>(info.typeSource);
expectAssignable<'search' | 'vectorSearch' | undefined>(info.type);
expectAssignable<SearchIndexStatus>(info.status);
expectAssignable<SearchIndexStatus>('READY');
expectAssignable<SearchIndexStatus>('PAUSED_BY_SERVER');
expectAssignable<KnownSearchIndexStatus>('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' }));
Expand Down
34 changes: 34 additions & 0 deletions test/unit/search-indexes/search-indexes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
createSearchIndexesOp,
dropSearchIndexOp,
getSearchIndexOp,
isKnownSearchIndexStatus,
listSearchIndexesOp,
normalizeSearchIndexInfo,
updateSearchIndexOp,
Expand Down Expand Up @@ -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' },
Expand Down Expand Up @@ -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');
Expand Down
14 changes: 12 additions & 2 deletions types/search-index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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. */
Expand Down
Loading