File size: 8,217 Bytes
aa89cb5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
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(); }
});