Compare commits
No commits in common. "87d3a945a53ec7470b8b72f70942ed4849176b75" and "a2820e81afc629b76a3d842e9331902a082df70f" have entirely different histories.
87d3a945a5
...
a2820e81af
7 changed files with 39 additions and 211 deletions
|
|
@ -412,23 +412,18 @@ export class MigrationInProgressError extends BrainyError {
|
||||||
* embedding parked in the metadata bag would mint 384 postings for one row.
|
* embedding parked in the metadata bag would mint 384 postings for one row.
|
||||||
* The bound exists to keep that out of the index.
|
* The bound exists to keep that out of the index.
|
||||||
*
|
*
|
||||||
* 256 is hardcoded on purpose (the zero-config law: no knob). It sits far above
|
* 64 is hardcoded on purpose (the zero-config law: no knob). It sits far above
|
||||||
* every legitimate multi-value field the engine has seen — tags, authors,
|
* every legitimate multi-value field the engine has seen — tags, authors,
|
||||||
* categories, labels, keyword lists, participant lists — and still below the
|
* categories, labels, participant lists — and far below any real embedding
|
||||||
* narrowest embedding this engine will ever meet (384 dimensions, the smallest
|
* width, so the two populations do not overlap and no caller has to tune it.
|
||||||
* model it ships), so the two populations do not overlap and no caller has to
|
|
||||||
* tune it. A vector parked in metadata is refused; a long keyword list is not.
|
|
||||||
*
|
*
|
||||||
* It replaces a limit of 10 that was applied SILENTLY: a row whose `tags` array
|
* It replaces a limit of 10 that was applied SILENTLY: a row whose `tags` array
|
||||||
* held eleven entries had that field skipped entirely and dropped out of every
|
* held eleven entries had that field skipped entirely and dropped out of every
|
||||||
* filtered search on it, with no error, no warning and no way to tell the
|
* filtered search on it, with no error, no warning and no way to tell the
|
||||||
* difference from "no row matches". A rule this consequential is a law with a
|
* difference from "no row matches". A rule this consequential is a law with a
|
||||||
* name and a refusal, not a `continue`.
|
* name and a refusal, not a `continue`.
|
||||||
*
|
|
||||||
* This is the ONE place the number lives. Every message, warning, doc line and
|
|
||||||
* pin derives it from here — never a literal.
|
|
||||||
*/
|
*/
|
||||||
export const MAX_INDEXED_ARRAY_LENGTH = 256
|
export const MAX_INDEXED_ARRAY_LENGTH = 64
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A metadata field carries an array longer than {@link MAX_INDEXED_ARRAY_LENGTH}.
|
* A metadata field carries an array longer than {@link MAX_INDEXED_ARRAY_LENGTH}.
|
||||||
|
|
|
||||||
|
|
@ -88,36 +88,11 @@ describe('an idle brain costs nothing', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// (a) + (b): nothing ran, nothing was said.
|
// (a) + (b): nothing ran, nothing was said.
|
||||||
//
|
expect(logged.filter((l) => /All indexes flushed to disk/.test(l))).toEqual([])
|
||||||
// THE SPIES COME FIRST, AND THEY ARE THE ATTRIBUTABLE HALF. They are bound
|
expect(logged.filter((l) => /Flushing Brainy indexes/.test(l))).toEqual([])
|
||||||
// to THIS brain's providers, so they answer "did this brain flush?" and
|
|
||||||
// nothing else. The console filters below cannot: the gate config runs the
|
|
||||||
// whole suite in ONE process (`pool: 'forks'`, `singleFork: true`), so
|
|
||||||
// `console.log` carries the narration of every brain alive in that
|
|
||||||
// process — including one a previous file opened and never closed, whose
|
|
||||||
// unref'd cadence timer is still doing honest work. A neighbour narrating
|
|
||||||
// is a REAL finding about suite hygiene, but it is not this brain failing
|
|
||||||
// its own law, and the two must not be reported as the same thing.
|
|
||||||
//
|
|
||||||
// So: spies first (whose failure means the engine broke the law), console
|
|
||||||
// second (whose failure means SOMETHING in the process narrated), and the
|
|
||||||
// console assertion carries the captured lines in its message. vitest's
|
|
||||||
// stdout blocks are prefixed `stdout | <file> > <test>`, so those lines
|
|
||||||
// plus the surrounding gate log name the brain that printed them.
|
|
||||||
expect(countsSpy).not.toHaveBeenCalled()
|
expect(countsSpy).not.toHaveBeenCalled()
|
||||||
expect(metadataSpy).not.toHaveBeenCalled()
|
expect(metadataSpy).not.toHaveBeenCalled()
|
||||||
expect(graphSpy).not.toHaveBeenCalled()
|
expect(graphSpy).not.toHaveBeenCalled()
|
||||||
|
|
||||||
const flushChatter = logged.filter(
|
|
||||||
(l) => /All indexes flushed to disk/.test(l) || /Flushing Brainy indexes/.test(l)
|
|
||||||
)
|
|
||||||
expect(
|
|
||||||
flushChatter,
|
|
||||||
`a flush narrated during the ${IDLE_WATCH_MS}ms idle window. This brain's own ` +
|
|
||||||
`providers were NOT called (asserted above), so the lines below were printed by ` +
|
|
||||||
`another brain alive in this process — find it by the 'stdout | <file> > <test>' ` +
|
|
||||||
`prefix in the run log:\n${flushChatter.join('\n')}`
|
|
||||||
).toEqual([])
|
|
||||||
}, 180_000)
|
}, 180_000)
|
||||||
|
|
||||||
it('an explicit flush over a clean brain calls no provider and prints nothing', async () => {
|
it('an explicit flush over a clean brain calls no provider and prints nothing', async () => {
|
||||||
|
|
|
||||||
|
|
@ -161,8 +161,7 @@ describe('Metadata Vector Exclusion Fix', () => {
|
||||||
// silence at a bound of 10 — the field simply vanished from the index and
|
// silence at a bound of 10 — the field simply vanished from the index and
|
||||||
// the row dropped out of every `where` on it, indistinguishably from "no
|
// the row dropped out of every `where` on it, indistinguishably from "no
|
||||||
// row matches". The bound is now MAX_INDEXED_ARRAY_LENGTH and it REFUSES.
|
// row matches". The bound is now MAX_INDEXED_ARRAY_LENGTH and it REFUSES.
|
||||||
const overTheBound = MAX_INDEXED_ARRAY_LENGTH + 1
|
const largeArray = Array.from({ length: 100 }, (_, i) => `item${i}`)
|
||||||
const largeArray = Array.from({ length: overTheBound }, (_, i) => `item${i}`)
|
|
||||||
|
|
||||||
const err = await brainy
|
const err = await brainy
|
||||||
.add({
|
.add({
|
||||||
|
|
@ -177,7 +176,7 @@ describe('Metadata Vector Exclusion Fix', () => {
|
||||||
|
|
||||||
expect(err).toBeInstanceOf(MetadataArrayTooLargeError)
|
expect(err).toBeInstanceOf(MetadataArrayTooLargeError)
|
||||||
expect(err.field).toBe('items')
|
expect(err.field).toBe('items')
|
||||||
expect(err.length).toBe(overTheBound)
|
expect(err.length).toBe(100)
|
||||||
expect(err.limit).toBe(MAX_INDEXED_ARRAY_LENGTH)
|
expect(err.limit).toBe(MAX_INDEXED_ARRAY_LENGTH)
|
||||||
|
|
||||||
// Nothing was indexed from the refused write — no 'items' field, and above
|
// Nothing was indexed from the refused write — no 'items' field, and above
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
import { Brainy } from '../../../src/brainy'
|
import { Brainy } from '../../../src/brainy'
|
||||||
import { MetadataArrayTooLargeError, MAX_INDEXED_ARRAY_LENGTH } from '../../../src/errors/brainyError'
|
|
||||||
import {
|
import {
|
||||||
createAddParams,
|
createAddParams,
|
||||||
generateTestVector,
|
generateTestVector,
|
||||||
|
|
@ -269,23 +268,13 @@ describe('Brainy.get()', () => {
|
||||||
expect(entity!.id).toBe(id)
|
expect(entity!.id).toBe(id)
|
||||||
})
|
})
|
||||||
|
|
||||||
// THE INDEXABLE-ARRAY BOUND, from get()'s side. This case used to park a
|
it('should get entity with very large metadata', async () => {
|
||||||
// 1000-element array in the metadata bag and assert it came back. That
|
// Arrange
|
||||||
// shape is refused at the write door now — an array field mints one
|
|
||||||
// posting per element, so an unbounded array is an unbounded write — so
|
|
||||||
// the case pins BOTH halves of the law that replaced it: a large SCALAR
|
|
||||||
// payload still round-trips whole, and an array over the bound refuses by
|
|
||||||
// name. Every length derives from MAX_INDEXED_ARRAY_LENGTH so the pin
|
|
||||||
// follows the constant wherever it moves.
|
|
||||||
it('should get an entity with a large scalar metadata payload', async () => {
|
|
||||||
// Arrange — large in every dimension EXCEPT array length: a long string,
|
|
||||||
// many fields, deep nesting, and an array sitting exactly ON the bound.
|
|
||||||
const largeMetadata = {
|
const largeMetadata = {
|
||||||
atTheBound: Array.from({ length: MAX_INDEXED_ARRAY_LENGTH }, (_, i) => `item${i}`),
|
bigArray: new Array(1000).fill('item'),
|
||||||
bigObject: Object.fromEntries(
|
bigObject: Object.fromEntries(
|
||||||
Array.from({ length: 100 }, (_, i) => [`key${i}`, `value${i}`])
|
Array.from({ length: 100 }, (_, i) => [`key${i}`, `value${i}`])
|
||||||
),
|
),
|
||||||
longString: 'x'.repeat(10_000),
|
|
||||||
deepNesting: Array(10).fill(null).reduce(
|
deepNesting: Array(10).fill(null).reduce(
|
||||||
(acc) => ({ nested: acc }),
|
(acc) => ({ nested: acc }),
|
||||||
{ value: 'deep' }
|
{ value: 'deep' }
|
||||||
|
|
@ -301,43 +290,10 @@ describe('Brainy.get()', () => {
|
||||||
// Act
|
// Act
|
||||||
const entity = await brain.get(id)
|
const entity = await brain.get(id)
|
||||||
|
|
||||||
// Assert — the payload comes back whole, first element to last
|
// Assert
|
||||||
expect(entity).not.toBeNull()
|
expect(entity).not.toBeNull()
|
||||||
expect(entity!.metadata.atTheBound).toHaveLength(MAX_INDEXED_ARRAY_LENGTH)
|
expect(entity!.metadata.bigArray).toHaveLength(1000)
|
||||||
expect(entity!.metadata.atTheBound[0]).toBe('item0')
|
|
||||||
expect(entity!.metadata.atTheBound[MAX_INDEXED_ARRAY_LENGTH - 1])
|
|
||||||
.toBe(`item${MAX_INDEXED_ARRAY_LENGTH - 1}`)
|
|
||||||
expect(Object.keys(entity!.metadata.bigObject)).toHaveLength(100)
|
expect(Object.keys(entity!.metadata.bigObject)).toHaveLength(100)
|
||||||
expect(entity!.metadata.longString).toHaveLength(10_000)
|
|
||||||
|
|
||||||
// ...including the deep nest, walked to the bottom.
|
|
||||||
let cursor: any = entity!.metadata.deepNesting
|
|
||||||
for (let depth = 0; depth < 10; depth++) cursor = cursor.nested
|
|
||||||
expect(cursor.value).toBe('deep')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should refuse a metadata array over the indexing bound, by name', async () => {
|
|
||||||
// Arrange
|
|
||||||
const overTheBound = MAX_INDEXED_ARRAY_LENGTH + 1
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const err = await brain
|
|
||||||
.add(createAddParams({
|
|
||||||
data: 'Large metadata',
|
|
||||||
type: 'thing',
|
|
||||||
metadata: { bigArray: new Array(overTheBound).fill('item') }
|
|
||||||
}))
|
|
||||||
.catch((e: any) => e)
|
|
||||||
|
|
||||||
// Assert — the field, the length and the bound, on the error and in the
|
|
||||||
// message, so a handler can report or repair without parsing prose.
|
|
||||||
expect(err).toBeInstanceOf(MetadataArrayTooLargeError)
|
|
||||||
expect(err.field).toBe('bigArray')
|
|
||||||
expect(err.length).toBe(overTheBound)
|
|
||||||
expect(err.limit).toBe(MAX_INDEXED_ARRAY_LENGTH)
|
|
||||||
expect(err.message).toContain('bigArray')
|
|
||||||
expect(err.message).toContain(String(overTheBound))
|
|
||||||
expect(err.message).toContain(String(MAX_INDEXED_ARRAY_LENGTH))
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
import { Brainy } from '../../../src/brainy'
|
import { Brainy } from '../../../src/brainy'
|
||||||
import { MetadataArrayTooLargeError, MAX_INDEXED_ARRAY_LENGTH } from '../../../src/errors/brainyError'
|
|
||||||
import {
|
import {
|
||||||
createAddParams,
|
createAddParams,
|
||||||
createTestConfig,
|
createTestConfig,
|
||||||
|
|
@ -249,21 +248,14 @@ describe('Brainy.relate()', () => {
|
||||||
expect(matches.length).toBe(1) // Only one relationship should exist
|
expect(matches.length).toBe(1) // Only one relationship should exist
|
||||||
})
|
})
|
||||||
|
|
||||||
// THE INDEXABLE-ARRAY BOUND, from relate()'s side. This case used to pass a
|
it('should handle very long metadata', async () => {
|
||||||
// 100-element array through relate() and assert it came back — a length
|
// Arrange
|
||||||
// hardcoded either side of a bound it never named, so it read green or red
|
|
||||||
// purely by where the constant happened to sit. Both halves of the law are
|
|
||||||
// pinned here instead, and every length derives from
|
|
||||||
// MAX_INDEXED_ARRAY_LENGTH so the pin follows the constant.
|
|
||||||
it('should handle a large scalar metadata payload on a relation', async () => {
|
|
||||||
// Arrange — large in every dimension EXCEPT array length: a long string,
|
|
||||||
// many fields, and an array sitting exactly ON the bound.
|
|
||||||
const largeMetadata = {
|
const largeMetadata = {
|
||||||
atTheBound: Array.from({ length: MAX_INDEXED_ARRAY_LENGTH }, (_, i) => `item${i}`),
|
bigArray: new Array(100).fill('item'),
|
||||||
bigObject: Object.fromEntries(
|
bigObject: Object.fromEntries(
|
||||||
Array.from({ length: 50 }, (_, i) => [`key${i}`, `value${i}`])
|
Array.from({ length: 50 }, (_, i) => [`key${i}`, `value${i}`])
|
||||||
),
|
),
|
||||||
longString: 'x'.repeat(10_000)
|
longString: 'x'.repeat(1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -274,45 +266,11 @@ describe('Brainy.relate()', () => {
|
||||||
metadata: largeMetadata
|
metadata: largeMetadata
|
||||||
})
|
})
|
||||||
|
|
||||||
// Assert — the payload comes back whole, first element to last
|
// Assert
|
||||||
const relations = await brain.related({ from: entity1Id })
|
const relations = await brain.related({ from: entity1Id })
|
||||||
const relation = relations.find(r => r.to === entity2Id)
|
const relation = relations.find(r => r.to === entity2Id)
|
||||||
expect(relation).toBeDefined()
|
expect(relation).toBeDefined()
|
||||||
expect(relation!.metadata?.atTheBound).toHaveLength(MAX_INDEXED_ARRAY_LENGTH)
|
expect(relation!.metadata?.bigArray).toHaveLength(100)
|
||||||
expect(relation!.metadata?.atTheBound[0]).toBe('item0')
|
|
||||||
expect(relation!.metadata?.atTheBound[MAX_INDEXED_ARRAY_LENGTH - 1])
|
|
||||||
.toBe(`item${MAX_INDEXED_ARRAY_LENGTH - 1}`)
|
|
||||||
expect(Object.keys(relation!.metadata?.bigObject)).toHaveLength(50)
|
|
||||||
expect(relation!.metadata?.longString).toHaveLength(10_000)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should refuse a relation metadata array over the indexing bound, by name', async () => {
|
|
||||||
// Arrange
|
|
||||||
const overTheBound = MAX_INDEXED_ARRAY_LENGTH + 1
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const err = await brain
|
|
||||||
.relate({
|
|
||||||
from: entity1Id,
|
|
||||||
to: entity3Id,
|
|
||||||
type: 'relatedTo',
|
|
||||||
metadata: { bigArray: new Array(overTheBound).fill('item') }
|
|
||||||
})
|
|
||||||
.catch((e: any) => e)
|
|
||||||
|
|
||||||
// Assert — the field, the length and the bound, on the error and in the
|
|
||||||
// message, so a handler can report or repair without parsing prose.
|
|
||||||
expect(err).toBeInstanceOf(MetadataArrayTooLargeError)
|
|
||||||
expect(err.field).toBe('bigArray')
|
|
||||||
expect(err.length).toBe(overTheBound)
|
|
||||||
expect(err.limit).toBe(MAX_INDEXED_ARRAY_LENGTH)
|
|
||||||
expect(err.message).toContain('bigArray')
|
|
||||||
expect(err.message).toContain(String(overTheBound))
|
|
||||||
expect(err.message).toContain(String(MAX_INDEXED_ARRAY_LENGTH))
|
|
||||||
|
|
||||||
// Refused means not written: no relation of this shape exists.
|
|
||||||
const relations = await brain.related({ from: entity1Id })
|
|
||||||
expect(relations.some(r => r.to === entity3Id && r.metadata?.bigArray)).toBe(false)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle special characters in metadata', async () => {
|
it('should handle special characters in metadata', async () => {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
import { Brainy } from '../../../src/brainy'
|
import { Brainy } from '../../../src/brainy'
|
||||||
import { MetadataArrayTooLargeError, MAX_INDEXED_ARRAY_LENGTH } from '../../../src/errors/brainyError'
|
|
||||||
import {
|
import {
|
||||||
createAddParams,
|
createAddParams,
|
||||||
createTestConfig,
|
createTestConfig,
|
||||||
|
|
@ -356,27 +355,18 @@ describe('Brainy.update()', () => {
|
||||||
expect(final!.metadata.counter).toBeLessThanOrEqual(10)
|
expect(final!.metadata.counter).toBeLessThanOrEqual(10)
|
||||||
})
|
})
|
||||||
|
|
||||||
// THE INDEXABLE-ARRAY BOUND, from update()'s side. This case used to write
|
it('should handle very large metadata updates', async () => {
|
||||||
// a 1000-element array through update() and assert it came back. That
|
|
||||||
// shape is refused at the write door now — an array field mints one
|
|
||||||
// posting per element, so an unbounded array is an unbounded write — so
|
|
||||||
// the case pins BOTH halves of the law that replaced it. Every length
|
|
||||||
// derives from MAX_INDEXED_ARRAY_LENGTH so the pin follows the constant.
|
|
||||||
it('should handle a large scalar metadata update', async () => {
|
|
||||||
// Arrange
|
// Arrange
|
||||||
const id = await brain.add(createAddParams({
|
const id = await brain.add(createAddParams({
|
||||||
data: 'Large metadata test',
|
data: 'Large metadata test',
|
||||||
type: 'thing'
|
type: 'thing'
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Large in every dimension EXCEPT array length: a long string, many
|
|
||||||
// fields, deep nesting, and an array sitting exactly ON the bound.
|
|
||||||
const largeMetadata = {
|
const largeMetadata = {
|
||||||
atTheBound: Array.from({ length: MAX_INDEXED_ARRAY_LENGTH }, (_, i) => `item${i}`),
|
bigArray: new Array(1000).fill('item'),
|
||||||
bigObject: Object.fromEntries(
|
bigObject: Object.fromEntries(
|
||||||
Array.from({ length: 100 }, (_, i) => [`key${i}`, `value${i}`])
|
Array.from({ length: 100 }, (_, i) => [`key${i}`, `value${i}`])
|
||||||
),
|
),
|
||||||
longString: 'x'.repeat(10_000),
|
|
||||||
deepNesting: Array(10).fill(null).reduce(
|
deepNesting: Array(10).fill(null).reduce(
|
||||||
(acc) => ({ nested: acc }),
|
(acc) => ({ nested: acc }),
|
||||||
{ value: 'deep' }
|
{ value: 'deep' }
|
||||||
|
|
@ -390,54 +380,11 @@ describe('Brainy.update()', () => {
|
||||||
merge: false
|
merge: false
|
||||||
})
|
})
|
||||||
|
|
||||||
// Assert — the payload comes back whole, first element to last
|
// Assert
|
||||||
const updated = await brain.get(id)
|
const updated = await brain.get(id)
|
||||||
expect(updated).not.toBeNull()
|
expect(updated).not.toBeNull()
|
||||||
expect(updated!.metadata.atTheBound).toHaveLength(MAX_INDEXED_ARRAY_LENGTH)
|
expect(updated!.metadata.bigArray).toHaveLength(1000)
|
||||||
expect(updated!.metadata.atTheBound[0]).toBe('item0')
|
|
||||||
expect(updated!.metadata.atTheBound[MAX_INDEXED_ARRAY_LENGTH - 1])
|
|
||||||
.toBe(`item${MAX_INDEXED_ARRAY_LENGTH - 1}`)
|
|
||||||
expect(Object.keys(updated!.metadata.bigObject)).toHaveLength(100)
|
expect(Object.keys(updated!.metadata.bigObject)).toHaveLength(100)
|
||||||
expect(updated!.metadata.longString).toHaveLength(10_000)
|
|
||||||
|
|
||||||
// ...including the deep nest, walked to the bottom.
|
|
||||||
let cursor: any = updated!.metadata.deepNesting
|
|
||||||
for (let depth = 0; depth < 10; depth++) cursor = cursor.nested
|
|
||||||
expect(cursor.value).toBe('deep')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should refuse an update whose metadata array is over the indexing bound, by name', async () => {
|
|
||||||
// Arrange
|
|
||||||
const id = await brain.add(createAddParams({
|
|
||||||
data: 'Large metadata test',
|
|
||||||
type: 'thing',
|
|
||||||
metadata: { keep: 'me' }
|
|
||||||
}))
|
|
||||||
const overTheBound = MAX_INDEXED_ARRAY_LENGTH + 1
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const err = await brain
|
|
||||||
.update({
|
|
||||||
id,
|
|
||||||
metadata: { bigArray: new Array(overTheBound).fill('item') },
|
|
||||||
merge: false
|
|
||||||
})
|
|
||||||
.catch((e: any) => e)
|
|
||||||
|
|
||||||
// Assert — the field, the length and the bound, on the error and in the
|
|
||||||
// message, so a handler can report or repair without parsing prose.
|
|
||||||
expect(err).toBeInstanceOf(MetadataArrayTooLargeError)
|
|
||||||
expect(err.field).toBe('bigArray')
|
|
||||||
expect(err.length).toBe(overTheBound)
|
|
||||||
expect(err.limit).toBe(MAX_INDEXED_ARRAY_LENGTH)
|
|
||||||
expect(err.message).toContain('bigArray')
|
|
||||||
expect(err.message).toContain(String(overTheBound))
|
|
||||||
expect(err.message).toContain(String(MAX_INDEXED_ARRAY_LENGTH))
|
|
||||||
|
|
||||||
// Refused means unchanged: the row still carries what it had before.
|
|
||||||
const unchanged = await brain.get(id)
|
|
||||||
expect(unchanged!.metadata.keep).toBe('me')
|
|
||||||
expect(unchanged!.metadata.bigArray).toBeUndefined()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should preserve entity ID during update', async () => {
|
it('should preserve entity ID during update', async () => {
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,9 @@
|
||||||
* and the caller had no way to tell that from "no row matches". Eleven tags is
|
* and the caller had no way to tell that from "no row matches". Eleven tags is
|
||||||
* not an exotic shape; the eleventh tag made the row invisible.
|
* not an exotic shape; the eleventh tag made the row invisible.
|
||||||
*
|
*
|
||||||
* THE LAW. Arrays of scalars index up to {@link MAX_INDEXED_ARRAY_LENGTH},
|
* THE LAW. Arrays of scalars index up to {@link MAX_INDEXED_ARRAY_LENGTH} = 64,
|
||||||
* hardcoded (the zero-config law: no knob), which clears every legitimate
|
* hardcoded (the zero-config law: no knob), which clears every legitimate
|
||||||
* multi-value field — tags, authors, keyword lists — and stays below the
|
* multi-value field and stays far below any embedding width. Above it the WRITE
|
||||||
* narrowest embedding this engine meets (384 dimensions). Above it the WRITE
|
|
||||||
* IS REFUSED by name — `MetadataArrayTooLargeError`, carrying the field, the
|
* IS REFUSED by name — `MetadataArrayTooLargeError`, carrying the field, the
|
||||||
* length and the bound — at `add`, `update`, `relate` and `updateRelation`
|
* length and the bound — at `add`, `update`, `relate` and `updateRelation`
|
||||||
* alike. Nothing is skipped in silence.
|
* alike. Nothing is skipped in silence.
|
||||||
|
|
@ -66,7 +65,7 @@ describe('the indexable-array bound', () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('indexes right up to the bound — every element of it', async () => {
|
it('indexes right up to the bound — all 64 elements', async () => {
|
||||||
await brain.add({
|
await brain.add({
|
||||||
id: 'at-bound',
|
id: 'at-bound',
|
||||||
data: 'a row at the bound',
|
data: 'a row at the bound',
|
||||||
|
|
@ -75,9 +74,8 @@ describe('the indexable-array bound', () => {
|
||||||
vector: []
|
vector: []
|
||||||
})
|
})
|
||||||
|
|
||||||
// The first, the last, and one in the middle — all derived from the
|
// The first, the last, and one in the middle.
|
||||||
// bound, so the case follows the constant wherever it moves.
|
for (const tag of ['t0', `t${MAX_INDEXED_ARRAY_LENGTH - 1}`, 't31']) {
|
||||||
for (const tag of ['t0', `t${MAX_INDEXED_ARRAY_LENGTH - 1}`, `t${Math.floor(MAX_INDEXED_ARRAY_LENGTH / 2)}`]) {
|
|
||||||
const hits = await brain.find({ where: { tags: tag }, limit: 10 } as any)
|
const hits = await brain.find({ where: { tags: tag }, limit: 10 } as any)
|
||||||
expect(hits.map((r: any) => r.id)).toContain(resolveEntityId('at-bound'))
|
expect(hits.map((r: any) => r.id)).toContain(resolveEntityId('at-bound'))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Reference in a new issue