Skip to content
Merged
11 changes: 11 additions & 0 deletions .changeset/24371-data-read-one-overlay-read.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@objectstack/metadata-protocol': patch
---

Clause-②: no

perf(metadata-protocol): the by-name item read reads the item's stored active rows once, so a `GET /api/v1/data/:object` makes one `sys_metadata` read of the object instead of five

- **What changed.** `getMetaItem` (the read the API-exposure gate makes on every `/data` request) used to send its three active-row selections to the store one at a time: which package the item belongs to, the served row once per spelling of the type (`object`, then `objects`), and the rows its lock is chosen from, once per spelling. Now it reads the item's active rows once, for both spellings (`type IN ('object', 'objects')`), and each selection picks its rows from that one read with the same conditions it used to send the store. The item read's `version` reuses the package binding the read already selected.
- **Measured.** On Postgres behind a 50 ms round-trip proxy, the median `GET /api/v1/data/todo_task` (example todo app, signed-in admin) went from 551 ms with 5 `sys_metadata` statements to 340 ms with 1.
- **Unchanged.** The served item, its lock and `version`, and every refusal stay the same. Nothing is cached across calls, so a metadata write is seen by the next request. An object with a stored row still has one more read behind `version` (the repository's write-address head).
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ function keyOf(w: Record<string, unknown>) {
return `${w.type}|${w.name}|${w.organization_id ?? '__env__'}|${w.state ?? 'active'}|${w.package_id ?? '__nopkg__'}`;
}

/** [#24371] One `where` value as the store reads it: `{ $in: [...] }` (the by-name read's both-spellings `type`) or equality. */
const whereValueMatches = (actual: unknown, want: unknown): boolean =>
want !== null && typeof want === 'object' && Array.isArray((want as { $in?: unknown }).$in)
? (want as { $in: unknown[] }).$in.includes(actual)
: actual === want;

function matchesMetadataWhere(r: Row, where: Record<string, unknown>): boolean {
for (const [k, v] of Object.entries(where)) {
if (k === '$or') {
Expand All @@ -89,7 +95,7 @@ function matchesMetadataWhere(r: Row, where: Record<string, unknown>): boolean {
continue;
}
if (v === undefined) continue;
if ((r as any)[k] !== v) return false;
if (!whereValueMatches((r as any)[k], v)) return false;
}
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,16 @@ interface Row {
version?: number;
}

/** [#24371] One `where` value as the store reads it: `{ $in: [...] }` (the by-name read's both-spellings `type`) or equality. */
const whereValueMatches = (actual: unknown, want: unknown): boolean =>
want !== null && typeof want === 'object' && Array.isArray((want as { $in?: unknown }).$in)
? (want as { $in: unknown[] }).$in.includes(actual)
: actual === want;

function matches(r: Row, where: Record<string, unknown>): boolean {
for (const [k, v] of Object.entries(where)) {
if (v === undefined) continue;
if ((r as any)[k] !== v) return false;
if (!whereValueMatches((r as any)[k], v)) return false;
}
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,17 @@ const shadowRow = (name: string): Row => ({
checksum: 'sha256_pre_fix_shadow',
});

/** [#24371] One `where` value as the store reads it: `{ $in: [...] }` (the by-name read's both-spellings `type`) or equality. */
const whereValueMatches = (actual: unknown, want: unknown): boolean =>
want !== null && typeof want === 'object' && Array.isArray((want as { $in?: unknown }).$in)
? (want as { $in: unknown[] }).$in.includes(actual)
: actual === want;

const matchesWhere = (row: Record<string, unknown>, where: Record<string, unknown>) =>
Object.entries(where ?? {}).every(([k, v]) => {
if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`);
if (v === null || v === undefined) return row[k] === null || row[k] === undefined;
return row[k] === v;
return whereValueMatches(row[k], v);
});

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ function makeKernel(opts: { seed?: Row[] } = {}) {
if (k === '$or') return (v as Array<Record<string, unknown>>).some((c) => match(r, c));
return v === null || v === undefined
? (r as any)[k] === null || (r as any)[k] === undefined
: (r as any)[k] === v;
: whereValueMatches((r as any)[k], v);
});

const engine: any = {
Expand Down Expand Up @@ -343,6 +343,12 @@ const FINDING_PROSE = "Field 'b' removed — existing data in this column will b
// 1. The duplication is real — both channels carry the same findings
// ═══════════════════════════════════════════════════════════════════════════

/** [#24371] One `where` value as the store reads it: `{ $in: [...] }` (the by-name read's both-spellings `type`) or equality. */
const whereValueMatches = (actual: unknown, want: unknown): boolean =>
want !== null && typeof want === 'object' && Array.isArray((want as { $in?: unknown }).$in)
? (want as { $in: unknown[] }).$in.includes(actual)
: actual === want;

describe('[#10886] the 409 renders its findings into the message AND attaches them', () => {
it('declares the ADR-0112 envelope and attaches the structured findings', async () => {
const err = await destructiveRefusal();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ function makeKernel(seed: Row[] = [], artifacts: Record<string, unknown> = {}) {
if (k === '$or') return (v as Array<Record<string, unknown>>).some((c) => match(r, c));
return v === null || v === undefined
? (r as any)[k] === null || (r as any)[k] === undefined
: (r as any)[k] === v;
: whereValueMatches((r as any)[k], v);
});

const engine: any = {
Expand Down Expand Up @@ -261,6 +261,12 @@ afterEach(() => {
// 0. CONTROL — the harness really does reach the gate
// ═══════════════════════════════════════════════════════════════════════════

/** [#24371] One `where` value as the store reads it: `{ $in: [...] }` (the by-name read's both-spellings `type`) or equality. */
const whereValueMatches = (actual: unknown, want: unknown): boolean =>
want !== null && typeof want === 'object' && Array.isArray((want as { $in?: unknown }).$in)
? (want as { $in: unknown[] }).$in.includes(actual)
: actual === want;

describe('[#11014 §0 CONTROL] the gate fires for `object` through this harness', () => {
it('refuses a field-dropping `object` save with the ADR-0112 envelope', async () => {
const { protocol } = makeKernel([objectRow(PARENT, ['a', 'b'])]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ const storedRow = (type: string, name: string, body: unknown, extra: Partial<Sto
* entry the way `applyProtection` does and leaves a bare registration's
* provenance alone.
*/
/** [#24371] One `where` value as the store reads it: `{ $in: [...] }` (the by-name read's both-spellings `type`) or equality. */
const whereValueMatches = (actual: unknown, want: unknown): boolean =>
want !== null && typeof want === 'object' && Array.isArray((want as { $in?: unknown }).$in)
? (want as { $in: unknown[] }).$in.includes(actual)
: actual === want;

function registryDouble() {
const byType = new Map<string, Map<string, Record<string, unknown>>>();
const collection = (type: string) => {
Expand Down Expand Up @@ -143,7 +149,7 @@ function harness(rows: StoredRow[], services: Map<string, unknown> = new Map())
if (k.startsWith('$')) throw new Error(`[test double] unsupported WHERE combinator '${k}'`);
}
return rows.filter((r) =>
Object.entries(where).every(([k, v]) => v === undefined || (r as unknown as Record<string, unknown>)[k] === v),
Object.entries(where).every(([k, v]) => v === undefined || whereValueMatches((r as unknown as Record<string, unknown>)[k], v)),
);
};
const engine: any = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,16 @@ interface Row {
version?: number;
}

/** [#24371] One `where` value as the store reads it: `{ $in: [...] }` (the by-name read's both-spellings `type`) or equality. */
const whereValueMatches = (actual: unknown, want: unknown): boolean =>
want !== null && typeof want === 'object' && Array.isArray((want as { $in?: unknown }).$in)
? (want as { $in: unknown[] }).$in.includes(actual)
: actual === want;

function matches(r: Row, where: Record<string, unknown>): boolean {
for (const [k, v] of Object.entries(where)) {
if (v === undefined) continue;
if ((r as any)[k] !== v) return false;
if (!whereValueMatches((r as any)[k], v)) return false;
}
return true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
//
// [#24371] The by-name item read takes the item's ACTIVE rows from ONE store
// read of both spellings (`readItemActiveRows`), and each of its three
// active-row selections — the package-less binding (`packagelessAddress`),
// the served row (`findServedOverlayRow`) and the lock's scope
// (`overlayLockLayerAt`) — selects from those rows by the `where` it used to
// send the store. Before, they were five serial store reads on every `/data`
// request (the API-exposure gate reads the object by name).
//
// The served answer must not move. This file is the differential half of that
// claim: over every arrangement of up to three stored rows drawn from a pool
// (both spellings, the package-unbound row and two packages' rows, a bundled
// type's canonical and non-canonical member, three lock states), and every
// address a read can name, each selection made from the one read equals the
// selection the store answers by itself. The store double answers `where` as
// a driver does: equality, `null` as IS NULL, `$in` membership, rows in
// insertion order; anything else is refused.
//
// The count half — one read per `GET /api/v1/data/:object` — is pinned at the
// door, `packages/rest/src/rest-data-read-one-overlay-read.test.ts`.

import { describe, it, expect } from 'vitest';
import { SINGULAR_TO_PLURAL } from '@objectstack/spec/shared';
import { assertEngineFindOnePredicate } from '@objectstack/metadata-core';
import { ObjectStackProtocolImplementation } from './protocol.js';

type Row = Record<string, unknown>;

const valueMatches = (actual: unknown, want: unknown): boolean => {
if (want !== null && typeof want === 'object') {
const set = (want as { $in?: unknown }).$in;
if (!Array.isArray(set)) throw new Error(`store double: unsupported operator in ${JSON.stringify(want)}`);
return set.includes(actual ?? null);
}
return (actual ?? null) === (want ?? null);
};
const matches = (row: Row, where: Row = {}): boolean => Object.entries(where).every(([k, v]) => {
if (k.startsWith('$')) throw new Error(`store double: unsupported combinator ${k}`);
return v === undefined || valueMatches(row[k], v);
});

function protocolOver(rows: readonly Row[]): any {
const engine = {
registry: undefined,
find: async (table: string, q: { where?: Row; limit?: number } = {}) => {
if (table !== 'sys_metadata') return [];
const hits = rows.filter((r) => matches(r, q.where));
return typeof q.limit === 'number' ? hits.slice(0, q.limit) : hits;
},
findOne: async (table: string, q: { where?: Row } = {}) => {
assertEngineFindOnePredicate(table, q as never);
return table === 'sys_metadata' ? rows.find((r) => matches(r, q.where)) ?? null : null;
},
};
return new ObjectStackProtocolImplementation(engine as never, () => new Map());
}

const LOCKS = [undefined, 'full', 'no-delete'] as const;

/** Every distinct row key a type's item can hold, each with a lock from {@link LOCKS} by position. */
function pool(type: string, discriminated: boolean): Row[] {
const spellings = [type, SINGULAR_TO_PLURAL[type]].filter((t): t is string => typeof t === 'string');
const members = discriminated ? [null, 'zh-CN'] : [null];
const out: Row[] = [];
for (const spelling of spellings) {
for (const pkg of [null, 'pkg_a', 'pkg_b']) {
for (const member of members) {
const lock = LOCKS[out.length % LOCKS.length];
out.push({
id: `r${out.length}`, type: spelling, name: 'item', state: 'active', package_id: pkg,
bundle_member: member,
metadata: JSON.stringify({ name: 'item', label: `row ${out.length}`, ...(lock ? { _lock: lock } : {}) }),
});
}
}
}
return out;
}

function arrangements(rows: readonly Row[]): Row[][] {
const out: Row[][] = [];
for (let i = 0; i < rows.length; i++) {
out.push([rows[i]!]);
for (let j = i + 1; j < rows.length; j++) {
out.push([rows[i]!, rows[j]!], [rows[j]!, rows[i]!]);
for (let k = j + 1; k < rows.length; k++) out.push([rows[i]!, rows[j]!, rows[k]!], [rows[k]!, rows[j]!, rows[i]!]);
}
}
return out;
}

const idOf = (served: { row: Row } | undefined): unknown => served?.row.id;

for (const { type, discriminated } of [
{ type: 'object', discriminated: false },
{ type: 'email_template', discriminated: true },
]) {
describe(`[#24371] ${type}: each active-row selection made from the one read equals the store's own answer`, () => {
const all = arrangements(pool(type, discriminated));
const members = discriminated ? [undefined, 'zh-CN'] : [undefined];

it('the matrix is the one this file claims (not silently emptied)', () => {
expect(SINGULAR_TO_PLURAL[type]).toBeTypeOf('string');
// 6 row keys for `object` (2 spellings x 3 bindings), 12 for the
// bundled type (x 2 members): every 1-row arrangement, and every
// 2- and 3-row one in two store orders.
expect(all.length).toBe(discriminated ? 584 : 76);
});

it('one read serves every row the selections consult, of both spellings and nothing else', async () => {
for (const rows of all) {
const p = protocolOver([...rows, { ...rows[0]!, id: 'other_name', name: 'another' }]);
const read = await p.readItemActiveRows(type, 'item');
expect(read.map((r: Row) => r.id)).toEqual(rows.map((r) => r.id));
}
});

it('the package-less binding, the served row and the lock are the store\'s answers', async () => {
let compared = 0;
for (const rows of all) {
const p = protocolOver(rows);
const read = await p.readItemActiveRows(type, 'item');
for (const member of members) {
expect(await p.packagelessAddress(type, 'item', member, 'active', read))
.toEqual(await p.packagelessAddress(type, 'item', member, 'active'));
for (const packageId of [undefined, null, 'pkg_a', 'pkg_b']) {
const args = { type, name: 'item', state: 'active', packageId, ...(member ? { member } : {}) };
expect(idOf(await p.findServedOverlayRow(args, read)))
.toBe(idOf(await p.findServedOverlayRow(args)));
compared += 1;
}
}
for (const packageId of [undefined, 'pkg_a', 'pkg_b']) {
const address = { type, name: 'item', packageId };
expect(await p.overlayLockLayerAt(address, { otherSpelling: true }, read))
.toEqual(await p.overlayLockLayerAt(address, { otherSpelling: true }));
}
}
expect(compared).toBe(all.length * members.length * 4);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ interface StoredRow {
metadata: string;
}

/** [#24371] One `where` value as the store reads it: `{ $in: [...] }` (the by-name read's both-spellings `type`) or equality. */
const whereValueMatches = (actual: unknown, want: unknown): boolean =>
want !== null && typeof want === 'object' && Array.isArray((want as { $in?: unknown }).$in)
? (want as { $in: unknown[] }).$in.includes(actual)
: actual === want;

function stored(key: RowKey, state: 'active' | 'draft' = 'active', type: string = TYPE): StoredRow {
const spec: RowSpec = ROWS[key];
const label = state === 'draft' ? `${spec.label} (draft)` : spec.label;
Expand Down Expand Up @@ -123,7 +129,7 @@ function harness(rows: StoredRow[], artifact = false, type: string = TYPE) {
if (k.startsWith('$')) throw new Error(`[test double] unsupported WHERE combinator '${k}'`);
}
return rows.filter((r) =>
Object.entries(where).every(([k, v]) => v === undefined || (r as unknown as Record<string, unknown>)[k] === v),
Object.entries(where).every(([k, v]) => v === undefined || whereValueMatches((r as unknown as Record<string, unknown>)[k], v)),
);
};
const engine: any = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ const packaged = (name: string, lock: Lock, extra: Record<string, unknown> = {})
* that keeps nothing (the `_lock` gate records its denial through it). No
* write verbs beyond that — an admitted write is read off the gate itself.
*/
/** [#24371] One `where` value as the store reads it: `{ $in: [...] }` (the by-name read's both-spellings `type`) or equality. */
const whereValueMatches = (actual: unknown, want: unknown): boolean =>
want !== null && typeof want === 'object' && Array.isArray((want as { $in?: unknown }).$in)
? (want as { $in: unknown[] }).$in.includes(actual)
: actual === want;

function harness(environmentId: string | undefined, rows: StoredRow[] = [], artifacts: Artifacts = {}) {
const registry = {
getArtifactItem(type: string, name: string) {
Expand All @@ -109,7 +115,7 @@ function harness(environmentId: string | undefined, rows: StoredRow[] = [], arti
if (k.startsWith('$')) throw new Error(`[test double] unsupported WHERE combinator '${k}'`);
}
return rows.filter((r) =>
Object.entries(where).every(([k, v]) => v === undefined || (r as unknown as Record<string, unknown>)[k] === v),
Object.entries(where).every(([k, v]) => v === undefined || whereValueMatches((r as unknown as Record<string, unknown>)[k], v)),
);
};
const engine: any = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,12 @@ function describePackageRow(r: PackageRowAxis): string {
function describeOtherPackageRow(r: PackageRowAxis): string {
return r.row === 'none' ? '' : `env-wide other package's row _lock=${r.lock}`;
}
/** [#24371] One `where` value as the store reads it: `{ $in: [...] }` (the by-name read's both-spellings `type`) or equality. */
const whereValueMatches = (actual: unknown, want: unknown): boolean =>
want !== null && typeof want === 'object' && Array.isArray((want as { $in?: unknown }).$in)
? (want as { $in: unknown[] }).$in.includes(actual)
: actual === want;

export function titleOf(row: Row): string {
return [
`${row.topology.kernel} kernel`,
Expand Down Expand Up @@ -465,7 +471,7 @@ export function harness(
if (k.startsWith('$')) throw new Error(`[test double] unsupported WHERE combinator '${k}'`);
}
return rows.filter((r) =>
Object.entries(where).every(([k, v]) => v === undefined || (r as unknown as Record<string, unknown>)[k] === v),
Object.entries(where).every(([k, v]) => v === undefined || whereValueMatches((r as unknown as Record<string, unknown>)[k], v)),
);
};
const engine: any = {
Expand Down
Loading
Loading