/** * Computed-fields expression language (v1 — id-only). * * Grammar * ------- * ``` * expression := term (("+" | "-") term)* * term := factor (("*" | "/" | "%") factor)* // "*" = multiply * factor := integer-literal | path | function-call | "(" expression ")" * path := id ( "." id )* // "." = link/binding hop * function := "sum" | "count" | "avg" | "min" | "max" "(" expression ")" * ``` * * Every symbol is a numeric field `id` (per-table dense counter, nested * children included). There are no decimal literals: `.` is reserved as the * path separator, so `3.4` is a link hop (`field 3`, then `field 4` in the * table `field 3` links to), never the decimal 3.4. Fractional results are * reachable via division (`314 / 100` → 3.14). * * A bare integer that **matches a field id in the table's schema** is that * field's value (ids are locative); a bare integer that matches no field id * is an integer literal. This is what makes both `sum(4 * 3.4)` (id 4 = * quantity) and `314 / 100` (no such ids) usable. * * Compiled expressions are persisted with the schema as * `{ expr: string, ast: CompiledExpressionNode }` so key/table renames never * retarget an expression: the AST carries only field ids (+ the id of the * array ancestor a helper iterates), and every key path is re-derived from * the *current* schema at write time. */ import type { ErrorLang, Field, Schema } from "./index.js"; /** Cap on the raw `computed` string length (bound work at validation). */ export declare const COMPUTED_EXPR_MAX_LENGTH = 512; /** Cap on the parsed AST depth (protects the parser/evaluator recursion). */ export declare const COMPUTED_EXPR_MAX_DEPTH = 64; export type ComputedFunctionName = "sum" | "count" | "avg" | "min" | "max"; export type BinaryOp = "add" | "sub" | "mul" | "div" | "mod"; /** Raw AST produced by {@link parseExpression} (field ids unresolved). */ export type RawExpressionNode = { kind: "num"; value: number; } | { kind: "path"; ids: number[]; } | { kind: "bin"; op: BinaryOp; left: RawExpressionNode; right: RawExpressionNode; } | { kind: "fn"; name: ComputedFunctionName; arg: RawExpressionNode; }; /** * Compiled AST stored in the schema. Path nodes carry only ids (+ the id of * the array ancestor a helper iterates); all key paths are resolved against * the current schema at evaluation time, so renames never break expressions. */ export type CompiledExpressionNode = { kind: "num"; value: number; } | { kind: "path"; ids: number[]; arrayFieldId: number | null; } | { kind: "bin"; op: BinaryOp; left: CompiledExpressionNode; right: CompiledExpressionNode; } | { kind: "fn"; name: ComputedFunctionName; /** id of the array ancestor every path in `arg` lives in. */ arrayFieldId: number; arg: CompiledExpressionNode; }; /** Persisted form of a `computed` schema property. (A type alias so it stays * assignable to Inison's recursive `Data` type.) */ export type ComputedFieldSpec = { expr: string; ast: CompiledExpressionNode; }; /** * Schema index used for id resolution: maps a field id to its dotted key path, * the resolved field, and the nearest array-of-objects ancestor (if any). */ export interface FieldRef { key: string; field: Field; /** Nearest `array`-typed ancestor with object children, or null. */ arrayAncestor: { id: number; key: string; } | null; /** True when `arrayAncestor` itself sits inside another array of objects. */ nestedInArrayOfArrays: boolean; } /** * Parse a `computed` expression into a raw AST. Throws * `COMPUTED_FIELD_SYNTAX` on invalid syntax, oversized input or excessive * nesting. */ export declare function parseExpression(source: string, language?: ErrorLang, fieldKey?: string): RawExpressionNode; /** * Build the id → {@link FieldRef} index of a schema (nested children included, * using dotted key paths). Container fields (array/object with object * children) are indexed too so unknown ids are detected, but they can't be * referenced by an expression. */ export declare function buildFieldIndex(schema: Schema): Map; export interface ResolveContext { language: ErrorLang; /** Key of the computed field being compiled (error context). */ ownKey: string; /** Id index of the table the computed field belongs to. */ index: Map; /** Fetch (and cache) the id index of another table, or undefined. */ getTableIndex: (tableName: string) => Promise | undefined>; /** * When set, the expression being compiled belongs to an *element* of the * array-of-objects field with this id (a child computed field). Every * referenced field must be a sibling child of that same array; top-level * fields and helpers are rejected. */ elementContext?: { arrayRootId: number; } | null; } /** * Resolve a raw expression against the schema (and linked tables), returning * the compiled node plus the set of field ids it reads from the current * table (used for dependency ordering / cycle detection between computed * fields). */ export declare function resolveExpression(expression: RawExpressionNode, ctx: ResolveContext): Promise<{ ast: CompiledExpressionNode; deps: Set; }>; /** Every field id a compiled expression reads from the *current* table. */ export declare function collectFieldDeps(node: CompiledExpressionNode): Set; export interface ComputedFieldMeta { id: number; key: string; deps: Set; } /** * Order computed fields so every dependency is evaluated before its dependents. * Throws `COMPUTED_FIELD_CYCLE` when a field (transitively) depends on itself. */ export declare function topoSortComputedFields(fields: ComputedFieldMeta[], language: ErrorLang): ComputedFieldMeta[]; /** * Flatten a formatted row into a dot-notation record. Objects are flattened * (`meta.age`), arrays of objects are combined per child key * (`items.quantity` -> array of per-element values) and everything else is * stored under its dotted key. */ export declare function flattenRecord(obj: Record, prefix?: string): Record; /** * Resolve a dotted key path against a structured frame in place — the direct * read-path counterpart of `flattenRecord`. Walks `obj` segment by segment * and returns `undefined` when any intermediate is null, undefined, a * non-object, or an array (arrays resolve numerically only, so they never * match a dotted segment), mirroring the leaves `flattenRecord` would have * produced without materialising the flattened record. */ export declare function resolveFramePath(obj: Record, dottedKey: string): any;