Skip to content

Commit 8c8cf98

Browse files
committed
wip(spec,lint): one cross-field comparison classification, read by the RLS and sharing-rule authoring arms (#20347)
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
1 parent eee0974 commit 8c8cf98

10 files changed

Lines changed: 1471 additions & 6 deletions
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#20347] PARITY: this driver's cross-field comparison boundary answers
5+
* exactly what `crossFieldComparisonVerdict` (`@objectstack/spec/data`) answers,
6+
* on every pair of declared columns.
7+
*
8+
* The spec's classification was LIFTED from this driver's module-private
9+
* `crossFieldComparisonClass` (the #5222 boundary) so the authoring door and
10+
* the write check could read one definition. Lifting it made a second copy
11+
* for as long as the driver keeps its own, so this file holds the two equal:
12+
* one object declaring every `FieldType` member once (`f_<type>`) plus every
13+
* multi-capable member flagged `multiple: true` (`m_<type>`), and every
14+
* ordered pair of those columns compiled as `{ a: { $eq: { $field: b } } }`.
15+
*
16+
* The driver's verdict is read from what it DOES, never from its prose: the
17+
* pair compiles and runs (admitted), or it is refused in the withheld
18+
* `INVALID_FILTER` / 400 envelope the cross-field boundary raises (#7929 —
19+
* `withheldFilterDiagnosticOf` answers non-null only for that family). Any
20+
* other outcome fails the case: it means the pair never reached the class
21+
* question, and a parity claim over it would be a claim about nothing. The
22+
* fixture keeps the boundary's other refusals out by construction — every
23+
* column is declared, no reference is dotted, and no tenant-isolation column
24+
* is compared.
25+
*
26+
* Only `$eq` is driven: the class question is asked once per comparison,
27+
* before the operator is read, for all six operators the boundary compiles
28+
* (`sql-driver-cross-field-reference.test.ts` pins the operator matrix).
29+
*
30+
* The engine lane's rewire of this driver onto the spec export keeps this file
31+
* green by construction; until then it is the proof the lift changed nothing.
32+
*/
33+
34+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
35+
import { SqlDriver, withheldFilterDiagnosticOf } from './index.js';
36+
import {
37+
FieldType,
38+
MULTI_CAPABLE_TYPES,
39+
REFERENCE_VALUE_TYPES,
40+
crossFieldComparisonVerdict,
41+
type FilterCondition,
42+
} from '@objectstack/spec/data';
43+
44+
const OBJ = 'cfc_parity_probe';
45+
46+
interface ProbeColumn {
47+
name: string;
48+
type: string;
49+
multiple?: boolean;
50+
}
51+
52+
const columns: ProbeColumn[] = [
53+
...FieldType.options.map((type) => ({ name: `f_${type}`, type })),
54+
...[...MULTI_CAPABLE_TYPES].map((type) => ({ name: `m_${type}`, type, multiple: true })),
55+
];
56+
57+
/** A declaration the driver's DDL accepts for each probe column. */
58+
function declarationOf(c: ProbeColumn): Record<string, unknown> {
59+
const decl: Record<string, unknown> = { name: c.name, type: c.type };
60+
if (c.multiple) decl.multiple = true;
61+
if (REFERENCE_VALUE_TYPES.has(c.type)) decl.reference = OBJ;
62+
if (c.type === 'formula') decl.expression = '1';
63+
return decl;
64+
}
65+
66+
type Observed = 'admitted' | 'refused';
67+
68+
describe('[#20347] driver-sql cross-field boundary ⇔ crossFieldComparisonVerdict, every declared pair', () => {
69+
let driver: SqlDriver;
70+
71+
beforeAll(async () => {
72+
driver = new SqlDriver({
73+
client: 'better-sqlite3',
74+
connection: { filename: ':memory:' },
75+
useNullAsDefault: true,
76+
});
77+
await driver.initObjects([
78+
{
79+
name: OBJ,
80+
fields: Object.fromEntries([
81+
['id', { name: 'id', type: 'text' }],
82+
...columns.map((c) => [c.name, declarationOf(c)] as const),
83+
]),
84+
} as never,
85+
]);
86+
});
87+
88+
afterAll(async () => {
89+
await driver.disconnect();
90+
});
91+
92+
async function observe(target: string, ref: string): Promise<Observed> {
93+
const where = { [target]: { $eq: { $field: ref } } } as FilterCondition;
94+
try {
95+
await driver.find(OBJ, { fields: ['id'], where });
96+
return 'admitted';
97+
} catch (e) {
98+
const err = e as { code?: unknown; status?: unknown };
99+
// The ADR-0112 envelope AND the cross-field boundary's own withheld form:
100+
// anything else never reached the class question.
101+
expect({ code: err.code, status: err.status }, `${target} vs ${ref}: ${String(e)}`)
102+
.toEqual({ code: 'INVALID_FILTER', status: 400 });
103+
expect(withheldFilterDiagnosticOf(e), `${target} vs ${ref}: not the cross-field boundary's refusal`)
104+
.not.toBeNull();
105+
return 'refused';
106+
}
107+
}
108+
109+
it('the probe covers every FieldType member and every multi-capable member flagged multiple', () => {
110+
expect(columns.filter((c) => !c.multiple).map((c) => c.type).sort()).toEqual([...FieldType.options].sort());
111+
expect(columns.filter((c) => c.multiple).map((c) => c.type).sort()).toEqual([...MULTI_CAPABLE_TYPES].sort());
112+
});
113+
114+
for (const target of columns) {
115+
it(`${target.name} against every declared column`, async () => {
116+
const mismatches: string[] = [];
117+
for (const ref of columns) {
118+
const verdict = crossFieldComparisonVerdict(target, ref).verdict;
119+
const expected: Observed = verdict === 'comparable' ? 'admitted' : 'refused';
120+
const observed = await observe(target.name, ref.name);
121+
if (observed !== expected) {
122+
mismatches.push(`${target.name} vs ${ref.name}: spec says ${verdict}, driver ${observed}`);
123+
}
124+
}
125+
expect(mismatches).toEqual([]);
126+
});
127+
}
128+
});

0 commit comments

Comments
 (0)