Skip to content

Commit 99eb366

Browse files
committed
test(objectql): the cascade anchor pin's stub driver honours the caller's limit
check:objectql-double-limit could not drive the stub's find (its failure lookup threw on the gate's row stub) and refused it as a new unjudged double. The find now reads rows from a table map, applies the bound after the filter by presence, and the gate grades it as applying the bound. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5c5d9b3 commit 99eb366

1 file changed

Lines changed: 41 additions & 36 deletions

File tree

‎packages/objectql/src/engine-cascade-federated-tenant-anchor.test.ts‎

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,18 @@ import { ObjectQL } from './engine.js';
4242
const REMOTE = 'remote_ds';
4343
const PACKAGE_ID = 'test-21910';
4444

45-
type Rows = Map<string, Map<string, Record<string, unknown>>>;
45+
type Row = Record<string, unknown>;
4646

4747
/**
4848
* A stub driver that records every read into a shared log and can be told to
49-
* refuse reads of one object with an exact error object.
49+
* refuse reads of one object with an exact error object. Its `find` applies
50+
* the caller's `limit` after the filter, by presence.
5051
*/
5152
function makeDriver(name: string, log: { reads: string[]; begun: number }) {
52-
const stores: Rows = new Map();
53+
const tables: Record<string, Row[]> = {};
5354
const failReads = new Map<string, unknown>();
54-
const storeFor = (o: string) => {
55-
let s = stores.get(o);
56-
if (!s) { s = new Map(); stores.set(o, s); }
57-
return s;
58-
};
59-
const matches = (row: Record<string, unknown>, where: any): boolean => {
55+
const rowsOf = (o: string): Row[] => (tables[o] ??= []);
56+
const matches = (row: Row, where: any): boolean => {
6057
if (!where || typeof where !== 'object') return true;
6158
for (const [k, v] of Object.entries(where)) {
6259
if (k.startsWith('$')) continue;
@@ -65,47 +62,55 @@ function makeDriver(name: string, log: { reads: string[]; begun: number }) {
6562
}
6663
return true;
6764
};
68-
const gate = (o: string) => {
69-
log.reads.push(o);
70-
if (failReads.has(o)) throw failReads.get(o);
71-
};
7265
const driver: any = {
7366
name, version: '0.0.0', supports: {},
7467
async connect() {}, async disconnect() {}, async checkHealth() { return true; }, async execute() { return null; },
7568
async syncSchema() {},
7669
registerExternalObject() {},
7770
async find(o: string, ast: any) {
78-
gate(o);
79-
return Array.from(storeFor(o).values()).filter((r) => matches(r, ast?.where));
71+
log.reads.push(o);
72+
const failure = failReads.get(o);
73+
if (failure !== undefined) throw failure;
74+
const hit = (tables[o] ?? []).filter((r) => matches(r, ast?.where));
75+
return typeof ast?.limit === 'number' ? hit.slice(0, ast.limit) : hit;
8076
},
8177
async findOne(o: string, ast: any) {
82-
gate(o);
83-
for (const r of storeFor(o).values()) if (matches(r, ast?.where)) return r;
84-
return null;
78+
const [first] = await this.find(o, { ...ast, limit: 1 });
79+
return first ?? null;
8580
},
8681
async count(o: string, ast: any) {
87-
gate(o);
88-
return Array.from(storeFor(o).values()).filter((r) => matches(r, ast?.where)).length;
82+
return (await this.find(o, { where: ast?.where })).length;
8983
},
90-
async create(o: string, data: Record<string, unknown>) {
84+
async create(o: string, data: Row) {
9185
const row = { ...data, id: String(data.id) };
92-
storeFor(o).set(row.id, row);
86+
rowsOf(o).push(row);
9387
return row;
9488
},
95-
async update(o: string, id: string, data: Record<string, unknown>) {
96-
const cur = storeFor(o).get(String(id));
97-
if (!cur) throw new Error(`not found ${o}/${id}`);
98-
const up = { ...cur, ...data, id: String(id) };
99-
storeFor(o).set(String(id), up);
100-
return up;
89+
async update(o: string, id: string, data: Row) {
90+
const rows = rowsOf(o);
91+
const at = rows.findIndex((r) => r.id === String(id));
92+
if (at < 0) throw new Error(`not found ${o}/${id}`);
93+
rows[at] = { ...rows[at], ...data, id: String(id) };
94+
return rows[at];
95+
},
96+
async upsert(o: string, data: Row) { return this.create(o, data); },
97+
async delete(o: string, id: string) {
98+
const rows = rowsOf(o);
99+
const at = rows.findIndex((r) => r.id === String(id));
100+
if (at < 0) return false;
101+
rows.splice(at, 1);
102+
return true;
101103
},
102-
async upsert(o: string, data: Record<string, unknown>) { return this.create(o, data); },
103-
async delete(o: string, id: string) { return storeFor(o).delete(String(id)); },
104104
async bulkCreate() { return []; }, async bulkUpdate() { return []; }, async bulkDelete() {},
105105
async beginTransaction() { log.begun += 1; return { id: `trx_${log.begun}` }; },
106106
async commit() {}, async rollback() {},
107107
};
108-
return { driver, stores, failReads, seed: (o: string, row: Record<string, unknown>) => storeFor(o).set(String(row.id), row) };
108+
return {
109+
driver,
110+
failReads,
111+
has: (o: string, id: string) => rowsOf(o).some((r) => r.id === id),
112+
seed: (o: string, row: Row) => void rowsOf(o).push(row),
113+
};
109114
}
110115

111116
/** The deleted object. Its own injected anchor makes it self-referencing, harmlessly. */
@@ -202,7 +207,7 @@ describe('[#21910] the cascade scan skips a federated object\'s injected tenant
202207
log.reads.length = 0;
203208
await engine.delete('sys_organization', { where: { id: ORG_ID } } as any);
204209

205-
expect(local.stores.get('sys_organization')?.has(ORG_ID)).toBe(false);
210+
expect(local.has('sys_organization', ORG_ID)).toBe(false);
206211
expect(log.reads).not.toContain('ext_customer');
207212
// CONTROL: the scan ran for this delete, and probed the local object's injected anchor.
208213
expect(log.reads).toContain('acct');
@@ -222,7 +227,7 @@ describe('[#21910] the cascade scan skips a federated object\'s injected tenant
222227
expect(err.code).toBe('INVALID_FILTER');
223228
expect(err.status).toBe(400);
224229
expect(log.reads).toContain('ext_tenant_customer');
225-
expect(local.stores.get('sys_organization')?.has(ORG_ID)).toBe(true);
230+
expect(local.has('sys_organization', ORG_ID)).toBe(true);
226231
});
227232

228233
it('still probes any other lookup the author declared on a federated object, and its failure propagates (#8895)', async () => {
@@ -237,7 +242,7 @@ describe('[#21910] the cascade scan skips a federated object\'s injected tenant
237242
expect(err.code).toBe('INVALID_FILTER');
238243
expect(err.status).toBe(400);
239244
expect(log.reads).toContain('ext_order');
240-
expect(local.stores.get('sys_organization')?.has(ORG_ID)).toBe(true);
245+
expect(local.has('sys_organization', ORG_ID)).toBe(true);
241246
});
242247
});
243248

@@ -247,7 +252,7 @@ describe('[#21910] the cascade atomicity plan agrees with the scan about who tak
247252

248253
await engine.delete('sys_organization', { where: { id: ORG_ID } } as any);
249254

250-
expect(local.stores.get('sys_organization')?.has(ORG_ID)).toBe(false);
255+
expect(local.has('sys_organization', ORG_ID)).toBe(false);
251256
expect(log.begun).toBe(1);
252257
expect(warnings.filter((w) => w.includes(NOT_ATOMIC))).toEqual([]);
253258
});
@@ -257,7 +262,7 @@ describe('[#21910] the cascade atomicity plan agrees with the scan about who tak
257262

258263
await engine.delete('sys_organization', { where: { id: ORG_ID } } as any);
259264

260-
expect(local.stores.get('sys_organization')?.has(ORG_ID)).toBe(false);
265+
expect(local.has('sys_organization', ORG_ID)).toBe(false);
261266
expect(log.reads).toContain('ext_order');
262267
expect(log.begun).toBe(0);
263268
expect(warnings.filter((w) => w.includes(NOT_ATOMIC))).toHaveLength(1);

0 commit comments

Comments
 (0)