File size: 6,666 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
import { SelectQuery } from '../src/core/ast';
import { UserContext } from '../src/core/context';
import { PLAINTEXT_LOG_ENV, PLAINTEXT_LOG_ACK, projectSQLLog } from '../src/core/log-privacy';
import { AbstractSQLTeaQLClient, EntitySchema, 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 schemas: Record<string, EntitySchema> = {
  Customer: { table: 'customer_data', auditMaskFields: ['name'], columns: {
    id: column('id', true), version: column('version', true), name: column('name'), password: column('password'),
  }, relations: { orders: { targetEntity: 'Order', localKey: 'id', foreignKey: 'customerId', many: true } } },
  Order: { table: 'order_data', auditMaskFields: ['name'], columns: {
    id: column('id', true), version: column('version', true), customerId: column('customer_id', true), name: column('name'),
  }, relations: { lines: { targetEntity: 'Line', localKey: 'id', foreignKey: 'orderId', many: true } } },
  Line: { table: 'line_data', columns: {
    id: column('id', true), version: column('version', true), orderId: column('order_id', true),
  } },
};
class Driver extends SQLiteDriver {
  failingTable?: string;
  readonly failure = new Error('DRIVER-CANARY');
  readonly reads: Array<{ sql: string; values: any[] }> = [];
  async query(sql: string, values: any[] = []): Promise<SqlQueryResult> {
    if (sql.startsWith('SELECT')) {
      this.reads.push({ sql, values: [...values] });
      if (this.failingTable && sql.includes(this.failingTable)) throw this.failure;
    }
    return super.query(sql, values);
  }
  async *stream(sql: string, values: any[] = []): AsyncIterable<any> {
    this.reads.push({ sql, values: [...values] });
    yield* super.stream(sql, values);
  }
}
class Client extends AbstractSQLTeaQLClient {
  readonly dispatched: string[] = [];
  constructor(driver: Driver) { super(driver, schemas); }
  override async executeQuery<T = any>(query: any): Promise<T[]> {
    this.dispatched.push(query.entity);
    return super.executeQuery<T>(query);
  }
}
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, payload] of [
    ['Customer', { name: 'Riverside', password: 'PASSWORD-CANARY' }],
    ['Order', { customerId: '1', name: 'Lakeside' }],
    ['Line', { orderId: '1' }],
  ] as const) await client.executeMutation({ entity, action: 'Create', id: '1', payload, comment: 'seed relation fixture' });
  const logs: SQLExecutionMetadata[] = [];
  client.setDiagnosticSQLLogSink({ write: entry => logs.push(entry) });
  driver.reads.length = 0;
  return { client, driver, logs };
}
const previous = process.env[PLAINTEXT_LOG_ENV];
beforeEach(() => { delete process.env[PLAINTEXT_LOG_ENV]; });
afterEach(() => {
  if (previous === undefined) delete process.env[PLAINTEXT_LOG_ENV];
  else process.env[PLAINTEXT_LOG_ENV] = previous;
});

const cases = ['batch', 'probe', 'window', 'aggregate', 'nested', 'stream'].flatMap(shape =>
  [false, true].flatMap(debug => [false, true].map(failure => ({ shape, debug, failure }))));
test.each(cases)('relation intent $shape debug=$debug failure=$failure', async ({ shape, debug, failure }) => {
  const f = await fixture();
  try {
    if (debug) process.env[PLAINTEXT_LOG_ENV] = PLAINTEXT_LOG_ACK;
    if (failure) f.driver.failingTable = shape === 'nested' ? 'line_data' : 'order_data';
    const query = new SelectQuery('Customer').filter({ name: { $eq: 'Riverside' }, password: { $eq: 'PASSWORD-CANARY' } })
      .limit(1).comment('what: load Riverside PASSWORD-CANARY details').purpose('why: verify derived query intent');
    const child = new SelectQuery('Order');
    if (shape === 'probe') child.limit(1).topNProbeParentThreshold(32);
    if (shape === 'window') child.limit(1).topNProbeParentThreshold(0);
    if (shape === 'nested') child.filter({ name: { $eq: 'Lakeside' } }).relationQuery('lines', new SelectQuery('Line').limit(2));
    if (shape === 'aggregate') query.relationAggregate('orders', 'count', child);
    else if (shape === 'batch') query.relation('orders');
    else query.relationQuery('orders', child);
    const execute = async () => {
      if (shape !== 'stream') return f.client.executeQuery(query);
      const rows: any[] = [];
      for await (const chunk of f.client.executeForStream(query, 1)) rows.push(...chunk);
      return rows;
    };
    if (failure) await expect(execute()).rejects.toBe(f.driver.failure);
    else expect(await execute()).toHaveLength(1);
    expect(f.client.dispatched).toEqual(shape === 'stream' ? ['Order']
      : shape === 'nested' ? ['Customer', 'Order', 'Line'] : ['Customer', 'Order']);
    expect(f.logs).toHaveLength(shape === 'nested' ? 3 : 2);
    const entry = f.logs.find(item => item.parameterizedSQL.includes(shape === 'nested' ? 'line_data' : 'order_data'))!;
    expect(entry.comment).toContain('what: load');
    expect(entry.purpose).toBe('why: verify derived query intent');
    expect(entry.executionOutcome).toBe(failure ? 'failure' : 'success');
    expect(entry.comment?.includes('Riverside')).toBe(debug);
    expect(JSON.stringify(f.logs)).not.toContain('PASSWORD-CANARY');
    if (!debug) expect(JSON.stringify(f.logs)).not.toContain('Riverside');
    expect(entry.debugSQL).toContain(shape === 'nested' ? 'line_data' : 'order_data');
    expect(entry.parameters).toHaveLength(f.driver.reads[f.driver.reads.length - 1].values.length);
    expect(f.driver.reads[0].values).toContain('PASSWORD-CANARY');
    if (shape === 'batch') expect(entry.parameterizedSQL).toContain(' IN (');
    if (shape === 'window') expect(entry.parameterizedSQL).toMatch(/ROW_NUMBER\(\) OVER/i);
    if (shape === 'probe') {
      expect(entry.parameterizedSQL).not.toContain(' IN (');
      expect(entry.parameterizedSQL).not.toMatch(/ROW_NUMBER/i);
    }
    delete process.env[PLAINTEXT_LOG_ENV];
    expect(JSON.stringify(projectSQLLog(entry))).not.toMatch(/Riverside|PASSWORD-CANARY/);
    f.driver.failingTable = undefined;
    await f.client.executeQuery(new SelectQuery('Customer').limit(1)
      .comment('what: independent Riverside').purpose('why: verify provenance isolation'));
    expect(f.logs[f.logs.length - 1].comment).toBe('what: independent Riverside');
  } finally { await f.client.close(); }
});