brainy/src/indexes/columnStore/ColumnManifest.ts

204 lines
5.2 KiB
TypeScript
Raw Normal View History

/**
* @module columnStore/ColumnManifest
* @description Per-field manifest tracking segment files and field metadata.
*
feat(8.0)!: delete fork/branch/commit/history/versions — superseded by the Db API The COW version-control surface (fork, branches, checkout, commit, getHistory/streamHistory, asOfCommit, brain.versions) is gone, along with its subsystems: src/versioning/, the COW object store (CommitLog, CommitObject, RefManager, TreeObject), HistoricalStorageAdapter, and the TypeAwareHNSWIndex path it kept alive. The Db API (now/transact/asOf/ with/persist/restore) is the one versioning model in 8.0. Survivors and replacements: - BlobStorage survives (the VFS stores file content through it), relocated to src/storage/blobStorage.ts with binaryDataCodec.ts; its adapter interface is now BlobStoreAdapter, slimmed to the consumed surface (write/read/has/delete/getMetadata + MIME-aware compression policy). - brain.migrate() backup branches are replaced by persist-before-migrate: MigrateOptions.backupTo persists a hard-link snapshot of the current generation before any transform runs; MigrationResult.backupPath reports it, and brain.restore(path) brings it back wholesale. - CLI: cow.ts (fork/branch/checkout/history/migrate) is replaced by snapshot.ts — snapshot <path>, restore <path>, history (tx-log), generation. - New public read API: brain.transactionLog({limit}) exposes the reified tx-log (generation/timestamp/meta, newest first) that backs the CLI history command; TxLogEntry is exported. Tests: superseded suites deleted; fork/commit blocks excised from shared suites; BlobStorage tests relocated + reworked against the slimmed store; migration tests now prove the backupTo snapshot/restore round trip; new transactionLog coverage in db-mvcc.
2026-06-10 15:22:47 -07:00
* Stored as JSON at `_column_index/{field}/MANIFEST.json` using storage-root-
* relative paths. Rewritten atomically on every flush and compaction.
*
* The manifest is the single source of truth for which segments exist for a
* field. On startup, the column store loads each field's manifest and
* discovers segments from it no directory listing needed.
*/
import type { StorageAdapter } from '../../coreTypes.js'
import type { ManifestData, SegmentMeta } from './types.js'
import { ValueType } from './types.js'
/**
* Manages a single field's segment manifest.
*
* @example
* const manifest = new ColumnManifest('createdAt', '_column_index')
* await manifest.load(storage)
* manifest.addSegment({ id: 1, level: 0, count: 50000, ... })
* await manifest.save(storage)
*/
export class ColumnManifest {
/** Field this manifest covers. */
readonly fieldName: string
/** Base storage path for the column index. */
readonly basePath: string
/** Manifest data — loaded from storage or initialized fresh. */
private data: ManifestData
/**
* @param fieldName - The field name (e.g. 'createdAt', 'status', '__words__')
* @param basePath - Base path for segment files (default: '_column_index')
*/
constructor(fieldName: string, basePath: string = '_column_index') {
this.fieldName = fieldName
this.basePath = basePath
this.data = {
version: 0,
field: fieldName,
valueType: ValueType.Number,
multiValue: false,
segments: [],
nextSegmentId: 1,
buildComplete: false
}
}
/**
* Load the manifest from storage. If not found, starts fresh.
*
* @param storage - Storage adapter
*/
async load(storage: StorageAdapter): Promise<void> {
try {
const path = this.manifestPath()
const data = await (storage as any).readObjectFromPath(path)
if (data && data.field === this.fieldName) {
this.data = data as ManifestData
}
} catch {
// Not found — start fresh (data is already initialized in constructor)
}
}
/**
* Save the manifest to storage atomically.
*
* @param storage - Storage adapter
*/
async save(storage: StorageAdapter): Promise<void> {
this.data.version++
const path = this.manifestPath()
await (storage as any).writeObjectToPath(path, this.data)
}
/**
* Register a new segment in the manifest.
*
* @param meta - Segment metadata
*/
addSegment(meta: SegmentMeta): void {
this.data.segments.push(meta)
// Keep sorted by level then id for consistent ordering
this.data.segments.sort((a, b) => a.level !== b.level ? a.level - b.level : a.id - b.id)
}
/**
* Remove segments by ID (e.g. after compaction replaces them).
*
* @param ids - Segment IDs to remove
*/
removeSegments(ids: number[]): void {
const idSet = new Set(ids)
this.data.segments = this.data.segments.filter(s => !idSet.has(s.id))
}
/**
* Allocate the next segment ID (monotonically increasing).
*
* @returns Next available segment ID
*/
nextSegmentId(): number {
return this.data.nextSegmentId++
}
/**
* Get all segments at a specific compaction level.
*
* @param level - LSM compaction level (0 = freshest)
* @returns Segments at that level
*/
getSegmentsAtLevel(level: number): SegmentMeta[] {
return this.data.segments.filter(s => s.level === level)
}
/**
* Get all segments across all levels.
*/
getAllSegments(): SegmentMeta[] {
return this.data.segments
}
/**
* Whether the manifest has any segments.
*/
isEmpty(): boolean {
return this.data.segments.length === 0
}
/**
* Total entry count across all segments.
*/
get totalCount(): number {
return this.data.segments.reduce((sum, s) => sum + s.count, 0)
}
/**
* Value type for this field.
*/
get valueType(): ValueType {
return this.data.valueType
}
set valueType(type: ValueType) {
this.data.valueType = type
}
/**
* Whether this is a multi-value field (e.g. __words__).
*/
get multiValue(): boolean {
return this.data.multiValue
}
set multiValue(mv: boolean) {
this.data.multiValue = mv
}
/**
* Whether the initial migration from existing entities has completed.
*/
get buildComplete(): boolean {
return this.data.buildComplete
}
set buildComplete(done: boolean) {
this.data.buildComplete = done
}
/**
* Generate the storage path for a segment file.
*
* @param level - Compaction level
* @param id - Segment ID
* @returns Path like `_column_index/createdAt/L0-000001.cidx`
*/
segmentPath(level: number, id: number): string {
const paddedId = String(id).padStart(6, '0')
return `${this.basePath}/${this.fieldName}/L${level}-${paddedId}.cidx`
}
/**
* Generate the storage path for this field's manifest JSON.
*
* @returns Path like `_column_index/createdAt/MANIFEST.json`
*/
manifestPath(): string {
return `${this.basePath}/${this.fieldName}/MANIFEST.json`
}
/**
* Get the raw manifest data (for serialization or debugging).
*/
getData(): Readonly<ManifestData> {
return this.data
}
}