Chart builder: escape ./[/] in built field names

This commit is contained in:
2026-06-11 19:21:28 +03:00
parent d666599a58
commit 983c052f3b
2 changed files with 31 additions and 7 deletions
+10 -7
View File
@@ -27,6 +27,7 @@ import { DISTINCT_CAP } from './profile';
import type { ColumnType } from './type-inference';
import { VEGA_LITE_SCHEMA_URL } from './snippet';
import { validateExpression } from './expr-validate';
import { escapeVegaField } from './rendering';
/** The five mark types the builder offers, in selector order (spec §06). */
export const MARK_TYPES = ['bar', 'line', 'point', 'area', 'circle'] as const;
@@ -792,12 +793,12 @@ function coerceFilterValue(value: string, fieldType: FieldType): string | number
* predicate in a `{ not }` (Vega-Lite has no bare inequality predicate).
*/
function predicateObject(filter: BuilderFilter): Record<string, unknown> | null {
const { field, op } = filter;
if (!field || !op) return null;
// TODO: the data-derived column name reaches `field:` unescaped here, as it also
// does in encodingObject. A column whose name contains `.`/`[`/`]` is then read as
// a nested-property accessor rather than a literal field. core/rendering.ts exports
// escapeVegaField for exactly this but nothing currently wires it into the builder.
const { op } = filter;
if (!filter.field || !op) return null;
// Escape `.`/`[`/`]` so a column literally named e.g. `user.age` is read as that
// field, not a nested-property accessor (same convention as encodingObject and the
// renderer — docs/architecture/05 §4).
const field = escapeVegaField(filter.field);
const type = filter.fieldType ?? 'nominal';
const value = filter.value ?? '';
const coerce = (v: string) => coerceFilterValue(v, type);
@@ -938,7 +939,9 @@ function encodingObject(mapping: ChannelMapping): Record<string, unknown> {
return { aggregate: 'count', type: 'quantitative' };
}
const enc: Record<string, unknown> = {};
if (mapping.field !== undefined) enc.field = mapping.field;
// Escape `.`/`[`/`]` so a column literally named e.g. `user.age` is read as that
// field, not a nested-property accessor (docs/architecture/05 §4).
if (mapping.field !== undefined) enc.field = escapeVegaField(mapping.field);
enc.type = mapping.type;
if (mapping.aggregate) enc.aggregate = mapping.aggregate;
if (mapping.bin) enc.bin = true;