123 lines
4.8 KiB
TypeScript
123 lines
4.8 KiB
TypeScript
|
|
/**
|
||
|
|
* @module metadata-field-typing.unit.test
|
||
|
|
* @description Regression: a metadata field that holds more than one value
|
||
|
|
* KIND stays fully filterable on every kind it holds.
|
||
|
|
*
|
||
|
|
* The defect this pins, reproduced on the released engine: the metadata index
|
||
|
|
* fixed a field's value type from the FIRST value it saw, and every later value
|
||
|
|
* of a different type was coerced to that type or, when coercion failed,
|
||
|
|
* dropped from the index in silence. Writing `category: 'electronics'` rows and
|
||
|
|
* then `category: 5` rows left `find({ where: { category: 5 } })` returning
|
||
|
|
* nothing — while the same rows in a numbers-only corpus answered correctly.
|
||
|
|
* The rows themselves were never lost: they stayed readable by id and by vector
|
||
|
|
* search, and only ever went missing from equality filters on that one field,
|
||
|
|
* which is what made it so quiet.
|
||
|
|
*
|
||
|
|
* Order is the whole point of these cases. Neither writer owns the field, so
|
||
|
|
* strings-then-numbers and numbers-then-strings must give the same answers.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { describe, it, expect } from 'vitest'
|
||
|
|
import { Brainy } from '../../src/brainy.js'
|
||
|
|
import { NounType } from '../../src/types/graphTypes.js'
|
||
|
|
|
||
|
|
/** A brain over memory storage, with a corpus written in the given order. */
|
||
|
|
async function brainWith(
|
||
|
|
rows: Array<{ label: string; category: unknown }>
|
||
|
|
): Promise<Brainy> {
|
||
|
|
const brainy = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
|
||
|
|
await brainy.init()
|
||
|
|
for (const row of rows) {
|
||
|
|
await brainy.add({
|
||
|
|
data: `item ${row.label}`,
|
||
|
|
type: NounType.Thing,
|
||
|
|
metadata: { label: row.label, category: row.category }
|
||
|
|
})
|
||
|
|
}
|
||
|
|
return brainy
|
||
|
|
}
|
||
|
|
|
||
|
|
const labelsOf = (results: Array<{ metadata?: Record<string, unknown> }>): string[] =>
|
||
|
|
results.map((r) => String(r.metadata?.label)).sort()
|
||
|
|
|
||
|
|
describe('regression: a mixed-kind metadata field filters on every kind', { timeout: 180_000 }, () => {
|
||
|
|
it('finds number rows written after string rows', async () => {
|
||
|
|
const brainy = await brainWith([
|
||
|
|
{ label: 'e1', category: 'electronics' },
|
||
|
|
{ label: 'f1', category: 'furniture' },
|
||
|
|
{ label: 'n1', category: 5 },
|
||
|
|
{ label: 'n2', category: 5 },
|
||
|
|
{ label: 'n3', category: 7 }
|
||
|
|
])
|
||
|
|
try {
|
||
|
|
expect(labelsOf(await brainy.find({ where: { category: 5 }, limit: 100 }))).toEqual(['n1', 'n2'])
|
||
|
|
expect(labelsOf(await brainy.find({ where: { category: 7 }, limit: 100 }))).toEqual(['n3'])
|
||
|
|
expect(labelsOf(await brainy.find({ where: { category: 'electronics' }, limit: 100 }))).toEqual(['e1'])
|
||
|
|
expect(labelsOf(await brainy.find({ where: { category: 'furniture' }, limit: 100 }))).toEqual(['f1'])
|
||
|
|
} finally {
|
||
|
|
await brainy.close()
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
it('finds string rows written after number rows', async () => {
|
||
|
|
const brainy = await brainWith([
|
||
|
|
{ label: 'n1', category: 5 },
|
||
|
|
{ label: 'n2', category: 5 },
|
||
|
|
{ label: 'e1', category: 'electronics' },
|
||
|
|
{ label: 'e2', category: 'electronics' }
|
||
|
|
])
|
||
|
|
try {
|
||
|
|
expect(labelsOf(await brainy.find({ where: { category: 'electronics' }, limit: 100 }))).toEqual(['e1', 'e2'])
|
||
|
|
expect(labelsOf(await brainy.find({ where: { category: 5 }, limit: 100 }))).toEqual(['n1', 'n2'])
|
||
|
|
} finally {
|
||
|
|
await brainy.close()
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
it('keeps `5` and `\'5\'` apart — a kind is part of the value, not a formatting detail', async () => {
|
||
|
|
const brainy = await brainWith([
|
||
|
|
{ label: 'num', category: 5 },
|
||
|
|
{ label: 'str', category: '5' }
|
||
|
|
])
|
||
|
|
try {
|
||
|
|
expect(labelsOf(await brainy.find({ where: { category: 5 }, limit: 100 }))).toEqual(['num'])
|
||
|
|
expect(labelsOf(await brainy.find({ where: { category: '5' }, limit: 100 }))).toEqual(['str'])
|
||
|
|
} finally {
|
||
|
|
await brainy.close()
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
it('serves booleans mixed into a field that already holds strings', async () => {
|
||
|
|
const brainy = await brainWith([
|
||
|
|
{ label: 's1', category: 'yes' },
|
||
|
|
{ label: 'b1', category: true },
|
||
|
|
{ label: 'b2', category: false }
|
||
|
|
])
|
||
|
|
try {
|
||
|
|
expect(labelsOf(await brainy.find({ where: { category: true }, limit: 100 }))).toEqual(['b1'])
|
||
|
|
expect(labelsOf(await brainy.find({ where: { category: false }, limit: 100 }))).toEqual(['b2'])
|
||
|
|
expect(labelsOf(await brainy.find({ where: { category: 'yes' }, limit: 100 }))).toEqual(['s1'])
|
||
|
|
} finally {
|
||
|
|
await brainy.close()
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
it('ranges over the numeric part of a mixed field', async () => {
|
||
|
|
const brainy = await brainWith([
|
||
|
|
{ label: 'unpriced', category: 'on request' },
|
||
|
|
{ label: 'cheap', category: 100 },
|
||
|
|
{ label: 'mid', category: 500 },
|
||
|
|
{ label: 'dear', category: 900 }
|
||
|
|
])
|
||
|
|
try {
|
||
|
|
const found = await brainy.find({
|
||
|
|
where: { category: { greaterThan: 200 } },
|
||
|
|
limit: 100
|
||
|
|
})
|
||
|
|
expect(labelsOf(found)).toEqual(['dear', 'mid'])
|
||
|
|
} finally {
|
||
|
|
await brainy.close()
|
||
|
|
}
|
||
|
|
})
|
||
|
|
})
|