fix(8.0): real bugs surfaced by integration hardening — where-intersect, related() offset, relate() updatedAt
Three of the five bugs the rot pass surfaced (correct inputs → wrong output), fixed:
1. where-filter dropped all but the last operator on a field. getIdsForFilter()'s
per-operator loop overwrote fieldResults each iteration, so
{ year: { greaterThan: 2009, lessThan: 2020 } } kept only lessThan. Now each
operator's match set is AND-intersected (multi-operator-per-field works).
(metadataIndex.ts)
2. related({ offset }) ignored the offset — getVerbs() zeroed it and smuggled it via
an unimplemented cursor, so every page returned items [0, limit). Now the real
offset is passed through to getVerbsWithPagination (which slices [offset,+limit)).
(baseStorage.ts) — verified: get-relations-fix pagination test passes.
3. relate() never persisted updatedAt, so reads fabricated a fresh Date.now() each
call (non-idempotent). Now createdAt and updatedAt share one timestamp at create.
(brainy.ts) — verified: get-relations-fix equivalence test passes.
Follow-ups (precisely diagnosed, not papered over): exclusive range bounds —
ColumnStore.rangeQuery(field,min,max) is inclusive-only, so getIdsForRange() drops
includeMin/includeMax (metadataIndex.ts:996) and lessThan/greaterThan behave as
lte/gte when the column store is active (the dual-bound test's popularity:95 boundary
stays red); counts not rehydrating after restart; unscoped VFS path-cache.
This commit is contained in:
parent
e5997a1516
commit
5eaf579937
3 changed files with 26 additions and 13 deletions
|
|
@ -2583,6 +2583,9 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
|
|
||||||
// Prepare verb metadata
|
// Prepare verb metadata
|
||||||
// User metadata spread FIRST, then system fields ALWAYS win (prevents collision)
|
// User metadata spread FIRST, then system fields ALWAYS win (prevents collision)
|
||||||
|
// One timestamp for both createdAt and updatedAt so a never-updated edge reports a
|
||||||
|
// stable updatedAt (=== createdAt) instead of a fresh Date.now() fabricated per read.
|
||||||
|
const relateTs = Date.now()
|
||||||
const verbMetadata = {
|
const verbMetadata = {
|
||||||
...(params.metadata || {}),
|
...(params.metadata || {}),
|
||||||
verb: params.type,
|
verb: params.type,
|
||||||
|
|
@ -2593,7 +2596,8 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
weight: params.weight ?? 1.0,
|
weight: params.weight ?? 1.0,
|
||||||
...(params.confidence !== undefined && { confidence: params.confidence }),
|
...(params.confidence !== undefined && { confidence: params.confidence }),
|
||||||
...(params.service !== undefined && { service: params.service }),
|
...(params.service !== undefined && { service: params.service }),
|
||||||
createdAt: Date.now(),
|
createdAt: relateTs,
|
||||||
|
updatedAt: relateTs,
|
||||||
...(params.data !== undefined && { data: params.data })
|
...(params.data !== undefined && { data: params.data })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2043,15 +2043,11 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
||||||
|
|
||||||
// Check if the adapter has a paginated method for getting verbs
|
// Check if the adapter has a paginated method for getting verbs
|
||||||
if (typeof this.getVerbsWithPagination === 'function') {
|
if (typeof this.getVerbsWithPagination === 'function') {
|
||||||
// Use the adapter's paginated method
|
// getVerbsWithPagination honors `offset` directly (it slices the
|
||||||
// Convert offset to cursor if no cursor provided (adapters use cursor for offset)
|
// [offset, offset+limit) window; `cursor` is not yet implemented there).
|
||||||
const effectiveCursor = cursor || (offset > 0 ? offset.toString() : undefined)
|
// Pass the real offset through. Previously offset was zeroed and smuggled
|
||||||
|
// via an ignored `cursor`, so every page returned items [0, limit) and
|
||||||
// offset is carried via effectiveCursor for adapters with cursor-based
|
// offset was silently dropped (related({ offset }) paginated incorrectly).
|
||||||
// pagination; 0 here matches the long-standing destructure default in
|
|
||||||
// getVerbsWithPagination (this call never passed offset). The
|
|
||||||
// annotation widens totalCount to optional: adapter overrides follow
|
|
||||||
// BaseStorageAdapter's contract, where totalCount may be absent.
|
|
||||||
const result: {
|
const result: {
|
||||||
items: HNSWVerbWithMetadata[]
|
items: HNSWVerbWithMetadata[]
|
||||||
totalCount?: number
|
totalCount?: number
|
||||||
|
|
@ -2059,12 +2055,11 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
||||||
nextCursor?: string
|
nextCursor?: string
|
||||||
} = await this.getVerbsWithPagination({
|
} = await this.getVerbsWithPagination({
|
||||||
limit,
|
limit,
|
||||||
offset: 0,
|
offset,
|
||||||
cursor: effectiveCursor,
|
cursor,
|
||||||
filter: options?.filter
|
filter: options?.filter
|
||||||
})
|
})
|
||||||
|
|
||||||
// Items are already offset by the adapter via cursor, no need to slice
|
|
||||||
const items = result.items
|
const items = result.items
|
||||||
|
|
||||||
// CRITICAL SAFETY CHECK: Prevent infinite loops
|
// CRITICAL SAFETY CHECK: Prevent infinite loops
|
||||||
|
|
|
||||||
|
|
@ -1820,7 +1820,14 @@ export class MetadataIndexManager implements MetadataIndexProvider {
|
||||||
if (condition && typeof condition === 'object' && !Array.isArray(condition)) {
|
if (condition && typeof condition === 'object' && !Array.isArray(condition)) {
|
||||||
// Handle Brainy Field Operators (canonical operators defined)
|
// Handle Brainy Field Operators (canonical operators defined)
|
||||||
// See docs/api/README.md for complete operator reference
|
// See docs/api/README.md for complete operator reference
|
||||||
|
//
|
||||||
|
// Multiple operators on ONE field are AND-combined (intersected): e.g.
|
||||||
|
// { greaterThan: 2009, lessThan: 2020 } requires BOTH bounds to hold. Each
|
||||||
|
// operator computes its own match set, then intersects with the running set.
|
||||||
|
let opIndex = 0
|
||||||
for (const [op, operand] of Object.entries(condition)) {
|
for (const [op, operand] of Object.entries(condition)) {
|
||||||
|
const prevOpResults = fieldResults
|
||||||
|
fieldResults = []
|
||||||
switch (op) {
|
switch (op) {
|
||||||
// ===== EQUALITY OPERATORS =====
|
// ===== EQUALITY OPERATORS =====
|
||||||
// Canonical: 'eq' | Alias: 'equals' | Deprecated: 'is'
|
// Canonical: 'eq' | Alias: 'equals' | Deprecated: 'is'
|
||||||
|
|
@ -1950,6 +1957,13 @@ export class MetadataIndexManager implements MetadataIndexProvider {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Intersect this operator's matches with the running set (AND semantics
|
||||||
|
// for multiple operators on the same field).
|
||||||
|
if (opIndex > 0) {
|
||||||
|
const prevSet = new Set(prevOpResults)
|
||||||
|
fieldResults = fieldResults.filter((id) => prevSet.has(id))
|
||||||
|
}
|
||||||
|
opIndex++
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Direct value match (shorthand for 'eq' operator)
|
// Direct value match (shorthand for 'eq' operator)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue