Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/searchable-fields-stored-hint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
"@objectstack/lint": patch
---

fix(lint): `searchable-field-unknown` / `searchable-field-unsearchable` prescribe a **stored** mirror, not a formula (#6673)

Both authoring-time hints for a bad `searchableFields` entry told the author to
mirror a related record's value onto a formula field — a fix that can never
work:

- FROM (dotted-path entry, e.g. `project_id.name`): "…expand the relation and
search the related object, or copy the value onto **a formula field** here."
- TO: "…or copy the value onto **a stored text field** here."

- FROM (a `lookup`/`master_detail` column outside the allowed set): "…mirror
it onto **a text/formula** field here and declare that instead."
- TO: "…mirror it onto **a stored text** field here and declare that instead."

A `formula` field is virtual — no driver materializes a column for it
(`packages/objectql/src/engine.ts`, `driver-sql/src/schema-drift.ts`,
`driver-turso/src/remote-transport.ts`), so a `$contains` predicate against one
has nothing to scan. A CEL formula also only reads the record's own fields
(`record.<field>`), so it cannot fetch the related title in the first place.
Only the **stored** half of the old prescription ever worked; an author who
followed it verbatim got metadata that passed both lint and the `#4254`
runtime gate and then just never matched.

The corpus already prescribes the stored-field mirror everywhere else
(`content/docs/data-modeling/schema-design.mdx`, the `objectstack-data` and
`objectstack-ui` skills, PR #6670 / #6898) — this brings the tool's own hint
text into agreement with it.

Message text only — no schema, rule id, severity, or runtime behaviour change.
9 changes: 8 additions & 1 deletion packages/lint/src/validate-searchable-fields.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,10 @@ describe('validateSearchableFields — dotted paths', () => {
expect(findings).toHaveLength(1);
expect(findings[0].path).toBe('objects[0].searchableFields[1]');
expect(findings[0].hint).toContain("scans this object's own columns");
// The prescription must be a STORED field — a `formula` field is virtual
// (no driver materializes a column for it), so it can never be scanned.
expect(findings[0].hint).toContain('copy the value onto a stored text field');
expect(findings[0].hint).not.toContain('formula');
});
});

Expand Down Expand Up @@ -315,8 +319,11 @@ describe('validateSearchableFields — list views that narrow the set', () => {
expect(findings[0].message).toContain("type 'lookup'");
expect(findings[0].message).toContain('400 INVALID_FIELD');
// The lookup-specific prescription: search cannot cross objects, so the
// related record's title must be mirrored onto a local text/formula field.
// related record's title must be mirrored onto a local STORED text field —
// never a `formula` field, which is virtual and materializes no column.
expect(findings[0].hint).toContain('mirror');
expect(findings[0].hint).toContain('mirror it onto a stored text field');
expect(findings[0].hint).not.toContain('formula');
});

it('flags a real field outside the object\'s declared searchableFields', () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/lint/src/validate-searchable-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ export function checkSearchableFieldList(
(dotted
? `'search' scans this object's own columns, so a related record's ` +
`column cannot be a search target — expand the relation and search ` +
`the related object, or copy the value onto a formula field here. `
`the related object, or copy the value onto a stored text field here. `
: `Fix the name, or add "${name}" to ${objectName}.fields. `) +
`Clients echo this declaration verbatim as the '$searchFields' ` +
`override, so a stale entry becomes a 400 INVALID_FIELD on list ` +
Expand Down Expand Up @@ -411,7 +411,7 @@ export function checkSearchableFieldList(
(isReference
? `A ${meta?.type} column stores only the referenced record's id, so it ` +
`cannot be a keyword target — drop "${name}" from this view and, to ` +
`search by the related record's title, mirror it onto a text/formula ` +
`search by the related record's title, mirror it onto a stored text ` +
`field here and declare that instead. `
: `Drop "${name}" from this view, or target a text-like field instead. `) +
`Declaring 'searchableFields' on object "${objectName}" chooses the ` +
Expand Down
Loading