Skip to content

Commit 18f7a01

Browse files
committed
test(objectql): pin the $search skip on the auto branch (searchable: true)
#29385 landed: with no searchableFields, a field marked searchable: true joins the auto set of any type. A flagged number / date / json field is admitted to the set and still gets no text clause; the text control is scanned, and naming a flagged field is accepted and matches nothing. On the engine (AST) and the SQLite Data API door. Claude-Session: https://claude.ai/code/session_01ADHkmRnyoAJD6oToQ2QZUK Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9cd8fa4 commit 18f7a01

2 files changed

Lines changed: 58 additions & 3 deletions

File tree

‎packages/objectql/src/engine-search-non-text-column.test.ts‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type Row = Record<string, unknown>;
4040

4141
const OBJECT = 'search_non_text_29394';
4242
const ONLY_NON_TEXT = 'search_only_non_text_29394';
43+
const AUTO_FLAGGED = 'search_auto_flagged_29394';
4344

4445
const FIELDS = {
4546
name: { type: 'text' },
@@ -101,7 +102,7 @@ function makeDriver() {
101102
const reads: Array<{ object: string; where: unknown }> = [];
102103
const read = (object: string, ast: any) => {
103104
reads.push({ object, where: ast?.where });
104-
const rows = (object === OBJECT || object === ONLY_NON_TEXT ? ROWS : []).filter((r) => matches(r, ast?.where));
105+
const rows = ([OBJECT, ONLY_NON_TEXT, AUTO_FLAGGED].includes(object) ? ROWS : []).filter((r) => matches(r, ast?.where));
105106
return typeof ast?.limit === 'number' ? rows.slice(0, ast.limit) : rows;
106107
};
107108
const driver: any = {
@@ -144,6 +145,19 @@ describe('[#29394] $search emits no text clause over a never-string-capable colu
144145
searchableFields: ['qty', 'born', 'active', 'meta'],
145146
fields: FIELDS,
146147
}, 'test');
148+
// The AUTO branch (ruling 6573569376 letter A): no `searchableFields`, so
149+
// every field marked `searchable: true` joins the set, of any type. The
150+
// resolver admits the flagged number / date / json fields; the expansion
151+
// must still emit no text clause for them.
152+
registry.registerObject({
153+
name: AUTO_FLAGGED,
154+
fields: {
155+
name: { type: 'text' },
156+
qty: { type: 'number', searchable: true },
157+
born: { type: 'date', searchable: true },
158+
meta: { type: 'json', searchable: true },
159+
},
160+
}, 'test');
147161
registry.registerObject({
148162
...TEXT_OPERATOR_DOOR_FIXTURE,
149163
// Every fixture field the declared branch keeps: one per FieldType member.
@@ -208,6 +222,19 @@ describe('[#29394] $search emits no text clause over a never-string-capable colu
208222
expect(scannedFields(lastWhere())).toEqual(['name']);
209223
});
210224

225+
it('the auto branch: flagged searchable: true number / date / json fields get no clause; the text control is still scanned', async () => {
226+
const rows = await engine.find(AUTO_FLAGGED, { search: 'qq9' });
227+
expect(scannedFields(lastWhere(AUTO_FLAGGED))).toEqual(['name']);
228+
expect(rows.map((r) => r.id)).toEqual(['r1', 'r2']);
229+
for (const term of ['42', '2026']) {
230+
expect(await engine.find(AUTO_FLAGGED, { search: term }), term).toEqual([]);
231+
}
232+
// The flag still admits them into the set: naming one is honoured, not
233+
// refused, and answers no rows.
234+
expect(await engine.find(AUTO_FLAGGED, { search: '42', searchFields: ['qty'] })).toEqual([]);
235+
expect(lastWhere(AUTO_FLAGGED)).toEqual({ $or: [] });
236+
});
237+
211238
it('control: an override outside the declared set is still refused, judged on the unchanged set', async () => {
212239
await expect(engine.find(OBJECT, { search: 'qq9', searchFields: ['nope'] }))
213240
.rejects.toMatchObject({ status: 400, code: 'INVALID_FIELD', param: 'searchFields', field: 'nope' });

‎packages/rest/src/data-search-non-text-column-door.test.ts‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { installDefaultOrganization } from './default-organization.testkit.js';
3333

3434
const MIXED = 'rest_search_non_text_mixed_29394';
3535
const ONLY_NON_TEXT = 'rest_search_non_text_only_29394';
36+
const AUTO_FLAGGED = 'rest_search_non_text_auto_29394';
3637

3738
const FIELDS = {
3839
name: { name: 'name', type: 'text' as const },
@@ -83,11 +84,25 @@ describe('[#29394] $search over a declared non-text column on the Data API door
8384
searchableFields: ['qty', 'born', 'active', 'meta'],
8485
fields: FIELDS,
8586
} as any);
87+
// The auto branch: no `searchableFields`, flagged non-text fields join the
88+
// set (ruling 6573569376 letter A) and must still contribute no clause.
89+
engine.registry.registerObject({
90+
name: AUTO_FLAGGED, label: 'Auto flagged 29394',
91+
fields: {
92+
name: FIELDS.name,
93+
qty: { ...FIELDS.qty, searchable: true },
94+
born: { ...FIELDS.born, searchable: true },
95+
meta: { ...FIELDS.meta, searchable: true },
96+
},
97+
} as any);
8698
await engine.syncSchemas();
8799
// A real `single` install's Default Organization (ADR-0131 D9, D1).
88100
await installDefaultOrganization(engine);
89-
for (const object of [MIXED, ONLY_NON_TEXT]) {
90-
for (const row of ROWS) await engine.insert(object, { ...row } as any);
101+
for (const object of [MIXED, ONLY_NON_TEXT, AUTO_FLAGGED]) {
102+
for (const row of ROWS) {
103+
const { active: _active, ...rest } = row;
104+
await engine.insert(object, (object === AUTO_FLAGGED ? rest : { ...row }) as any);
105+
}
91106
}
92107

93108
const protocol = new ObjectStackProtocolImplementation(engine as any);
@@ -158,6 +173,19 @@ describe('[#29394] $search over a declared non-text column on the Data API door
158173
expect(ids(both.body)).toEqual(['r1', 'r2']);
159174
});
160175

176+
it('the auto branch: flagged searchable: true number / date / json fields add no clause — the text column matches, 200', async () => {
177+
const got = await list(AUTO_FLAGGED, { $search: 'qq9' });
178+
expect(got.status, JSON.stringify(got.body)).toBe(200);
179+
expect(ids(got.body)).toEqual(['r1', 'r2']);
180+
const rendered = await list(AUTO_FLAGGED, { $search: '2026' });
181+
expect(rendered.status, JSON.stringify(rendered.body)).toBe(200);
182+
expect(ids(rendered.body)).toEqual([]);
183+
// The flag admits the field at the ingress door: naming it is accepted, and matches nothing.
184+
const named = await list(AUTO_FLAGGED, { $search: 'qq9', $searchFields: 'meta' });
185+
expect(named.status, JSON.stringify(named.body)).toBe(200);
186+
expect(ids(named.body)).toEqual([]);
187+
});
188+
161189
it('control: the caller\'s own text operator on a non-text column is still refused by the #15661 door', async () => {
162190
const posted = await query(MIXED, { where: { qty: { $contains: '4' } } });
163191
expect(posted.status, JSON.stringify(posted.body)).toBe(400);

0 commit comments

Comments
 (0)