browser-sqlite-playground / source /teaql-ts /tests /sql-relation-membership.test.ts
philipgreat's picture
Deploy traceable order management playground
aa89cb5 verified
Raw History Blame Contribute Delete
8.22 kB
import { SelectQuery, OrderBy } from '../src/core/ast';
import { UserContext } from '../src/core/context';
import { AbstractSQLTeaQLClient, EntitySchema, SQLExecutionEvidenceStore, SQLExecutionMetadata, SqlQueryResult } from '../src/sql/core';
import { SQLiteDriver } from '../src/sql/sqlite';
const column = (columnName: string, integer = false) => ({ columnName,
logicalType: integer ? 'integer' as const : 'text' as const,
decode: integer ? 'string' as const : 'native' as const, logPolicy: 'plain' as const });
const identity = { id: column('id', true), version: column('version', true) };
const model: Record<string, EntitySchema> = {
Owner: { table: 'owner_data', auditMaskFields: [], columns: identity,
relations: { parents: { targetEntity: 'Parent', localKey: 'id', foreignKey: 'ownerId', many: true } } },
Parent: { table: 'parent_data', auditMaskFields: ['name'], columns: {
...identity, ownerId: column('owner_id', true), code: column('code'), name: column('name'),
}, relations: { children: { targetEntity: 'Child', localKey: 'code', foreignKey: 'parentRef', many: true } } },
Child: { table: 'child_data', auditMaskFields: [], columns: { ...identity, parentRef: column('parent_ref') },
relations: {
parentRef: { targetEntity: 'Parent', localKey: 'parentRef', foreignKey: 'code', many: false },
parentAgain: { targetEntity: 'Parent', localKey: 'parentRef', foreignKey: 'code', many: false },
} },
};
class Driver extends SQLiteDriver {
reads = 0;
async query(sql: string, values: any[] = []): Promise<SqlQueryResult> {
if (sql.startsWith('SELECT')) this.reads++;
return super.query(sql, values);
}
async *stream(sql: string, values: any[] = []): AsyncIterable<any> {
this.reads++;
yield* super.stream(sql, values);
}
}
class Client extends AbstractSQLTeaQLClient {
constructor(driver: Driver, schemas = model) { super(driver, schemas); }
}
async function fixture() {
const driver = new Driver(':memory:');
const client = new Client(driver).setDiagnosticSQLLogSink(undefined);
await new UserContext().insertResource('dataService', client).ensureSchema();
for (const [entity, id, payload] of [
['Owner', '1', {}],
['Parent', '1', { ownerId: '1', code: 'P-A', name: 'PRIVATE-PARENT' }],
['Parent', '2', { ownerId: '1', code: 'P-B', name: 'PRIVATE-EMPTY' }],
['Child', '1', { parentRef: 'P-A' }], ['Child', '2', { parentRef: 'P-A' }],
] as const) await client.executeMutation({ entity, id, action: 'Create', payload,
comment: 'seed native relation membership fixture' });
const evidence = new SQLExecutionEvidenceStore();
const logs: SQLExecutionMetadata[] = [];
client.setRuntimeTelemetrySink(evidence);
client.setDiagnosticSQLLogSink({ write: entry => logs.push(entry) });
driver.reads = 0;
return { client, driver, evidence, logs };
}
const cases = ['list', 'stream-full', 'stream-tail'].flatMap(mode =>
[false, true].flatMap(nested => [false, true].flatMap(logging =>
['scalar-control', 'visible', 'filtered', 'filtered-sibling'].map(shape => ({ mode, nested, logging, shape })))));
test.each(cases)('membership $mode nested=$nested logging=$logging $shape', async ({ mode, nested, logging, shape }) => {
const f = await fixture();
try {
f.client.setQueryLoggingEnabled(logging);
const child = new SelectQuery('Child').order(OrderBy.asc('id')).limit(10).topNProbeParentThreshold(0)
.relationAggregate('parentRef', 'parentCount', new SelectQuery('Parent'));
if (shape !== 'scalar-control') child.relationQuery('parentRef', new SelectQuery('Parent')
.filter({ name: { $eq: shape === 'visible' ? 'PRIVATE-PARENT' : 'NOT-VISIBLE' } }));
if (shape === 'filtered-sibling') child.relationQuery('parentAgain', new SelectQuery('Parent'));
const parents = new SelectQuery('Parent').order(OrderBy.asc('id')).limit(2).topNProbeParentThreshold(0)
.relationQuery('children', child);
const query = (nested ? new SelectQuery('Owner').limit(1).relationQuery('parents', parents) : parents)
.comment('load parent membership').purpose('verify scalar keys across relation hydration');
const rows: any[] = [];
if (mode === 'list') rows.push(...await f.client.executeQuery(query));
else for await (const chunk of f.client.executeForStream(query, mode === 'stream-full' ? 1 : 3)) rows.push(...chunk);
const loaded = nested ? rows[0].parents : rows;
expect(loaded).toHaveLength(2);
expect(loaded[0].children).toHaveLength(2);
expect(loaded[1].children).toEqual([]);
for (const row of loaded[0].children) {
expect(row.parentCount).toBe(1);
if (shape === 'scalar-control') expect(row.parentRef).toBe('P-A');
else if (shape === 'visible') expect(row.parentRef.code).toBe('P-A');
else {
expect(row.parentRef).toEqual({ code: 'P-A' });
expect(Object.prototype.hasOwnProperty.call(row.parentRef, 'name')).toBe(false);
}
if (shape === 'filtered-sibling') expect(row.parentAgain.code).toBe('P-A');
}
const expected = 4 + Number(nested) + Number(mode === 'stream-full' && !nested)
- Number(shape === 'scalar-control') + Number(shape === 'filtered-sibling');
expect(f.driver.reads).toBe(expected);
const entries = f.evidence.snapshot();
expect(entries).toHaveLength(expected);
expect(f.logs).toHaveLength(logging ? expected : 0);
{
expect(entries.every(e => e.comment === 'load parent membership' && e.purpose === query.purposeText)).toBe(true);
expect(JSON.stringify(entries)).not.toContain('PRIVATE-PARENT');
const count = entries.find(e => /COUNT\(/i.test(e.parameterizedSQL))!;
expect(count.tracePath.map(frame => [frame.kind, frame.name])).toEqual([
['operation', query.entity], ['request', query.entity],
...(nested ? [['relation', 'parents']] : []), ['relation', 'children'], ['relation', 'parentRef'],
['provider', 'sqlite'], ['sql', 'select'],
]);
}
const plain = await f.client.executeQuery<any>(new SelectQuery('Child').limit(2)
.comment('independent request').purpose('verify no assembly state escapes'));
expect(plain.map(row => row.parentRef)).toEqual(['P-A', 'P-A']);
expect(plain.map(row => Object.keys(row).sort())).toEqual([['id', 'parentRef', 'version'], ['id', 'parentRef', 'version']]);
const final = f.evidence.snapshot();
expect(final[final.length - 1].tracePath.map(frame => frame.kind))
.toEqual(['operation', 'request', 'provider', 'sql']);
} finally { await f.client.close(); }
});
test('ID reference keeps identity while actual SQL NULL stays null', async () => {
const driver = new Driver(':memory:');
const client = new Client(driver, {
Parent: model.Parent,
Child: { ...model.Child, columns: { ...identity, parentRef: column('parent_ref', true) },
relations: { parent: { targetEntity: 'Parent', localKey: 'parentRef', foreignKey: 'id', many: false } } },
}).setDiagnosticSQLLogSink(undefined);
try {
await new UserContext().insertResource('dataService', client).ensureSchema();
for (const [entity, id, payload] of [
['Parent', '1', { name: 'visible' }],
['Child', '1', { parentRef: '1' }], ['Child', '2', { parentRef: null }],
] as const) await client.executeMutation({ entity, id, action: 'Create', payload,
comment: 'seed nullable ID reference' });
const plain = await client.executeQuery<any>(new SelectQuery('Child').select(['id']).limit(2)
.comment('load identity only').purpose('prove unrequested relation stays absent'));
expect(plain.every(row => !Object.prototype.hasOwnProperty.call(row, 'parent'))).toBe(true);
const rows = await client.executeQuery<any>(new SelectQuery('Child').select(['id'])
.order(OrderBy.asc('id')).limit(2).relationQuery('parent', new SelectQuery('Parent')
.filter({ name: { $eq: 'absent' } }))
.comment('load filtered ID references').purpose('distinguish NotLoaded details from SQL NULL'));
expect(rows[0].parent).toEqual({ id: '1' });
expect(rows[0].parentRef).toBe('1');
expect(rows[1].parent).toBeNull();
expect(rows[1].parentRef).toBeNull();
} finally { await client.close(); }
});