brainy/tests/unit/brainy/upsert.test.ts
David Snelling 4cc2088aed feat(8.0): upsert + FindParams.includeVectors + removeMany adaptive chunking
Three additive ergonomics from the API-simplification audit (no behavior change
to existing call sites):

- AddParams.upsert: create-or-update in one call. With a custom id, an existing
  entity is MERGED via the update path (merges metadata, re-embeds changed data,
  bumps _rev, PRESERVES createdAt) instead of the destructive full overwrite a
  plain add() does. Mutually exclusive with ifAbsent (throws if both set);
  ignored when no id is supplied. Wired into add(), addMany (per-item flag
  propagation), and the transact add op (routes to planTxUpdate). Kills the
  get()-then-add() round-trip for idempotent writes.
- FindParams.includeVectors: mirror of GetOptions.includeVectors — find() returns
  stored vectors when set; default stays empty (the perf contract is preserved).
  Honored on both the query and metadata-only where paths, and in db.find().
- removeMany adaptive chunking: replaced the hardcoded chunkSize=10 with
  params.chunkSize ?? storageConfig.maxBatchSize, matching addMany/relateMany —
  one storage-adaptive batch policy across all *Many methods (no thrash on
  high-latency backends).

Tests: tests/unit/brainy/upsert.test.ts (insert/merge/createdAt-preserved/
re-embed/ifAbsent-conflict/no-id/addMany/transact), find-include-vectors.test.ts
(true/default/where-path), batch-operations.test.ts (removeMany >10 items).
2026-06-20 16:34:20 -07:00

194 lines
6.1 KiB
TypeScript

/**
* Unit tests for AddParams.upsert — create-or-update in a single add() call.
*
* Covers the live add() path and the addMany batch flag. Verifies that upsert
* inserts on a fresh id, merges (not overwrites) into an existing id while
* preserving createdAt and bumping _rev, is mutually exclusive with ifAbsent,
* and is a plain insert when no id is supplied.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { Brainy } from '../../../src/brainy'
import { createTestConfig } from '../../helpers/test-factory'
describe('AddParams.upsert', () => {
let brain: Brainy
beforeEach(async () => {
brain = new Brainy(createTestConfig())
await brain.init()
})
afterEach(async () => {
await brain.close()
})
it('inserts a new entity when the id does not exist (_rev 1)', async () => {
// 8.0 normalizes a natural string key to a stable canonical UUID; the entity is
// still reachable by the natural key via get()'s id resolution.
const id = await brain.add({
id: 'customer-1',
type: 'thing',
data: 'Acme Corp',
metadata: { tier: 'silver' },
upsert: true
})
expect(typeof id).toBe('string')
const entity = await brain.get('customer-1')
expect(entity).not.toBeNull()
expect(entity!.type).toBe('thing')
expect(entity!.metadata.tier).toBe('silver')
expect(entity!._rev).toBe(1)
})
it('merges into an existing entity, preserves createdAt, bumps _rev, applies changed data', async () => {
// Seed the entity.
await brain.add({
id: 'invoice-9',
type: 'thing',
data: 'Original line items',
metadata: { status: 'draft', amount: 100 }
})
const before = await brain.get('invoice-9')
expect(before).not.toBeNull()
const originalCreatedAt = before!.createdAt
expect(before!._rev).toBe(1)
// Upsert over the existing id with a partial metadata patch + new data.
await brain.add({
id: 'invoice-9',
type: 'thing',
data: 'Revised line items',
metadata: { status: 'sent' },
upsert: true
})
const after = await brain.get('invoice-9')
expect(after).not.toBeNull()
// Metadata MERGED, not replaced — the untouched 'amount' field survives.
expect(after!.metadata.status).toBe('sent')
expect(after!.metadata.amount).toBe(100)
// Changed data applied.
expect(after!.data).toBe('Revised line items')
// _rev bumped, createdAt preserved.
expect(after!._rev).toBe(2)
expect(after!.createdAt).toBe(originalCreatedAt)
})
it('re-embeds when data changes on upsert', async () => {
await brain.add({
id: 'doc-vec',
type: 'document',
data: 'cats and dogs and small furry animals'
})
const before = await brain.get('doc-vec', { includeVectors: true })
expect(before!.vector.length).toBeGreaterThan(0)
await brain.add({
id: 'doc-vec',
type: 'document',
data: 'quarterly financial revenue projections spreadsheet',
upsert: true
})
const after = await brain.get('doc-vec', { includeVectors: true })
expect(after!.vector.length).toBe(before!.vector.length)
// A genuinely different document re-embeds to a different vector.
expect(after!.vector).not.toEqual(before!.vector)
})
it('throws when both ifAbsent and upsert are true', async () => {
await expect(
brain.add({
id: 'conflict-1',
type: 'thing',
data: 'x',
ifAbsent: true,
upsert: true
})
).rejects.toThrow(/mutually exclusive/)
})
it('behaves like a plain insert when no id is supplied', async () => {
const id = await brain.add({
type: 'thing',
data: 'no id supplied',
metadata: { k: 'v' },
upsert: true
})
expect(typeof id).toBe('string')
const entity = await brain.get(id)
expect(entity).not.toBeNull()
expect(entity!._rev).toBe(1)
expect(entity!.metadata.k).toBe('v')
})
it('merges an existing item via the addMany batch upsert flag', async () => {
// Seed an item that the batch will later upsert into.
await brain.add({
id: 'order-1',
type: 'thing',
data: 'first',
metadata: { region: 'west', count: 1 }
})
const seeded = await brain.get('order-1')
const seededCreatedAt = seeded!.createdAt
const result = await brain.addMany({
upsert: true,
items: [
{ id: 'order-1', type: 'thing', data: 'updated', metadata: { count: 2 } },
{ id: 'order-2', type: 'thing', data: 'brand new', metadata: { region: 'east' } }
]
})
// Both items resolved (returned ids are the canonical UUIDs the natural keys map to).
expect(result.failed).toHaveLength(0)
expect(result.successful).toHaveLength(2)
// Existing item merged (region survived), _rev bumped, createdAt preserved.
const merged = await brain.get('order-1')
expect(merged!.metadata.region).toBe('west')
expect(merged!.metadata.count).toBe(2)
expect(merged!.data).toBe('updated')
expect(merged!._rev).toBe(2)
expect(merged!.createdAt).toBe(seededCreatedAt)
// New item inserted fresh.
const fresh = await brain.get('order-2')
expect(fresh!._rev).toBe(1)
expect(fresh!.metadata.region).toBe('east')
})
it('upserts inside a transact batch — existing id merges, new id inserts', async () => {
await brain.add({
id: 'acct-1',
type: 'thing',
data: 'seed',
metadata: { plan: 'free', seats: 1 }
})
const seeded = await brain.get('acct-1')
const seededCreatedAt = seeded!.createdAt
await brain.transact([
{ op: 'add', id: 'acct-1', type: 'thing', data: 'seed', metadata: { plan: 'pro' }, upsert: true },
{ op: 'add', id: 'acct-2', type: 'thing', data: 'new account', metadata: { plan: 'free' }, upsert: true }
])
const merged = await brain.get('acct-1')
expect(merged!.metadata.plan).toBe('pro')
expect(merged!.metadata.seats).toBe(1) // merged, not overwritten
expect(merged!._rev).toBe(2)
expect(merged!.createdAt).toBe(seededCreatedAt)
const fresh = await brain.get('acct-2')
expect(fresh!._rev).toBe(1)
expect(fresh!.metadata.plan).toBe('free')
})
})