@objectstack/service-analytics 17.1.0 → 17.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +195 -0
- package/README.md +5 -3
- package/dist/index.cjs +179 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +86 -13
- package/dist/index.d.ts +86 -13
- package/dist/index.js +179 -20
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/analytics-service.ts","../src/cube-registry.ts","../src/strategies/filter-normalizer.ts","../src/comparand-shape.ts","../src/like-pattern.ts","../src/read-scope-sql.ts","../src/dataset-refusal.ts","../src/strategies/native-sql-strategy.ts","../src/strategies/objectql-strategy.ts","../src/strategies/cross-object-rebucket.ts","../src/dataset-compiler.ts","../src/dataset-executor.ts","../src/dimension-labels.ts","../src/preview-evaluator.ts","../src/plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n// Core service\nexport { AnalyticsService } from './analytics-service.js';\nexport type { AnalyticsServiceConfig } from './analytics-service.js';\n\n// Kernel plugin\nexport { AnalyticsServicePlugin } from './plugin.js';\nexport type { AnalyticsServicePluginOptions } from './plugin.js';\n\n// Cube registry\nexport { CubeRegistry } from './cube-registry.js';\n\n// Dataset semantic layer (ADR-0021)\nexport { compileDataset } from './dataset-compiler.js';\nexport type {\n CompiledDataset,\n DatasetCompileOptions,\n DerivedMeasureSpec,\n RelationshipResolver,\n RelationshipTarget,\n} from './dataset-compiler.js';\n\nexport {\n resolveDimensionLabels,\n pickDisplayField,\n createOrderLabelResolver,\n withLabelFetchCache,\n} from './dimension-labels.js';\nexport type { DimensionLabelDeps, FieldMetaLite, OrderLabelResolver } from './dimension-labels.js';\nexport {\n DatasetExecutor,\n evaluateDerivedMeasures,\n combineFilters,\n shiftRange,\n mergeByDimensions,\n fillEmptyGroups,\n} from './dataset-executor.js';\nexport type { DatasetSelection, CompareTo } from './dataset-executor.js';\nexport { compileScopedFilterToSql } from './read-scope-sql.js';\n\n// Strategies\nexport { NativeSQLStrategy } from './strategies/native-sql-strategy.js';\nexport { ObjectQLStrategy } from './strategies/objectql-strategy.js';\nexport type { AnalyticsStrategy, StrategyContext, AnalyticsDriverCapabilities } from './strategies/types.js';\n\n// Note: InMemoryStrategy is exported from @objectstack/driver-memory\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n IAnalyticsService,\n AnalyticsQuery,\n AnalyticsResult,\n CubeMeta,\n DatasetSelection,\n} from '@objectstack/spec/contracts';\nimport { percentScaleOf, type Cube, type FilterCondition } from '@objectstack/spec/data';\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\nimport type { Dataset } from '@objectstack/spec/ui';\n// [#6761] The ONE shared `I18nLabel → string` resolver (#6765, maintainer\n// ruling B). Imported, never re-implemented: a private twin here is exactly the\n// fork the ruling exists to prevent — it would render the same authored map\n// differently from objectui's `pickLocalized` with neither end erroring.\nimport { resolveI18nLabel } from '@objectstack/spec/ui';\nimport type { Logger } from '@objectstack/spec/contracts';\nimport { createLogger, getEnv, bucketKeyToCalendarRange, zonedDateStartToUtcMs } from '@objectstack/core';\n// [#6615] The Postgres `\"x\" of relation \"y\"` phrase, owned once. This is the\n// only reason this package depends on `@objectstack/types` — see the module's\n// docblock for why the edge is acyclic and why it was worth adding.\nimport { matchMissingColumnOfRelation } from '@objectstack/types';\nimport { CubeRegistry } from './cube-registry.js';\nimport type { AnalyticsStrategy, AnalyticsDriverCapabilities, StrategyContext } from './strategies/types.js';\nimport { NativeSQLStrategy } from './strategies/native-sql-strategy.js';\nimport { ObjectQLStrategy } from './strategies/objectql-strategy.js';\n// [#5669] The `where` source-field gate reads the filter tree through the SAME\n// pair the strategies compile it with, so \"the field the gate saw\" and \"the\n// column that reached SQL\" cannot be two different things.\nimport {\n normalizeAnalyticsFilterTree,\n collectFilterLeaves,\n lowerAnalyticsWhere,\n conjunctFieldKeys,\n} from './strategies/filter-normalizer.js';\nimport { findCrossFieldComparand } from './comparand-shape.js';\nimport { compileDataset, type CompiledDataset, type RelationshipResolver } from './dataset-compiler.js';\nimport { DatasetExecutor, resolveDimensionGranularity, type DateGranularityValue } from './dataset-executor.js';\nimport {\n resolveDimensionLabels,\n createOrderLabelResolver,\n withLabelFetchCache,\n type DimensionLabelDeps,\n} from './dimension-labels.js';\nimport { evaluateAnalyticsQueryOverRows } from './preview-evaluator.js';\n// [#5918] The measure mint refuses a dotted member through the SAME constructor\n// the strategies' member-level refusals use — `INVALID_FIELD` / 400, naming the\n// member as the request spelled it (see `dataset-refusal.ts`'s header for why\n// that code and not `DATASET_INVALID`).\nimport { invalidMemberError } from './dataset-refusal.js';\n\n/**\n * Analytics result augmented with drill-through metadata (ADR-0021 D2; see\n * queryDataset). Carried alongside `rows` so the host can drill a clicked bucket\n * back to the underlying records without the renderer knowing field mappings.\n */\ntype AnalyticsResultWithDrill = AnalyticsResult & {\n /** The dataset's base object — the host drills into its records. */\n object?: string;\n /** Selected drillable dimension NAME → underlying object FIELD name. */\n dimensionFields?: Record<string, string>;\n /**\n * RAW grouped values per row, aligned to `rows` by index — each a map of\n * drillable dimension NAME → stored value (BEFORE label resolution rewrote\n * `rows[i][dim]` to the display label). The exact-match drill filter is built\n * from these, never from the display labels.\n */\n drillRawRows?: Array<Record<string, unknown>>;\n /**\n * RAW grouped values for the totals/subtotal rows (#3214), the totals-side\n * companion to `drillRawRows`: `drillRawTotals[i]` aligns to `result.totals[i]`\n * and `drillRawTotals[i][j]` to `result.totals[i].rows[j]`. Each map holds that\n * grouping's DRILLABLE dimension NAME → stored value, snapshotted in the SAME\n * pre-label-resolution pass (the totals loop below overwrites a subtotal row's\n * dimension value with its display label just like the data rows). Restricted\n * to the drillable dims present in the grouping, so the grand-total grouping\n * (`[]`) contributes an empty map per row — which keeps the index alignment\n * intact and correctly drills the whole (unfiltered) object.\n */\n drillRawTotals?: Array<Array<Record<string, unknown>>>;\n /**\n * #1752 — half-open date-range drill scope per row, the RANGE companion to\n * `drillRawRows` (which handles equality dims). A time-bucketed date\n * dimension (`dateGranularity`) groups a SPAN of records into one bucket\n * (\"2026-Q2\"), so its drill needs `[gte, lt)`, not equality — the humanized\n * bucket can't be exact-matched (which is why date dims are excluded from\n * `dimensionFields`/`drillRawRows`). Aligned to `rows` by index; each entry\n * maps a drillable date-dimension NAME → `{ field, gte, lt }` with `gte`\n * inclusive and `lt` exclusive (bounds as `YYYY-MM-DD`). Present only for\n * buckets whose boundaries are unambiguous — a `datetime` field under a\n * non-UTC reference timezone is omitted (host drills an unscoped superset)\n * until instant-boundary support lands.\n */\n drillRanges?: Array<Record<string, { field: string; gte: string; lt: string }>>;\n};\n\n/**\n * [#5717] Does this error carry an ADR-0112 envelope — i.e. did its PRODUCER\n * already classify it?\n *\n * The structural fact, read exactly as `rest-server.ts`'s\n * `/analytics/dataset/query` catch reads it (`envelopeStatus`/`envelopeCode`):\n * a numeric `status` plus a non-empty string `code`. Deliberately the SAME\n * predicate rather than a second dialect of \"looks enveloped\" — a producer that\n * ships half an envelope has a bug of its own and must be found, not guessed at\n * from either end of the wire.\n *\n * Status RANGE is deliberately not part of it. The 4xx case is the loud one\n * (#5717's own: a `DATASET_INVALID` / 400 refusal must reach the caller as a\n * 400, never as an empty grid), but a DECLARED 5xx — `read-scope-sql.ts`'s\n * `READ_SCOPE_COMPILE_FAILED` / 500 fail-closed refusals — is if anything worse\n * to swallow: an RLS lowering that failed closed, rendered as a confident empty\n * chart, is a server fault nobody is told about. Either way the producer has\n * ANSWERED the classification question, and {@link isMissingSourceError} — a\n * heuristic over DRIVER phrasing — has no business re-opening it.\n */\nfunction hasDeclaredErrorEnvelope(err: unknown): boolean {\n const e = err as { code?: unknown; status?: unknown } | null | undefined;\n return typeof e?.status === 'number' && typeof e?.code === 'string' && e.code.length > 0;\n}\n\n/**\n * [#6035] Postgres's MISSING COLUMN wording — the one driver phrase that is a\n * missing-SOURCE phrase by substring while meaning the opposite.\n *\n * `column \"label\" of relation \"acct\" does not exist` (SQLSTATE 42703)\n *\n * `relation \"acct\" does not exist` sits inside it verbatim, so both\n * {@link isMissingSourceError} and {@link missingSourceRelation} read it as\n * \"the table `acct` is gone\" — which would degrade the widget to an empty grid\n * (when `acct` is the dataset's own object) or report a cross-datasource\n * topology error (when it is a joined one). Neither is true: `acct` is right\n * there and a COLUMN NAME IS MISPELLED — precisely the class both docblocks\n * promise to leave as a hard failure.\n *\n * Subtracting it first is the shape `rest-server.ts`'s `mapDataError` has used\n * since #5352 (its `unknownColumn` probe extracts this same phrase ahead of the\n * unknown-object branch, so the REST face answers `400 INVALID_FIELD` rather\n * than `404`; the case is pinned in `rest.test.ts`). This is deliberately that\n * regex rather than a second dialect of it — the two faces must not disagree\n * about what counts as postgres saying \"column\".\n *\n * Both quotes are required because postgres always emits them here (its errmsg\n * template is `column \"%s\" of relation \"%s\" does not exist`), and requiring\n * them is the safe direction of error: a wording this misses merely keeps\n * today's verdict, while one it over-matches would turn a genuinely missing\n * table into a hard failure and regress #5033's deliberate leniency.\n *\n * [#6615] \"Deliberately that regex rather than a second dialect of it\" is now\n * enforced rather than asserted: the phrase moved to\n * {@link matchMissingColumnOfRelation} in `@objectstack/types`, which\n * `rest-server.ts`'s `mapDataError` and `metadata`'s `MISSING_TABLE.excludes`\n * also read. The two faces can no longer disagree about what postgres says by\n * one of them being edited. Same pattern, byte for byte — only its owner moved.\n */\nfunction isMissingColumnOfRelation(message: string): boolean {\n return matchMissingColumnOfRelation(message) !== undefined;\n}\n\n/**\n * Detect the \"backing object/table isn't present in this kernel\" class of\n * error so a dataset query can degrade to an empty result instead of failing\n * the widget with a 500. Matches the missing-relation signatures across the\n * drivers ObjectStack runs on (sqlite/libsql, postgres, mysql) plus the\n * framework's own unknown-object signal. Deliberately scoped to MISSING SOURCE\n * (table/object/relation) — not column/syntax errors, which stay hard failures\n * so real query bugs still surface.\n *\n * ⚠️ It is a heuristic over driver PHRASING, so it is the SECOND question the\n * degradation path asks, never the first: {@link hasDeclaredErrorEnvelope} runs\n * ahead of it (#5717), and only an error whose producer declared nothing is\n * classified by its words here.\n *\n * [#5717] The postgres limb is ANCHORED to postgres's actual wording\n * (`relation \"x\" does not exist`, relation name quoted or bare) instead of the\n * `includes('relation') && includes('does not exist')` conjunction it used to\n * be. That conjunction matched any sentence carrying both words — including\n * `dataset-compiler.ts`'s `… includes relationship \"R\" which does not exist on\n * object \"O\"`, where the \"relation\" is inside \"relationship\" and the missing\n * thing is a RELATIONSHIP, not a table. The anchor is the same pattern the\n * sibling {@link missingSourceRelation} already uses for postgres (and the same\n * shape as `metadata/src/utils/schema-sync-errors.ts`), so \"is something\n * missing\" and \"what is missing\" can no longer disagree on this limb.\n *\n * MEASURED over the wordings this repo actually carries — 13 strings: the three\n * driver families' phrasings (including sql-prefixed and schema-qualified\n * forms), the framework's not-registered signals, and this package's own\n * refusals — exactly ONE verdict moves, the compiler refusal above. No driver\n * wording changes, which is what makes this a narrowing rather than a\n * behaviour change for #5033's leniency.\n *\n * [#6035] The residue #5717 left and named here is now closed by\n * {@link isMissingColumnOfRelation}, subtracted BEFORE any limb below runs.\n * The anchor above cannot do it alone, for a reason worth stating plainly: the\n * missing-COLUMN wording literally CONTAINS a well-formed missing-relation\n * wording, so no tightening of \"does this say a relation is missing\" can ever\n * exclude it — only asking the more specific question FIRST can. That makes the\n * ORDER the fix, not the pattern.\n */\nfunction isMissingSourceError(err: unknown): boolean {\n const raw = String((err as { message?: unknown })?.message ?? err ?? '');\n // [#6035] Missing COLUMN is not missing SOURCE — the paragraph above promises\n // column errors stay hard failures, and this is where that promise is kept.\n if (isMissingColumnOfRelation(raw)) return false;\n const msg = raw.toLowerCase();\n return (\n msg.includes('no such table') || // sqlite / libsql\n /relation\\s+[`\"']?[A-Za-z0-9_$.]+[`\"']?\\s+does not exist/i.test(raw) || // postgres\n msg.includes(\"doesn't exist\") || // mysql (\"table ... doesn't exist\")\n msg.includes('not registered') || // framework: object not in registry\n msg.includes('unknown object') ||\n msg.includes('is not a registered object')\n );\n}\n\n/**\n * #5033 — the relation a missing-source error NAMES, when it names one.\n *\n * `isMissingSourceError` answers \"is something missing\"; this answers \"what\".\n * The distinction decides whether the widget may degrade: a dataset whose OWN\n * backing table is absent is a kernel that never mounted the object (degrade —\n * that is the case the graceful path exists for), while a dataset whose\n * *joined* table is absent on the datasource the base object routed to is a\n * cross-datasource dataset, i.e. a topology error that must be reported as\n * itself instead of hiding behind \"backing object … is unavailable\".\n *\n * Returns the bare relation name (schema/database qualifiers stripped —\n * `mydb.crm_account` → `crm_account`; Prime Directive #6 makes object name =\n * table name, so the result is comparable to a dataset's `object`), or\n * `undefined` when the driver's phrasing carries no name. Unparseable ⇒ the\n * caller keeps today's degradation, never a louder guess.\n *\n * [#6035] It subtracts {@link isMissingColumnOfRelation} for the same reason\n * its sibling does, and the reason is CONSISTENCY rather than a second bug:\n * measured on `origin/main`, the column wording made this function answer\n * `sys_team`, so fixing only \"is something missing\" would leave the pair\n * DISAGREEING — one saying nothing is missing, the other naming a table. That\n * disagreement is the exact defect #5717 closed on the postgres limb, and\n * re-opening it here would re-arm the same mine one edit away: today this\n * function is only ever called behind a true `isMissingSourceError`, so the\n * guard is unreachable, but \"unreachable\" is a property of the CALL ORDER at\n * one call site, not of this function. Guarding both keeps the two answers\n * derivable from the wording alone.\n */\nfunction missingSourceRelation(err: unknown): string | undefined {\n const msg = String((err as { message?: unknown })?.message ?? err ?? '');\n if (isMissingColumnOfRelation(msg)) return undefined;\n const patterns = [\n /no such table:\\s*[`\"'[]?([A-Za-z0-9_$.]+)/i, // sqlite / libsql\n /relation\\s+[`\"']?([A-Za-z0-9_$.]+)[`\"']?\\s+does not exist/i, // postgres\n /table\\s+[`\"']?([A-Za-z0-9_$.]+)[`\"']?\\s+doesn't exist/i, // mysql\n /(?:object|table)\\s+[`\"']([A-Za-z0-9_$.]+)[`\"']\\s+is not registered/i, // framework\n /unknown object:?\\s*[`\"']?([A-Za-z0-9_$.]+)/i,\n /[`\"']([A-Za-z0-9_$.]+)[`\"']\\s+is not a registered object/i,\n ];\n for (const re of patterns) {\n const m = re.exec(msg);\n if (m?.[1]) {\n const parts = m[1].split('.').filter(Boolean);\n const bare = parts[parts.length - 1];\n if (bare) return bare;\n }\n }\n return undefined;\n}\n\n/**\n * [#4437] A name that is a plain column/table identifier and nothing else.\n * Anything with a dot, a paren, whitespace or an operator is a SQL EXPRESSION\n * (or a cross-object reference) whose parts this layer cannot attribute to a\n * single field — such measures pass the source-field gate untouched.\n */\nconst BARE_IDENTIFIER = /^[a-z_][a-z0-9_]*$/i;\n\n/**\n * The `cube.dimensions` / `cube.measures` KEY a member resolves to, mirroring\n * the strategies' own `lookupMember` — including its deliberate LAST case: a\n * dotted member that matches no declared key is a synthetic relation traversal\n * handed to the JOIN machinery, which the source-field gates must not judge\n * (hence `undefined`, read as \"nothing to check\" rather than \"undeclared bare\n * column\").\n *\n * `kind` is which bags to consult, and it is NOT cosmetic — it is the difference\n * between the two callers' real resolution rules:\n *\n * - `'dimension'` — {@link AnalyticsService.assertDimensionFields}, matching\n * `NativeSQLStrategy.resolveDimensionSql` / `ObjectQLStrategy.resolveFieldName\n * (…, 'dimension')`, which look in `cube.dimensions` only.\n * - `'any'` — {@link AnalyticsService.assertWhereFields}, matching the FILTER\n * member resolution in `NativeSQLStrategy.resolveFieldSql` and\n * `ObjectQLStrategy.resolveFieldName(…, 'any')`, which fall through to\n * `cube.measures`. Consulting dimensions only would have made the where gate\n * reject a query that works on both strategies today: a cube declaring\n * `measures.revenue = {sql: 'annual_revenue'}` answers\n * `where: {revenue: {$gt: 100}}` as `annual_revenue > ?`, and a\n * dimensions-only lookup would have called `revenue` a missing column.\n *\n * Extracted from #5520's gate so #5669's second caller reads the tree the same\n * way — two open-coded copies of `lookupMember` in one file is exactly how\n * \"what the gate sees\" and \"what reaches SQL\" drift apart.\n */\nfunction declaredMemberEntry(\n cube: Cube,\n member: string,\n kind: 'dimension' | 'any',\n): { key: string; sql?: unknown } | undefined {\n const bags: Array<Record<string, { sql?: unknown } | undefined>> =\n kind === 'dimension'\n ? [cube.dimensions as Record<string, { sql?: unknown } | undefined>]\n : [\n cube.dimensions as Record<string, { sql?: unknown } | undefined>,\n cube.measures as Record<string, { sql?: unknown } | undefined>,\n ];\n // `key` is spread LAST in every arm: it is the bag key this member RESOLVED\n // to, and a `key` property on the cube entry itself must not shadow it.\n for (const bag of bags) {\n if (bag[member]) return { ...bag[member], key: member };\n if (member.includes('.')) {\n const [first, ...rest] = member.split('.');\n const tail = rest.join('.');\n if (first === cube.name && bag[tail]) return { ...bag[tail], key: tail };\n if (bag[tail]) return { ...bag[tail], key: tail };\n const flat = member.replace(/\\./g, '_');\n if (bag[flat]) return { ...bag[flat], key: flat };\n }\n }\n return undefined;\n}\n\n/**\n * The cube KEY a member resolves to (for the rejection's suggestion list) and\n * the bare COLUMN it compiles to — `source: null` meaning \"nothing this gate can\n * check\", which covers every deliberate stand-down at member level: an\n * expression `sql` (`CASE WHEN …`, `*`), and a dotted relation traversal whose\n * join target the gate cannot see.\n *\n * Shared by the dimension gate (#5520) and the `where` gate (#5669) — see\n * {@link declaredMemberEntry} for why `kind` differs between them.\n */\nfunction resolveMemberSource(\n cube: Cube,\n member: string,\n kind: 'dimension' | 'any',\n): { key: string; source: string | null } {\n const entry = declaredMemberEntry(cube, member, kind);\n if (entry) {\n const source = typeof entry.sql === 'string' ? entry.sql.trim() : '';\n return { key: entry.key, source: source && BARE_IDENTIFIER.test(source) ? source : null };\n }\n // Undeclared. A dotted spelling is the relation traversal above; a bare one IS\n // the column the strategies will emit.\n if (member.includes('.')) return { key: member, source: null };\n return { key: member, source: BARE_IDENTIFIER.test(member) ? member : null };\n}\n\n/**\n * Configuration for AnalyticsService.\n */\nexport interface AnalyticsServiceConfig {\n /** Pre-defined cube definitions (from manifest). */\n cubes?: Cube[];\n /** Logger instance. */\n logger?: Logger;\n /**\n * Probe driver capabilities for the object that backs a cube.\n * The service calls this function to decide which strategy can handle a query.\n */\n queryCapabilities?: (cubeName: string) => AnalyticsDriverCapabilities;\n /**\n * Execute raw SQL on the driver for a given object.\n * Required for NativeSQLStrategy.\n */\n executeRawSql?: (objectName: string, sql: string, params: unknown[]) => Promise<Record<string, unknown>[]>;\n /**\n * Execute an ObjectQL aggregate query.\n * Required for ObjectQLStrategy.\n */\n executeAggregate?: (objectName: string, options: {\n groupBy?: string[];\n aggregations?: Array<{ field: string; method: string; alias: string }>;\n filter?: Record<string, unknown>;\n /** Reference timezone (IANA) for date bucketing — ADR-0053 Phase 2. */\n timezone?: string;\n /**\n * ADR-0021 D-C (#3602) — the request's ExecutionContext. Bridges MUST\n * forward it to `engine.aggregate` so engine-side RLS applies; see\n * `StrategyContext.executeAggregate` for why this is a second belt rather\n * than a replacement for `getReadScope`.\n */\n context?: ExecutionContext;\n }) => Promise<Record<string, unknown>[]>;\n /**\n * Fallback IAnalyticsService (e.g. MemoryAnalyticsService).\n * Used by InMemoryStrategy.\n */\n fallbackService?: IAnalyticsService;\n /**\n * Custom strategies to add/replace the defaults.\n * They are merged with the built-in strategies and sorted by priority.\n */\n strategies?: AnalyticsStrategy[];\n /**\n * ADR-0021 D-C — context-aware per-object read scope (tenant + RLS). Supplied\n * by the runtime that owns the sharing middleware; receives the current\n * request's ExecutionContext and returns the RLS `FilterCondition` for the\n * object (exactly what `RLSCompiler` emits). The service binds the active\n * context per query and the strategy compiles the filter into alias-qualified\n * SQL injected into every base and joined table.\n *\n * MAY be async: the production bridge resolves RLS from the `security`\n * service's `getReadFilter`, which can hit the database. The service\n * pre-resolves the scope for every base + joined object of a query (before\n * the synchronous SQL builder runs), so a sync return still works unchanged.\n */\n getReadScope?: (\n objectName: string,\n context?: ExecutionContext,\n ) =>\n | FilterCondition\n | null\n | undefined\n | Promise<FilterCondition | null | undefined>;\n /**\n * ADR-0021 D-C — join allowlist per cube (the dataset's declared `include`).\n * Joins outside this set are rejected by the strategy. Compiled datasets\n * (via `queryDataset`/`registerDataset`) supply this automatically; this\n * config hook is a fallback for legacy hand-authored cubes.\n */\n getAllowedRelationships?: (cubeName: string) => Set<string> | undefined;\n /**\n * Coerce a filter comparand to a temporal column's storage form so a\n * relative-date / ISO-string value compares correctly on the active driver\n * (SQLite `Field.datetime` → epoch ms; `Field.date` / native timestamp →\n * unchanged). Threaded into the StrategyContext and consulted by\n * `NativeSQLStrategy` when binding filter values. See the contract docs on\n * `StrategyContext.coerceTemporalFilterValue` for the full rationale.\n */\n coerceTemporalFilterValue?: (objectName: string, fieldName: string, value: unknown) => unknown;\n /**\n * Normalise the COLUMN side of the same comparison to that storage form — the\n * other half of the fix, needed because a SQLite `Field.datetime` holds both an\n * INTEGER epoch (a `Date` write) and ISO TEXT (a REST/JSON write, a `NOW()`\n * default) at once, so coercing only the comparand matches one of them and\n * misses the other (#3912). See `StrategyContext.coerceTemporalFilterColumn`.\n */\n coerceTemporalFilterColumn?: (objectName: string, fieldName: string, columnSql: string) => string;\n /**\n * ADR-0062 D6 — report whether an object is federated (external datasource).\n * Threaded into the StrategyContext so `NativeSQLStrategy` declines external\n * objects (which it would otherwise query against the wrong physical table),\n * routing them to the driver-correct ObjectQL aggregate path instead. See\n * `StrategyContext.isExternalObject`.\n */\n isExternalObject?: (objectName: string) => boolean;\n /**\n * [#5033] The datasource `objectName` is bound to, or `undefined` when it\n * rides the default one (or nothing authoritative can answer).\n *\n * It never selects a driver (that is `engine.execute`'s `object` key, which\n * the `plugin.ts` bridge now passes). It exists so that when a dataset's SQL\n * references a table that is NOT on the datasource its base object routed to,\n * the failure can name the actual cause — *table X is not on datasource Y* —\n * instead of the misleading \"backing object … is unavailable\" that a\n * cross-datasource join used to produce.\n *\n * [#5115] The same probe now also gates COMPILATION: `registerDataset` hands\n * it to `compileDataset`, which rejects a dataset whose join crosses\n * datasources before any query is ever built. Absence keeps the pre-#5115\n * behaviour exactly (\"cannot answer, do not block\") — the query-time\n * diagnostic above stays as the backstop.\n *\n * [#5288] \"Bound to\" above is the whole contract, and it took until #5288 for\n * the built-in host to honour it: `plugin.ts` answered with the object's\n * DECLARED `datasource` — step 1 of the five `ObjectQL.getDriver` routes by —\n * so an object placed by a `datasourceMapping` rule, by the ADR-0057 §3.6\n * lifecycle split, or by its package's `defaultDatasource` reported\n * `'default'` and sent the message above to the wrong database. It now asks\n * `ObjectQL.resolveEffectiveDatasource`. A custom host owes the same answer:\n * the datasource an object is BOUND to, `undefined` when nothing binds it.\n */\n getObjectDatasource?: (objectName: string) => string | undefined;\n /**\n * [#3867] Is `name` a registered object in this kernel's schema registry?\n *\n * Consulted by {@link AnalyticsService.ensureCube} on the auto-inference\n * path only. When no Cube is registered under the queried name, the service\n * infers a minimal one whose `sql` IS that name — the intended \"metric over\n * an object\" path (an `object-metric` KPI widget queries `crm_account`\n * without anyone authoring a Cube). Without this hook that inference accepts\n * ANY string, so an arbitrary physical table name reached the driver: the\n * analytics-side twin of the data-path gap closed in #3770.\n *\n * Optional, and absence means \"skip the check\" — same tiering as #3770's\n * `assertObjectRegistered`: with no registry to consult the question cannot\n * be answered, and failing closed would break every embedding that runs\n * analytics without a data engine. The production bridge in `plugin.ts`\n * always wires it.\n */\n isRegisteredObject?: (name: string) => boolean;\n /**\n * [#4437] The FIELD NAMES `objectName` declares, or `undefined` when nothing\n * authoritative can answer.\n *\n * Consulted by {@link AnalyticsService.ensureCube} to validate the SOURCE\n * FIELD a measure resolves to BEFORE any SQL is built. `inferMeasure` maps a\n * suffix convention onto a field name (`ghost_sum` → `SUM(ghost)`) and used\n * to accept any spelling, so a typo'd measure reached the driver as a column\n * and came back as an opaque `500 SQLITE_ERROR` — a driver error class on the\n * wire for a caller-shaped mistake (ADR-0112). The DATA route already refuses\n * the same mistake with a `400 INVALID_FIELD` naming the field (#4315/#4254);\n * this hook is what lets the ANALYTICS route give the same answer.\n *\n * [#5520] The same probe now answers for DIMENSIONS too\n * ({@link AnalyticsService.assertDimensionFields}). #4437 gated only the\n * measure half, so the identical typo one key over — `dimensions:\n * ['bogus_dim']` — still reached the driver as a `GROUP BY` column and came\n * back as the same 500. One probe, one answer, both member kinds.\n *\n * [#5669] …and for the `where` members ({@link AnalyticsService.assertWhereFields}),\n * the third and last request key that carries a field name. `where:\n * {bogus_col: 'x'}` compiled straight into `WHERE bogus_col = $1` for exactly\n * as long as #4437 and #5520 had each closed only their own key. One probe now\n * answers for all three.\n *\n * Same tiering as {@link isRegisteredObject}: absence means \"skip the check\"\n * (registry-less hosts, engine doubles, external datasources whose columns\n * are not mirrored locally). The production bridge in `plugin.ts` wires it\n * from the same schema registry the data path's gate reads, so \"which fields\n * exist\" has ONE answer across `/data` and `/analytics`.\n */\n getObjectFieldNames?: (objectName: string) => readonly string[] | undefined;\n /**\n * ADR-0021 — optional object-graph resolver used when compiling datasets:\n * `(baseObject, relationshipName) => relatedObjectName | undefined`. When\n * provided, `queryDataset` validates that every declared `include` exists.\n */\n relationshipResolver?: RelationshipResolver;\n /**\n * Resolve the metadata of a dimension's or measure's SOURCE FIELD on the\n * dataset's base object — the one seam through which display semantics that\n * live on the field reach the result columns. `undefined` for an unknown\n * field. Feeds three chains:\n *\n * - ADR-0053 currency: a monetary measure that omits an explicit `currency`\n * falls back to the field's declared currency, then the tenant default\n * (`ctx.currency`). Non-`currency` fields never get a code.\n * - Percent scale (objectui#3136): a measure over a `percent` field inherits\n * that field's storage scale via `percentScaleOf`, so a renderer scales by\n * declared metadata instead of guessing from the value.\n * - Date bucketing: a date vs datetime dimension drills by the right bound.\n */\n sourceFieldMeta?: (object: string, field: string) => { type?: string; defaultCurrency?: string; max?: number } | undefined;\n /** Pre-defined datasets to compile + register at construction (ADR-0021). */\n datasets?: Dataset[];\n /**\n * ADR-0021 — resolve raw dimension values to human display labels. When\n * provided, `queryDataset` post-processes result rows so a `select` dimension\n * shows its option label (not the stored value) and a `lookup`/`master_detail`\n * dimension shows the related record's display name (not the FK id). Injected\n * by the plugin from the `data` engine; omit to keep raw values.\n */\n labelResolver?: DimensionLabelDeps;\n\n /**\n * ADR-0037 Phase 3 — draft data preview. Resolve the PENDING `seed` draft\n * rows for an object (returns null when the object has no pending seed).\n * When provided and `queryDataset` is called with `previewDrafts`, the\n * selection is evaluated over these rows in memory instead of the engine —\n * the Live Canvas charts real numbers from the drafted sample data, and\n * because publish materializes the SAME seed, the numbers are continuous\n * across the publish boundary. Reads only; never touches physical tables.\n */\n draftRowsResolver?: (\n objectName: string,\n context?: ExecutionContext,\n ) => Promise<Record<string, unknown>[] | null>;\n\n /**\n * [#8286] Echo the executed statement back to the CALLER in\n * `AnalyticsResult.sql`. **Off unless this host opts in.**\n *\n * The contract has always declared the echo debug-only —\n * `AnalyticsResultResponseSchema.data.sql` (`spec/api/analytics.zod.ts`) is\n * `optional()` and describes itself as \"Executed SQL (if debug enabled)\" —\n * but no implementation ever read a debug switch, so every `/analytics/query`\n * response carried the statement, on production deployments included. This\n * field is that switch: declared = enforced, restored at the one seam the\n * response leaves through ({@link AnalyticsService.query}).\n *\n * **Default** — `NODE_ENV === 'development'`, and nothing else. In\n * particular an UNSET `NODE_ENV` counts as production and the echo stays\n * off: that is the maintainer's 2026-08-06 ruling for machine-readable\n * environment answers (see `resolveDiscoveryEnvironment` and the note at\n * `runtime/src/http-dispatcher.ts`), and of the two ways to be wrong,\n * disclosing on a production deployment whose operator forgot the variable\n * is the dangerous one. `os start` forces `NODE_ENV='production'` when\n * unset, `os serve` resolves `NODE_ENV || 'production'`, `os doctor` derives\n * the same expression — this switch now reads the absence the same way.\n *\n * **Why not a request field.** There is none, deliberately: a caller-set\n * debug flag would let any tenant re-open the disclosure on demand, which is\n * the shape of the defect rather than a fix for it. The echo is a HOST\n * decision, and the caller-facing surface for \"show me the SQL\" already\n * exists as the dedicated dry-run route `/api/v1/analytics/sql`\n * (`generateSql`), which this switch does not touch.\n *\n * **Why not the plugin's `debug` (log) option.** Server-side log verbosity\n * and what travels to a caller are different decisions with different blast\n * radii; folding them together means a support engineer raising log level on\n * a live deployment silently reopens the disclosure. Two switches, named for\n * what they open.\n */\n debugSql?: boolean;\n}\n\n/**\n * Default capabilities when probing is not configured — assumes in-memory only.\n */\nconst DEFAULT_CAPABILITIES: AnalyticsDriverCapabilities = {\n nativeSql: false,\n objectqlAggregate: false,\n inMemory: true,\n};\n\n/**\n * AnalyticsService — Multi-driver analytics orchestrator.\n *\n * Implements `IAnalyticsService` by delegating to a priority-ordered\n * strategy chain:\n *\n * | Priority | Strategy | Condition |\n * |:---:|:---|:---|\n * | P1 (10) | NativeSQLStrategy | Driver supports raw SQL |\n * | P2 (20) | ObjectQLStrategy | Driver supports aggregate AST |\n * | P3 (30) | (custom / InMemoryStrategy from driver-memory) | Injected by user |\n *\n * When `fallbackService` is configured, an internal delegate strategy\n * is automatically appended at priority 30 as a safety net.\n *\n * The service also owns a `CubeRegistry` for metadata discovery and\n * auto-inference from object schemas.\n */\nexport class AnalyticsService implements IAnalyticsService {\n private readonly strategies: AnalyticsStrategy[];\n /** Context-independent part of the StrategyContext (no per-request scope). */\n private readonly baseCtx: StrategyContext;\n /** Context-aware read-scope provider (bound to the request's context per call). */\n private readonly readScopeProvider?: AnalyticsServiceConfig['getReadScope'];\n /** Compiled datasets by name — feeds the join allowlist (D-C) and queryDataset. */\n private readonly datasetRegistry = new Map<string, CompiledDataset>();\n /** Optional object-graph resolver used when compiling datasets. */\n private readonly relationshipResolver?: RelationshipResolver;\n private readonly sourceFieldMeta?: AnalyticsServiceConfig['sourceFieldMeta'];\n /** Optional dimension display-label resolver (select options / lookup names). */\n private readonly labelResolver?: DimensionLabelDeps;\n /** ADR-0037 P3: pending-seed row resolver for draft data preview. */\n private readonly draftRowsResolver?: AnalyticsServiceConfig['draftRowsResolver'];\n /** [#3867] Schema-registry probe gating cube auto-inference. */\n private readonly isRegisteredObject?: AnalyticsServiceConfig['isRegisteredObject'];\n /** [#4437] Field-name probe gating measure source-field resolution. */\n private readonly getObjectFieldNames?: AnalyticsServiceConfig['getObjectFieldNames'];\n /**\n * [#5033] Datasource probe for the missing-source triage — and, since #5115,\n * for the compile-time cross-datasource join gate in `compileDataset`.\n */\n private readonly getObjectDatasource?: AnalyticsServiceConfig['getObjectDatasource'];\n /** ADR-0062 D6 — federated-object probe (strategy routing + #5115's gate). */\n private readonly isExternalObject?: AnalyticsServiceConfig['isExternalObject'];\n /** [#3867] One-shot flag for the {@link assertInferableCube} stand-down warning. */\n private warnedNoObjectRegistry = false;\n /**\n * [#8286] Does the executed statement travel back to the caller?\n * See {@link AnalyticsServiceConfig.debugSql} for the switch and its default.\n */\n private readonly debugSql: boolean;\n readonly cubeRegistry: CubeRegistry;\n private readonly logger: Logger;\n\n constructor(config: AnalyticsServiceConfig = {}) {\n this.logger = config.logger || createLogger({ level: 'info', format: 'pretty' });\n this.cubeRegistry = new CubeRegistry();\n\n // Register pre-defined cubes\n if (config.cubes) {\n this.cubeRegistry.registerAll(config.cubes);\n }\n\n this.readScopeProvider = config.getReadScope;\n this.relationshipResolver = config.relationshipResolver;\n this.sourceFieldMeta = config.sourceFieldMeta;\n this.labelResolver = config.labelResolver;\n this.draftRowsResolver = config.draftRowsResolver;\n this.isRegisteredObject = config.isRegisteredObject;\n this.getObjectFieldNames = config.getObjectFieldNames;\n this.getObjectDatasource = config.getObjectDatasource;\n this.isExternalObject = config.isExternalObject;\n // [#8286] Resolved ONCE, at construction, from the host's explicit choice\n // or from `NODE_ENV`. An unset `NODE_ENV` is not development — see the\n // field's doc for the ruling this inherits.\n this.debugSql = config.debugSql ?? (getEnv('NODE_ENV') === 'development');\n\n // Compile + register pre-defined datasets (ADR-0021).\n if (config.datasets) {\n for (const ds of config.datasets) {\n try {\n this.registerDataset(ds);\n } catch (e) {\n this.logger?.warn?.(`[Analytics] Failed to register dataset \"${ds?.name}\": ${String((e as Error)?.message ?? e)}`);\n }\n }\n }\n\n // Build the context-independent strategy context. `getReadScope` is bound\n // per query in `callCtx(context)` so it can resolve the active tenant.\n this.baseCtx = {\n getCube: (name) => this.cubeRegistry.get(name),\n queryCapabilities: config.queryCapabilities || (() => DEFAULT_CAPABILITIES),\n executeRawSql: config.executeRawSql,\n executeAggregate: config.executeAggregate,\n fallbackService: config.fallbackService,\n // Prefer a compiled dataset's declared relationships (D-C join allowlist);\n // fall back to any explicitly-configured provider for legacy cubes.\n getAllowedRelationships: (cubeName: string) =>\n this.datasetRegistry.get(cubeName)?.allowedRelationships\n ?? config.getAllowedRelationships?.(cubeName),\n coerceTemporalFilterValue: config.coerceTemporalFilterValue,\n coerceTemporalFilterColumn: config.coerceTemporalFilterColumn,\n isExternalObject: config.isExternalObject,\n };\n\n // Build strategy chain (built-in + custom, sorted by priority)\n // InMemoryStrategy is NOT built-in — it lives in @objectstack/driver-memory\n // and should be passed via config.strategies when needed.\n // When fallbackService is configured, an internal delegate is added at P3.\n const builtIn: AnalyticsStrategy[] = [\n new NativeSQLStrategy(),\n new ObjectQLStrategy(),\n ];\n\n // Auto-add fallback delegate when fallbackService is provided\n if (config.fallbackService) {\n builtIn.push(new FallbackDelegateStrategy());\n }\n\n const custom = config.strategies || [];\n this.strategies = [...builtIn, ...custom].sort((a, b) => a.priority - b.priority);\n\n this.logger.info(\n `[Analytics] Initialized with ${this.cubeRegistry.size} cubes, ` +\n `${this.strategies.length} strategies: ${this.strategies.map(s => s.name).join(' → ')}`,\n );\n }\n\n /**\n * Build a per-call StrategyContext that binds the read-scope provider to the\n * current request's ExecutionContext (ADR-0021 D-C). The strategy then sees a\n * `getReadScope(objectName)` that already knows the active tenant.\n */\n private async callCtx(\n query: AnalyticsQuery,\n context?: ExecutionContext,\n ): Promise<StrategyContext> {\n // #3602 — `context` rides along unconditionally. It is the ENGINE-side belt\n // (forwarded to `engine.aggregate`, where the middleware chain applies its\n // own RLS), so it must not be gated on the analytics-side belt being wired:\n // a deployment with no `getReadScope` provider is exactly the one that most\n // needs the engine to scope for it.\n if (!this.readScopeProvider) return { ...this.baseCtx, context };\n // Pre-resolve the read scope for every object the strategy will scan (base\n // + all declared joins) BEFORE the synchronous SQL builder runs, since the\n // provider may be async (the production `security.getReadFilter` bridge).\n // The strategy then reads each object's filter synchronously from the map.\n const scopes = await this.resolveReadScopes(query, context);\n return {\n ...this.baseCtx,\n context,\n getReadScope: (objectName: string) => scopes.get(objectName) ?? null,\n };\n }\n\n /**\n * Resolve the read scope (tenant + RLS `FilterCondition`) for the base object\n * AND every joined object of the query's cube, keyed by object name. This is\n * the async pre-pass that lets the synchronous strategy enforce scoping even\n * when the provider (security `getReadFilter`) resolves asynchronously.\n *\n * The object set is `cube.sql` (base) plus every `cube.joins[*].name` — a\n * SUPERSET of what the strategy actually scans (the strategy only joins along\n * declared relationships), so no scanned object is ever left unscoped.\n *\n * Fail-closed: if the provider throws for an object, the whole query is\n * rejected rather than emitting SQL with that object unscoped.\n */\n private async resolveReadScopes(\n query: AnalyticsQuery,\n context?: ExecutionContext,\n ): Promise<Map<string, FilterCondition>> {\n const map = new Map<string, FilterCondition>();\n const provider = this.readScopeProvider;\n if (!provider || !query.cube) return map;\n const cube = this.cubeRegistry.get(query.cube);\n if (!cube) return map;\n\n const objects = new Set<string>();\n if (typeof cube.sql === 'string' && cube.sql.trim()) {\n objects.add(cube.sql.trim());\n }\n const joins = (cube as { joins?: Record<string, { name?: string }> }).joins;\n if (joins) {\n for (const [alias, j] of Object.entries(joins)) {\n objects.add(j?.name ?? alias);\n }\n }\n\n for (const object of objects) {\n let filter: FilterCondition | null | undefined;\n try {\n filter = await provider(object, context);\n } catch (e) {\n // Deny the entire query — never fall through to unscoped SQL.\n this.logger.error?.(\n `[Analytics] read-scope resolution failed for object \"${object}\" — ` +\n `rejecting query (fail-closed, ADR-0021 D-C)`,\n e instanceof Error ? e : new Error(String(e)),\n );\n throw new Error(\n `[Analytics] read-scope resolution failed for \"${object}\"; query denied (fail-closed).`,\n );\n }\n if (filter != null) map.set(object, filter);\n }\n return map;\n }\n\n /**\n * Execute an analytical query by delegating to the first capable strategy.\n *\n * A strategy can discover only AT EXECUTION TIME that the underlying driver\n * cannot serve it — the canonical case is NativeSQLStrategy on an in-memory\n * driver, whose `execute()` returns null for raw SQL (the auto-bridge throws\n * `RAW_SQL_UNSUPPORTED`). That is a capability miss, not a query error: fall\n * back to the next capable strategy (e.g. ObjectQLStrategy over the\n * aggregate bridge) instead of failing — or worse, fabricating empty rows.\n * Any other error propagates untouched.\n */\n async query(query: AnalyticsQuery, context?: ExecutionContext): Promise<AnalyticsResult> {\n if (!query.cube) {\n throw new Error('Cube name is required in analytics query');\n }\n\n this.ensureCube(query);\n const ctx = await this.callCtx(query, context);\n let skip: Set<AnalyticsStrategy> | undefined;\n for (;;) {\n const strategy = this.resolveStrategy(query, ctx, skip);\n this.logger.debug(`[Analytics] Query on cube \"${query.cube}\" → ${strategy.name}`);\n try {\n // [#8286] ONE gate, every strategy. This is the single seam a strategy\n // result leaves the service through — `NativeSQLStrategy` returns the\n // statement it ran, `ObjectQLStrategy` renders a representative one,\n // and `FallbackDelegateStrategy` passes through whatever the delegated\n // service minted (e.g. `MemoryAnalyticsService`, which always echoes).\n // Gating any one of those would leave the others serving, which is the\n // shape the defect already had.\n return this.applySqlEchoPolicy(await strategy.execute(query, ctx));\n } catch (e) {\n if ((e as { code?: string })?.code === 'RAW_SQL_UNSUPPORTED') {\n this.logger.warn(\n `[Analytics] ${strategy.name} cannot run on this driver (raw SQL unsupported) — falling back to the next strategy.`,\n );\n (skip ??= new Set()).add(strategy);\n continue;\n }\n throw e;\n }\n }\n }\n\n /**\n * [#8286] Withhold the executed statement unless this host enabled the echo.\n *\n * Applied at {@link query}, which is the response-assembly seam for BOTH\n * faces that serve callers: `/api/v1/analytics/query` calls it directly, and\n * `queryDataset` reaches it through `DatasetExecutor`, so a dataset response\n * inherits the same verdict without a second gate to keep in step.\n * `generateSql` — the dedicated `/api/v1/analytics/sql` dry-run route — is\n * deliberately NOT gated: asking for the statement is that route's entire\n * purpose, and it is the surface a debugging author is meant to use.\n *\n * What the echo disclosed, and why \"it is only a table name\" understates it:\n * the statement carries the compiled read scope, i.e. the SHAPE of the\n * isolation predicate (`\"sys_user\".\"id\" IN ($2, $3, …)` rather than an\n * `organization_id` comparison) plus its bound-parameter arity, which counts\n * the caller's own org membership. No wall was breached by it — the echo is\n * information disclosure, and this is the disclosure closing.\n */\n private applySqlEchoPolicy(result: AnalyticsResult): AnalyticsResult {\n if (this.debugSql || result?.sql === undefined) return result;\n // Copy-and-delete rather than mutate: the strategy (or a delegated\n // fallback service) owns the object it returned, and a cached result on\n // the other side of that boundary must not lose a field because this\n // service handed it to a caller once.\n const withheld: AnalyticsResult = { ...result };\n delete withheld.sql;\n return withheld;\n }\n\n /**\n * Compile a `dataset` (ADR-0021) and register its Cube + join allowlist so it\n * can be queried by name. Idempotent (re-registering overwrites). Returns the\n * compiled dataset.\n */\n registerDataset(dataset: Dataset): CompiledDataset {\n // #5115 — the datasource/federation probes turn a cross-datasource join\n // from a query-time explosion into a registration-time rejection. Both are\n // optional and tiered \"cannot answer, do not block\" inside the compiler.\n const compiled = compileDataset(dataset, this.relationshipResolver, {\n getObjectDatasource: this.getObjectDatasource,\n isExternalObject: this.isExternalObject,\n });\n this.cubeRegistry.register(compiled.cube);\n this.datasetRegistry.set(dataset.name, compiled);\n return compiled;\n }\n\n /**\n * Execute a semantic-layer dataset (ADR-0021). Compiles the dataset (saved or\n * inline draft — Studio preview), registers its Cube + join allowlist, then\n * runs the selection through the `DatasetExecutor` with the request context so\n * tenant/RLS scoping (D-C) is applied. See {@link IAnalyticsService.queryDataset}.\n */\n async queryDataset(\n dataset: Dataset,\n selection: DatasetSelection,\n context?: ExecutionContext,\n options?: { previewDrafts?: boolean },\n ): Promise<AnalyticsResult> {\n const compiled = this.registerDataset(dataset);\n this.logger.debug(`[Analytics] queryDataset \"${dataset.name}\" (object=${dataset.object}, include=${(dataset.include ?? []).join(',') || '—'})`);\n\n // ── ADR-0037 P3 — draft data preview ────────────────────────────────────\n // When the request renders the as-if-published world AND the base object\n // has a PENDING seed draft, evaluate the selection over the seed's rows in\n // memory (a query-evaluating proxy feeds the unchanged DatasetExecutor, so\n // measure filters / compareTo / derived measures all behave identically).\n // No pending seed → fall through to the real engine: published objects\n // keep charting live data even inside a preview.\n if (options?.previewDrafts && this.draftRowsResolver) {\n let seedRows: Record<string, unknown>[] | null = null;\n try {\n seedRows = await this.draftRowsResolver(dataset.object, context);\n } catch (e) {\n this.logger.warn(`[Analytics] draft preview resolver failed for \"${dataset.object}\" — falling back to live data: ${String((e as Error)?.message ?? e)}`);\n }\n if (seedRows) {\n this.logger.debug(`[Analytics] queryDataset \"${dataset.name}\" → preview over ${seedRows.length} drafted seed row(s)`);\n const previewService = {\n query: async (q: AnalyticsQuery) => evaluateAnalyticsQueryOverRows(q, compiled.cube, seedRows!),\n } as IAnalyticsService;\n const previewResult = await new DatasetExecutor(previewService).execute(compiled, selection, context);\n // Label resolution is skipped on purpose: drafted seed rows reference\n // lookups by NAME (the seed convention), which already reads well.\n return previewResult;\n }\n }\n\n // [#6761] The audience's language for THIS request, and the only locale\n // either field-label enrichment site below is entitled to use.\n //\n // `ExecutionContext.locale` is the BCP-47 tag `resolveExecutionContext`\n // resolves per request — the caller's `Accept-Language` when it expressed a\n // preference, else the workspace `localization` setting. It is the same\n // context field the currency chain a few lines down already reads, so both\n // display decisions in this response answer to one request identity.\n //\n // `undefined` (no context, or an anonymous request that skips localization)\n // is passed through deliberately rather than defaulted here: the shared\n // resolver documents nullish as \"no locale known\" and answers `en`, the\n // platform's source language. Choosing a different default in this file\n // would be this service disagreeing with the renderer about the same map.\n const requestLocale = context?.locale;\n\n // #3602 — every label lookup in this request (sort keys below, display\n // labels further down) reads the REFERENCED object, so bind that object's\n // own read scope to this request once, up front.\n const provider = this.readScopeProvider;\n const resolveScope = provider\n ? (targetObject: string) => provider(targetObject, context)\n : undefined;\n // #3680 — per-request label-fetch cache. A selection that sorts by a\n // lookup dimension resolves labels twice (pre-window sort keys, then\n // post-window display); the cache makes the display pass reuse the ids the\n // sort already fetched, so label-ordering costs ONE id→name read total.\n const labelDeps = this.labelResolver ? withLabelFetchCache(this.labelResolver) : undefined;\n // #3680 — hand the executor the sort-key label hook so an `order` on a\n // select/lookup dimension sorts by the label the user reads. Built over\n // the SAME capabilities (and read scope) as the display resolution below.\n const orderLabels = labelDeps && dataset.dimensions?.length\n ? createOrderLabelResolver(\n dataset.object,\n dataset.dimensions\n .filter((d) => !!d.field)\n .map((d) => ({ name: d.name, field: d.field as string })),\n labelDeps,\n resolveScope,\n context,\n )\n : undefined;\n\n // Graceful degradation: a dashboard/report widget whose backing object or\n // table is not present in this kernel (e.g. a platform dashboard like\n // System Overview that charts `sys_audit_log`, opened in an environment\n // that never mounted the audit object) must render as \"no data\" — NOT\n // crash the widget with a 500. Datasets were the one read surface that\n // hard-failed on a missing source.\n //\n // #5033 — that leniency is scoped to the dataset's OWN source. Once the raw-SQL\n // bridge routes by object (`plugin.ts`), a dataset that JOINS across datasources\n // fails on the base object's datasource with the JOINED table missing. Reporting\n // that as \"backing object … is unavailable\" would be the misleading old shape\n // wearing a new cause: the base table is right there, and the widget would keep\n // rendering the confident `0` this issue is about. So triage by WHICH relation\n // the driver named, and let a cross-datasource dataset fail loudly.\n //\n // #5717 — and the leniency is scoped to errors NOBODY classified. The\n // triage below reads message text, so before it runs, an error that carries\n // an ADR-0112 envelope is re-thrown untouched: its producer already said\n // what it is, and a `DATASET_INVALID` / 400 turned into `{rows: []}` is the\n // #5033 symptom wearing the opposite disguise — the caller's own mistake\n // reported as \"no data\", with no exception, no 4xx, no 5xx, just a warn and\n // a confident empty chart. See {@link hasDeclaredErrorEnvelope}.\n let result: AnalyticsResult;\n try {\n result = await new DatasetExecutor(this, orderLabels).execute(compiled, selection, context);\n } catch (err) {\n // The producer answered the classification question — the route's\n // envelope reader serves it (4xx as itself, declared 5xx through the\n // `ANALYTICS_QUERY_FAILED` path). Nothing here may re-judge it by wording.\n if (hasDeclaredErrorEnvelope(err)) throw err;\n if (isMissingSourceError(err)) {\n const missing = missingSourceRelation(err);\n const detail = String((err as Error)?.message ?? err);\n // A named relation that is NOT the dataset's own object is a joined table.\n // If that object IS registered in this kernel, it exists — just not on the\n // datasource this query ran against: a topology error, not an absence.\n // (`isRegisteredObject` absent / unable to answer ⇒ treat as registered,\n // the same \"cannot answer, do not block\" tiering it carries elsewhere;\n // here the honest report is the loud one, since the base table resolved.)\n const joined = missing && missing.toLowerCase() !== dataset.object.toLowerCase() ? missing : undefined;\n if (joined && (this.isRegisteredObject?.(joined) ?? true)) {\n const baseDs = this.getObjectDatasource?.(dataset.object);\n const joinedDs = this.getObjectDatasource?.(joined);\n const where = baseDs ? `datasource \"${baseDs}\"` : 'the default datasource';\n const joinedWhere = joinedDs ? `datasource \"${joinedDs}\"` : 'the default datasource';\n throw new Error(\n `[Analytics] dataset \"${dataset.name}\" cannot be executed as one statement: table \"${joined}\" ` +\n `is not on ${where}, which is where its base object \"${dataset.object}\" lives — ` +\n `\"${joined}\" is registered on ${joinedWhere}. A dataset JOIN cannot cross datasources. ` +\n `Fix it by binding both objects to the same datasource, or by dropping the cross-datasource ` +\n `relationship from the dataset's \\`include\\`/dimensions. (driver said: ${detail})`,\n );\n }\n this.logger.warn(\n `[Analytics] dataset \"${dataset.name}\" backing object \"${dataset.object}\" is unavailable ` +\n `(${detail}); returning an empty result instead of failing the widget`,\n );\n return { rows: [], fields: [], totals: [] };\n }\n throw err;\n }\n\n // Selected dimensions resolved against the dataset definition — shared by\n // drill metadata, label resolution, and dimension field-label enrichment.\n const selectedDims = (selection.dimensions ?? [])\n .map((name) => dataset.dimensions?.find((d) => d.name === name))\n .filter((d): d is NonNullable<typeof d> => !!d);\n\n // ADR-0021 D2 — drill-through metadata. A host (dashboard/report) drills a\n // clicked bucket back to the underlying records, but it only knows the\n // dimension NAMES, and the label resolution below OVERWRITES the raw grouped\n // value in each row with its display label. So before that happens, snapshot\n // the raw grouped values into a PARALLEL array (aligned to `rows` by index —\n // the result rows are NOT mutated) and expose the dataset's `object` +\n // dimension→field mapping so the renderer can build an exact-match filter.\n // Date buckets are excluded — a humanized bucket (\"2026-06\") can't be\n // exact-matched against the stored timestamp, so they are not drillable.\n const drillDims = selectedDims.filter((d) => !!d.field && d.type !== 'date');\n if (drillDims.length && result.rows.length) {\n (result as AnalyticsResultWithDrill).object = dataset.object;\n (result as AnalyticsResultWithDrill).dimensionFields = Object.fromEntries(\n drillDims.map((d) => [d.name, d.field as string]),\n );\n (result as AnalyticsResultWithDrill).drillRawRows = result.rows.map((row) => {\n const raw: Record<string, unknown> = {};\n for (const d of drillDims) raw[d.name] = row[d.name];\n return raw;\n });\n // #3214 — the totals/subtotal rows (#1753) carry dimension values too and\n // go through the SAME label resolution below, so snapshot their raw\n // grouped values here in the same pre-label pass. Aligned to `result.totals`\n // by index; each grouping is restricted to the drillable dims it actually\n // groups by (the grand-total grouping `[]` keeps empty maps, so a subtotal\n // drill filters by the stored value while the grand total drills unfiltered).\n if (result.totals?.length) {\n (result as AnalyticsResultWithDrill).drillRawTotals = result.totals.map((total) => {\n const groupingDims = drillDims.filter((d) => total.dimensions.includes(d.name));\n return total.rows.map((row) => {\n const raw: Record<string, unknown> = {};\n for (const d of groupingDims) raw[d.name] = row[d.name];\n return raw;\n });\n });\n }\n }\n\n // #1752 — date-range drill scope. A `dateGranularity` dimension groups a\n // SPAN of records into one bucket, so drilling it needs a half-open range\n // `[gte, lt)`, which the equality `drillRawRows` sidecar can't express\n // (that's exactly why date dims are excluded from `drillDims` above). Emit\n // a parallel range sidecar computed — via the shared inverse util so server\n // and client agree on boundaries — from the canonical bucket KEY, which is\n // still in `rows[i][dim]` here (this runs BEFORE label resolution rewrites\n // it to a display label).\n const rangeTz = selection.timezone ?? context?.timezone ?? 'UTC';\n // Per drillable date+granularity dim, decide how to serialize its bounds\n // (ADR-0053 temporal semantics):\n // - `datetime` → the reference tz's MIDNIGHT INSTANT (ISO), because the\n // bucket is defined on that tz's calendar (works under any tz, incl. DST);\n // - `date` → `YYYY-MM-DD` calendar bounds, a tz-naive calendar day that is\n // exact under ANY reference tz;\n // - unknown field type → safe only under UTC (where the calendar day and\n // its instant coincide); under a non-UTC tz we can't tell whether to\n // shift, so the dim is omitted and the host drills a superset.\n // The bucket size to invert MUST be the one the query actually grouped by —\n // `selection.dateGranularity` overrides the dataset dimension's default\n // (#3588). Reading `d.dateGranularity` here meant a widget that bucketed by\n // quarter or year got its ranges computed from the dataset's month (or,\n // when the dataset declared none, dropped entirely) — so drilling a bucket\n // opened the wrong span, or the chart lost drill-through altogether.\n const rangeDims: Array<{ d: (typeof selectedDims)[number]; granularity: DateGranularityValue; instant: boolean }> = [];\n for (const d of selectedDims) {\n if (!d.field || d.type !== 'date') continue;\n const granularity = resolveDimensionGranularity(selection, d.name, d.dateGranularity);\n if (!granularity) continue;\n const ftype = this.sourceFieldMeta?.(dataset.object, d.field as string)?.type;\n if (ftype === 'datetime') rangeDims.push({ d, granularity, instant: true });\n else if (ftype === 'date') rangeDims.push({ d, granularity, instant: false });\n else if (rangeTz === 'UTC') rangeDims.push({ d, granularity, instant: false });\n // else: unknown field type under a non-UTC reference tz → omit (superset).\n }\n if (rangeDims.length && result.rows.length) {\n const bound = (ymd: string, instant: boolean): string =>\n instant ? new Date(zonedDateStartToUtcMs(ymd, rangeTz)).toISOString() : ymd;\n (result as AnalyticsResultWithDrill).drillRanges = result.rows.map((row) => {\n const ranges: Record<string, { field: string; gte: string; lt: string }> = {};\n for (const { d, granularity, instant } of rangeDims) {\n // A row in the empty bucket carries `null` here (#3839) and yields no\n // range, so that row simply gets no drill bound — the superset.\n const cal = bucketKeyToCalendarRange(row[d.name] as string | null, granularity);\n if (cal) {\n ranges[d.name] = { field: d.field as string, gte: bound(cal.start, instant), lt: bound(cal.end, instant) };\n }\n }\n return ranges;\n });\n // The equality drill block sets `object` only when a NON-date drill dim\n // exists; a report grouped ONLY by time still needs the base object so the\n // host can open its list. Safe to (re)set to the same dataset object.\n (result as AnalyticsResultWithDrill).object = dataset.object;\n }\n\n // ADR-0021 — resolve grouped dimension values to human display labels\n // (select option label, lookup related-record name). Charts render the\n // dimension key verbatim, so this is the single place that turns a stored\n // value / FK id into the text a user expects to read.\n if (labelDeps && selectedDims.length) {\n // Same single-source rule as the drill ranges above: a date bucket must be\n // LABELLED with the granularity it was actually grouped by (#3588).\n // Formatting a `year` bucket with the dataset's `month` default rendered\n // it as \"1970-01\" — the year key re-parsed as an epoch millisecond count.\n const dims = selectedDims\n .filter((d) => !!d.field)\n .map((d) => ({\n name: d.name,\n field: d.field,\n type: d.type,\n dateGranularity: resolveDimensionGranularity(selection, d.name, d.dateGranularity),\n }));\n if (dims.length) {\n // `resolveScope` (hoisted above) binds the referenced object's read\n // scope to THIS request so the label lookup (a per-record read of the\n // related object) cannot surface a record the referenced object's RLS\n // would hide (#3602). `labelDeps` is the per-request cache over the\n // configured resolver, so ids the sort-key pass (#3680) already fetched\n // are not fetched again here.\n try {\n // `context` rides alongside `resolveScope` — the label lookup's SECOND\n // belt. `resolveScope` is this layer's own predicate; the context lets\n // the engine's middleware scope the same per-record read itself.\n await resolveDimensionLabels(dataset.object, dims, result.rows, labelDeps, resolveScope, context);\n // Totals rows (#1753) carry dimension values too (a row subtotal is\n // keyed by its row bucket) — resolve each grouping's own subset.\n for (const total of result.totals ?? []) {\n const subset = dims.filter((d) => total.dimensions.includes(d.name));\n if (subset.length) {\n await resolveDimensionLabels(dataset.object, subset, total.rows, labelDeps, resolveScope, context);\n }\n }\n } catch (e) {\n this.logger?.warn?.(`[Analytics] dimension label resolution failed for \"${dataset.name}\": ${String((e as Error)?.message ?? e)}`);\n }\n }\n }\n\n // ADR-0021 — enrich measure columns with their display `label` + `format`\n // so presentations show \"Tasks\" / \"$616,000\" instead of the raw measure\n // name \"task_count\" / \"616000\". Carried on the result fields; the renderer\n // applies the format (it can't be baked into the numeric row value).\n //\n // [#6761] The label is resolved through the shared `I18nLabel → string`\n // resolver, so BOTH authorized forms reach the wire: a plain string, and the\n // inline locale map `I18nLabelSchema` has authorized since #5728. The old\n // `typeof m.label === 'string'` test dropped the map silently — a dataset\n // written the way the schema documents shipped a column with no header at\n // all. The wire type is unchanged (`fields[].label?: string`, both ends):\n // this resolves TO a string rather than widening the contract.\n if (result.fields?.length && dataset.measures?.length) {\n const measureByName = new Map(dataset.measures.map((m) => [m.name, m]));\n for (const f of result.fields) {\n const m = measureByName.get(f.name) ?? measureByName.get(f.name.replace(/__compare$/, ''));\n if (!m) continue;\n // `undefined` from the resolver means \"nothing was picked\" — an absent\n // label, or a map with no usable entry. Nothing is written in that case:\n // this enrichment describes columns, it never invents a header.\n if (f.label == null) {\n const label = resolveI18nLabel(m.label, requestLocale);\n if (label !== undefined) f.label = label;\n }\n if (f.format == null && m.format) f.format = m.format;\n // ADR-0053 currency chain. A MONETARY measure resolves its display\n // currency from: explicit measure `currency` → source-field\n // `currencyConfig.defaultCurrency` → tenant default (`ctx.currency`). A\n // measure is monetary if it declares a currency OR aggregates a\n // `currency`-type field; non-monetary measures (count, avg of a plain\n // number) never receive a currency code.\n const fc = f as { currency?: string };\n const mc = m as { currency?: string };\n const meta = m.field ? this.sourceFieldMeta?.(dataset.object, m.field) : undefined;\n if (fc.currency == null) {\n const monetary = !!mc.currency || meta?.type === 'currency';\n if (monetary) {\n const resolved = mc.currency ?? meta?.defaultCurrency ?? context?.currency;\n if (resolved) fc.currency = resolved;\n }\n }\n // Percent scale chain (objectui#3136) — the currency chain's sibling.\n // A `%` format says how to PRINT a number, not what scale it is on, and\n // the two readings collide at exactly 1 (\"100%\" vs \"1%\"). Both answers\n // are in metadata, so answer here instead of leaving the renderer to\n // guess from the value's magnitude: a `ratio` is a 0–1 fraction by\n // definition, and any aggregate of a `percent` field (avg/min/max —\n // sum is already rejected as incoherent) keeps that field's own scale.\n if (f.percentScale == null) {\n f.percentScale = m.derived?.op === 'ratio' ? 'fraction' : percentScaleOf(meta);\n }\n }\n }\n\n // Enrich DIMENSION columns with their display `label` too, so a grouped\n // table header reads \"Status\" instead of the raw field name \"status\". The\n // measure-only enrichment above left dimension headers bare (the renderer\n // then fell back to the raw dimension name).\n //\n // This ENRICHES existing entries and deliberately mints none: whether a\n // column exists at all is the query layer's answer, and inventing one here\n // would describe a column the rows may not carry. #5537 was that gap read\n // from the wrong end — a selection whose base measures were ALL\n // filter-scoped ran no primary query, so `DatasetExecutor.runMeasurePass`\n // assembled a grid with dimension columns in every row and no dimension\n // entry in `fields` for this pass to enrich. Fixed where the grid is\n // assembled (see that method's #5537 note), which is also the only place\n // that knows what the active strategy actually projected.\n //\n // #5688 — the set to describe FROM is wider than `selection.dimensions`. A\n // `timeDimensions` entry that resolves a granularity is GROUPED BY, so it is\n // a result column even when the caller never listed it under `dimensions`\n // (#4033's `projectedDimensions`) — and reading `selection.dimensions` alone\n // left exactly that column carrying a `type` and no `label`, the blind spot\n // #5537's PR pinned as a control case. Widening the LOOKUP is not the same\n // as widening the projection: the loop still enriches only entries\n // `result.fields` already carries, so an entry that stays a pure window\n // contributes no column and receives no descriptor.\n //\n // Kept out of `selectedDims` deliberately. Drill metadata and row-value\n // label resolution above answer a different question — which dimensions the\n // caller GROUPED THE GRID BY, i.e. what a click can be turned back into\n // records — and widening those would change drill payloads and row values,\n // not table headers.\n const describableDims = [...selectedDims];\n for (const t of selection.timeDimensions ?? []) {\n if (describableDims.some((d) => d.name === t.dimension)) continue;\n const d = dataset.dimensions?.find((x) => x.name === t.dimension);\n if (d) describableDims.push(d);\n }\n if (result.fields?.length && describableDims.length) {\n const dimByName = new Map(describableDims.map((d) => [d.name, d]));\n const dimByField = new Map(describableDims.filter((d) => !!d.field).map((d) => [d.field as string, d]));\n for (const f of result.fields) {\n if (f.label != null) continue;\n // Result fields may be keyed by the dataset dimension NAME or the\n // underlying cube FIELD depending on strategy — match either.\n const d = dimByName.get(f.name) ?? dimByField.get(f.name);\n if (!d) continue;\n // [#6761] Same resolver, same locale, same \"write nothing on a miss\"\n // rule as the measure enrichment above.\n const label = resolveI18nLabel(d.label, requestLocale);\n if (label !== undefined) f.label = label;\n }\n }\n return result;\n }\n\n /**\n * Get cube metadata for discovery.\n */\n async getMeta(cubeName?: string): Promise<CubeMeta[]> {\n // If a fallback service is configured, merge its metadata with the registry\n const cubes = cubeName\n ? [this.cubeRegistry.get(cubeName)].filter(Boolean) as Cube[]\n : this.cubeRegistry.getAll();\n\n return cubes.map(cube => ({\n name: cube.name,\n title: cube.title,\n measures: Object.entries(cube.measures).map(([key, measure]) => ({\n name: `${cube.name}.${key}`,\n type: measure.type,\n title: measure.label,\n })),\n dimensions: Object.entries(cube.dimensions).map(([key, dimension]) => ({\n name: `${cube.name}.${key}`,\n type: dimension.type,\n title: dimension.label,\n })),\n }));\n }\n\n /**\n * Generate SQL for a query without executing it (dry-run).\n */\n async generateSql(query: AnalyticsQuery, context?: ExecutionContext): Promise<{ sql: string; params: unknown[] }> {\n if (!query.cube) {\n throw new Error('Cube name is required for SQL generation');\n }\n\n this.ensureCube(query);\n const ctx = await this.callCtx(query, context);\n const strategy = this.resolveStrategy(query, ctx);\n this.logger.debug(`[Analytics] generateSql on cube \"${query.cube}\" → ${strategy.name}`);\n\n return strategy.generateSql(query, ctx);\n }\n\n // ── Internal ─────────────────────────────────────────────────────\n\n /**\n * Ensure a cube exists for the given query and that it knows about every\n * measure referenced by the query.\n *\n * - If no cube is registered for `query.cube`, infer a minimal cube from\n * the query so downstream strategies (which assume `cube.sql` exists)\n * don't crash.\n * - If a cube exists but the query references measures that aren't in\n * `cube.measures` (e.g. `amount_sum`, `amount_avg` emitted by dashboard\n * widget translators), inject suffix-inferred Metric entries so the\n * strategies pick the right aggregation function and field.\n *\n * It is also where the three SOURCE-FIELD gates run, on every path out of this\n * method and always BEFORE the (possibly augmented) cube is registered:\n * {@link assertMeasureFields} (#4437), {@link assertDimensionFields} (#5520)\n * and {@link assertWhereFields} (#5669) — one per request key that can carry a\n * field name. All three answer the same question — does the object actually\n * have the column this member resolves to — and all three must answer it here,\n * because from the strategy onwards the answer is the driver's `no such\n * column`.\n *\n * They run in request-key order (measures → dimensions/timeDimensions →\n * where), so a query that gets several wrong is answered about one at a time,\n * naming a real mistake either way.\n */\n private ensureCube(query: AnalyticsQuery): void {\n const name = query.cube!;\n let cube = this.cubeRegistry.get(name);\n\n if (!cube) {\n // [#3867] Auto-inference below sets `cube.sql = name`, so from here on\n // the queried string IS a physical table name. Verify it names a\n // registered object BEFORE that happens — otherwise `/analytics/query`\n // is a way to aggregate over any table the connection can see, exactly\n // the hole #3770 closed on the data path. A registered Cube needs no\n // such check: it was authored, and its `sql` is whatever it declares.\n this.assertInferableCube(name);\n cube = this.inferCubeFromQuery(query);\n // [#4437] Validate the inferred measures' SOURCE FIELDS before the cube\n // is registered — a rejected query must leave no trace in the registry\n // (same rule the #3867 gate above keeps), or a retry would find a\n // \"registered\" cube carrying the bogus measure and sail straight to SQL.\n this.assertMeasureFields(query, cube, Object.keys(cube.measures));\n // [#5520] …and the dimensions', for exactly the same reason. On this path\n // `cube.dimensions` was minted from the query moments ago, so the bogus\n // spelling is in there — which is why the suggestion list is computed by\n // subtraction inside the gate rather than echoed verbatim.\n this.assertDimensionFields(query, cube, Object.keys(cube.dimensions));\n // [#5669] …and the `where`'s, third and last of the three request keys that\n // carry a field name. Its members are read from the filter TREE, not from\n // `cube.dimensions` — which on this path was minted from this very query,\n // bogus spelling included.\n this.assertWhereFields(query, cube, Object.keys(cube.dimensions));\n this.cubeRegistry.register(cube);\n // A scalar query — only measures, no grouping (no `dimensions`/\n // `timeDimensions`) — is the first-class \"metric over an object\" path\n // (e.g. the `object-metric` KPI widget). Auto-inferring a count/sum cube\n // is the intended behaviour there, so log at debug. A query that groups\n // by an explicit dimension or time bucket almost certainly meant to hit a\n // registered cube; keep that at warn so a forgotten registration is loud.\n const isScalarMetric =\n (query.dimensions?.length ?? 0) === 0 && (query.timeDimensions?.length ?? 0) === 0;\n const message =\n `[Analytics] No cube registered for \"${name}\"; auto-inferred a minimal cube ` +\n `(sql=\"${name}\", measures=${Object.keys(cube.measures).join(',') || '(none)'}, ` +\n `dimensions=${Object.keys(cube.dimensions).join(',') || '(none)'}). ` +\n `Define an explicit Cube in your stack for full control.`;\n if (isScalarMetric) this.logger.debug(message);\n else this.logger.warn(message);\n return;\n }\n\n // Cube exists — check for unknown measures referenced by the query and\n // augment the cube with suffix-inferred Metric definitions so callers\n // that pass `<field>_sum` / `<field>_avg` etc. get the right aggregation.\n //\n // [#5918] This is the SECOND measure mint, and it judges a dotted spelling\n // exactly as the ad-hoc one does — `mintableMeasureKey` owns the rule. It\n // has to: the ad-hoc path REGISTERS what it infers, so from the second\n // request onwards a cube minted moments ago by `inferCubeFromQuery` is\n // \"registered\" and arrives here. Measured on `origin/main` `01faeb13a`,\n // one service, two queries:\n //\n // ① measures: ['count'] → SELECT COUNT(*) … (warms the registry)\n // ② measures: ['owner.region_count_distinct'] → SELECT COUNT(DISTINCT region) AS \"owner.region_count_distinct\"\n //\n // i.e. the silent wrong column #5918 reports, reached through this loop\n // instead of that one. Refusing in only one of the two would have closed the\n // cold request and left every warm one exactly as it was.\n //\n // A measure the cube DECLARES is never minted, so it never reaches the\n // rule — including a declared DOTTED key, which `lookupMember` resolves by\n // direct hit and which this loop must therefore check for verbatim FIRST or\n // it would refuse a cube's own authored vocabulary.\n const extraMeasures: Record<string, any> = {};\n for (const m of query.measures || []) {\n if (cube.measures[m] || extraMeasures[m]) continue;\n const key = mintableMeasureKey(m, name);\n if (cube.measures[key] || extraMeasures[key]) continue;\n extraMeasures[key] = inferMeasure(key);\n }\n if (Object.keys(extraMeasures).length > 0) {\n const augmented: Cube = {\n ...cube,\n measures: { ...cube.measures, ...extraMeasures },\n };\n // [#4437] The cube's DECLARED measures are the ones a caller may name;\n // the suffix-inferred entries just added are a convenience, not a\n // vocabulary. Snapshot the declared list BEFORE registering the augmented\n // cube so the rejection can suggest what the caller could have meant —\n // and so a rejected query leaves the registry as it found it.\n this.assertMeasureFields(query, augmented, Object.keys(cube.measures));\n // [#5520] Dimensions are never augmented (nothing infers one), so the\n // authored/compiled list IS the vocabulary a caller may name — and the one\n // the rejection suggests.\n this.assertDimensionFields(query, augmented, Object.keys(cube.dimensions));\n // [#5669] The `where` gate resolves a filter member through dimensions AND\n // measures (that is what the strategies do for a filter member), so it is\n // handed the AUGMENTED cube — a caller filtering on a suffix-inferred\n // measure must be judged against the same bag the strategy will read.\n this.assertWhereFields(query, augmented, Object.keys(cube.dimensions));\n this.cubeRegistry.register(augmented);\n this.logger.debug(\n `[Analytics] Augmented cube \"${name}\" with inferred measures: ${Object.keys(extraMeasures).join(',')}`,\n );\n } else {\n // No inference happened — every measure is declared. Still validate: an\n // authored cube can declare a measure over a field the object dropped.\n this.assertMeasureFields(query, cube, Object.keys(cube.measures));\n this.assertDimensionFields(query, cube, Object.keys(cube.dimensions));\n this.assertWhereFields(query, cube, Object.keys(cube.dimensions));\n }\n }\n\n /**\n * [#4437] Reject a measure whose SOURCE FIELD the backing object does not\n * have, BEFORE the strategy compiles it into SQL.\n *\n * `inferMeasure` maps a suffix convention onto a field name and has no way to\n * know whether that field exists: `ghost_sum` happily became `SUM(ghost)`, the\n * driver threw `no such column`, and the caller got\n * `500 {\"code\":\"SQLITE_ERROR\",\"message\":\"Internal server error\"}` — a driver\n * error class on the wire, and nothing actionable, for what is a plain typo.\n * The DATA route has refused the same mistake with a `400 INVALID_FIELD`\n * naming the field since #4315/#4254; this is the analytics half of that\n * answer, and it is deliberately the SAME envelope (`code`/`field`/`object`/\n * `param`) so one mistake has one shape across both routes.\n *\n * What it checks, and what it deliberately does not:\n *\n * - Only when the cube's `sql` is a bare OBJECT NAME. An authored cube whose\n * `sql` is a real SQL expression has no field list to check against.\n * - Only when {@link AnalyticsServiceConfig.getObjectFieldNames} answers.\n * Absent hook / unknown object → stand down (see the config field's doc).\n * - Only measures whose source is a BARE COLUMN. `count(*)` has no source\n * field, and a dotted reference (`account.industry`) resolves through a\n * join whose target this check cannot see — both pass through untouched.\n * - `id` / `created_at` / `updated_at` are admitted unconditionally, matching\n * the data path's `resolveQueryFields`: they are engine-assigned rather than\n * declared, and a gate stricter than the engine it guards would reject\n * queries that used to work.\n *\n * [#5918] Its `stripPrefix` below is deliberately NOT narrowed the way the two\n * MINTS were. This is a RESOLVER — it mirrors `lookupMember`'s tiers to answer\n * \"which Metric will the strategy read\", and that tier order did not change.\n * What changed is what can reach it: a dotted measure is now either a\n * `<cube>.` qualifier or a key the cube itself declares, because every other\n * dotted spelling is refused at the mint before this gate runs.\n */\n private assertMeasureFields(query: AnalyticsQuery, cube: Cube, declaredMeasures: string[]): void {\n const probe = this.getObjectFieldNames;\n if (!probe) return;\n const measures = query.measures ?? [];\n if (measures.length === 0) return;\n\n const object = typeof cube.sql === 'string' ? cube.sql.trim() : '';\n if (!object || !BARE_IDENTIFIER.test(object)) return;\n const fieldNames = probe(object);\n if (!fieldNames || fieldNames.length === 0) return;\n const known = new Set<string>([...fieldNames, 'id', 'created_at', 'updated_at']);\n\n const stripPrefix = (m: string) => (m.includes('.') ? m.split('.').slice(1).join('.') : m);\n /** The source field a measure aggregates, or null when there is nothing to check. */\n const sourceFieldOf = (measure: string): string | null => {\n const metric = cube.measures[stripPrefix(measure)] as { type?: string; sql?: unknown } | undefined;\n if (!metric) return null;\n // `count(*)` is the one legitimately field-less aggregate.\n if (metric.type === 'count' && (metric.sql === '*' || metric.sql == null)) return null;\n const source = typeof metric.sql === 'string' ? metric.sql.trim() : '';\n if (!source || source === '*' || !BARE_IDENTIFIER.test(source)) return null;\n return source;\n };\n\n // Two passes so the rejection can suggest the measures that WOULD have\n // worked. On the auto-inference path `cube.measures` already carries the\n // caller's own bogus spelling (it was inferred from the query moments ago),\n // so echoing the cube's measure list verbatim would offer the typo back as\n // a valid alternative — the one suggestion guaranteed to be wrong.\n const invalid = new Set<string>();\n for (const measure of measures) {\n const source = sourceFieldOf(measure);\n if (source && !known.has(source)) invalid.add(stripPrefix(measure));\n }\n if (invalid.size === 0) return;\n const usable = declaredMeasures.filter((m) => !invalid.has(m));\n\n for (const measure of measures) {\n const source = sourceFieldOf(measure);\n if (!source || known.has(source)) continue;\n\n const err = new Error(\n `Measure '${measure}' on cube '${cube.name}' aggregates field '${source}', which object ` +\n `'${object}' does not have. ` +\n `Valid measures: ${usable.join(', ') || '(none)'}. ` +\n `Other measures are inferred from the object's OWN fields as ` +\n `'<field>_sum' / '_avg' / '_min' / '_max' / '_count_distinct', so check the spelling of ` +\n `'${source}' — known fields: ${[...fieldNames].sort().join(', ')}.`,\n ) as Error & { code?: string; status?: number; field?: string; object?: string; param?: string; measure?: string };\n err.code = 'INVALID_FIELD';\n err.status = 400;\n err.field = source;\n err.object = object;\n err.param = 'measures';\n err.measure = measure;\n throw err;\n }\n }\n\n /**\n * [#5520] Reject a DIMENSION whose source field the backing object does not\n * have, BEFORE the strategy compiles it into `GROUP BY`.\n *\n * The symmetric half of {@link assertMeasureFields}. #4437 closed the measure\n * side and stopped there, so the identical mistake one request key over still\n * reached the driver:\n *\n * ```\n * POST /analytics/query {\"cube\":\"crm_account\",\"measures\":[\"account_count\"],\"dimensions\":[\"bogus_dim\"]}\n * → 500 {\"code\":\"SQLITE_ERROR\",\"message\":\"Internal server error\"}\n *\n * POST /analytics/dataset/query {\"selection\":{\"dimensions\":[\"bogus_dim\"],…}}\n * → 500 {\"code\":\"ANALYTICS_QUERY_FAILED\",\n * \"error\":\"SELECT bogus_dim AS \\\"bogus_dim\\\", … GROUP BY bogus_dim - no such column: bogus_dim\"}\n * ```\n *\n * A driver error class as the caller's `error.code` is the ADR-0112 violation\n * #4437 named, and the dataset face additionally echoed the generated\n * statement — physical table and column names — back to the caller. The\n * envelope here is deliberately the SAME as the measure gate's\n * (`INVALID_FIELD`/400 + `field`/`object`/`param`), because \"the query names a\n * field the object does not have\" is ONE mistake and must have one wire shape\n * whichever member kind carried it.\n *\n * What it checks, and what it deliberately does not:\n *\n * - **Both dimension keys.** `query.dimensions` and `query.timeDimensions`\n * land in the same `cube.dimensions` bag, are resolved by the same\n * `lookupMember`, and produced the same 500 (a bogus time dimension became\n * `date_trunc('month', bogus_at)`); `param` reports which key carried it.\n * - **An UNDECLARED but real field stays legal.** `dimensions: ['phone']` on a\n * cube that never declared `phone` groups by `phone` today — the dimension\n * twin of measure auto-inference, and an established contract. So the\n * question asked is \"does the OBJECT have this field\", never \"did the cube\n * declare this dimension\". An undeclared member is checked against the\n * object under the name the strategies would use as the column (their own\n * `resolveDimensionSql`/`resolveFieldName` fallback: the member itself).\n * - Only when the cube's `sql` is a bare OBJECT NAME, only when\n * {@link AnalyticsServiceConfig.getObjectFieldNames} answers, and only for\n * sources that are BARE COLUMNS — same three stand-downs as the measure\n * gate, for the same reasons (no field list to check against; nothing\n * authoritative to consult; a dotted reference resolves through a join whose\n * target this gate cannot see, so it belongs to the join allowlist).\n * - `id` / `created_at` / `updated_at` are admitted unconditionally, matching\n * the data path's `resolveQueryFields`.\n *\n * Runs after the measure gate and before the `where` gate on each `ensureCube`\n * path, so a query that gets several wrong is answered about its measure\n * first — one rejection at a time, naming a real mistake either way.\n */\n private assertDimensionFields(query: AnalyticsQuery, cube: Cube, declaredDimensions: string[]): void {\n const probe = this.getObjectFieldNames;\n if (!probe) return;\n /** Every dimension this query names, tagged with the request key it came from. */\n const members: Array<{ member: string; param: 'dimensions' | 'timeDimensions' }> = [\n ...(query.dimensions ?? []).map((member) => ({ member, param: 'dimensions' as const })),\n ...(query.timeDimensions ?? []).map((td) => ({ member: td.dimension, param: 'timeDimensions' as const })),\n ];\n if (members.length === 0) return;\n\n const object = typeof cube.sql === 'string' ? cube.sql.trim() : '';\n if (!object || !BARE_IDENTIFIER.test(object)) return;\n const fieldNames = probe(object);\n if (!fieldNames || fieldNames.length === 0) return;\n const known = new Set<string>([...fieldNames, 'id', 'created_at', 'updated_at']);\n\n /**\n * The `cube.dimensions` key a member resolves to (for the suggestion list)\n * and the column it groups by — `source: null` meaning \"nothing to check\".\n *\n * [#5669] The body moved to the module-level {@link resolveMemberSource},\n * shared with the `where` gate. `'dimension'` keeps this call site's\n * resolution exactly as #5520 wrote it: `cube.dimensions` only, matching\n * `resolveDimensionSql` / `resolveFieldName(…, 'dimension')`.\n */\n const resolve = (member: string): { key: string; source: string | null } =>\n resolveMemberSource(cube, member, 'dimension');\n\n // Two passes, for the reason the measure gate has two: on the auto-inference\n // path `cube.dimensions` was minted from this very query, so echoing its keys\n // verbatim would offer the caller their own typo back as a valid alternative.\n const invalid = new Set<string>();\n for (const { member } of members) {\n const { key, source } = resolve(member);\n if (source && !known.has(source)) invalid.add(key);\n }\n if (invalid.size === 0) return;\n const usable = declaredDimensions.filter((d) => !invalid.has(d));\n\n for (const { member, param } of members) {\n const { source } = resolve(member);\n if (!source || known.has(source)) continue;\n\n const kind = param === 'timeDimensions' ? 'Time dimension' : 'Dimension';\n const verb = param === 'timeDimensions' ? 'buckets' : 'groups by';\n const err = new Error(\n `${kind} '${member}' on cube '${cube.name}' ${verb} field '${source}', which object ` +\n `'${object}' does not have. ` +\n `Valid dimensions: ${usable.join(', ') || '(none)'}. ` +\n `Any of the object's OWN fields may also be used as a dimension without the cube ` +\n `declaring it, so check the spelling of ` +\n `'${source}' — known fields: ${[...fieldNames].sort().join(', ')}.`,\n ) as Error & { code?: string; status?: number; field?: string; object?: string; param?: string; dimension?: string };\n err.code = 'INVALID_FIELD';\n err.status = 400;\n err.field = source;\n err.object = object;\n err.param = param;\n err.dimension = member;\n throw err;\n }\n }\n\n /**\n * [#5669] Reject a `where` member whose source field the backing object does\n * not have, BEFORE the strategy compiles it into `WHERE`.\n *\n * The third and last param of one defect. #4437 gated `measures`, #5520 gated\n * `dimensions`/`timeDimensions`, and the filter face — the request key that\n * most often carries a hand-typed field name — had no gate at all:\n *\n * ```\n * POST /analytics/query {\"cube\":\"crm_account\",\"measures\":[\"count\"],\"where\":{\"bogus_col\":\"x\"}}\n * → SELECT COUNT(*) AS \"count\" FROM \"crm_account\" WHERE bogus_col = $1\n * → 500 {\"code\":\"SQLITE_ERROR\",\"message\":\"Internal server error\"}\n * ```\n *\n * Same envelope as its two siblings (`INVALID_FIELD`/400 + `field`/`object`/\n * `param`), because \"the query names a field the object does not have\" is ONE\n * mistake whichever request key carried it, and the DATA route has answered it\n * that way since #4315/#4254 (`resolveQueryFields`).\n *\n * # Where the field names come from: the SQL producer's own reader\n *\n * The members are collected through `normalizeAnalyticsFilterTree` +\n * `collectFilterLeaves` — the SAME pair both strategies call to build the\n * predicate. This is deliberate and is the whole reason this gate is not a\n * second filter-tree walker: a hand-rolled walk would have to re-derive\n * `$and`/`$or`/`$not` recursion, `$`-prefixed operator keys, `$between`\n * lowering, the nested-relation dot flattening (`{owner: {region: 'NA'}}` →\n * member `owner.region`) and the #5334 array lowering, and every divergence\n * would show up as \"the field the gate saw\" not being \"the column that reached\n * SQL\" — in either direction (a phantom rejection, or a hole).\n * `collectFilterLeaves` discards structure, which is exactly right here:\n * whether a predicate sits under an `$or` changes nothing about whether its\n * column exists. (Its doc's warning — never rebuild a predicate from this list\n * — does not apply; this gate builds nothing.)\n *\n * # Three stand-downs at query level, plus the per-member ones\n *\n * - No {@link AnalyticsServiceConfig.getObjectFieldNames}, cube `sql` that is\n * not a bare object name, or a probe that cannot answer for the object — the\n * same three tiers as the measure and dimension gates, for the same reasons.\n * - A `where` the normalizer REFUSES (an unknown operator, a non-array\n * `$and`, an unlowerable filter array) is not judged here: this gate stands\n * down and lets the refusal happen where it already does. Those inputs\n * already answer `INVALID_FILTER`/400 from the strategy (#5352/#5367's\n * geography, not this gate's), and pulling them forward into `ensureCube`\n * would newly refuse them on the draft-preview path too, whose\n * `matchesWhere` never consults the normalizer at all. A field gate that\n * cannot read the tree has nothing to say about it.\n * - Per member, {@link resolveMemberSource} stands down on an expression `sql`\n * and on a dotted relation traversal — for the dimension gate's reasons.\n *\n * # Array `where` IS gated, and #5353's fix did not change that\n *\n * Since #5334 an array `where` is lowered by `normalizeAnalyticsFilterTree`\n * and compiles to the identical predicate — a measured fact,\n * `where: [['bogus_col','=','x']]` and `where: {bogus_col: 'x'}` both produce\n * `WHERE bogus_col = $1` and hand `executeAggregate` the same\n * `{bogus_col: 'x'}`. Gating one spelling and not the other would answer one\n * mistake two ways, which is the split this whole gate family exists to close.\n *\n * `inferCubeFromQuery` used to skip an array `where` when minting the ad-hoc\n * cube's `dimensions` — a separate question (the cube's dimension VOCABULARY,\n * not which columns reach the driver), fixed by #5353 by lowering before\n * reading keys. Because this gate reads filter LEAVES rather than\n * `cube.dimensions`, that fix could not change its verdicts, and measurement\n * confirms it did not: the array where's keys now reach `cube.dimensions`, so\n * {@link resolveMemberSource} takes the DECLARED-dimension branch for those\n * members instead of the undeclared-bare-column one — and both branches yield\n * the same `source` for the same member, since the minted dimension's `sql` IS\n * the member name. What did change is the rejection's suggestion list, in the\n * direction that closes the split: `Valid filter members:` now reads the same\n * for both spellings of one filter.\n */\n private assertWhereFields(query: AnalyticsQuery, cube: Cube, declaredDimensions: string[]): void {\n const probe = this.getObjectFieldNames;\n if (!probe) return;\n const where = (query as { where?: unknown }).where;\n if (!where || typeof where !== 'object') return;\n\n const object = typeof cube.sql === 'string' ? cube.sql.trim() : '';\n if (!object || !BARE_IDENTIFIER.test(object)) return;\n const fieldNames = probe(object);\n if (!fieldNames || fieldNames.length === 0) return;\n const known = new Set<string>([...fieldNames, 'id', 'created_at', 'updated_at']);\n\n /** Every member the compiled predicate will bind against, structure discarded. */\n let members: string[];\n try {\n members = collectFilterLeaves(normalizeAnalyticsFilterTree(query)).map((leaf) => leaf.member);\n } catch {\n // A `where` this layer refuses outright — see the stand-down note above.\n return;\n }\n if (members.length === 0) return;\n\n // Two passes, for the reason the measure and dimension gates have two: on the\n // auto-inference path `inferCubeFromQuery` mints the `where`'s own field keys\n // into `cube.dimensions` — since #5353 for the array spelling too — so\n // echoing that bag verbatim would offer the caller their own typo back as a\n // valid filter member.\n const invalid = new Set<string>();\n for (const member of members) {\n const { key, source } = resolveMemberSource(cube, member, 'any');\n if (source && !known.has(source)) invalid.add(key);\n }\n if (invalid.size === 0) return;\n const usable = declaredDimensions.filter((d) => !invalid.has(d));\n\n for (const member of members) {\n const { source } = resolveMemberSource(cube, member, 'any');\n if (!source || known.has(source)) continue;\n\n const err = new Error(\n `Filter member '${member}' in 'where' on cube '${cube.name}' constrains field ` +\n `'${source}', which object '${object}' does not have. ` +\n `Valid filter members: ${usable.join(', ') || '(none)'}. ` +\n `Any of the object's OWN fields may also be filtered on without the cube ` +\n `declaring it, so check the spelling of ` +\n `'${source}' — known fields: ${[...fieldNames].sort().join(', ')}.`,\n ) as Error & { code?: string; status?: number; field?: string; object?: string; param?: string; member?: string };\n err.code = 'INVALID_FIELD';\n err.status = 400;\n err.field = source;\n err.object = object;\n err.param = 'where';\n err.member = member;\n throw err;\n }\n }\n\n /**\n * [#3867] Gate on the cube auto-inference path: a name with no registered\n * Cube may only be inferred into one if it is a registered object.\n *\n * Rejects with `status: 404` / `code: 'CUBE_NOT_FOUND'` so the HTTP boundary\n * answers \"no such cube\" instead of letting the name reach the driver as a\n * table and surfacing whatever the driver says about it. The message names\n * both ways the request could be made valid, because from here the two are\n * genuinely indistinguishable: register a Cube, or register the object.\n *\n * Skips when `isRegisteredObject` was not supplied — see the config field's\n * doc for why that tier is a deliberate stand-down and not a hole.\n */\n private assertInferableCube(name: string): void {\n const isRegisteredObject = this.isRegisteredObject;\n if (!isRegisteredObject) {\n if (!this.warnedNoObjectRegistry) {\n this.warnedNoObjectRegistry = true;\n this.logger.warn(\n '[Analytics] no object-registry hook configured — the cube-inference existence gate ' +\n '(#3867) is INACTIVE for this service; an unregistered cube name reaches the driver ' +\n 'as a raw table name.',\n );\n }\n return;\n }\n if (isRegisteredObject(name)) return;\n const err = new Error(\n `Cube '${name}' not found: no cube is registered under that name, and it is not a ` +\n `registered object either (a cube can only be auto-inferred from a registered object). ` +\n `Define a Cube in your stack, or check the object name.`,\n ) as Error & { code?: string; status?: number; cube?: string };\n err.code = 'CUBE_NOT_FOUND';\n err.status = 404;\n err.cube = name;\n throw err;\n }\n\n /** Build a minimal Cube from the fields referenced by an AnalyticsQuery. */\n private inferCubeFromQuery(query: AnalyticsQuery): Cube {\n const cubeName = query.cube!;\n const measures: Record<string, any> = {};\n const dimensions: Record<string, any> = {};\n\n // [#5739] Strip the `<cube>.` QUALIFIER, and nothing else.\n //\n // The predecessor (`stripPrefix`) dropped the first segment of ANY dotted\n // member, which conflated two different facts wearing the same punctuation:\n //\n // `deal.stage` — the canonical analytics QUALIFIER. `getMeta` hands\n // members out cube-prefixed and callers echo them back,\n // so the prefix is noise and stripping it is right.\n // `owner.region` — a relation TRAVERSAL. Stripping it minted\n // `dimensions.region = {sql: 'region'}`, a BASE-TABLE\n // column, and `lookupMember`'s \"plain second-segment\"\n // tier then found it BEFORE its synthetic-traversal tier\n // could hand the dotted path to the JOIN machinery. Where\n // the base table happened to carry a same-named column\n // that filtered/grouped the WRONG column with no error to\n // read; where it did not, the 400 named `region` for a\n // caller who wrote `owner.region`.\n //\n // Only the first is a qualifier, and only the first is stripped. Everything\n // else is minted VERBATIM (`{sql: 'owner.region'}`), which is precisely what\n // `lookupMember`'s synthetic tier already hands the strategies for an\n // undeclared dotted member — so the ad-hoc path now compiles the traversal\n // the array `where` spelling has compiled all along, and the two spellings\n // converge instead of disagreeing. Maintainer ruling, 2026-08-06 (#5739).\n //\n // Scope: this governs the DIMENSION-shaped mints (`dimensions`, the\n // `where`'s field keys, `timeDimensions`), which SERVE a traversal. The\n // measure mint applies the same qualifier rule but ends the other way —\n // `mintableMeasureKey` refuses a non-qualifier dot outright, because the\n // measure side has no traversal to serve (#5918, and the loop below).\n const stripCubeQualifier = (m: string): string => {\n const dot = m.indexOf('.');\n if (dot < 0) return m;\n return m.slice(0, dot) === cubeName ? m.slice(dot + 1) : m;\n };\n\n // Always provide a default `count` measure\n measures.count = { name: 'count', label: 'Count', type: 'count', sql: '*' };\n\n for (const m of query.measures || []) {\n // [#5918] MEASURES no longer take the blanket strip #5739 left them with.\n // That strip cast `owner.region_count_distinct` onto the BASE `region`\n // column — silently where the object had one, and as a #4437 400 naming\n // the stripped tail where it did not. Neither is the caller's query.\n //\n // The ruling here is NOT #5739's (mint the traversal verbatim): measures\n // have no traversal tier to converge on, so there is nothing correct to\n // converge to. It is a loud refusal instead — see `mintableMeasureKey`,\n // which owns the rule and the envelope, and which the augmentation mint in\n // `ensureCube` shares so the same spelling gets the same answer on a warm\n // registry. Maintainer ruling, 2026-08-07 (#5918).\n const key = mintableMeasureKey(m, cubeName);\n if (measures[key]) continue;\n const inferred = inferMeasure(key);\n measures[key] = inferred;\n }\n\n for (const d of query.dimensions || []) {\n const key = stripCubeQualifier(d);\n if (dimensions[key]) continue;\n dimensions[key] = { name: key, label: key, type: 'string', sql: key };\n }\n\n // The `where`'s field keys seed dimensions too. LOWER FIRST, then read keys:\n // the rule this bag has always followed is \"the `where`'s own top-level keys\n // are field names\", and the only reason an ARRAY `where` was skipped here is\n // that it was not a filter when the code was written.\n //\n // [#5353] The `!Array.isArray(query.where)` guard this replaces predates\n // #5334. Since #5334 an array `where` IS a filter — lowered by\n // `lowerAnalyticsWhere` and compiling to a byte-identical predicate — so\n // skipping it meant ONE filter, spelled two ways, minted two different\n // cubes: `{stage: 'won'}` seeded `dimensions.stage`, `[['stage','=','won']]`\n // seeded nothing. Lowering first makes the spelling stop mattering, which is\n // the same fix #5334 applied one layer down.\n let lowered: Record<string, unknown> | null = null;\n try {\n lowered = lowerAnalyticsWhere(query);\n } catch {\n // A `where` the lowering REFUSES (an unlowerable array) is not judged\n // here — the same stand-down `assertWhereFields` makes, for the same\n // reason: that refusal already happens in the strategy with an\n // `INVALID_FILTER`/400 envelope (#5352/#5367), and raising it from\n // `ensureCube` instead would move the answer's geography and would newly\n // refuse the draft-preview path, whose `matchesWhere` never consults the\n // normalizer at all. A cube minted without those keys is exactly what a\n // query that is about to be refused needs.\n }\n if (lowered) {\n // `conjunctFieldKeys` descends `$and` — which the LOWERING introduces for a\n // flat array (`[[a,…],[b,…]]` → `{$and: [{a…},{b…}]}`) — and not `$or` /\n // `$not`, which contribute no key on either spelling. Deliberately NOT\n // `collectFilterLeaves`: the leaves answer \"what does the compiled\n // predicate BIND\", this bag answers \"what may a caller NAME\", and the two\n // genuinely differ — `{stage: {$in: []}}` lowers to the boolean constant\n // FALSE, binding nothing while still naming `stage`.\n for (const key of conjunctFieldKeys(lowered)) {\n // [#5739] Dotted keys ride this loop too, and that is the FOLD #5353 left\n // for this issue. Until the ruling, a dotted key was skipped here and\n // re-minted from the RAW object `where` by a separate residue loop —\n // stripped to its tail, so one filter got one answer per spelling: the\n // object spelling mis-cast `owner.region` to base `region`, the array\n // spelling minted nothing and compiled the traversal. One loop over the\n // LOWERED condition mints both spellings identically, and\n // `stripCubeQualifier` keeps them a traversal instead of a base column,\n // so the cube AND the compiled SQL now match on either spelling.\n const minted = stripCubeQualifier(key);\n if (dimensions[minted] || measures[minted]) continue;\n dimensions[minted] = { name: minted, label: minted, type: 'string', sql: minted };\n }\n }\n\n for (const td of query.timeDimensions || []) {\n const key = stripCubeQualifier(td.dimension);\n if (dimensions[key]) continue;\n dimensions[key] = {\n name: key, label: key, type: 'time', sql: key,\n granularities: ['day', 'week', 'month', 'quarter', 'year'],\n };\n }\n\n return {\n name: cubeName,\n title: cubeName,\n sql: cubeName,\n measures,\n dimensions,\n public: false,\n };\n }\n\n /**\n * Walk the strategy chain and return the first strategy that can handle the\n * query. `skip` excludes strategies that already proved incapable at\n * execution time (see {@link query}'s RAW_SQL_UNSUPPORTED fallback).\n */\n private resolveStrategy(\n query: AnalyticsQuery,\n ctx: StrategyContext,\n skip?: Set<AnalyticsStrategy>,\n ): AnalyticsStrategy {\n for (const strategy of this.strategies) {\n if (skip?.has(strategy)) continue;\n if (strategy.canHandle(query, ctx)) {\n return strategy;\n }\n }\n // [#7598] Name the one decline that is about the QUERY rather than about\n // the deployment. Since the 2026-08-12 ruling `NativeSQLStrategy` declines\n // a cross-field `{ $field }` comparison so it routes to the engine path —\n // so a host advertising `nativeSql` WITHOUT an aggregate bridge now reaches\n // this exit for a filter it used to (wrongly) compile. The bare message\n // below would send that operator off to check their driver configuration,\n // which is not the problem: every other query on that cube still works.\n //\n // Reachability, measured: NOT from `AnalyticsServicePlugin`, whose default\n // `queryCapabilities` derives BOTH flags from the bridges it wired\n // (`objectqlAggregate: !!executeAggregate`) and auto-wires the aggregate\n // bridge from the engine — so a real deployment that has `nativeSql` has\n // `objectqlAggregate` too. Reachable only from a host that overrides\n // `queryCapabilities` by hand. Cheap to say, and the alternative is a dead\n // end that reads like a misconfiguration.\n const crossField = findCrossFieldComparand(lowerAnalyticsWhereQuietly(query));\n throw new Error(\n `[Analytics] No strategy can handle query for cube \"${query.cube}\". ` +\n `Checked: ${this.strategies.map(s => s.name).join(', ')}${skip?.size ? ` (skipped at runtime: ${[...skip].map((s) => s.name).join(', ')})` : ''}. ` +\n (crossField\n ? `This query's filter compares against the field reference ` +\n `{ \"$field\": \"${crossField.ref}\" } under \"${crossField.op}\" on \"${crossField.field}\", and ` +\n `NativeSQLStrategy DECLINES a cross-field comparison so that it routes to the ObjectQL ` +\n `engine path — whose driver compiles it and enforces the #5222 rulings with metadata it ` +\n `owns (#7598). No such path is configured here, so the capability is unavailable on this ` +\n `deployment: supply an \\`executeAggregate\\` bridge (the plugin auto-wires one from the ` +\n `engine), or compare against a literal value. Every other query on this cube is ` +\n `unaffected. `\n : '') +\n 'Ensure a compatible driver is configured or a fallback service is registered.',\n );\n }\n}\n\n/**\n * [#7598] A query's `where`, lowered — the same input the strategies scan, so\n * the diagnostic above describes the filter the decline actually saw.\n *\n * Quiet by construction: a `where` that cannot even be lowered is refused\n * downstream with its own message and envelope, and there is no reference to\n * find in one that does not lower.\n */\nfunction lowerAnalyticsWhereQuietly(query: AnalyticsQuery): unknown {\n try {\n return lowerAnalyticsWhere(query);\n } catch {\n return null;\n }\n}\n\n/**\n * [#5918] The `cube.measures` KEY a request's `measures` entry may be MINTED\n * under — or a loud refusal when the entry is a dotted member.\n *\n * Two mint sites feed {@link inferMeasure}, and both go through here: the\n * ad-hoc mint in {@link AnalyticsService.inferCubeFromQuery} (no cube\n * registered) and the suffix-augmentation loop in\n * {@link AnalyticsService.ensureCube} (a cube exists but does not declare the\n * measure). They are the same act — inventing a Metric out of a request\n * spelling — so they must judge the spelling the same way.\n *\n * ## What is refused, and why it is a refusal rather than a traversal\n *\n * A `<cube>.` QUALIFIER is stripped, exactly as `inferCubeFromQuery`'s\n * `stripCubeQualifier` does for the dimension-shaped mints (#5739): `getMeta`\n * hands members out cube-prefixed and\n * callers echo them back, so that prefix is noise. **Every other dot is\n * refused.** The predecessor dropped the first segment of ANY dotted member,\n * which meant `owner.region_count_distinct` minted\n * `measures.region_count_distinct = {type:'count_distinct', sql:'region'}` — the\n * BASE table's own `region` column — and then:\n *\n * ```\n * NativeSQL → SELECT COUNT(DISTINCT region) AS \"owner.region_count_distinct\" FROM \"crm_account\"\n * ObjectQL → aggregations: [{field:'region', method:'count_distinct', alias:'owner.region_count_distinct'}]\n * ```\n *\n * No JOIN, no error, and a response column LABELLED with a relation attribute\n * whose number came from the base table — a wrong answer the caller cannot see\n * (measured on `origin/main` `01faeb13a`). Where the base object had no\n * same-named column it degraded instead to #4437's `400 INVALID_FIELD` naming\n * the STRIPPED tail (`Measure 'owner.score_sum' … aggregates field 'score'`) —\n * honest about what reached SQL, but naming a string the caller never wrote.\n *\n * #5739 fixed the same punctuation on the three DIMENSION-shaped mints by\n * minting the traversal verbatim, and that ruling deliberately does NOT carry\n * over here: `lookupMember`'s synthetic relation-traversal tier is\n * dimension-only (`if (kind === 'dimension')`), so a dotted measure has no\n * correct traversal answer to converge on. Minting it verbatim would only\n * re-route it into ObjectQL's `cannot evaluate a cross-object measure` — which\n * would be the wrong diagnosis for a plain typo like `total.sum`. Maintainer\n * ruling, 2026-08-07 (#5918, option 3): refuse the dotted measure LOUDLY, with\n * the caller's own spelling in the envelope. A genuine traversal measure\n * (`SUM(\"owner\".\"amount\")` + LEFT JOIN) would be a capability with its own\n * justification, not a side effect of a strip.\n *\n * The refusal deliberately reaches BOTH the typo (`total.sum`) and the genuine\n * traversal intent (`owner.amount_sum`): the two are lexically indistinguishable\n * on this path, and telling them apart would need field metadata the ad-hoc\n * path does not have (`getObjectFieldNames` answers names, not types or\n * relation targets). One honest 400 for both beats a metadata capability nobody\n * has asked for.\n *\n * A measure the cube DECLARES under a dotted key is not this function's\n * business — it was authored, not minted, and `lookupMember` resolves it by\n * direct hit. Both call sites check that first.\n */\nfunction mintableMeasureKey(member: string, cubeName: string): string {\n const dot = member.indexOf('.');\n if (dot < 0) return member;\n if (member.slice(0, dot) === cubeName) return member.slice(dot + 1);\n\n throw invalidMemberError(\n `[Analytics] Measure '${member}' on cube '${cubeName}' is a DOTTED member, and ` +\n `measures do not traverse relationships — only dimensions do — so there is no ` +\n `related column for this to aggregate. Until #5918 the prefix was silently ` +\n `dropped, so the aggregate ran against '${cubeName}' itself while the result ` +\n `column kept the label '${member}'. Aggregate one of the object's OWN fields ` +\n `instead ('<field>_sum' / '_avg' / '_min' / '_max' / '_count_distinct'), or ` +\n `declare a Cube whose measure names the related column in its own 'sql'. The ` +\n `only dot a measure may carry is the '${cubeName}.' qualifier.`,\n { member, param: 'measures', cube: cubeName },\n );\n}\n\n/**\n * Infer a Metric definition from a measure key name.\n *\n * Recognised suffix conventions (matches dashboard widget translators that\n * emit measures like `<field>_sum`, `<field>_avg`):\n *\n * | Suffix | Aggregation |\n * |:-------------------|:----------------|\n * | `count` | `count(*)` |\n * | `_sum` | `sum(field)` |\n * | `_avg` / `_average`| `avg(field)` |\n * | `_min` | `min(field)` |\n * | `_max` | `max(field)` |\n * | `_count_distinct` | `count(distinct field)` |\n *\n * Anything else is treated as a `sum(<key>)` — best-effort default for an\n * unknown numeric measure.\n */\nexport function inferMeasure(key: string): { name: string; label: string; type: 'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'; sql: string } {\n if (key === 'count') {\n return { name: 'count', label: 'Count', type: 'count', sql: '*' };\n }\n const suffixes: Array<[string, 'sum' | 'avg' | 'min' | 'max' | 'count_distinct']> = [\n ['_count_distinct', 'count_distinct'],\n ['_sum', 'sum'],\n ['_avg', 'avg'],\n ['_average', 'avg'],\n ['_min', 'min'],\n ['_max', 'max'],\n ];\n for (const [suffix, type] of suffixes) {\n if (key.endsWith(suffix)) {\n const field = key.slice(0, -suffix.length) || '*';\n return { name: key, label: key, type, sql: field };\n }\n }\n return { name: key, label: key, type: 'sum', sql: key };\n}\n\n/**\n * FallbackDelegateStrategy — Internal strategy for fallback service delegation.\n *\n * Automatically added to the strategy chain when `fallbackService` is configured.\n * Not exported — consumers who need explicit in-memory support should use\n * `InMemoryStrategy` from `@objectstack/driver-memory`.\n */\nclass FallbackDelegateStrategy implements AnalyticsStrategy {\n readonly name = 'FallbackDelegateStrategy';\n readonly priority = 30;\n\n canHandle(query: AnalyticsQuery, ctx: StrategyContext): boolean {\n if (!query.cube) return false;\n return !!ctx.fallbackService;\n }\n\n async execute(query: AnalyticsQuery, ctx: StrategyContext): Promise<AnalyticsResult> {\n return ctx.fallbackService!.query(query);\n }\n\n async generateSql(query: AnalyticsQuery, ctx: StrategyContext): Promise<{ sql: string; params: unknown[] }> {\n if (ctx.fallbackService?.generateSql) {\n return ctx.fallbackService.generateSql(query);\n }\n return {\n sql: `-- FallbackDelegateStrategy: SQL generation not supported for cube \"${query.cube}\"`,\n params: [],\n };\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Cube } from '@objectstack/spec/data';\n\n/**\n * CubeRegistry — Central registry for analytics cube definitions.\n *\n * Cubes can be registered from two sources:\n * 1. **Manifest definitions** — Explicit cube definitions in `objectstack.config.ts`.\n * 2. **Object schema inference** — Auto-generated cubes from ObjectQL object schemas.\n *\n * The registry is the single source of truth for cube metadata discovery\n * (used by `getMeta()` and the strategy chain).\n */\nexport class CubeRegistry {\n private cubes = new Map<string, Cube>();\n\n /** Register a single cube definition. Overwrites if name already exists. */\n register(cube: Cube): void {\n this.cubes.set(cube.name, cube);\n }\n\n /** Register multiple cube definitions at once. */\n registerAll(cubes: Cube[]): void {\n for (const cube of cubes) {\n this.register(cube);\n }\n }\n\n /** Get a cube definition by name. */\n get(name: string): Cube | undefined {\n return this.cubes.get(name);\n }\n\n /** Check if a cube is registered. */\n has(name: string): boolean {\n return this.cubes.has(name);\n }\n\n /** Return all registered cubes. */\n getAll(): Cube[] {\n return Array.from(this.cubes.values());\n }\n\n /** Return all cube names. */\n names(): string[] {\n return Array.from(this.cubes.keys());\n }\n\n /** Number of registered cubes. */\n get size(): number {\n return this.cubes.size;\n }\n\n /** Remove all cubes. */\n clear(): void {\n this.cubes.clear();\n }\n\n /**\n * Auto-generate a cube definition from an object schema.\n *\n * Heuristic rules:\n * - `number` fields → `sum`, `avg`, `min`, `max` measures\n * - `boolean` fields → `count` measure (count where true)\n * - All non-computed fields → dimensions\n * - `date`/`datetime` fields → time dimensions with standard granularities\n * - A default `count` measure is always added\n *\n * @param objectName - The snake_case object name (used as table/cube name)\n * @param fields - Array of field descriptors `{ name, type, label? }`\n */\n inferFromObject(\n objectName: string,\n fields: Array<{ name: string; type: string; label?: string }>,\n ): Cube {\n const measures: Record<string, any> = {\n count: {\n name: 'count',\n label: 'Count',\n type: 'count',\n sql: '*',\n },\n };\n const dimensions: Record<string, any> = {};\n\n for (const field of fields) {\n const label = field.label || field.name;\n\n // All fields become dimensions\n const dimType = this.fieldTypeToDimensionType(field.type);\n dimensions[field.name] = {\n name: field.name,\n label,\n type: dimType,\n sql: field.name,\n ...(dimType === 'time'\n ? { granularities: ['day', 'week', 'month', 'quarter', 'year'] }\n : {}),\n };\n\n // Numeric fields also become aggregation measures\n if (field.type === 'number' || field.type === 'currency' || field.type === 'percent') {\n measures[`${field.name}_sum`] = {\n name: `${field.name}_sum`,\n label: `${label} (Sum)`,\n type: 'sum',\n sql: field.name,\n };\n measures[`${field.name}_avg`] = {\n name: `${field.name}_avg`,\n label: `${label} (Avg)`,\n type: 'avg',\n sql: field.name,\n };\n }\n }\n\n const cube: Cube = {\n name: objectName,\n title: objectName,\n sql: objectName,\n measures,\n dimensions,\n public: false,\n };\n\n this.register(cube);\n return cube;\n }\n\n private fieldTypeToDimensionType(fieldType: string): string {\n switch (fieldType) {\n case 'number':\n case 'currency':\n case 'percent':\n return 'number';\n case 'boolean':\n return 'boolean';\n case 'date':\n case 'datetime':\n return 'time';\n default:\n return 'string';\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Filter Normalization for the Analytics Layer\n *\n * The analytics endpoint accepts filters via the canonical `where`\n * field per the unified Query DSL (`spec/data/query.zod.ts`):\n *\n * - MongoDB-style FilterCondition: `{ field: value }` /\n * `{ field: { $op: value } }` / `{ $and: [...] }` — defined in\n * `spec/data/filter.zod.ts` and used by `find()`, dashboard\n * widget `filter`, RLS, etc.\n *\n * `normalizeAnalyticsFilters` flattens the FilterCondition tree into\n * the internal array form used by the SQL/Mongo pipeline strategies.\n * Strategies stay simple — they only need to know one shape — and the\n * spec is honoured: dashboard metadata is authored once in the\n * canonical MongoDB form and the server normalizes at the boundary.\n *\n * # Coverage — a dropped predicate WIDENS the query, so nothing is dropped\n *\n * Failing to map an operator is not \"not supporting\" it: the predicate simply\n * disappears, the compiled SQL stays valid, and the query returns rows the\n * author excluded. It reads as a chart drawn over the whole dataset (#3650's\n * symptom) and is invisible to any test that asserts the emitted SQL string.\n * `$between`, `$startsWith`, `$endsWith` and `$null` each sat broken that way\n * (#4128), so what this maps is now a complete capability claim over\n * `filter.zod.ts`'s authorable vocabulary:\n *\n * - mapped 1:1 — `$eq` `$ne` `$gt` `$gte` `$lt` `$lte` `$in` `$nin`\n * `$contains` `$notContains` `$startsWith` `$endsWith`;\n * - value-DEPENDENT, so resolved explicitly rather than through the map —\n * `$null` and `$exists`, whose meaning flips with their boolean;\n * - lowered — `$between`, which becomes its two bounds so each strategy's\n * existing upper-bound handling applies the calendar-day whole-day rule\n * (see the note at the lowering);\n * - structural — `$and` / `$or` / `$not`, carried as tree nodes;\n * - anything else THROWS. An operator outside the vocabulary is a caller\n * error, and a loud one beats a silently widened read — the call\n * driver-memory made for the same shape in #3948.\n *\n * `$or` / `$not` were the last of that family, and they were dropped for a\n * structural reason rather than an oversight: this module produced a flat\n * ARRAY, which cannot carry a disjunction. So an author's `{$or: […]}`\n * vanished from the WHERE clause and the widget drew every row. The output is\n * now a {@link NormalizedFilterNode} tree, and each strategy compiles it the\n * way its own backend expresses a disjunction.\n *\n * # `null` is TRUE, and TRUE is a VALUE — not \"nothing happened\" (#5325)\n *\n * {@link buildNode} returns `null` for a condition that constrains nothing\n * (`{}`, an all-`{}` `$and`). That `null` is the boolean constant TRUE, and the\n * two places a compiler forgets it are exactly where this one used to be wrong —\n * the same two squares `read-scope-sql.ts` was wrong on (#5297), because this\n * module was written from it:\n *\n * - TRUE is the AND identity, so dropping it from a `$and` is right — but it\n * ABSORBS a `$or`: one TRUE disjunct makes the whole disjunction TRUE.\n * Filtering it out (`{$or: [{}, {a: 1}]}` → `a = 1`) silently NARROWED a\n * widget's filter to its surviving branches.\n * - `NOT TRUE ≡ FALSE`, so `{$not: {}}` is the zero-row predicate. Producing\n * `null` for it meant no `WHERE` was emitted at all and the widget charted\n * the ENTIRE dataset — the #3650 / #4128 silent-widening class again.\n *\n * FALSE therefore has a spelling of its own ({@link NormalizedFilterNode}'s\n * `const` kind) instead of being representable only as silence. Every compiler\n * of this tree implements it: `native-sql-strategy.compileFilterNode`,\n * `objectql-strategy.filterNodeToCondition` and its display-SQL twin\n * `renderFilterNodeSql`.\n *\n * The EMPTY combinators complete the same boolean algebra (#5322 ruling):\n * `{$and: []}` is TRUE and `{$or: []}` is FALSE — this module used to refuse\n * both fail-closed while the five `FILTER_LOGIC_CASES` backends reduced them;\n * see the note inside {@link buildNode}'s combinator branch for the history\n * and the reasoning the ruling adopted.\n *\n * # `$not` is NULL-safe (#5146)\n *\n * SQL is three-valued and a `WHERE` keeps only TRUE, so a bare `NOT (col = ?)`\n * drops every row whose `col` is NULL — while `driver-memory`, `formula` and\n * (since #5296) `driver-sql` return those rows. One widget filter, two row sets,\n * chosen by whichever backend answered. #5146 ruled the JS answer canonical, and\n * {@link nullSafeNegationOperand} applies the same leaf-wise totalisation\n * `sql-driver.ts` and `read-scope-sql.ts` apply.\n *\n * The rewrite lives HERE rather than in `native-sql-strategy` on purpose: at\n * this layer the guard is STRUCTURE (one more `{col: {$null: false}}` conjunct),\n * not a SQL trick, so it survives `filterNodeToCondition` handing the tree to\n * the ObjectQL engine and holds on any driver behind it — including one that is\n * not NULL-safe by itself. Guarding only in the SQL strategy would make \"what\n * does this widget's `$not` mean\" depend on which backend caught it, which is\n * what #5146 spent a round eliminating. The cost is that the engine path can\n * guard twice (this rewrite, then `driver-sql`'s own); that is idempotent —\n * `NOT (c IS NOT NULL AND (c IS NOT NULL AND c = v))` is the same predicate —\n * so it buys portability for one redundant conjunct.\n *\n * # `$ne` / `$nin` / `$notContains` are NULL-safe too (#5298)\n *\n * Same rule, same reason, one ruling later. The operators that carry their OWN\n * negation had the defect #5146 fixed for `$not`: a bare `col <> ?` is UNKNOWN\n * for a NULL column and the `WHERE` drops the row, while the JS backends return\n * it. Measured on this package's own fixture before the fix (#5977), for\n * `{stage: {$ne: 'won'}}` over rows whose `stage` is NULL:\n *\n * | path | was | now (= JS family) |\n * |----------------------------------------|-----------|-------------------|\n * | `NativeSQLStrategy` (raw SQL) | `2` | `2,3,4` |\n * | `ObjectQLStrategy` display SQL echo | `2` | `2,3,4` |\n * | `ObjectQLStrategy` → engine condition | `2,3,4` | `2,3,4` |\n *\n * The engine column was already right, and that is the whole argument for\n * fixing it HERE: it was right because `driver-sql` guards for itself (#5962),\n * so the Cube face's answer depended on which compiler downstream caught the\n * leaf — three emitters, two answers. `fieldLeaves` now emits the guard as\n * STRUCTURE, an `or` of `notSet` with the comparison, so all three compile the\n * same predicate and none of them needs to know the rule. That is the same\n * trade the `$not` rewrite above took, including its cost: the engine path\n * guards twice, which is idempotent (`c IS NULL OR (c IS NULL OR c <> v)`).\n *\n * Which operators get the guard is NOT a new list — it is\n * {@link nullValueSatisfiesOperator} and {@link operatorIsNullTotal}, the same\n * pair `nullGuardForFieldSpec` consults for the `$not` rewrite, asked about one\n * operator instead of a whole field spec. A leaf is guarded exactly when a NULL\n * value SATISFIES the operator and the compiled leaf is not already total, which\n * is that pair's `allowNull` verdict. Hard-coding the three names would have put\n * a second polarity table in this file, free to drift from the first — and the\n * `$eq`/`$ne` arms of the existing one already turn on the COMPARAND (`$ne:\n * null` compiles to `set`, which is total and must never be widened), so a name\n * list would have been wrong as well as duplicated.\n *\n * # A `null` COMPARAND is a null predicate, not a value (#5332)\n *\n * `{stage: null}` compiled to `IS NULL` while `{stage: {$eq: null}}` compiled to\n * `stage = ''` — one meaning, two answers, inside this one file. The cause was\n * that the `$eq` / `$ne` pair fell through to {@link MONGO_TO_CUBE_OP} like any\n * other comparison and `stringifyForCube(null)` handed it the empty STRING, so a\n * \"stage is empty\" widget compared a real value against columns that are NULL\n * and charted zero rows — silently, with nothing for the author to read. On a\n * text column the `$ne` direction was worse than empty: `''` is a value rows\n * genuinely store, so \"stage is not empty\" EXCLUDED exactly the rows it was asked\n * to keep.\n *\n * The pair is not merely similar to `{$null: true|false}` — `driver-mongodb`\n * TRANSLATES `$null` into it — so {@link fieldLeaves} now emits the same\n * `notSet` / `set` leaves for all three spellings, and the #5146 guard table\n * moved in the same commit (see {@link nullValueSatisfiesOperator}); a guard that\n * still described the old emitter would have negated an always-false conjunction\n * and answered `{$not: {stage: {$eq: null}}}` with every row.\n *\n * # A comparand keeps its own TYPE — there is no round trip any more (#5526)\n *\n * A leaf's `values` used to be `string[]`, so every comparand was encoded to a\n * string on the way in (`stringifyForCube`) and GUESSED back into a type on the\n * way out (`recoverNumber`, behind `coerceFilterValueForSql` /\n * `coerceFilterValueForObjectQL`). An encoding whose alphabet is \"all strings\"\n * and whose decoder is \"does this string look like a number/boolean/null\" has no\n * escape, so author strings COLLIDED with the tokens the encoder wrote for other\n * types. Measured on `main` (#5526's table), for `{code: {$eq: v}}`:\n *\n * | author's `v` | bound (SQL) | bound (engine) |\n * |---|---|---|\n * | `'007'` | `7` (#5528: fixed) | `7` (#5528: fixed) |\n * | `'1.50'` | `1.5` (#5528: fixed)| `1.5` (#5528: fixed)|\n * | `'null'` | real `NULL` | real `null` |\n * | `'true'` | `1` | `true` |\n *\n * Every row of that table is one defect: a TEXT column storing the author's\n * spelling stops matching. `'007'` on SQLite compares an integer against a TEXT\n * column and is never equal; on Postgres `text = integer` is a type error. The\n * `'null'` row is worse than empty — a comparison against real NULL is UNKNOWN\n * for every row, so the widget can never draw anything. Zero-padded strings,\n * `'true'`/`'false'` as enum-ish codes and `'null'` as a literal label are all\n * ordinary business shapes (order numbers, SKUs, postcodes, dialling codes).\n *\n * #5528 narrowed the number half of the decoder (canonical spelling only) as a\n * STOPGAP and said so; this is the ruled fix. `values` is now `unknown[]`: the\n * comparand the author wrote travels through the tree untouched, and no\n * stringification happens at all except where a boundary genuinely demands it:\n *\n * - {@link toSqlBindValue} — the ONLY survivor, and it is one-way (a value →\n * its SQL bind form), never a decoder. It exists because a SQL driver cannot\n * bind every JS type: better-sqlite3 refuses a `boolean`, a `Date` and a\n * plain object. Nothing about it inspects a string.\n * - the LIKE family, whose comparand `filter.zod.ts` declares a `string`\n * (`$contains: z.string()`), so `like-pattern.ts` stringifies at the emitter\n * — the same `String(value)` `driver-sql`'s `applyLike` applies, which is\n * what keeps one `$contains` meaning one thing on both faces.\n *\n * The ObjectQL path needs NO conversion at all now: the engine compares against\n * the stored runtime type, and the value it receives is the author's own.\n *\n * Two shapes changed reading as a consequence, both toward fail-closed and both\n * pinned in `filter-value-type-fidelity.test.ts`:\n *\n * - `{name: {$contains: null}}` compiled to `LIKE '%%'` — matching EVERY\n * non-NULL row — because `stringifyForCube(null)` was `''`. It is now\n * `LIKE '%null%'`, which is what `driver-sql` has always compiled it to.\n * - `{amount: {$gt: null}}` compiled to `amount > ''`, a real comparison\n * against the empty string. It now binds NULL, so the predicate is UNKNOWN\n * and the widget draws nothing — the honest answer for an unordered\n * comparand, and the one `driver-memory` / `formula` give. (#5332 named this\n * comparand position as covered by no ruling and left the `''` placeholder\n * alone; deleting the encoder decides it by construction.)\n *\n * # A `where` ARRAY is lowered here, not dropped (#5334)\n *\n * `FilterArray` — `['stage', '=', 'won']`, `['and', […], […]]`, `[[…], […]]` —\n * is INPUT-ONLY authoring sugar (`spec/data/filter.zod.ts`, #5285), and #5158's\n * ruling C says every door into the runtime LOWERS it through the one\n * `parseFilterAST` sink before anything downstream sees a filter. #5329 closed\n * the engine's six entry points that way and deleted the four drivers' array\n * dialects. Analytics is the FIFTH door: it compiles `where` itself — to SQL\n * (`NativeSQLStrategy`) or to a `FilterCondition` for the engine\n * (`ObjectQLStrategy`) — so nothing upstream lowers for it.\n *\n * Until #5334 this function answered an array with `return null`: the WHOLE\n * `where` disappeared, no error, no trace, and the widget charted the entire\n * dataset — the #3650 / #4128 silent-widening class again, reached through the\n * array spelling. {@link normalizeAnalyticsFilterTree} now gives the same three\n * answers the engine door gives, so one query means one thing on every path.\n *\n * # Every refusal here is a 400, and SAYS so (#5352)\n *\n * All of the above only helps the author if the refusal REACHES them. Each\n * refusal in this module is a caller-shaped mistake — a misspelled operator, a\n * `$between` with one bound, a `{}` where an operator belongs — and ADR-0112's\n * rule is that such an error carries its own machine-readable semantics\n * (`code` + `status`) rather than leaving each consumer to guess from the\n * message text. Until #5352 only the #5334 array refusals did; the other seven\n * were bare `throw new Error(…)`, so `/analytics/dataset/query` had nothing to\n * read and answered `500 ANALYTICS_QUERY_FAILED` — \"the platform is broken\" for\n * what is a typo in a widget's filter, counted as a 5xx by ops alerting. The\n * same mistake on `find()` has answered `400 INVALID_FILTER` since #3948.\n *\n * So {@link invalidFilterError} is now the ONLY way this module refuses, and\n * `rest-server.ts`'s analytics catch reads that envelope before anything else.\n * #5352 changed the SHAPE of these errors and nothing about WHICH inputs are\n * refused — the refusal set is pinned input-by-input in\n * `filter-refusal-envelope.test.ts` precisely so that stays true.\n *\n * # An `undefined` COMPARAND is refused, not read seven different ways (#6386)\n *\n * #6050 ruled on 2026-08-07 (ruling B) that `undefined` sitting where a\n * comparand belongs is REFUSED, and landed that on `driver-sql` / `driver-turso`.\n * #6125 pushed it to this package's OTHER door, `read-scope-sql.ts` (PR #6390).\n * This door — the `where` the CALLER writes — had never been named by any of\n * those rulings, and it read the one shape seven ways. Measured on `origin/main`\n * (`5faa23ca3`) by calling `normalizeAnalyticsFilterTree({ where })` directly:\n *\n * | `where` | normalized to | reading |\n * |---|---|---|\n * | `{d: undefined}` | `null` | the WHOLE where dropped — the query ran with NO filter |\n * | `{stage:'won', d: undefined}` | `stage equals 'won'` | the `d` conjunct vanished in silence |\n * | `{$not: {d: undefined}}` | `NOT (d set)`, i.e. `d IS NULL` | a predicate the author never wrote |\n * | `{d: {$eq: undefined}}` | `d equals [null]` | a value comparison, NOT `$eq: null`'s `notSet` |\n * | `{d: {$gt: undefined}}` | `d gt [null]` | ditto |\n * | `{d: {$in: [undefined]}}` | `d in [null]` | ditto |\n * | `{d: {$ne: undefined}}` | `d notSet OR d notEquals [null]` | ditto |\n *\n * The first three are the whole argument. They WIDEN — which is the failure mode\n * the note at {@link MONGO_TO_CUBE_OP}'s miss branch has forbidden in this very\n * function since #4128 (\"NEVER drop: a missing predicate does not narrow the\n * query, it WIDENS it … That failure mode is #3650's\"). `buildNode`'s first line\n * was `if (raw === undefined) continue;`: the module did, at its entry, the exact\n * thing its own body refuses to do a few dozen lines further down.\n *\n * Row three is the strangest and is worth stating separately, because it is not\n * \"one conjunct fewer\": the #5146 rewrite splits the leaf into `{d: {$null:\n * false}} AND {d: undefined}`, the entry gate dropped the second half, and the\n * surviving guard was then negated — so a predicate GREW OUT of a discarded leaf.\n *\n * ⚠️ The direction is silently WRONG RESULTS, not a permission bypass. Read\n * scope is compiled by the other door (`read-scope-sql.ts` → `applyReadScope`)\n * and never passes through here, so a caller still saw only rows it was entitled\n * to — just more of them than it asked for. What was lost is the ANSWER: an\n * analytics figure, a report total, an aggregate, wrong with nothing to read.\n *\n * The refusal is {@link undefinedComparandError}, in this module's existing\n * envelope (`INVALID_FILTER` / 400) — the opposite attribution from\n * `read-scope-sql`'s 500, and deliberately so: that door compiles a platform\n * artifact, this one receives what the CALLER wrote.\n *\n * ⛔ `null` does not move, and that is the way this change could do harm: the two\n * live one `===` apart in every polarity table here. `{d: null}`, `{$eq: null}`,\n * `{$ne: null}`, `{$null: …}`, `{$exists: …}` and `$contains: null`'s `%null%`\n * (#5526) keep their exact lowering, pinned as a control group in\n * `filter-normalizer-undefined-comparand.test.ts`.\n *\n * # A field wrapper cannot MIX $-operators with non-$ members (#6444)\n *\n * The value-independent sibling of the #6386 defect, in the same function, ruled\n * Option A (refuse) by the maintainer on 2026-08-08. A field constraint object\n * that carries `$`-operator keys AND non-`$` keys at once used to compile its\n * operators and silently DROP every non-`$` sibling — `fieldLeaves`'s\n * `if (opKeys.length > 0) { …; return out; }` arm never looked at them, and the\n * nested-relation flatten sits after that early return. Measured on\n * `origin/main` (`1a53a0253`):\n *\n * | `where` | normalized to | reading |\n * |---|---|---|\n * | `{d: {$eq: 1, nested: 'x'}}` | `d equals [1]` | the `nested` conjunct vanished in silence |\n * | `{amount: {gte: 10, $lte: 20}}` | `amount lte 20` | the missing-`$` typo: the LOWER BOUND silently gone |\n * | `{$not: {d: {$null: true, nested: 'x'}}}` | `NOT(d set AND d notSet)` | a contradiction that negates to TRUE — EVERY row |\n *\n * Row two is the likeliest producer — a dropped `$` is a canonical agent typo —\n * and row three is the strangest: the #5146 guard was computed while the sibling\n * still existed (`requireValue`), the sibling then vanished inside\n * `fieldLeaves`, and the surviving conjunction was contradictory, so the\n * negation widened to the whole dataset. All three WIDEN — the #3650 failure\n * mode the note at {@link MONGO_TO_CUBE_OP}'s miss branch forbids in this very\n * function.\n *\n * The refusal is {@link mixedFieldWrapperError} via\n * {@link assertUnmixedFieldWrapper}, in this module's one envelope\n * (`INVALID_FILTER` / 400, #5352). The message must do one thing more than the\n * module's other refusals: the shape has TWO legitimate repairs answering two\n * intents this module cannot tell apart — an operator missing its `$`\n * (`gte` → `$gte`) and a nested-relation member that needs a wrapper of its own\n * — so the message names the offending key(s) and shows BOTH rewrites (ruling\n * requirement, #6444).\n *\n * Option B — flattening the non-`$` siblings as nested paths next to the\n * operators — was REJECTED by the same ruling: it would compile the likely-real\n * cause (a dropped `$`) into a predicate on a non-existent member `amount.gte`,\n * turning a diagnosable mistake into a harder one.\n *\n * ⛔ What does not move: a wrapper that is ALL non-`$` keys keeps flattening to\n * the dotted member (`{d: {nested: 'x'}}` → `d.nested`); a wrapper that is ALL\n * `$`-operators compiles exactly as before; `$null` / `$exists` flag semantics\n * (#5526 / #5332 / #5347) and {@link comparand} are untouched. The sibling door\n * `read-scope-sql.ts` already fails closed on this exact shape\n * (`compileField`'s non-`$`-key check) and is not touched — this change makes\n * the two doors give one answer.\n *\n * Row-result cover: `filter-operator-coverage.test.ts` for the operator\n * vocabulary, `native-sql-filter-logic-conformance.test.ts`, which runs the\n * SHARED combinator table (`FILTER_LOGIC_CASES`, #3774) that the SQL compiler,\n * the in-memory matcher, `formula` and `read-scope-sql` are already held to,\n * `filter-normalizer-not-null-safe.test.ts` for the two squares that table\n * deliberately does not carry (NULL handling, boolean identities),\n * `filter-array-lowering.test.ts` for the array door (#5334),\n * `filter-value-type-fidelity.test.ts` for what each comparand TYPE binds on both\n * consumers (#5526, carrying #5528's cases forward as end-to-end assertions),\n * `filter-normalizer-undefined-comparand.test.ts` for the `undefined` refusal and\n * its `null` control group (#6386), and\n * `filter-normalizer-mixed-wrapper.test.ts` for the mixed `$`/non-`$` wrapper\n * refusal and its pure-shape control groups (#6444).\n */\n\nimport { isFilterAST, parseFilterAST, VALID_AST_OPERATORS } from '@objectstack/spec/data';\nimport { StandardErrorCode } from '@objectstack/spec/api';\nimport {\n CROSS_FIELD_COMPARISON_OPERATORS,\n fieldReferenceBetweenBoundMessage,\n isBindableComparand,\n isFieldReference,\n isRenderableTextComparand,\n TEXT_PATTERN_OPERATORS,\n unbindableListMemberMessage,\n unrenderableTextComparandMessage,\n} from '../comparand-shape.js';\n\nexport interface NormalizedAnalyticsFilter {\n member: string;\n operator: string;\n /** The author's comparands, at their own types — see {@link NormalizedFilterNode}. */\n values: unknown[];\n}\n\n// ── [#5334 / #5352] The refusal envelope ─────────────────────────────────────\n\n/**\n * [#5334, generalised by #5352] A filter refusal in the ADR-0112 envelope every\n * sibling filter refusal in the repo speaks — `INVALID_FILTER` / 400.\n *\n * The twin of `driver-sql`'s and `driver-memory`'s `unsupportedFilterError`.\n * A caller that writes a filter this module cannot compile has made a\n * 400-class mistake, and a coded refusal is what lets the `/analytics` face\n * answer it as one instead of as an opaque 500.\n *\n * ⛔ **The only way this module refuses.** #5334 introduced it for the two\n * array-door refusals while the other seven sites stayed bare `Error`s, and a\n * half-enveloped module is indistinguishable from an unenveloped one at the\n * REST boundary: `error.code` was `undefined` for the operator typo that is by\n * far the commonest of the nine, so the whole family landed as\n * `500 ANALYTICS_QUERY_FAILED` (#5352). A new refusal added to this file must\n * be thrown through here; a bare `throw new Error` is the defect returning.\n *\n * It carries no `#5352`-specific wording on purpose — the envelope is the\n * contract, the message stays whatever the refusing site says.\n *\n * [#7598] EXPORTED, and the \"only way this module refuses\" invariant is\n * unchanged by it: that rule is about this file's own sites, and the export\n * exists so a sibling in this directory cannot invent a SECOND spelling of the\n * same envelope. `ObjectQLStrategy.generateSql` refuses a cross-field\n * comparison it cannot honestly render, and that refusal is a `where`-door\n * refusal in every respect that matters — the caller authored the input, the\n * repair is theirs — so it takes the `where` door's envelope rather than a\n * hand-rolled twin. (`read-scope-sql.ts` keeps its OWN local error factory\n * because its envelope genuinely differs: a read scope is not caller-authored,\n * hence `READ_SCOPE_COMPILE_FAILED` / 500 by the #5367 ruling.)\n */\nexport function invalidFilterError(message: string): Error {\n const err = new Error(message) as Error & { code?: string; status?: number };\n err.code = StandardErrorCode.enum.INVALID_FILTER;\n err.status = 400;\n return err;\n}\n\n/**\n * The value-INDEPENDENT operators: the pipeline name depends only on the key.\n *\n * `$null` and `$exists` are deliberately absent — their meaning flips with\n * their boolean value, which a key→name map cannot express. Putting `$exists`\n * here anyway is what made `{$exists: false}` compile to `IS NOT NULL`, the\n * exact inverse of what it asks for; both are handled explicitly below.\n */\nconst MONGO_TO_CUBE_OP: Record<string, string> = {\n $eq: 'equals',\n $ne: 'notEquals',\n $gt: 'gt',\n $gte: 'gte',\n $lt: 'lt',\n $lte: 'lte',\n $in: 'in',\n $nin: 'notIn',\n $contains: 'contains',\n $notContains: 'notContains',\n $startsWith: 'startsWith',\n $endsWith: 'endsWith',\n // [#6520] The case-INSENSITIVE twin, ASCII fold only. A separate cube operator\n // rather than a flag on `contains`, because the two compile to different SQL\n // and one name would make the renderers guess which was meant.\n $icontains: 'icontains',\n};\n\n/**\n * The comparand a leaf carries: the author's value, at the author's type.\n *\n * [#5526] This function is what used to be `stringifyForCube`, and the whole of\n * its former body is gone: `values` is `unknown[]`, so a comparand needs no\n * encoding and there is nothing for a decoder downstream to guess at. What\n * remains is one normalisation, and it is not a type conversion:\n *\n * `undefined` becomes `null`. JSON has no `undefined`, so no authored\n * `FilterCondition` can carry one — `{$eq: undefined}` is a key the author did\n * not mean to write (#5332's reading, unchanged here) — while a `values` entry\n * that IS `undefined` is a bind error on better-sqlite3 rather than a predicate.\n * `null` is the fail-closed reading: the comparison is UNKNOWN, so the widget\n * draws nothing instead of drawing rows chosen by an accident.\n *\n * Note what this does NOT do: `{$eq: undefined}` still compiles to an `equals`\n * leaf, not to `notSet`. Only `=== null` is the null PREDICATE (#5332's identity\n * test, which this module reads at the operator branch, above this function),\n * and widening it to `== null` here would re-decide that ruling sideways.\n *\n * ## Addendum (#6386): the `undefined` arm is now UNREACHABLE from this door\n *\n * {@link assertDefinedComparands} refuses an `undefined` before any comparand is\n * read, and it covers every call site of this function — the `$between` bounds,\n * the operator value and its array members, the bare-array `$in` and the implicit\n * `=` — so nothing can arrive here holding `undefined` any more. The refusal\n * tests enumerate exactly that set of positions, which is what makes the claim\n * checkable rather than asserted.\n *\n * ⛔ It is left in place ON PURPOSE, code untouched. Both statements this\n * function makes were RULED — `undefined` → `null` by #5526, and \"only `=== null`\n * is the null predicate\" by #5332 — and #6386 refuses an INPUT without reopening\n * either. Deleting a now-dead arm would be a semantic edit smuggled in as a\n * cleanup: it is #5526's call whether the normalisation still earns its place\n * once its last caller is gated, and that is a separate decision from this one.\n */\nfunction comparand(v: unknown): unknown {\n return v === undefined ? null : v;\n}\n\n/**\n * One node of the normalized filter TREE.\n *\n * A tree rather than the flat array this module used to produce, because a flat\n * array cannot express `$or` — and what it did with one was DROP it, which does\n * not narrow a query, it widens it to rows the author excluded (#3650's\n * symptom). The structure is the minimum that survives that: leaves carry the\n * pipeline's `{member, operator, values}` triple unchanged, and the combinators\n * are explicit so each strategy can compile them the way its own backend\n * expresses them — recursive SQL for the raw-SQL path, a passed-through\n * `$or`/`$not` for the engine path.\n *\n * The `const` kind is the boolean constant (#5325). The union carried only\n * `leaf | and | or | not`, so there was no way to SAY \"matches nothing\": a\n * `{$not: {}}` — whose meaning is exactly that — could only be expressed by\n * emitting nothing, which every compiler reads as \"no constraint\", i.e. the\n * opposite. TRUE keeps its existing spelling (`null` = no constraint, the AND\n * identity); FALSE needs a node because it must survive into the WHERE clause.\n *\n * [#5526] A leaf's `values` is `unknown[]`, not `string[]`. The author's\n * comparand travels at its own type: a number stays a number, a boolean a\n * boolean, and — the defect this fixed — a STRING stays the string the author\n * typed, so `'007'` is never the integer `7` and `'null'` is never real NULL.\n * The compilers of this tree convert only where their own boundary forces it\n * ({@link toSqlBindValue} for a SQL parameter, `like-pattern.ts` for the LIKE\n * family, whose comparand the spec declares a `string`); the ObjectQL engine path\n * converts nothing, because the engine compares against the stored runtime type\n * and the value it is handed is the author's own. See the module header.\n */\nexport type NormalizedFilterNode =\n | { kind: 'leaf'; member: string; operator: string; values: unknown[] }\n | { kind: 'const'; value: boolean }\n | { kind: 'and'; children: NormalizedFilterNode[] }\n | { kind: 'or'; children: NormalizedFilterNode[] }\n | { kind: 'not'; child: NormalizedFilterNode };\n\n/**\n * The SQL boolean constants the compilers of this tree emit for a `const` node.\n *\n * `1 = 0` / `1 = 1` are the spellings already used on both sides of the repo —\n * `read-scope-sql.ts` compiles an empty `$in` to `1 = 0`, `driver-sql`'s\n * `applyFalseConstant` emits the same (#5134), and Knex renders an empty\n * `whereIn` that way. They need no bindings, are valid on every dialect these\n * strategies target (unlike a bare `FALSE`), and keep the statement a normal\n * SELECT so `GROUP BY` / `LIMIT` still behave.\n */\nexport const SQL_CONST_FALSE = '1 = 0';\nexport const SQL_CONST_TRUE = '1 = 1';\n\n/** The tree's FALSE. A fresh object per call — nodes are never shared. */\nfunction falseNode(): NormalizedFilterNode {\n return { kind: 'const', value: false };\n}\n\n/**\n * `NOT` of a node, with `null` read as the constant TRUE it is.\n *\n * `NOT TRUE ≡ FALSE` is the whole point: `{$not: {}}` used to fall off the tree\n * here, taking the WHERE clause with it (#5325). A `NOT` of a constant folds to\n * the opposite constant, so `{$not: {$not: {}}}` is TRUE again rather than a\n * `NOT (1 = 0)` that only happens to evaluate right.\n */\nfunction notOf(inner: NormalizedFilterNode | null): NormalizedFilterNode {\n if (!inner) return falseNode();\n if (inner.kind === 'const') return { kind: 'const', value: !inner.value };\n return { kind: 'not', child: inner };\n}\n\n/** A node the normalizer can walk: a plain object, not `null` and not an array. */\nfunction isFilterObject(v: unknown): v is Record<string, unknown> {\n return v !== null && typeof v === 'object' && !Array.isArray(v) && !(v instanceof Date);\n}\n\n/** `null` means \"no constraint\" — an empty object contributes no predicate. */\nfunction andOf(children: NormalizedFilterNode[]): NormalizedFilterNode | null {\n if (children.length === 0) return null;\n if (children.length === 1) return children[0];\n return { kind: 'and', children };\n}\n\n/**\n * [#5234] The comparand-SHAPE gate for the analytics `where` door.\n *\n * This runs before a leaf exists, which is the whole reason it is here rather\n * than at the three emitters. {@link fieldLeaves} is the ONLY producer of leaf\n * nodes in this package, so one refusal here covers all three consumers of the\n * tree at once — `NativeSQLStrategy.buildFilterClause` (the statement that\n * executes), `ObjectQLStrategy.buildFilterClauseSql` (the `/analytics/sql` echo)\n * and `ObjectQLStrategy.convertFilter` (the engine path). Guarding the emitters\n * instead would have been three guards, three envelopes, and one of them —\n * `convertFilter`'s `String(v0)` — would still have LAUNDERED the object into\n * `'[object Object]'` before any driver could refuse it, so a strict driver\n * downstream could never see the shape it was strict about.\n *\n * Prime Directive #12, applied literally: refuse at the door, do not tolerate at\n * the consumer. `read-scope-sql.ts` is this package's OTHER door — it compiles a\n * `FilterCondition` that never passes through here — and carries the same two\n * checks in its own fail-closed envelope.\n *\n * Two shapes are refused, the two #5234 measured. `$eq` and friends keep\n * binding any OTHER object as JSON (`toSqlBindValue`), which remains a separate\n * account.\n *\n * ⚠️ [#7598, maintainer ruling 2026-08-12 Q1 = B] A THIRD arm briefly lived\n * here — a `{$field}` reference in the comparand of the six scalar comparison\n * operators, added by #7694 as the shipped interim while the routing question\n * was with the maintainer. It is GONE, and its removal is the point of the\n * ruling rather than a cleanup: this function runs inside {@link fieldLeaves},\n * which is the one producer of leaf nodes for ALL THREE consumers of this tree\n * — including `ObjectQLStrategy.convertFilter`, the ENGINE path. Refusing here\n * therefore refused the very execution B routes such a query to, so the arm and\n * the ruling cannot both stand. `NativeSQLStrategy.canHandle` now declines\n * instead (see the ruling recorded there), and `driver-sql` compiles the\n * comparison under the four #5222 rulings with the metadata it owns.\n *\n * What did NOT move is the `$between` arm — see\n * {@link assertNoFieldReferenceComparand}, which is now that arm alone.\n */\nfunction assertCompilableComparand(opKey: string, field: string, value: unknown): void {\n if (TEXT_PATTERN_OPERATORS.has(opKey)) {\n // An array reaches this door as `values[0]` — i.e. every member after the\n // first is silently DROPPED — while `read-scope-sql` and `driver-sql`\n // stringify the whole array. That split is why an array is refused and not\n // merely stringified consistently.\n if (!isRenderableTextComparand(value)) {\n throw invalidFilterError(`[analytics] ${unrenderableTextComparandMessage(opKey, field, value)}`);\n }\n return;\n }\n if ((opKey === '$in' || opKey === '$nin') && Array.isArray(value)) {\n value.forEach((member, index) => {\n if (!isBindableComparand(member)) {\n throw invalidFilterError(`[analytics] ${unbindableListMemberMessage(opKey, field, member, index)}`);\n }\n });\n }\n}\n\n/**\n * [#7598] A `{ $field: 'col' }` reference in a `$between` ENDPOINT — the one\n * position on this door where the gate is still load-bearing after the\n * 2026-08-12 ruling, and the one place its removal would silently CREATE a\n * capability rather than remove a refusal.\n *\n * ## Why this arm survived when the scalar-comparand arm did not\n *\n * The ruling (Q1 = B) moved the six scalar comparison operators OUT of this\n * door's business entirely: `NativeSQLStrategy.canHandle` declines a `where`\n * carrying one, the query routes to the engine, and `driver-sql` compiles the\n * comparison under the four #5222 rulings. Refusing them here would refuse the\n * execution the routing exists to reach, so that arm is gone.\n *\n * `$between` is the opposite case, because of a LOWERING this door performs and\n * the driver never sees. {@link fieldLeaves}'s `$between` branch splits\n * `{ $between: [a, b] }` into a `gte` leaf and an `lte` leaf, and\n * `ObjectQLStrategy.convertFilter` hands those to the engine as `{ $gte: a }` /\n * `{ $lte: b }`. So a reference in an endpoint would arrive at `driver-sql`\n * wearing a `$gte` it was never authored with — and `$gte` is a position #5222\n * COMPILES. The result would be that `{ amount: { $between: [{ $field:\n * 'budget' }, 100] } }` quietly SUCCEEDS on the analytics face while\n * `CROSS_FIELD_REFUSALS` pins it as refused on both SQL drivers, and while\n * #7596 has removed the position from `FieldReferenceSchema` altogether\n * (maintainer ruling 2026-08-11, ADR-0049 declared = enforced). One shape, two\n * answers, created by a laundering this module does on the way past — exactly\n * the class #7598 was filed about, spelled backwards.\n *\n * Refusing here therefore CONVERGES with `driver-sql`'s own #5222 refusal arm,\n * which is what every surviving `{$field}` refusal in this package now does:\n * the LIKE family through {@link assertCompilableComparand}'s\n * `isRenderableTextComparand` call, `$in` / `$nin` members through its\n * `isBindableComparand` one, a bare `{ field: { $field: … } }` as an unsupported\n * operator, and this. The scalar comparands are the only positions where the\n * two faces now differ, and they differ by the analytics face DECLINING to\n * serve them itself rather than by refusing them.\n *\n * Asserted under the `$between` name, not under the `gte` / `lte` the bounds\n * lower to, because the author wrote `$between` and that is the key they have\n * to repair.\n */\nfunction assertNoFieldReferenceComparand(opKey: string, field: string, value: unknown): void {\n if (opKey !== '$between' || !Array.isArray(value)) return;\n value.forEach((member, index) => {\n if (!isFieldReference(member)) return;\n throw invalidFilterError(\n `[analytics] ${fieldReferenceBetweenBoundMessage(opKey, field, member.$field, index)}`,\n );\n });\n}\n\n/**\n * [#6386, on #6050's ruling B] `undefined` in a COMPARAND position.\n *\n * ONE wording for every position (#5240 — one condition, one wording); only\n * `path` varies, because only the position does. The wording names BOTH measured\n * consequences rather than one, which is where this differs from\n * `read-scope-sql`'s twin: there all four cells failed the same way (a silent\n * NULL bind), here the same input is read two different ways depending on where\n * it sits — the key is DROPPED in the three positions `buildNode` used to skip,\n * and compiled as a comparison against `null` in the four {@link comparand}\n * normalises. An author who hits either one needs to be told which of their keys\n * is unreadable, and both halves of what it would otherwise have done.\n *\n * ## Why 400 here and 500 on the sibling door\n *\n * `read-scope-sql.ts` refuses the same shape as `READ_SCOPE_COMPILE_FAILED` /\n * 500 because a read scope is compiled by the PLATFORM from CEL and stored\n * policy — billing the caller for it would be wrong. This door is the mirror\n * image: `where` is what the caller itself passed to `AnalyticsService.query`,\n * so it is a 400-class mistake and takes the envelope every other refusal in\n * this module already carries (#5352).\n *\n * ## Why the producer named is the CALLER, and what shape to look for\n *\n * `undefined` cannot cross JSON, so neither REST door can carry it: it can only\n * come from in-process code building the object — `{ owner_id: ctx.user?.id }`,\n * the shape #6050 proved reachable on `driver-sql`. Note the platform's OWN\n * answer to that need is already fail-closed and is not this shape: a\n * `{current_user_id}` placeholder in a dataset / widget / report filter is\n * resolved by `resolveFilterTokens` (`@objectstack/core`), which throws\n * `FILTER_TOKEN_UNRESOLVED` / 400 rather than emitting `undefined`.\n */\nfunction undefinedComparandError(field: string, path: string): Error {\n return invalidFilterError(\n `[analytics] comparand at ${path} is undefined — refusing to compile this filter. ` +\n `@objectstack/spec FieldOperatorsSchema declares no undefined comparand, and in JavaScript a key ` +\n `whose value is undefined cannot be told apart from an ABSENT key — yet the two mean OPPOSITE ` +\n `things (a predicate versus no constraint at all), so there is no reading of it that is not a ` +\n `guess. It used to compile, two ways: in a FIELD position the key was dropped outright, so a ` +\n `single-key where ran with no filter at all and the chart was drawn over every row (#3650's ` +\n `widening, which this module refuses everywhere else); in an OPERATOR or list position it ` +\n `became a comparison against null, which is UNKNOWN for every row and charts nothing. ` +\n `Write null if the null predicate was meant ({ \"${field}\": null } or { \"${field}\": { \"$null\": true } }), ` +\n `or omit the key entirely when the value is genuinely absent — an omitted key is the same \"no ` +\n `constraint\" without the ambiguity. The producer to fix is whoever BUILT this where: undefined ` +\n `cannot cross JSON, so it is in-process code spreading a possibly-absent value into a filter ` +\n `object (#6050 ruling B, pushed down to this door by #6386).`,\n );\n}\n\n/**\n * [#6386] Refuse every `undefined` sitting in a comparand position of ONE field\n * constraint.\n *\n * The positions are enumerated rather than swept, because \"comparand\" is a\n * POSITION and not a type:\n *\n * - the DIRECT comparand — `{d: undefined}`, the implicit `=`. Reached for a\n * nested relation too, because {@link fieldLeaves} recurses into one with the\n * DOTTED member name, so `{profile: {verified: undefined}}` is refused as\n * `\"profile.verified\"` — the member the leaf would have carried, not the\n * relation. (`read-scope-sql`'s twin has no such case: it refuses nested\n * relations outright.)\n * - a MEMBER of the bare-array implicit `$in` — `{d: [1, undefined]}`. The\n * array itself is a legitimate comparand here, so its elements are comparands\n * in their own right. This is the deliberate divergence from that twin, which\n * refuses a bare array as a whole and so must not relabel it.\n * - an OPERATOR's comparand — `{d: {$gt: undefined}}`, `$eq`, `$ne`, the LIKE\n * family, every other single-value operator;\n * - a MEMBER of a list operator's array — `{d: {$in: [undefined]}}`, `$nin`,\n * and `$between`'s two bounds.\n *\n * `$null` / `$exists` are deliberately NOT swept, exactly as on the twin: their\n * comparand is a declared BOOLEAN — a flag, not a value to compare against — so\n * `undefined` there is not a comparand at all. ⚠️ This module reads that flag by\n * IDENTITY (`=== true` / `=== false`, see {@link fieldLeaves}) where the twin\n * reads it by truthiness, so `{$null: undefined}` lowers here to `set`\n * (`IS NOT NULL`). That is the boolean-DOMAIN question #5347 / #5369 opened and\n * #6387 is measuring on the sibling door; it is a different cell and is not\n * decided as a rider on this one.\n *\n * ## Why the gate sits HERE, and what that decides for `{$not: {d: undefined}}`\n *\n * {@link fieldLeaves} is the only producer of leaf nodes in this module, so one\n * gate covers all three consumers of the tree at once — the same argument\n * {@link assertCompilableComparand} makes one function below.\n *\n * That places it DOWNSTREAM of {@link nullSafeNegationOperand}, and for row three\n * of the header's table that choice is the whole question: a gate on the far side\n * of the #5146 rewrite refuses, while a rewrite that could swallow the leaf first\n * would leave a CHANGED SHAPE for the gate to bless. Measured rather than\n * assumed, because the same trap cost PR #6390 a lap on the sibling door — and\n * the reasoning there does NOT transfer, since the two modules' polarity tables\n * are spelled differently (that one is uniformly `=== null`; this one mixes\n * `=== null` for `$eq`/`$ne` with IDENTITY reads for `$null`/`$exists`). What the\n * measurement shows here is that the rewrite never drops a leaf: every guard\n * disposition — `requireValue` pushes `{k: {$null: false}}, {k: spec}`,\n * `allowNull` pushes `{$or: [{k: {$null: true}}, {k: spec}]}`, `none` writes\n * `out[k] = spec` — carries `spec` through by reference, so the author's\n * `undefined` always reaches this gate and always throws. Pinned in\n * `filter-normalizer-undefined-comparand.test.ts` as its own block: one case per\n * rewrite path that can carry a SWEPT comparand (`requireValue`, `allowNull`, and\n * the nested-relation recursion), plus the measured reason there is no third —\n * `none` needs every operator to satisfy {@link operatorIsNullTotal}, which is\n * false for an `undefined` comparand on every operator this gate sweeps, so the\n * only field specs that reach it holding one are the `$null` / `$exists` flags it\n * deliberately does not sweep.\n */\nfunction assertDefinedComparands(field: string, spec: unknown): void {\n const root = `\"${field}\"`;\n if (spec === undefined) throw undefinedComparandError(field, root);\n if (Array.isArray(spec)) {\n spec.forEach((member, index) => {\n if (member === undefined) throw undefinedComparandError(field, `${root}[${index}]`);\n });\n return;\n }\n if (!isFilterObject(spec)) return;\n for (const [op, opValue] of Object.entries(spec)) {\n if (!op.startsWith('$') || op === '$null' || op === '$exists') continue;\n const opPath = `${root}.${op}`;\n if (opValue === undefined) throw undefinedComparandError(field, opPath);\n if (!Array.isArray(opValue)) continue;\n opValue.forEach((member, index) => {\n if (member === undefined) throw undefinedComparandError(field, `${opPath}[${index}]`);\n });\n }\n}\n\n/**\n * [#6444, ruled Option A on 2026-08-08] A field wrapper mixing `$`-operator\n * keys with non-`$` sibling keys.\n *\n * ONE wording whatever the mix (#5240 — one condition, one wording); only the\n * field and the two key lists vary, because only those do. Where this message\n * has to do MORE than the module's other refusals: the shape has two\n * legitimate repairs answering two different intents, and the module cannot\n * tell which one the author held — that inability is exactly why the shape is\n * refused rather than read. So the message must present BOTH (ruling\n * requirement):\n *\n * - an OPERATOR missing its `$` — the canonical agent typo\n * `{amount: {gte: 10, $lte: 20}}` — repaired by the prefixed spelling\n * (`\"gte\" → \"$gte\"`);\n * - a NESTED-RELATION member that strayed into an operator wrapper —\n * repaired by giving it a wrapper of its own (`{ \"d\": { \"nested\": … } }`\n * compiles to the member `d.nested`), ANDed with the operator constraint\n * explicitly, since one JSON object cannot spell the same field key twice.\n *\n * Option B — flattening the sibling as a nested path beside the operators —\n * was rejected by the same ruling: it would compile the missing-`$` typo into\n * a predicate on a non-existent member `amount.gte`, converting a diagnosable\n * mistake into a harder one.\n */\nfunction mixedFieldWrapperError(field: string, opKeys: string[], nonOpKeys: string[]): Error {\n const offending = nonOpKeys.map((k) => `\"${k}\"`).join(', ');\n const rewrites = nonOpKeys.map((k) => `\"${k}\" → \"$${k}\"`).join(', ');\n const example = nonOpKeys[0];\n return invalidFilterError(\n `[analytics] \"${field}\" mixes $-operator keys (${opKeys.join(', ')}) with non-$ sibling key(s) ` +\n `${offending} in ONE field constraint — refusing to compile this filter. A $-prefixed key is an ` +\n `OPERATOR and a bare key is a NESTED-RELATION member; the two readings of ${offending} lead to ` +\n `different predicates and this module cannot tell which was meant, so any silent choice is a ` +\n `guess. If an operator missing its \"$\" was meant — the usual authoring slip — spell it with the ` +\n `prefix: ${rewrites}, as in { \"${field}\": { \"$${example}\": ... } }. If a nested-relation member ` +\n `was meant, give it a wrapper of its OWN with no $ siblings — { \"${field}\": { \"${example}\": ... } } ` +\n `compiles to the member \"${field}.${example}\" — and AND it with the operator constraint ` +\n `explicitly: { \"$and\": [{ \"${field}\": { \"$op\": ... } }, { \"${field}\": { \"${example}\": ... } }] }. ` +\n `This shape used to compile by silently DROPPING every non-$ sibling, and a dropped conjunct ` +\n `does not narrow the query, it WIDENS it: the chart included rows the author excluded, with ` +\n `nothing to read (#3650's failure mode, which this module refuses everywhere else). The sibling ` +\n `door in this package (read-scope-sql.ts) already fails closed on this exact shape — one shape, ` +\n `one answer (#6444).`,\n );\n}\n\n/**\n * [#6444] Refuse ONE field wrapper that mixes `$`-operator keys with non-`$`\n * sibling keys — the value-independent sibling of {@link assertDefinedComparands}.\n *\n * What made the mix silent: {@link fieldLeaves}'s operator arm iterates\n * `opKeys` only and returns, and the nested-relation flatten sits after that\n * early return — so with even one `$` key present, every non-`$` sibling was\n * simply never visited. Dropping a conjunct WIDENS (#3650), and inside a `$not`\n * it did worse than widen by one conjunct: {@link nullGuardForFieldSpec} judged\n * the wrapper while the sibling still existed (a non-`$` key never satisfies\n * {@link operatorIsNullTotal}, so the disposition was `requireValue` or\n * `allowNull`, never `none`), the sibling then vanished here, and for a\n * null-predicate operator the surviving guard was CONTRADICTORY —\n * `{$not: {d: {$null: true, nested: 'x'}}}` compiled to `NOT(d set AND d\n * notSet)`, which is TRUE for every row. That same never-`none` fact is what\n * guarantees the #5146 rewrite carries a mixed wrapper to this gate by\n * reference instead of swallowing it — pinned in\n * `filter-normalizer-mixed-wrapper.test.ts`'s rewrite block.\n *\n * ## Ordering against the neighbouring gates\n *\n * Runs in {@link fieldLeaves}'s wrapper arm, after the #5240 zero-operator\n * refusal (disjoint by construction: `{}` has no keys of either kind) and\n * after {@link assertDefinedComparands} at the function's entry — so\n * `{d: {$eq: undefined, nested: 'x'}}` is refused as an undefined comparand,\n * not as a mix. Both are refusals in the same envelope, so the REST face\n * answers 400 either way; the ordering is pinned as a measured fact, not a\n * contract.\n *\n * ## What is deliberately not judged here\n *\n * A wrapper that is ALL `$`-operators or ALL non-`$` members passes untouched —\n * this gate moves the refusal set by exactly the mixed shape. Whether a non-`$`\n * KEY is a real member of the modeled object is the schema's question at a\n * different layer, not this compiler's.\n */\nfunction assertUnmixedFieldWrapper(field: string, wrapper: Record<string, unknown>): void {\n const keys = Object.keys(wrapper);\n const opKeys = keys.filter((k) => k.startsWith('$'));\n if (opKeys.length === 0) return;\n const nonOpKeys = keys.filter((k) => !k.startsWith('$'));\n if (nonOpKeys.length === 0) return;\n throw mixedFieldWrapperError(field, opKeys, nonOpKeys);\n}\n\n/**\n * Compile one `field: value | { $op: … }` entry into its leaves.\n *\n * Multiple operators on one field AND together — the rule\n * `FILTER_LOGIC_CASES` pins for every other backend, and the one a range\n * `{ $gte, $lte }` depends on.\n */\nfunction fieldLeaves(key: string, raw: unknown): NormalizedFilterNode[] {\n // [#6386] `undefined` in a comparand position, refused before any leaf exists.\n // First statement of the only leaf producer, so no consumer of the tree can be\n // handed one — see {@link assertDefinedComparands} for the position list and\n // for why this side of the `$not` rewrite is the load-bearing choice.\n assertDefinedComparands(key, raw);\n\n const out: NormalizedFilterNode[] = [];\n const leaf = (operator: string, values: unknown[]): void => {\n out.push({ kind: 'leaf', member: key, operator, values });\n };\n\n if (raw === null) {\n leaf('notSet', []);\n return out;\n }\n\n if (typeof raw === 'object' && !Array.isArray(raw) && !(raw instanceof Date)) {\n const wrapper = raw as Record<string, unknown>;\n // A field constrained by ZERO operators, ruled on in #5240: REFUSE it, the\n // way `driver-sql`, `driver-memory` and `formula` now do. This module used\n // to produce no leaf for it — and \"no leaf\" is the constant TRUE, which is\n // load-bearing since #5325 made TRUE absorb a `$or`: left alone,\n // `{$or: [{a: {}}, {b: 2}]}` would have gone from `b = 2` to EVERY row.\n // Neither silent reading is the author's intent (the shape is an authoring\n // accident — a filter builder that recorded a field and never its operator),\n // and a loud refusal is the answer the rest of the repo already gives.\n if (Object.keys(wrapper).length === 0) {\n throw invalidFilterError(\n `[analytics] \"${key}\" carries a field constraint with zero operators ({}). ` +\n `Refusing rather than reading it as \"every row\" or \"no row\" — #5240 ruled this ` +\n `shape refused on every backend.`,\n );\n }\n // [#6444] A wrapper mixing $-operator keys with non-$ siblings is refused\n // BEFORE the operator arm below gets to iterate `opKeys` only and return —\n // that early return is exactly how the non-$ siblings used to vanish. See\n // {@link assertUnmixedFieldWrapper} for the two-intent message contract and\n // the `$not` interaction.\n assertUnmixedFieldWrapper(key, wrapper);\n const opKeys = Object.keys(wrapper).filter((k) => k.startsWith('$'));\n if (opKeys.length > 0) {\n for (const opKey of opKeys) {\n // `$between [min, max]` LOWERS to its two bounds rather than getting a\n // `between` operator of its own. Both strategies already carry the\n // calendar-day whole-day rule on their upper bound — NativeSQLStrategy\n // compiles a bare-day `lte` half-open (#3777), ObjectQLStrategy hands\n // `$lte` to the driver, which does the same — so a range's max\n // inherits that rule by construction instead of needing a second\n // implementation to keep in step. (The preview evaluator's `$between`\n // gap was closed the same way, sharing its `$lte` helper.)\n //\n // Before this, `$between` was simply absent from the operator map and\n // fell to the `continue` below: the predicate VANISHED from the WHERE\n // clause, so a dashboard widget carrying a range filter charted the\n // entire dataset — #3650's symptom, on the surface #3650 was about.\n // The temporal conformance matrix caught it as row results\n // (`native-sql-temporal-conformance.test.ts`).\n if (opKey === '$between') {\n const v = wrapper[opKey];\n if (!Array.isArray(v) || v.length !== 2) {\n // Never drop it: an unbounded read is the failure mode this whole\n // branch exists to prevent, and it is indistinguishable from a\n // legitimately wide query. Same stance driver-memory took for the\n // same shape (#3948).\n throw invalidFilterError(\n `[analytics] \"$between\" on \"${key}\" needs a two-element [min, max] array, got ` +\n `${JSON.stringify(v)}. Dropping the predicate would silently widen the query to every row.`,\n );\n }\n // [#7598] The endpoints are comparands in their own right, and this\n // branch RETURNS before `assertCompilableComparand` below — so a\n // reference in a `$between` bound was the one comparand position on\n // this door that no shape gate ever saw. Asserted under the `$between`\n // name, not under the `gte` / `lte` the bounds lower to, because the\n // author wrote `$between` and that is the key they have to repair.\n assertNoFieldReferenceComparand(opKey, key, v);\n leaf('gte', [comparand(v[0])]);\n leaf('lte', [comparand(v[1])]);\n continue;\n }\n\n // The two null predicates read their BOOLEAN, not just their key —\n // which is why neither can live in MONGO_TO_CUBE_OP. `$null: true`\n // asks for IS NULL (`notSet`), `$null: false` for IS NOT NULL\n // (`set`); `$exists` is the mirror image. `$null` is the shape the\n // console emits for an \"is empty\" / \"is not empty\" filter\n // (`is_null`/`is_not_null` normalise to it in `filter.zod.ts`), so\n // dropping it silently meant such a widget showed every row.\n if (opKey === '$null' || opKey === '$exists') {\n const isNull = opKey === '$null' ? wrapper[opKey] === true : wrapper[opKey] === false;\n leaf(isNull ? 'notSet' : 'set', []);\n continue;\n }\n\n // [#5332] A `null` COMPARAND is a null PREDICATE, not a value\n // comparison: `$eq: null` is `IS NULL` (`notSet`) and `$ne: null` is\n // `IS NOT NULL` (`set`) — the same two leaves the `raw === null` branch\n // above and `{$null: true|false}` beside it already produce. `$eq: null`\n // and `$null: true` are not merely similar spellings; `driver-mongodb`\n // TRANSLATES the latter into the former (`mongodb-filter.ts`'s `$null`\n // arm), so they are one predicate in the contract, and\n // `read-scope-sql.ts`'s `compileOperator`, `driver-sql`, `driver-memory`\n // and `formula` all compile them alike.\n //\n // Without this branch the pair fell through to MONGO_TO_CUBE_OP and\n // `stringifyForCube(null)` → `''`, i.e. `stage = ''` / `stage != ''`\n // (#5332). One meaning had two answers inside ONE file, and the wrong\n // one bound a real value a NULL column can never equal: an \"is empty\"\n // widget charted ZERO rows with no error to read, and on a text column —\n // where `''` is a value rows genuinely store — `$ne: null` additionally\n // EXCLUDED the empty-string rows it was asked to keep.\n //\n // Identity against `null`, matching `read-scope-sql` and `driver-sql`:\n // `null` is what an authored `FilterCondition` can carry (JSON has no\n // `undefined`, and `$eq: undefined` is a key the author did not mean to\n // write). [#5526] `comparand`'s `undefined` → `null` normalisation\n // deliberately does NOT widen this test to `== null`: it makes the VALUE\n // bindable, while this branch decides what the operator MEANS, and the\n // meaning is #5332's to change, not a side effect of deleting an encoder.\n if ((opKey === '$eq' || opKey === '$ne') && wrapper[opKey] === null) {\n leaf(opKey === '$eq' ? 'notSet' : 'set', []);\n continue;\n }\n\n // An EMPTY set is a boolean constant, not an absent predicate (#5134).\n // `buildFilterClause` returns `null` for a value-less `in`/`notIn`, and\n // a `null` clause is read as \"no constraint\" by every compiler of this\n // tree — so `{stage: {$in: []}}` charted every row instead of none. It\n // also has to be a CONSTANT rather than a dropped clause for the\n // NULL-safe `$not` rewrite below to stay correct: a dropped conjunct\n // inside a negation flips the whole negation's answer, while `1 = 0`\n // negates to `1 = 1` the way `read-scope-sql.ts` already has it.\n if ((opKey === '$in' || opKey === '$nin') && Array.isArray(wrapper[opKey]) && (wrapper[opKey] as unknown[]).length === 0) {\n out.push({ kind: 'const', value: opKey === '$nin' });\n continue;\n }\n\n const cubeOp = MONGO_TO_CUBE_OP[opKey];\n if (!cubeOp) {\n // NEVER drop: a missing predicate does not narrow the query, it\n // WIDENS it — the compiled SQL stays valid and simply returns rows\n // the author excluded, which is indistinguishable from a\n // legitimately broad query and invisible to any test that asserts\n // the emitted SQL. That failure mode is #3650's, and skipping\n // unmapped operators is how `$between` reproduced it (#4128).\n // driver-memory made the same call for the same reason in #3948.\n throw invalidFilterError(\n `[analytics] Unsupported filter operator \"${opKey}\" on \"${key}\". ` +\n `Supported: ${Object.keys(MONGO_TO_CUBE_OP).join(', ')}, $between, $null, $exists, ` +\n `and the $and/$or/$not combinators. ` +\n `Dropping it would silently widen the query to rows the filter excludes.`,\n );\n }\n const v = wrapper[opKey];\n // [#5234] The comparand SHAPE gate runs before anything reads `v` — see\n // {@link assertCompilableComparand} for why this door and not the three\n // emitters downstream of it.\n assertCompilableComparand(opKey, key, v);\n const values = Array.isArray(v) ? v.map(comparand) : [comparand(v)];\n // [#5298] The operators that carry their own negation are NULL-safe,\n // here as everywhere else — see the module header's section on it.\n if (nullValueSatisfiesOperator(opKey, v) && !operatorIsNullTotal(opKey, v)) {\n out.push({\n kind: 'or',\n children: [\n { kind: 'leaf', member: key, operator: 'notSet', values: [] },\n { kind: 'leaf', member: key, operator: cubeOp, values },\n ],\n });\n continue;\n }\n leaf(cubeOp, values);\n }\n return out;\n }\n // Nested relation (e.g. {profile: {verified: true}}). Flatten with\n // dot-prefixed keys so cube field path resolution still works.\n for (const [nestedKey, nestedVal] of Object.entries(wrapper)) {\n out.push(...fieldLeaves(`${key}.${nestedKey}`, nestedVal));\n }\n return out;\n }\n\n // Implicit equality / array → in. An empty array is the same constant its\n // explicit `{$in: []}` spelling is — see the note at that branch.\n if (Array.isArray(raw)) {\n if (raw.length === 0) out.push({ kind: 'const', value: false });\n else leaf('in', raw.map(comparand));\n } else leaf('equals', [comparand(raw)]);\n return out;\n}\n\n/**\n * Compile a `FilterCondition` object into a node. `null` = no constraint (TRUE).\n *\n * Every entry of one object ANDs with its siblings, at every depth — the rule\n * `filter-logic-conformance.ts` exists to hold each backend to (#3774). The\n * combinator handling deliberately mirrors `read-scope-sql.ts`'s\n * `compileNode` — the `{}`/`{$not: {}}` identities since #5325, and the EMPTY\n * `$and`/`$or` identities since the #5322 ruling (see the note at the\n * `length === 0` branch) — so the two SQL-producing paths in this package\n * cannot drift apart about what a filter MEANS.\n */\nfunction buildNode(cond: Record<string, unknown>): NormalizedFilterNode | null {\n const children: NormalizedFilterNode[] = [];\n\n for (const [key, raw] of Object.entries(cond)) {\n // [#6386] What used to be here — `if (raw === undefined) continue;` — was\n // the entry gate doing the one thing the rest of this file forbids: a key\n // dropped without trace, which does not narrow the query, it WIDENS it (see\n // the module header's table and the note at MONGO_TO_CUBE_OP's miss branch).\n // Removing it does NOT create four new refusals; every key kind now reaches\n // the branch that already had the truest thing to say about it:\n //\n // {d: undefined} → `fieldLeaves` → `assertDefinedComparands` (#6386)\n // {$and|$or: undefined} → \"requires an array of filter objects, got undefined\"\n // {$not: undefined} → \"requires a filter object, got undefined\"\n // {$other: undefined} → \"Unsupported top-level filter operator\"\n //\n // ⚠️ An absent `where` is untouched and still means \"no constraint\":\n // `lowerAnalyticsWhere` answers `null` for `{where: undefined}` before this\n // function runs. The refusal is about a KEY INSIDE a `where`, where dropping\n // it silently changes which rows the author gets.\n if (key === '$and' || key === '$or') {\n if (!Array.isArray(raw)) {\n throw invalidFilterError(\n `[analytics] \"${key}\" requires an array of filter objects, got ${JSON.stringify(raw)}. ` +\n `Dropping it would silently widen the query to rows the filter excludes.`,\n );\n }\n if (raw.length === 0) {\n // Boolean identity (#5322 ruling, 2026-08-04): the empty `$and` is the\n // AND identity — TRUE, no constraint — and the empty `$or` is the OR\n // identity — FALSE, zero rows. Until that ruling this function REFUSED\n // both; its error message argued, verbatim, that \"An empty combinator\n // has no defensible reading — dropping it widens the query, and\n // treating it as 'match nothing' silently empties a chart\" — while the\n // five FILTER_LOGIC_CASES backends already reduced them. The ruling\n // took the reduction: only a reduction can evaluate a NESTED tree (a\n // rejection must first reduce to judge `$and: []` as the third branch\n // of a `$or`, which concedes the point), and `{$or: []}` = zero rows\n // is fail-closed where it matters — a disjunct list that loops to zero\n // items hides every row instead of widening (#5134). Loud\n // AUTHORING-time rejection of the literal spellings is #5330's scope.\n // Note the guard above did NOT loosen: a non-array `$and`/`$or` still\n // throws, as do non-object branches below.\n if (key === '$or') children.push(falseNode());\n continue;\n }\n const branches = raw.map((sub) => {\n // A non-object element is refused rather than skipped: skipping it\n // NARROWS a `$or` to its remaining branches and, under the TRUE-absorbs\n // rule below, would otherwise have to be read as TRUE and widen the\n // query to every row. Neither is a defensible reading of garbage input —\n // `read-scope-sql.ts` refuses the same shape.\n if (!isFilterObject(sub)) {\n throw invalidFilterError(\n `[analytics] \"${key}\" branches must be filter objects, got ${JSON.stringify(sub)}. ` +\n `Skipping it would silently change which rows the filter admits.`,\n );\n }\n return buildNode(sub);\n });\n // A `null` branch is the constant TRUE. It is the AND identity, so it\n // drops out of a `$and` — but it ABSORBS a `$or`: one TRUE disjunct makes\n // the whole disjunction TRUE, so the group contributes NO constraint\n // rather than collapsing to its surviving branches. Collapsing is what\n // narrowed `{$or: [{}, {stage: 'won'}]}` to `stage = 'won'` (#5325).\n if (key === '$or' && branches.some((n) => n === null)) continue;\n const kept = branches.filter((n): n is NormalizedFilterNode => n !== null);\n if (kept.length === 0) continue;\n // `$and` folds into this object's own AND; `$or` becomes a node, since\n // OR is exactly the structure a flat list could not carry.\n if (key === '$and') children.push(...kept);\n else children.push(kept.length === 1 ? kept[0] : { kind: 'or', children: kept });\n continue;\n }\n\n if (key === '$not') {\n if (!isFilterObject(raw)) {\n // Same call as the branch elements above: a `$not` of garbage used to\n // vanish, which turns \"exclude these rows\" into \"exclude nothing\".\n throw invalidFilterError(\n `[analytics] \"$not\" requires a filter object, got ${JSON.stringify(raw)}. ` +\n `Dropping it would silently widen the query to rows the filter excludes.`,\n );\n }\n // NULL-safe negation (#5146): totalise the operand's leaves FIRST, so the\n // negation can never be UNKNOWN and this path admits the same rows\n // `driver-memory` / `formula` / `driver-sql` admit. The guard is added as\n // STRUCTURE here, which is what makes it survive into the ObjectQL engine\n // path too (see the module header).\n const inner = buildNode(nullSafeNegationOperand(raw));\n // `notOf` turns a TRUE operand into FALSE instead of nothing: `{$not: {}}`\n // is the zero-row filter, and emitting nothing for it charted every row.\n children.push(notOf(inner));\n continue;\n }\n\n if (key.startsWith('$')) {\n throw invalidFilterError(\n `[analytics] Unsupported top-level filter operator \"${key}\". ` +\n `Dropping it would silently widen the query to rows the filter excludes.`,\n );\n }\n\n children.push(...fieldLeaves(key, raw));\n }\n\n return andOf(children);\n}\n\n// ── [#5146 / #5325] NULL-safe `$not` ─────────────────────────────────────────\n\n/**\n * What one field constraint needs so the leaves it produces are TOTAL — TRUE or\n * FALSE for every row, never UNKNOWN.\n *\n * - `'none'` — already total (`set` / `notSet`, a boolean constant), or\n * a shape this normalizer refuses, which must keep refusing.\n * - `'requireValue'` — a NULL column does NOT satisfy it: `col IS NOT NULL AND (…)`.\n * - `'allowNull'` — a NULL column DOES satisfy it: `col IS NULL OR (…)`.\n */\ntype NullGuard = 'none' | 'requireValue' | 'allowNull';\n\n/**\n * Does a NULL column satisfy this one operator, under the semantics the JS\n * backends (`driver-memory`'s `match`, `formula`'s `matchesFilterCondition`)\n * give it? They evaluate a missing value in ordinary two-valued JS — `undefined\n * !== 'won'` is simply `true` — and #5146 ruled that answer canonical.\n *\n * This is `sql-driver.ts`'s and `read-scope-sql.ts`'s table, with the\n * differences that come from THIS module's emitter rather than from a different\n * reading of #5146 — each guard matches its own emitter, which is the invariant,\n * not the literal table:\n *\n * - `$null` / `$exists` are read by IDENTITY (`=== true` / `=== false`)\n * because {@link fieldLeaves} reads them that way, where `read-scope-sql`\n * uses truthiness because its emitter does. Immaterial in practice: both\n * compile to a null predicate, so they are total either way and never\n * reach the polarity question.\n * - `$between` exists in this vocabulary; it lowers to `gte` + `lte`, two\n * positive comparisons, so it takes the same default they do.\n *\n * `$eq` / `$ne` DO carry `read-scope-sql`'s `value === null` arms — since #5332,\n * and only since then. While {@link fieldLeaves} stringified a `null` comparand\n * to `''`, these two arms had to describe THAT emitter: `{$eq: null}` was an\n * ordinary value comparison here, the guard said so, and the TSDoc recorded the\n * `''` comparand as a separate defect deliberately left undecided. #5332 decided\n * it — the emitter now compiles the pair to `notSet` / `set` — so the arms moved\n * with it, in the same commit. The invariant is not \"copy the sibling table\", it\n * is \"each guard matches its OWN emitter\"; the two tables agreeing again is the\n * consequence of the emitters agreeing, not the reason for the edit.\n *\n * The default is the large positive-comparison family (`$gt` / `$in` /\n * `$contains` / …), every member of which answers `false` for a value that is\n * not there. An operator this module does not support also lands here; it is\n * guarded and then still THROWS from {@link fieldLeaves}, so fail-closed is\n * preserved.\n */\nfunction nullValueSatisfiesOperator(op: string, value: unknown): boolean {\n switch (op) {\n // [#5332] `$eq: null` IS the null predicate — a NULL column satisfies it,\n // and no other comparand does.\n case '$eq': return value === null;\n // Mirror image: `$ne: null` compiles to `set` (`IS NOT NULL`), which a NULL\n // column FAILS. Any other comparand is the two-valued JS `!==`, which an\n // absent value passes — the arm this used to be for every comparand.\n case '$ne': return value !== null;\n case '$null': return value === true;\n case '$exists': return value === false;\n // Negative-polarity set / substring tests hold vacuously for an absent value.\n case '$nin': return true;\n // `$notContains` is the one operator where the two JS backends disagree for\n // a null-valued field (`driver-memory` answers false, `formula` true).\n // `formula` is followed because `driver-sql` and `read-scope-sql` follow it,\n // so this module casts no vote on a disagreement that is filed elsewhere.\n case '$notContains': return true;\n default: return false;\n }\n}\n\n/** Is this operator's compiled leaf already total for a NULL column? */\nfunction operatorIsNullTotal(op: string, value: unknown): boolean {\n // [#7598, maintainer ruling 2026-08-12] A `{ $field }` comparand on any of the\n // six scalar comparison operators is TOTAL AT THE BACKEND, so this module must\n // add no guard of its own — and MEASURED, adding one changes the answer.\n //\n // Every other entry in this switch is total because THIS module compiles the\n // operator into a null predicate. This one is total because of where the leaf\n // ends up: since the ruling, a `where` carrying a reference is declined by\n // `NativeSQLStrategy.canHandle` and served on the engine path, where\n // `driver-sql`'s `applyCrossFieldComparison` emits a predicate written total\n // across NULLs by construction (it repeats both column expressions for exactly\n // that reason — see `cross-field-conformance-cases.ts`, whose rows 4-6 carry\n // every NULL arrangement a pair of columns can be in). `@objectstack/formula`\n // resolves the reference and then compares in two-valued JS. The two agree,\n // and the corpus's declared id lists are the third statement of it.\n //\n // ## What the guard did before this arm existed — measured on the wasm driver\n //\n // The `$ne` arm of {@link nullValueSatisfiesOperator} answers `true` for any\n // non-null comparand, so a reference took the negative-polarity totalisation\n // in {@link fieldLeaves} and `{ amount: { $ne: { $field: 'budget' } } }`\n // lowered to `{$or: [{amount: null}, {amount: {$ne: ref}}]}`. That admitted\n // fixture row 6 — BOTH columns NULL — where the corpus, both SQL drivers and\n // the memory evaluator all EXCLUDE it, because row 6 satisfies the inner\n // `$eq` and `$ne` is its exact complement. Six corpus cases moved: the three\n // `$ne` class-pair cases, `a column differs from itself on no row`, and the\n // two `$not`-of-`$eq` cases (which reach the same guard through\n // {@link nullGuardForFieldSpec}). Widening a `$ne`, on a shape whose producer\n // is an RLS rule, is the direction that matters.\n //\n // The guard is right for a LITERAL comparand and is untouched there: `{amount:\n // {$ne: 5}}` must still admit a NULL `amount`, which is #5298's ruling and the\n // JS backends' answer. What differs is only that a reference's NULL semantics\n // are already decided by the referent, not by the target column alone — so\n // there is nothing left for a guard to decide.\n if (CROSS_FIELD_COMPARISON_OPERATORS.has(op) && isFieldReference(value)) return true;\n switch (op) {\n // Compile to `set` / `notSet` — `IS NULL` / `IS NOT NULL`, two-valued by\n // construction, on every strategy that compiles this tree.\n case '$null':\n case '$exists':\n return true;\n // [#5332] A `null` comparand makes these null PREDICATES too — `notSet` /\n // `set`, not comparisons — so they are total by construction and take NO\n // guard. Left out, `{$not: {stage: {$eq: null}}}` wrapped `stage IS NOT NULL\n // AND stage IS NULL` (an always-false conjunction) and negated it to EVERY\n // row, for a filter meaning \"stage is not empty\".\n case '$eq':\n case '$ne':\n return value === null;\n // An EMPTY set compiles to a boolean CONSTANT (see `fieldLeaves`), and a\n // constant is total. Wrapping a guard around it would only add a redundant\n // conjunct to a predicate whose value is already decided.\n case '$in':\n case '$nin':\n return Array.isArray(value) && value.length === 0;\n default:\n return false;\n }\n}\n\n/**\n * The guard one field constraint needs. A constraint is the AND of its\n * operators, so it is total when every operator is, and a NULL column satisfies\n * it only when it satisfies all of them.\n */\nfunction nullGuardForFieldSpec(spec: unknown): NullGuard {\n // `{field: null}` compiles to `notSet` (`IS NULL`) — already total.\n if (spec === null) return 'none';\n // A bare array is an implicit `$in`; an EMPTY one is the FALSE constant.\n if (Array.isArray(spec)) return spec.length === 0 ? 'none' : 'requireValue';\n // A scalar / Date is an implicit `=`; a NULL column fails it.\n if (typeof spec !== 'object' || spec instanceof Date) return 'requireValue';\n const entries = Object.entries(spec as Record<string, unknown>);\n // `{field: {}}` is REFUSED by `fieldLeaves` (#5240). Passing it through\n // unrewritten is what keeps that refusal reachable — a guard wrapped around it\n // would only change which message the caller sees.\n if (entries.length === 0) return 'none';\n let total = true;\n let nullSatisfies = true;\n for (const [op, value] of entries) {\n if (!operatorIsNullTotal(op, value)) total = false;\n if (!nullValueSatisfiesOperator(op, value)) nullSatisfies = false;\n }\n if (total) return 'none';\n return nullSatisfies ? 'allowNull' : 'requireValue';\n}\n\n/**\n * Guard one `field: spec` entry, writing either the untouched entry into `out`\n * or its guarded form into `guarded`.\n *\n * A nested relation spec (`{account: {region: 'NA'}}`) is flattened with the\n * dotted key {@link fieldLeaves} would have produced, so the guard lands on the\n * SAME member as the leaf it protects — guarding `account` when the leaf reads\n * `account.region` would test a column that does not exist.\n */\nfunction guardFieldEntry(\n key: string,\n spec: unknown,\n out: Record<string, unknown>,\n guarded: unknown[],\n): void {\n if (\n isFilterObject(spec) &&\n Object.keys(spec).length > 0 &&\n !Object.keys(spec).some((k) => k.startsWith('$'))\n ) {\n for (const [nested, value] of Object.entries(spec)) {\n guardFieldEntry(`${key}.${nested}`, value, out, guarded);\n }\n return;\n }\n\n const guard = nullGuardForFieldSpec(spec);\n if (guard === 'none') {\n out[key] = spec;\n } else if (guard === 'requireValue') {\n // `col IS NOT NULL AND (…)` — both conjuncts of the enclosing node.\n guarded.push({ [key]: { $null: false } }, { [key]: spec });\n } else {\n // `col IS NULL OR (…)` — one conjunct, so the OR binds tighter than the AND\n // this node's keys form.\n guarded.push({ $or: [{ [key]: { $null: true } }, { [key]: spec }] });\n }\n}\n\n/**\n * [#5146] Rewrite the operand of a `$not` so every leaf compiles to a TOTAL\n * predicate — which is what makes `NOT (…)` mean here what it means in\n * `driver-memory`, `formula` and (since #5296) `driver-sql`.\n *\n * # Why the guard rides the LEAF, not the `NOT`\n *\n * For a flat operand `NOT (a IS NOT NULL AND a = ?)` and `NOT (a = ?) OR a IS\n * NULL` are the same predicate. They stop being the same as soon as the operand\n * nests: hoisting the guard above a `$not` whose operand is a `$or` re-admits\n * rows the JS backends exclude — a NULL `a` would satisfy the whole negation\n * even when the `$or`'s OTHER branch is satisfied. Totalising each leaf makes\n * the rewrite compositional instead: De Morgan is sound over two-valued leaves,\n * so `$and`, `$or` and a nested `$not` all stay correct with no special cases.\n *\n * # Why polarity is per operator\n *\n * A blanket `OR col IS NULL` would WIDEN the negative-polarity operators:\n * `{$not: {a: {$ne: 5}}}` means \"a is 5\", and both JS backends exclude a NULL\n * row from it. Adding an unconditional null escape there would hand back exactly\n * the rows the filter excludes. So each leaf is guarded in the direction its own\n * operator answers, per {@link nullValueSatisfiesOperator}.\n *\n * # Why it is a REWRITE of the condition, not of the tree\n *\n * The output is still a `FilterCondition`, so `buildNode` compiles it with no\n * new cases and — the point of doing it here rather than in the SQL strategy —\n * the guard reaches the ObjectQL engine as structure too. Running only inside a\n * `$not` keeps every other comparison's shape untouched, and a NESTED `$not` is\n * left alone on purpose: its own branch totalises its operand, and\n * `NOT <total>` is itself total, so recursing would stack a redundant guard on\n * the same column.\n */\nfunction nullSafeNegationOperand(node: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n const guarded: unknown[] = [];\n for (const [key, value] of Object.entries(node)) {\n if ((key === '$and' || key === '$or') && Array.isArray(value)) {\n // A non-object element is passed through so `buildNode` still refuses it\n // with its own message.\n out[key] = value.map((element) => (isFilterObject(element) ? nullSafeNegationOperand(element) : element));\n continue;\n }\n if (key.startsWith('$')) {\n // `$not` (handled by its own branch) and anything else `$`-prefixed keep\n // whatever this module does with them today — the rewrite rules on NULL,\n // not on the operator vocabulary, and an unknown one must still throw.\n out[key] = value;\n continue;\n }\n guardFieldEntry(key, value, out, guarded);\n }\n if (guarded.length > 0) {\n const existing = Array.isArray(out.$and) ? out.$and : [];\n out.$and = [...existing, ...guarded];\n }\n return out;\n}\n\n// ── [#5334] The FilterArray door ─────────────────────────────────────────────\n\n/**\n * [#5334] A `where` array this door cannot lower.\n *\n * Deliberately the same refusal the other doors give, in the same envelope:\n * `driver-sql` / `driver-memory` / `driver-mongodb`'s\n * `filterArrayReachedDriverError` (#5158/#5329) and the engine's own\n * `lowerWhereFilterArray`. The INFIX join form (`[condA, 'or', condB]`) is the\n * shape that makes this branch load-bearing — no schema declares it,\n * `FilterArraySchema` excludes it and `parseFilterAST` has no lowering for it,\n * so it can only be refused; silently dropping it returns the UNFILTERED\n * dataset, which is what this whole module exists to prevent.\n */\nfunction filterArrayNotLowerableError(where: unknown[]): Error {\n return invalidFilterError(\n `[analytics] received a 'where' array that is not a filter: ${JSON.stringify(where)}. ` +\n `A filter array is a comparison [field, operator, value], a logical node ` +\n `[\"and\"|\"or\", ...conditions], or a list of those — it is INPUT-ONLY sugar (spec ` +\n `'FilterArray'), lowered to a FilterCondition by @objectstack/spec parseFilterAST() at ` +\n `every door, this one included (#5158/#5334). This value cannot be lowered, and an ` +\n `unapplied filter would have charted the UNFILTERED dataset. Recognised operators: ` +\n `${[...VALID_AST_OPERATORS].sort().join(', ')}. Infix joins ([condA, \"or\", condB]) are ` +\n `NOT one of the shapes — write the prefix form [\"or\", condA, condB].`,\n );\n}\n\n/**\n * Lower an analytics query's `where` to the CANONICAL `FilterCondition` object,\n * before any node is built. `null` when the query carries no `where`.\n *\n * Extracted from {@link normalizeAnalyticsFilterTree} for #5353's second reader\n * (`inferCubeFromQuery`, which needs the lowered condition's own KEYS rather\n * than the compiled tree's leaves — see that function for why the two readers\n * want different views of one filter). The three arrival answers documented on\n * {@link normalizeAnalyticsFilterTree} are all decided HERE; that function is\n * now this lowering plus {@link buildNode}.\n *\n * Keeping the lowering in ONE place is the point of the extraction. The\n * alternative — a second `isFilterAST`/`parseFilterAST` call at the new reader —\n * is how \"the shape the cube was minted from\" and \"the shape that reached SQL\"\n * drift apart, and both refusal paths below would then have had to be\n * re-derived to stay in step.\n */\nexport function lowerAnalyticsWhere(\n query: { where?: unknown } | unknown,\n): Record<string, unknown> | null {\n if (!query || typeof query !== 'object') return null;\n const where = (query as { where?: unknown }).where;\n if (!where || typeof where !== 'object') return null;\n\n if (Array.isArray(where)) {\n // (1) `[]` is \"no filter\", not a failed filter.\n if (where.length === 0) return null;\n // (3) Not a shape `parseFilterAST` can express.\n if (!isFilterAST(where)) throw filterArrayNotLowerableError(where);\n // (2) The declared path.\n const condition = parseFilterAST(where);\n if (!condition || typeof condition !== 'object' || Array.isArray(condition)) {\n // Unreachable by construction — `isFilterAST` accepted the shape, so\n // `parseFilterAST` has a lowering for it. Loud rather than silent for the\n // same reason the engine door is: the failure mode of the two spec\n // functions disagreeing is a dropped predicate, i.e. every row.\n throw invalidFilterError(\n `[analytics] filter array ${JSON.stringify(where)} passed isFilterAST() but ` +\n `parseFilterAST() lowered it to ${JSON.stringify(condition)}. Refusing rather than ` +\n `charting the dataset unfiltered (#5158/#5334).`,\n );\n }\n return condition as Record<string, unknown>;\n }\n\n return where as Record<string, unknown>;\n}\n\n/**\n * The FIELD KEYS a lowered `FilterCondition` names in its top-level\n * CONJUNCTION — `$and` descended through, `$or` / `$not` deliberately not.\n *\n * # Why a conjunction walker and not {@link collectFilterLeaves}\n *\n * This answers a VOCABULARY question, not a predicate question: #5353's caller\n * mints an ad-hoc cube's `dimensions` from the `where`, and the rule that bag\n * has always followed is \"the `where`'s own top-level keys are field names\".\n * `collectFilterLeaves` answers a different question (every member the compiled\n * predicate binds, structure discarded) and substituting it here would have\n * changed three behaviours #5353 does not ask about — see the caller's note.\n *\n * `$and` is descended because the LOWERING ITSELF introduces it: a flat filter\n * array `[[a,…],[b,…]]` is the array spelling of the object `{a…, b…}`, and\n * `parseFilterAST` lowers it to `{$and: [{a…}, {b…}]}`. Without descending, the\n * two spellings of one filter would still mint two different cubes — the whole\n * defect. Conjunction is associative and flat, so nested `$and`s are descended\n * too; recursing at all is safe here precisely because every entry of one\n * object ANDs with its siblings at every depth (`buildNode`'s rule).\n *\n * `$or` / `$not` are NOT descended, and both spellings agree on that today:\n * `{$or: […]}` and `[\"or\", …]` each contribute no key. Reading a disjunction's\n * branches as cube dimensions is a separate question from #5353's asymmetry —\n * and answering it would force a policy on DOTTED members that #5739 owns.\n */\nexport function conjunctFieldKeys(condition: Record<string, unknown>): string[] {\n const keys: string[] = [];\n const walk = (cond: Record<string, unknown>): void => {\n for (const [key, value] of Object.entries(cond)) {\n if (key === '$and' && Array.isArray(value)) {\n for (const child of value) {\n if (isFilterObject(child)) walk(child);\n }\n continue;\n }\n // Every other `$` key is a combinator this walk does not enter (`$or`,\n // `$not`) or an operator that belongs to a field ENTRY, not to the\n // condition — neither names a field here.\n if (key.startsWith('$')) continue;\n keys.push(key);\n }\n };\n walk(condition);\n return keys;\n}\n\n/**\n * Normalize an analytics query's `where` into the tree the strategies compile.\n * `null` when the query carries no `where` — i.e. no constraint.\n *\n * `where` is declared a `FilterCondition` (`AnalyticsQuerySchema`), and the\n * object form is the whole of the contract downstream. An ARRAY nevertheless\n * arrives — it is the `FilterArray` authoring sugar four published contracts\n * teach, and analytics is a door into the runtime like any other — so it is\n * LOWERED by {@link lowerAnalyticsWhere} (#5334, on #5158's ruling C), giving\n * the same three answers `ObjectQL`'s six entry points give since #5329:\n *\n * 1. `[]` — \"no filter\", not a failed filter: `null`, the same reading every\n * layer gives it (the engine door DELETES the key; `parseFilterAST([])` is\n * `undefined`). No predicate is emitted and no error is raised.\n * 2. A well-formed `FilterArray` — lowered through `parseFilterAST` and\n * compiled by {@link buildNode}, so the author gets the SAME rows either\n * spelling produces. `isFilterAST` gates first so the operator vocabulary is\n * checked before `parseFilterAST`'s lenient `$${op}` fallback can turn a\n * misspelling into a `$sounds_like` condition nothing executes.\n * 3. Anything else array-shaped — REFUSED, loudly. Before #5334 all three of\n * these arrivals answered the same way: `return null`, which for (2) and (3)\n * means the predicate VANISHED and the chart was drawn over every row.\n *\n * Lowering rather than refusing outright is what keeps ONE dashboard's\n * metadata meaning one thing: the same `where` on a plain `find()` already\n * lowers at the engine door (#5329), so refusing it here would have forked the\n * product by which face read the metadata.\n */\nexport function normalizeAnalyticsFilterTree(\n query: { where?: unknown } | unknown,\n): NormalizedFilterNode | null {\n const condition = lowerAnalyticsWhere(query);\n if (!condition) return null;\n return buildNode(condition);\n}\n\n/**\n * Every leaf in the tree, structure discarded.\n *\n * For asking WHICH MEMBERS a filter touches — the cross-object envelope check\n * is the caller. Never for building a predicate: the leaves of an `$or` read\n * as a conjunction here, so compiling from this list would turn `a OR b` into\n * `a AND b`. Use {@link normalizeAnalyticsFilterTree} for that.\n */\nexport function collectFilterLeaves(\n node: NormalizedFilterNode | null,\n): NormalizedAnalyticsFilter[] {\n if (!node) return [];\n if (node.kind === 'leaf') return [{ member: node.member, operator: node.operator, values: node.values }];\n // A boolean constant names no member — it constrains rows, not columns — so\n // it contributes nothing to the cross-object envelope check.\n if (node.kind === 'const') return [];\n if (node.kind === 'not') return collectFilterLeaves(node.child);\n return node.children.flatMap(collectFilterLeaves);\n}\n\n/**\n * [#5526] Put one comparand into a form a SQL driver can BIND. One-way, and the\n * only stringification left in this module's value path.\n *\n * This replaces `coerceFilterValueForSql` / `coerceFilterValueForObjectQL`, and\n * the difference is the whole of #5526: those two were DECODERS — they received a\n * string and guessed which type it had been before `stringifyForCube` flattened\n * it, so `'007'` became `7`, `'null'` became real NULL and `'true'` became `1`,\n * whatever the author meant. Nothing here inspects a string. A `string` comparand\n * is returned untouched, always, because a `string` is already bindable; only the\n * JS types a driver CANNOT bind are converted, each to the one form SQL has for\n * it:\n *\n * - `boolean` → `1` / `0`. better-sqlite3 refuses a JS boolean outright\n * (\"can only bind numbers, strings, bigints, buffers, and null\"), and `1`/`0`\n * is how every dialect these strategies target spells a bit. The ObjectQL\n * path deliberately does NOT do this — the engine compares against the\n * STORED boolean, where `1` never matches `true` (the regression\n * `objectql-strategy-boolean-filter.test.ts` guards).\n * - `Date` → canonical UTC ISO text. A comparand on a temporal column has\n * normally been through `StrategyContext.coerceTemporalFilterValue` (the\n * driver's own storage convention, ADR-0053 D-A2) before it gets here; this\n * arm is the fallback for the hookless / non-temporal case, where an\n * unbindable object would otherwise reach the driver.\n * - any other object / array → JSON text. Not a meaningful comparison on any\n * column, but the shape `filter.zod.ts` cannot exclude, and a driver-level\n * bind error tells the author nothing about their filter.\n *\n * `number`, `bigint`, `null` and `string` pass through — `null` included, and\n * that is deliberate: `col > NULL` is UNKNOWN, so the widget draws nothing. It is\n * the honest answer for an unordered comparand and the one the JS backends give;\n * the `''` this used to bind was a real comparison against the empty string,\n * which on a text column silently matched rows (see the module header).\n */\nexport function toSqlBindValue(v: unknown): unknown {\n if (typeof v === 'boolean') return v ? 1 : 0;\n if (v instanceof Date) return v.toISOString();\n if (v !== null && typeof v === 'object') return JSON.stringify(v);\n return v;\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport {\n isUninterpretableTemporalComparand,\n type TemporalComparandKind,\n} from '@objectstack/core';\n\n/**\n * Which comparand SHAPES this package's filter compilers can express (#5234).\n *\n * Two questions, asked of every value that reaches a predicate:\n *\n * 1. can it become a bound parameter at all ({@link isBindableComparand})?\n * 2. does it have a faithful rendering as the TEXT of a `LIKE` pattern\n * ({@link isRenderableTextComparand})?\n *\n * They are different questions about the same value — a binary buffer binds\n * fine and renders to nothing meaningful — and they are asked at different\n * operators, so they are two predicates rather than one with a flag.\n *\n * ## Why this file exists at all\n *\n * `driver-sql`'s `applyLike` and this package's {@link likePattern} both reached\n * their comparand through `String(value)`, and `String({})` is the literal\n * `'[object Object]'`. The result was never an error: it was a parameterised,\n * syntactically perfect `LIKE '%[object Object]%'` — a pattern the author never\n * wrote. Measured against a row whose text really is `[object Object]`, that\n * pattern MATCHED it, and `$notContains` EXCLUDED it. The `$in` / `$nin` half is\n * quieter still: an object member binds, compares equal to nothing, and the list\n * silently loses an entry — so `{status: {$nin: [{…}]}}` excludes nothing while\n * claiming to exclude something.\n *\n * ## The fence is an ALLOW-list, and it is measured\n *\n * An allow-list because a deny-list silently re-admits whatever value form is\n * invented next — the lesson `driver-turso`'s `RemoteTransport` wrote down when\n * it refused these same two shapes in remote mode (cloud#1004 / #1058), which is\n * also the precedent this rule follows rather than inventing a second policy.\n *\n * What stays IN the fence was measured across every face before being kept, not\n * assumed:\n *\n * | comparand | `driver-sql` | `driver-memory` | analytics (both doors) |\n * |---|---|---|---|\n * | `{$contains: 5}` | `%5%` | `%5%` | `%5%` |\n * | `{$contains: null}` | `%null%` | no match | `%null%` (#5526 pinned) |\n * | `{$contains: {}}` | matched a row reading `[object Object]` | same | same |\n * | `{$contains: ['al','be']}` | `%al,be%` | — | `%al,be%` (read scope) / `%al%` (`where` door) |\n *\n * The primitives agree, so refusing them would BREAK agreement — #5526 kept\n * `{$contains: 5}` deliberately for exactly that reason. The last row is the\n * opposite case: an array comparand already answered two different ways inside\n * this one package, so refusing it closes a live split.\n *\n * ## [#8186] The TYPE membership is IMPORTED; only the local extras are mirrored\n *\n * These predicates used to restate `driver-sql`'s `isBindableComparand` /\n * `isRenderableTextComparand` in full, because `service-analytics` depends on no\n * driver (see its `package.json` — only `@objectstack/core`,\n * `@objectstack/spec` and `@objectstack/types`) and those are module-private\n * functions with no export to reach for. The set itself no longer needs\n * reaching for: #7872 promoted it to the shared comparand-type door in\n * `@objectstack/spec/data`, `driver-sql` and `driver-turso` consume it there,\n * and since #8186 so does this file — `isAcceptedFilterComparand` for the six\n * types, {@link ACCEPTED_FILTER_COMPARAND_TYPES_SENTENCE} for the sentence the\n * refusals quote. The old comment's own prescription, applied: \"a THIRD\n * hand-copy is the thing to refuse: import from one of the two.\"\n *\n * What is still mirrored is the part the door deliberately does not carry —\n * each face's LOCAL extras (this package's `undefined` and binary arms), which\n * `driver-sql` records at its own use sites for the same reasons. Those stay\n * held by `__tests__/like-metacharacter-escape.test.ts`, which asserts both\n * predicates against the driver's post-#7872 expressions over a shared value\n * table, and by `__tests__/comparand-door-single-source.test.ts`, which pins the\n * end-to-end accept/refuse matrix this reconciliation had to leave untouched.\n *\n * ⛔ The ENVELOPES and the position logic below are this package's own and were\n * deliberately NOT moved: a caller-authored `where` refuses with a 400\n * `INVALID_FILTER`, a read scope fails closed with a 500 (ADR-0021 D-C), and\n * only the type membership and the shared sentence come from the door.\n *\n * ## ⚠️ [#7598] What the mirror does NOT cover: a position no gate ever reached\n *\n * `{ $field: 'col' }` is the shape the two predicates above are most often\n * assumed to handle, and they do not — not because they drifted, but because\n * they are only ASKED about two of the positions a comparand can sit in. Both\n * still classify a reference object exactly as `driver-sql`'s twins do (an\n * object is neither bindable nor renderable), and both doors call them for the\n * LIKE family and for `$in`/`$nin`/`$between` MEMBERS only. The whole comparand\n * of a scalar comparison — `{ amount: { $gt: { $field: 'budget' } } }` — was\n * asked of neither, so it was BOUND: measured on `origin/main` (`5823d593d`),\n * the read-scope door compiled `\"t\".\"amount\" > ?` with the reference OBJECT in\n * the bind list and the analytics `where` door compiled the same predicate with\n * the JSON TEXT `{\"$field\":\"budget\"}`. Nothing refused, nothing logged, and the\n * predicate compares a column against a value no row can hold.\n *\n * That is why this file gained a THIRD question — {@link isFieldReference} —\n * rather than a widened answer to the first two: the defect was never a\n * misclassification, so tightening `isBindableComparand` would have changed\n * cells that were already right (and broken the mirror) while leaving the\n * unasked position unasked.\n *\n * ## …and what the answer to that question is now — maintainer ruling 2026-08-12\n *\n * #7694 stopped the bind by REFUSING the shape on both doors, as the shipped\n * interim while the routing question sat with the maintainer. The ruling (Q1 =\n * B) replaced that with routing: `NativeSQLStrategy.canHandle` DECLINES a query\n * whose `where` or read scope carries a scalar reference, the query falls\n * through to the ObjectQL/engine path, and `driver-sql` compiles the comparison\n * under the four #5222 rulings using the `initObjects` metadata it owns. The\n * capability is therefore AVAILABLE on the analytics face, and the four security\n * rulings live in exactly one place — the option-A alternative (a\n * `StrategyContext` enumeration hook plus a second copy of those rulings here)\n * was rejected precisely because a guard that exists twice is a guard that will\n * disagree with itself.\n *\n * What this file contributes to that is {@link findCrossFieldComparand}, the\n * routing predicate, alongside the two refusal sentences that survive it:\n * {@link fieldReferenceComparandMessage} for the `/analytics/sql` echo, which\n * cannot honestly RENDER a predicate it does not emit, and\n * {@link fieldReferenceBetweenBoundMessage} for a `$between` endpoint, which no\n * backend serves and which #7596 removed from the spec.\n */\n\nimport {\n isAcceptedFilterComparand,\n ACCEPTED_FILTER_COMPARAND_TYPES_SENTENCE,\n} from '@objectstack/spec/data';\n\n/**\n * Can this value be handed to a driver as a bound parameter at all?\n *\n * [#8186] The TYPE membership is the shared comparand-type door's\n * (`isAcceptedFilterComparand`, `@objectstack/spec/data`), not this file's. It\n * used to be spelled out here, and identically again in\n * {@link isRenderableTextComparand} — three copies of one six-type set counting\n * the door itself, which is the drift risk #8186 was filed on rather than a\n * defect: the copies AGREED with the door, cell for cell, right up to this\n * change (`__tests__/comparand-door-single-source.test.ts` measured the whole\n * matrix on `origin/main` first, then re-ran it unchanged after).\n *\n * `driver-sql`'s twin was reconciled the same way by #7872 and this mirrors its\n * post-#7872 spelling, so the two are still a value-for-value mirror — see the\n * \"Mirrored, not imported\" section above, whose subject is now the LOCAL EXTRAS\n * rather than the set.\n *\n * ## The one package-local extra here: binary\n *\n * `ArrayBuffer.isView` (which covers `Buffer`, a `Uint8Array`) is a bindable the\n * engine-level door does not admit, and it is deliberately kept: a blob column\n * really is comparable on this driver family, and the read path measured it\n * accepted in every bind position. It is `driver-sql`'s own recorded extra too.\n */\nexport function isBindableComparand(value: unknown): boolean {\n // `undefined` — see {@link isRenderableTextComparand}'s note; it is admitted\n // here for the same reason and is just as unreachable through either door.\n if (value === undefined) return true;\n return isAcceptedFilterComparand(value) || ArrayBuffer.isView(value);\n}\n\n/**\n * Does this value have a faithful rendering as the text of a `LIKE` pattern?\n *\n * [#8186] The bindable set minus binary — and, like its sibling, the six-type\n * membership is now the shared door's (`isAcceptedFilterComparand`,\n * `@objectstack/spec/data`) rather than a local re-spelling. Binary binds but\n * renders to nothing a caller meant, which is why the two questions are two\n * predicates rather than one with a flag.\n *\n * ## The package-local extra here: `undefined` — kept, and unreachable\n *\n * `undefined` is inside the fence because it is not authorable (JSON has no\n * `undefined`) and `filter-normalizer.ts`'s `comparand()` normalises it to\n * `null` rather than refusing it (#5526, #5332).\n *\n * ⚠️ It no longer REACHES either predicate from either door, and the two\n * refusals arrived separately:\n *\n * | door | what refuses an `undefined` comparand first | envelope |\n * |---|---|---|\n * | read scope | `read-scope-sql.ts`, per #6050 ruling B pushed down by #6125 | `READ_SCOPE_COMPILE_FAILED` / 500 |\n * | analytics `where` | `assertDefinedComparands` (#6386, same ruling) | `INVALID_FILTER` / 400 |\n *\n * So `comparand()`'s normalise-to-`null` is itself a deliberately-kept dead arm\n * (its own TSDoc says so, and says reopening it is #5526's call, not a\n * cleanup's) — and this branch is one too. ⛔ Neither is evidence that this\n * package TOLERATES `undefined` where the shared door refuses it: measured\n * through both doors, `undefined` is REFUSED here exactly as the door would\n * refuse it (`__tests__/comparand-door-single-source.test.ts` pins that row at\n * both doors, in all three comparand positions). The branch stays because these\n * predicates are a value-for-value mirror of `driver-sql`'s twins, which keep\n * theirs for the identical reason — refused upstream there too, since #6050.\n * Narrowing the fence here would break the mirror without removing a reachable\n * answer.\n */\nexport function isRenderableTextComparand(value: unknown): boolean {\n return value === undefined || isAcceptedFilterComparand(value);\n}\n\n/**\n * [#7598] Is this comparand a `{ $field: 'col' }` reference — the shape\n * `FieldReferenceSchema` declares and `compileCelToFilter` really produces for a\n * field-to-field comparison in a CEL permission / RLS rule?\n *\n * Character for character `driver-sql`'s module-private `fieldReferenceOf`\n * (`sql-driver.ts`), read as a boolean: a plain object, not an array, carrying a\n * `$field` whose value is a STRING. Two deliberate consequences of mirroring\n * that spelling rather than inventing a third:\n *\n * - **Extra keys do not disqualify it.** `{ $field: 'budget', extra: 1 }` IS a\n * reference on all three faces, because `@objectstack/formula`'s\n * `resolveValue` reads `'$field' in raw` and ignores the remainder. A\n * narrower reading here would let the remainder be re-bound as a literal on\n * one face and resolved on another — the split this whole file exists to\n * close (#5222 measured the same cell driver-side and moved its own test).\n * - **A non-string `$field` is NOT one.** `{ $field: 5 }` falls through to the\n * ordinary object-comparand account — `driver-sql` binds it as JSON there\n * and so does this package (#5234 left `{$eq: {…}}` alone on purpose). That\n * cell is untouched here; changing it would be a different ruling, not a\n * rider on this one.\n *\n * ⚠️ `@objectstack/formula` is the WIDER of the two (`'$field' in raw`, any\n * value type). The driver's spelling is mirrored because this file's contract is\n * to be a value-for-value mirror of `driver-sql`, and because the wider reading\n * would refuse a shape the drivers bind — a new divergence in a change that\n * exists to remove one.\n */\nexport function isFieldReference(value: unknown): value is { $field: string } {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n return typeof (value as Record<string, unknown>).$field === 'string';\n}\n\n/**\n * [#7598] The comparison operators whose whole comparand `driver-sql` compiles\n * into a same-table column-to-column comparison since #5222 — and exactly the\n * positions where a `{ $field }` silently BOUND on this package's two doors.\n *\n * A mirror of `driver-sql`'s module-private `CROSS_FIELD_COMPARISON_OPERATORS`,\n * held by `__tests__/cross-field-reference-refusal.test.ts` rather than by this\n * comment: that suite drives the SHARED corpus (`CROSS_FIELD_CASES`, exported\n * from `@objectstack/driver-sql` precisely so a second face can be held to the\n * same table), so an operator the driver starts or stops compiling shows up as a\n * corpus case this package answers differently.\n *\n * Every OTHER position a reference can occupy was already refused on both doors\n * and is deliberately left alone, wording included — the LIKE family through\n * {@link isRenderableTextComparand}, `$in` / `$nin` members through\n * {@link isBindableComparand}, and a bare `{ field: { $field: … } }` as an\n * unsupported operator. Those refusals CONVERGE with `driver-sql`, which refuses\n * the same positions in its own #5222 refusal arm; only this set diverged.\n */\nexport const CROSS_FIELD_COMPARISON_OPERATORS: ReadonlySet<string> = new Set([\n '$eq', '$ne', '$gt', '$gte', '$lt', '$lte',\n]);\n\n/**\n * [#7598, maintainer ruling 2026-08-12 Q1 = B] The first `{ $field }` reference\n * sitting in a position `driver-sql` COMPILES since #5222 — or `null`.\n *\n * ## What this is FOR, which is not what the two predicates above are for\n *\n * {@link isBindableComparand} and {@link isRenderableTextComparand} answer\n * \"may this ONE value reach that ONE position\". This walks a WHOLE filter —\n * an analytics `where` (already lowered by `lowerAnalyticsWhere`, so the\n * authored array sugar arrives here as a `FilterCondition`) or an RLS read\n * scope — and answers a routing question instead: **does serving this query\n * require the cross-field capability?** `NativeSQLStrategy.canHandle` reads it\n * to DECLINE, so the query falls through to the ObjectQL/engine path, where\n * `driver-sql` compiles the comparison and enforces the four #5222 rulings\n * with the `initObjects` metadata it owns. See that method for the ruling.\n *\n * ## Why only the six scalar operators, when the ruling says \"carries `$field`\"\n *\n * Every OTHER position a reference can occupy is refused IDENTICALLY on both\n * sides of the routing decision — the LIKE family and `$in` / `$nin` members\n * through this file's two predicates here and through `driver-sql`'s own #5222\n * refusal arm, a `$between` endpoint through\n * `filter-normalizer.ts`'s surviving gate, a bare `{ field: { $field: … } }` as\n * an unsupported operator. Declining for those would swap one refusal for\n * another refusal a package further away, trading this package's precise\n * wording for the driver's without changing a single outcome. The scalar\n * comparands are the whole of what routing BUYS, so they are the whole of what\n * it tests.\n *\n * The walk is structural and total: it descends into `$and` / `$or` arrays,\n * `$not` operands, nested relation objects and any other nesting, because a\n * reference three combinators deep still needs the engine path. It is\n * deliberately blind to whether the referenced column is DECLARED, is the\n * tenant column, or has a comparable type — those are the four rulings, they\n * live in exactly one place (`driver-sql`), and re-asking them here is the\n * duplicated-guard the ruling rejected as option A.\n */\nexport function findCrossFieldComparand(\n filter: unknown,\n): { op: string; field: string; ref: string } | null {\n return findIn(filter, '');\n}\n\nfunction findIn(\n node: unknown,\n field: string,\n): { op: string; field: string; ref: string } | null {\n if (!node || typeof node !== 'object') return null;\n if (Array.isArray(node)) {\n for (const child of node) {\n const hit = findIn(child, field);\n if (hit) return hit;\n }\n return null;\n }\n if (node instanceof Date || ArrayBuffer.isView(node)) return null;\n for (const [key, value] of Object.entries(node as Record<string, unknown>)) {\n if (CROSS_FIELD_COMPARISON_OPERATORS.has(key) && isFieldReference(value)) {\n return { op: key, field, ref: value.$field };\n }\n // A `$`-prefixed key is an operator or a combinator, so the FIELD in scope\n // does not change; anything else names a field (or a nested relation\n // member) and becomes the new scope. Only used for the message.\n const hit = findIn(value, key.startsWith('$') ? field : key);\n if (hit) return hit;\n }\n return null;\n}\n\n/**\n * [#8690, maintainer ruling 2026-08-15] The first comparand a declared TEMPORAL\n * member's storage rule cannot read — or `null`.\n *\n * ## What it is FOR: the raw-SQL bypass named in the ruling\n *\n * The refusal itself lives at the ObjectQL engine's filter collection point\n * (`@objectstack/objectql`, `temporal-comparand-door.ts`), which is the one\n * seam that holds a comparand and the field's declared type at the same moment.\n * `NativeSQLStrategy` never reaches it: it compiles its own\n * `SELECT … WHERE col >= $N` and binds the comparand directly, so a raw-SQL\n * deployment would keep answering the silent zero the engine door now refuses.\n * The ruling closes that by name:\n *\n * > `NativeSQLStrategy.canHandle` must **decline** an uninterpretable temporal\n * > comparand so raw-SQL paths fall through to the engine door.\n *\n * So this answers a ROUTING question, exactly as {@link findCrossFieldComparand}\n * does one seam over: not \"is this filter legal\" but \"does serving it correctly\n * need the path that judges it\". Declining sends the query to the ObjectQL\n * strategy, whose `engine.aggregate` passes through that door — one refusal,\n * one wording, one place, whichever strategy the deployment's driver selects.\n *\n * ## Why the KIND is supplied by the caller\n *\n * This package holds no field map — it depends on `core`, `spec` and `types`,\n * and on no driver. The temporal fact therefore has to arrive with the query,\n * and it already does: a cube DIMENSION declares `type: 'time'` (compiled from\n * the dataset's `type: 'date'`), and `resolveStorageTarget`/`lookupMember`\n * already map a filter member to it. `kindOf` is that lookup, passed in, so\n * this walk stays a pure function of the filter and the caller's classification.\n *\n * ⚠️ Consequence, recorded rather than hidden: a temporal column filtered\n * WITHOUT being declared as a time dimension on the cube is not classified\n * here, so it is not declined and keeps today's behaviour on the raw-SQL path.\n * That is a strictly smaller hole than \"every raw-SQL query bypasses the door\",\n * it fails in the safe direction (a missed decline degrades to today's\n * behaviour, never to a NEW wrong answer), and closing it fully would take a\n * field map this package deliberately does not have.\n *\n * The walk is structural and total for the same reason its sibling's is: a\n * comparand three combinators deep still needs the engine path.\n */\nexport function findUninterpretableTemporalMember(\n filter: unknown,\n kindOf: (member: string) => TemporalComparandKind | null,\n): { field: string; kind: TemporalComparandKind; value: string } | null {\n return findUninterpretableIn(filter, '', kindOf);\n}\n\nfunction findUninterpretableIn(\n node: unknown,\n field: string,\n kindOf: (member: string) => TemporalComparandKind | null,\n): { field: string; kind: TemporalComparandKind; value: string } | null {\n if (!node || typeof node !== 'object') return null;\n if (Array.isArray(node)) {\n for (const child of node) {\n const hit = findUninterpretableIn(child, field, kindOf);\n if (hit) return hit;\n }\n return null;\n }\n if (node instanceof Date || ArrayBuffer.isView(node)) return null;\n // A reference is not a literal — the same position this file's sibling walk\n // routes on, and never a value any storage rule reads.\n if (isFieldReference(node)) return null;\n for (const [key, value] of Object.entries(node as Record<string, unknown>)) {\n // `$`-prefixed keys are operators and combinators: the field in scope does\n // not change. Anything else names a member and becomes the new scope.\n const scope = key.startsWith('$') ? field : key;\n const kind = scope ? kindOf(scope) : null;\n if (kind) {\n const hit = judgeTemporalLiterals(value, scope, kind);\n if (hit) return hit;\n continue;\n }\n const hit = findUninterpretableIn(value, scope, kindOf);\n if (hit) return hit;\n }\n return null;\n}\n\n/**\n * Every literal reachable in one classified member's value position — the\n * comparand itself, an operator bag's comparands, and each MEMBER of a list\n * operator's array, which is a comparand in its own right.\n */\nfunction judgeTemporalLiterals(\n value: unknown,\n field: string,\n kind: TemporalComparandKind,\n): { field: string; kind: TemporalComparandKind; value: string } | null {\n if (Array.isArray(value)) {\n for (const member of value) {\n const hit = judgeTemporalLiterals(member, field, kind);\n if (hit) return hit;\n }\n return null;\n }\n if (value && typeof value === 'object') {\n if (value instanceof Date || ArrayBuffer.isView(value) || isFieldReference(value)) return null;\n for (const nested of Object.values(value as Record<string, unknown>)) {\n const hit = judgeTemporalLiterals(nested, field, kind);\n if (hit) return hit;\n }\n return null;\n }\n return isUninterpretableTemporalComparand(kind, value)\n ? { field, kind, value: value as string }\n : null;\n}\n\n/**\n * The Filter Protocol operators whose comparand becomes the text of a `LIKE`\n * pattern — the ones every compiler in this package routes through\n * {@link likePattern}.\n *\n * `$regex` is absent because this package's operator vocabulary does not carry\n * it: `MONGO_TO_CUBE_OP` has no entry and `read-scope-sql` refuses it by name.\n * (`driver-sql` DOES list it, because the better-auth adapter emits it there for\n * a substring search.)\n *\n * [#7693] `$icontains` belongs here for the same reason its four siblings do,\n * and was missing for the ordinary reason a set goes stale: the operator\n * arrived AFTER the fence. #6520 added it to `MONGO_TO_CUBE_OP` and gave\n * `read-scope-sql`'s arm its `assertRenderableText` call, but not this entry —\n * so the analytics `where` door, this set's only reader, applied NO\n * comparand-shape gate to it at all. Measured on `origin/main` @ `b54aaab`:\n *\n * | door | `{name: {$icontains: {foo: 1}}}` |\n * |---|---|\n * | analytics `where` | compiled — `NativeSQLStrategy` bound `'%[object Object]%'` into its `LIKE` |\n * | `read-scope-sql` | REFUSED (`READ_SCOPE_COMPILE_FAILED` / 500) |\n *\n * One operator, two answers inside one package — #5234's defect verbatim, at\n * the operator its fence was never extended to. The ASCII fold `$icontains`\n * adds rides ON TOP of the pattern text (`likeShape` maps it to `'contains'`\n * on both executing compilers), so the question this set asks of a comparand\n * is the same question and the answer had no business differing. `driver-sql`'s\n * own `TEXT_PATTERN_OPERATORS` has listed it since #6520; this entry closes the\n * third and last face, after #7158 closed the objectql `having` one.\n */\nexport const TEXT_PATTERN_OPERATORS: ReadonlySet<string> = new Set([\n '$contains', '$notContains', '$startsWith', '$endsWith', '$icontains',\n]);\n\n/** A short, non-throwing rendering of an offending comparand for a message. */\nexport function shapePreview(value: unknown): string {\n try {\n const json = JSON.stringify(value);\n if (typeof json !== 'string') return typeof value;\n return json.length > 80 ? `${json.slice(0, 77)}...` : json;\n } catch {\n return typeof value;\n }\n}\n\n/**\n * The sentence both doors say about an object where a `LIKE` pattern's text\n * belongs, so the analytics `where` door and the read-scope lowering do not\n * describe one rule two ways. Each door wraps it in its OWN envelope — a 400\n * `INVALID_FILTER` for a caller-authored filter, a fail-closed compile refusal\n * for a read scope — because the envelope is what differs between them, not the\n * diagnosis.\n */\nexport function unrenderableTextComparandMessage(op: string, field: string, value: unknown): string {\n return (\n `\"${op}\" on \"${field}\" matches against the TEXT of a pattern, but its comparand is ` +\n `${Array.isArray(value) ? 'an array' : 'an object'} (${shapePreview(value)}). filter.zod.ts ` +\n `declares it a string (StringOperatorSchema); ${ACCEPTED_FILTER_COMPARAND_TYPES_SENTENCE} is ` +\n `accepted. Refusing rather than stringifying it: String({}) is \"[object Object]\", so the ` +\n `pattern that ran would be one nobody wrote — and a row storing that literal text matches it.`\n );\n}\n\n/**\n * [#7598] What `read-scope-sql` says about a `{ $field }` comparand it cannot\n * lower — the ONE surviving caller of this sentence after the 2026-08-12 ruling,\n * and the reason it now reads as a RENDERING boundary rather than a capability\n * one.\n *\n * ## What changed under the ruling, and why the wording had to follow\n *\n * Until that ruling this sentence was said by both doors and meant \"the platform\n * will not serve this here\". It no longer means that. Q1 = B routes a query\n * whose `where` or read scope carries a reference to the ObjectQL/engine path,\n * where `driver-sql` compiles the comparison and enforces the four #5222 rulings\n * with metadata it owns — so `/analytics/query` SERVES these queries and returns\n * rows. What is left is `compileScopedFilterToSql`, and the caller that still\n * reaches it with such a scope is `ObjectQLStrategy.generateSql`: the\n * `/analytics/sql` ECHO, a display string for an execution it does not perform.\n * There is no honest rendering of a total column-to-column predicate available\n * to that renderer, and the ruling's answer for the echo was explicit —\n * 「一致的响亮答案,不半渲染」 (one consistent, loud answer; no half-rendering).\n *\n * So the message tells a reader three things it could not tell them before: the\n * query itself is fine, the ECHO is what declined, and the rows are one call\n * away on `/analytics/query`. Telling them instead to \"compare against a literal\"\n * would send them to repair a rule that works.\n *\n * It still names what USED to happen, because that is the part a reader cannot\n * reconstruct: the reference was BOUND. The predicate was syntactically perfect,\n * the query ran, and a column was compared against a value no row can hold — no\n * error, no log line, an admin's read scope quietly answering the wrong row set.\n * That is the #3650 / #5234 class, and naming it is what stops the next reader\n * from \"restoring\" the old tolerance as a convenience.\n *\n * ⛔ `position` is no longer passed by any caller for a `$between` endpoint —\n * that arm says {@link fieldReferenceBetweenBoundMessage} instead, because it is\n * refused permanently and everywhere rather than declined by one renderer. The\n * parameter stays for a caller that needs to locate a reference inside a nested\n * scope.\n */\nexport function fieldReferenceComparandMessage(\n op: string,\n field: string,\n ref: string,\n position?: string,\n): string {\n return (\n `\"${op}\" on \"${field}\"${position ? ` (${position})` : ''} compares against the field reference ` +\n `{ \"$field\": \"${ref}\" }, which this compiler does not lower into a column-to-column ` +\n `comparison. Refusing rather than binding it: the reference object used to become the BOUND ` +\n `VALUE of the comparison, so the emitted predicate compared \"${field}\" against the reference ` +\n `itself — a value no row can hold — and a read scope built from it answered the wrong row set ` +\n `with nothing to read. ⚠️ This is NOT the platform declining the rule. @objectstack/spec ` +\n `declares this shape (FieldReferenceSchema), @objectstack/formula resolves it per record in ` +\n `memory, driver-sql / driver-sqlite-wasm compile it to a same-table column comparison for the ` +\n `six scalar operators since #5222, and since the 2026-08-12 ruling on #7598 the analytics ` +\n `native-SQL strategy DECLINES such a query so it routes to the ObjectQL engine path and runs ` +\n `there — the driver enforcing declared-only enumeration, the tenant-isolation ban and the ` +\n `comparison class with metadata it owns. What refuses here is this SQL lowering, whose only ` +\n `remaining caller is the /analytics/sql display echo; it has no faithful rendering of the ` +\n `predicate the engine path actually runs, and half-rendering one would describe a query that ` +\n `returns different rows. Run the query itself (/analytics/query) to get its rows (#7598).`\n );\n}\n\n/**\n * [#7598] A `{ $field }` in a `$between` ENDPOINT — a separate sentence from\n * {@link fieldReferenceComparandMessage} because it is a separate condition,\n * and #5240's rule cuts the other way here: two shapes with two repairs must\n * not share one wording.\n *\n * The scalar comparands above are SERVED, one path over. A `$between` endpoint\n * is not served anywhere and is not going to be: `driver-sql` and\n * `driver-sqlite-wasm` refuse it (`CROSS_FIELD_REFUSALS` pins both endpoints),\n * the memory evaluator has no reading of it either — `resolveValue` returns an\n * array unchanged, so the bounds are ordered against the raw reference OBJECT —\n * and #7596 removed the position from `FieldReferenceSchema` outright under\n * ADR-0049 declared = enforced (maintainer ruling 2026-08-11). Pointing that\n * author at the engine path would point them at another refusal.\n *\n * It matters most on THIS door, which is why the arm exists here at all: the\n * analytics `where` lowering splits `$between` into a `gte` leaf and an `lte`\n * leaf, so an endpoint reference would reach the driver wearing an operator\n * #5222 COMPILES — succeeding on the analytics face alone, in defiance of both\n * the driver corpus and the schema. See `filter-normalizer.ts`'s\n * `assertNoFieldReferenceComparand`.\n */\nexport function fieldReferenceBetweenBoundMessage(\n op: string,\n field: string,\n ref: string,\n index: number,\n): string {\n return (\n `\"${op}\" on \"${field}\" has the field reference { \"$field\": \"${ref}\" } at index ${index} of its ` +\n `[min, max] bounds. A range BOUND may not be a field reference on any backend: driver-sql and ` +\n `driver-sqlite-wasm refuse both endpoints (#5222), @objectstack/formula does not resolve a ` +\n `reference inside a list either — it orders the bounds against the raw reference object, which ` +\n `no value compares meaningfully to — and @objectstack/spec no longer declares the position at ` +\n `all (#7596 removed FieldReferenceSchema from the $between endpoint union, ADR-0049 declared = ` +\n `enforced). Refusing rather than lowering it: this compiler splits $between into its two ` +\n `bounds, so the reference would arrive at the driver under a \"$gte\" / \"$lte\" the author never ` +\n `wrote — a position the SQL drivers DO compile — and the range would quietly succeed here ` +\n `while the identical filter is refused everywhere else. Use a literal bound, or spell the ` +\n `comparison you meant as a scalar one ({ \"${field}\": { \"$gte\": { \"$field\": \"${ref}\" } } }), ` +\n `which IS served — on the ObjectQL engine path, where the driver enforces the #5222 rulings ` +\n `(#7598).`\n );\n}\n\n/**\n * The sentence both doors say about a list member that cannot be bound. See\n * {@link unrenderableTextComparandMessage} for why the message is shared and the\n * envelope is not.\n *\n * [#8186] The accepted-set clause is {@link ACCEPTED_FILTER_COMPARAND_TYPES_SENTENCE},\n * with binary kept as this package's own parenthetical extra — the exact shape\n * `driver-sql`'s twin took when #7872 reconciled it, so the two faces describe\n * one rule in one wording again. The hand-copy it replaces read \"a string,\n * number, boolean, null, Date or binary value\", which had silently gone WRONG\n * in the quieter direction: it omitted `bigint`, a type both predicates here\n * have always accepted and both doors have always compiled. Quoting the door\n * fixes the omission as a side effect of removing the copy — the accepted set\n * itself does not move (`__tests__/comparand-door-single-source.test.ts`).\n */\nexport function unbindableListMemberMessage(\n op: string,\n field: string,\n value: unknown,\n index: number,\n): string {\n return (\n `\"${op}\" on \"${field}\" has a value at index ${index} of its list that cannot be bound as a SQL ` +\n `parameter: ${shapePreview(value)}. Every member of an $in/$nin/$between list is a comparand ` +\n `in its own right — use ${ACCEPTED_FILTER_COMPARAND_TYPES_SENTENCE} (or a binary value). ` +\n `Refusing rather than binding it: the member can equal no stored value, so the list silently ` +\n `loses that entry (and a $nin loses the exclusion the caller wrote).`\n );\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * LIKE pattern construction for this package's three SQL compilers (#5567).\n *\n * A `$contains` / `$notContains` / `$startsWith` / `$endsWith` comparand is a\n * LITERAL the author typed. Concatenating it straight into a wildcard position\n * silently reinterprets it as a pattern, because `_` is LIKE's single-character\n * wildcard and `%` its multi-character one:\n *\n * - `{name: {$contains: '_admin'}}` matched `xyadmin` as well as `x_admin`;\n * - `{name: {$contains: '50%'}}` matched `off 5012 now` as well as `off 50% now`.\n *\n * Both directions are WIDENING, and one of the three call sites is\n * `read-scope-sql.ts` — the ADR-0021 D-C read-scope (tenant + RLS) lowering,\n * where a wider predicate is over-reach rather than a loose filter (#5347 /\n * #5324, on that same file). Prime Directive #3 forces machine names to\n * `snake_case`, so essentially every machine-name comparand carries a `_` and\n * hits this silently.\n *\n * ## The two halves are one fix\n *\n * Escaping the value and declaring the escape character are not independent\n * steps — either alone is a different bug:\n *\n * - Escaping alone: `%\\_admin%` with no escape character in force is a search\n * for a literal backslash. SQLite has NO default escape character, so this\n * would return zero rows there.\n * - The clause alone: nothing in the pattern is escaped, so nothing changes.\n *\n * Hence {@link likePattern} always produces a pattern escaped for\n * {@link LIKE_ESCAPE_CHAR}, and every emitter pairs it with an `ESCAPE` binding.\n *\n * ## Why the escape character is BOUND, never written as a literal\n *\n * Every emitter here binds it as an ordinary placeholder (`LIKE ? ESCAPE ?`)\n * rather than writing `ESCAPE '\\'` into the SQL text. Two reasons, both load-bearing:\n *\n * 1. **The literal spelling is not portable.** MySQL applies C escape syntax\n * inside string literals — \"If you want a LIKE string to contain a literal\n * `\\`, you must double it\" — so the backslash escape character is spelled\n * `'\\\\'` there and `'\\'` on SQLite/Postgres. These compilers do not know\n * which dialect will run their output. A bound value is escaped by the\n * driver for its own dialect, so there is exactly one spelling here.\n * 2. **It rides the existing placeholder plumbing.** `read-scope-sql.ts` emits\n * `?` and BOTH of its consumers (`NativeSQLStrategy.applyReadScope`,\n * `ObjectQLStrategy.generateSql`) renumber `?` → `$N` while pushing the\n * matching value. Because the escape character is a bound value it is\n * carried by that rewrite with no change at the upper layer — which answers\n * the \"which layer does the ESCAPE clause belong to\" question in the issue:\n * the predicate layer, entirely, because nothing above it has to know.\n *\n * Dialect support for the clause itself, confirmed against the vendors' own\n * reference manuals (quoted in PR for #5567): Postgres defaults to backslash and\n * accepts `ESCAPE`; MySQL assumes `\\` unless `NO_BACKSLASH_ESCAPES` is set and\n * accepts `ESCAPE` with an argument that \"must evaluate as a constant at\n * execution time\" (a bound placeholder is); SQLite honours NO default escape\n * character at all, which is the reason the explicit clause is required rather\n * than merely tidy.\n *\n * ## Relationship to `driver-sql`'s `applyLike`\n *\n * This is deliberately the same transform `SqlDriver.applyLike`\n * (`packages/drivers/driver-sql/src/sql-driver.ts`) applies — same escaped\n * character class, same three wildcard shapes, same bound `ESCAPE` — and its\n * TSDoc points back here. It is a SECOND implementation on purpose, not an\n * oversight:\n *\n * - `service-analytics` depends on no driver (see its `package.json`: only\n * `@objectstack/core` and `@objectstack/spec`), and `applyLike` is a private\n * method on a knex builder — it takes a builder and a field, not a string,\n * so there is nothing importable even if the dependency existed.\n * - Promoting it to a shared package would add a new public surface to\n * `@objectstack/core` for three call sites inside one package. Not worth a\n * new export until a fourth consumer outside this package needs it.\n *\n * What keeps the two from drifting is not these comments: it is\n * `__tests__/like-metacharacter-escape.test.ts`, which asserts\n * {@link escapeLikePattern} against `applyLike`'s expression character for\n * character. A third hand-copy of this logic anywhere is the thing to refuse —\n * import from here, or add a consumer to that test.\n *\n * ## [#6518] Case sensitivity: why this file still emits a plain `LIKE`\n *\n * #4706 Q2 = A rules the `$contains` family case-SENSITIVE on every backend, and\n * #6518 moved the driver family onto that answer — `SqlDriver`'s\n * `textMatchPredicate` now picks the construct per DIALECT, because `LIKE` folds\n * ASCII on SQLite and follows the collation on MySQL. The obvious question is\n * why the compilers here did not move with it, and the answer is measured\n * rather than assumed:\n *\n * 1. **These compilers emit Postgres-shaped SQL, and on Postgres `LIKE` is\n * already exactly the ruled semantics.** Both consumers number their\n * placeholders `$1`, `$2`, … (`native-sql-strategy.ts`'s `buildFilterClause`\n * and `objectql-strategy.ts`'s filter render), and `applyReadScope` /\n * `generateSql` rewrite this file's `?` into `$N` on the way out;\n * identifiers are `\"double quoted\"`. Measured on a live PostgreSQL 16\n * against the shared nine-row fixture: `LIKE '%acme%'` answers row 2 alone\n * and `LIKE '%ACME%'` answers row 1 alone — case-exact, which is the\n * contract. So there is no divergence to close HERE, and changing the\n * construct would create one.\n * 2. **The RLS fork the issue warned about does not open.** #6518's concern\n * was that a driver-only fix would compile one permission rule into two row\n * sets. It does not, because the two paths meet only on Postgres — where\n * `textMatchPredicate`'s postgres arm is also a plain `LIKE`, unchanged.\n *\n * What that reasoning DEPENDS on is the dialect, so it is the thing to re-open\n * rather than the code: **if these compilers ever emit for SQLite or MySQL, this\n * file is wrong and `$contains` silently over-matches there** — on\n * `read-scope-sql.ts`'s output that is ADR-0021 read-scope over-reach, not a\n * loose filter (#3948). Two things would have to arrive together: a dialect\n * input reaching these three compilers, and the per-dialect construct table\n * `textMatchPredicate` already carries. Neither exists today and neither is\n * invented here on speculation. `__tests__/like-metacharacter-escape.test.ts`\n * pins both halves of the claim — that the emitted statement is\n * Postgres-shaped, and that the family is compiled case-EXACT — so this\n * paragraph goes red rather than merely stale.\n *\n * `$icontains` IS implemented here since #6520, and it is a separate construct\n * rather than a flag on the family above: it folds ASCII case on BOTH sides via\n * {@link asciiLowerSqlExpr}, while the `$contains` family stays case-EXACT. The\n * two must not be collapsed — a shared \"case-insensitive\" path would give the\n * `$contains` family the fold the ruling took away from it.\n *\n * ## `String(value)` is safe here because nothing unrenderable reaches it (#5234)\n *\n * The `String()` below used to be the whole defect on the other side: `String({})`\n * is the literal `'[object Object]'`, so an object comparand built a parameterised\n * `LIKE '%[object Object]%'` — valid SQL, a pattern nobody wrote, and one that\n * MATCHED a row whose text really was `[object Object]`. This function is NOT the\n * place that was fixed. Both of this package's doors refuse an object comparand\n * before a pattern is built — `filter-normalizer.ts`'s `fieldLeaves` for the\n * analytics `where` path and `read-scope-sql.ts`'s `compileOperator` for the RLS\n * lowering — using the one rule in `comparand-shape.ts`, and `driver-sql`'s\n * `assertCompilableComparand` does the same for `applyLike`.\n *\n * So `escapeLikePattern` keeps its `unknown` parameter and its unconditional\n * `String()` on purpose: what arrives is a string, number, bigint, boolean,\n * `null`, `undefined` or `Date`, each of which `String()` renders faithfully, and\n * a number comparand (`{$contains: 5}` → `%5%`) is deliberately still accepted —\n * it agrees across every face and #5526 kept it. Do NOT add a tolerant reading of\n * an object here; add it to neither door either.\n */\n\n/**\n * Where the wildcard sits relative to the comparand. Named exactly as\n * `driver-sql`'s `applyLike` names its `shape` parameter, so the two read alike:\n * `contains` → `%v%`, `starts` → `v%`, `ends` → `%v`.\n */\nexport type LikeShape = 'contains' | 'starts' | 'ends';\n\n/**\n * The escape character every emitter in this package binds into its `ESCAPE`\n * clause. A single backslash — the value `driver-sql` binds, and the default\n * Postgres and MySQL already assume.\n */\nexport const LIKE_ESCAPE_CHAR = '\\\\';\n\n/**\n * Escape the LIKE metacharacters (`%`, `_`) and the escape character itself\n * (`\\`) so a comparand matches literally.\n *\n * Character for character the expression `driver-sql`'s `applyLike` uses; the\n * shared test holds them to each other.\n */\nexport function escapeLikePattern(value: unknown): string {\n return String(value).replace(/[\\\\%_]/g, '\\\\$&');\n}\n\n/**\n * Build the LIKE pattern for one comparand: escaped, then wrapped in the\n * wildcards `shape` calls for.\n *\n * The result MUST be bound together with {@link LIKE_ESCAPE_CHAR} as the\n * predicate's `ESCAPE` argument — see the escaping-alone note in this file's\n * header for what happens on SQLite otherwise.\n */\nexport function likePattern(shape: LikeShape, value: unknown): string {\n const escaped = escapeLikePattern(value);\n return shape === 'starts' ? `${escaped}%` : shape === 'ends' ? `%${escaped}` : `%${escaped}%`;\n}\n\n/** `A`..`Z` and the `a`..`z` they fold onto — the #4706 Q1 = A domain, as data. */\nconst ASCII_UPPER_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';\nconst ASCII_LOWER_LETTERS = 'abcdefghijklmnopqrstuvwxyz';\n\n/**\n * [#6520] Wrap a SQL expression in `$icontains`' ASCII-ONLY case fold.\n *\n * Character for character `driver-sql`'s postgres arm\n * (`textMatchPredicate` → `translate(expr, 'ABC…', 'abc…')`), and shared by this\n * package's three compilers for the same reason `likePattern` is: the escaping\n * and the fold interact, and a second copy of either is how two compilers of one\n * filter tree start describing different queries (#5333).\n *\n * ## Why `translate()` and not `LOWER()`\n *\n * Postgres' `LOWER()` is locale-aware — it folds `É` to `é`, and the contract\n * says it must not (#4706 Q1 = A, because SQLite folds ASCII only and three of\n * the five drivers are SQLite underneath). `translate()` with an explicit\n * 26-character domain folds exactly `A-Z` and leaves every other code point\n * alone, so it is the fold the ruling names rather than the one the database\n * happens to offer.\n *\n * ## The dialect this assumes, stated so it can go red rather than stale\n *\n * Postgres, like everything else these three compilers emit — the claim this\n * file's header already makes and `__tests__/like-metacharacter-escape.test.ts`\n * pins. `translate()` is Postgres/Oracle; SQLite has no such function. So the\n * warning in the header applies to this helper WORD FOR WORD: if these compilers\n * ever emit for SQLite or MySQL, this expression does not merely over-match, it\n * fails to parse. The remedy is the per-dialect construct table `driver-sql`'s\n * `textMatchPredicate` already carries — nested `REPLACE` for the dialects\n * without `translate` — not a quiet fallback to `LOWER()`, which would silently\n * restore the Unicode fold this function exists to avoid.\n *\n * The caller must apply it to BOTH sides of the comparison. Folding only the\n * comparand compares a folded needle against a raw column and matches just the\n * rows that were already lower-case.\n */\nexport function asciiLowerSqlExpr(expr: string): string {\n return `translate(${expr}, '${ASCII_UPPER_LETTERS}', '${ASCII_LOWER_LETTERS}')`;\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { FilterCondition } from '@objectstack/spec/data';\nimport type { RegisteredErrorCode } from '@objectstack/spec/api';\nimport { likePattern, LIKE_ESCAPE_CHAR, asciiLowerSqlExpr } from './like-pattern.js';\nimport {\n CROSS_FIELD_COMPARISON_OPERATORS,\n fieldReferenceBetweenBoundMessage,\n fieldReferenceComparandMessage,\n isBindableComparand,\n isFieldReference,\n isRenderableTextComparand,\n unbindableListMemberMessage,\n unrenderableTextComparandMessage,\n} from './comparand-shape.js';\n\n/**\n * Compile an RLS / tenant read-scope `FilterCondition` into a parameterized,\n * alias-qualified SQL predicate (ADR-0021 D-C).\n *\n * This is the single, security-critical translation point between the\n * canonical Mongo-style filter the `RLSCompiler` emits and the raw SQL the\n * analytics `NativeSQLStrategy` runs. It is deliberately:\n *\n * - **Fail-closed.** Any operator, value shape, or identifier it cannot\n * translate THROWS. A read-scope predicate must never be silently dropped —\n * dropping it would run the query unscoped and leak cross-tenant data.\n * - **Injection-safe.** Field/alias identifiers are validated against a strict\n * snake_case pattern and every value is bound as a `?` placeholder (the\n * strategy renumbers `?` → `$N`). No value is ever interpolated into SQL.\n * - **Alias-qualified.** Bare fields become `\"alias\".\"field\"` so the same\n * predicate applies to the base table or any joined table.\n *\n * Supports the operators the RLS layer and common policies emit: implicit\n * equality, `$eq/$ne/$gt/$gte/$lt/$lte/$in/$nin/$between/$contains/$notContains/\n * $startsWith/$endsWith/$null/$exists`, and `$and/$or/$not` combinators.\n *\n * ## `''` means TRUE, and that is a value — not \"nothing happened\"\n *\n * `compileNode` returns `''` for a node that constrains nothing (`{}`, an\n * all-TRUE `$and`). That empty string is the boolean constant TRUE, and the two\n * places where a compiler forgets it are exactly where this one used to be\n * wrong (#5297):\n *\n * - TRUE is the AND identity, so dropping it from a `$and` is correct — but it\n * ABSORBS a `$or`, so one TRUE disjunct makes the whole `$or` TRUE. Filtering\n * it out of `$or` (`{$or: [{}, {a: 1}]}` → `a = 1`) silently NARROWED.\n * - `NOT TRUE ≡ FALSE`, so `{$not: {}}` is the zero-row predicate. Emitting\n * nothing for it made `compileScopedFilterToSql` return `''`, and\n * `applyReadScope`'s `if (!sql) return;` then added no `WHERE` at all — a\n * read scope that should have shown zero rows exposed the whole table. In an\n * RLS lowering that is a permission bypass, not a rounding error.\n *\n * So a group is now compiled into its own bind buffer and only committed when it\n * survives, and FALSE has a spelling ({@link FALSE_CLAUSE}) instead of being\n * representable only as silence.\n *\n * ## Empty combinators are boolean identities (#5322)\n *\n * `{$and: []}` is TRUE (every row), `{$or: []}` is FALSE (zero rows), and\n * `{$not: {}}` is `NOT TRUE` — FALSE. This compiler used to refuse the empty\n * arrays fail-closed while the five `FILTER_LOGIC_CASES` backends reduced\n * them; the 2026-08-04 #5322 ruling aligned this file and the analytics\n * `filter-normalizer` with the reduction (see the note at the `length === 0`\n * branch in {@link compileNode} for why). Reduction happens structurally over\n * the whole tree, and it composes with the #5146 NULL-safe `$not` rewrite as\n * \"reduce first\": {@link nullSafeNegationOperand} maps combinator arrays\n * element-wise (an empty array stays empty, a `{}` leaf has no field to\n * guard), so the identity a constant reduces to is untouched by the rewrite\n * and the rewrite only ever guards leaves that survive it.\n *\n * ## `$not` is NULL-safe (#5146)\n *\n * SQL is three-valued and a `WHERE` keeps only TRUE, so a bare `NOT (col = ?)`\n * drops every row whose `col` is NULL — while `driver-memory` and `formula`\n * (and, since #5296, `driver-sql`) return those rows. One read scope, two\n * visible sets, chosen by which backend answered. #5146 ruled the JS answer\n * canonical; {@link nullSafeNegationOperand} here is the same rewrite\n * `sql-driver.ts` applies, so an analytics query and an ordinary `find()` scope\n * the same rows.\n *\n * ## The LIKE family compares LITERALS (#5567)\n *\n * `_` is LIKE's single-character wildcard and `%` its multi-character one, so a\n * comparand concatenated straight into a pattern position stops meaning what the\n * author wrote: `{owner_name: {$contains: '_admin'}}` also admitted `xyadmin`,\n * and `{$contains: '50%'}` also admitted `off 5012 now`. Every LIKE arm below\n * therefore binds an ESCAPED pattern plus its escape character — see\n * `like-pattern.ts` for the transform, for why the escape character is a bound\n * value rather than a SQL literal, and for its correspondence with `driver-sql`'s\n * `applyLike`.\n *\n * On THIS compiler that widening was the #5347 / #5324 shape again: a read scope\n * admitting rows the policy did not is over-reach, not a degraded filter. Note\n * the file was fail-closed everywhere else — the LIKE family was the one place an\n * author's literal was silently reinterpreted rather than refused.\n *\n * ## Every refusal here is a SERVER fault, and says so (#5367, maintainer ruling 2026-08-06)\n *\n * The ten fail-closed refusals below were bare `throw new Error(…)`, and\n * `/analytics/dataset/query` classified them by matching `read-scope-sql` in the\n * message text — the last surviving entry of the hardcoded substring list #5352\n * introduced. It answered `400 DATASET_INVALID`, which was wrong twice:\n *\n * - **Wrong attribution.** Neither input is the caller's. `filter` is the RLS\n * `FilterCondition` the security service compiles from an ADMIN-authored\n * sharing rule / permission set; `alias` is a join alias the DATASET COMPILER\n * generated. The caller's own predicate travels a different road entirely\n * (`filter-normalizer.ts`, `INVALID_FILTER` / 400 since #5352). So the two\n * things that can land here are an administrator's broken policy and drift\n * between two of OUR components (#5557's `$regex` was exactly the second) —\n * and for the caller of this request both are a server fault. `400` told them\n * to fix a request that was never the problem, and hid the fault from the 5xx\n * alerting that should have seen it.\n * - **Wrong disclosure.** A 400 echoed the message verbatim, so\n * `unsafe field identifier \"…\"` / `unsupported operator \"$x\" on \"owner\"`\n * handed the caller the FIELD NAMES AND COMPARANDS OF THE RLS POLICY — the\n * one document a tenant must not be able to read out of an error body.\n *\n * {@link readScopeCompileError} is now the only way this module refuses:\n * `READ_SCOPE_COMPILE_FAILED` / **500**. The status is what makes the retirement\n * safe — `rest-server.ts`'s envelope branch is 4xx-only, so a declared 5xx falls\n * through to the `ANALYTICS_QUERY_FAILED` envelope BY DECLARATION rather than by\n * nothing having been declared, and that route withholds the message of any\n * producer that declares a server fault (the full text goes to `logError`).\n *\n * ⚠️ The withhold is NOT inherited from `looksLikeInternalErrorLeak`. That\n * predicate is a heuristic over SQL/driver PHRASING, and measured, every message\n * below returns FALSE from it — so retiring the route's message list on its own\n * would have moved the policy content from a 400 body into a 500 body instead of\n * out of the response. Teaching the heuristic to recognise `[read-scope-sql]`\n * would have been more message sniffing, which is the mechanism #5367 exists to\n * remove; the route keys on the DECLARATION instead.\n *\n * The code is what a machine reads: `dispatcher-plugin.errorResponseBase`, the\n * sibling `/analytics/query` exit, puts a thrown `err.code` on the wire at\n * `error.code` (#3842), so `READ_SCOPE_COMPILE_FAILED` is legible there without\n * anyone parsing prose.\n *\n * ⚠️ At `error.code` — NOT `error.details.code`, which is where this note\n * pointed until #6123 corrected it. `errorResponseBase` only STAGES the code in\n * a `details` object; `buildApiError` then runs `splitSemanticCode`\n * (`@objectstack/runtime`, `src/error-envelope.ts:117`), which PROMOTES it into\n * the declared `ApiErrorSchema` field and returns the now-empty `details` as\n * `undefined` — so the key is omitted from the body and `error.details.code` is\n * never present to read. The measured 500 body is exactly:\n *\n * ```json\n * {\"success\":false,\"error\":{\"code\":\"READ_SCOPE_COMPILE_FAILED\",\n * \"message\":\"Internal server error\",\"httpStatus\":500}}\n * ```\n *\n * Pinned end-to-end in `@objectstack/runtime`'s\n * `analytics-query-read-scope-withhold.test.ts`, which asserts the code at\n * `error.code` against a real `AnalyticsService` on a real mounted route.\n *\n * ⚠️ Deliberately NOT a 4xx of any flavour, including a 422. Option A on the\n * decision card was `READ_SCOPE_INVALID` / 422 (\"not your fault, not a crash\");\n * it was rejected because no consumer reads a code on this path (so a new\n * vocabulary had no measured pull), because a 4xx cannot be fixed by the client\n * and therefore misreports the condition, and because 422 would have left the\n * disclosure question to be re-decided message by message.\n *\n * ## An `undefined` comparand is refused, not bound (#6125, PM ruling 2026-08-07)\n *\n * That makes ELEVEN refusing sites; the envelope above is what all eleven carry,\n * and {@link undefinedComparandError} is the eleventh. #6050 ruled on 2026-08-07\n * that `undefined` in a comparand position is refused everywhere (ruling B), and\n * implemented it on `driver-sql` / `driver-turso` — the surfaces where the shape\n * was PROVEN reachable. This module was measured in the same round and answered\n * a fourth way again: legal SQL, one bound NULL, and not a single log line.\n *\n * The #6125 ruling scoped the push-down to THIS file and kept its own envelope\n * (`READ_SCOPE_COMPILE_FAILED` / 500 — see above): a read scope is compiled by\n * the platform from CEL and stored metadata, so telling the caller to fix their\n * request would name the wrong author. `@objectstack/formula` reads the same\n * value as a THIRD semantics (\"the key is absent from the record\") and is\n * deliberately left alone — deciding it here would settle #5299's\n * key-missing-vs-value-null question as a side effect — and `driver-memory` /\n * `driver-mongodb` stay pin-only under the #5499 freeze.\n *\n * The eleventh message was measured against `looksLikeInternalErrorLeak` before\n * being added, because the section above turns on that predicate answering FALSE\n * for this family: it does for all four positions, so the new refusal is\n * withheld from the response BY DECLARATION exactly like the other ten, and no\n * message-sniffing list learns a twelfth phrase.\n *\n * ## A non-boolean `$null` / `$exists` is refused too (#6387, applying #5347 / #5369)\n *\n * TWELVE refusing sites, and {@link nonBooleanFlagComparandError} is the\n * twelfth's — ONE message for BOTH operators, because both failed the same way\n * (see that function for the measured table and the #5240 argument). #5347 and\n * #5369 refused a non-boolean comparand for these two operators on `driver-sql`;\n * #6387 measured that neither ruling had been pushed down here, where the\n * emitter still read them by plain TRUTHINESS. The consequence was sharper than\n * on the driver face: `{ $exists: \"false\" }`, written to scope to rows with NO\n * owner, compiled to `IS NOT NULL` — the rows that HAVE one. In a module whose\n * contract is fail-closed, that is a WIDENING, which is why this cell was graded\n * above #6125's silent-zero-rows one even though its reachability is narrower\n * (measured: not reachable from stored metadata — the CEL lowering emits `$null`\n * only with hard-coded booleans and `$exists` never; reachable only from an\n * in-process `getReadScope` producer).\n *\n * The twelfth site is one gate over TWO triggers, exactly like `quoteIdent`'s\n * alias-vs-field split, and the refusal-envelope inventory lists it as two rows\n * over one site for that reason. The message was measured against\n * `looksLikeInternalErrorLeak` too — FALSE, like the other eleven — so it is\n * withheld from the response BY DECLARATION and teaches no sniffing list a new\n * phrase.\n *\n * ## A `{ $field }` comparand is refused, not bound (#7598)\n *\n * THIRTEEN refusing sites, and {@link assertNoFieldReferenceComparand} is the\n * thirteenth's — the first whose shape is not wrong everywhere, only\n * uncompilable HERE. #5222 taught `driver-sql` / `driver-sqlite-wasm` to compile\n * `{ amount: { $gt: { $field: 'budget' } } }` into a same-table column-to-column\n * comparison, under four maintainer rulings (same-table only, declared-only\n * enumeration, tenant-isolation column forbidden on both sides, same comparison\n * class). This compiler was measured in the same family and answered a fifth way\n * again: it BOUND the reference object, so an admin's RLS predicate compared a\n * column against a value no row can hold — see that function for the measured\n * table and for why the four rulings cannot be enforced from here at all\n * (`StrategyContext` exposes neither an object's declared field set nor its\n * tenant-isolation column, so the enumeration the rulings turn on does not\n * exist on this side).\n *\n * ⚠️ [UPDATED — maintainer ruling 2026-08-12, #7598 Q1 = B] When this section\n * was written the refusal was the whole answer, and it said so: \"it does not\n * make the capability available\". **It does now, by getting out of the way.**\n * `NativeSQLStrategy.canHandle` DECLINES a query whose read scope carries a\n * reference, so the query falls through to the ObjectQL/engine path — where\n * `ObjectQLStrategy` ANDs the scope into the `FilterCondition` it hands\n * `engine.aggregate`, the reference reaches `driver-sql` intact, and the driver\n * compiles it under all four #5222 rulings using its own `initObjects`\n * metadata. A field-to-field RLS rule is therefore SERVED on the analytics\n * face, and the security rules live in exactly one place rather than two.\n *\n * What that leaves for the gate below is a narrower but still live job:\n * `applyReadScope` (`native-sql-strategy.ts`) no longer reaches it — the\n * decline runs first — but `ObjectQLStrategy.generateSql` does. That is the\n * `/analytics/sql` ECHO, a display string for an execution it does not perform,\n * and it has no faithful rendering of the total column-to-column predicate the\n * engine path runs. The ruling answered that face explicitly —\n * 「一致的响亮答案,不半渲染」 (one consistent, loud answer; no half-rendering) —\n * so the refusal here IS the echo's decline. `compileScopedFilterToSql` is also\n * a public export of this package (`index.ts`), so the gate additionally holds\n * for any consumer outside these two.\n *\n * ⛔ The ENVELOPE is untouched, and deliberately: Q2 = A kept the #5367 ruling\n * verbatim — `READ_SCOPE_COMPILE_FAILED` / 500 with the message withheld. No new\n * ADR-0112 code for the unsupported-rule class (option C was declined for zero\n * measured pull; #5367 recorded that no consumer reads a code on this path), and\n * no 4xx (option B reintroduces both defects #5367 closed). The paragraph above\n * beginning \"⚠️ Deliberately NOT a 4xx of any flavour\" is that ruling's own text\n * and is not to be rewritten.\n */\n\nconst IDENT = /^[a-z_][a-z0-9_]*$/i;\n\n/**\n * `READ_SCOPE_COMPILE_FAILED`, pinned against the ADR-0112 D3 ledger.\n *\n * Typed as `RegisteredErrorCode` so dropping the ledger row (or misspelling the\n * code here) fails `tsc` instead of shipping a code `ApiErrorSchema` rejects.\n */\nconst READ_SCOPE_COMPILE_FAILED: RegisteredErrorCode = 'READ_SCOPE_COMPILE_FAILED';\n\n/**\n * [#5367] A read-scope lowering failure in the ADR-0112 envelope —\n * `READ_SCOPE_COMPILE_FAILED` / 500.\n *\n * ⛔ **The only way this module refuses.** Module-local for the same reason\n * `filter-normalizer.ts`'s `invalidFilterError` is: every refusing site lives in\n * this one file, so a shared module would buy nothing and a second spelling\n * would cost the invariant. A bare `throw new Error` added below is the defect\n * returning — and a half-enveloped module is indistinguishable from an\n * unenveloped one at the HTTP boundary (the lesson #5352 paid for when seven of\n * `filter-normalizer.ts`'s nine sites stayed bare).\n *\n * The message stays whatever the refusing site says: it is for the operator's\n * log, which after #5367 is its only destination.\n */\nfunction readScopeCompileError(message: string): Error {\n const err = new Error(message) as Error & { code?: string; status?: number };\n err.code = READ_SCOPE_COMPILE_FAILED;\n err.status = 500;\n return err;\n}\n\n/**\n * The FALSE constant. `''` is this compiler's TRUE, so FALSE needs a spelling of\n * its own — `1 = 0` is already what an empty `$in` lowers to, and what\n * `driver-sql` emits for the same identity (#5243).\n */\nconst FALSE_CLAUSE = '1 = 0';\n\n/** A node the compiler can walk: a plain object, not `null` and not an array. */\nfunction isFilterNode(v: unknown): v is Record<string, unknown> {\n return v !== null && typeof v === 'object' && !Array.isArray(v);\n}\n\nfunction quoteIdent(name: string, kind: string): string {\n if (typeof name !== 'string' || !IDENT.test(name)) {\n throw readScopeCompileError(`[read-scope-sql] unsafe ${kind} identifier \"${String(name)}\" — refusing to build read scope (fail-closed).`);\n }\n return `\"${name}\"`;\n}\n\nexport function compileScopedFilterToSql(\n filter: FilterCondition,\n alias: string,\n): { sql: string; params: unknown[] } {\n const quotedAlias = quoteIdent(alias, 'alias');\n const params: unknown[] = [];\n const sql = compileNode(filter, quotedAlias, params);\n return { sql, params };\n}\n\n/**\n * Compile a child node into its OWN bind buffer.\n *\n * A group can turn out to be a boolean identity only after its children have\n * been compiled — and compiling them appends to `params`. Binding straight into\n * the parent's array and then discarding the clause would leave those values\n * behind with no `?` to consume them, shifting every later placeholder onto the\n * wrong value: a read scope that binds the wrong tenant id is worse than one\n * that is merely too wide. Buffer per child, commit only what survives.\n */\nfunction compileSub(node: unknown, qAlias: string): { sql: string; params: unknown[] } {\n const params: unknown[] = [];\n const sql = compileNode(node, qAlias, params);\n return { sql, params };\n}\n\n/** Compile a filter node into a boolean SQL expression ('' = TRUE, no constraint). */\nfunction compileNode(node: unknown, qAlias: string, params: unknown[]): string {\n if (!isFilterNode(node)) {\n throw readScopeCompileError('[read-scope-sql] read scope must be a filter object (fail-closed).');\n }\n const clauses: string[] = [];\n for (const [key, value] of Object.entries(node)) {\n if (key === '$and' || key === '$or') {\n if (!Array.isArray(value)) {\n throw readScopeCompileError(`[read-scope-sql] \"${key}\" requires an array (fail-closed).`);\n }\n if (value.length === 0) {\n // Boolean identity (#5322 ruling, 2026-08-04): the empty `$and` is the\n // AND identity — TRUE, no constraint — and the empty `$or` is the OR\n // identity — FALSE, zero rows. Until that ruling this compiler REFUSED\n // both (\"requires a non-empty array (fail-closed)\"), while the five\n // FILTER_LOGIC_CASES backends reduced them; #5322 took the reduction:\n // it is the only reading that lets a nested tree be evaluated at all\n // (a rejection cannot answer what `$and: []` means as the third branch\n // of a `$or`), and `{$or: []}` = zero rows is itself fail-closed for an\n // RLS scope — a disjunct list that loops to zero items hides every row\n // instead of exposing the table (#5134). Authoring-time loud rejection\n // of the literal spelling is tracked separately (#5330).\n if (key === '$or') clauses.push(FALSE_CLAUSE);\n continue;\n }\n const compiled = (value as unknown[]).map((child) => compileSub(child, qAlias));\n // A `''` branch is the constant TRUE. It ABSORBS a disjunction — one TRUE\n // disjunct makes the whole `$or` TRUE — so the group contributes nothing\n // rather than collapsing to its remaining branches, which would have\n // narrowed `{$or: [{}, {a: 1}]}` to `a = 1` (#5297).\n if (key === '$or' && compiled.some((c) => c.sql.length === 0)) continue;\n // For `$and` the same constant is the identity, so it just drops out.\n const kept = compiled.filter((c) => c.sql.length > 0);\n if (kept.length === 0) continue;\n for (const part of kept) params.push(...part.params);\n const joiner = key === '$and' ? ' AND ' : ' OR ';\n clauses.push(`(${kept.map((c) => c.sql).join(joiner)})`);\n } else if (key === '$not') {\n // NULL-safe negation (#5146): totalise the operand's leaves first, so\n // `NOT (…)` can never be UNKNOWN and this compiler admits the same rows\n // `driver-sql` / `driver-memory` / `formula` admit. A non-node operand is\n // left alone so `compileNode` still rejects it with its own message.\n const operand = isFilterNode(value) ? nullSafeNegationOperand(value) : value;\n const inner = compileSub(operand, qAlias);\n if (inner.sql.length === 0) {\n // `NOT TRUE ≡ FALSE`. Emitting nothing here is what let a `{$not: {}}`\n // read scope through `applyReadScope`'s `if (!sql) return;` and ran the\n // analytics query completely unscoped (#5297).\n clauses.push(FALSE_CLAUSE);\n } else {\n params.push(...inner.params);\n clauses.push(`NOT (${inner.sql})`);\n }\n } else if (key.startsWith('$')) {\n throw readScopeCompileError(`[read-scope-sql] unsupported top-level operator \"${key}\" (fail-closed).`);\n } else {\n clauses.push(compileField(key, value, qAlias, params));\n }\n }\n return clauses.join(' AND ');\n}\n\n/** Compile a single `field: value | { $op: ... }` entry. */\nfunction compileField(field: string, value: unknown, qAlias: string, params: unknown[]): string {\n const col = `${qAlias}.${quoteIdent(field, 'field')}`;\n\n // [#6125] `undefined` in a comparand position, refused before anything binds —\n // and after `quoteIdent`, so an unsafe identifier (the injection vector) keeps\n // its own message and its precedence. See {@link assertDefinedComparands} for\n // why this one call site covers the whole tree.\n assertDefinedComparands(field, value);\n\n // [#6387] …and the two comparands that are NOT positions but DOMAINS: `$null`\n // and `$exists` take a declared boolean. Deliberately a second call rather\n // than a widened first one — the two gates gate different things, and their\n // domains are disjoint by construction (`assertDefinedComparands` skips these\n // two operators by name), so neither can shadow the other's message.\n assertBooleanFlagComparands(field, value);\n\n // [#7598] …and the comparand that is neither a position nor a domain problem\n // but a CAPABILITY one: a `{ $field }` reference the SQL drivers compile since\n // #5222 and this compiler cannot. Third call rather than a widened first, for\n // the reason the second one is separate — the three gates gate different\n // things, and their triggers are disjoint by construction (a reference is\n // never `undefined`, and `$null` / `$exists` are outside this gate's operator\n // set), so none can shadow another's message. Runs AFTER both, so a\n // `{ $gt: undefined }` keeps being an undefined comparand rather than becoming\n // \"not a field reference\".\n assertNoFieldReferenceComparand(field, value);\n\n // Scalar / null → implicit equality.\n if (value === null) return `${col} IS NULL`;\n if (typeof value !== 'object' || value instanceof Date) {\n params.push(value);\n return `${col} = ?`;\n }\n if (Array.isArray(value)) {\n throw readScopeCompileError(`[read-scope-sql] bare array value for \"${field}\" — use { $in: [...] } (fail-closed).`);\n }\n\n const ops = value as Record<string, unknown>;\n const keys = Object.keys(ops);\n // A value object must be ALL operators; a non-$ key means a nested relation,\n // which a flat read scope cannot join — fail closed.\n if (keys.length === 0 || keys.some((k) => !k.startsWith('$'))) {\n throw readScopeCompileError(`[read-scope-sql] \"${field}\" has a nested/relation value which is not supported in a read scope (fail-closed).`);\n }\n\n const parts: string[] = [];\n for (const op of keys) {\n parts.push(compileOperator(col, op, ops[op], field, params));\n }\n return parts.length === 1 ? parts[0] : `(${parts.join(' AND ')})`;\n}\n\nfunction bind(params: unknown[], v: unknown): string {\n params.push(v);\n return '?';\n}\n\n/**\n * [#5567] Bind a LIKE pattern together with its escape character: `? ESCAPE ?`.\n *\n * Both are ordinary bound values, so this whole concern stays inside the\n * predicate: `applyReadScope` (`native-sql-strategy.ts`) and `generateSql`\n * (`objectql-strategy.ts`) rewrite `?` → `$N` while pushing the matching value\n * from `params`, and they carry the escape character for free — neither consumer\n * needed a change. A SQL literal `ESCAPE '\\'` would have pushed the problem up a\n * layer AND been unportable: MySQL strips one backslash inside a string literal,\n * so the literal spelling differs per dialect while a bound value does not.\n *\n * The clause is not optional decoration. SQLite honours no default escape\n * character, so the escaped pattern alone would search for a literal backslash\n * there and match nothing — the two halves are one fix (see `like-pattern.ts`).\n */\nfunction bindLike(params: unknown[], pattern: string): string {\n // Left-to-right evaluation of the template puts the pattern in `params` before\n // the escape character, which is the order the `?` appear.\n return `${bind(params, pattern)} ESCAPE ${bind(params, LIKE_ESCAPE_CHAR)}`;\n}\n\n/**\n * [#5298] Wrap a negative-polarity value test so a row whose column has no value\n * SATISFIES it: `(col IS NULL OR <test>)`.\n *\n * The read-scope twin of `driver-sql`'s `applyNullSafeNegative`, and the reason\n * this compiler had to move in the same PR rather than a later one: an RLS rule\n * is authored once and evaluated on BOTH sides — this file lowers it for the\n * read path while `formula`'s `matchesFilterCondition` evaluates it for the\n * write-side `check`. Leaving the two on different answers for `$ne` is one\n * permission rule admitting two different row sets, which is the security\n * defect #5146 named for `$not` and #5298 ruled for the rest.\n *\n * OR-expansion rather than `IS DISTINCT FROM` / `IS NOT` / `<=>`, for the three\n * reasons recorded on the driver-side twin: `NOT LIKE` has no such form, the\n * SQLite spelling depends on an engine version nothing here pins, and the\n * measured query plans are identical either way.\n *\n * The parentheses are not optional. {@link compileField} joins a field's\n * operators with bare ` AND `, so an unwrapped `col IS NULL OR …` would bind\n * looser than that AND and silently widen the whole scope.\n */\nfunction nullSafeNegative(col: string, test: string): string {\n return `(${col} IS NULL OR ${test})`;\n}\n\n/**\n * [#5234] The comparand-SHAPE gate for this door.\n *\n * `compileScopedFilterToSql` takes a `FilterCondition` that never passes through\n * `filter-normalizer`'s `fieldLeaves`, so this module needs the two checks in\n * its own right — same rule, stated once in `comparand-shape.ts`, wrapped in\n * THIS module's envelope. The envelope difference is the point: a read scope is\n * compiled from a policy, not authored by the caller, so an uncompilable\n * comparand here is a 500 fail-closed refusal (see the header) rather than a 400.\n *\n * The direction matters more here than anywhere else this rule lands. A\n * read-scope `{$nin: [{…}]}` compiled to `NOT IN ('[object Object]')`, which\n * excludes NOTHING — the scope's exclusion silently did not happen, which is\n * over-reach on a tenant/RLS predicate rather than a loose filter. That is the\n * same reading #5347 / #5324 made on this very file, and the reason the #5234\n * issue's \"fail-closed, so lower risk\" framing does not survive contact with the\n * `$nin` / `$notContains` half.\n */\nfunction assertCompilableMembers(op: string, field: string, members: unknown[]): void {\n members.forEach((member, index) => {\n if (!isBindableComparand(member)) {\n throw readScopeCompileError(`[read-scope-sql] ${unbindableListMemberMessage(op, field, member, index)}`);\n }\n });\n}\n\n/** [#5234] See {@link assertCompilableMembers}; this is the LIKE-family half. */\nfunction assertRenderableText(op: string, field: string, val: unknown): void {\n if (isRenderableTextComparand(val)) return;\n throw readScopeCompileError(`[read-scope-sql] ${unrenderableTextComparandMessage(op, field, val)}`);\n}\n\n/**\n * [#6125, PM ruling 2026-08-07] `undefined` in a COMPARAND position.\n *\n * ONE wording for all four positions #6125 measured (#5240 — one condition, one\n * wording); only `path` varies, because only the position does. What the four\n * had in common is why a shared sentence is right rather than merely shorter:\n * every one of them compiled to legal SQL with the JS value `undefined` in the\n * bind list, which the external driver renders as NULL — and every comparison\n * against NULL is UNKNOWN, so the scope matched ZERO rows and said nothing.\n *\n * Re-measured on `origin/main` (`d8e8d9cbc`) with the refusal disabled, alias\n * `t`, field `d` — the same four rows #6125's table recorded on `cba7454df`:\n *\n * | read scope | compiled to | bind list |\n * |---|---|---|\n * | `{ d: undefined }` | `\"t\".\"d\" = ?` | `[undefined]` |\n * | `{ d: { $gt: undefined } }` | `\"t\".\"d\" > ?` | `[undefined]` |\n * | `{ d: { $in: [undefined] } }` | `\"t\".\"d\" IN (?)` | `[undefined]` |\n * | `{ $not: { d: undefined } }` | `NOT ((\"t\".\"d\" IS NOT NULL AND \"t\".\"d\" = ?))` | `[undefined]` |\n *\n * ⚠️ `[undefined]`, not `[null]` — one correction to the issue's table. Nothing\n * in this package coerces it: `applyReadScope` (`native-sql-strategy.ts`) pushes\n * `scopeParams[i]` into the driver's bind array verbatim while it renumbers\n * `?` → `$N`. So the NULL is the DRIVER's reading of a JS `undefined`, which is\n * also why the same cell reads as a bare `Undefined binding(s)` crash on the\n * drivers that refuse to guess (#6050's LOCAL column). Two failure modes from\n * one bind, decided by which driver the datasource happens to be — the reason\n * this is refused at the compiler and not repaired at any one consumer.\n *\n * ⛔ What deliberately does NOT move: `null`. `{ d: null }`, `{ $eq: null }`,\n * `{ $ne: null }`, `$null` and `$exists` keep their exact lowering — `null` IS a\n * declared comparand and IS the null predicate, and the whole point of this\n * refusal is the JS value that cannot be told apart from an ABSENT key. ⚠️ Read\n * `$null` / `$exists` there as \"with their declared BOOLEAN comparand\": #6387\n * later refused every other comparand for those two, `{ $null: null }` included,\n * on the separate domain grounds {@link assertBooleanFlagComparands} states. The\n * `null` this paragraph promises not to move is `null` in a COMPARAND position,\n * which is untouched by both changes and still pinned row for row. Pinned\n * as its own control group in `read-scope-undefined-comparand.test.ts`, because\n * refusing `null` along with `undefined` is the way this change could do harm.\n *\n * ## Why the direction here is not #6050's direction\n *\n * On `driver-sql` the same shape was over-reach: `{ owner_id: ctx.user?.id }`\n * with a missing id compiled to `IS NULL` on Turso's remote transport and\n * matched every env-wide row. Here it is fail-CLOSED — zero rows, never extra\n * rows — so this is not a latent permission bypass and was not graded as one.\n * It is refused anyway because a read scope that answers a question nobody asked,\n * with no log line, is indistinguishable from one that worked: the value of this\n * change is turning silence into noise, which is exactly the grading #6125's\n * ruling recorded.\n */\nfunction undefinedComparandError(field: string, path: string): Error {\n return readScopeCompileError(\n `[read-scope-sql] comparand at ${path} is undefined — refusing to build read scope (fail-closed). ` +\n `@objectstack/spec FieldOperatorsSchema declares no undefined comparand, and in JavaScript a key ` +\n `whose value is undefined cannot be told apart from an ABSENT key — yet the two mean OPPOSITE ` +\n `things (a predicate versus no constraint at all), so there is no reading of it that is not a ` +\n `guess. It used to compile: undefined went into the bind list, the driver read it as SQL NULL, ` +\n `every comparison against NULL is UNKNOWN, and the scope matched ZERO rows in silence. Write null if the null ` +\n `predicate was meant ({ \"${field}\": null } or { \"${field}\": { \"$null\": true } }), or omit the key ` +\n `when the value is genuinely absent. The producer to fix is whoever BUILT this read scope — an ` +\n `admin-authored sharing rule / permission set, its CEL lowering, or the in-process code that ` +\n `assembled the FilterCondition — never the caller of this query, who cannot author it (#6050 ` +\n `ruling B, pushed down to this compiler by #6125).`,\n );\n}\n\n/**\n * [#6125] Refuse every `undefined` sitting in a comparand position of ONE field\n * constraint.\n *\n * The positions are enumerated rather than swept, because \"comparand\" is a\n * POSITION and not a type:\n *\n * - the DIRECT comparand — `{ d: undefined }`, the implicit `=`;\n * - an OPERATOR's comparand — `{ d: { $gt: undefined } }`, `$eq`, `$ne`, the\n * LIKE family, every other single-value operator;\n * - a MEMBER of a list operator's array — `{ d: { $in: [undefined] } }`,\n * `$nin`, `$between`. The array itself IS `$in`'s legitimate comparand;\n * each element is a comparand in its own right, which is the same split\n * {@link assertCompilableMembers} already makes.\n *\n * Three positions are deliberately NOT swept, each because this module already\n * refuses the enclosing shape with a TRUER diagnosis — #5240's rule read in the\n * direction that matters here, since a second wording for a shape that is\n * refused either way only sends the operator to the wrong repair:\n *\n * - `$null` / `$exists`. Their comparand is a declared BOOLEAN — a flag, not a\n * value to compare against — so `undefined` there is not a comparand at all.\n * `driver-sql`'s twin skips them for the same reason. ✅ [#6387] And it now\n * skips them the way that twin does: to a boolean-DOMAIN gate,\n * {@link assertBooleanFlagComparands}, which #6387 pushed down from #5347 /\n * #5369. When this note was written that gate did not exist here, so\n * `{ $null: undefined }` lowered by truthiness to `IS NOT NULL` and\n * `{ $null: \"false\" }` — the STRING, which is truthy — landed on the side\n * opposite the `false` it was written to mean. Both are refused today, and\n * `undefined` is refused there rather than here on purpose: outside the\n * declared domain is a truer diagnosis for a flag than \"this comparand\n * position is undefined\".\n * - a bare ARRAY in direct comparand position (`{ d: [1, undefined] }`).\n * {@link compileField} refuses the array as a whole (\"use `{ $in: [...] }`\"),\n * and inspecting its members here would relabel a shape refused either way.\n * - a NON-`$` key inside the constraint object (`{ owner: { manager_id:\n * undefined } }`). That is a nested relation, which {@link compileField}\n * refuses outright; answering \"the comparand is undefined\" would send the\n * operator to write `null` there, and `{ owner: { manager_id: null } }` does\n * not compile either. This is the one deliberate divergence from\n * `driver-sql`'s twin, and it comes from THIS module refusing nested\n * relations — not from a different reading of #6050.\n *\n * ## Why the call site is {@link compileField} and not a pre-pass\n *\n * `driver-sql` refuses on its separate validating walk because its emitter\n * short-circuits: a boolean identity can resolve an enclosing node before a\n * malformed sibling is ever visited, so a gate in the emitter would be\n * conditional on evaluation order. THIS compiler has no such blind spot —\n * {@link compileNode} `.map()`s every `$and`/`$or` child into its own buffer\n * BEFORE any identity is applied (the `$or` TRUE-absorption and the `$and`\n * identity filter both read the fully-compiled list), and\n * {@link nullSafeNegationOperand} rewrites a `$not` operand without dropping a\n * single leaf. Every comparand therefore reaches `compileField`, which is also\n * the only path to {@link bind} — one gate, on the one road.\n *\n * The other half of `driver-sql`'s \"runs FIRST\" argument does not transfer\n * either, and that is worth stating rather than copying: there, the refusal had\n * to precede the `$not` rewrite because the polarity tables spelled `=== null`\n * while the `$ne` emitter spelled `== null`, so the two disagreed about\n * `undefined` itself. Here {@link nullValueSatisfiesOperator},\n * {@link operatorIsNullTotal} and every arm of {@link compileOperator} spell it\n * `=== null` alike, so the tables and the emitter agree that `undefined` is \"a\n * value\" — the rewrite for a `{ $not: … }` operand runs, produces a leaf, and\n * that leaf is refused. Nothing inconsistent is being outrun; the silent NULL\n * bind is.\n */\nfunction assertDefinedComparands(field: string, spec: unknown): void {\n const root = `\"${field}\"`;\n if (spec === undefined) throw undefinedComparandError(field, root);\n if (!isFilterNode(spec)) return;\n for (const [op, opValue] of Object.entries(spec)) {\n if (!op.startsWith('$') || op === '$null' || op === '$exists') continue;\n const opPath = `${root}.${op}`;\n if (opValue === undefined) throw undefinedComparandError(field, opPath);\n if (!Array.isArray(opValue)) continue;\n opValue.forEach((member, index) => {\n if (member === undefined) throw undefinedComparandError(field, `${opPath}[${index}]`);\n });\n }\n}\n\n/**\n * [#6387, applying #5347 / #5369] `$null` / `$exists` whose comparand is not a\n * boolean.\n *\n * ## ONE wording for BOTH operators (#5240), and why that is right here\n *\n * `driver-sql` gives its twins two messages, because each names the direction\n * ITS OWN emitter defaulted to and those directions differ. This module had one\n * emitter rule covering both — plain TRUTHINESS — so both operators failed the\n * same way, in the same sentence, and #5240's rule applies in the direction it\n * usually does: one condition, one wording. Only the operator NAME and the\n * `path` vary, and `read-scope-boolean-flag-comparand.test.ts` pins that \"only\n * those vary\" so a later change cannot give one of them a bespoke phrasing.\n *\n * ## What it used to do — measured on `origin/main` (`5faa23ca3`), alias `t`\n *\n * The emitter read `val ? … : …`, so every non-boolean was sorted by JS\n * truthiness into one of the two declared answers:\n *\n * | read scope | compiled to | |\n * |---|---|---|\n * | `{ owner_id: { $null: \"false\" } }` | `\"t\".\"owner_id\" IS NULL` | ⛔ the OPPOSITE of what was written |\n * | `{ owner_id: { $null: \"true\" } }` | `\"t\".\"owner_id\" IS NULL` | |\n * | `{ owner_id: { $null: 0 } }` | `\"t\".\"owner_id\" IS NOT NULL` | |\n * | `{ owner_id: { $null: null } }` | `\"t\".\"owner_id\" IS NOT NULL` | |\n * | `{ owner_id: { $null: undefined } }`| `\"t\".\"owner_id\" IS NOT NULL` | |\n * | `{ owner_id: { $exists: \"false\" } }`| `\"t\".\"owner_id\" IS NOT NULL` | ⛔ the OPPOSITE of what was written |\n * | `{ owner_id: { $exists: 0 } }` | `\"t\".\"owner_id\" IS NULL` | |\n * | `{ owner_id: { $exists: \"no\" } }` | `\"t\".\"owner_id\" IS NOT NULL` | |\n *\n * The string `\"false\"` is TRUTHY, so the two rows marked ⛔ are the ones that\n * matter: a scope written to say \"rows with NO owner\" compiled to \"rows that\n * HAVE one\". Unlike #6125's cell — which was fail-CLOSED, zero rows, merely\n * silent — this direction ADMITS the rows the policy meant to exclude, in a\n * module whose own contract is \"a read-scope predicate must never be silently\n * dropped\". That is why the disposition needed no new judgement: #5347 (`$null`)\n * and #5369 (`$exists`) already refused this shape on `driver-sql`, and their\n * stated reason transfers word for word.\n *\n * ## ⚠️ Reachability, measured — and the half that came back NEGATIVE\n *\n * #6387 asked for a decisive answer to \"can `{ $null: <non-boolean> }` travel\n * from STORED metadata to this compiler\". Measured on `5faa23ca3`, it cannot —\n * three independent gates close that road, and this is recorded because the\n * issue's severity argument rested on it:\n *\n * 1. `RowLevelSecurityPolicySchema` declares `using` / `check` as `z.string()`\n * — a CEL predicate, not a `FilterCondition`. A stored object is rejected\n * at write (\"expected string, received object\").\n * 2. The CEL lowering never emits this shape. `@objectstack/formula`'s\n * `cel-to-filter.ts` emits `$null` at exactly two sites, both with a\n * HARD-CODED boolean (`== null` → `{ $null: true }`, `!= null` →\n * `{ $null: false }`), and emits `$exists` nowhere at all. An unresolved\n * `current_user.*` yields `unresolved-variable` → the policy drops → the\n * deny sentinel, never a stray comparand.\n * 3. Even bypassing the schema, a raw object predicate throws inside\n * `sqlPredicateToCel` (`expression.replace is not a function`), and\n * `getReadFilter`'s catch turns that into `RLS_DENY_FILTER`. A JSON STRING\n * of a FilterCondition stores fine and then fails to parse as CEL → `null`\n * → deny. Both roads end fail-closed.\n *\n * The other read-scope producers cannot emit it either: the Layer 0 tenant\n * filter, `plugin-sharing`'s `buildReadFilter` (`{owner: id}` / `$in` / `$or` /\n * `{id:'__deny_all__'}`), the controlled-by-parent filter (`{fk: {$in: […]}}`)\n * and `RLS_DENY_FILTER` contain no `$null` or `$exists` at all.\n *\n * ⚠️ What IS open, and why this gate is still worth having: `getReadScope` is a\n * DOCUMENTED public option on `AnalyticsPluginOptions` (`plugin.ts`), so a host\n * that supplies its own read scope — from JSON config, or from JS where the\n * `FilterCondition` type is not checked — is a live producer with no gate\n * between it and here. #6387 also confirmed the issue's other measurement:\n * `plugin-security` performs no `FilterConditionSchema` / `safeParse` anywhere\n * on this path. So the shape is not reachable from stored metadata TODAY, and\n * nothing structural stops the next producer; refusing it at the compiler is\n * what makes \"declared boolean\" mean enforced boolean regardless of who writes\n * the scope. Graded on that measurement, not on the issue's opening wording.\n */\nfunction nonBooleanFlagComparandError(op: string, field: string, path: string): Error {\n return readScopeCompileError(\n `[read-scope-sql] comparand for \"${op}\" at ${path} is not a boolean — refusing to build read scope ` +\n `(fail-closed). @objectstack/spec FieldOperatorsSchema declares both $null and $exists as ` +\n `z.boolean(), and this compiler used to read the comparand by TRUTHINESS instead — so a ` +\n `non-boolean was silently sorted into one of the two declared answers rather than refused. The ` +\n `string \"false\" is TRUTHY, which is the case that matters: it landed on the side OPPOSITE the ` +\n `false it was written to mean, turning \"rows with no ${field}\" into \"rows that have one\" — a ` +\n `read scope that ADMITS the rows the policy excludes. Write the boolean itself (true or false), ` +\n `not a string, a number, null or undefined. The producer to fix is whoever BUILT this read ` +\n `scope — an admin-authored sharing rule / permission set, its CEL lowering, or the in-process ` +\n `code (a getReadScope option) that assembled the FilterCondition — never the caller of this ` +\n `query, who cannot author it (#5347 / #5369, pushed down to this compiler by #6387).`,\n );\n}\n\n/**\n * [#6387] Refuse a non-boolean `$null` / `$exists` comparand on ONE field\n * constraint.\n *\n * `hasOwnProperty` rather than `in`, so an inherited key can never trip the\n * gate, and rather than `Object.hasOwn` to match `driver-sql`'s twin\n * (`reduceFilterKey`) line for line. `{ $null: undefined }` DOES count: the key\n * is own and enumerable, and `undefined` is one of the comparands #6387\n * measured a flip on — it lowered to `IS NOT NULL`, which\n * `read-scope-undefined-comparand.test.ts` pinned as \"the cell #6125\n * deliberately left alone\". This is the ruling that picks it up. Refusing it\n * here rather than in {@link assertDefinedComparands} keeps that gate's claim\n * honest — `undefined` is refused as a value OUTSIDE the declared BOOLEAN\n * DOMAIN, which is a truer diagnosis than \"a comparand position is undefined\"\n * for a flag that was never a comparand position.\n *\n * ## Why this call site, and not the `$not` pre-pass\n *\n * Same reason {@link assertDefinedComparands} sits here: {@link compileField} is\n * the one road every field constraint travels, because {@link compileNode}\n * `.map()`s every child into its own buffer BEFORE any boolean identity is\n * applied, so no sibling can absorb a malformed one. It runs AFTER\n * {@link nullSafeNegationOperand} for a `$not` operand — harmless, and worth\n * stating: that rewrite consults {@link nullValueSatisfiesOperator}, which now\n * reads these two by identity, so a non-boolean is classified before it is\n * refused. The classification is DISCARDED either way (the leaf still reaches\n * `compileField` and still throws), and the rewrite's own synthesised leaves\n * (`{ $null: false }`, `{ $null: true }`) are literal booleans by construction.\n */\nfunction assertBooleanFlagComparands(field: string, spec: unknown): void {\n if (!isFilterNode(spec)) return;\n for (const op of ['$null', '$exists'] as const) {\n if (!Object.prototype.hasOwnProperty.call(spec, op)) continue;\n if (typeof spec[op] === 'boolean') continue;\n throw nonBooleanFlagComparandError(op, field, `\"${field}\".${op}`);\n }\n}\n\n/**\n * [#7598] A `{ $field: 'col' }` reference in a comparand position this compiler\n * BOUND instead of refusing — the THIRTEENTH refusing site, and the first one\n * whose shape is executed correctly somewhere else.\n *\n * ## The measured cell, on `origin/main` (`5823d593d`), alias `person`\n *\n * | read scope | compiled to | bind list |\n * |---|---|---|\n * | `{ amount: { $gt: { $field: 'budget' } } }` | `\"person\".\"amount\" > ?` | `[{\"$field\":\"budget\"}]` |\n * | `{ amount: { $eq: { $field: 'budget' } } }` | `\"person\".\"amount\" = ?` | `[{\"$field\":\"budget\"}]` |\n *\n * The reference OBJECT goes into the bind list verbatim — `applyReadScope`\n * (`native-sql-strategy.ts`) pushes `scopeParams[i]` into the driver's array\n * while it renumbers `?` → `$N`, exactly as #6125 measured for `undefined`. What\n * the driver then does with a plain object is its own business: JSON text on the\n * `toSqlBindValue` drivers, a bare `Undefined binding(s)`-class crash on the ones\n * that refuse to guess. Either way an admin's RLS predicate compared a column\n * against a value no row can hold, silently. In a module whose contract is\n * \"a read-scope predicate must never be silently dropped\" a predicate that is\n * silently MEANINGLESS is the same defect one step further on — and unlike\n * #6125's cell it is not reliably fail-closed, because the comparison it\n * degrades to depends on the driver rather than on the scope.\n *\n * ## Which positions this gate covers, and why the others keep their wording\n *\n * ONLY the positions that were BOUND: the whole comparand of the six scalar\n * comparison operators ({@link CROSS_FIELD_COMPARISON_OPERATORS}) and the two\n * `$between` endpoints. Everything else a reference can occupy already refused\n * here BEFORE this change, with a diagnosis of its own, and each of those\n * refusals converges with `driver-sql`'s own #5222 refusal arm — so widening\n * this gate over them would restate a rule that is already right in a second\n * wording (#5240, read in the direction that matters: a second sentence for a\n * shape refused either way only sends the operator to the wrong repair):\n *\n * - the LIKE family → {@link assertRenderableText} (\"matches against the TEXT\n * of a pattern\"), and `driver-sql` refuses a reference there too — a\n * column-side LIKE pattern cannot be metacharacter-escaped portably;\n * - `$in` / `$nin` members → {@link assertCompilableMembers} (\"cannot be bound\n * as a SQL parameter\"), and `driver-sql` refuses those members as well,\n * because the memory evaluator does not resolve a reference inside a list\n * either;\n * - a bare `{ field: { $field: … } }` → `unsupported operator \"$field\"` from\n * {@link compileOperator}'s default arm.\n *\n * `$between` is in the covered set even though {@link assertCompilableMembers}\n * would also refuse its endpoints, because this gate runs FIRST and the truer\n * diagnosis wins: \"a range bound may not be a field reference on any backend\"\n * tells the policy author what to write, where \"cannot be bound as a SQL\n * parameter\" describes a consequence of the shape rather than the shape. The two\n * covered positions now say DIFFERENT sentences, because the 2026-08-12 ruling\n * made them different conditions — see {@link fieldReferenceComparandMessage}\n * (a rendering boundary on a rule the platform SERVES) versus\n * {@link fieldReferenceBetweenBoundMessage} (a position refused everywhere, and\n * removed from the spec by #7596).\n *\n * ## What reaches this gate after the 2026-08-12 ruling\n *\n * Not `applyReadScope`. `NativeSQLStrategy.canHandle` declines a query whose\n * read scope carries a scalar reference before that method runs, so the scope\n * is served on the engine path instead (module header). What DOES reach it is\n * `ObjectQLStrategy.generateSql` — the `/analytics/sql` echo — plus any external\n * consumer of the `compileScopedFilterToSql` export. The gate is therefore the\n * echo's decline, which is what the ruling asked that face for.\n *\n * ## Envelope: unchanged, deliberately (#5367 ruling 2026-08-06, re-affirmed as\n * #7598 Q2 = A on 2026-08-12)\n *\n * `READ_SCOPE_COMPILE_FAILED` / 500, like the other twelve. The two arguments\n * #5367 gave apply to this shape verbatim rather than by analogy: the producer\n * is an ADMIN-authored sharing rule / permission set and its CEL lowering —\n * `compileCelToFilter` is exactly what emits `{ $field: path }` — so a 4xx would\n * bill the caller for a document they cannot author, and a 4xx echoes the\n * message, which here names the POLICY's field names. #7598 put the question to\n * the maintainer and it was answered A: keep #5367 as it stands, add no new\n * ADR-0112 code for the unsupported-rule class (option C had zero measured pull\n * — #5367 recorded that no consumer reads a code on this path — and a zero-pull\n * vocabulary is recorded, not built), and do not move to 4xx.\n */\nfunction assertNoFieldReferenceComparand(field: string, spec: unknown): void {\n if (!isFilterNode(spec)) return;\n for (const [op, opValue] of Object.entries(spec)) {\n if (CROSS_FIELD_COMPARISON_OPERATORS.has(op) && isFieldReference(opValue)) {\n throw readScopeCompileError(\n `[read-scope-sql] ${fieldReferenceComparandMessage(op, field, opValue.$field)}`,\n );\n }\n if (op !== '$between' || !Array.isArray(opValue)) continue;\n opValue.forEach((member, index) => {\n if (!isFieldReference(member)) return;\n throw readScopeCompileError(\n `[read-scope-sql] ${fieldReferenceBetweenBoundMessage(op, field, member.$field, index)}`,\n );\n });\n }\n}\n\nfunction compileOperator(col: string, op: string, val: unknown, field: string, params: unknown[]): string {\n switch (op) {\n case '$eq': return val === null ? `${col} IS NULL` : `${col} = ${bind(params, val)}`;\n // [#5298] `$ne: null` stays `IS NOT NULL` — already total, and \"has any\n // value\" is false for a row that has none. Only the comparison is guarded.\n case '$ne': return val === null ? `${col} IS NOT NULL` : nullSafeNegative(col, `${col} <> ${bind(params, val)}`);\n case '$gt': return `${col} > ${bind(params, val)}`;\n case '$gte': return `${col} >= ${bind(params, val)}`;\n case '$lt': return `${col} < ${bind(params, val)}`;\n case '$lte': return `${col} <= ${bind(params, val)}`;\n case '$in': {\n if (!Array.isArray(val)) throw readScopeCompileError(`[read-scope-sql] $in for \"${field}\" needs an array (fail-closed).`);\n if (val.length === 0) return FALSE_CLAUSE; // IN () matches nothing — safe\n assertCompilableMembers(op, field, val);\n return `${col} IN (${val.map((v) => bind(params, v)).join(', ')})`;\n }\n case '$nin': {\n if (!Array.isArray(val)) throw readScopeCompileError(`[read-scope-sql] $nin for \"${field}\" needs an array (fail-closed).`);\n if (val.length === 0) return '1 = 1'; // NOT IN () excludes nothing\n assertCompilableMembers(op, field, val);\n // [#5298] NULL-safe: \"not among this list\" holds vacuously for a value\n // that is not there.\n return nullSafeNegative(col, `${col} NOT IN (${val.map((v) => bind(params, v)).join(', ')})`);\n }\n case '$between': {\n if (!Array.isArray(val) || val.length !== 2) throw readScopeCompileError(`[read-scope-sql] $between for \"${field}\" needs [min,max] (fail-closed).`);\n assertCompilableMembers(op, field, val);\n return `${col} BETWEEN ${bind(params, val[0])} AND ${bind(params, val[1])}`;\n }\n // [#5567] The comparand is a LITERAL, so it is escaped and the escape\n // character is bound with it. See {@link bindLike}.\n // [#5234] …and it must be a value `String()` can render, which is asserted\n // BEFORE `likePattern` sees it — see {@link assertRenderableText}.\n case '$contains': assertRenderableText(op, field, val); return `${col} LIKE ${bindLike(params, likePattern('contains', val))}`;\n /**\n * [#6520] `$icontains` on the READ-SCOPE lowering — the one compiler in this\n * package where a wrong answer is an ADR-0021 scope over-reach rather than a\n * loose chart filter, which is why the fold is the spec's ruled one and not\n * `LOWER()`.\n *\n * `assertRenderableText` first, exactly as its case-exact twin above: the\n * comparand has to be something `String()` renders faithfully before a\n * pattern is built from it (#5234).\n *\n * The fold wraps BOTH the column and the bound pattern. Folding one side\n * only would compare a folded needle against a raw column — matching just\n * the rows already lower-case — and on a read scope that is a row set the\n * policy author never wrote, in the narrowing direction here but in the\n * WIDENING direction under a `$not`.\n */\n case '$icontains': {\n assertRenderableText(op, field, val);\n // The two binds are spelled out rather than taken from `bindLike`, because\n // only the PATTERN placeholder is folded and the `ESCAPE` one must not be.\n // Left-to-right, so the values land in `params` in placeholder order —\n // the ordering invariant `bindLike`'s own comment states.\n const patternRef = asciiLowerSqlExpr(bind(params, likePattern('contains', val)));\n return `${asciiLowerSqlExpr(col)} LIKE ${patternRef} ESCAPE ${bind(params, LIKE_ESCAPE_CHAR)}`;\n }\n // [#5298] NULL-safe: `NOT LIKE` is UNKNOWN for a NULL column, and \"does not\n // contain\" is true of a value that is not there.\n case '$notContains': assertRenderableText(op, field, val); return nullSafeNegative(col, `${col} NOT LIKE ${bindLike(params, likePattern('contains', val))}`);\n case '$startsWith': assertRenderableText(op, field, val); return `${col} LIKE ${bindLike(params, likePattern('starts', val))}`;\n case '$endsWith': assertRenderableText(op, field, val); return `${col} LIKE ${bindLike(params, likePattern('ends', val))}`;\n // [#6387] `val` is a boolean here — {@link assertBooleanFlagComparands}\n // refused anything else at {@link compileField}, before this emitter runs.\n // So `=== true` is an exhaustive TWO-WAY choice over the declared domain,\n // not the \"anything truthy is IS NULL\" rule it used to be. That old rule is\n // what put the STRING `\"false\"` on the side opposite the `false` it was\n // written to mean; the identity spelling cannot, and it is the spelling\n // {@link nullValueSatisfiesOperator} now mirrors (#5146 / #5298).\n case '$null': return val === true ? `${col} IS NULL` : `${col} IS NOT NULL`;\n case '$exists': return val === true ? `${col} IS NOT NULL` : `${col} IS NULL`;\n default:\n throw readScopeCompileError(`[read-scope-sql] unsupported operator \"${op}\" on \"${field}\" (fail-closed).`);\n }\n}\n\n// ── [#5146] NULL-safe `$not` ─────────────────────────────────────────────────\n\n/**\n * What one field constraint needs so its compiled SQL is TOTAL — TRUE or FALSE\n * for every row, never UNKNOWN.\n *\n * - `'none'` — already total (`IS NULL` / `IS NOT NULL`), or a shape\n * this compiler refuses outright, which must keep refusing.\n * - `'requireValue'` — a NULL column does NOT satisfy it: `col IS NOT NULL AND (…)`.\n * - `'allowNull'` — a NULL column DOES satisfy it: `col IS NULL OR (…)`.\n */\ntype NullGuard = 'none' | 'requireValue' | 'allowNull';\n\n/**\n * Does a NULL column satisfy this one operator, under the semantics the JS\n * backends (`driver-memory`'s `match`, `formula`'s `matchesFilterCondition`)\n * give it? They evaluate a missing value in ordinary two-valued JS — `undefined\n * !== 'won'` is simply `true` — and #5146 ruled that answer canonical.\n *\n * This is `sql-driver.ts`'s `nullValueSatisfiesOperator` table, entry for entry,\n * with ONE deliberate difference that comes from THIS file's emitter rather than\n * from a different reading of #5146:\n *\n * - `$between` exists in this compiler and not in that table; it is a\n * positive comparison, so it takes the default (a value that is not there\n * does not lie between two bounds) exactly as the other comparisons do.\n *\n * ⚠️ [#6387] There used to be a SECOND difference, and its removal is half of\n * that change rather than a tidy-up. `$null` / `$exists` were read here by\n * TRUTHINESS — `Boolean(value)` / `!value` — because {@link compileOperator}\n * wrote them as `val ? … : …`, while `driver-sql` read them by identity because\n * its emitter did. That was correct under the invariant #5146 / #5298 state:\n * each polarity table pins the spelling of ITS OWN emitter, not the other\n * file's. So when the emitter stopped guessing at a non-boolean, these two arms\n * had to move WITH it in the same change — leaving them truthy would have\n * broken the invariant silently, at its own definition, with nothing red. The\n * divergence is gone now because its cause is: both emitters read the declared\n * boolean domain, so both tables spell it by identity, and the two files agree\n * on every arm for the first time.\n *\n * The default is the large positive-comparison family (`$gt`/`$in`/`$contains`/\n * …), every member of which answers `false` for a value that is not there. An\n * operator this compiler does not support also lands here; it is guarded and\n * then still throws from {@link compileOperator}, so fail-closed is preserved.\n */\nfunction nullValueSatisfiesOperator(op: string, value: unknown): boolean {\n switch (op) {\n // `$eq: null` IS the null predicate; any other comparand is a value test.\n case '$eq': return value === null;\n // Mirror image: `$ne: null` compiles to `IS NOT NULL`, which a NULL fails.\n case '$ne': return value !== null;\n // [#6387] Identity, matching this file's emitter (see the note above).\n // `assertBooleanFlagComparands` refuses anything but `true` / `false` before\n // this table is consulted, so each arm is an exhaustive TWO-WAY choice over\n // the declared domain — and the strict spelling is chosen over the lenient\n // one it replaces for the reason #5347 gave: `Boolean(value)` and\n // `value === true` are equivalent only while the gate upstream holds, and\n // the lenient spelling would quietly resume answering for shapes nobody\n // ruled on if that gate were ever moved. A NULL column satisfies `$null`\n // exactly when the author asked for null…\n case '$null': return value === true;\n // …and satisfies `$exists` exactly when the author asked for \"no value\".\n // `$null: true` and `$exists: false` are the same question, so these two\n // arms are correctly each other's MIRROR, not each other's copy (#5369).\n case '$exists': return value === false;\n // Negative-polarity set / substring tests hold vacuously for an absent value.\n case '$nin': return true;\n // `$notContains` is the one operator where the two JS backends disagree for\n // a null-valued field (`driver-memory` answers false, `formula` true).\n // `formula` is followed because `driver-sql` follows it, so this compiler\n // does not cast a vote on a disagreement that is filed elsewhere.\n case '$notContains': return true;\n default: return false;\n }\n}\n\n/** Is this operator's compiled SQL already total for a NULL column? */\nfunction operatorIsNullTotal(op: string, value: unknown): boolean {\n switch (op) {\n // Compile to `IS NULL` / `IS NOT NULL` — two-valued by construction.\n case '$null':\n case '$exists':\n return true;\n // A null comparand makes these null PREDICATES too, not comparisons.\n case '$eq':\n case '$ne':\n return value === null;\n default:\n return false;\n }\n}\n\n/**\n * The guard one field constraint needs. A constraint is the AND of its\n * operators, so it is total when every operator is, and a NULL column satisfies\n * it only when it satisfies all of them.\n */\nfunction nullGuardForFieldSpec(spec: unknown): NullGuard {\n // `{ field: null }` compiles to `IS NULL` — already total.\n if (spec === null) return 'none';\n // A scalar / Date is an implicit `=`; a NULL column fails it. A bare array is\n // REFUSED by `compileField`; classifying it here keeps that refusal reachable\n // (the unrewritten `{field: […]}` conjunct still throws its own message).\n if (typeof spec !== 'object' || spec instanceof Date || Array.isArray(spec)) return 'requireValue';\n const entries = Object.entries(spec as Record<string, unknown>);\n // `{ field: {} }` and any non-`$` key are shapes `compileField` throws on.\n // Passing them through unrewritten is what preserves the exact error; a guard\n // wrapped around them would only change which message the caller sees.\n if (entries.length === 0) return 'none';\n let total = true;\n let nullSatisfies = true;\n for (const [op, value] of entries) {\n if (!operatorIsNullTotal(op, value)) total = false;\n if (!nullValueSatisfiesOperator(op, value)) nullSatisfies = false;\n }\n if (total) return 'none';\n return nullSatisfies ? 'allowNull' : 'requireValue';\n}\n\n/**\n * [#5146] Rewrite the operand of a `$not` so every leaf compiles to a TOTAL\n * predicate — which is what makes `NOT (…)` mean here what it means in\n * `driver-memory`, `formula` and (since #5296) `driver-sql`.\n *\n * # Why the guard rides the LEAF, not the `NOT`\n *\n * For a flat operand `NOT (a IS NOT NULL AND a = ?)` and `NOT (a = ?) OR a IS\n * NULL` are the same predicate. They stop being the same as soon as the operand\n * nests: hoisting the guard above a `$not` whose operand is a `$or` re-admits\n * rows the JS backends exclude — a NULL `a` would satisfy the whole negation\n * even when the `$or`'s OTHER branch is satisfied. Totalising each leaf makes\n * the rewrite compositional instead: De Morgan is sound over two-valued leaves,\n * so `$and`, `$or` and a nested `$not` all stay correct with no special cases.\n * On an RLS lowering that difference is rows a policy excludes becoming visible,\n * so it is the whole reason this is a rewrite and not a suffix.\n *\n * # Why polarity is per operator\n *\n * A blanket `OR col IS NULL` would WIDEN the negative-polarity operators:\n * `{$not: {a: {$ne: 5}}}` means \"a is 5\", and both JS backends exclude a NULL\n * row from it. Adding an unconditional null escape there would hand back exactly\n * the rows the scope excludes. So each leaf is guarded in the direction its own\n * operator answers, per {@link nullValueSatisfiesOperator}.\n *\n * The rewrite runs ONLY inside a `$not`; an ordinary comparison's SQL is\n * untouched, so nothing outside a negation changes shape. A nested `$not` is\n * left alone on purpose — its own branch totalises its operand, and\n * `NOT <total>` is itself total, so recursing would stack a redundant guard on\n * the same column.\n */\nfunction nullSafeNegationOperand(node: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n const guarded: unknown[] = [];\n for (const [key, value] of Object.entries(node)) {\n if ((key === '$and' || key === '$or') && Array.isArray(value)) {\n // A non-node element is passed through so `compileNode` still rejects it.\n out[key] = value.map((element) => (isFilterNode(element) ? nullSafeNegationOperand(element) : element));\n continue;\n }\n if (key.startsWith('$')) {\n // `$not` (handled by its own branch) and anything else `$`-prefixed keep\n // whatever this compiler does with them today — the rewrite rules on NULL,\n // not on the operator vocabulary, and an unknown one must still throw.\n out[key] = value;\n continue;\n }\n const guard = nullGuardForFieldSpec(value);\n if (guard === 'none') {\n out[key] = value;\n } else if (guard === 'requireValue') {\n // `col IS NOT NULL AND (…)` — both conjuncts of the enclosing node.\n guarded.push({ [key]: { $null: false } }, { [key]: value });\n } else {\n // `col IS NULL OR (…)` — one conjunct, so the OR binds tighter than the\n // AND this node's keys form.\n guarded.push({ $or: [{ [key]: { $null: true } }, { [key]: value }] });\n }\n }\n if (guarded.length > 0) {\n const existing = Array.isArray(out.$and) ? out.$and : [];\n out.$and = [...existing, ...guarded];\n }\n return out;\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * [#5367] The dataset refusals this service raises, in the ADR-0112 envelope.\n *\n * ## Why this file exists\n *\n * `/analytics/dataset/query` classifies a thrown error by reading its `code` +\n * 4xx `status` (#5352 / PR #5366). Five refusals in this package were still bare\n * `throw new Error(…)`, so the route could not read them at all — and they only\n * kept answering `400 DATASET_INVALID` because the catch carried a hardcoded\n * list of message SUBSTRINGS as a transitional fallback:\n *\n * ```\n * /not declared in the dataset|not backed by a declared relationship|\n * not supported by the v1 dataset runtime|read-scope-sql|\n * not a selected dimension or measure|is not a subset of the selected dimensions/\n * ```\n *\n * Prime Directive #12 allows an accommodation like that only while it is\n * declared, loud, tested **and removable on a schedule**. #5366 delivered the\n * first three and nothing carried the fourth, which made the HTTP status of five\n * error families a property of their WORDING: rephrasing\n * \"is not declared in the dataset's `include`\" — no logic change, no test red, no\n * gate red — silently moved that refusal from 400 to 500, i.e. re-opened #5352\n * for a different family. #5367 is that schedule; this constructor is how the\n * five families leave the list.\n *\n * ## The envelope, and why it is `DATASET_INVALID` / 400\n *\n * Same shape as `filter-normalizer.ts`'s `invalidFilterError`\n * (`INVALID_FILTER` / 400) and `analytics-service.ts`'s dimension/measure gates\n * (`INVALID_FIELD` / 400): the code names the caller-shaped mistake, the status\n * says whose fault it is, and the message stays whatever the refusing site says.\n * `DATASET_INVALID` is not a new code — it is what the route's fallback list has\n * answered for these five families since #5352, registered in\n * `ERROR_CODE_LEDGER` (ADR-0112 D3). Producing it HERE, rather than deriving it\n * there from message text, is the whole change: one condition, one wire shape,\n * chosen by the producer that knows.\n *\n * The `RegisteredErrorCode` annotation is load-bearing rather than decorative —\n * it is what makes an unregistered code a compile error instead of a string that\n * only fails when some route happens to parse its own response body.\n *\n * ## [#5716] The second constructor, and how to choose between them\n *\n * #5352 named six refusal families and #5367 enveloped five of them. Reading\n * every `throw` in this package afterwards turned up NINE more sites of exactly\n * the same kind — caller- or author-shaped refusals that never entered the\n * route's message list at all, so they were answering `500` with nobody's\n * regex to rescue them — plus the `objectql-strategy.ts` `planCrossObject`\n * family (PM ruling on #5716, 2026-08-06). What decides the CODE is not which\n * file throws but what the refusal is a verdict ABOUT:\n *\n * - {@link datasetInvalidError} — a verdict about the DATASET or the whole\n * SELECTION: an `include` path that cannot be joined, an aggregate v1 cannot\n * lower, a `compareTo` with no window to shift, a `dateRange` that is not a\n * date. The caller fixes the dataset definition or the selection.\n * - {@link invalidMemberError} — a verdict about ONE MEMBER the request named:\n * a measure the cube does not declare, a member this engine cannot join to.\n * The caller fixes (or drops) that member.\n *\n * The member family is `INVALID_FIELD` / 400 rather than a second\n * `DATASET_INVALID` for two measured reasons. First, the three shipped analytics\n * gates already answer `INVALID_FIELD` / 400 to the NEIGHBOURING member-level\n * mistakes on the very same request keys — `measures` (#4437), `dimensions` /\n * `timeDimensions` (#5520), `where` (#5669) — so a caller who mistypes a member\n * and a caller who names one the engine cannot serve would otherwise get two\n * wire shapes for one class of mistake, which is the defect ADR-0112 exists to\n * remove. Second, these sites are NOT dataset-only: `planCrossObject` and the\n * undeclared-measure refusal fire on `/analytics/query` too, where there is no\n * dataset at all — `DATASET_INVALID` would name a document the caller never\n * sent, while `INVALID_FIELD` reads correctly on both faces.\n *\n * ## What deliberately does NOT go through here\n *\n * Not every `throw` in this package is the caller's mistake, and enveloping one\n * that isn't would be the mirror-image defect — an internal fault re-labelled\n * `400`, which hides it from ops alerting and tells the author to fix something\n * they did not write. Three families are deliberately NOT `DATASET_INVALID`:\n *\n * - **`read-scope-sql.ts`'s ten fail-closed refusals** — a SERVER fault, and\n * since the maintainer's 2026-08-06 ruling they say so: that module's own\n * `readScopeCompileError` gives all ten `READ_SCOPE_COMPILE_FAILED` / **500**.\n * Its inputs are an RLS `FilterCondition` the security service compiled from\n * an admin-authored policy and a join alias the dataset compiler generated —\n * neither is caller input, so `400` both misattributed the fault and echoed\n * policy field names back to the tenant. (This bullet said \"stays bare,\n * verdict pending\" until that ruling; the route's message list is now gone\n * entirely and #5367's retirement schedule is paid off.)\n * - **Internal invariants** — e.g. `dataset-compiler.ts`'s \"non-derived measure\n * has no aggregate\", which the spec refinement already guarantees. An\n * arrival there is our bug; an undeclared `500` is the honest answer, and\n * staying bare keeps it readable in the response (#5667's tiering) instead of\n * withheld like a declared server fault. [#5716] `native-sql-strategy.ts`'s\n * \"measure … has unrecognised type\" joins this bullet after measurement, and\n * against #5716's own list, which had it down as author-shaped: `Metric.type`\n * is the CLOSED `AggregationMetricType` enum, `metric-type-coverage.test.ts`\n * pins that every member of it is handled (its second case is literally \"leaves\n * no metric type to the unrecognised-type throw\"), the dataset compiler maps\n * only `SUPPORTED_AGGREGATES` into a cube, and `inferMeasure` mints six known\n * types. So no spec-valid cube can reach it — an arrival is our own drift or a\n * host registering an unparsed cube object, which is the same 500 tier as the\n * line above, not the author's 400.\n * - **Producer/consumer drift between two of OUR tables** — the posture\n * `objectql-strategy.ts`'s display-SQL renderer already states explicitly\n * (\"Deliberately NOT `invalidFilterError`'s 400 envelope: this is drift\n * between two of our own tables, not a caller-shaped mistake\", #5333).\n *\n * So this module is deliberately NOT \"the only way this package refuses\" — the\n * claim `invalidFilterError` can make about `filter-normalizer.ts`, and\n * `readScopeCompileError` about `read-scope-sql.ts`. It is the way this package\n * refuses **the caller**.\n */\n\nimport type { RegisteredErrorCode, StandardErrorCode } from '@objectstack/spec/api';\n\n/**\n * `DATASET_INVALID`, pinned against the ledger.\n *\n * Typed as `RegisteredErrorCode` so removing the ledger row (or misspelling the\n * code here) fails `tsc` rather than shipping a code `ApiErrorSchema` rejects.\n */\nconst DATASET_INVALID: RegisteredErrorCode = 'DATASET_INVALID';\n\n/**\n * [#5716] `INVALID_FIELD`, pinned against the STANDARD catalog.\n *\n * Same load-bearing annotation as `DATASET_INVALID` above, one tier over: this\n * code is platform-wide (`StandardErrorCode`), not registered per package, which\n * is precisely why the member-level refusals use it — see the module header.\n */\nconst INVALID_FIELD: StandardErrorCode = 'INVALID_FIELD';\n\n/**\n * [#5716] Which request key named the member — the analytics vocabulary, spelled\n * exactly as the shipped source-field gates spell it in `err.param`.\n */\nexport type AnalyticsRequestKey = 'measures' | 'dimensions' | 'timeDimensions' | 'where';\n\n/**\n * A dataset refusal in the ADR-0112 envelope — `DATASET_INVALID` / 400.\n *\n * Use it for a refusal the CALLER can fix by changing the request or the dataset\n * definition they authored: a selection that names something the dataset does not\n * declare, a dataset whose fields traverse an undeclared relationship, an\n * aggregate the v1 runtime does not implement. See this module's header for the\n * families that deliberately stay bare `Error`s.\n */\nexport function datasetInvalidError(message: string): Error {\n const err = new Error(message) as Error & { code?: string; status?: number };\n err.code = DATASET_INVALID;\n err.status = 400;\n return err;\n}\n\n/**\n * [#5716] A refusal about ONE MEMBER the request named — `INVALID_FIELD` / 400.\n *\n * Use it when the verdict is about a single `measures` / `dimensions` /\n * `timeDimensions` / `where` entry rather than about the dataset or the whole\n * selection: a measure the cube does not declare (#4157), a member this engine\n * cannot evaluate because it traverses a relationship the driver cannot join\n * (`planCrossObject`). The message stays whatever the refusing site says — every\n * one of these already names the member and how to fix it, and #5923's tests\n * assert that wording.\n *\n * `member` is the entry AS THE REQUEST SPELLED IT — `revenue`, not the\n * `account.balance` it resolved to — because that is the string the caller can\n * find in the body they sent; the resolved form stays in the message, which is\n * where the explanation lives. `member` / `param` / `cube` mirror the diagnostic\n * fields the three shipped gates attach\n * (`err.field`/`err.param`/`err.measure`…). `field` is deliberately\n * NOT among them: those gates resolve a member to a base COLUMN and name the\n * column that is missing, while here either there is no such column (an\n * undeclared measure) or the column exists and is perfectly fine on another\n * driver (a cross-object member). Naming one would be inventing a fact.\n */\nexport function invalidMemberError(\n message: string,\n meta: { member: string; param?: AnalyticsRequestKey; cube?: string },\n): Error {\n const err = new Error(message) as Error & {\n code?: string;\n status?: number;\n member?: string;\n param?: string;\n cube?: string;\n };\n err.code = INVALID_FIELD;\n err.status = 400;\n err.member = meta.member;\n if (meta.param) err.param = meta.param;\n if (meta.cube) err.cube = meta.cube;\n return err;\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { AnalyticsQuery, AnalyticsResult } from '@objectstack/spec/contracts';\nimport type { Cube } from '@objectstack/spec/data';\nimport type { AnalyticsStrategy, StrategyContext } from './types.js';\nimport {\n lowerAnalyticsWhere,\n normalizeAnalyticsFilterTree,\n toSqlBindValue,\n SQL_CONST_FALSE,\n SQL_CONST_TRUE,\n type NormalizedFilterNode,\n} from './filter-normalizer.js';\nimport { findCrossFieldComparand, findUninterpretableTemporalMember } from '../comparand-shape.js';\nimport { compileScopedFilterToSql } from '../read-scope-sql.js';\nimport { datasetInvalidError, invalidMemberError } from '../dataset-refusal.js';\nimport { likePattern, LIKE_ESCAPE_CHAR, asciiLowerSqlExpr, type LikeShape } from '../like-pattern.js';\nimport { nextUtcCalendarDay } from '@objectstack/core';\n\n/**\n * The SQL wrapper for each aggregate a measure's `type` can name.\n *\n * A table rather than a `switch` so its coverage is *assertable*: the aggregate\n * vocabulary lives in `@objectstack/spec` (`AggregationFunction`), the dataset\n * compiler subtracts whatever it cannot lower (`UNSUPPORTED_AGGREGATES` — empty\n * since #6188 retired its two members, `array_agg` and `string_agg`, from the\n * spec itself), and `aggregation-lockstep.test.ts` checks that what remains is\n * exactly the keys below. A `switch` gave that no purchase — the missing case fell to\n * `default: COUNT(*)`, so an aggregate the spec grew would have returned a row\n * count instead of the number the author asked for, silently. objectui#2945.\n *\n * Non-aggregate metric types (`number`/`string`/`boolean`) are deliberately\n * absent — they are handled by {@link EXPRESSION_METRIC_TYPES}, which emits the\n * author's expression rather than wrapping it.\n */\nconst AGGREGATE_SQL: Record<string, (col: string) => string> = {\n 'count': () => 'COUNT(*)',\n 'sum': (col) => `SUM(${col})`,\n 'avg': (col) => `AVG(${col})`,\n 'min': (col) => `MIN(${col})`,\n 'max': (col) => `MAX(${col})`,\n 'count_distinct': (col) => `COUNT(DISTINCT ${col})`,\n};\n\n/** Exported for the lockstep guard — the aggregates this strategy can lower. */\nexport const SUPPORTED_AGGREGATE_SQL_KEYS = Object.keys(AGGREGATE_SQL);\n\n/**\n * Metric types that are a custom SQL *expression*, not an aggregate to wrap.\n *\n * `AggregationMetricType` (`data/analytics.zod.ts`) documents these three as\n * \"Custom SQL expression returning a number / string / boolean\" — the measure's\n * `sql` IS the whole computation (a ratio, a `CASE`, a window function), so the\n * only correct emission is the expression itself. They used to fall through to\n * `resolveMeasureSql`'s `COUNT(*)` fallback, which threw the expression away and\n * returned a row count. #4157.\n *\n * Named rather than derived as \"everything that is not an aggregate\": deriving it\n * would silently classify a *new* aggregate the spec grows (`median`, …) as an\n * expression and emit a bare column. `metric-type-coverage.test.ts` asserts these\n * two sets partition `AggregationMetricType`, so a new member fails a test\n * instead of picking a default.\n */\nexport const EXPRESSION_METRIC_TYPES = new Set(['number', 'string', 'boolean']);\n\n/**\n * A dot-separated chain of bare identifiers — `amount`, `account.amount`,\n * `account.owner.region`. Distinguishes a relationship PATH, which\n * {@link NativeSQLStrategy.qualifyAndRegisterJoin} lowers into joins, from a SQL\n * expression that merely contains a dot. #4157.\n */\nconst IDENTIFIER_PATH = /^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)*$/;\n\n/**\n * NativeSQLStrategy — Priority 1\n *\n * Pushes the analytics query down to the database as a native SQL statement.\n * This is the most efficient path and is preferred whenever the backing driver\n * supports raw SQL execution (e.g. Postgres, MySQL, SQLite).\n *\n * `resolveMeasureSql` used to answer `COUNT(*)` to three different questions it\n * could not otherwise answer — an undeclared measure, a custom-SQL-expression\n * metric type, and an unrecognised type. All three returned a plausible number\n * for a query that asked for something else. They now emit the expression or\n * throw; see that method. #4157.\n */\nexport class NativeSQLStrategy implements AnalyticsStrategy {\n readonly name = 'NativeSQLStrategy';\n readonly priority = 10;\n\n canHandle(query: AnalyticsQuery, ctx: StrategyContext): boolean {\n if (!query.cube) return false;\n // This strategy groups by the raw column expression (`GROUP BY <col>`) and\n // emits no `date_trunc` — it cannot bucket a date dimension to a coarser\n // granularity, nor resolve buckets on a non-UTC calendar. When the query\n // asks for granularity bucketing we therefore DECLINE so the lower-priority\n // ObjectQLStrategy handles it via `engine.aggregate` (native date_trunc when\n // UTC-safe, else uniform in-memory bucketing). Without this, a date-bucketed\n // query silently grouped by the raw timestamp — one bucket per row — and a\n // non-UTC reference timezone was ignored entirely (ADR-0053 Phase 2, #1982).\n if (query.timeDimensions?.some((td) => !!td.granularity)) return false;\n // ADR-0062 D6 — DECLINE federated (external-datasource) objects. This\n // strategy hand-compiles `FROM \"<object>\"` and bare column references, which\n // bypass the driver's physical-table resolution (`external.remoteName` /\n // `remoteSchema` / `columnMap`) and would query the WRONG table. Routing the\n // query to the lower-priority ObjectQL aggregate path keeps it correct —\n // that path goes through the driver's `getBuilder` (#2138/#2149). Applies to\n // the base object AND any joined object (a join would also hit the wrong\n // table). Until native-SQL learns the driver's resolution, \"disabled\" beats\n // \"silently wrong\".\n if (typeof ctx.isExternalObject === 'function') {\n const cube = ctx.getCube(query.cube);\n if (cube) {\n if (ctx.isExternalObject(this.extractObjectName(cube))) return false;\n const joinTargets = cube.joins ? Object.values(cube.joins) : [];\n for (const j of joinTargets) {\n const joinedObject = (j as { name?: string })?.name;\n if (joinedObject && ctx.isExternalObject(joinedObject)) return false;\n }\n }\n }\n // ── [#7598] DECLINE a `{ $field }` cross-field comparison ───────────────\n //\n // ## The maintainer ruling this implements (2026-08-12, Q1 = B)\n //\n // 「`NativeSQLStrategy.canHandle` 对携带 `$field` 的 `where` / read scope\n // **decline**,落回 ObjectQL/engine 路径,由 driver 用它自有的 metadata 强制\n // 全部四条 #5222 裁定 —— 安全规则只存在一处,不复制、不新增\n // `StrategyContext` 钩子、不动 `packages/spec`。⚠️ canHandle 依据 filter\n // 内容路由是新行为 —— 认可并接受,实现时在 canHandle 处注释记录本裁定。」\n //\n // (Q1 = B; option A — `StrategyContext.getDeclaredFields` / `getTenantColumn`\n // hooks plus a SECOND implementation of the four rulings inside this package\n // — was explicitly rejected: it builds an enumeration surface with no\n // measured consumer, and its fallback when a host omits a hook is either\n // \"refuse\" or \"skip the check\", and skipping the check is the defect #7598\n // exists to close. Q2 = A: `read-scope-sql`'s envelope is untouched.)\n //\n // ## What is new here, and why it is sound\n //\n // Every other decline above turns on the query's SHAPE (a granularity, a\n // federated object). This one turns on filter CONTENT, which is new\n // behaviour for `canHandle` — named as such in the ruling and accepted\n // there. It is the same mechanism ADR-0062 D6 already uses one branch up:\n // when this strategy cannot compile something CORRECTLY, routing to the\n // lower-priority ObjectQL path is better than compiling it anyway. What it\n // cannot compile correctly here is a column-to-column comparison, because\n // the four #5222 rulings (same-table columns only, declared-only\n // enumeration, tenant-isolation column forbidden on BOTH sides, same\n // comparison class) each turn on metadata `StrategyContext` does not expose\n // — an object's declared field set, its declared types, its\n // tenant-isolation column. `driver-sql` reads all four out of its own\n // `initObjects` capture, so declining puts the query in front of the one\n // component that can enforce them, instead of enforcing them twice.\n //\n // ## Both inputs, because a read scope is not the caller's `where`\n //\n // The caller's `where` and the RLS read scope are separate producers and\n // either can carry a reference — `compileCelToFilter` emits `{ $field }`\n // for a field-to-field comparison in an ADMIN-authored CEL rule, which is\n // the read-scope half and the one #5041 measured. The scopes of the JOINED\n // objects are read too, for the same reason `generateSql` injects them:\n // `applyReadScope` would compile each of them through `read-scope-sql`.\n //\n // `lowerAnalyticsWhere` rather than `query.where` raw, so the authored\n // ARRAY sugar (`['amount', '=', { $field: 'budget' }]`) is seen after\n // `parseFilterAST` has lowered it (#7597). A THROW from that lowering is\n // not this gate's to answer — the filter is malformed either way and\n // `normalizeAnalyticsFilterTree` refuses it a moment later with the message\n // and envelope it has always had — so it is caught and read as \"no\n // reference found\".\n if (this.carriesCrossFieldComparison(query, ctx)) return false;\n // ── [#8690] DECLINE an uninterpretable TEMPORAL comparand ───────────────\n //\n // ## The maintainer ruling this implements (2026-08-15, option B)\n //\n // > refuse the uninterpretable temporal comparand at the ObjectQL engine's\n // > single filter collection point … Includes the measured gap:\n // > `NativeSQLStrategy.canHandle` must **decline** an uninterpretable\n // > temporal comparand so raw-SQL paths fall through to the engine door.\n //\n // The refusal itself is NOT here and must not be: judging \"can this column\n // read this comparand\" needs the field's declared TYPE, which only the\n // engine's filter collection point holds (this package depends on no\n // driver and carries no field map). What is here is the ROUTING half —\n // without it a raw-SQL deployment binds `WHERE col >= 'last_30_days'`\n // directly, never reaches the door, and keeps answering 200 with zero rows.\n //\n // Same mechanism, same direction, as the two declines above and the #7598\n // one below it: when this strategy cannot serve something CORRECTLY,\n // routing to the lower-priority ObjectQL path beats compiling it anyway.\n // Content-based rather than shape-based, which #7598's ruling already\n // named as new-but-accepted behaviour for `canHandle`.\n //\n // ⚠️ Deliberately NO fail-closed backstop at the emitter, unlike #7598's.\n // There the routing gate's failure mode was a NEW wrong answer (a bound\n // `{\"$field\":…}` object); here a missed decline degrades to exactly\n // today's behaviour, and a throw at the emitter would answer 500 for a\n // filter the engine door answers 400 for — two envelopes for one mistake,\n // which is the drift this card exists to remove.\n if (this.carriesUninterpretableTemporalComparand(query, ctx)) return false;\n const caps = ctx.queryCapabilities(query.cube);\n return caps.nativeSql && typeof ctx.executeRawSql === 'function';\n }\n\n /**\n * [#8690] Does the query's `where` compare a declared TIME dimension against\n * a value no temporal storage rule can read? See the ruling at\n * {@link canHandle}.\n *\n * The classification comes from the CUBE, the only metadata this package has:\n * a dimension declares `type: 'time'` (compiled from the dataset dimension's\n * `type: 'date'`), and {@link lookupMember} is the same resolution every other\n * member lookup in this strategy uses, so \"the member the gate classified\"\n * and \"the member the compiler emits\" cannot drift apart.\n *\n * A `time` dimension is read with the DATETIME rule — the permissive one of\n * the three. That is the right direction because this is a routing decision,\n * not a verdict: the engine door re-judges with the field's real declared\n * type and has the final say, so under-classifying an exotic spelling merely\n * leaves today's behaviour, while over-classifying would silently move a\n * working dashboard off the fast path. The comparands this card measured\n * (`last_30_days`, `not-a-date-at-all`) are unreadable under all three rules,\n * so the decline fires for them whichever backing type the dimension has.\n *\n * `lowerAnalyticsWhere` rather than `query.where` raw, so the authored ARRAY\n * sugar is seen after `parseFilterAST` has lowered it; a THROW from that\n * lowering is not this gate's to answer — the filter is malformed either way\n * and `normalizeAnalyticsFilterTree` refuses it a moment later with the\n * message and envelope it has always had.\n */\n private carriesUninterpretableTemporalComparand(\n query: AnalyticsQuery,\n ctx: StrategyContext,\n ): boolean {\n const cube = query.cube ? ctx.getCube(query.cube) : undefined;\n if (!cube) return false;\n let where: unknown = null;\n try {\n where = lowerAnalyticsWhere(query);\n } catch {\n return false;\n }\n if (!where) return false;\n return findUninterpretableTemporalMember(\n where,\n (member) => (this.lookupMember(cube, member, 'dimension')?.type === 'time' ? 'datetime' : null),\n ) !== null;\n }\n\n /**\n * [#7598] Does serving this query require the cross-field capability this\n * strategy declines? See the ruling recorded at {@link canHandle}.\n *\n * ⚠️ This and {@link assertNoCrossFieldComparison} read the SAME inputs\n * through the SAME detector, which is what makes the decline and the\n * fail-closed backstop unable to drift: a shape one of them recognises is a\n * shape the other recognises.\n */\n private carriesCrossFieldComparison(query: AnalyticsQuery, ctx: StrategyContext): boolean {\n return this.crossFieldComparisonIn(query, ctx) !== null;\n }\n\n private crossFieldComparisonIn(\n query: AnalyticsQuery,\n ctx: StrategyContext,\n ): { source: string; op: string; field: string; ref: string } | null {\n let where: unknown = null;\n try {\n where = lowerAnalyticsWhere(query);\n } catch {\n // A `where` this compiler cannot even lower is refused downstream, with\n // its own message. Nothing to route.\n return null;\n }\n const inWhere = findCrossFieldComparand(where);\n if (inWhere) return { source: 'the query\\'s `where`', ...inWhere };\n\n if (typeof ctx.getReadScope !== 'function') return null;\n const cube = query.cube ? ctx.getCube(query.cube) : undefined;\n if (!cube) return null;\n const objects = [this.extractObjectName(cube)];\n for (const alias of Object.keys(cube.joins ?? {})) {\n objects.push(cube.joins?.[alias]?.name ?? alias);\n }\n for (const objectName of objects) {\n const scope = ctx.getReadScope(objectName);\n if (scope === undefined || scope === null) continue;\n const inScope = findCrossFieldComparand(scope);\n if (inScope) return { source: `the read scope of \"${objectName}\"`, ...inScope };\n }\n return null;\n }\n\n /**\n * [#7598] The fail-closed backstop at the door that BINDS.\n *\n * ⚠️ **Unreachable by construction, and kept deliberately** — saying so\n * because #7598's brief asks that a refusal arm which has become unreachable\n * be named rather than left to be re-discovered. {@link canHandle} declines\n * every query this would fire on, and it declines using\n * {@link crossFieldComparisonIn} — the same walk over the same two inputs —\n * so `resolveStrategy` cannot hand this strategy a query carrying one.\n *\n * It is kept because of what the failure mode is if that ever stops being\n * true. The defect #7598 measured was not a missing error: it was a SILENT\n * BIND — `toSqlBindValue` JSON-stringifies the reference object, so the\n * statement compiled perfectly and compared a column against the text\n * `{\"$field\":\"budget\"}`, a value no row can hold. A routing gate that misses\n * a shape therefore degrades to a wrong ANSWER rather than to an error, and\n * that is the one class this package refuses to leave to a single guard\n * (Prime Directive #12 — refuse at the door, do not tolerate at the\n * consumer). One line, no measurable cost, and it turns a routing regression\n * into a loud refusal instead of an empty chart.\n *\n * Deliberately BARE — an undeclared 500, not `INVALID_FILTER` / 400 — for the\n * reason `buildFilterClauseSql`'s #5333 exit in `objectql-strategy.ts` gives\n * for the same class: the caller's filter is legal and is served on the\n * engine path, so an arrival here is drift between our own routing gate and\n * our own emitter. Billing the caller 400 for that would hide a platform bug\n * from 5xx alerting and tell a dashboard user to fix a filter that is fine.\n * Same tier as `resolveMeasureSql`'s unrecognised-`Metric.type` throw below.\n */\n private assertNoCrossFieldComparison(query: AnalyticsQuery, ctx: StrategyContext): void {\n const hit = this.crossFieldComparisonIn(query, ctx);\n if (!hit) return;\n throw new Error(\n `[native-sql-strategy] ${hit.source} carries a field reference ` +\n `{ \"$field\": \"${hit.ref}\" } under \"${hit.op}\" on \"${hit.field}\", which this strategy does not ` +\n `compile into a column-to-column comparison — it would BIND the reference object as the ` +\n `comparison's value and answer a wrong row set silently (#7598). \\`canHandle\\` declines such a ` +\n `query so it routes to the ObjectQL/engine path, whose driver compiles it and enforces the ` +\n `#5222 rulings with metadata it owns; reaching this throw means the decline and this emitter ` +\n `stopped agreeing, which is our bug and must never degrade to a silent answer.`,\n );\n }\n\n async execute(query: AnalyticsQuery, ctx: StrategyContext): Promise<AnalyticsResult> {\n const { sql, params } = await this.generateSql(query, ctx);\n const cube = ctx.getCube(query.cube!)!;\n const objectName = this.extractObjectName(cube);\n\n const rows = await ctx.executeRawSql!(objectName, sql, params);\n\n // Build field metadata\n const fields = this.buildFieldMeta(query, cube);\n\n return { rows, fields, sql };\n }\n\n async generateSql(query: AnalyticsQuery, ctx: StrategyContext): Promise<{ sql: string; params: unknown[] }> {\n const cube = ctx.getCube(query.cube!);\n if (!cube) {\n throw new Error(`Cube not found: ${query.cube}`);\n }\n\n // [#7598] Unreachable by construction — `canHandle` declined this query.\n // See {@link assertNoCrossFieldComparison} for why it is asserted anyway.\n this.assertNoCrossFieldComparison(query, ctx);\n\n const params: unknown[] = [];\n const selectClauses: string[] = [];\n const groupByClauses: string[] = [];\n const tableName = this.extractObjectName(cube);\n // Map of relation alias → JOIN clause. Populated lazily as dotted\n // dimensions/measures/filters are resolved.\n const joins = new Map<string, string>();\n\n // Build SELECT for dimensions\n if (query.dimensions && query.dimensions.length > 0) {\n for (const dim of query.dimensions) {\n const colExpr = this.resolveDimensionSql(cube, dim, tableName, joins);\n selectClauses.push(`${colExpr} AS \"${dim}\"`);\n groupByClauses.push(colExpr);\n }\n }\n\n // Build SELECT for measures\n if (query.measures && query.measures.length > 0) {\n for (const measure of query.measures) {\n const aggExpr = this.resolveMeasureSql(cube, measure, tableName, joins);\n selectClauses.push(`${aggExpr} AS \"${measure}\"`);\n }\n }\n\n // Build WHERE clause. The filter is a TREE, so it compiles recursively —\n // a flat loop can only ever AND, which is precisely why an author's `$or`\n // used to be dropped instead of compiled.\n const whereClauses: string[] = [];\n const filterSql = this.compileFilterNode(\n normalizeAnalyticsFilterTree(query),\n cube,\n tableName,\n joins,\n params,\n ctx,\n );\n if (filterSql) whereClauses.push(filterSql);\n\n // Build time dimension filters\n if (query.timeDimensions && query.timeDimensions.length > 0) {\n for (const td of query.timeDimensions) {\n const colExpr = this.resolveFieldSql(cube, td.dimension, tableName, joins);\n if (td.dateRange) {\n const range = Array.isArray(td.dateRange) ? td.dateRange : [td.dateRange, td.dateRange];\n if (range.length === 2) {\n // Same epoch-vs-text root cause as buildFilterClause: a dateRange on a\n // SQLite `Field.datetime` column compares ISO TEXT against an INTEGER\n // epoch and matches nothing. Coerce both bounds to the storage form —\n // and normalise the column to that form too, because the column holds\n // BOTH forms at once and coercing only the bounds still empties the\n // half the writer stored the other way (#3912).\n const td2 = this.resolveStorageTarget(cube, td.dimension, tableName);\n const column = this.temporalColumn(ctx, td2, colExpr);\n // A bare-day window end means \"through that whole day\" (#3777). A\n // BETWEEN's inclusive upper bound anchors a bare `YYYY-MM-DD` to\n // midnight on a datetime column, dropping the final day's rows, so\n // the window compiles half-open — `>= start AND < end+1day` — the\n // same `[gte, lt)` the drill ranges emit. Equivalent to the old\n // BETWEEN for a `date` column (plain `YYYY-MM-DD` ordering), which\n // is what lets this path stay column-type-blind.\n const nextDay = nextUtcCalendarDay(range[1]);\n params.push(this.coerceTemporal(ctx, td2, range[0]));\n const lower = `${column} >= $${params.length}`;\n if (nextDay != null) {\n params.push(this.coerceTemporal(ctx, td2, nextDay));\n whereClauses.push(`(${lower} AND ${column} < $${params.length})`);\n } else {\n params.push(this.coerceTemporal(ctx, td2, range[1]));\n whereClauses.push(`(${lower} AND ${column} <= $${params.length})`);\n }\n }\n }\n }\n }\n\n // ── ADR-0021 D-C — enforce the join allowlist + inject per-object RLS ──\n // 1. Reject any join not backed by a relationship the dataset declared.\n const allowed = ctx.getAllowedRelationships?.(query.cube!);\n if (allowed) {\n for (const alias of joins.keys()) {\n if (!allowed.has(alias)) {\n // [#5367] `DATASET_INVALID` / 400 — verified caller-shaped before\n // enveloping. Every join in `joins` was registered by\n // `qualifyAndRegisterJoin`, and on the dataset route the only inputs\n // that can register one OUTSIDE the allowlist are the REQUEST's own:\n // `lookupMember`'s synthetic relation fallback mints a dotted\n // dimension nobody declared, so `selection.dimensions`,\n // `selection.timeDimensions` and a `runtimeFilter` member spelled\n // `account.name` each land here. The dataset's OWN dimensions and\n // measures cannot: `compileDataset`'s `assertDeclared` refuses an\n // undeclared relationship path at compile time (also 400\n // `DATASET_INVALID`, so the two agree rather than diverge), and\n // `resolveMeasureSql` has no synthetic fallback at all.\n //\n // The one non-caller trigger is the legacy\n // `config.getAllowedRelationships` hook for hand-authored cubes,\n // where a mismatch is the host's configuration rather than the\n // caller's query. It is unreachable from\n // `/analytics/dataset/query`: `queryDataset` registers the compiled\n // dataset first, so `getAllowedRelationships` answers from\n // `datasetRegistry` and never falls through to the hook.\n throw datasetInvalidError(\n `[NativeSQLStrategy] join \"${alias}\" is not backed by a declared relationship on ` +\n `cube \"${query.cube}\". v1 only joins along relationships listed in the dataset's \\`include\\`.`,\n );\n }\n }\n }\n // 2. Inject the tenant/RLS read scope for the base table AND every joined\n // object — this is the predicate the raw-SQL path would otherwise skip.\n this.applyReadScope(this.extractObjectName(cube), tableName, ctx, whereClauses, params);\n for (const alias of joins.keys()) {\n // The joined OBJECT (for the RLS lookup) is the target table from the\n // cube's join map; the ALIAS is how it's referenced in SQL. These differ\n // for namespaced objects (alias `account` → object `crm_account`).\n const joinedObject = cube.joins?.[alias]?.name ?? alias;\n this.applyReadScope(joinedObject, alias, ctx, whereClauses, params);\n }\n\n let sql = `SELECT ${selectClauses.join(', ')} FROM \"${tableName}\"`;\n if (joins.size > 0) {\n sql += ' ' + Array.from(joins.values()).join(' ');\n }\n if (whereClauses.length > 0) {\n sql += ` WHERE ${whereClauses.join(' AND ')}`;\n }\n if (groupByClauses.length > 0) {\n sql += ` GROUP BY ${groupByClauses.join(', ')}`;\n }\n if (query.order && Object.keys(query.order).length > 0) {\n const orderClauses = Object.entries(query.order).map(([f, d]) => `\"${f}\" ${d.toUpperCase()}`);\n sql += ` ORDER BY ${orderClauses.join(', ')}`;\n }\n if (query.limit != null) {\n sql += ` LIMIT ${query.limit}`;\n }\n if (query.offset != null) {\n sql += ` OFFSET ${query.offset}`;\n }\n\n return { sql, params };\n }\n\n // ── Helpers ──────────────────────────────────────────────────────\n\n /**\n * ADR-0021 D-C — inject an object's read scope (tenant + RLS predicate) into\n * the WHERE clause. The scope is a canonical `FilterCondition` (what the\n * RLSCompiler emits); `compileScopedFilterToSql` turns it into alias-qualified,\n * parameterized SQL (fail-closed — it throws rather than drop a predicate).\n * The `?` placeholders are then renumbered into the strategy's `$N` scheme.\n * No-op when the runtime provides no scope hook (the caller is then\n * responsible for isolation — see contract note).\n */\n private applyReadScope(\n objectName: string,\n alias: string,\n ctx: StrategyContext,\n whereClauses: string[],\n params: unknown[],\n ): void {\n if (typeof ctx.getReadScope !== 'function') return;\n const filter = ctx.getReadScope(objectName);\n if (filter === undefined || filter === null) return;\n const { sql, params: scopeParams } = compileScopedFilterToSql(filter, alias);\n if (!sql) return;\n let i = 0;\n const rendered = sql.replace(/\\?/g, () => {\n params.push(scopeParams[i++]);\n return `$${params.length}`;\n });\n whereClauses.push(`(${rendered})`);\n }\n\n /** SQL-safe join alias for a relationship path (dots → `__`); single-segment\n * paths are unchanged. Mirrors the dataset compiler's `cube.joins` keying so\n * alias, allowlist, and per-hop RLS all agree on one valid identifier. */\n private joinAlias(path: string): string {\n return path.replace(/\\./g, '__');\n }\n\n /**\n * Resolve a dimension/measure/filter SQL expression that may reference a\n * related table via dot notation (e.g. `account.industry`).\n *\n * A dotted `sql` is a relationship PATH (ADR-0071 multi-hop): every segment\n * but the last is a to-one relationship hop, the last is the column. Each hop\n * synthesises a `LEFT JOIN` aliased by its full path prefix, chained\n * parent→child. The convention (matching the auto-cube generator and\n * ObjectStack object schemas) for a single hop is:\n *\n * <parentTable>.<lookupField> = <lookupField>.id\n *\n * i.e. the lookup field name on the parent table equals the related\n * table name. This holds for all `Field.lookup({ object: '...' })`\n * declarations where the field is named after its target object.\n *\n * Returns the qualified SQL reference (e.g. `\"account\".\"industry\"`).\n * Pure column references (no dot) are returned as-is.\n */\n private qualifyAndRegisterJoin(\n rawSql: string,\n parentTable: string,\n joins: Map<string, string>,\n cube?: Cube,\n ): string {\n if (!rawSql.includes('.')) {\n // Base-table column. When the cube can join other tables, a bare column\n // that also exists on a joined table (e.g. base `status` vs joined\n // `account.status`) makes the SQL engine raise \"ambiguous column name\".\n // Qualify plain identifiers with the base table; leave SQL expressions\n // and `*` untouched. Single-object cubes (no joins) keep bare columns so\n // their generated SQL is byte-for-byte unchanged.\n const canJoin = !!cube?.joins && Object.keys(cube.joins).length > 0;\n if (canJoin && /^[A-Za-z_][A-Za-z0-9_]*$/.test(rawSql)) {\n return `\"${parentTable}\".\"${rawSql}\"`;\n }\n return rawSql;\n }\n // A dot does not by itself mean \"relationship path\". `SUM(account.amount)`\n // is one SQL EXPRESSION that happens to contain a dot, and splitting it as a\n // path produced `\"SUM(account\".\"amount)\"` plus a phantom\n // `LEFT JOIN \"SUM(account\"` — invalid SQL and a join to a table that does not\n // exist. Only qualify when every segment is a bare identifier; otherwise the\n // author wrote an expression and it is returned as-is. #4157.\n if (!IDENTIFIER_PATH.test(rawSql)) return rawSql;\n // Multi-hop (ADR-0071): the dotted path IS the join chain. Every segment but\n // the last is a relationship hop; the last is the column. The join ALIAS at\n // each hop is the full path PREFIX (`account`, then `account.owner`), which\n // encodes its own parent (the prefix minus its last segment) and FK column\n // (that segment). Register one LEFT JOIN per prefix, chaining parent→child.\n const segments = rawSql.split('.');\n const column = segments[segments.length - 1];\n const hops = segments.slice(0, -1);\n if (hops.length === 0 || !column) return rawSql;\n let parentAlias = parentTable;\n let prefix = '';\n for (const seg of hops) {\n prefix = prefix ? `${prefix}.${seg}` : seg;\n const alias = this.joinAlias(prefix);\n if (!joins.has(alias)) {\n // The joined TABLE is resolved from the Cube's `joins` map (emitted by\n // the dataset compiler, keyed by the same alias); fall back to the alias\n // as the table for legacy/same-name cubes.\n const joinTable = cube?.joins?.[alias]?.name ?? alias;\n // Only emit an explicit alias when the table differs from it; when they\n // match, `LEFT JOIN \"account\" ON …` is cleaner (and back-compat).\n const tableRef = joinTable === alias ? `\"${alias}\"` : `\"${joinTable}\" \"${alias}\"`;\n joins.set(\n alias,\n `LEFT JOIN ${tableRef} ON \"${parentAlias}\".\"${seg}\" = \"${alias}\".\"id\"`,\n );\n }\n parentAlias = alias;\n }\n return `\"${parentAlias}\".\"${column}\"`;\n }\n\n /**\n * Resolve a member reference (dimension, measure, or filter field) to its\n * cube definition.\n *\n * Accepts three naming conventions:\n * 1. `<cube>.<field>` — the canonical analytics qualifier (stripped to `<field>`).\n * 2. `<lookup>.<field>` — a relation traversal (e.g. `account.industry`).\n * First tried as the literal key, then as the underscore-flattened\n * key (`account_industry`), and finally returned as a synthetic\n * definition whose `sql` is the dotted reference so the JOIN\n * machinery can pick it up.\n * 3. `<field>` — a bare field name on the cube's table.\n */\n private lookupMember(\n cube: Cube,\n member: string,\n kind: 'dimension' | 'measure',\n ): { sql: string; type?: string } | undefined {\n const bag = kind === 'dimension' ? cube.dimensions : cube.measures;\n // Direct hit on the registered key (handles `cube.field` and exact dotted keys).\n if (bag[member]) return bag[member];\n if (member.includes('.')) {\n const [first, ...rest] = member.split('.');\n const tail = rest.join('.');\n // `<cube>.<field>` style.\n if (first === cube.name && bag[tail]) return bag[tail];\n // Plain second-segment lookup (legacy behaviour).\n if (bag[tail]) return bag[tail];\n // Underscore-flattened relation lookup (e.g. `account_industry`).\n const flat = member.replace(/\\./g, '_');\n if (bag[flat]) return bag[flat];\n // Synthetic relation traversal — let qualifyAndRegisterJoin handle it.\n if (kind === 'dimension') {\n return { sql: member, type: 'string' };\n }\n } else if (bag[member]) {\n return bag[member];\n }\n return undefined;\n }\n\n private resolveDimensionSql(\n cube: Cube,\n member: string,\n parentTable: string,\n joins: Map<string, string>,\n ): string {\n const dim = this.lookupMember(cube, member, 'dimension');\n const raw = dim ? dim.sql : (member.includes('.') ? member.split('.')[1] : member);\n return this.qualifyAndRegisterJoin(raw, parentTable, joins, cube);\n }\n\n private resolveMeasureSql(\n cube: Cube,\n member: string,\n parentTable: string,\n joins: Map<string, string>,\n ): string {\n const measure = this.lookupMember(cube, member, 'measure') as\n | { sql: string; type: string }\n | undefined;\n // `lookupMember`'s synthetic relation fallback is dimension-only, so an\n // undeclared measure name lands here — a typo, or a query naming a metric\n // this cube does not have. It used to return `COUNT(*)`: the caller asked\n // for revenue and got a row count, aliased AS \"revenue\". #4157.\n if (!measure) {\n const declared = Object.keys(cube.measures ?? {});\n // [#5716] `INVALID_FIELD` / 400, naming the member — the request's\n // `measures` entry is the only input, and #4437's gate already answers\n // exactly this code for the measure one character away (a measure whose\n // SOURCE FIELD the object lacks). Two spellings of \"your `measures` entry\n // is wrong\" must not get two wire shapes. `DATASET_INVALID` would be wrong\n // on the other face this fires on: `/analytics/query` names a cube, not a\n // dataset.\n throw invalidMemberError(\n `[native-sql-strategy] cube \"${cube.name}\" declares no measure \"${member}\"` +\n (declared.length ? ` (declared: ${declared.join(', ')})` : ' (it declares none)'),\n { member, param: 'measures', cube: cube.name },\n );\n }\n\n const col = measure.sql === '*'\n ? '*'\n : this.qualifyAndRegisterJoin(measure.sql, parentTable, joins, cube);\n\n const wrap = AGGREGATE_SQL[measure.type];\n if (wrap) return wrap(col);\n // A custom SQL expression: the measure's `sql` IS the computation, so emit\n // it unwrapped. In a grouped query the expression must itself be\n // aggregate-shaped — measures never join `GROUP BY` (only dimensions do), so\n // a scalar expression there is invalid SQL. That is the author's contract to\n // keep; silently substituting `COUNT(*)` did not keep it for them.\n if (EXPRESSION_METRIC_TYPES.has(measure.type)) return col;\n\n // [#5716] Deliberately BARE — an undeclared 500, and the one site on that\n // issue's list of nine that is NOT the author's mistake. `Metric.type` is the\n // CLOSED `AggregationMetricType` enum; `metric-type-coverage.test.ts` pins\n // that {@link AGGREGATE_SQL} ∪ {@link EXPRESSION_METRIC_TYPES} partitions it\n // exactly, `dataset-compiler` only ever writes a `SUPPORTED_AGGREGATES`\n // member into a cube, and `inferMeasure` mints six known types. So no\n // spec-valid cube can arrive here: what does is our own drift or a host\n // registering a cube object that never met `CubeSchema`. Answering the\n // CALLER 400 for that would hide a platform bug from ops alerting and tell a\n // dashboard user to fix metadata they cannot see. Same tier as\n // `dataset-compiler`'s \"non-derived measure has no aggregate\"; the reasoning\n // is written once in `dataset-refusal.ts`'s header.\n throw new Error(\n `[native-sql-strategy] measure \"${member}\" on cube \"${cube.name}\" has ` +\n `unrecognised type \"${measure.type}\" — expected an aggregate ` +\n `(${SUPPORTED_AGGREGATE_SQL_KEYS.join(', ')}) or a custom-expression type ` +\n `(${[...EXPRESSION_METRIC_TYPES].join(', ')}).`,\n );\n }\n\n private resolveFieldSql(\n cube: Cube,\n member: string,\n parentTable: string,\n joins: Map<string, string>,\n ): string {\n const dim = this.lookupMember(cube, member, 'dimension');\n if (dim) return this.qualifyAndRegisterJoin(dim.sql, parentTable, joins, cube);\n const measure = this.lookupMember(cube, member, 'measure');\n if (measure) return this.qualifyAndRegisterJoin(measure.sql, parentTable, joins, cube);\n const fieldName = member.includes('.') ? member.split('.')[1] : member;\n return fieldName;\n }\n\n /**\n * Resolve the (object, column) a filter member binds against, so its\n * comparand can be coerced to that column's on-disk storage form.\n *\n * Mirrors `resolveFieldSql`'s `sql` resolution but yields the *logical*\n * target rather than the qualified SQL:\n * - A dotted column (`account.region`, emitted for a relation traversal)\n * belongs to the JOINED object — resolve the alias → target table via the\n * cube's `joins` map (alias `account` → object `crm_account` when\n * namespaced) and take the tail as the column.\n * - Otherwise the column lives on the cube's BASE table. Use the dimension's\n * resolved `sql` (the real column, which may differ from the member name,\n * e.g. dimension `assessed` → column `assessed_at`) rather than the member.\n */\n private resolveStorageTarget(\n cube: Cube,\n member: string,\n baseTable: string,\n ): { object: string; field: string } {\n const dim = this.lookupMember(cube, member, 'dimension');\n const measure = dim ? undefined : this.lookupMember(cube, member, 'measure');\n const rawSql = dim?.sql ?? measure?.sql ?? (member.includes('.') ? member.split('.').slice(1).join('.') : member);\n\n if (rawSql.includes('.')) {\n // Multi-hop (ADR-0071): the column's owning object is the join at the\n // relationship PATH (all segments but the last); the column is the last.\n const segments = rawSql.split('.');\n const field = segments[segments.length - 1];\n const relPath = segments.slice(0, -1).join('.');\n const object = cube.joins?.[this.joinAlias(relPath)]?.name ?? relPath;\n return { object, field };\n }\n return { object: baseTable, field: rawSql };\n }\n\n /**\n * Apply the storage-form coercion for a single comparand. Prefers the\n * driver-backed `coerceTemporalFilterValue` hook (single source of truth for\n * the date/datetime storage convention — see StrategyContext); when the hook\n * is absent, or returns the value unchanged (the field is not a temporal\n * column, or the dialect stores it as a native timestamp), falls back to\n * {@link toSqlBindValue} so an unbindable JS type still reaches the driver as\n * something it can bind.\n *\n * [#5526] `value` is `unknown`, not `string`, because a leaf now carries the\n * author's comparand at its own type. Both halves of this method were already\n * `unknown`-typed for it: the hook's contract is\n * `coerceTemporalFilterValue(object, field, value: unknown)` and the fallback\n * converts only what a driver cannot bind. What CHANGED is that a string is no\n * longer re-typed on the way out — the fallback used to be\n * `coerceFilterValueForSql`, which read `'007'` as the integer `7`.\n */\n private coerceTemporal(\n ctx: StrategyContext,\n target: { object: string; field: string },\n value: unknown,\n ): unknown {\n if (typeof ctx.coerceTemporalFilterValue === 'function') {\n const coerced = ctx.coerceTemporalFilterValue(target.object, target.field, value);\n // Hook returns the value untouched for non-temporal / native-timestamp\n // columns; only short-circuit when it actually changed the value.\n if (coerced !== value) return coerced;\n }\n return toSqlBindValue(value);\n }\n\n /**\n * The column side of {@link coerceTemporal}: normalise the reference so it\n * reads in the storage form the comparand was coerced into.\n *\n * A SQLite `Field.datetime` column carries an INTEGER epoch (a `Date` write)\n * and ISO TEXT (a REST/JSON write, a `NOW()` default — including the platform's\n * own `created_at`) at the SAME time, so coercing the value alone fixes one half\n * and empties the other. That is #3912: a `dateRange: last_30_days` on\n * `created_date` read 0 with 29 rows in range. Every other column and dialect\n * gets its reference back verbatim.\n */\n private temporalColumn(\n ctx: StrategyContext,\n target: { object: string; field: string },\n col: string,\n ): string {\n if (typeof ctx.coerceTemporalFilterColumn !== 'function') return col;\n return ctx.coerceTemporalFilterColumn(target.object, target.field, col) || col;\n }\n\n /**\n * Compile a normalized filter node into a boolean SQL expression, recursing\n * through the combinators. `null` = no constraint.\n *\n * Leaves go through {@link buildFilterClause} exactly as they did when this\n * was a flat loop, so the storage-form coercion and the calendar-day\n * upper-bound rule (#3777) apply at every depth — including inside an `$or`,\n * where a second, combinator-aware implementation would have been free to\n * drift from the first.\n *\n * Parenthesisation is explicit rather than left to SQL's precedence: `AND`\n * does bind tighter than `OR`, so `a AND b OR c` happens to be right, but\n * being right by construction is what keeps a future edit from making it\n * wrong.\n *\n * # `null` is the constant TRUE, and TRUE absorbs a disjunction (#5325)\n *\n * A `null` return means \"constrains nothing\", which is the boolean TRUE — the\n * AND identity, so it drops out of an `and`, but the OR ABSORBER, so one TRUE\n * disjunct makes the whole `or` TRUE. Filtering it out of an `or` narrowed the\n * query to the surviving branches. `NOT TRUE ≡ FALSE`, so a negation whose\n * operand constrains nothing compiles to the FALSE constant rather than\n * disappearing (which added no `WHERE` and charted every row).\n *\n * # The invariant that keeps `params` aligned\n *\n * **A call that returns `null` leaves `params` exactly as it found it.** It\n * has to: a value bound with no `$n` to consume it shifts every later\n * placeholder onto the wrong value, and a filter that binds the WRONG comparand\n * is worse than one that is merely too wide (#5297). Leaves decide emptiness\n * before they bind, and the absorbing `or` — the one place a clause that HAS\n * bound is discarded — truncates back to the length it started at, so the\n * invariant holds inductively for every node kind.\n */\n private compileFilterNode(\n node: NormalizedFilterNode | null,\n cube: Cube,\n parentTable: string,\n joins: Map<string, string>,\n params: unknown[],\n ctx: StrategyContext,\n ): string | null {\n if (!node) return null;\n\n if (node.kind === 'const') {\n return node.value ? SQL_CONST_TRUE : SQL_CONST_FALSE;\n }\n\n if (node.kind === 'leaf') {\n const colExpr = this.resolveFieldSql(cube, node.member, parentTable, joins);\n // Resolve the (object, column) this member binds against so the value\n // can be coerced to the column's storage form (see buildFilterClause).\n const target = this.resolveStorageTarget(cube, node.member, parentTable);\n return this.buildFilterClause(colExpr, node.operator, node.values, params, ctx, target);\n }\n\n if (node.kind === 'not') {\n const inner = this.compileFilterNode(node.child, cube, parentTable, joins, params, ctx);\n // `NOT TRUE ≡ FALSE`. Returning `null` here is what made `{$not: {}}` emit\n // no `WHERE` at all — a filter meaning \"no rows\" that showed all of them.\n // The normalizer already folds that case into a `const` node; this arm is\n // the same identity applied to anything else that constrains nothing.\n return inner ? `NOT (${inner})` : SQL_CONST_FALSE;\n }\n\n // Everything committed before this group, so an absorbed `or` can put both\n // back exactly as they were.\n const paramBase = params.length;\n const joinBase = new Map(joins);\n const parts: string[] = [];\n for (const child of node.children) {\n const clause = this.compileFilterNode(child, cube, parentTable, joins, params, ctx);\n if (clause === null) {\n // TRUE: the AND identity, the OR absorber.\n if (node.kind !== 'or') continue;\n params.length = paramBase;\n joins.clear();\n for (const [alias, clauseSql] of joinBase) joins.set(alias, clauseSql);\n return null;\n }\n parts.push(clause);\n }\n if (parts.length === 0) return null;\n if (parts.length === 1) return parts[0];\n return `(${parts.join(node.kind === 'or' ? ' OR ' : ' AND ')})`;\n }\n\n private buildFilterClause(\n rawCol: string,\n operator: string,\n // [#5526] `unknown[]`: the author's comparands, at their own types. Every\n // conversion below is one a BOUNDARY demands — `likePattern` because\n // `filter.zod.ts` declares the LIKE comparand a `string`, `coerceTemporal`\n // because a driver cannot bind every JS type — never a guess about which\n // type a string \"really\" was.\n values: unknown[] | undefined,\n params: unknown[],\n ctx: StrategyContext,\n target: { object: string; field: string },\n ): string | null {\n const opMap: Record<string, string> = {\n equals: '=', notEquals: '!=', gt: '>', gte: '>=', lt: '<', lte: '<=',\n contains: 'LIKE', notContains: 'NOT LIKE',\n startsWith: 'LIKE', endsWith: 'LIKE',\n // [#6520] `$icontains` — `LIKE` like its neighbours; what separates it is\n // the ASCII fold applied below, not the keyword.\n icontains: 'LIKE',\n };\n /**\n * Where each string operator puts the wildcard. [#5567] The pattern itself is\n * built by the shared `likePattern`, which ESCAPES the comparand — `_` and\n * `%` are LIKE wildcards, so the old inline table quietly turned an author's\n * literal into a pattern (`$contains: '_admin'` also matched `xyadmin`).\n * `objectql-strategy.ts`'s `LIKE_SQL_OPS` carries the same table for the\n * `/analytics/sql` echo of this statement; they move together.\n */\n const likeShape: Record<string, LikeShape> = {\n contains: 'contains', notContains: 'contains',\n startsWith: 'starts', endsWith: 'ends',\n // [#6520] Same wildcard placement as `contains`; the case fold is what\n // differs, and it is applied to both sides of the comparison below.\n icontains: 'contains',\n };\n\n // Null predicates and the LIKE family read the column as stored — the former\n // is storage-independent, the latter is a substring match on the raw text —\n // so only the value comparisons take the normalised reference.\n if (operator === 'set') return `${rawCol} IS NOT NULL`;\n if (operator === 'notSet') return `${rawCol} IS NULL`;\n\n if (operator === 'in' || operator === 'notIn') {\n if (!values || values.length === 0) return null;\n // Dates can legitimately appear in an `in`/`notIn` set (e.g. a multi-day\n // KPI), so coerce each element to the column's storage form too — same\n // SQLite epoch-vs-text root cause as the scalar operators below.\n const placeholders = values.map(v => { params.push(this.coerceTemporal(ctx, target, v)); return `$${params.length}`; }).join(', ');\n return `${this.temporalColumn(ctx, target, rawCol)} ${operator === 'in' ? 'IN' : 'NOT IN'} (${placeholders})`;\n }\n\n const sqlOp = opMap[operator];\n if (!sqlOp || !values || values.length === 0) return null;\n\n // The LIKE family reads the column as stored — a substring/prefix/suffix\n // match is on the raw text — so it keeps the un-normalised reference.\n const shape = likeShape[operator];\n if (shape) {\n // [#5567] Escaped pattern AND an explicit `ESCAPE`, bound together: the\n // escaping alone would search for a literal backslash on SQLite (no\n // default escape character there), the clause alone would change nothing.\n params.push(likePattern(shape, values[0]));\n const patternRef = `$${params.length}`;\n params.push(LIKE_ESCAPE_CHAR);\n // [#6520] `$icontains` folds ASCII case on BOTH sides. Only this operator\n // folds: the rest of the family is case-EXACT by ruling (#4706 Q2 = A),\n // and `objectql-strategy.ts`'s echo of this statement carries the same\n // `fold` flag on the same single row so the two keep describing one query.\n if (operator === 'icontains') {\n return `${asciiLowerSqlExpr(rawCol)} ${sqlOp} ${asciiLowerSqlExpr(patternRef)} ESCAPE $${params.length}`;\n }\n return `${rawCol} ${sqlOp} ${patternRef} ESCAPE $${params.length}`;\n }\n\n // A bare-day `lte` bound means \"through that whole day\" (#3777): compile\n // half-open (`< day+1`) so a datetime column keeps the final day's rows.\n // Equivalent to `<=` for a `date` column, so no column-type lookup needed.\n if (operator === 'lte') {\n const nextDay = nextUtcCalendarDay(values[0]);\n if (nextDay != null) {\n params.push(this.coerceTemporal(ctx, target, nextDay));\n return `${this.temporalColumn(ctx, target, rawCol)} < $${params.length}`;\n }\n }\n\n // Coerce so booleans/numbers bind as their native SQL types AND so a\n // relative-date / ISO-string comparand on a SQLite `Field.datetime`\n // column is converted to its INTEGER epoch storage form. Without this a\n // dashboard filter like `assessed_at >= '2025-06-18'` compiles to a\n // TEXT-vs-INTEGER affinity compare that is always false → \"No rows\",\n // even though the rows exist (the confirmed time-series chart bug).\n params.push(this.coerceTemporal(ctx, target, values[0]));\n return `${this.temporalColumn(ctx, target, rawCol)} ${sqlOp} $${params.length}`;\n }\n\n private extractObjectName(cube: Cube): string {\n return cube.sql.trim();\n }\n\n private buildFieldMeta(query: AnalyticsQuery, cube: Cube): Array<{ name: string; type: string }> {\n const fields: Array<{ name: string; type: string }> = [];\n if (query.dimensions) {\n for (const dim of query.dimensions) {\n const d = this.lookupMember(cube, dim, 'dimension');\n fields.push({ name: dim, type: d?.type || 'string' });\n }\n }\n if (query.measures) {\n for (const m of query.measures) {\n fields.push({ name: m, type: 'number' });\n }\n }\n return fields;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { AnalyticsQuery, AnalyticsResult } from '@objectstack/spec/contracts';\nimport type { Cube } from '@objectstack/spec/data';\n// [#8220] The read-scope provenance mark: `withReadScope` below is one of the\n// two merge boundaries that stamp it.\nimport { markFilterSubtreeProvenance } from '@objectstack/spec/data';\nimport type { AnalyticsStrategy, StrategyContext } from './types.js';\nimport {\n invalidFilterError,\n lowerAnalyticsWhere,\n normalizeAnalyticsFilterTree,\n collectFilterLeaves,\n SQL_CONST_FALSE,\n SQL_CONST_TRUE,\n type NormalizedFilterNode,\n} from './filter-normalizer.js';\nimport { findCrossFieldComparand, isFieldReference } from '../comparand-shape.js';\nimport { compileScopedFilterToSql } from '../read-scope-sql.js';\nimport { invalidMemberError } from '../dataset-refusal.js';\nimport { likePattern, LIKE_ESCAPE_CHAR, asciiLowerSqlExpr, type LikeShape } from '../like-pattern.js';\nimport { nextUtcCalendarDay } from '@objectstack/core';\nimport {\n rebucketCrossObject,\n RECOMBINABLE_METHODS,\n type CrossObjectDim,\n type MeasureRecombine,\n type RecombinableMethod,\n} from './cross-object-rebucket.js';\n\n/** Scalar analytics operators → their SQL spelling (display SQL only). */\nconst SCALAR_SQL_OPS: Record<string, string> = {\n equals: '=', notEquals: '!=', gt: '>', gte: '>=', lt: '<', lte: '<=',\n};\n\n/**\n * The LIKE family: SQL spelling + where each one puts the wildcard.\n *\n * Deliberately the same pair of tables `NativeSQLStrategy.buildFilterClause`\n * carries (`opMap` / `likeShape`), because this file renders a description of\n * the statement THAT compiler produces. Keeping them as one table here is the\n * point of #5333: `startsWith` / `endsWith` were in neither the branch above nor\n * `SCALAR_SQL_OPS`, so they fell to the unmapped exit and the predicate vanished\n * from the echo while the query it documents ran `LIKE 'w%'`.\n *\n * [#5567] The pattern comes from the shared `likePattern`, which ESCAPES the\n * comparand, and the renderer binds an explicit `ESCAPE` alongside it. That is\n * not cosmetic for an echo: the execution this file describes goes through the\n * engine to `driver-sql`, whose `applyLike` has always escaped and bound\n * `ESCAPE`. Rendering the raw comparand meant the echoed statement was WIDER\n * than the query it claims to reproduce whenever the comparand carried a `_` or\n * `%` — the #3601 / #3602 / #3650 failure this render block exists to prevent.\n */\nconst LIKE_SQL_OPS: Record<string, { sql: string; shape: LikeShape; fold?: boolean }> = {\n contains: { sql: 'LIKE', shape: 'contains' },\n notContains: { sql: 'NOT LIKE', shape: 'contains' },\n startsWith: { sql: 'LIKE', shape: 'starts' },\n endsWith: { sql: 'LIKE', shape: 'ends' },\n // [#6520] `$icontains`: the same escaped pattern and bound `ESCAPE` as its\n // four case-EXACT neighbours, with `fold` adding the ASCII-only case fold to\n // both sides of the comparison. The flag is on this row alone — the family\n // above it is case-sensitive by ruling (#4706 Q2 = A).\n icontains: { sql: 'LIKE', shape: 'contains', fold: true },\n};\n\n/** One cross-object grouping dimension planned for FK-expand (#3654). */\ninterface CrossObjectPlanDim {\n /** The caller's dimension name (output key), e.g. `region`. */\n outputName: string;\n /** The base lookup FK column to group the base aggregate by, e.g. `account`. */\n fkField: string;\n /** The related object's attribute to resolve the FK to, e.g. `region`. */\n attr: string;\n /** The related object name (join target), e.g. `crm_account`. */\n refObject: string;\n}\n\ninterface CrossObjectPlan {\n crossDims: CrossObjectPlanDim[];\n}\n\n/**\n * ObjectQLStrategy — Priority 2\n *\n * Translates an analytics query into an ObjectQL `engine.aggregate()` call.\n * This path works with any driver that supports the ObjectQL aggregate AST\n * (Postgres, Mongo, SQLite, etc.) without requiring raw SQL access.\n */\nexport class ObjectQLStrategy implements AnalyticsStrategy {\n readonly name = 'ObjectQLStrategy';\n readonly priority = 20;\n\n canHandle(query: AnalyticsQuery, ctx: StrategyContext): boolean {\n if (!query.cube) return false;\n const caps = ctx.queryCapabilities(query.cube);\n return caps.objectqlAggregate && typeof ctx.executeAggregate === 'function';\n }\n\n async execute(query: AnalyticsQuery, ctx: StrategyContext): Promise<AnalyticsResult> {\n const cube = ctx.getCube(query.cube!)!;\n const objectName = this.extractObjectName(cube);\n\n // Build groupBy from dimensions, honouring `timeDimensions` granularity.\n // A date dimension with a granularity becomes a STRUCTURED groupBy item\n // `{ field, dateGranularity }` — which `engine.aggregate()` buckets (driver\n // date_trunc or in-memory). Without this the ObjectQL path grouped raw\n // timestamps (one bucket per row) and date-bucketed dataset widgets never\n // matched their legacy `categoryGranularity` counterpart.\n type GroupByItem = string | { field: string; dateGranularity: string };\n const granByDim = new Map<string, string>();\n for (const td of query.timeDimensions ?? []) {\n if (td.granularity) granByDim.set(td.dimension, td.granularity);\n }\n const groupBy: GroupByItem[] = [];\n if (query.dimensions && query.dimensions.length > 0) {\n for (const dim of query.dimensions) {\n const field = this.resolveFieldName(cube, dim, 'dimension');\n const gran = granByDim.get(dim);\n groupBy.push(gran ? { field, dateGranularity: gran } : field);\n granByDim.delete(dim);\n }\n }\n // Time dimensions not also listed in `dimensions` still bucket + group.\n for (const [dim, gran] of granByDim) {\n groupBy.push({ field: this.resolveFieldName(cube, dim, 'dimension'), dateGranularity: gran });\n }\n\n // Build aggregations from measures\n const aggregations: Array<{ field: string; method: string; alias: string }> = [];\n if (query.measures && query.measures.length > 0) {\n for (const measure of query.measures) {\n const { field, method } = this.resolveMeasureAggregation(cube, measure);\n aggregations.push({ field, method, alias: measure });\n }\n }\n\n // Build the engine filter. Every predicate — the caller's `where` and the\n // time-dimension windows alike — is contributed through\n // `mergeFilterOperand`, because one field routinely carries MULTIPLE\n // operators (a range `{$gte, $lte}` on `close_date`) and a plain assignment\n // would keep only the last.\n const filter: Record<string, unknown> = {};\n // Operands that cannot merge into their field's entry without one silently\n // replacing the other; ANDed in below so the engine intersects them.\n const conjuncts: Record<string, unknown>[] = [];\n this.applyFilterNode(normalizeAnalyticsFilterTree(query), cube, filter, conjuncts);\n // #3650 — and the time-dimension WINDOWS, through the SAME merge, so a\n // `dateRange` and a caller `where` bound on one field compose instead of\n // clobbering each other.\n for (const { field, bounds } of this.dateRangeBounds(cube, query)) {\n const extra = this.mergeFilterOperand(filter, field, bounds);\n if (extra) conjuncts.push(extra);\n }\n if (conjuncts.length > 0) {\n filter.$and = [...(Array.isArray(filter.$and) ? filter.$and : []), ...conjuncts];\n }\n\n // #3654 — classify cross-object references. A cross-object DIMENSION within\n // the supported envelope is served by an FK-expand (`executeCrossObject`);\n // everything the engine cannot serve (cross-object measures/filters,\n // multi-hop, non-recombinable measures) is REJECTED by `planCrossObject` —\n // the engine has no join, and a silent mis-bucket is worse than a loud\n // error. `null` ⇒ the query is base-only and takes the direct path below.\n const plan = this.planCrossObject(cube, query, filter);\n if (plan) {\n return this.executeCrossObject(cube, query, aggregations, filter, plan, ctx);\n }\n\n // ADR-0021 D-C — the base object's read scope (tenant + RLS) MUST be ANDed\n // in before the query leaves the strategy (#3597). A base-only query has a\n // single object in play, so one base-object scope is sufficient here.\n const rows = await ctx.executeAggregate!(objectName, {\n // Structured groupBy items ({field, dateGranularity}) pass through the\n // executeAggregate bridge to engine.aggregate, which buckets them. The\n // contract types groupBy as string[]; the cast carries the richer shape.\n groupBy: groupBy.length > 0 ? (groupBy as unknown as string[]) : undefined,\n aggregations: aggregations.length > 0 ? aggregations : undefined,\n filter: this.withReadScope(objectName, filter, ctx),\n // ADR-0053 Phase 2 (D2): forward the reference tz so date buckets resolve\n // on that zone's calendar days. A non-UTC zone makes the engine bucket\n // in-memory (uniform across drivers); UTC/unset keeps the DB fast path.\n timezone: query.timezone,\n // ADR-0021 D-C (#3602): the second belt. `withReadScope` above is this\n // layer's own scoping; handing the engine the context makes ITS middleware\n // inject RLS too, so a future strategy that forgets `withReadScope` still\n // cannot read across tenants. Without it the operation reaches the engine\n // principal-less and plugin-security falls open — the #3597 shape.\n context: ctx.context,\n });\n\n // Remap short field names back to cube-qualified names. Driven by\n // `projectedDimensions`, so a `timeDimensions`-only bucket — grouped by\n // just above, and therefore present in `row` — reaches the caller instead\n // of being silently dropped (#4033).\n const mappedRows = rows.map(row => {\n const mapped: Record<string, unknown> = {};\n for (const dim of this.projectedDimensions(query)) {\n const shortName = this.resolveFieldName(cube, dim, 'dimension');\n if (shortName in row) mapped[dim] = row[shortName];\n }\n if (query.measures) {\n for (const m of query.measures) {\n // Alias was set to the full measure name\n if (m in row) mapped[m] = row[m];\n }\n }\n return mapped;\n });\n\n const fields = this.buildFieldMeta(query, cube);\n // Echo a representative SQL alongside the rows (#3588). `NativeSQLStrategy`\n // returns the statement it actually ran, and dataset responses surface that\n // string — it is how an author checks what their widget compiled to. This\n // path builds an AST, so it had nothing to echo, and the `sql` field simply\n // vanished from the response whenever a query was date-bucketed (native SQL\n // declines granularity, handing those queries here). An author reading the\n // response then couldn't tell \"bucketing is not implemented\" from \"this\n // strategy doesn't report\". Best-effort: rendering is a debugging aid and\n // must never fail a query that already ran.\n let sql: string | undefined;\n try {\n sql = (await this.generateSql(query, ctx)).sql;\n } catch {\n sql = undefined;\n }\n return sql ? { rows: mappedRows, fields, sql } : { rows: mappedRows, fields };\n }\n\n /**\n * Render a REPRESENTATIVE SQL string for an ObjectQL aggregate query.\n *\n * This path executes through `engine.aggregate()`, not raw SQL, so the string\n * is documentation rather than the literal statement — but it must be an\n * honest account of what the query does, because dataset responses echo it\n * and authors read it to verify their widget options landed (#3588). It\n * therefore renders date bucketing (`date_trunc`), the WHERE predicate,\n * ordering, and the row window.\n *\n * Filter VALUES are rendered as `$n` placeholders and returned in `params`,\n * never inlined: the echoed statement travels to the browser, and a filter\n * comparand can carry tenant data.\n */\n async generateSql(query: AnalyticsQuery, ctx: StrategyContext): Promise<{ sql: string; params: unknown[] }> {\n const cube = ctx.getCube(query.cube!);\n if (!cube) {\n throw new Error(`Cube not found: ${query.cube}`);\n }\n\n // [#7598, maintainer ruling 2026-08-12] The echo DECLINES a cross-field\n // comparison — 「`/analytics/sql` 的 echo 同样 decline(一致的响亮答案,\n // 不半渲染)」.\n //\n // This renderer describes an execution it does not perform, and there is no\n // honest description of a cross-field comparison available to it. The\n // reference reaches `engine.aggregate` intact and `driver-sql` compiles it\n // into a TOTAL column-to-column predicate — several repetitions of both\n // column expressions, so the answer matches the memory evaluator across\n // NULLs. What this file's `buildFilterClauseSql` can render is `amount >\n // $1` with the reference OBJECT in `params`: not a simplification of that\n // predicate but a different one, comparing a column against a value no row\n // can hold. Rendering it would hand a debugger SQL that reproduces NONE of\n // the rows the query returned — the #3601 / #3602 / #3650 failure this\n // whole render block exists to prevent, in its worst direction.\n //\n // Note what this does NOT affect: `execute()` calls `generateSql` inside a\n // `try`/`catch` precisely because the echo is a debugging aid that must\n // never fail a query that already ran, so `/analytics/query` still serves\n // these queries and returns rows — the response simply carries no `sql`\n // string. Only the dry-run face (`/analytics/sql`) refuses, which is the\n // \"one consistent, loud answer\" the ruling asked for.\n //\n // The READ SCOPE half needs no arm of its own: `compileScopedFilterToSql`\n // below still refuses a reference in its own fail-closed envelope\n // (`READ_SCOPE_COMPILE_FAILED` / 500, #5367 ruling kept verbatim by Q2 = A),\n // and that refusal is now reached from HERE rather than from\n // `NativeSQLStrategy.applyReadScope` — see `read-scope-sql.ts`'s header.\n const crossField = findCrossFieldComparand(this.loweredWhere(query));\n if (crossField) {\n throw invalidFilterError(\n `[analytics] cannot render display SQL for the field reference ` +\n `{ \"$field\": \"${crossField.ref}\" } under \"${crossField.op}\" on \"${crossField.field}\". ` +\n `The query itself is SERVED — \\`NativeSQLStrategy.canHandle\\` declines a cross-field ` +\n `comparison so it routes to the ObjectQL engine path, where driver-sql compiles it into a ` +\n `column-to-column predicate written TOTAL across NULLs and enforces the #5222 rulings ` +\n `(#7598, maintainer ruling 2026-08-12). This renderer has no faithful rendering of that ` +\n `predicate: what it can emit is a comparison against the reference object as a bound VALUE, ` +\n `which reproduces none of the rows the query returns. Refusing rather than half-rendering — ` +\n `an echo that contradicts execution is worse than no echo (#3601 / #3602 / #3650). Run the ` +\n `query itself (/analytics/query) to get its rows.`,\n );\n }\n\n const selectParts: string[] = [];\n const groupByParts: string[] = [];\n const params: unknown[] = [];\n\n // Date-bucketed dimensions render as `date_trunc('<granularity>', col)` —\n // the SQL shape the driver's own bucketing implements — so a `month` trend\n // no longer reads as if it grouped by the raw column.\n const granByDim = new Map<string, string>();\n for (const td of query.timeDimensions ?? []) {\n if (td.granularity) granByDim.set(td.dimension, td.granularity);\n }\n const tableName = this.extractObjectName(cube);\n // #3654 — plan cross-object dims (throws for out-of-envelope, so\n // `/analytics/sql` and `execute()` accept/reject the SAME set). An in-envelope\n // cross-object dim renders as a LEFT JOIN — its logical shape; `execute()`\n // serves it via FK-expand.\n // EVERY member the filter touches, including ones nested in an `$or` —\n // the envelope check rejects cross-object filters, so a member it cannot\n // see is a filter it cannot reject.\n const plan = this.planCrossObject(cube, query, Object.fromEntries(\n collectFilterLeaves(normalizeAnalyticsFilterTree(query))\n .map((f) => [this.resolveFieldName(cube, f.member, 'any'), true]),\n ));\n const crossByDim = new Map((plan?.crossDims ?? []).map((cd) => [cd.outputName, cd]));\n const joinClauses: string[] = [];\n const dimExpr = (dim: string): string => {\n const cd = crossByDim.get(dim);\n if (cd) {\n joinClauses.push(\n `LEFT JOIN \"${cd.refObject}\" ON \"${tableName}\".\"${cd.fkField}\" = \"${cd.refObject}\".\"id\"`,\n );\n return `\"${cd.refObject}\".\"${cd.attr}\"`;\n }\n const col = this.resolveFieldName(cube, dim, 'dimension');\n const gran = granByDim.get(dim);\n return gran ? `date_trunc('${gran}', ${col})` : col;\n };\n\n if (query.dimensions) {\n for (const dim of query.dimensions) {\n const expr = dimExpr(dim);\n selectParts.push(`${expr} AS \"${dim}\"`);\n groupByParts.push(expr);\n }\n }\n // A time dimension that is bucketed but not also listed in `dimensions`\n // still groups (see `execute`), so it belongs in the rendered GROUP BY too.\n for (const [dim] of granByDim) {\n if (query.dimensions?.includes(dim)) continue;\n const expr = dimExpr(dim);\n selectParts.push(`${expr} AS \"${dim}\"`);\n groupByParts.push(expr);\n }\n if (query.measures) {\n for (const m of query.measures) {\n const { field, method } = this.resolveMeasureAggregation(cube, m);\n const aggSql = method === 'count'\n ? 'COUNT(*)'\n : method === 'count_distinct'\n ? `COUNT(DISTINCT ${field})`\n : `${method.toUpperCase()}(${field})`;\n selectParts.push(`${aggSql} AS \"${m}\"`);\n }\n }\n\n // ADR-0021 D-C (#3602) — render the READ SCOPE too, not just the caller's\n // own filters (#3652 added those). Without it this string still reads as an\n // unscoped table scan while the real aggregate is scoped (#3601), so anyone\n // debugging a \"why is this row missing\" gets SQL that cannot reproduce the\n // result. Nothing leaks — the string is never executed, and scope VALUES\n // stay in `params`, which `execute()`'s echo discards — but a rendering\n // that contradicts execution is worse than no rendering.\n //\n // The cross-object guard runs here for the same reason: this must not\n // render SQL for a query `execute()` would reject outright (#3654).\n //\n // Faithfulness cuts both ways: the time-dimension WINDOWS render too, from\n // the same `dateRangeBounds` lowering `execute()` sends to the engine\n // (#3650). This comment used to explain why a BETWEEN was deliberately\n // absent — because `execute()` dropped the window and rendering one would\n // have invented a predicate. Now that it applies the window, omitting it\n // here would be the lie in the other direction.\n // (The cross-object envelope was already enforced by `planCrossObject` above,\n // so `/analytics/sql` rejects the same out-of-envelope set `execute()` does.)\n\n const whereParts: string[] = [];\n // Recursive, so the echoed statement carries the same disjunctions the\n // engine filter does — the echo exists to REPRODUCE execution, and an\n // `$or` rendered as a conjunction (or dropped) is exactly the lie this\n // block's comment above warns about, in the other direction.\n const filterClause = this.renderFilterNodeSql(\n normalizeAnalyticsFilterTree(query),\n cube,\n params,\n );\n if (filterClause) whereParts.push(filterClause);\n // Bounds bind as `$n` placeholders like every other comparand: this string\n // travels to the browser, and a window can carry tenant-derived dates.\n // A bare-day upper bound renders half-open (`< day+1`) because that is\n // what `execute()`'s driver actually runs for it on a datetime column\n // (#3777) — rendering the BETWEEN would hand a debugger SQL that drops\n // the final day's rows and cannot reproduce the result.\n for (const { field, bounds } of this.dateRangeBounds(cube, query)) {\n const nextDay = nextUtcCalendarDay(bounds.$lte);\n params.push(bounds.$gte, nextDay ?? bounds.$lte);\n whereParts.push(\n `(${field} >= $${params.length - 1} AND ${field} ${nextDay ? '<' : '<='} $${params.length})`,\n );\n }\n // Read scope last, so it reads as the outermost constraint. Compiled by the\n // same fail-closed compiler `NativeSQLStrategy` uses — it throws rather than\n // drop a predicate, which is the correct posture even for a display string:\n // silently omitting the scope is exactly the misleading output being fixed.\n const scope = ctx.getReadScope?.(tableName);\n if (scope != null) {\n const { sql: scopeSql, params: scopeParams } = compileScopedFilterToSql(scope, tableName);\n if (scopeSql) {\n let i = 0;\n // `compileScopedFilterToSql` emits `?`; renumber into this builder's $N.\n const rendered = scopeSql.replace(/\\?/g, () => {\n params.push(scopeParams[i++]);\n return `$${params.length}`;\n });\n whereParts.push(`(${rendered})`);\n }\n }\n\n let sql = `SELECT ${selectParts.join(', ')} FROM \"${tableName}\"`;\n if (joinClauses.length > 0) sql += ' ' + joinClauses.join(' ');\n if (whereParts.length > 0) {\n sql += ` WHERE ${whereParts.join(' AND ')}`;\n }\n if (groupByParts.length > 0) {\n sql += ` GROUP BY ${groupByParts.join(', ')}`;\n }\n if (query.order && Object.keys(query.order).length > 0) {\n const orderClauses = Object.entries(query.order).map(([f, d]) => `\"${f}\" ${d.toUpperCase()}`);\n sql += ` ORDER BY ${orderClauses.join(', ')}`;\n }\n if (query.limit != null) sql += ` LIMIT ${query.limit}`;\n if (query.offset != null) sql += ` OFFSET ${query.offset}`;\n\n return { sql, params };\n }\n\n // ── Helpers ──────────────────────────────────────────────────────\n\n /**\n * ADR-0021 D-C (#3597) — AND the object's read scope (tenant + RLS) into the\n * filter handed to `engine.aggregate`.\n *\n * This path used to drop the scope entirely, and the engine could not make up\n * for it: the aggregate bridge passes no `ExecutionContext`, so the security\n * middleware's principal-less fall-open skipped its own RLS injection. Both\n * belts were off at once — an authenticated caller received aggregates\n * computed over EVERY tenant's rows.\n *\n * Composed with `$and`, never by key merge: the query's own filter and the\n * scope can name the SAME field (e.g. a dashboard filtering `organization_id`),\n * and a spread would let caller input silently overwrite the security\n * predicate. `$and` makes that structurally impossible.\n */\n private withReadScope(\n objectName: string,\n filter: Record<string, unknown>,\n ctx: StrategyContext,\n ): Record<string, unknown> | undefined {\n // [#8220, A of #7929] This is the second read-scope MERGE BOUNDARY (the\n // first is plugin-security's CRUD injection), so the provenance mark is\n // stamped here: the scope is `'policy'` — a cross-field refusal from\n // inside it keeps the #7929 redaction — and the strategy-built user filter\n // is `'author'`: every name in it came from the caller's own query\n // (dimensions, measures, `where`, time windows) through this class's own\n // compilation, which is exactly the vouch the mark declares. Unmarked\n // content anywhere else stays withheld downstream, by the mark's declared\n // fail direction.\n const userFilter =\n Object.keys(filter).length > 0 ? markFilterSubtreeProvenance(filter, 'author') : undefined;\n if (typeof ctx.getReadScope !== 'function') return userFilter;\n const scope = ctx.getReadScope(objectName);\n if (scope === undefined || scope === null) return userFilter;\n const scopeFilter = markFilterSubtreeProvenance(scope as Record<string, unknown>, 'policy');\n if (!userFilter) return scopeFilter;\n return { $and: [userFilter, scopeFilter] };\n }\n\n /** Is `field` a resolved cross-object (relationship-traversal) reference? */\n private isCrossObjectField(cube: Cube, field: string, baseObject: string): boolean {\n if (!field.includes('.')) return false;\n const alias = field.split('.')[0];\n const joinedObject = cube.joins?.[alias]?.name ?? alias;\n return joinedObject !== baseObject;\n }\n\n /**\n * Plan how to serve cross-object references on this join-less path (#3654).\n *\n * `engine.aggregate()` cannot join. A cross-object DIMENSION within a\n * supported envelope is served by an FK-expand (`executeCrossObject`): group\n * the base aggregate on the lookup FK, resolve the FK to the related attribute\n * with a SCOPED read, re-bucket in memory. Returns `null` for a base-only\n * query (direct path), a plan for an in-envelope cross-object query.\n *\n * THROWS for anything outside the envelope — a cross-object MEASURE or FILTER\n * (needs a real join to evaluate), a MULTI-HOP dimension (`a.b.c`), or a\n * non-recombinable measure (`avg`/`count_distinct`, whose sub-bucket values\n * cannot be merged). A loud error beats the silent mis-bucket #3654 kills.\n * `generateSql()` calls this too, so the preview accepts/rejects the same set.\n *\n * [#5716] All four refusals below are `invalidMemberError` — `INVALID_FIELD` /\n * 400, naming the member — and the MESSAGES are unchanged (they are good\n * diagnostics, and #5923's tests read them). Each is decided by two caller-side\n * facts and nothing else: a member the query named, and whether that member\n * resolves across a join. Neither is an internal invariant — a cube where the\n * member exists and a driver that could serve it are both perfectly ordinary,\n * which is exactly what the \"run this on a native-SQL driver\" half of each\n * message says. They are member-level rather than dataset-level (hence not\n * `datasetInvalidError`) because the fix is always to change or drop ONE named\n * member, and because they fire on `/analytics/query` where no dataset exists.\n *\n * Detection is on RESOLVED field names, so a dotted dimension the cube\n * flattens to a real column is treated as base, not cross-object.\n */\n private planCrossObject(\n cube: Cube,\n query: AnalyticsQuery,\n filter: Record<string, unknown>,\n ): CrossObjectPlan | null {\n const baseObject = this.extractObjectName(cube);\n\n // A date bucket over a related object's field is not supported. Checked\n // FIRST: since #3650 a `dateRange` also lands in `filter`, so a cross-object\n // time dimension would otherwise be reported as a \"cross-object filter\" —\n // true of the lowered predicate, but not what the author wrote.\n for (const td of query.timeDimensions ?? []) {\n const field = this.resolveFieldName(cube, td.dimension, 'dimension');\n if (this.isCrossObjectField(cube, field, baseObject)) {\n throw invalidMemberError(\n `[Analytics] ObjectQLStrategy cannot bucket a cross-object time dimension (\"${field}\").`,\n { member: td.dimension, param: 'timeDimensions', cube: cube.name },\n );\n }\n }\n\n // A cross-object MEASURE or FILTER can only be evaluated with a real join.\n // [#5716] `member` is the entry AS THE REQUEST SPELLED IT (`revenue`), which\n // is what a caller can act on; `field` is what it RESOLVED to\n // (`account.balance`), which is what the message explains the refusal with.\n // The measure's request spelling used to be dropped here — the map kept only\n // the resolved field — so the envelope had nothing to name.\n const nonDim = [\n ...(query.measures ?? []).map((m) => ({\n where: 'measure', member: m, field: this.resolveMeasureAggregation(cube, m).field,\n })),\n ...Object.keys(filter).map((f) => ({ where: 'filter', member: f, field: f })),\n ].filter((r) => this.isCrossObjectField(cube, r.field, baseObject));\n if (nonDim.length > 0) {\n throw invalidMemberError(\n `[Analytics] ObjectQLStrategy cannot evaluate a cross-object ${nonDim[0].where} ` +\n `(\"${nonDim[0].field}\") — the engine cannot join in an aggregate. Run this ` +\n `query on a native-SQL driver, or remove the cross-object ${nonDim[0].where}.`,\n {\n member: nonDim[0].member,\n // The two kinds share one throw, so the request key follows the kind\n // rather than being guessed by the reader of the message.\n param: nonDim[0].where === 'measure' ? 'measures' : 'where',\n cube: cube.name,\n },\n );\n }\n\n // Collect cross-object DIMENSIONS (single-hop only).\n const crossDims: CrossObjectPlanDim[] = [];\n for (const dim of query.dimensions ?? []) {\n const field = this.resolveFieldName(cube, dim, 'dimension');\n if (!this.isCrossObjectField(cube, field, baseObject)) continue;\n const [alias, ...rest] = field.split('.');\n const attr = rest.join('.');\n if (attr.includes('.')) {\n throw invalidMemberError(\n `[Analytics] ObjectQLStrategy supports only single-hop cross-object ` +\n `dimensions; \"${field}\" traverses more than one relationship.`,\n { member: dim, param: 'dimensions', cube: cube.name },\n );\n }\n crossDims.push({ outputName: dim, fkField: alias, attr, refObject: cube.joins?.[alias]?.name ?? alias });\n }\n\n if (crossDims.length === 0) return null;\n\n // Every measure must re-combine across the intermediate FK sub-buckets.\n for (const m of query.measures ?? []) {\n const { method } = this.resolveMeasureAggregation(cube, m);\n if (!RECOMBINABLE_METHODS.has(method)) {\n throw invalidMemberError(\n `[Analytics] ObjectQLStrategy cannot group by a cross-object dimension ` +\n `with a \"${method}\" measure (\"${m}\") — its value cannot be recombined ` +\n `across the intermediate FK grouping. Use sum/count/min/max, or run on ` +\n `a native-SQL driver.`,\n { member: m, param: 'measures', cube: cube.name },\n );\n }\n }\n\n return { crossDims };\n }\n\n /**\n * Serve a cross-object-dimension query by FK-expand (#3654). The pure\n * re-bucketing step lives in `cross-object-rebucket.ts`.\n */\n private async executeCrossObject(\n cube: Cube,\n query: AnalyticsQuery,\n aggregations: Array<{ field: string; method: string; alias: string }>,\n filter: Record<string, unknown>,\n plan: CrossObjectPlan,\n ctx: StrategyContext,\n ): Promise<AnalyticsResult> {\n const baseObject = this.extractObjectName(cube);\n const crossByDim = new Map(plan.crossDims.map((cd) => [cd.outputName, cd]));\n\n // Rewrite group-by: a cross-object dim becomes its base FK column; base and\n // time dims pass through. `baseDimFields` are the group keys carried into\n // the re-bucket verbatim (the FK columns are replaced by resolved attrs).\n type GroupByItem = string | { field: string; dateGranularity: string };\n const granByDim = new Map<string, string>();\n for (const td of query.timeDimensions ?? []) {\n if (td.granularity) granByDim.set(td.dimension, td.granularity);\n }\n const groupBy: GroupByItem[] = [];\n const baseDimFields: string[] = [];\n for (const dim of query.dimensions ?? []) {\n const cd = crossByDim.get(dim);\n if (cd) {\n groupBy.push(cd.fkField);\n continue;\n }\n const field = this.resolveFieldName(cube, dim, 'dimension');\n const gran = granByDim.get(dim);\n groupBy.push(gran ? { field, dateGranularity: gran } : field);\n baseDimFields.push(field);\n granByDim.delete(dim);\n }\n for (const [dim, gran] of granByDim) {\n const field = this.resolveFieldName(cube, dim, 'dimension');\n groupBy.push({ field, dateGranularity: gran });\n baseDimFields.push(field);\n }\n\n // Base aggregate, grouped by the FK, scoped to the base object. Threads the\n // ExecutionContext for the engine-side second belt too (#3602).\n const baseRows = await ctx.executeAggregate!(baseObject, {\n groupBy: groupBy.length > 0 ? (groupBy as unknown as string[]) : undefined,\n aggregations: aggregations.length > 0 ? aggregations : undefined,\n filter: this.withReadScope(baseObject, filter, ctx),\n timezone: query.timezone,\n context: ctx.context,\n });\n\n // Resolve each cross-object dim's FK → attribute, SCOPED to the referenced\n // object: a related record the caller cannot read never yields its\n // attribute, so it buckets as RESTRICTED (no leak; ADR-0021 D-C / #3602).\n const resolvedDims: CrossObjectDim[] = [];\n for (const cd of plan.crossDims) {\n const fkValues = [...new Set(baseRows.map((r) => r[cd.fkField]).filter((v) => v != null))];\n const fkToAttr = await this.resolveFkAttr(cd.refObject, cd.attr, fkValues, ctx);\n resolvedDims.push({ outputName: cd.outputName, fkField: cd.fkField, fkToAttr });\n }\n\n const measures: MeasureRecombine[] = (query.measures ?? []).map((m) => ({\n alias: m,\n // planCrossObject already asserted every measure is recombinable.\n method: this.resolveMeasureAggregation(cube, m).method as RecombinableMethod,\n }));\n\n const merged = rebucketCrossObject(baseRows, baseDimFields, resolvedDims, measures);\n\n // Map resolved group keys back to the caller's dimension names.\n const mappedRows = merged.map((row) => {\n const out: Record<string, unknown> = {};\n // Same projection set as the direct path and `buildFieldMeta`\n // ({@link projectedDimensions}) — a cross-object dimension carries the\n // caller's name already, everything else is remapped from its short name.\n for (const dim of this.projectedDimensions(query)) {\n if (crossByDim.has(dim)) {\n if (dim in row) out[dim] = row[dim];\n } else {\n const field = this.resolveFieldName(cube, dim, 'dimension');\n if (field in row) out[dim] = row[field];\n }\n }\n for (const m of query.measures ?? []) {\n if (m in row) out[m] = row[m];\n }\n return out;\n });\n\n return { rows: mappedRows, fields: this.buildFieldMeta(query, cube) };\n }\n\n /**\n * Resolve `fkValues` (ids of `refObject`) to their `attr` values, applying the\n * referenced object's OWN read scope (#3654 / #3602). Reuses the aggregate\n * bridge — `group by (id, attr)` is one row per record. Ids the scope hides\n * are simply absent from the map (⇒ RESTRICTED bucket downstream).\n */\n private async resolveFkAttr(\n refObject: string,\n attr: string,\n fkValues: unknown[],\n ctx: StrategyContext,\n ): Promise<Map<unknown, unknown>> {\n const map = new Map<unknown, unknown>();\n if (fkValues.length === 0 || typeof ctx.executeAggregate !== 'function') return map;\n const idFilter: Record<string, unknown> = { id: { $in: fkValues } };\n const scope = typeof ctx.getReadScope === 'function' ? ctx.getReadScope(refObject) : null;\n // [#8220] Same boundary family as `withReadScope`: the scope arm is policy.\n // `idFilter` is this method's own plumbing, not the caller's text — it\n // stays unmarked, which withholds, and that is correct for a filter no\n // author typed.\n if (scope != null) markFilterSubtreeProvenance(scope, 'policy');\n const filter = scope != null ? { $and: [idFilter, scope] } : idFilter;\n const rows = await ctx.executeAggregate(refObject, {\n groupBy: ['id', attr],\n aggregations: [{ field: 'id', method: 'count', alias: '_c' }],\n filter,\n context: ctx.context,\n });\n for (const r of rows) {\n if (r.id != null) map.set(r.id, r[attr]);\n }\n return map;\n }\n\n /**\n * Render one normalized filter as a display SQL predicate for `generateSql`.\n *\n * Mirrors `NativeSQLStrategy.buildFilterClause`'s operator vocabulary so the\n * two previews read alike, but binds the comparand VERBATIM: the value shown is\n * the one THIS path actually hands the engine (a real boolean, not SQL's 1/0).\n *\n * [#5526] \"Verbatim\" is now literal. This used to bind through\n * `coerceFilterValueForObjectQL`, which decoded the string a `string[]` leaf\n * carried back into a type — so an echo could show `7` for a filter the author\n * wrote as `'007'`. A leaf carries the author's value at its own type, so the\n * echo needs no conversion at all to stay honest about execution. The LIKE\n * family is still the one exception, for the reason `filter.zod.ts` gives: its\n * comparand is declared a `string`, and what binds is the PATTERN.\n *\n * `null` means \"this leaf carries no predicate\" — a value-less scalar leaf,\n * which `execute()` and `NativeSQLStrategy` drop too. It does NOT mean \"I could\n * not render that operator\": #5333 was exactly that conflation, and an\n * unrenderable operator now THROWS (see the exit below).\n */\n private buildFilterClauseSql(\n col: string,\n operator: string,\n values: unknown[] | undefined,\n params: unknown[],\n ): string | null {\n if (operator === 'set') return `${col} IS NOT NULL`;\n if (operator === 'notSet') return `${col} IS NULL`;\n\n if (!values || values.length === 0) return null;\n\n if (operator === 'in' || operator === 'notIn') {\n const placeholders = values\n .map((v) => { params.push(v); return `$${params.length}`; })\n .join(', ');\n return `${col} ${operator === 'in' ? 'IN' : 'NOT IN'} (${placeholders})`;\n }\n\n // The LIKE family binds its PATTERN, which is text by construction — same\n // reason `NativeSQLStrategy` keeps the un-normalised column reference for\n // these: a prefix/suffix/substring match reads the column as stored.\n const like = LIKE_SQL_OPS[operator];\n if (like) {\n // [#5567] Escaped pattern + an explicit `ESCAPE`, matching what\n // `driver-sql`'s `applyLike` binds for the same operator — so an author\n // who copies this statement out runs the predicate that ran.\n params.push(likePattern(like.shape, values[0]));\n const patternRef = `$${params.length}`;\n params.push(LIKE_ESCAPE_CHAR);\n // [#6520] The fold, when the operator carries one, wraps BOTH sides:\n // folding only the comparand compares a folded needle against a raw column\n // and returns just the rows that were already lower-case — a wrong row set\n // that looks like a working predicate.\n const lhs = like.fold ? asciiLowerSqlExpr(col) : col;\n const rhs = like.fold ? asciiLowerSqlExpr(patternRef) : patternRef;\n return `${lhs} ${like.sql} ${rhs} ESCAPE $${params.length}`;\n }\n\n const op = SCALAR_SQL_OPS[operator];\n if (!op) {\n // [#5333] THROW rather than `return null`. `renderFilterNodeSql` reads a\n // `null` as \"this node constrains nothing\", so the old exit deleted the\n // predicate from the echoed statement — and a rendering WIDER than\n // execution is the failure this whole render block exists to prevent\n // (#3601 / #3602 / #3650): the author runs it to reproduce a result, gets\n // more rows, and concludes the filter never applied.\n //\n // It can throw because the vocabulary upstream is CLOSED: `fieldLeaves`\n // in `filter-normalizer.ts` is the only producer of leaf nodes, and it\n // refuses an operator outside `MONGO_TO_CUBE_OP` with `INVALID_FILTER` /\n // 400 before a leaf exists. So no caller-authored filter can land here —\n // an arrival means the normalizer's table gained an entry this renderer\n // has no arm for, which is our bug, not the caller's, and the one answer\n // that must never be given for it is a silently wider query. Same call\n // `convertFilter`'s `default:` arm made when it stopped reading an\n // unmapped operator as equality (#4128). Deliberately NOT\n // `invalidFilterError`'s 400 envelope: this is drift between two of our\n // own tables, not a caller-shaped mistake.\n throw new Error(\n `[analytics] ObjectQLStrategy cannot render display SQL for filter operator ` +\n `\"${operator}\" (on \"${col}\"). The analytics operator vocabulary is closed — ` +\n `filter-normalizer.ts refuses anything it cannot map — so this means a new ` +\n `operator reached the normalizer without an arm here. Add one rather than ` +\n `dropping the predicate: an echo without it describes a WIDER query than the ` +\n `one that ran (#5333).`,\n );\n }\n params.push(values[0]);\n return `${col} ${op} $${params.length}`;\n }\n\n /**\n * Resolve a member ref to a `{ sql, type? }` definition.\n *\n * Mirrors `NativeSQLStrategy.lookupMember` so the two strategies\n * accept the same naming conventions:\n * 1. `<cube>.<field>` — canonical analytics qualifier.\n * 2. `<lookup>.<field>` — relation traversal (e.g. `account.industry`).\n * Tries literal key, then underscore-flattened key, then falls\n * back to a synthetic dim whose `sql` is the dotted path so the\n * ObjectQL aggregate engine can traverse it via the lookup field.\n * 3. `<field>` — bare column on the cube's table.\n */\n private lookupMember(\n cube: Cube,\n member: string,\n kind: 'dimension' | 'measure',\n ): { sql: string; type?: string } | undefined {\n const bag = kind === 'dimension' ? cube.dimensions : cube.measures;\n if (bag[member]) return bag[member];\n if (member.includes('.')) {\n const [first, ...rest] = member.split('.');\n const tail = rest.join('.');\n if (first === cube.name && bag[tail]) return bag[tail];\n if (bag[tail]) return bag[tail];\n const flat = member.replace(/\\./g, '_');\n if (bag[flat]) return bag[flat];\n if (kind === 'dimension') return { sql: member, type: 'string' };\n } else if (bag[member]) {\n return bag[member];\n }\n return undefined;\n }\n\n private resolveFieldName(cube: Cube, member: string, kind: 'dimension' | 'measure' | 'any'): string {\n if (kind === 'dimension' || kind === 'any') {\n const dim = this.lookupMember(cube, member, 'dimension');\n if (dim) return dim.sql.replace(/^\\$/, '');\n }\n if (kind === 'measure' || kind === 'any') {\n const measure = this.lookupMember(cube, member, 'measure');\n if (measure) return measure.sql.replace(/^\\$/, '');\n }\n return member.includes('.') ? member.split('.')[1] : member;\n }\n\n private resolveMeasureAggregation(cube: Cube, measureName: string): { field: string; method: string } {\n const direct = this.lookupMember(cube, measureName, 'measure') as\n | { sql: string; type: string }\n | undefined;\n if (direct) {\n return {\n field: direct.sql.replace(/^\\$/, ''),\n method: direct.type === 'count_distinct' ? 'count_distinct' : direct.type,\n };\n }\n // Accept `${field}_${type}` aliases (e.g. 'amount_sum') for measures whose\n // canonical name is just `${field}` (e.g. measure 'amount' of type 'sum').\n // This matches the convention used by clients that build measure names\n // from (field, function) pairs (e.g. the data-objectstack adapter).\n const fieldName = measureName.includes('.') ? measureName.split('.')[1] : measureName;\n const aggTypes = ['count', 'sum', 'avg', 'min', 'max', 'count_distinct'];\n for (const type of aggTypes) {\n const suffix = `_${type}`;\n if (fieldName.endsWith(suffix)) {\n const baseField = fieldName.slice(0, -suffix.length);\n const candidate = cube.measures[baseField];\n if (candidate && candidate.type === type) {\n return {\n field: candidate.sql.replace(/^\\$/, ''),\n method: candidate.type === 'count_distinct' ? 'count_distinct' : candidate.type,\n };\n }\n }\n }\n return { field: '*', method: 'count' };\n }\n\n /**\n * AND one more operand onto `filter[field]`, merging operator objects rather\n * than overwriting them. Returns a standalone conjunct when the two cannot\n * share one entry, or `null` when the merge absorbed the operand.\n *\n * Every predicate this strategy contributes goes through here — the caller's\n * `where` and the time-dimension `dateRange` alike. Two operands on one field\n * are the normal case (`{$gte}` from a `where` plus `{$gte,$lte}` from a\n * window on `close_date`), and a plain assignment would keep only the last:\n * that is how a range used to lose a bound.\n *\n * Spreading is sound only while the operands name DIFFERENT operators. Where\n * they collide — two `$gte` bounds on one field, which a window makes routine\n * and which a `where` can already produce on its own through `$and` — the\n * spread keeps whichever came last and WIDENS the query. Same for a bare\n * equality meeting an operator object: neither can absorb the other. Those\n * are handed back for the caller to AND in separately, so the engine\n * intersects them instead of the strategy picking a winner.\n */\n /**\n * Fold a normalized filter node into the engine filter being built.\n *\n * AND-ed LEAVES merge per field through {@link mergeFilterOperand}, exactly\n * as the flat loop this replaced did — so a query without combinators still\n * produces byte-identical engine input. Anything structural (`$or`, `$not`,\n * a nested `$and` that cannot merge) becomes its own conjunct, which the\n * caller ANDs in. The engine speaks these combinators natively\n * (`FilterCondition` declares them and every driver compiles them), so this\n * path hands them over rather than lowering them.\n */\n private applyFilterNode(\n node: NormalizedFilterNode | null,\n cube: Cube,\n filter: Record<string, unknown>,\n conjuncts: Record<string, unknown>[],\n ): void {\n if (!node) return;\n\n if (node.kind === 'leaf') {\n const fieldName = this.resolveFieldName(cube, node.member, 'any');\n const extra = this.mergeFilterOperand(filter, fieldName, this.convertFilter(node.operator, node.values));\n if (extra) conjuncts.push(extra);\n return;\n }\n\n if (node.kind === 'and') {\n for (const child of node.children) this.applyFilterNode(child, cube, filter, conjuncts);\n return;\n }\n\n const rendered = this.filterNodeToCondition(node, cube);\n if (rendered) conjuncts.push(rendered);\n }\n\n /**\n * A node as a standalone `FilterCondition` the engine can consume.\n *\n * `null` = no constraint, which is the boolean TRUE — the AND identity but the\n * OR ABSORBER, so a `null` branch makes the whole disjunction unconstrained\n * instead of collapsing it to its surviving branches (#5325). FALSE is handed\n * to the engine as `{$not: {}}`, the spelling `driver-sql`, `formula` and\n * `driver-memory`'s matcher all already pin as the zero-row filter (#5134) —\n * this strategy invents no second one.\n */\n private filterNodeToCondition(\n node: NormalizedFilterNode | null,\n cube: Cube,\n ): Record<string, unknown> | null {\n if (!node) return null;\n\n if (node.kind === 'const') {\n return node.value ? null : { $not: {} };\n }\n\n if (node.kind === 'not') {\n const inner = this.filterNodeToCondition(node.child, cube);\n // `NOT TRUE ≡ FALSE` — a negation of nothing is the zero-row filter, not\n // the absence of a filter (which is what let `{$not: {}}` chart every row).\n return inner ? { $not: inner } : { $not: {} };\n }\n\n if (node.kind === 'or') {\n const branches = node.children.map((child) => this.filterNodeToCondition(child, cube));\n // One TRUE disjunct absorbs the disjunction.\n if (branches.some((c) => c === null)) return null;\n const kept = branches.filter((c): c is Record<string, unknown> => !!c);\n return kept.length > 0 ? { $or: kept } : null;\n }\n\n // `leaf` and `and` share the merge path so one field carrying several\n // operators composes here the same way it does at the top level.\n const filter: Record<string, unknown> = {};\n const conjuncts: Record<string, unknown>[] = [];\n this.applyFilterNode(node, cube, filter, conjuncts);\n if (conjuncts.length > 0) {\n filter.$and = [...(Array.isArray(filter.$and) ? filter.$and : []), ...conjuncts];\n }\n return Object.keys(filter).length > 0 ? filter : null;\n }\n\n /**\n * Render a normalized filter node as the display SQL `/analytics/sql`\n * echoes. Values still bind as `$n` placeholders — the echo travels to the\n * browser, so a comparand is never inlined.\n *\n * The boolean identities render too (#5325). This string exists to REPRODUCE\n * execution: a `{$not: {}}` filter that runs as zero rows but echoes SQL with\n * no `WHERE` hands whoever is debugging \"why is this chart empty\" a statement\n * that returns the whole table. Same reason the absorbed `$or` branch and the\n * `params` truncation below match {@link NativeSQLStrategy.compileFilterNode}\n * exactly — including the invariant that a `null` return leaves `params`\n * untouched, so no comparand is left with no placeholder to consume it.\n */\n private renderFilterNodeSql(\n node: NormalizedFilterNode | null,\n cube: Cube,\n params: unknown[],\n ): string | null {\n if (!node) return null;\n\n if (node.kind === 'const') {\n return node.value ? SQL_CONST_TRUE : SQL_CONST_FALSE;\n }\n\n if (node.kind === 'leaf') {\n return this.buildFilterClauseSql(\n this.resolveFieldName(cube, node.member, 'any'),\n node.operator,\n node.values,\n params,\n );\n }\n\n if (node.kind === 'not') {\n const inner = this.renderFilterNodeSql(node.child, cube, params);\n return inner ? `NOT (${inner})` : SQL_CONST_FALSE;\n }\n\n const paramBase = params.length;\n const parts: string[] = [];\n for (const child of node.children) {\n const clause = this.renderFilterNodeSql(child, cube, params);\n if (clause === null) {\n if (node.kind !== 'or') continue;\n params.length = paramBase;\n return null;\n }\n parts.push(clause);\n }\n if (parts.length === 0) return null;\n if (parts.length === 1) return parts[0];\n return `(${parts.join(node.kind === 'or' ? ' OR ' : ' AND ')})`;\n }\n\n private mergeFilterOperand(\n filter: Record<string, unknown>,\n field: string,\n operand: unknown,\n ): Record<string, unknown> | null {\n const existing = filter[field];\n if (existing === undefined) {\n filter[field] = operand;\n return null;\n }\n const mergeable = (v: unknown): v is Record<string, unknown> =>\n !!v && typeof v === 'object' && !Array.isArray(v);\n if (!mergeable(existing) || !mergeable(operand)) return { [field]: operand };\n if (Object.keys(operand).some((op) => op in existing)) return { [field]: operand };\n filter[field] = { ...existing, ...operand };\n return null;\n }\n\n /**\n * Lower `timeDimensions[].dateRange` into resolved-field bounds (#3650).\n *\n * `dateRange` states a WINDOW on a time dimension; it is a SIBLING of `where`,\n * never folded into it. `normalizeAnalyticsFilters` reads only `where`, so\n * this path used to drop the window on the floor — no error, just every row\n * ever recorded. Nor is that a corner case: `NativeSQLStrategy.canHandle`\n * declines any query carrying a `granularity`, so a date-bucketed trend lands\n * HERE on every driver — and \"bucketed trend\" is precisely the shape that also\n * carries a range (\"last 12 months\", \"this quarter\").\n *\n * Bounds are inclusive on both ends — logically \"from day X through day Y\".\n * The `$lte` end is left as the bare calendar day on purpose: the driver's\n * filter compiler owns the calendar-day → instant translation, compiling a\n * bare-day `$lte` on a `datetime` column into the half-open `< nextDay`\n * (#3777) while a `date` column keeps the plain `<=`. `NativeSQLStrategy`\n * performs the same half-open translation itself because it binds into raw\n * SQL, so one dashboard reads the same on every driver.\n *\n * [#5526] Bounds are forwarded at the type `dateRange` is DECLARED with —\n * `string` (`AnalyticsQuerySchema`'s `timeDimensions[].dateRange: string[]`) —\n * and nothing re-types them. They used to pass through\n * `coerceFilterValueForObjectQL`, whose TSDoc advertised that \"an epoch-ms\n * bound recovers as a number\"; that was a lenient CONSUMER rescuing a shape the\n * contract does not declare, and the same guess is what read a `'007'` filter\n * comparand as `7` (Prime Directive #12 — the producer or the spec is where an\n * epoch-ms window would have to be declared, not here). An author who wants an\n * instant window writes it as one; a declared `string` binds as a string. No\n * STORAGE coercion happens here either, deliberately: `NativeSQLStrategy` needs\n * `coerceTemporal` because it binds into raw SQL and had to learn that a\n * SQLite `Field.datetime` is an INTEGER epoch (#2034); this path goes through\n * `engine.aggregate()`, where the driver's own CRUD filter coercion applies —\n * the very coercion that already makes a `where` bound on that same column\n * work today.\n *\n * A bare-string `dateRange` degenerates to the single point `[s, s]`, matching\n * `NativeSQLStrategy`. Relative phrases (\"Last 7 days\") are NOT resolved here;\n * neither SQL path resolves them, and inventing a second interpretation on the\n * driver-independent path is how the two would drift apart again.\n *\n * An oddly-sized array (the schema types `dateRange` as a plain `string[]`)\n * takes its first two entries, a one-entry array degenerating to a point.\n * `NativeSQLStrategy` drops such a window entirely — but \"drop the window\"\n * means \"plot all of history\", which is the very failure this fixes, so the\n * fallback here errs toward the narrower query instead.\n */\n private dateRangeBounds(\n cube: Cube,\n query: AnalyticsQuery,\n ): Array<{ field: string; bounds: Record<string, unknown> }> {\n const out: Array<{ field: string; bounds: Record<string, unknown> }> = [];\n for (const td of query.timeDimensions ?? []) {\n if (!td.dateRange) continue;\n const range = Array.isArray(td.dateRange) ? td.dateRange : [td.dateRange, td.dateRange];\n const [start, end = start] = range;\n if (start == null) continue;\n out.push({\n field: this.resolveFieldName(cube, td.dimension, 'dimension'),\n bounds: { $gte: start, $lte: end },\n });\n }\n return out;\n }\n\n /**\n * One leaf as the operand the engine's `FilterCondition` expects.\n *\n * [#5526] The comparand is passed through UNCONVERTED. That is the whole of\n * this path's share of the fix: the engine compares against the value as\n * STORED, and a leaf now carries the value the author wrote, so `'007'` stays\n * `'007'`, `true` stays `true` and `7` stays `7` with nothing in between to\n * re-type them. The two `coerceFilterValueForObjectQL` calls this replaced\n * existed only to undo `stringifyForCube`, and undoing it required guessing.\n *\n * The four LIKE-family arms are the exception, and a contract one:\n * `filter.zod.ts` declares `$contains` / `$notContains` / `$startsWith` /\n * `$endsWith` as `z.string()`, so this PRODUCER must hand the engine a real\n * string — `String(…)`, the same normalisation `like-pattern.ts` applies at the\n * two SQL emitters and `driver-sql`'s `applyLike` applies at the driver, so one\n * `$contains` means one thing on every face (#5567's invariant).\n *\n * [#5234] Those four `String(…)` calls now only ever see a value that renders\n * faithfully: `fieldLeaves` refuses an object comparand on this family before a\n * leaf exists. That ordering is load-bearing rather than incidental — this arm\n * is a PRODUCER for the engine, so stringifying an object here would have\n * laundered it into `'[object Object]'` and handed a driver a perfectly\n * well-typed string. A strict driver downstream could never have seen the shape\n * it was strict about, which is why the guard sits at the door and not here.\n */\n private convertFilter(operator: string, values?: unknown[]): unknown {\n if (operator === 'set') return { $ne: null };\n if (operator === 'notSet') return null;\n if (!values || values.length === 0) return undefined;\n\n const v0 = values[0];\n // A COPY, not the leaf's own array: the `$in` / `$nin` operand below travels\n // into the filter object the engine receives, and a node of this tree is\n // never shared (see `falseNode`). The old `values.map(coerce…)` copied as a\n // side effect of converting; dropping the conversion must not drop the copy.\n const all = [...values];\n switch (operator) {\n // [#7598] IMPLICIT equality for a literal, EXPLICIT `$eq` for a field\n // reference — the branch is on the COMPARAND, not on the operator, which\n // is the same fix and the same reasoning #7597 applied to\n // `parseFilterAST`, the spec's own lowering sink.\n //\n // `{ amount: 5 }` is implicit equality and every backend reads it that\n // way. `{ amount: { $field: 'budget' } }` is NOT: it is a field-spec\n // object whose only key is `$field`, which no backend reads as an\n // equality — `driver-sql` sees an unrecognised operator key and the\n // memory evaluator sees a comparand it never resolves. So the bare return\n // was correct for four years' worth of literals and silently wrong for\n // the one comparand the 2026-08-12 ruling routes HERE on purpose: with it,\n // `{ amount: { $eq: { $field: 'budget' } } }` — the shape\n // `compileCelToFilter` emits for a field-to-field CEL rule, and the shape\n // `canHandle` now declines native SQL for — would arrive at the driver as\n // something the driver cannot read, so the capability B exists to serve\n // would fail on its single most important spelling. Its five siblings\n // (`$ne`/`$gt`/`$gte`/`$lt`/`$lte`) were never affected: they emit their\n // operator explicitly two lines down.\n case 'equals': return isFieldReference(v0) ? { $eq: v0 } : v0;\n case 'notEquals': return { $ne: v0 };\n case 'gt': return { $gt: v0 };\n case 'gte': return { $gte: v0 };\n case 'lt': return { $lt: v0 };\n case 'lte': return { $lte: v0 };\n // [#5557] `contains` was `{ $regex: values[0] }` — the comparand dropped\n // VERBATIM into a regex position while its three siblings below already\n // passed as canonical spec operators. Three things were wrong with that,\n // and none of them waits on #4706's ruling about what `$regex` should\n // mean:\n //\n // 1. `$regex` is not in `filter.zod.ts`'s `FILTER_OPERATORS`, so this\n // was a PRODUCER emitting an operator the contract does not declare\n // (Prime Directive #12 — fix the producer, not the consumers).\n // 2. `compileScopedFilterToSql` in this very package is a\n // `FilterCondition` consumer and fails closed on `$regex`, so one\n // filter tree no longer travelled between two consumers of the same\n // contract sitting in the same directory.\n // 3. On a backend that reads `$regex` as a real regex — driver-memory's\n // `memory-matcher.ts` does, deliberately, for plugin-auth's adapter\n // — an unescaped comparand changes what the author asked for:\n // `a.b` also matched `axb`, and `50% (+)` did not compile at all, so\n // the `catch { return false }` answered zero rows in silence.\n // `driver-sql` meanwhile compiles `$regex` to a substring LIKE, so\n // the same widget returned different row sets per driver.\n //\n // `MONGO_TO_CUBE_OP` maps `$contains` → `contains` and nothing else does,\n // so returning `$contains` here is the round trip of the author's own key.\n case 'contains': return { $contains: String(v0) };\n // `notContains` had no arm and fell to the `default` below, which returns\n // a BARE VALUE — i.e. `{field: 'x'}`, an equality. \"does not contain x\"\n // was compiled as \"equals x\". These three pass through as the canonical\n // spec operators every driver implements directly, so an anchored match\n // stays anchored rather than depending on regex dialect (#4128).\n case 'notContains': return { $notContains: String(v0) };\n case 'startsWith': return { $startsWith: String(v0) };\n case 'endsWith': return { $endsWith: String(v0) };\n case 'in': return { $in: all };\n case 'notIn': return { $nin: all };\n default:\n // Was `return v0` — a silent reinterpretation of the operator as an\n // equality, the write-side twin of the normalizer's dropped predicate\n // (#4128). Every operator `normalizeAnalyticsFilters` can emit is\n // handled above, so reaching here means the two drifted apart.\n throw new Error(\n `[analytics] ObjectQL strategy cannot express filter operator \"${operator}\". ` +\n `Treating it as an equality would silently query something the author did not ask for.`,\n );\n }\n }\n\n private extractObjectName(cube: Cube): string {\n return cube.sql.trim();\n }\n\n /**\n * [#7598] The query's `where`, lowered — the same input\n * `NativeSQLStrategy.canHandle` scans, so the strategy that DECLINED and the\n * echo that refuses read one shape rather than two.\n *\n * A throw from the lowering is swallowed for the same reason it is there: the\n * `where` is malformed either way and `normalizeAnalyticsFilterTree` below\n * refuses it with the message and envelope it has always had. This helper's\n * only job is finding a reference, and there is none to find in a filter that\n * does not lower.\n */\n private loweredWhere(query: AnalyticsQuery): unknown {\n try {\n return lowerAnalyticsWhere(query);\n } catch {\n return null;\n }\n }\n\n /**\n * The dimensions this query PROJECTS, in the order the result carries them:\n * every `dimensions` entry, then every granular `timeDimensions` entry that\n * is not already one of them.\n *\n * `timeDimensions` is not merely a filter carrier. An entry with a\n * `granularity` is GROUPED BY — see the `td.granularity` sites that build\n * groupBy here, in `generateSql` and in the cross-object path — so its\n * bucket is a COLUMN of the result; an entry without one only contributes a\n * `dateRange` predicate and must NOT be projected.\n *\n * Grouping, row mapping and {@link buildFieldMeta} have to agree on exactly\n * that set. When they did not, a bucketed query returned rows carrying only\n * the measures and a `fields` list that never mentioned the bucket — a trend\n * chart got N values and no x-axis (#4033) — even though the SQL had\n * selected `date_trunc(…) AS \"<dim>\"` all along. One definition, every\n * consumer.\n */\n private projectedDimensions(query: AnalyticsQuery): string[] {\n const out = [...(query.dimensions ?? [])];\n for (const td of query.timeDimensions ?? []) {\n if (td.granularity && !out.includes(td.dimension)) out.push(td.dimension);\n }\n return out;\n }\n\n private buildFieldMeta(query: AnalyticsQuery, cube: Cube): Array<{ name: string; type: string }> {\n const fields: Array<{ name: string; type: string }> = [];\n for (const dim of this.projectedDimensions(query)) {\n const d = this.lookupMember(cube, dim, 'dimension');\n fields.push({ name: dim, type: d?.type || 'string' });\n }\n if (query.measures) {\n for (const m of query.measures) {\n fields.push({ name: m, type: 'number' });\n }\n }\n return fields;\n }\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Cross-object dimension re-bucketing (#3654 capability).\n *\n * `engine.aggregate()` cannot join, so the ObjectQL path cannot group directly\n * by a related object's attribute (`account.region`). Instead the strategy:\n * 1. groups the base aggregate by the LOOKUP FK column (`account`), which the\n * engine CAN do (it is a plain base column), and\n * 2. resolves each FK id to the related attribute (`region`) with a SCOPED\n * read of the referenced object, then\n * 3. re-buckets the base aggregate by that attribute here, in memory,\n * recombining the measures.\n *\n * This module is the pure, deterministic step (3): given the base rows, the\n * FK→attribute maps, and the measures' aggregation methods, produce the rows a\n * direct cross-object grouping would have. It is unit-tested in isolation\n * because a wrong re-combination silently corrupts totals — exactly the class of\n * bug #3654 exists to kill.\n *\n * A base row whose FK does not resolve (the referenced record is hidden by the\n * referenced object's own RLS) buckets under {@link RESTRICTED_BUCKET}: its\n * measure still counts, so grand totals are preserved, but the hidden record's\n * attribute value never appears (no leak — ADR-0021 D-C, the #3602 class).\n */\n\n/** Only aggregation methods that re-combine across sub-buckets are supported. */\nexport type RecombinableMethod = 'sum' | 'count' | 'min' | 'max';\n\nexport const RECOMBINABLE_METHODS: ReadonlySet<string> = new Set<RecombinableMethod>([\n 'sum',\n 'count',\n 'min',\n 'max',\n]);\n\n/** Sentinel bucket for base rows whose referenced record the caller cannot read. */\nexport const RESTRICTED_BUCKET = '(restricted)';\n\nexport interface CrossObjectDim {\n /** Output key for the resolved attribute (the original dimension name), e.g. `region`. */\n outputName: string;\n /** The base FK column the base aggregate was grouped by, e.g. `account`. */\n fkField: string;\n /** `fkValue → attributeValue`. An FK absent from the map buckets as RESTRICTED. */\n fkToAttr: Map<unknown, unknown>;\n}\n\nexport interface MeasureRecombine {\n /** The measure's output key in each base row. */\n alias: string;\n method: RecombinableMethod;\n}\n\n/**\n * Order two measure values. A number orders as itself; a `Date` or an ISO\n * timestamp orders as its instant, so a `min`/`max` over a temporal measure\n * compares correctly instead of collapsing to `NaN` (#3797). `NaN` means \"not\n * orderable\" and the caller keeps the other side.\n */\nfunction orderableValue(v: unknown): number {\n if (v == null) return NaN;\n if (typeof v === 'number') return v;\n if (v instanceof Date) return v.getTime();\n const n = Number(v);\n if (Number.isFinite(n)) return n;\n return Date.parse(String(v));\n}\n\n/**\n * Combine two measure values under an aggregation method (either may be\n * undefined).\n *\n * `sum`/`count` are numeric by construction and stay so. `min`/`max` return the\n * winning ORIGINAL value rather than a number: the value they pick is a value\n * OF the column, so a temporal measure has to come back out in the same shape\n * the driver presented it (#3797) — coercing it to a number here would put the\n * epoch leak back one layer up, and on any dialect whose driver returns an ISO\n * string it would produce `NaN` outright.\n */\nfunction recombine(method: RecombinableMethod, acc: unknown, next: unknown): unknown {\n if (method === 'min' || method === 'max') {\n if (acc === undefined) return next ?? 0;\n const a = orderableValue(acc);\n const n = orderableValue(next);\n if (Number.isNaN(n)) return acc;\n if (Number.isNaN(a)) return next;\n const nextWins = method === 'min' ? n < a : n > a;\n return nextWins ? next : acc;\n }\n const n = Number(next ?? 0);\n return acc === undefined ? n : Number(acc) + n;\n}\n\n/**\n * Re-bucket base aggregate rows by resolved cross-object attributes.\n *\n * @param baseRows rows grouped by `baseDimFields` + every `crossDims[*].fkField`.\n * @param baseDimFields the NON-cross-object group keys carried through unchanged\n * (base columns and date buckets), keyed as in `baseRows`.\n * @param crossDims one entry per cross-object dimension (its FK→attr map).\n * @param measures measure keys + their (recombinable) aggregation method.\n * @returns rows keyed by `baseDimFields` + each `crossDims[*].outputName` + measures.\n */\nexport function rebucketCrossObject(\n baseRows: Record<string, unknown>[],\n baseDimFields: string[],\n crossDims: CrossObjectDim[],\n measures: MeasureRecombine[],\n): Record<string, unknown>[] {\n const buckets = new Map<string, Record<string, unknown>>();\n\n for (const row of baseRows) {\n // Resolve each cross-object FK to its attribute (or RESTRICTED).\n const resolved: Record<string, unknown> = {};\n for (const cd of crossDims) {\n const fk = row[cd.fkField];\n resolved[cd.outputName] = cd.fkToAttr.has(fk) ? cd.fkToAttr.get(fk) : RESTRICTED_BUCKET;\n }\n\n // Bucket key = base dims (unchanged) + resolved attributes. `\\u0001` is a\n // separator no group value contains, matching the engine's own convention.\n const keyParts: string[] = [];\n // JSON-encoded, so the empty bucket (`null` on both aggregation paths since\n // #3839) stays distinct from a row whose value is the literal string\n // `\"null\"` — plain interpolation renders both as `null` and would merge two\n // real groups into one. Only this composite id is affected; the emitted\n // bucket keeps the row's own value verbatim below.\n for (const f of baseDimFields) keyParts.push(`${f}=${JSON.stringify(row[f] ?? null)}`);\n for (const cd of crossDims) keyParts.push(`${cd.outputName}=${String(resolved[cd.outputName])}`);\n const key = keyParts.join('\\u0001');\n\n let bucket = buckets.get(key);\n if (!bucket) {\n bucket = {};\n for (const f of baseDimFields) bucket[f] = row[f];\n for (const cd of crossDims) bucket[cd.outputName] = resolved[cd.outputName];\n buckets.set(key, bucket);\n }\n for (const m of measures) {\n bucket[m.alias] = recombine(m.method, bucket[m.alias], row[m.alias]);\n }\n }\n\n return [...buckets.values()];\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Cube, Metric, Dimension as CubeDimension, CubeJoin } from '@objectstack/spec/data';\nimport { AggregationFunction } from '@objectstack/spec/data';\nimport type { Dataset, DatasetMeasure, DatasetDimension } from '@objectstack/spec/ui';\nimport { resolveI18nLabel } from '@objectstack/spec/ui';\nimport type { FilterCondition } from '@objectstack/spec/data';\nimport { datasetInvalidError } from './dataset-refusal.js';\n\n/**\n * Dataset → Cube compiler (ADR-0021 D-A=(c), WS2).\n *\n * Lowers a declarative `dataset` (base object + included relationships +\n * declared dimensions/measures + derived measures) into the existing Cube\n * analytics runtime model. The author never writes an `ON` clause: joins are\n * DERIVED from the `include` relationship names and the dotted `relationship.field`\n * references on dimensions/measures, matching the NativeSQLStrategy convention\n * `<parentTable>.<relationship> = <relationship>.id`.\n *\n * Safety (D-C): every dotted field reference must point at a relationship that\n * the dataset explicitly declared in `include`; otherwise the compile fails.\n * The returned `allowedRelationships` set is the join allowlist the strategy\n * enforces at SQL-build time.\n */\n\n/**\n * Aggregates v1 does NOT compile to the Cube SQL switch — surfaced as a clear error.\n *\n * **EMPTY since #6188, and deliberately kept.** It named `array_agg` and\n * `string_agg`: the two aggregates the spec declared and this runtime could not\n * lower. ADR-0049 resolved that the honest way round — both were retired from\n * `AggregationFunction` itself, so they are now refused one layer earlier, by\n * the parse, with a prescription that tells the author what to do instead.\n * `count_distinct` was the third unlowered function on the SQL drivers and was\n * NOT retired (maintainer ruling, 2026-08-07): this compiler lowers it already,\n * and the driver-side implementation follows on its own card.\n *\n * The set stays because it is one half of an arithmetic the lockstep tests\n * enforce (`SUPPORTED = spec vocabulary − this`), and that arithmetic is what\n * stops the next aggregate added to the spec from reaching the strategy's\n * `default` and returning a row count in place of the requested value. Empty is\n * the correct current reading — every declared aggregate is lowered — not a\n * leftover.\n */\nexport const UNSUPPORTED_AGGREGATES = new Set<string>();\n\n/**\n * What v1 *can* lower — derived from the spec's vocabulary rather than restated.\n *\n * The list used to be hand-written prose inside the error message below, which\n * made it a third copy of one vocabulary (after `AggregationFunction` and the\n * `native-sql-strategy` switch) with nothing keeping the three in step. An\n * aggregate added to the spec would have passed this gate, been reported as\n * supported by that message, and then hit the strategy's `default` — returning\n * a row count in place of the requested number. objectui#2945.\n */\nexport const SUPPORTED_AGGREGATES: string[] = AggregationFunction.options\n .filter((a: string) => !UNSUPPORTED_AGGREGATES.has(a));\n\nexport interface DerivedMeasureSpec {\n name: string;\n op: 'ratio' | 'sum' | 'difference' | 'product';\n of: string[];\n}\n\nexport interface CompiledDataset {\n /** The Cube the dataset compiles to (consumed by the strategy chain). */\n cube: Cube;\n /**\n * Every join alias the dataset may use — each declared `include` path AND its\n * intermediate prefixes (ADR-0071). The join allowlist (D-C): the\n * NativeSQLStrategy rejects any join alias not in this set.\n */\n allowedRelationships: Set<string>;\n /** Derived measures, computed post-aggregation by the executor (Q1). */\n derived: DerivedMeasureSpec[];\n /** Definition-level filter (the dataset's intrinsic scope). */\n filter?: FilterCondition;\n /** Per-measure scoped filters, keyed by measure name (applied by executor). */\n measureFilters: Record<string, FilterCondition>;\n}\n\n/**\n * The related object reached by traversing a relationship: its logical object\n * name (used to resolve the NEXT hop in a multi-hop chain — ADR-0071) and its\n * physical table name (the join target).\n */\nexport interface RelationshipTarget {\n object: string;\n table: string;\n}\n\n/**\n * Resolves a relationship name on a base object to the related object/table,\n * using the runtime's object graph. Optional: when omitted the compiler trusts\n * the declared `include` names (the NativeSQLStrategy convention assumes the\n * relationship name equals the related table name).\n *\n * May return a bare table-name `string` (legacy single-hop: object name is\n * assumed equal to the table) or a {@link RelationshipTarget} (required to\n * traverse further along a multi-hop path, where object differs from table for\n * namespaced objects).\n */\nexport type RelationshipResolver = (\n baseObject: string,\n relationshipName: string,\n) => string | RelationshipTarget | undefined;\n\n/**\n * Optional probes the compiler consults to reject metadata that is decidable\n * BEFORE any query runs. Every probe is optional and every one of them is\n * tiered \"cannot answer, do not block\" (the same stand-down as\n * `isRegisteredObject` / `getObjectFieldNames` on `AnalyticsServiceConfig`):\n * a host without a data engine compiles exactly as it did before.\n */\nexport interface DatasetCompileOptions {\n /**\n * [#5115] The datasource `objectName` is BOUND to, or `undefined` when nothing\n * authoritative can answer (no data engine, unknown object) — or when nothing\n * binds the object at all and it rides the deployment's default datasource.\n *\n * With it the compiler can settle at COMPILE time what #5033 could only\n * report at QUERY time: a dataset whose join crosses datasources declares a\n * statement no driver can execute, because the analytics engine lowers the\n * whole dataset into ONE SQL statement on the base object's datasource.\n *\n * [#5288] What the host supplies here changed shape, the rule below did not.\n * It used to be the object's DECLARED `datasource` — step 1 of the five\n * `ObjectQL.getDriver` routes by — so an object placed by a\n * `datasourceMapping` rule, by the ADR-0057 §3.6 lifecycle split, or by its\n * package's `defaultDatasource` answered `'default'` and was read here as\n * unanswered. The built-in host (`plugin.ts`) now asks the engine's own\n * resolver instead, so those three placements ARE visible from here and a join\n * between two objects bound to two different datasources is decidable\n * whichever mechanism bound them.\n *\n * Still deliberately UNANSWERED: `'default'`, and the object that no rule\n * places anywhere. The deployment's default driver keeps its natural name\n * (#3826), so \"rides the default\" is reported as `undefined` rather than as a\n * name — which means a join from a bound object to a default-riding one stays\n * undecidable here and remains the query-time diagnostic's business (#5288\n * records this boundary; widening it is #5115's follow-up, not this rule's).\n * See {@link compileDataset}.\n */\n getObjectDatasource?: (objectName: string) => string | undefined;\n /**\n * ADR-0062 D6 — is `objectName` federated (bound to an external datasource)?\n *\n * A federated participant is EXEMPT from the cross-datasource rejection:\n * `NativeSQLStrategy.canHandle` already declines a cube whose base or joined\n * object is external, so such a dataset is served by the ObjectQL FK-expand\n * path (two reads, joined in memory) — which crosses datasources by\n * construction. Rejecting it here would break a path that works today.\n */\n isExternalObject?: (objectName: string) => boolean;\n}\n\n/** Map a dataset measure's aggregate to the Cube metric `type`. */\nfunction aggregateToMetricType(m: DatasetMeasure): Metric['type'] {\n // Only reached for non-derived measures, where the spec refinement guarantees\n // an aggregate; guard defensively so the type narrows from `optional`.\n if (!m.aggregate) {\n throw new Error(`[dataset-compiler] non-derived measure \"${m.name}\" has no aggregate`);\n }\n if (UNSUPPORTED_AGGREGATES.has(m.aggregate)) {\n // [#5367] `DATASET_INVALID` / 400 — the aggregate is the dataset author's\n // choice, and the message already names the ones that would work.\n //\n // Unreachable while `UNSUPPORTED_AGGREGATES` is empty (#6188 retired its two\n // members from the spec, which now refuses them at parse). Kept as the\n // landing site for the next aggregate the spec declares before this runtime\n // can lower it: without it that aggregate reaches the strategy's `default`\n // and comes back as a row count. The lockstep suite is what decides which\n // of the two states we are in, so this branch cannot rot unnoticed.\n throw datasetInvalidError(\n `[dataset-compiler] measure \"${m.name}\" uses aggregate \"${m.aggregate}\" which is ` +\n `not supported by the v1 dataset runtime (supported: ${SUPPORTED_AGGREGATES.join(', ')}).`,\n );\n }\n return m.aggregate as Metric['type'];\n}\n\n/** Map a dataset dimension type to the Cube dimension `type`. */\nfunction dimensionType(d: DatasetDimension): CubeDimension['type'] {\n switch (d.type) {\n case 'date': return 'time';\n case 'number': return 'number';\n case 'boolean': return 'boolean';\n case 'lookup': return 'string';\n case 'string': return 'string';\n default: return 'string';\n }\n}\n\n/** The relationship PATH a dotted field traverses — all segments but the final\n * column — or null for a base-object field. E.g. `account.owner.region` →\n * `account.owner`; `account.region` → `account`; `region` → null. */\nfunction fieldRelationshipPath(field: string): string | null {\n const idx = field.lastIndexOf('.');\n return idx > 0 ? field.slice(0, idx) : null;\n}\n\n/** Max relationship hops in one `include` path — base → 3 hops = 4 objects\n * (ADR-0071; Salesforce-report-type parity). To-one chains never fan out, so\n * this is a performance/complexity guard, not a correctness limit. */\nconst MAX_JOIN_HOPS = 3;\n\n/** SQL-safe join alias for a relationship PATH. The dotted path is the author-\n * facing form; the alias replaces dots with `__` (Cube.js convention) so each\n * prefix is one valid identifier — quoted dotted identifiers are rejected by\n * the read-scope SQL guard (fail-closed). Single-segment paths are unchanged,\n * so single-hop joins stay byte-for-byte identical. */\nconst joinAlias = (path: string): string => path.replace(/\\./g, '__');\n\n/**\n * [#6761] The locale this compiler resolves an inline-locale-map label at:\n * **none**, i.e. the platform source language `en` per `resolveI18nLabel`'s\n * documented nullish-tolerance.\n *\n * This is a decision, not an omission, and it is spelled as a named constant so\n * it stays visible and greppable rather than reading as a forgotten argument\n * (the resolver takes `locale` positionally for exactly that reason).\n *\n * **A compiled Cube is a REGISTRY artifact, not a response.** `registerDataset`\n * writes it into `CubeRegistry` under the dataset's name, `queryDataset`\n * re-registers on every call, and `getMeta()` — the `/analytics/meta` face —\n * reads it back with **no execution context at all** (`IAnalyticsService.getMeta`\n * takes `cubeName?` and nothing else, and the route calls it without one). So\n * the request locale must NOT be baked in here: one `zh-CN` query would leave a\n * Chinese-labelled cube in a registry every later reader shares, and\n * `/analytics/meta` would answer whoever queried last. Request-scoped\n * resolution belongs where a request is in hand — `queryDataset`'s two field\n * enrichment sites, which read `context.locale`.\n *\n * **The fallback stays `d.name`, and that is safe against the `f.label == null`\n * guard** (#5199 route A / #6761). `Metric.label` and `Dimension.label` are\n * REQUIRED strings in `analytics.zod.ts`, so an unresolvable label must still\n * produce one, and the machine name is what this compiler already wrote. It\n * cannot pre-empt the document-sourced label downstream because a cube label\n * never reaches `AnalyticsResult.fields[]`: both strategies' `buildFieldMeta`,\n * the draft preview evaluator, and `DatasetExecutor`'s #5537 descriptor\n * adoption all emit `{ name, type }` only. The enrichment sites therefore still\n * see `f.label == null` and write the locale-resolved label over nothing.\n */\nconst REGISTRY_LOCALE: string | undefined = undefined;\n\nexport function compileDataset(\n dataset: Dataset,\n resolver?: RelationshipResolver,\n options?: DatasetCompileOptions,\n): CompiledDataset {\n const include = dataset.include ?? [];\n\n // ── #5115 — cross-datasource joins are rejected HERE, at compile time ──────\n //\n // A dataset lowers to ONE SQL statement executed on the base object's\n // datasource (`plugin.ts` routes raw SQL by object since #5033). So a join\n // whose target lives on a DIFFERENT datasource is not a query that sometimes\n // fails — it is metadata that can never execute, and the question \"which\n // datasource is each participant bound to\" is fully answerable the moment the\n // dataset is compiled. #5033 made that failure loud at QUERY time (in front of\n // whoever opened the dashboard); this gate moves the same verdict to\n // registration, where the AUTHOR is still holding the metadata.\n //\n // Tiering — \"cannot answer, do not block\": no probe, no answer for the base\n // object, or no answer for a target ⇒ compile as before and let #5033's\n // query-time defence report it. A false ALLOW costs a loud runtime error that\n // already exists; a false REJECT would blank a working dashboard on upgrade,\n // so this gate fires ONLY on a conflict the metadata itself proves.\n //\n // What counts as an ANSWER: a non-`'default'` datasource NAME for the object.\n // Two objects bound to two different names are provably in two databases.\n //\n // `'default'` is not one. It is what `ObjectSchema.datasource` defaults to and\n // what `ObjectQL.getDriver` reads as \"no explicit binding, keep looking\", so\n // treating it as \"the primary DB\" would reject a dataset whose two objects a\n // mapping rule in fact lands on the SAME datasource, and would make the\n // verdict depend on whether the object happened to be Zod-parsed (which\n // materializes the default). It is read as UNANSWERED, as is `undefined`.\n //\n // [#5288] The probe used to report only step 1 of that resolution — the\n // DECLARED value — which left every object placed by a `datasourceMapping`\n // rule, by the ADR-0057 §3.6 lifecycle split (audit/telemetry/event → the\n // `telemetry` datasource), or by its package's `defaultDatasource` answering\n // `'default'` and therefore unanswerable here. The built-in host now asks\n // `ObjectQL.resolveEffectiveDatasource`, so this rule — unchanged — sees those\n // placements too. What it still cannot see is the object nothing binds at all:\n // that one rides the deployment's default driver and is reported as\n // `undefined`, so a join from a bound object to a default-riding one is not\n // decidable here and stays with #5033's query-time diagnostic.\n const declaredDatasource = (objectName: string): string | undefined => {\n const declared = options?.getObjectDatasource?.(objectName);\n return declared && declared.toLowerCase() !== 'default' ? declared : undefined;\n };\n const isExternal = (objectName: string): boolean =>\n options?.isExternalObject?.(objectName) ?? false;\n const baseDatasource = declaredDatasource(dataset.object);\n // Datasource ids are compared case-insensitively: an id differing only in case\n // is not evidence of two different databases, and an uncertain answer must\n // not reject.\n const sameDatasource = (a: string, b: string) => a.toLowerCase() === b.toLowerCase();\n const baseIsFederated = isExternal(dataset.object);\n const assertSameDatasource = (targetObject: string, path: string): void => {\n // Judgeable only when the BASE side is placed and non-federated; it is the\n // side every comparison is made against, so an unplaceable base means no\n // join can be judged (never reject every join for want of the base).\n if (!baseDatasource || baseIsFederated) return;\n if (isExternal(targetObject)) return; // served by the FK-expand path, not by one statement\n const targetDatasource = declaredDatasource(targetObject);\n if (!targetDatasource) return; // cannot answer for this target\n if (sameDatasource(targetDatasource, baseDatasource)) return;\n // [#5716] `DATASET_INVALID` / 400 — the AUTHOR's verdict, decided entirely\n // from metadata before any query runs: the dataset's `include` path and the\n // two objects' `datasource` bindings. Both are things the author (or an\n // admin) can change, and the message already names both fixes. Nothing here\n // is a runtime fault, so a 500 told the author \"the platform is broken\"\n // about a document they wrote.\n throw datasetInvalidError(\n `[dataset-compiler] dataset \"${dataset.name}\" declares a JOIN that crosses datasources: ` +\n `its base object \"${dataset.object}\" is on datasource \"${baseDatasource}\", but the joined ` +\n `object \"${targetObject}\" — reached via the \\`include\\` path \"${path}\" — is on datasource ` +\n `\"${targetDatasource}\". A dataset JOIN cannot cross datasources: the whole dataset is ` +\n `executed as ONE statement on the base object's datasource, so \"${targetObject}\" is simply ` +\n `not there. Fix it by binding both objects to the same datasource, or by dropping \"${path}\" ` +\n `from the dataset's \\`include\\` (and every dimension/measure that references it).`,\n );\n };\n\n // Resolve each declared relationship PATH into its ordered join chain, emitting\n // one Cube join per PATH PREFIX (ADR-0071 multi-hop, to-one only). The join\n // ALIAS is the full dotted path (`account.owner`), which self-describes the\n // chain: the parent alias is the path minus its last segment, the FK column is\n // that last segment. So declaring `account.owner` auto-adds the intermediate\n // `account` join, and the strategy can rebuild every `ON` from the alias alone.\n // Without a resolver, each segment's relationship name is assumed to equal both\n // the related object and its table (legacy convention / unit tests).\n const resolveHop = (fromObject: string, rel: string): RelationshipTarget => {\n if (!resolver) return { object: rel, table: rel };\n const resolved = resolver(fromObject, rel);\n if (!resolved) {\n // [#5716] `DATASET_INVALID` / 400 — the dataset's own `include` names a\n // relationship the object graph does not have: a typo or a stale dataset,\n // fixable only by the author. Sibling of the already-enveloped \"…is not\n // declared in the dataset's `include`\" (#5367), one step earlier in the\n // same resolution.\n //\n // ⚠️ The WORDING is left exactly as it was, and that is not an oversight:\n // it contains both \"relation\"(ship) and \"does not exist\", so\n // `analytics-service.ts`'s `isMissingSourceError` matches it — the mine\n // #5717 filed. Enveloping it does not disarm that sniffer (it reads the\n // message, not the envelope) and does not arm it either (this throw is\n // still OUTSIDE `queryDataset`'s try, which is the only reason the mine\n // has never gone off). What it DOES do is make #5717's option B — \"never\n // degrade an error that declares a 4xx envelope to an empty result\" —\n // able to cover this site, which before today it could not.\n throw datasetInvalidError(\n `[dataset-compiler] dataset \"${dataset.name}\" includes relationship \"${rel}\" ` +\n `which does not exist on object \"${fromObject}\".`,\n );\n }\n return typeof resolved === 'string' ? { object: resolved, table: resolved } : resolved;\n };\n const joins: Record<string, CubeJoin> = {};\n for (const path of include) {\n const segments = path.split('.');\n if (segments.length > MAX_JOIN_HOPS) {\n // [#5716] `DATASET_INVALID` / 400 — a limit of the v1 runtime, reported\n // against a path the author wrote. Same family as the aggregate refusal a\n // few lines up (\"not supported by the v1 dataset runtime\", #5367): what\n // the runtime cannot do is stated as a property of the dataset, because\n // that is what the author has to change.\n throw datasetInvalidError(\n `[dataset-compiler] dataset \"${dataset.name}\" include path \"${path}\" exceeds the ` +\n `${MAX_JOIN_HOPS}-hop limit (${segments.length} hops). Deeper traversal is not supported.`,\n );\n }\n let fromObject = dataset.object;\n let parentAlias = dataset.object;\n let prefix = '';\n for (const seg of segments) {\n prefix = prefix ? `${prefix}.${seg}` : seg;\n const target = resolveHop(fromObject, seg);\n // #5115 — every hop is a join target in the single statement, so each one\n // (not just the last segment of a path) must sit on the base datasource.\n assertSameDatasource(target.object, prefix);\n const alias = joinAlias(prefix);\n if (!joins[alias]) {\n // KEY is the SQL-safe alias; `name` carries the join TABLE; the strategy\n // rebuilds the ON clause from the alias convention (`<parent>.<seg> = <alias>.id`).\n joins[alias] = {\n name: target.table,\n relationship: 'many_to_one',\n sql: `${parentAlias}.${seg} = ${prefix}.id`,\n };\n }\n fromObject = target.object;\n parentAlias = prefix;\n }\n }\n\n // The join allowlist (D-C) is every registered alias — each declared path AND\n // its intermediate prefixes — so a multi-hop field's intermediate joins pass.\n const allowedRelationships = new Set(Object.keys(joins));\n\n // Assert any dotted field only traverses a DECLARED relationship PATH (D-C).\n const assertDeclared = (field: string, ownerKind: string, ownerName: string) => {\n const relPath = fieldRelationshipPath(field);\n if (relPath && !joins[joinAlias(relPath)]) {\n // [#5367] `DATASET_INVALID` / 400 — a dimension/measure traversing a\n // relationship the same document never declared in `include` is the\n // dataset author's mistake, and the fix is in the document they hold.\n throw datasetInvalidError(\n `[dataset-compiler] ${ownerKind} \"${ownerName}\" references relationship path \"${relPath}\" ` +\n `via \"${field}\", but \"${relPath}\" is not declared in the dataset's \\`include\\`. ` +\n `Only fields along a declared relationship path are joinable.`,\n );\n }\n };\n\n // Compile dimensions.\n const dimensions: Record<string, CubeDimension> = {};\n for (const d of dataset.dimensions) {\n assertDeclared(d.field, 'dimension', d.name);\n const dim: CubeDimension = {\n name: d.name,\n // [#6761] An inline locale map is a label, not a missing one. Before this,\n // the `typeof === 'string'` test dropped the map and substituted the\n // machine name, which `/analytics/meta` then published as a display title\n // (`title: 'owner'` for a dimension labelled `{ en: 'Owner', … }`).\n label: resolveI18nLabel(d.label, REGISTRY_LOCALE) ?? d.name,\n type: dimensionType(d),\n sql: d.field,\n };\n if (dim.type === 'time') {\n dim.granularities = d.dateGranularity\n ? [d.dateGranularity]\n : ['day', 'week', 'month', 'quarter', 'year'];\n }\n dimensions[d.name] = dim;\n }\n\n // Compile measures (non-derived → Cube metrics; derived → sidecar).\n const measures: Record<string, Metric> = {};\n const derived: DerivedMeasureSpec[] = [];\n const measureFilters: Record<string, FilterCondition> = {};\n\n for (const m of dataset.measures) {\n if (m.derived) {\n derived.push({ name: m.name, op: m.derived.op, of: m.derived.of });\n continue;\n }\n if (m.field) assertDeclared(m.field, 'measure', m.name);\n const metric: Metric = {\n name: m.name,\n // [#6761] Same as the dimension label above — see {@link REGISTRY_LOCALE}.\n label: resolveI18nLabel(m.label, REGISTRY_LOCALE) ?? m.name,\n type: aggregateToMetricType(m),\n // `count` with no field aggregates over rows (*).\n sql: m.field ?? '*',\n };\n if (typeof m.format === 'string') metric.format = m.format;\n measures[m.name] = metric;\n if (m.filter) measureFilters[m.name] = m.filter;\n }\n\n const cube: Cube = {\n name: dataset.name,\n // [#6761] The cube's own display title, same rule. `Cube.title` is optional\n // in the schema, but an absent dataset label already produced the machine\n // name here and that is not what this card changes — only the map case moves.\n title: resolveI18nLabel(dataset.label, REGISTRY_LOCALE) ?? dataset.name,\n sql: dataset.object,\n measures,\n dimensions,\n public: false,\n };\n if (Object.keys(joins).length > 0) cube.joins = joins;\n\n return {\n cube,\n allowedRelationships,\n derived,\n filter: dataset.filter,\n measureFilters,\n };\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n IAnalyticsService,\n AnalyticsQuery,\n AnalyticsResult,\n DatasetSelection,\n DatasetCompareTo,\n} from '@objectstack/spec/contracts';\nimport { emptyGroupValueFor, type FilterCondition } from '@objectstack/spec/data';\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\nimport { bucketKeyToCalendarRange, filterTokenContextFrom, resolveFilterTokens } from '@objectstack/core';\nimport type { CompiledDataset, DerivedMeasureSpec } from './dataset-compiler.js';\nimport { datasetInvalidError } from './dataset-refusal.js';\nimport type { OrderLabelResolver } from './dimension-labels.js';\n\n// Re-export the shared protocol shapes so existing importers keep working.\nexport type { DatasetSelection } from '@objectstack/spec/contracts';\n/** @deprecated use DatasetCompareTo from @objectstack/spec/contracts */\nexport type CompareTo = DatasetCompareTo;\n\n/**\n * Dataset executor (ADR-0021 WS2).\n *\n * Turns a compiled dataset + a presentation's selection (dimensions, measures,\n * runtime filter, compareTo) into one or more `AnalyticsQuery`s against the Cube\n * runtime, then post-processes the results:\n * - resolves the base measures a selection needs (including derived deps),\n * - applies measure-scoped filters via supplementary grouped queries — in\n * EVERY window it runs, the `compareTo` one included (#4820) — and keeps the\n * assembled grid's `fields` describing its DIMENSION columns even when there\n * was no primary query to carry them (#5537),\n * - fills the empty-group value into columns no query reported, by aggregate\n * kind (#4708) — a count/sum over an excluded group is 0, avg/min/max null,\n * - evaluates derived measures (ratio/sum/difference/product) row-by-row (Q1),\n * - shifts the queries for `compareTo` (previousPeriod / previousYear) and\n * attaches `<measure>__compare` columns, re-running the same measure pass\n * so a filtered measure means the same thing in both columns, and — when\n * the shifted dimension is the grid's own time axis — restating the\n * comparison rows' bucket keys in CURRENT-period terms so the two grids\n * merge into one row per bucket instead of stacking two windows on one\n * axis (#6007),\n * - computes server-side totals (`selection.totals.groupings`, #1753) by\n * re-running the selection per dimension subset, so matrix subtotals and\n * the grand total use each measure's true aggregate,\n * - orders and windows the final grid (`order` / `limit` / `offset`, #3588).\n *\n * **Where ordering happens, and why here.** `order`/`limit`/`offset` are applied\n * to the ASSEMBLED grid — after measure-scoped sub-queries are merged in, after\n * `compareTo` columns are attached, and after derived measures are computed —\n * never by forwarding them blindly to every sub-query. Two reasons:\n *\n * 1. **Correctness.** A supplementary measure-scoped query selects ONE measure;\n * forwarding `ORDER BY <other_measure>` to it emits SQL referencing a column\n * that query never selects, and forwarding `LIMIT` truncates it before the\n * merge, so rows silently vanish from the grid. A derived measure has no SQL\n * column at all, yet is a perfectly reasonable sort key.\n * 2. **Coverage.** Only `NativeSQLStrategy` honours `order`/`limit`; the\n * ObjectQL aggregate path has nowhere to put them (`EngineAggregateOptions`\n * has no ordering grammar), and date-bucketed queries are *forced* down that\n * path because native SQL declines granularity. Sorting here makes ordering\n * work identically on every driver and strategy.\n *\n * The single-query case still pushes `order`/`limit`/`offset` DOWN into the SQL\n * (see `canPushDownWindow`) so the database does the work and the echoed `sql`\n * shows it; the post-pass is then a no-op re-sort of already-sorted rows.\n *\n * **What the sort key IS for a label-bearing dimension (#3680).** An order key\n * naming a `select` or `lookup`/`master_detail` dimension sorts by the DISPLAY\n * label the response will carry (option label / related record name), not the\n * stored value — a \"sort by Account\" ordered by opaque FK ids presents as\n * arbitrary once the labels render. The mapping comes through an injected\n * {@link OrderLabelResolver} (built by `queryDataset` over the same\n * label-resolution capabilities the display pass uses); rows keep their raw\n * values — only the COMPARISON substitutes the label — so drill metadata still\n * snapshots stored values downstream. Such keys are never pushed into SQL (the\n * label is not a column there), and the label fetch happens BEFORE `applyWindow`\n * so a \"top 10 by account name\" truncates the right ten.\n *\n * RLS/tenant scoping is NOT handled here — it is enforced inside the strategy\n * via the StrategyContext read-scope hook (D-C). This layer is pure query\n * shaping + arithmetic; the order-label hook is an injected interface, not an\n * engine dependency.\n */\n\n/**\n * Expand `{filter-placeholder}` values across everything a dataset query\n * compares on (framework#3582): the dataset's intrinsic `filter`, the\n * presentation's `runtimeFilter` (a dashboard widget's own scope), every\n * measure-scoped filter, and the `dateRange` bounds of the selection's time\n * dimensions.\n *\n * The dashboard path needs its own call rather than inheriting the ObjectQL\n * engine's: `NativeSQLStrategy` compiles a raw `SELECT … WHERE` and binds\n * comparands directly, so a widget filtered on `{current_year_start}` never\n * passes through `engine.find()` at all — which is exactly why the token\n * reached SQLite as the literal text and every such widget rendered zero.\n *\n * Inputs are treated as immutable: a `CompiledDataset` lives in the service's\n * registry across requests, so resolving in place would bake one request's\n * user id (and one day's dates) into every later render. New objects are\n * allocated only when the tree actually held a placeholder.\n */\nfunction resolveSelectionTokens(\n compiled: CompiledDataset,\n selection: DatasetSelection,\n context?: ExecutionContext,\n): { compiled: CompiledDataset; selection: DatasetSelection } {\n // One instant for the whole call: the intrinsic filter, the runtime filter\n // and each measure filter are resolved in separate passes, and a query whose\n // pieces disagreed about \"now\" could straddle a period boundary — the primary\n // grid scoped to this month while a measure-scoped sub-query saw the next.\n const tokenCtx = filterTokenContextFrom(context, new Date());\n const resolve = <T>(v: T): T => resolveFilterTokens(v, tokenCtx);\n\n const filter = resolve(compiled.filter);\n const measureFilters = resolve(compiled.measureFilters);\n const runtimeFilter = resolve(selection.runtimeFilter);\n const timeDimensions = selection.timeDimensions?.map((td) =>\n td.dateRange == null ? td : { ...td, dateRange: resolve(td.dateRange) },\n );\n\n const compiledChanged =\n filter !== compiled.filter || measureFilters !== compiled.measureFilters;\n const selectionChanged =\n runtimeFilter !== selection.runtimeFilter ||\n (timeDimensions !== undefined &&\n timeDimensions.some((td, i) => td !== selection.timeDimensions![i]));\n\n return {\n compiled: compiledChanged ? { ...compiled, filter, measureFilters } : compiled,\n selection: selectionChanged ? { ...selection, runtimeFilter, timeDimensions } : selection,\n };\n}\n\n/** AND two optional FilterConditions into one (MongoDB-style). */\nexport function combineFilters(\n a?: FilterCondition,\n b?: FilterCondition,\n): FilterCondition | undefined {\n if (a && b) return { $and: [a, b] } as FilterCondition;\n return a ?? b;\n}\n\n/**\n * Partition base measures into those the dataset scopes with their own\n * measure-level `filter` and those it does not — the single place that answers\n * \"does this measure carry its own filter?\".\n *\n * Paired with {@link DatasetExecutor.runMeasurePass}, this is what keeps ONE\n * definition of \"how a measure filter is applied\" for every grouped pass the\n * executor runs: the current period, each `totals` subset, and the `compareTo`\n * window. `compareTo` used to issue a single shifted query over all base\n * measures with only the base filter, consulting `measureFilters` nowhere on\n * that path — so a measure declared `filter: { stage: 'closed_won' }` was\n * scoped in its own column and unscoped in `<measure>__compare`: two different\n * measures rendered side by side under one label, and biased the worst way\n * (the comparison window is inflated by exactly the rows the measure exists to\n * exclude, so \"won deals vs. last month\" reads as a collapse). #4820.\n *\n * The remedy is deliberately NOT a second copy of the filter logic on the\n * compare path — two implementations of one rule diverge again at the next\n * change. Both paths call the same split and the same pass.\n */\nexport function splitMeasuresByFilter(\n measures: Iterable<string>,\n measureFilters: Record<string, FilterCondition | undefined>,\n): { unfiltered: string[]; filtered: string[] } {\n const unfiltered: string[] = [];\n const filtered: string[] = [];\n for (const m of measures) (measureFilters[m] ? filtered : unfiltered).push(m);\n return { unfiltered, filtered };\n}\n\n/**\n * Evaluate derived measures on each aggregated row, mutating a shallow copy.\n * Division by zero (and missing operands) yields `null` rather than Infinity/NaN.\n */\nexport function evaluateDerivedMeasures(\n rows: Record<string, unknown>[],\n derived: DerivedMeasureSpec[],\n): Record<string, unknown>[] {\n if (derived.length === 0) return rows;\n return rows.map((row) => {\n const out = { ...row };\n for (const d of derived) {\n out[d.name] = computeDerived(d, out);\n }\n return out;\n });\n}\n\n/**\n * Fill the EMPTY-GROUP value into every measure column the assembled grid\n * LISTS but no query REPORTED — by aggregate kind (#4708, objectui#3136).\n *\n * The grid is assembled from several results: the primary query, one\n * supplementary query per measure-scoped filter, and (for `compareTo`) a\n * shifted pass. {@link mergeByDimensions} writes a measure's column only onto\n * rows its source result returned, and a `GROUP BY` over a filtered row set\n * emits NO group at all for a dimension value the filter excludes entirely.\n * The column therefore comes back **absent**, not `0` — and absent renders as\n * \"no data for this row\", which for a count is the opposite of what the row\n * means. A derived ratio over it goes null as well ({@link computeDerived}\n * treats a missing operand as unknowable), so the blank spreads.\n *\n * The bias runs the worst possible way: the rows that blank are the ones whose\n * numerator the filter excluded — the WORST-performing rows. A `lead_source`\n * that won nothing renders as \"no data\" while one that won everything renders\n * fine.\n *\n * **Filled strictly by aggregate kind**, never wholesale. `count` /\n * `count_distinct` over an excluded group is unambiguously `0` (\"how many rows\n * matched\" has an exact answer when the answer is none), and `sum` over the\n * empty set is its identity `0`. `avg` / `min` / `max` are genuinely null —\n * there is nothing to average — and flattening those to `0` would trade this\n * lie for the opposite one, reporting a measurement nobody made. The\n * kind→identity mapping is `emptyGroupValueFor` in `@objectstack/spec/data`,\n * shared with the authoring-side coherence checks so the two cannot drift.\n *\n * **Only rows that already exist are touched** — no group is invented. A\n * dimension value no query reported at all has genuinely no data and stays out\n * of the grid; this fills the cell, never the row.\n *\n * Deliberately NOT a `?? 0` in the widget or a `coalesce` in the measure: a\n * consumer-side patch must be repeated by every author of every ratio widget\n * forever, and forgetting it is silent. Only the executor knows which aggregate\n * produced the gap, so only the executor can tell `0` from unknown.\n *\n * Mutates `rows` in place (they are already this pipeline's own copies) and\n * returns them for chaining.\n *\n * @param columnAggregates - Grid column → the aggregate that produced it.\n * Includes `<measure>__compare` columns, which merge through the same seam.\n */\nexport function fillEmptyGroups(\n rows: Record<string, unknown>[],\n columnAggregates: Record<string, string | undefined>,\n): Record<string, unknown>[] {\n for (const [column, aggregate] of Object.entries(columnAggregates)) {\n const empty = emptyGroupValueFor(aggregate);\n if (empty === undefined) continue;\n for (const row of rows) if (row[column] == null) row[column] = empty;\n }\n return rows;\n}\n\nfunction num(v: unknown): number | null {\n if (v == null) return null;\n const n = typeof v === 'number' ? v : Number(v);\n return Number.isFinite(n) ? n : null;\n}\n\nfunction computeDerived(d: DerivedMeasureSpec, row: Record<string, unknown>): number | null {\n const vals = d.of.map((name) => num(row[name]));\n if (vals.some((v) => v === null)) return null;\n const nums = vals as number[];\n switch (d.op) {\n case 'ratio': {\n if (nums.length < 2 || nums[1] === 0) return null;\n return nums[0] / nums[1];\n }\n case 'difference':\n return nums.slice(1).reduce((acc, v) => acc - v, nums[0]);\n case 'sum':\n return nums.reduce((acc, v) => acc + v, 0);\n case 'product':\n return nums.reduce((acc, v) => acc * v, 1);\n default:\n return null;\n }\n}\n\n// ── date bucketing (#3588) ───────────────────────────────────────────────────\n\n/** The date-bucket vocabulary shared by the dataset, the selection, and the\n * bucketing utilities in `@objectstack/core`. */\nexport type DateGranularityValue = NonNullable<DatasetSelection['dateGranularity']>;\n\n/**\n * The EFFECTIVE bucket size for one date dimension of a selection — the single\n * source of truth for granularity precedence.\n *\n * Precedence, per dimension:\n * 1. a `granularity` already stated on that dimension's `timeDimensions`\n * entry — never overridden;\n * 2. `selection.dateGranularity` — the presentation's choice, so a widget can\n * bucket by month without the dataset committing every consumer to it;\n * 3. `datasetDefault` — the dataset dimension's own `dateGranularity`.\n *\n * The unit of precedence is the GRANULARITY, not the entry: a `timeDimensions`\n * entry carrying only a `dateRange` (what `compareTo` needs) states a WINDOW,\n * not a bucket size, and so cannot VETO a bucket size stated elsewhere for a\n * dimension that is being grouped.\n *\n * This answers \"how big is the bucket\", never \"is this dimension bucketed at\n * all\" — a separate question, decided per query by `buildQuery`'s\n * `bucketsUnstatedEntry` (#5688), because a window-only entry for a dimension\n * nobody grouped must stay a filter. Every caller here is already past that\n * question: `buildQuery` asks it first, and the post-processing sites in\n * `analytics-service` iterate `selection.dimensions`, which are grouped by\n * definition.\n *\n * **Why this is exported.** The bucket size chosen here decides three things\n * that MUST agree: the `GROUP BY` the query compiles to, the humanized label\n * each bucket key is rendered as, and the half-open `[gte, lt)` range a bucket\n * drills into. When the query layer resolved granularity and the post-processing\n * in `analytics-service` read the dataset default instead, they silently\n * disagreed for every selection that overrode it — a `year` query came back\n * labelled `1970-01` (a year bucket re-formatted as a month), a `day` query\n * collapsed to duplicate month labels, and `quarter`/`year` lost their drill\n * ranges entirely. One function, called from all three sites, is what stops\n * that drift recurring.\n */\nexport function resolveDimensionGranularity(\n selection: Pick<DatasetSelection, 'timeDimensions' | 'dateGranularity'>,\n dimension: string,\n datasetDefault?: string,\n): DateGranularityValue | undefined {\n // `timeDimensions[].granularity` and the compiled cube's `granularities` are\n // both typed as bare strings by their own layers (Cube.js heritage), but the\n // only values that reach here come from the dataset/selection granularity\n // vocabulary — the same five the bucketing utilities accept.\n const stated = (selection.timeDimensions ?? []).find((t) => t.dimension === dimension)?.granularity;\n if (stated) return stated as DateGranularityValue;\n return selection.dateGranularity ?? (datasetDefault as DateGranularityValue | undefined);\n}\n\n// ── ordering + windowing (#3588) ─────────────────────────────────────────────\n\n/**\n * Compare two grouped-cell values for ORDER BY, ascending.\n *\n * Nulls sort LAST regardless of direction (the SQL `NULLS LAST` convention, and\n * the one users expect: an empty bucket shouldn't win a \"top 10 by revenue\").\n * The caller negates the result for `desc`, so the null branch deliberately\n * returns its verdict BEFORE that negation can flip it — see `compareRows`.\n *\n * Numbers (and numeric strings, which is how some drivers return SUM results)\n * compare numerically so 9 sorts below 10; everything else compares as a string.\n * Dates arrive here already bucketed to sort-stable keys (\"2026-04\", \"2026-Q2\"),\n * so lexicographic ordering is chronological for them too.\n */\nfunction compareValues(a: unknown, b: unknown): number {\n const aNull = a == null || a === '';\n const bNull = b == null || b === '';\n if (aNull || bNull) return aNull && bNull ? 0 : aNull ? 1 : -1;\n if (a instanceof Date || b instanceof Date) {\n return Number(a instanceof Date ? a.getTime() : a) - Number(b instanceof Date ? b.getTime() : b);\n }\n if (typeof a === 'boolean' || typeof b === 'boolean') {\n return Number(a) - Number(b);\n }\n const an = typeof a === 'number' ? a : Number(a);\n const bn = typeof b === 'number' ? b : Number(b);\n if (Number.isFinite(an) && Number.isFinite(bn)) return an - bn;\n return String(a).localeCompare(String(b));\n}\n\n/**\n * Order rows by each key in `order`, in the object's own key order (first key is\n * the primary sort). Returns a NEW array; the input is not mutated. Null/empty\n * cells stay last in both directions (see {@link compareValues}).\n *\n * `sortKeys` substitutes the COMPARED value per key (#3680): when it holds a map\n * for an order key, each cell compares by its mapped value — the display label a\n * label-bearing dimension will render as — falling back to the raw cell where\n * unmapped (an orphaned id or RLS-hidden record renders raw too, so sort and\n * display stay consistent). The rows themselves are never rewritten here.\n */\nexport function applyOrdering(\n rows: Record<string, unknown>[],\n order: Record<string, 'asc' | 'desc'> | undefined,\n sortKeys?: Record<string, Map<unknown, unknown>>,\n): Record<string, unknown>[] {\n const keys = Object.entries(order ?? {});\n if (keys.length === 0 || rows.length < 2) return rows;\n // Array.prototype.sort is stable (ES2019+), so equal rows keep the order the\n // grouping produced — an important property for reproducible LIMITs.\n return [...rows].sort((ra, rb) => {\n for (const [key, dir] of keys) {\n const map = sortKeys?.[key];\n const av = map?.get(ra[key]) ?? ra[key];\n const bv = map?.get(rb[key]) ?? rb[key];\n const aNull = av == null || av === '';\n const bNull = bv == null || bv === '';\n // Nulls last in BOTH directions — decided before `desc` negation.\n if (aNull || bNull) {\n if (aNull && bNull) continue;\n return aNull ? 1 : -1;\n }\n const c = compareValues(av, bv);\n if (c !== 0) return dir === 'desc' ? -c : c;\n }\n return 0;\n });\n}\n\n/** Apply `offset`/`limit` to an already-ordered grid. */\nexport function applyWindow(\n rows: Record<string, unknown>[],\n limit?: number,\n offset?: number,\n): Record<string, unknown>[] {\n const start = offset != null && offset > 0 ? offset : 0;\n if (start === 0 && limit == null) return rows;\n return rows.slice(start, limit != null ? start + limit : undefined);\n}\n\n/**\n * Validate `order` keys and resolve the EFFECTIVE ordering for a selection.\n *\n * A key must name something the caller actually selected — a dimension, a\n * measure, or a `<measure>__compare` column. An unknown key throws rather than\n * being dropped: silently ignoring `sortBy` is precisely the failure mode this\n * change exists to remove (#3588), and a mistyped sort key that quietly returns\n * arbitrarily-ordered rows is worse than a loud 400.\n *\n * When `limit`/`offset` is requested WITHOUT an order, the selected dimensions\n * ascending become the implicit ordering, so the truncated window is\n * reproducible instead of \"whatever the group-by happened to emit\".\n *\n * Failing both, a selected TIME dimension defaults to ASCENDING (#3916). A time\n * axis has one order a reader expects — chronological — and until this default\n * existed nothing supplied it anywhere in the stack: the aggregate path has no\n * ordering grammar, so buckets came back in Map-insertion order, and the pivot\n * builds its column headers in row-arrival order. A month-bucketed matrix\n * therefore rendered `2026-07-01, 2026-07-05, …, 2026-07-02`. Bucket keys are\n * minted sort-stable for exactly this (`2026-07`, `2026-Q3`, `2026-W31`), so\n * ascending IS chronological. An explicit `order` still wins outright — this is\n * a default, not a policy — and non-time dimensions keep whatever order the\n * grouping produced unless the caller asks.\n *\n * @param timeDimensions - The selected dimensions the cube types as `time`, in\n * selection order. The executor resolves these (it owns the cube); passing\n * them in keeps this function pure and directly testable.\n */\nexport function resolveOrdering(\n selection: DatasetSelection,\n dimensions: string[],\n timeDimensions: string[] = [],\n): Record<string, 'asc' | 'desc'> | undefined {\n const order = selection.order;\n if (order && Object.keys(order).length > 0) {\n const selectable = new Set<string>([\n ...dimensions,\n ...selection.measures,\n ...selection.measures.map((m) => `${m}__compare`),\n ]);\n const unknown = Object.keys(order).filter((k) => !selectable.has(k));\n if (unknown.length) {\n // [#5367] `DATASET_INVALID` / 400 — `selection.order` is request input and\n // the message already lists what was selectable, so the caller can fix it.\n throw datasetInvalidError(\n `[dataset-executor] order key(s) ${unknown.map((k) => `\"${k}\"`).join(', ')} — ` +\n `not a selected dimension or measure. Selectable here: ` +\n `${[...selectable].join(', ') || '(none)'}.`,\n );\n }\n return order;\n }\n // Implicit, deterministic ordering so a bare `limit` is reproducible.\n if ((selection.limit != null || selection.offset != null) && dimensions.length > 0) {\n return Object.fromEntries(dimensions.map((d) => [d, 'asc' as const]));\n }\n // #3916 — chronological by default on the time axis.\n const timeKeys = timeDimensions.filter((d) => dimensions.includes(d));\n if (timeKeys.length > 0) {\n return Object.fromEntries(timeKeys.map((d) => [d, 'asc' as const]));\n }\n return undefined;\n}\n\n// ── compareTo date math (deterministic — no Date.now) ────────────────────────\n\nfunction parseUTC(date: string): number {\n // Accepts 'YYYY-MM-DD' (and ISO datetimes); interpreted as UTC.\n const ms = Date.parse(date.length === 10 ? `${date}T00:00:00Z` : date);\n // [#5716] `DATASET_INVALID` / 400 — the string comes from the REQUEST\n // (`selection.timeDimensions[].dateRange`, usually a dashboard's date filter),\n // reaches here only through `shiftRange`'s `compareTo` math, and no schema\n // refines it into a date. A caller who sends an unparseable bound gets told\n // which bound it was; nothing about it is a server fault.\n if (Number.isNaN(ms)) {\n throw datasetInvalidError(`[dataset-executor] invalid date in dateRange: \"${date}\"`);\n }\n return ms;\n}\n\nconst DAY_MS = 86_400_000;\n\nfunction toISODate(ms: number): string {\n return new Date(ms).toISOString().slice(0, 10);\n}\n\nfunction shiftYear(date: string, years: number): string {\n const d = new Date(parseUTC(date));\n d.setUTCFullYear(d.getUTCFullYear() + years);\n return toISODate(d.getTime());\n}\n\n/**\n * Resolve which time dimension a `compareTo` shifts (#5011).\n *\n * `DatasetCompareTo.dimension` is optional, and this is the rule that makes the\n * omission deterministic. It is an EXECUTOR resolution rule, not consumer-side\n * tolerance (PD #12): the choice is made once, here, at the producer of the\n * comparison — so a dashboard widget, a report, and a raw `queryDataset` caller\n * that all omit it get the same dimension or the same error, and no renderer is\n * ever tempted to guess one on their behalf.\n *\n * Candidates use the executor's own long-standing criterion, unchanged: a\n * `timeDimensions` entry that carries a `dateRange`. That is exactly the set\n * {@link shiftRange} can act on — a time dimension with no window has nothing to\n * shift — so the resolution can never select a dimension the shift then fails on.\n *\n * Ambiguity is loud, never silently first-wins: picking `created_at` when the\n * author meant `close_date` produces a comparison column that is wrong rather\n * than missing, which is the failure mode nobody audits. The message names every\n * candidate so the fix is a copy-paste.\n */\nfunction resolveCompareDimension(selection: DatasetSelection): string {\n const cmp = selection.compareTo!;\n const shiftable = (selection.timeDimensions ?? []).filter(\n (t) => (t as { dateRange?: unknown }).dateRange != null,\n );\n const names = shiftable.map((t) => t.dimension);\n\n if (cmp.dimension != null) {\n if (!names.includes(cmp.dimension)) {\n // [#5716] `DATASET_INVALID` / 400 for all three refusals in this function.\n // Every one of them is a verdict about the SELECTION as a whole — which\n // `timeDimensions` carry a `dateRange`, and whether `compareTo` can pick\n // one — so it is `datasetInvalidError`, not the member-level\n // `invalidMemberError`: the fix is to add a window or drop `compareTo`,\n // not to correct a misspelled member. Same request, same document, same\n // family as the order-key and totals refusals already enveloped here\n // (#5367).\n throw datasetInvalidError(\n `[dataset-executor] compareTo requires a timeDimension \"${cmp.dimension}\" with a dateRange. `\n + (names.length > 0\n ? `This selection dates ${names.map((n) => `\"${n}\"`).join(', ')} — name one of those, or omit compareTo.dimension to let the executor choose when there is only one.`\n : 'This selection declares no timeDimension with a dateRange, so there is no window to shift; give the dimension a dateRange (a dashboard date-range filter is the usual source).'),\n );\n }\n return cmp.dimension;\n }\n\n if (names.length === 1) return names[0];\n\n if (names.length === 0) {\n throw datasetInvalidError(\n '[dataset-executor] compareTo needs a dated window to shift, but this selection declares no '\n + 'timeDimension with a dateRange. Give the time dimension a dateRange (a dashboard date-range '\n + 'filter is the usual source), or drop compareTo — a period-over-period comparison is only '\n + 'defined against a bounded window.',\n );\n }\n\n throw datasetInvalidError(\n `[dataset-executor] compareTo.dimension is ambiguous: ${names.length} time dimensions carry a `\n + `dateRange (${names.map((n) => `\"${n}\"`).join(', ')}). Name the one to shift — `\n + `compareTo: { kind: '${cmp.kind}', dimension: '${names[0]}' }.`,\n );\n}\n\n/** Compute the comparison window for a [start,end] range. */\nexport function shiftRange(range: [string, string], kind: CompareTo['kind']): [string, string] {\n const [start, end] = range;\n if (kind === 'previousYear') {\n return [shiftYear(start, -1), shiftYear(end, -1)];\n }\n // previousPeriod — the equal-length window ending the day before `start`.\n const startMs = parseUTC(start);\n const endMs = parseUTC(end);\n const lengthDays = Math.round((endMs - startMs) / DAY_MS) + 1;\n const prevEndMs = startMs - DAY_MS;\n const prevStartMs = prevEndMs - (lengthDays - 1) * DAY_MS;\n return [toISODate(prevStartMs), toISODate(prevEndMs)];\n}\n\n// ── compareTo bucket alignment (#6007) ───────────────────────────────────────\n\n/**\n * The ISO-8601 week label (`2026-W23`) of the UTC calendar day at `ms`.\n *\n * Mirrors the week branch of `@objectstack/objectql`'s `bucketDateValue` — the\n * function that MINTS the bucket keys this executor then has to realign. It is\n * copied rather than imported because `service-analytics` does not depend on\n * `objectql` (it talks to the runtime through `IAnalyticsService`), and the\n * copy is not a blind one: {@link bucketKeyAtOrdinal} is pinned round-trip\n * against `bucketKeyToCalendarRange` — `@objectstack/core`'s exported INVERSE\n * of the same vocabulary, which rejects an impossible week outright — so a\n * drift in either direction fails a test rather than mislabelling a bucket.\n */\nfunction isoWeekKeyOfUtcMs(ms: number): string {\n const target = new Date(ms);\n const dayNum = (target.getUTCDay() + 6) % 7; // Mon=0..Sun=6\n target.setUTCDate(target.getUTCDate() - dayNum + 3); // that week's Thursday\n const firstThursday = new Date(Date.UTC(target.getUTCFullYear(), 0, 4));\n const weekNo =\n 1 +\n Math.round(\n ((target.getTime() - firstThursday.getTime()) / DAY_MS - 3 + ((firstThursday.getUTCDay() + 6) % 7)) / 7,\n );\n return `${target.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`;\n}\n\n/**\n * The ORDINAL of the bucket a UTC calendar day falls in: a monotone integer\n * that advances by exactly 1 per bucket, at every granularity.\n *\n * This is what makes \"the same relative position in the other window\" a\n * computable thing rather than an array index. Counting POSITIONS in the two\n * result sets would have been the obvious implementation and is wrong for the\n * reason every gap-sensitive alignment is wrong: a bucket the current window\n * reported no rows for is missing from that array, so every later bucket\n * silently shifts by one and the comparison column lands on its neighbour.\n * Ordinals are computed from the CALENDAR, so a gap costs nothing.\n *\n * @param ymd - a `YYYY-MM-DD` UTC calendar day.\n */\nexport function bucketOrdinalOfDay(ymd: string, granularity: DateGranularityValue): number {\n const ms = parseUTC(ymd);\n const d = new Date(ms);\n const y = d.getUTCFullYear();\n const m = d.getUTCMonth(); // 0-11\n switch (granularity) {\n case 'year':\n return y;\n case 'quarter':\n return y * 4 + Math.floor(m / 3);\n case 'month':\n return y * 12 + m;\n // 1970-01-01 was a Thursday, so shifting by 3 days puts the Monday boundary\n // on a multiple of 7 and the ordinal advances exactly at each ISO week start.\n case 'week':\n return Math.floor((ms + 3 * DAY_MS) / (7 * DAY_MS));\n case 'day':\n default:\n return Math.floor(ms / DAY_MS);\n }\n}\n\n/**\n * The canonical bucket KEY at an ordinal — the inverse of\n * {@link bucketOrdinalOfDay}, and the only place this package mints a bucket key\n * of its own.\n *\n * The keys produced here MUST be byte-identical to the ones the runtime's\n * grouping produced for the primary pass, because they are compared as merge\n * keys: `2026-01`, `2026-Q1`, `2026`, `2026-01-07`, `2026-W03`. That equality is\n * pinned round-trip against `bucketKeyToCalendarRange` rather than asserted by\n * eye — see `dataset-compare-bucket-alignment.test.ts`.\n */\nexport function bucketKeyAtOrdinal(ordinal: number, granularity: DateGranularityValue): string {\n switch (granularity) {\n case 'year':\n return String(ordinal);\n case 'quarter':\n return `${Math.floor(ordinal / 4)}-Q${(ordinal % 4) + 1}`;\n case 'month':\n return `${Math.floor(ordinal / 12)}-${String((ordinal % 12) + 1).padStart(2, '0')}`;\n case 'week':\n return isoWeekKeyOfUtcMs(ordinal * 7 * DAY_MS - 3 * DAY_MS);\n case 'day':\n default:\n return toISODate(ordinal * DAY_MS);\n }\n}\n\n/**\n * Restate ONE comparison bucket key in CURRENT-period terms (#6007) — the\n * maintainer's ruling of 2026-08-07, direction 1.\n *\n * ## What goes wrong without it\n *\n * When the dimension `compareTo` anchors on is ALSO a grid dimension — a trend\n * chart's own time axis, the standard \"this period vs the same period last\n * year\" shape — the comparison pass groups the SHIFTED window, so its rows key\n * to shifted buckets. {@link mergeByDimensions} keys on `selection.dimensions`,\n * and `2025-01` is not `2026-01`, so not one comparison row ever merged: every\n * one of them was APPENDED as a new row. The grid then read half real value and\n * half confident `0` on every row (`fillEmptyGroups` filling the halves each\n * pass never reported), with the shifted buckets sitting in it as rows from\n * outside the very window the caller filtered to. A 2×2 answer came back as\n * 4 rows × 2 columns with a zero in each.\n *\n * ## The two shift-backs\n *\n * - **`previousYear`** — the shift is a calendar year, so the inverse is a\n * calendar year forward, applied to the bucket's own first day and re-bucketed.\n * `2025-01` → `2026-01`, `2025-Q1` → `2026-Q1`, `2025-W03` → `2026-W03`\n * (the week one year on, which is how a year-over-year weekly trend reads).\n * It is deliberately the exact inverse of the {@link shiftRange} arithmetic\n * that produced the window, `setUTCFullYear` rollover included, so window and\n * key can never disagree about what \"one year\" meant.\n * - **`previousPeriod`** — an arbitrary-length day window has no calendar\n * counterpart, so alignment is by **bucket ordinal**: the n-th bucket of the\n * previous window is the n-th bucket of this one, with `n` counted from each\n * window's own start. This is a semantic the ruling states, not one derived\n * from anything already here.\n *\n * ## Fail-closed, in both directions\n *\n * Returns `null` — meaning \"leave this row's key alone\", i.e. exactly the\n * pre-#6007 behaviour — whenever the alignment is not certain:\n *\n * - the key is not a string (the empty bucket keys as `null` on both\n * aggregation paths, #3839; the two passes' empty buckets already merge with\n * each other, and \"one year after nothing\" is not a date);\n * - the key is not a bucket key of this granularity, so there is no span to\n * shift (a raw timestamp from an unbucketed date dimension lands here);\n * - the shifted-back bucket falls outside the CURRENT window. Two equal-length\n * day windows can tile into different bucket counts (a 31-day window\n * straddling a month boundary yields two month buckets, its neighbour one),\n * and the ordinal rule then has a last bucket with no counterpart. Moving it\n * to a bucket the caller did not ask for would trade a visibly foreign row\n * for a plausible-looking wrong one; it keeps its own key and appends, as it\n * did before.\n *\n * @param currentRange - the selection's own window for the anchor dimension.\n * @param shiftedRange - what {@link shiftRange} made of it.\n */\nexport function alignedCompareBucketKey(\n key: unknown,\n granularity: DateGranularityValue,\n kind: CompareTo['kind'],\n currentRange: [string, string],\n shiftedRange: [string, string],\n): string | null {\n if (typeof key !== 'string' || key.length === 0) return null;\n const span = bucketKeyToCalendarRange(key, granularity);\n if (!span) return null;\n\n const targetOrdinal =\n kind === 'previousYear'\n ? bucketOrdinalOfDay(shiftYear(span.start, 1), granularity)\n : bucketOrdinalOfDay(span.start, granularity) +\n (bucketOrdinalOfDay(currentRange[0], granularity) -\n bucketOrdinalOfDay(shiftedRange[0], granularity));\n\n const first = bucketOrdinalOfDay(currentRange[0], granularity);\n const last = bucketOrdinalOfDay(currentRange[1], granularity);\n if (targetOrdinal < first || targetOrdinal > last) return null;\n return bucketKeyAtOrdinal(targetOrdinal, granularity);\n}\n\nexport class DatasetExecutor {\n /**\n * @param service - The analytics service the executor issues its queries to.\n * @param orderLabels - Optional sort-key label hook (#3680). When provided,\n * an order key naming a label-bearing (`select`/`lookup`) dimension sorts\n * by its display label instead of the stored value. Omit to sort by stored\n * values everywhere (e.g. the draft-preview path, whose seed rows already\n * carry display names).\n */\n constructor(\n private readonly service: IAnalyticsService,\n private readonly orderLabels?: OrderLabelResolver,\n ) {}\n\n /**\n * Execute a dataset selection and return the shaped rows (+ field metadata).\n *\n * @param context - The request's ExecutionContext, threaded into every\n * underlying `IAnalyticsService.query` so the tenant/RLS read scope is\n * applied per request (ADR-0021 D-C).\n */\n async execute(\n compiledInput: CompiledDataset,\n selectionInput: DatasetSelection,\n context?: ExecutionContext,\n ): Promise<AnalyticsResult> {\n // framework#3582 — expand `{current_quarter_start}` / `{current_user_id}`\n // placeholders BEFORE any query is shaped, once for the whole call so every\n // sub-query (measure-scoped, totals, compareTo) shares one instant.\n const { compiled, selection } = resolveSelectionTokens(compiledInput, selectionInput, context);\n\n const result = await this.executeSelection(compiled, selection, context);\n\n // Server-side totals (#1753) — re-run the selection grouped by each\n // requested dimension subset, so a subtotal/grand total is the measure's\n // TRUE aggregate over the underlying rows (an avg total is the average of\n // all rows, not of bucket averages). Re-running the full pipeline keeps\n // measure-scoped filters, derived measures, and compareTo consistent with\n // the primary grid. order/limit/offset are dropped: totals cover the whole\n // selection, and an order key may reference a dimension the grouping drops.\n const groupings = selection.totals?.groupings;\n if (groupings?.length) {\n const selected = new Set(selection.dimensions ?? []);\n const totals: NonNullable<AnalyticsResult['totals']> = [];\n for (const grouping of groupings) {\n const unknown = grouping.filter((d) => !selected.has(d));\n if (unknown.length) {\n // [#5367] `DATASET_INVALID` / 400 — `selection.totals.groupings` is\n // request input, judged against the caller's own `selection.dimensions`.\n throw datasetInvalidError(\n `[dataset-executor] totals grouping [${grouping.join(', ')}] is not a subset of the selected dimensions — unknown: ${unknown.join(', ')}.`,\n );\n }\n const sub = await this.executeSelection(compiled, {\n ...selection,\n dimensions: grouping,\n totals: undefined,\n order: undefined,\n limit: undefined,\n offset: undefined,\n }, context);\n totals.push({ dimensions: grouping, rows: sub.rows });\n }\n result.totals = totals;\n }\n\n return result;\n }\n\n private async executeSelection(\n compiled: CompiledDataset,\n selection: DatasetSelection,\n context?: ExecutionContext,\n ): Promise<AnalyticsResult> {\n const derivedByName = new Map(compiled.derived.map((d) => [d.name, d]));\n const selectedDerived = selection.measures\n .map((m) => derivedByName.get(m))\n .filter((d): d is DerivedMeasureSpec => !!d);\n\n // Base measures = selected non-derived + dependencies of selected derived.\n const baseMeasures = new Set<string>();\n for (const m of selection.measures) {\n if (!derivedByName.has(m)) baseMeasures.add(m);\n }\n for (const d of selectedDerived) {\n for (const dep of d.of) baseMeasures.add(dep);\n }\n\n // Split measures into those with a scoped filter and those without.\n const { unfiltered, filtered } = splitMeasuresByFilter(baseMeasures, compiled.measureFilters);\n\n const baseFilter = combineFilters(compiled.filter, selection.runtimeFilter);\n const dimensions = selection.dimensions ?? [];\n\n // Effective ordering — validated against what this selection projects, with\n // a deterministic dimension order synthesized for a bare `limit` (#3588) and\n // an ascending default on the time axis (#3916).\n const order = resolveOrdering(selection, dimensions, this.timeDimensionsOf(compiled, dimensions));\n\n // #3680 — order keys naming a select/lookup dimension sort by the DISPLAY\n // label the response will carry, not the stored value / FK id. Resolved\n // over the assembled grid below; identified up front because such a key\n // also disqualifies SQL pushdown (the label is not a column the database\n // could ORDER BY, and a SQL LIMIT would truncate the wrong window).\n const labelOrderKeys = this.orderLabels\n ? Object.keys(order ?? {}).filter(\n (k) => dimensions.includes(k) && this.orderLabels!.isLabelBearing(k),\n )\n : [];\n\n // Push `order`/`limit`/`offset` down into the SQL only when this selection\n // is ONE query whose columns can satisfy them. With supplementary\n // measure-scoped queries, a compareTo pass, or derived measures in play, the\n // grid is assembled from several results — a sub-query LIMIT would drop rows\n // before the merge and an ORDER BY would name a column that sub-query never\n // selects. Those cases order in memory below instead.\n const singleQuery = filtered.length === 0 && !selection.compareTo && selectedDerived.length === 0;\n const pushDownKeys = new Set<string>([...dimensions, ...unfiltered]);\n const canPushDownWindow =\n singleQuery && labelOrderKeys.length === 0 &&\n Object.keys(order ?? {}).every((k) => pushDownKeys.has(k));\n const windowQuery = canPushDownWindow\n ? { order, limit: selection.limit, offset: selection.offset }\n : undefined;\n\n // The current-period pass: unfiltered base measures in one query plus one\n // supplementary query per measure-scoped filter, merged by dimension key.\n const result = await this.runMeasurePass(compiled, selection, {\n measures: [...baseMeasures],\n dimensions,\n baseFilter,\n window: windowQuery,\n context,\n });\n\n // compareTo — run the SAME pass over the shifted window and attach.\n if (selection.compareTo) {\n const compareRows = await this.runCompare(compiled, selection, [...baseMeasures], dimensions, baseFilter, context);\n result.rows = mergeByDimensions(\n result.rows,\n compareRows,\n dimensions,\n [...baseMeasures].map((m) => `${m}__compare`),\n );\n for (const m of baseMeasures) result.fields.push({ name: `${m}__compare`, type: 'number' });\n }\n\n // Empty-group fill (#4708) — a group a query never reported reads `0` for a\n // count/sum and stays null for avg/min/max. See {@link fillEmptyGroups} for\n // why this belongs to the executor and not to every widget author.\n //\n // Placed after EVERY merge and before the derived pass, because each merge\n // is a place a column can go missing and `mergeByDimensions` APPENDS rows:\n // - a supplementary measure-scoped query omits the groups its filter\n // excluded (the \"won nothing\" rows — the original defect);\n // - a later supplementary query can append rows for dimension keys no\n // earlier query saw, and those need the same fill;\n // - the compareTo pass appends a row for every bucket that existed in the\n // PREVIOUS window and not in this one, on which *every* base measure is\n // absent — including unfiltered ones, which is why the fill covers all\n // base measures rather than only the filter-scoped ones. Since #6007\n // that append is once again the EDGE it was always documented to be\n // (\"this group sold last month and nothing this month\"): when the\n // shifted dimension is the grid's own time axis the comparison keys are\n // realigned onto the current period first, so the append no longer\n // fires on every single row and no longer carries a bucket from outside\n // the caller's window.\n // Running it before the compare merge left that last class blank, so a lead\n // source that sold last month and nothing this month rendered as \"no data\"\n // instead of 0 — the same worst-row bias, one merge later.\n //\n // Derived measures are evaluated AFTER, so a ratio over a filled 0 computes\n // (0%) instead of being poisoned by an absent operand.\n const fillColumns: Record<string, string | undefined> = {};\n for (const m of baseMeasures) {\n const aggregate = compiled.cube.measures?.[m]?.type;\n fillColumns[m] = aggregate;\n if (selection.compareTo) fillColumns[`${m}__compare`] = aggregate;\n }\n fillEmptyGroups(result.rows, fillColumns);\n\n // Derived measures (computed from base + compare columns already present).\n result.rows = evaluateDerivedMeasures(result.rows, selectedDerived);\n for (const d of selectedDerived) result.fields.push({ name: d.name, type: 'number' });\n\n // Order + window the assembled grid (#3588). Every column the caller may\n // sort by exists by now — merged measure-scoped values, `__compare`\n // columns, and derived measures included. When the window was already\n // pushed into SQL this re-sorts an already-sorted grid (a no-op) and\n // re-slices an already-sliced one; when it could not be (the ObjectQL\n // aggregate path has no ordering grammar, and date-bucketed queries are\n // forced down it), this is what makes `sortBy` work at all.\n //\n // #3680 — for label-bearing order keys, substitute the display label as\n // the SORT KEY, resolved over the grid's distinct values BEFORE the window\n // (a \"top 10 by account name\" must pick the ten by name). Rows keep their\n // raw values — display rewriting stays in `queryDataset`, after the drill\n // metadata snapshots the stored values. A select dimension resolves from\n // field metadata (no query); a lookup costs one batched id→name read.\n let sortKeys: Record<string, Map<unknown, unknown>> | undefined;\n for (const key of labelOrderKeys) {\n const values = [...new Set(result.rows.map((r) => r[key]).filter((v) => v != null))];\n if (values.length === 0) continue;\n const labels = await this.orderLabels!.resolveLabels(key, values);\n if (labels && labels.size > 0) (sortKeys ??= {})[key] = labels;\n }\n result.rows = applyOrdering(result.rows, order, sortKeys);\n result.rows = applyWindow(result.rows, selection.limit, selection.offset);\n\n return result;\n }\n\n /**\n * Run ONE grouped pass over a set of base measures, honouring each measure's\n * own scoped `filter`: the unfiltered measures in a single query, plus one\n * supplementary query per filter-scoped measure, merged back by dimension key.\n *\n * **This is the executor's only implementation of \"how a measure filter is\n * applied\", and every window goes through it** — the current period, each\n * `totals` subset (which re-enters via `executeSelection`), and the\n * `compareTo` window. Before #4820 the comparison window had its own,\n * simpler answer: one shifted query over all base measures with only the\n * base filter, so `compiled.measureFilters` was never read on that path.\n * `won_count` counted won deals and `won_count__compare` counted every deal,\n * under one label, in adjacent columns. Only measures carrying a filter were\n * wrong — which is what made it survive: the unfiltered ones next to them\n * compared correctly.\n *\n * The caller supplies the `selection` this pass queries under, which is how\n * the comparison window differs at all: same measures, same dimensions, same\n * filters — a `timeDimensions` shifted by {@link shiftRange}. Nothing else\n * about the two passes may drift, because anything that does becomes a\n * discrepancy between two columns the reader is invited to subtract.\n *\n * Cost: one extra query per filter-scoped measure when `compareTo` is set.\n * The alternative — declaring the discrepancy in the response — is not one,\n * since the two columns exist to be directly comparable.\n *\n * @param window - Ordering/window to push into the SQL. Only ever set for a\n * selection the caller proved is a single self-sufficient query; a pass\n * that fans out must return its whole grid for the merge.\n */\n private async runMeasurePass(\n compiled: CompiledDataset,\n selection: DatasetSelection,\n opts: {\n measures: string[];\n dimensions: string[];\n baseFilter?: FilterCondition;\n window?: { order?: Record<string, 'asc' | 'desc'>; limit?: number; offset?: number };\n context?: ExecutionContext;\n },\n ): Promise<AnalyticsResult> {\n const { measures, dimensions, baseFilter, window, context } = opts;\n const { unfiltered, filtered } = splitMeasuresByFilter(measures, compiled.measureFilters);\n\n // Primary query: all unfiltered base measures in one pass. When every base\n // measure is filter-scoped, the supplementary queries below build the grid.\n const primary = unfiltered.length > 0 || filtered.length === 0;\n let result: AnalyticsResult;\n if (primary) {\n result = await this.service.query(this.buildQuery(compiled, {\n measures: unfiltered,\n dimensions,\n where: baseFilter,\n selection,\n contextTimezone: context?.timezone,\n window,\n }), context);\n } else {\n result = { rows: [], fields: [] };\n }\n\n // Supplementary queries: one per measure-scoped filter, merged by dimension key.\n const measureNames = new Set(measures);\n for (const [i, m] of filtered.entries()) {\n const mFilter = combineFilters(baseFilter, compiled.measureFilters[m]);\n const sub = await this.service.query(this.buildQuery(compiled, {\n measures: [m], dimensions, where: mFilter, selection,\n contextTimezone: context?.timezone,\n }), context);\n result.rows = mergeByDimensions(result.rows, sub.rows, dimensions, [m]);\n // #5537 — with NO primary query there is nothing carrying the grid's\n // DIMENSION descriptors, and this loop only ever appends MEASURE ones. The\n // dimension columns are still in every row (they are the merge key), so\n // the grid silently described only half of itself: a consumer reading\n // column metadata got no `label` and no `type` for the grouped column and\n // fell back to humanizing the raw key — \"owner\" where the dataset declares\n // \"Owner\". Adopt them from the FIRST supplementary result, which projects\n // exactly the same dimensions this pass groups by.\n //\n // Deliberately taken from the sub-result rather than re-derived from\n // `compiled.cube` here. `buildFieldMeta` (both strategies) and the\n // draft-preview evaluator are the three producers of this shape, and each\n // decides its own projected set — every `dimensions` entry plus each\n // granular `timeDimensions` entry not already among them (#4033's \"one\n // definition, every consumer\"). An executor-side reconstruction would be a\n // fourth copy of that rule, free to drift from the very rows it labels;\n // reading the sub-result makes the two paths converge BY CONSTRUCTION, on\n // whatever the active strategy projects. Non-measure entries only, so the\n // measure descriptor stays the one this loop appends.\n if (!primary && i === 0) {\n for (const f of sub.fields ?? []) {\n if (!measureNames.has(f.name)) result.fields.push(f);\n }\n }\n result.fields.push({ name: m, type: 'number' });\n }\n\n return result;\n }\n\n /**\n * The selected dimensions the compiled cube types as `time`, in selection\n * order (#3916) — the axis {@link resolveOrdering} defaults to ascending.\n *\n * Membership is decided by the DIMENSION's declared type, not by whether the\n * selection happens to bucket it: a `date` dimension left ungranulated groups\n * raw timestamps, and those want chronological order every bit as much as\n * month buckets do. (Both sort correctly — `compareValues` compares Dates and\n * ISO strings chronologically, and bucket keys are minted sort-stable.)\n */\n private timeDimensionsOf(compiled: CompiledDataset, dimensions: string[]): string[] {\n return dimensions.filter((d) => compiled.cube.dimensions[d]?.type === 'time');\n }\n\n /**\n * The EFFECTIVE bucket size one dimension is grouped at for this selection,\n * or `undefined` when it is not a date dimension or nothing states a size (in\n * which case the runtime groups the raw column).\n *\n * One definition, two readers, deliberately: {@link buildQuery} uses it to\n * decide the `GROUP BY`, and {@link runCompare} uses it to realign the\n * comparison pass's bucket keys (#6007). Those two MUST agree — realigning\n * `month` keys a query grouped by `quarter` would move every comparison value\n * onto a bucket that does not exist — and the way to make them agree is to\n * have one of them, not two that look alike.\n */\n private granularityOf(\n compiled: CompiledDataset,\n selection: DatasetSelection,\n name: string,\n ): DateGranularityValue | undefined {\n const cd = compiled.cube.dimensions[name];\n if (cd?.type !== 'time') return undefined;\n const datasetDefault = cd.granularities?.length === 1 ? String(cd.granularities[0]) : undefined;\n return resolveDimensionGranularity(selection, name, datasetDefault);\n }\n\n private buildQuery(\n compiled: CompiledDataset,\n opts: {\n measures: string[];\n dimensions: string[];\n where?: FilterCondition;\n selection: DatasetSelection;\n contextTimezone?: string;\n /**\n * Ordering/window to push DOWN into this query. Set only for a selection\n * the caller proved is a single self-sufficient query (see\n * `canPushDownWindow`); omitted for supplementary/compare sub-queries,\n * which must return their full grid for the merge.\n */\n window?: { order?: Record<string, 'asc' | 'desc'>; limit?: number; offset?: number };\n },\n ): AnalyticsQuery {\n const q: AnalyticsQuery = {\n cube: compiled.cube.name,\n measures: opts.measures,\n dimensions: opts.dimensions,\n // Precedence: explicit selection tz → request's reference tz\n // (ExecutionContext.timezone, ADR-0053 Phase 2) → UTC.\n timezone: opts.selection.timezone ?? opts.contextTimezone ?? 'UTC',\n };\n if (opts.where) q.where = opts.where as Record<string, unknown>;\n // Bucket selected date dimensions. Without this a date dimension groups by\n // the raw timestamp — one bucket per ROW, which is why a \"new accounts by\n // month\" bar chart drew one bar per account instead of one per month\n // (#3588).\n //\n // Granularity precedence, per dimension:\n // 1. a `granularity` already stated on that dimension's\n // `selection.timeDimensions` entry — never overridden;\n // 2. `selection.dateGranularity` — the PRESENTATION's choice, so a widget\n // can bucket by month without the dataset committing every consumer to\n // that granularity;\n // 3. the dataset dimension's own default (the compiler lowers an explicit\n // `dateGranularity` to a single-entry `granularities`; the 5-entry\n // \"all granularities\" list means the dataset stated no default).\n //\n // Note the unit of precedence is the GRANULARITY, not the entry: a stated\n // `granularity` is never overridden, and an entry that states none does not\n // veto a bucket size the selection or the dataset supplies for a dimension\n // that IS being grouped. That is `resolveDimensionGranularity`'s job, and it\n // is unchanged — see `granularityFor` below.\n //\n // What an entry that carries only a `dateRange` decides is something else:\n // whether the dimension is grouped AT ALL (#5688). It is a WINDOW — a\n // filter, and a dashboard date-range picker is the usual source. A filter\n // removes rows; it does not add a grid column or split rows. Backfilling the\n // dataset's default bucket size onto such an entry turned \"by Owner, this\n // quarter\" into \"by Owner × month\": one extra column nobody selected, and\n // one row per owner PER MONTH, with a KPI card then reading the first of\n // them. So the backfill is scoped by {@link bucketsUnstatedEntry} to the\n // entries that are genuinely asking for a bucket.\n //\n // `compareTo` alignment survives that narrowing BY CONSTRUCTION, and this is\n // the pairing to keep in mind before widening or narrowing it again\n // (#3588/#4870 vs #5688 — the two demands meet exactly here):\n //\n // - the comparison pass re-enters this method through `runMeasurePass`\n // with the SAME `opts.dimensions` and the same `selection.dateGranularity`,\n // differing only in the shifted `dateRange`. Both passes therefore reach\n // the same verdict for the same entry, so the two grids are bucketed\n // alike or not at all — never one of each, which is the state #4870\n // fixed (a month-bucketed primary grid merged against raw-timestamp\n // comparison rows shared no dimension key, and every `__compare` column\n // came back empty);\n // - an anchor that IS grouped (listed in `dimensions`, or carrying its own\n // `granularity`) keeps its bucket on both passes — #3588/#4870 intact;\n // - an anchor used only as a window is grouped on NEITHER pass, so it is\n // not a column on either side and the two grids align on the dimensions\n // `mergeByDimensions` actually keys by. That merge has always keyed on\n // `dimensions` alone, so the bucket column the backfill added was never\n // part of the key: with several month-split rows per group the\n // comparison value landed on whichever one the index happened to hold\n // last and the rest read a confident `0`. Not bucketing the window\n // restores the alignment rather than weakening it.\n const selTimeDims = opts.selection.timeDimensions ?? [];\n const selDims = new Set(selTimeDims.map((t) => t.dimension));\n const groupedDims = new Set(opts.dimensions);\n const granularityFor = (name: string): string | undefined =>\n this.granularityOf(compiled, opts.selection, name);\n /**\n * Does a caller-supplied entry that stated NO granularity get one filled in?\n *\n * Only when something in the request says this date is being bucketed:\n * - the dimension is one of the grid dimensions this query groups by (so\n * it is a column regardless, and leaving it unbucketed would group raw\n * timestamps — one bucket per row, the #3588 defect); or\n * - `selection.dateGranularity` is set, which is the presentation stating\n * a bucket size for its date axes.\n * An entry carrying its own `granularity` never reaches here (nothing to\n * fill in), and stays grouped.\n *\n * The dataset dimension's own `dateGranularity` is deliberately NOT such a\n * signal on its own: it is how this date renders WHEN grouped, not a request\n * to group by it. Reading it as one is what made a date-range filter behave\n * like a second GROUP BY.\n */\n const bucketsUnstatedEntry = (dimension: string): boolean =>\n groupedDims.has(dimension) || opts.selection.dateGranularity != null;\n // Fill in a bucket size for caller-supplied entries that named none — for\n // the entries that are asking to be bucketed at all.\n const resolvedTimeDims = selTimeDims.map((t) => {\n if (t.granularity) return t;\n if (!bucketsUnstatedEntry(t.dimension)) return t;\n const granularity = granularityFor(t.dimension);\n return granularity ? { ...t, granularity } : t;\n });\n const explicitTimeDims: Array<{ dimension: string; granularity: string }> = [];\n for (const name of opts.dimensions) {\n if (selDims.has(name)) continue;\n const granularity = granularityFor(name);\n if (granularity) explicitTimeDims.push({ dimension: name, granularity });\n }\n const mergedTimeDims = [...resolvedTimeDims, ...explicitTimeDims];\n if (mergedTimeDims.length > 0) q.timeDimensions = mergedTimeDims as AnalyticsQuery['timeDimensions'];\n // Ordering/window: pushed down ONLY when the caller vouched for it. The\n // executor always re-applies both over the assembled grid, so omitting them\n // here costs correctness nothing — it only moves the work to memory.\n if (opts.window?.order && Object.keys(opts.window.order).length > 0) q.order = opts.window.order;\n if (opts.window?.limit != null) q.limit = opts.window.limit;\n if (opts.window?.offset != null) q.offset = opts.window.offset;\n return q;\n }\n\n private async runCompare(\n compiled: CompiledDataset,\n selection: DatasetSelection,\n measures: string[],\n dimensions: string[],\n baseFilter: FilterCondition | undefined,\n context?: ExecutionContext,\n ): Promise<Record<string, unknown>[]> {\n const cmp = selection.compareTo!;\n // `dimension` is optional since #5011; resolve it (or fail loudly) before\n // touching a window. Both the \"which one?\" and the \"with a dateRange\"\n // questions are answered in one place — see `resolveCompareDimension`.\n const dimension = resolveCompareDimension(selection);\n const td = (selection.timeDimensions ?? []).find((t) => t.dimension === dimension)!;\n const range: [string, string] = Array.isArray(td.dateRange)\n ? [td.dateRange[0], td.dateRange[1] ?? td.dateRange[0]]\n : [td.dateRange as string, td.dateRange as string];\n const shifted = shiftRange(range, cmp.kind);\n const shiftedTd = (selection.timeDimensions ?? []).map((t) =>\n t.dimension === dimension ? { ...t, dateRange: shifted } : t,\n );\n // Run the SAME pass the current period ran, over the shifted window: same\n // measures, same dimensions, same base filter, and — since #4820 — the same\n // measure-scoped filters, applied by the same supplementary sub-queries.\n // Issuing one flat query here instead is what made `<measure>__compare`\n // report a different measure than the column beside it.\n //\n // Going through `runMeasurePass` (and so `buildQuery`) also keeps the\n // comparison pass bucketing its date dimensions EXACTLY like the primary\n // pass. Hand-rolling the query here skipped granularity resolution, so a\n // bucketed primary grid (\"2026-04\") was merged against raw-timestamp\n // comparison rows and no dimension key ever matched — every `__compare`\n // column came back empty. The shifted `timeDimensions` still win for their\n // own dimension (rule 1 of the precedence chain); `window` is deliberately\n // omitted — the comparison grid must stay whole for the merge.\n const sub = await this.runMeasurePass(\n compiled,\n { ...selection, timeDimensions: shiftedTd },\n { measures, dimensions, baseFilter, context },\n );\n\n // #6007 — when the anchor is ALSO a grid dimension, the comparison rows key\n // to the SHIFTED buckets they were grouped into, and `mergeByDimensions`\n // keys on `dimensions`: `2025-01` never equals `2026-01`, so every\n // comparison row was appended instead of merged and the grid came back with\n // twice the rows, a confident `0` in each, and the shifted buckets showing\n // as rows from outside the caller's own window. Restate each comparison\n // bucket key in current-period terms BEFORE the merge — see\n // {@link alignedCompareBucketKey} for the two shift-backs and for every\n // case it deliberately declines to align.\n //\n // Scoped to exactly the shape that is broken, and no wider:\n // - the anchor must be a GRID dimension. A window-only anchor is not a\n // column on either pass, so the two grids already align on the\n // dimensions the merge keys by (#5688) and there is nothing to move;\n // - it must be BUCKETED. An ungrouped date dimension groups raw\n // timestamps, which are instants and not bucket keys — there is no\n // \"same bucket, one year on\" for them, so they are left alone.\n // Both passes read the bucket size through the same `granularityOf`, so the\n // size realigned here is by construction the size grouped by.\n const granularity = dimensions.includes(dimension)\n ? this.granularityOf(compiled, selection, dimension)\n : undefined;\n\n // Rename measure columns to `<measure>__compare` so they merge alongside primary.\n return sub.rows.map((row) => {\n const out: Record<string, unknown> = {};\n for (const dim of dimensions) out[dim] = row[dim];\n if (granularity) {\n const aligned = alignedCompareBucketKey(row[dimension], granularity, cmp.kind, range, shifted);\n if (aligned != null) out[dimension] = aligned;\n }\n for (const m of measures) out[`${m}__compare`] = row[m];\n return out;\n });\n }\n}\n\n/**\n * Key segment for a dimension whose value is null/undefined (#4821).\n *\n * Every other segment {@link dimensionKeyOf} emits begins with a decimal digit\n * (its length prefix), so a segment beginning with anything else cannot be\n * produced by any real value — which is the entire requirement for a sentinel.\n *\n * `undefined` keys the same as `null` deliberately: a row that OMITS the column\n * and a row carrying an explicit null both mean \"no value here\", and drivers do\n * omit null columns from row objects. Splitting on that would re-introduce, one\n * level down, exactly the cross-query split this key exists to prevent.\n */\nconst NULL_DIMENSION_SEGMENT = '~';\n\n/**\n * The composite index key for one row's dimension tuple — **length-prefixed**,\n * so no two distinct tuples can share a key and no character is reserved\n * (#4821).\n *\n * ## What was actually wrong\n *\n * The previous key was `dimensions.map((d) => String(row[d] ?? '')).join(SOH)`,\n * where SOH was a **raw U+0001 byte written literally into the source**. #4821\n * was filed against it reading `join('')`, because a raw control byte renders as\n * nothing — in a terminal, in a GitHub issue body, and in this file. So the\n * headline defect it reports (`['ab','c']` and `['a','bc']` both keying `\"abc\"`)\n * did not in fact reproduce: the separator was present, merely invisible. Two\n * things did:\n *\n * 1. `?? ''` keys a genuinely NULL dimension identically to an empty-string\n * one, so \"unassigned\" merges into \"blank\" — one real group absorbing\n * another's measures, silently, which is the outcome #4821 describes,\n * reached through its second mechanism rather than its first.\n * 2. A single-character separator is unambiguous only while no dimension VALUE\n * contains that character. Dimension values are user data (text fields,\n * imported records), so that is an assumption, not a guarantee — and it\n * fails exactly as silently as the issue predicted.\n *\n * A length prefix settles (2) by construction — `2:ab1:c` and `1:a2:bc` differ\n * for every possible input, nothing is reserved, and no invisible byte is left\n * in the source for the next reader to misread (this comment's own issue was\n * filed because of one). (1) is settled separately and explicitly, by\n * {@link NULL_DIMENSION_SEGMENT}.\n *\n * ## Why each segment is still `String()`-coerced\n *\n * Deliberately — and it is NOT the trade-off {@link rebucketCrossObject} makes\n * one file over, whose key JSON-encodes each part. That function re-buckets the\n * rows of ONE aggregate result: every value in a column came back from a single\n * query and so carries a single JS type, which makes JSON free there and buys a\n * real distinction (the empty bucket `null` vs. the literal string `\"null\"`).\n *\n * This key does the opposite job — it ALIGNS rows across **different queries**:\n * the primary pass against each measure-scoped supplementary pass, and (since\n * #4870) the current window against the shifted `compareTo` window, which now\n * fans out per measure the same way. Nothing guarantees two queries type the\n * same group identically, and {@link compareValues} records this executor's own\n * encounter with it: \"numeric strings, which is how some drivers return SUM\n * results\". `String()` per segment is what keys numeric `1` and string `'1'` the\n * same, so such rows keep merging. A `JSON.stringify` key renders them `1` vs\n * `\"1\"` and would split a group that merges correctly today — trading one silent\n * defect for a new one. Pinned by test; do not \"simplify\" it away.\n */\nfunction dimensionKeyOf(row: Record<string, unknown>, dimensions: string[]): string {\n let key = '';\n for (const d of dimensions) {\n const value = row[d];\n if (value == null) {\n key += NULL_DIMENSION_SEGMENT;\n continue;\n }\n const s = String(value);\n key += `${s.length}:${s}`;\n }\n return key;\n}\n\n/**\n * Left-merge `extra` rows onto `base` rows by their dimension-key tuple,\n * copying the listed value columns. Rows in `extra` with no base match are\n * appended (outer-ish merge so comparison-only buckets still surface).\n *\n * Rows are matched by {@link dimensionKeyOf} — read its notes before changing\n * how the key is built. Both the ambiguity it removes and the type coercion it\n * keeps are load-bearing, and both fail silently when got wrong.\n */\nexport function mergeByDimensions(\n base: Record<string, unknown>[],\n extra: Record<string, unknown>[],\n dimensions: string[],\n valueColumns: string[],\n): Record<string, unknown>[] {\n const keyOf = (row: Record<string, unknown>) => dimensionKeyOf(row, dimensions);\n const index = new Map<string, Record<string, unknown>>();\n for (const row of base) index.set(keyOf(row), row);\n\n for (const row of extra) {\n const key = keyOf(row);\n const target = index.get(key);\n if (target) {\n for (const c of valueColumns) target[c] = row[c];\n } else {\n const fresh: Record<string, unknown> = {};\n for (const d of dimensions) fresh[d] = row[d];\n for (const c of valueColumns) fresh[c] = row[c];\n index.set(key, fresh);\n base.push(fresh);\n }\n }\n return base;\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Dimension display-label resolution (ADR-0021).\n *\n * Analytics groups by the raw stored value of a dimension field. For two field\n * kinds that value is NOT human-readable:\n *\n * - **select** — grouped by the stored option `value` (e.g. `backlog`), but the\n * user-facing text is the option `label` (e.g. `Backlog`).\n * - **lookup / master_detail** — grouped by the foreign-key `id` (e.g.\n * `8eqtuKI4G9IhUsPS`), but the user-facing text is the related record's\n * display field (its name/title).\n *\n * `resolveDimensionLabels` post-processes the result rows IN PLACE, replacing the\n * raw value at `row[dimension.name]` with its display label when one is found.\n * Unresolved values are left untouched so an orphaned id still renders as itself\n * rather than blanking out. Date / number / plain-string dimensions are no-ops.\n *\n * The resolution LOGIC lives here (and is unit-tested); the low-level capabilities\n * — reading an object's field map and fetching id→label pairs — are injected via\n * {@link DimensionLabelDeps} so this module stays free of any engine dependency.\n */\n\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\n\n/** The minimal field shape this resolver needs. */\nexport interface FieldMetaLite {\n type?: string;\n /** Lookup / master_detail target object name. */\n reference?: string;\n /** Select options — the value→label source. */\n options?: Array<{ value: unknown; label?: string }>;\n}\n\n/** Capabilities the resolver needs from the runtime (injected by the plugin). */\nexport interface DimensionLabelDeps {\n /** Return the field map for an object, or `undefined` if unknown. */\n getObjectFields(objectName: string): Record<string, FieldMetaLite> | undefined;\n /**\n * Fetch a map of `id → display label` for the given ids of a target object.\n * The implementation chooses the target's display field. Returning an empty\n * map (e.g. no display field, no data access) leaves the ids unresolved.\n *\n * `scope` (ADR-0021 D-C, #3602) is the TARGET object's own read scope — the\n * RLS/tenant `FilterCondition` the implementation must AND into the label\n * lookup so this never reveals a related record the target object's RLS would\n * hide. The label lookup is a per-record read (`group by id`) dressed as an\n * aggregate; without the scope it leaks display names whenever the referenced\n * object is more restricted than the base object whose rows carry the id.\n * `undefined` means \"no scope for this object\" (global table / unrestricted\n * caller) — the same contract as the read-scope provider.\n *\n * `context` is the request's ExecutionContext — the SECOND belt on the same\n * read (#3602). `scope` is the analytics layer's own predicate; forwarding the\n * context lets the ENGINE's middleware chain scope this per-record read\n * itself, so it stays scoped even if a caller ever reaches this hook without\n * a resolved `scope`. Implementations bridging to an ObjectQL engine MUST\n * forward it; a bridge with nowhere to put it may ignore it.\n */\n fetchRecordLabels(\n targetObject: string,\n ids: unknown[],\n scope?: Record<string, unknown>,\n context?: ExecutionContext,\n ): Promise<Map<unknown, string>>;\n}\n\n/**\n * Resolve the TARGET object's read scope for a label lookup (#3602). Returns the\n * object's RLS/tenant `FilterCondition`, `null`/`undefined` when the object is\n * unscoped, or a rejected promise when the scope cannot be resolved — in which\n * case the resolver fails CLOSED (skips that dimension's labels) rather than\n * fetching unscoped names.\n */\nexport type LabelScopeResolver = (\n targetObject: string,\n) => Promise<Record<string, unknown> | null | undefined> | Record<string, unknown> | null | undefined;\n\nconst LOOKUP_TYPES = new Set(['lookup', 'master_detail']);\n\n/**\n * Sort-key label resolution for `DatasetSelection.order` (#3680).\n *\n * The executor sorts the assembled grid BEFORE `queryDataset` rewrites stored\n * dimension values into display labels, so an order key naming a `select` or\n * `lookup`/`master_detail` dimension used to sort by the stored value / FK id —\n * an order that presents as arbitrary once the labels render. This hook hands\n * the executor JUST the value→label mapping for such a dimension so it can sort\n * by what the user will actually read, while the rows keep their raw values\n * (drill metadata depends on them) and ordering + windowing stay one adjacent\n * step. The executor stays engine-free: it sees this interface, never the\n * engine behind it.\n */\nexport interface OrderLabelResolver {\n /**\n * Whether the dimension's stored value differs from the label it renders as\n * (`select` options, `lookup`/`master_detail` FK ids). Synchronous — the\n * executor consults it when deciding whether the window may be pushed into\n * SQL, before any query runs.\n */\n isLabelBearing(dimension: string): boolean;\n /**\n * Map the given raw stored values of one dimension to display labels.\n * Values missing from the map sort by their raw form — the same thing the\n * user will see rendered for them.\n */\n resolveLabels(dimension: string, values: unknown[]): Promise<Map<unknown, string> | undefined>;\n}\n\n/**\n * Build the executor's {@link OrderLabelResolver} from the dataset's dimension\n * list and the injected label capabilities. Mirrors the classification in\n * {@link resolveDimensionLabels}: a dimension is label-bearing when its field\n * carries select `options` or is a lookup/master_detail with a `reference`.\n *\n * - `select` resolves from field metadata — no query at all.\n * - `lookup`/`master_detail` costs ONE batched id→name read over the distinct\n * grouped values, scoped to the REFERENCED object's own RLS (#3602). Fail\n * closed: an unresolvable scope degrades to sorting by the stored id rather\n * than fetching unscoped — consistent with the display pass, which renders\n * the raw id in that case too.\n */\nexport function createOrderLabelResolver(\n baseObject: string,\n dims: Array<{ name: string; field: string }>,\n deps: DimensionLabelDeps,\n resolveScope?: LabelScopeResolver,\n context?: ExecutionContext,\n): OrderLabelResolver {\n const dimByName = new Map(dims.map((d) => [d.name, d]));\n const metaFor = (dimension: string): FieldMetaLite | undefined => {\n const dim = dimByName.get(dimension);\n return dim ? deps.getObjectFields(baseObject)?.[dim.field] : undefined;\n };\n return {\n isLabelBearing(dimension) {\n const meta = metaFor(dimension);\n if (!meta) return false;\n if (Array.isArray(meta.options) && meta.options.length > 0) return true;\n return !!(meta.type && LOOKUP_TYPES.has(meta.type) && meta.reference);\n },\n async resolveLabels(dimension, values) {\n const meta = metaFor(dimension);\n if (!meta) return undefined;\n if (Array.isArray(meta.options) && meta.options.length > 0) {\n const labelByValue = new Map<unknown, string>();\n for (const opt of meta.options) {\n if (opt && opt.label != null) labelByValue.set(opt.value, String(opt.label));\n }\n return labelByValue;\n }\n if (meta.type && LOOKUP_TYPES.has(meta.type) && meta.reference) {\n let scope: Record<string, unknown> | null | undefined;\n if (resolveScope) {\n try {\n scope = await resolveScope(meta.reference);\n } catch {\n return undefined;\n }\n }\n return deps.fetchRecordLabels(meta.reference, values, scope ?? undefined, context);\n }\n return undefined;\n },\n };\n}\n\n/**\n * Wrap a {@link DimensionLabelDeps} so repeated `fetchRecordLabels` calls\n * within ONE request fetch each id at most once. A selection that sorts by a\n * lookup dimension resolves labels twice — once PRE-window for the sort keys\n * (#3680, over the full grid's ids), once post-window for display (a subset of\n * the same ids) — so with this cache the display pass costs no extra query.\n *\n * Per-request only: entries are keyed by target object alone, which is safe\n * because an object's read scope is constant within one request. Never share\n * an instance across requests.\n */\nexport function withLabelFetchCache(deps: DimensionLabelDeps): DimensionLabelDeps {\n // Per target object: id → label, with `null` marking \"fetched, no label\"\n // (RLS-hidden or orphaned) so unresolvable ids are not re-fetched every call.\n const cache = new Map<string, Map<unknown, string | null>>();\n return {\n getObjectFields: (objectName) => deps.getObjectFields(objectName),\n async fetchRecordLabels(targetObject, ids, scope, context) {\n let known = cache.get(targetObject);\n if (!known) {\n known = new Map();\n cache.set(targetObject, known);\n }\n const missing = ids.filter((id) => !known.has(id));\n if (missing.length > 0) {\n const fetched = await deps.fetchRecordLabels(targetObject, missing, scope, context);\n for (const id of missing) known.set(id, fetched.get(id) ?? null);\n }\n const out = new Map<unknown, string>();\n for (const id of ids) {\n const label = known.get(id);\n if (label != null) out.set(id, label);\n }\n return out;\n },\n };\n}\n\n/** Date-dimension granularity (mirrors the dataset `dateGranularity` enum). */\nexport type DateGranularity = 'day' | 'week' | 'month' | 'quarter' | 'year';\n\nconst pad = (n: number) => String(n).padStart(2, '0');\n\n/**\n * Format a raw date value (epoch-ms number, numeric string, ISO string, or\n * Date) to a human, sort-stable bucket label per granularity. Returns the input\n * unchanged when it isn't a parseable date, so a non-date value never blanks.\n *\n * year → \"2026\"\n * quarter → \"2026-Q2\"\n * month → \"2026-04\"\n * week → \"2026-04-13\" (ISO date of the bucket)\n * day → \"2026-04-15\"\n *\n * Intentionally UTC-only (ADR-0053 Phase 2): timezone bucketing happens\n * upstream in `bucketDate` / `bucketDateValue`, so by the time a value reaches\n * here it is *already* the reference-zone bucket (often a label string like\n * \"2026-Q2\"). Re-applying a timezone here would shift an already-correct\n * `YYYY-MM-DD` day bucket by a day — this is a pure, idempotent re-labeler.\n */\nexport function formatDateBucket(value: unknown, granularity?: DateGranularity | string): unknown {\n if (value == null || value instanceof Date === false) {\n if (typeof value !== 'number' && typeof value !== 'string') return value;\n }\n // A YEAR bucket's canonical key IS the bare year (\"2026\" / 2026) — which the\n // epoch heuristic below would read as 2026 milliseconds and relabel \"1970\".\n // Being idempotent over already-formatted bucket keys is this function's whole\n // contract, and every other granularity's key already survives the round trip\n // (\"2026-Q2\", \"2026-07\", \"2026-07-15\" all fail the pure-digit test); only the\n // year key collides with it. Recognised before parsing, for both the string\n // and numeric forms drivers return.\n if (granularity === 'year') {\n const y = typeof value === 'number' ? value : Number(String(value).trim());\n if (Number.isInteger(y) && y >= 1000 && y <= 9999) return String(y);\n }\n let d: Date;\n if (value instanceof Date) d = value;\n else if (typeof value === 'number') d = new Date(value);\n else {\n const s = String(value).trim();\n // Pure-digit strings are epoch millis (or seconds); otherwise let Date parse ISO.\n d = /^\\d+$/.test(s) ? new Date(Number(s) < 1e12 ? Number(s) * 1000 : Number(s)) : new Date(s);\n }\n if (Number.isNaN(d.getTime())) return value;\n const y = d.getUTCFullYear();\n const m = d.getUTCMonth(); // 0-11\n switch (granularity) {\n case 'year': return String(y);\n case 'quarter': return `${y}-Q${Math.floor(m / 3) + 1}`;\n case 'month': return `${y}-${pad(m + 1)}`;\n case 'week':\n case 'day':\n default: return `${y}-${pad(m + 1)}-${pad(d.getUTCDate())}`;\n }\n}\n\n/**\n * Replace raw dimension values with display labels, in place.\n *\n * @param baseObject - the dataset's base object (where the dimension fields live)\n * @param dims - selected dimensions as `{ name, field, type?, dateGranularity? }`\n * (row key = `name`)\n * @param rows - result rows, mutated in place\n * @param deps - injected runtime capabilities\n * @param resolveScope - (ADR-0021 D-C, #3602) resolves the referenced object's\n * own read scope for a lookup/master_detail dimension's label fetch. When it\n * throws, that dimension's labels are SKIPPED (fail-closed — the raw id renders\n * instead) rather than fetched unscoped. Omit when no read-scope provider is\n * configured (labels then fetch unscoped, as before — no security in play).\n * @param context - the request's ExecutionContext, forwarded to\n * {@link DimensionLabelDeps.fetchRecordLabels} so the engine's own middleware\n * scopes the per-record label read too — the second belt beside `resolveScope`\n * (#3602)\n */\nexport async function resolveDimensionLabels(\n baseObject: string,\n dims: Array<{ name: string; field: string; type?: string; dateGranularity?: DateGranularity | string }>,\n rows: Record<string, unknown>[],\n deps: DimensionLabelDeps,\n resolveScope?: LabelScopeResolver,\n context?: ExecutionContext,\n): Promise<void> {\n if (!rows.length || !dims.length) return;\n const fields = deps.getObjectFields(baseObject);\n if (!fields) return;\n\n for (const dim of dims) {\n const meta = fields[dim.field];\n\n // ── date: epoch / ISO → human bucket label ────────────────────────\n // A date dimension's grouped value is a raw timestamp (or a bucket start);\n // either way it must render as a readable date, not epoch millis.\n if (dim.type === 'date' || (meta && meta.type === 'date')) {\n for (const row of rows) {\n const formatted = formatDateBucket(row[dim.name], dim.dateGranularity);\n if (formatted != null) row[dim.name] = formatted;\n }\n continue;\n }\n\n if (!meta) continue;\n\n // ── select: value → option label ──────────────────────────────────\n if (Array.isArray(meta.options) && meta.options.length > 0) {\n const labelByValue = new Map<unknown, string>();\n for (const opt of meta.options) {\n if (opt && opt.label != null) labelByValue.set(opt.value, String(opt.label));\n }\n if (labelByValue.size === 0) continue;\n for (const row of rows) {\n const raw = row[dim.name];\n const label = labelByValue.get(raw);\n if (label != null) row[dim.name] = label;\n }\n continue;\n }\n\n // ── lookup / master_detail: id → related record display name ───────\n if (meta.type && LOOKUP_TYPES.has(meta.type) && meta.reference) {\n const ids = Array.from(\n new Set(rows.map((r) => r[dim.name]).filter((v) => v != null)),\n );\n if (ids.length === 0) continue;\n // #3602 — the label lookup reads the REFERENCED object by id. Scope it to\n // that object's own RLS so it never surfaces a related record the target's\n // RLS would hide (leak fires when the referenced object is stricter than\n // the base). Fail closed: if the scope can't be resolved, skip this\n // dimension's labels (raw id renders) rather than fetch unscoped.\n let scope: Record<string, unknown> | null | undefined;\n if (resolveScope) {\n try {\n scope = await resolveScope(meta.reference);\n } catch {\n continue;\n }\n }\n const labelById = await deps.fetchRecordLabels(meta.reference, ids, scope ?? undefined, context);\n if (!labelById || labelById.size === 0) continue;\n for (const row of rows) {\n const label = labelById.get(row[dim.name]);\n if (label != null) row[dim.name] = label;\n }\n }\n }\n}\n\n/**\n * Pick the display field for an object from its field map, by convention:\n * an explicit `name`/`title`/`label` field, else the first text-like field.\n * Returns `undefined` when nothing suitable exists.\n */\nexport function pickDisplayField(\n fields: Record<string, FieldMetaLite> | undefined,\n): string | undefined {\n if (!fields) return undefined;\n for (const preferred of ['name', 'title', 'label']) {\n if (fields[preferred]) return preferred;\n }\n for (const [name, meta] of Object.entries(fields)) {\n if (meta.type === 'text' || meta.type === 'string') return name;\n }\n return undefined;\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n//\n// ADR-0037 Phase 3 — draft data preview: evaluate an AnalyticsQuery over an\n// in-memory row set (the pending `seed` draft's records) instead of the real\n// data engine. This is what lets a Live Canvas dashboard chart REAL numbers\n// from the DRAFTED sample data before anything is published — and because\n// publish materializes the *same* seed, the numbers are continuous across\n// the publish boundary.\n//\n// Scope (deliberately the dataset-query subset, not a general engine):\n// • Mongo-style `where` filters ($eq implicit, $ne/$gt/$gte/$lt/$lte/\n// $between/$in/$nin/$contains, $and/$or/$not)\n// • timeDimensions date-range filtering + granularity bucketing\n// (day/week/month/quarter/year)\n// • group-by dimensions; count / countDistinct / sum / avg / min / max\n// • order + limit/offset\n// Anything beyond (joins via `include`, raw SQL) falls back to the caller's\n// normal execution path — the preview simply doesn't claim it.\n\nimport { calendarPartsInTzOrUtc, nextUtcCalendarDay, utcInstantMs } from '@objectstack/core';\nimport type { AnalyticsQuery, AnalyticsResult } from '@objectstack/spec/contracts';\nimport type { Cube } from '@objectstack/spec/data';\n\ntype Row = Record<string, unknown>;\n\n// ── Filters (the unified Query DSL subset) ──────────────────────────────────\n\n/**\n * Order two operands the way every other filter backend orders them.\n *\n * The `Date` arm is load-bearing rather than defensive: `String(new Date())`\n * is `'Mon Jul 27 2026 …'`, which under the plain string ordering below sorts\n * AFTER every `'2026-…'` comparand — so a preview row carrying an instant both\n * disappeared from windows it belongs in and appeared in ones it does not.\n * Measured against the shared matrix, 10 of 16 cases diverged, and unlike the\n * cross-type silence on the drivers this direction ADDS rows: a drafted chart\n * showed numbers no published chart would.\n *\n * The population is real. `Field.datetime`'s storage form is a BSON `Date` on\n * `driver-mongodb` (ADR-0053 D-E2), so rows fetched from a mongo-backed dataset\n * arrive here as `Date` objects, while the comparands are wire text.\n * {@link utcInstantMs} is the same primitive `formula`'s write-side evaluator\n * uses for the same pairing, so the two type-blind surfaces cannot drift.\n *\n * Deliberately narrow: the lift runs only when one side is a `Date` and both\n * read as instants, so string-vs-string keeps ISO lexicographic ordering and a\n * `Field.time` wall clock — which denotes no instant — is left untouched.\n */\nfunction compare(a: unknown, b: unknown): number {\n if (typeof a === 'number' && typeof b === 'number') return a - b;\n if (a instanceof Date || b instanceof Date) {\n const ai = utcInstantMs(a);\n const bi = utcInstantMs(b);\n if (ai !== null && bi !== null) return ai - bi;\n }\n return String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0;\n}\n\n/**\n * The inclusive-upper-bound comparison, with the calendar-day rule (#3777): a\n * bare-day bound means \"through that whole day\", so it is evaluated half-open\n * against the next day. String ordering makes `< nextDay` equivalent to\n * `<= day` for plain date values, so this needs no field-type lookup — which\n * matters here, because the preview sees drafted rows with no schema.\n *\n * Shared by `$lte` and the max of `$between` so the two cannot drift apart.\n */\nfunction lteBound(value: unknown, bound: unknown): boolean {\n const nextDay = nextUtcCalendarDay(bound);\n if (nextDay != null) return compare(value, nextDay) < 0;\n return compare(value, bound) <= 0;\n}\n\nfunction matchOp(value: unknown, op: string, expected: unknown): boolean {\n switch (op) {\n case '$eq': return value === expected || String(value) === String(expected);\n case '$ne': return !(value === expected || String(value) === String(expected));\n case '$gt': return value != null && compare(value, expected) > 0;\n case '$gte': return value != null && compare(value, expected) >= 0;\n case '$lt': return value != null && compare(value, expected) < 0;\n case '$lte': {\n if (value == null) return false;\n // A bare-day upper bound means \"through that whole day\" (#3777): the SQL\n // paths compile it half-open (`< day+1`), and the preview must agree or\n // a drafted chart shows different numbers than the published one. String\n // ordering makes `< nextDay` equivalent to `<= day` for plain date\n // values, so no type lookup is needed here either.\n return lteBound(value, expected);\n }\n case '$between': {\n // Was absent, so it fell to the permissive `default` and matched EVERY\n // row — a drafted chart with a range filter silently charted the whole\n // dataset, then changed at publish (found by the ADR-0053 D-A3 matrix,\n // #4081). The max takes the same whole-day rule as `$lte`.\n if (value == null || !Array.isArray(expected) || expected.length !== 2) return false;\n const [min, max] = expected;\n if (min == null || max == null) return false;\n return compare(value, min) >= 0 && lteBound(value, max);\n }\n case '$in': return Array.isArray(expected) && expected.some((e) => value === e || String(value) === String(e));\n case '$nin': return Array.isArray(expected) && !expected.some((e) => value === e || String(value) === String(e));\n case '$contains': return String(value ?? '').toLowerCase().includes(String(expected ?? '').toLowerCase());\n default: return true; // unknown operator — permissive (preview, reads only)\n }\n}\n\nexport function matchesWhere(row: Row, where: Record<string, unknown> | undefined): boolean {\n if (!where) return true;\n for (const [key, cond] of Object.entries(where)) {\n if (key === '$and') {\n if (!(cond as Row[]).every((c) => matchesWhere(row, c as Row))) return false;\n } else if (key === '$or') {\n if (!(cond as Row[]).some((c) => matchesWhere(row, c as Row))) return false;\n } else if (key === '$not') {\n if (matchesWhere(row, cond as Row)) return false;\n } else if (cond !== null && typeof cond === 'object' && !Array.isArray(cond)) {\n for (const [op, expected] of Object.entries(cond as Row)) {\n if (!matchOp(row[key], op, expected)) return false;\n }\n } else if (!(row[key] === cond || String(row[key]) === String(cond))) {\n return false; // implicit equality\n }\n }\n return true;\n}\n\n// ── Time bucketing ──────────────────────────────────────────────────────────\n\nexport function bucketDate(value: unknown, granularity: string, timezone?: string): string | null {\n const d = new Date(String(value));\n if (Number.isNaN(d.getTime())) return null;\n // ADR-0053 Phase 2: resolve the calendar day in the reference zone so an\n // instant near a tz day-boundary buckets where a user in that zone expects.\n // Unset / 'UTC' / invalid keeps the historical UTC bucketing.\n const { year: y, month, day: dayNum } = calendarPartsInTzOrUtc(d, timezone);\n const m = `${month}`.padStart(2, '0');\n const day = `${dayNum}`.padStart(2, '0');\n switch (granularity) {\n case 'year': return `${y}`;\n case 'quarter': return `${y}-Q${Math.floor((month - 1) / 3) + 1}`;\n case 'month': return `${y}-${m}`;\n case 'week': {\n // Build a UTC date from the zone-shifted parts, then step back to Monday.\n const monday = new Date(Date.UTC(y, month - 1, dayNum));\n const dow = (monday.getUTCDay() + 6) % 7; // Monday=0\n monday.setUTCDate(monday.getUTCDate() - dow);\n return monday.toISOString().slice(0, 10);\n }\n case 'day':\n default:\n return `${y}-${m}-${day}`;\n }\n}\n\n// ── Aggregation ─────────────────────────────────────────────────────────────\n\nfunction aggregate(rows: Row[], metricType: string, field: string): number {\n if (metricType === 'count' || field === '*') {\n if (metricType === 'countDistinct') {\n return new Set(rows.map((r) => r[field]).filter((v) => v != null)).size;\n }\n return rows.length;\n }\n const nums = rows.map((r) => Number(r[field])).filter((n) => Number.isFinite(n));\n switch (metricType) {\n case 'countDistinct': return new Set(rows.map((r) => r[field]).filter((v) => v != null)).size;\n case 'sum': return nums.reduce((a, b) => a + b, 0);\n case 'avg': return nums.length ? nums.reduce((a, b) => a + b, 0) / nums.length : 0;\n case 'min': return nums.length ? Math.min(...nums) : 0;\n case 'max': return nums.length ? Math.max(...nums) : 0;\n default: return nums.length ? nums.reduce((a, b) => a + b, 0) : rows.length;\n }\n}\n\n/**\n * Evaluate `query` over `rows` using the cube's measure/dimension specs.\n * Mirrors the engine strategies' output contract: rows keyed by bare\n * measure/dimension names, `fields` describing each output column.\n */\nexport function evaluateAnalyticsQueryOverRows(\n query: AnalyticsQuery,\n cube: Cube,\n rows: Row[],\n): AnalyticsResult {\n // 1. Row-level filters: `where`, then timeDimension dateRanges.\n let filtered = rows.filter((r) => matchesWhere(r, query.where));\n const timeDims = query.timeDimensions ?? [];\n for (const td of timeDims) {\n const dim = cube.dimensions?.[td.dimension];\n const field = String(dim?.sql ?? td.dimension);\n if (!td.dateRange) continue;\n const [start, end] = Array.isArray(td.dateRange) ? td.dateRange : [td.dateRange, td.dateRange];\n filtered = filtered.filter((r) => {\n const v = String(r[field] ?? '');\n // Bare-day end → half-open `< day+1`, the same translation the SQL\n // paths apply (#3777); a full-timestamp end keeps the historical\n // `'~'`-suffix trick (inclusive of that instant's own sub-values).\n const nextDay = nextUtcCalendarDay(end);\n const inUpper = nextDay != null ? v < nextDay : v <= `${end}~`;\n return v >= String(start) && inUpper;\n });\n }\n\n // 2. Grouping keys: each selected dimension (time dims bucketed).\n const dimensions = query.dimensions ?? [];\n const timezone = query.timezone; // ADR-0053 Phase 2: reference tz for bucketing\n const granByDim = new Map(timeDims.filter((t) => t.granularity).map((t) => [t.dimension, t.granularity!]));\n const keyOf = (r: Row): { key: string; values: Row } => {\n const values: Row = {};\n for (const name of dimensions) {\n const dim = cube.dimensions?.[name];\n const field = String(dim?.sql ?? name);\n const raw = r[field];\n const gran = granByDim.get(name) ?? (dim?.type === 'time' && dim.granularities?.length === 1 ? String(dim.granularities[0]) : undefined);\n values[name] = gran ? bucketDate(raw, gran, timezone) : (raw ?? null);\n }\n return { key: JSON.stringify(values), values };\n };\n\n const groups = new Map<string, { values: Row; rows: Row[] }>();\n for (const r of filtered) {\n const { key, values } = keyOf(r);\n const g = groups.get(key) ?? { values, rows: [] };\n g.rows.push(r);\n groups.set(key, g);\n }\n // No dimensions → a single overall group (even over zero rows: count = 0).\n if (dimensions.length === 0 && groups.size === 0) {\n groups.set('{}', { values: {}, rows: [] });\n }\n\n // 3. Aggregate each measure per group.\n const out: Row[] = [];\n for (const g of groups.values()) {\n const row: Row = { ...g.values };\n for (const m of query.measures) {\n const metric = cube.measures?.[m];\n row[m] = aggregate(g.rows, String(metric?.type ?? 'count'), String(metric?.sql ?? '*'));\n }\n out.push(row);\n }\n\n // 4. Order + paging.\n for (const [col, dir] of Object.entries(query.order ?? {}).reverse()) {\n out.sort((a, b) => (dir === 'desc' ? -1 : 1) * compare(a[col], b[col]));\n }\n const offset = query.offset ?? 0;\n const limited = out.slice(offset, query.limit != null ? offset + query.limit : undefined);\n\n return {\n rows: limited,\n fields: [\n ...dimensions.map((d) => ({ name: d, type: 'string' })),\n ...query.measures.map((m) => ({ name: m, type: 'number' })),\n ],\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { Cube, FilterCondition } from '@objectstack/spec/data';\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\nimport type { IAnalyticsService, IDataDriver } from '@objectstack/spec/contracts';\nimport { AnalyticsService } from './analytics-service.js';\nimport type { AnalyticsServiceConfig } from './analytics-service.js';\nimport type { AnalyticsDriverCapabilities } from './strategies/types.js';\nimport { pickDisplayField, type DimensionLabelDeps } from './dimension-labels.js';\n\n/**\n * Minimal IDataEngine surface required for the auto-bridge.\n * ObjectQL exposes:\n * - `aggregate(object, { where, groupBy, aggregations: [{ function, field, alias }] })`\n * - `execute(sql, options)` for raw SQL pass-through (enables NativeSQLStrategy\n * and lets the analytics layer emit JOINs for relation traversal).\n */\ninterface DataEngineLike {\n aggregate(object: string, options: {\n where?: Record<string, unknown>;\n groupBy?: string[];\n aggregations?: Array<{ function: string; field: string; alias: string }>;\n /** Reference timezone (IANA) for date bucketing — ADR-0053 Phase 2. */\n timezone?: string;\n /**\n * `BaseEngineOptions.context` — identity/tenant of the request. The engine\n * merges it into the operation context (`mergeReadContext`), which is what\n * lets its middleware chain inject RLS into `opCtx.ast.where` (#3602).\n */\n context?: ExecutionContext;\n }): Promise<unknown[]>;\n /**\n * Raw command pass-through (SQL on driver-sql). The options bag is spelled out\n * rather than left as `Record<string, unknown>` because **`object` is load-bearing**:\n * `ObjectQL.execute()` picks its driver in the order\n * `options.object` → `getDriver(object)`, then `options.datasource`, then the\n * default driver. A command that reads an object and omits `object` therefore\n * runs against the DEFAULT datasource — which is how every dataset backed by a\n * telemetry/audit-routed object read `0` from a populated table (#5033).\n */\n execute?(command: unknown, options?: {\n /** Bound parameters for the command. */\n args?: unknown[];\n /** The object this command reads — routes to that object's own datasource. */\n object?: string;\n }): Promise<unknown>;\n /** Return the registered object schema (relationship → target + display-label resolution). */\n getObject?(name: string): {\n fields?: Record<string, {\n type?: string;\n reference?: string;\n options?: Array<{ value: unknown; label?: string }>;\n }>;\n /** Federation marker (ADR-0015): set on objects bound to an external datasource. */\n external?: unknown;\n } | undefined;\n /**\n * [#5288] The datasource an object's rows actually live on, by NAME — the\n * engine's own five-step resolution (explicit `datasource` →\n * `datasourceMapping` → the ADR-0057 §3.6 lifecycle split → the owning\n * package's `defaultDatasource` → the deployment default), not the value the\n * object declares.\n *\n * The declared value used to be read straight off `getObject().datasource`,\n * and it is only step 1 of those five: `ObjectSchema.datasource` defaults to\n * `'default'`, which in the engine means \"no explicit binding, keep looking\".\n * So every object routed by steps 2-4 — `sys_audit_log` among them, routed by\n * `lifecycle.class: 'audit'` — answered `'default'` and pointed diagnostics at\n * a database its rows are not in.\n *\n * `undefined` ⇒ nothing binds the object anywhere and it rides the\n * deployment's default datasource (or this engine cannot answer). Optional\n * because the analytics service runs against engines other than ObjectQL;\n * absent, the probe below simply never answers, which is the same \"cannot\n * answer, do not block\" tiering it already carries.\n */\n resolveEffectiveDatasource?(objectName: string): string | undefined;\n /**\n * Resolve the storage driver backing an object (public ObjectQL accessor).\n * Used to delegate temporal storage-form coercion to the driver, which is the\n * single source of truth for how a `Field.date`/`Field.datetime` is stored on\n * the active dialect. When the hooks are absent, values and column SQL pass\n * through untouched — the contract's identity semantics.\n */\n getDriverForObject?(objectName: string): TemporalDriverSurface | undefined;\n}\n\n/**\n * The slice of the `IDataDriver` CONTRACT the analytics layer consumes —\n * `temporalFilterValue` / `temporalFilterColumnSql` are first-class contract\n * members since ADR-0053 D-A2, no longer a duck-typed local invention. Picked\n * (rather than using `IDataDriver` whole) because `getDriverForObject` hands\n * back whatever the engine registered, and this seam only needs the temporal\n * surface; the runtime `typeof` guards below remain the correct way to consume\n * an optional contract member.\n */\ntype TemporalDriverSurface = Pick<\n IDataDriver,\n 'temporalFilterValue' | 'temporalFilterColumnSql'\n>;\n\n/**\n * Configuration for AnalyticsServicePlugin.\n */\nexport interface AnalyticsServicePluginOptions {\n /** Pre-defined cube definitions (from manifest). */\n cubes?: Cube[];\n /**\n * Probe driver capabilities for a given cube.\n * When omitted, defaults to in-memory only.\n */\n queryCapabilities?: (cubeName: string) => AnalyticsDriverCapabilities;\n /**\n * Execute raw SQL on a driver. Enables NativeSQLStrategy.\n */\n executeRawSql?: (objectName: string, sql: string, params: unknown[]) => Promise<Record<string, unknown>[]>;\n /**\n * Execute ObjectQL aggregate. Enables ObjectQLStrategy.\n */\n executeAggregate?: (objectName: string, options: {\n groupBy?: string[];\n aggregations?: Array<{ field: string; method: string; alias: string }>;\n filter?: Record<string, unknown>;\n /** Reference timezone (IANA) for date bucketing — ADR-0053 Phase 2. */\n timezone?: string;\n /**\n * ADR-0021 D-C (#3602) — the request's ExecutionContext. A custom bridge\n * MUST forward it to its engine so engine-side RLS applies; dropping it is\n * what made the built-in bridge fall open in #3597.\n */\n context?: ExecutionContext;\n }) => Promise<Record<string, unknown>[]>;\n /**\n * ADR-0021 D-C — context-aware per-object read scope (tenant + RLS). The\n * runtime supplies this from its sharing middleware so the analytics raw-SQL\n * path cannot bypass tenant isolation. Receives the request's ExecutionContext\n * and returns the RLS `FilterCondition` for the object (what `RLSCompiler`\n * emits). When omitted, the plugin auto-bridges to a registered `'security'`\n * service exposing `getReadFilter(object, context)` if one is present.\n */\n getReadScope?: (\n objectName: string,\n context?: ExecutionContext,\n ) =>\n | FilterCondition\n | null\n | undefined\n | Promise<FilterCondition | null | undefined>;\n /**\n * ADR-0021 D-C — join allowlist per cube (the dataset's declared `include`).\n * Typically wired from the dataset registry's compiled `allowedRelationships`.\n */\n getAllowedRelationships?: (cubeName: string) => Set<string> | undefined;\n /** Enable debug logging. */\n debug?: boolean;\n /**\n * [#8286] Echo the executed statement back to CALLERS in\n * `AnalyticsResult.sql` (`/api/v1/analytics/query`).\n *\n * Distinct from {@link AnalyticsServicePluginOptions.debug} above, which is\n * server-side log verbosity only: raising log level must never widen what\n * travels to a tenant. Default and rationale live with the service config —\n * see `AnalyticsServiceConfig.debugSql`. Undefined here means \"no host\n * choice\", which the service resolves to development-only.\n */\n debugSql?: boolean;\n}\n\n/**\n * AnalyticsServicePlugin — Kernel plugin for multi-driver analytics.\n *\n * Lifecycle:\n * 1. **init** — Creates `AnalyticsService`, registers as `'analytics'` service.\n * If an existing analytics service is already registered (e.g. MemoryAnalyticsService\n * from dev-plugin), it is captured as the `fallbackService`.\n * 2. **start** — Triggers `'analytics:ready'` hook so other plugins can\n * register cubes or extend the service.\n * 3. **destroy** — Cleans up references.\n *\n * @example\n * ```ts\n * import { LiteKernel } from '@objectstack/core';\n * import { AnalyticsServicePlugin } from '@objectstack/service-analytics';\n *\n * const kernel = new LiteKernel();\n * kernel.use(new AnalyticsServicePlugin({\n * cubes: [ordersCube],\n * queryCapabilities: (cube) => ({ nativeSql: true, objectqlAggregate: true, inMemory: false }),\n * executeRawSql: async (obj, sql, params) => pgPool.query(sql, params).then(r => r.rows),\n * }));\n * await kernel.bootstrap();\n *\n * const analytics = kernel.getService<IAnalyticsService>('analytics');\n * const result = await analytics.query({ cube: 'orders', measures: ['orders.count'] });\n * ```\n */\nexport class AnalyticsServicePlugin implements Plugin {\n name = 'com.objectstack.service-analytics';\n /**\n * Services init() registers on every path (ADR-0116, #4131) — lets the\n * kernel name this plugin when a consumer requires one before it inits.\n */\n providesServices = ['analytics'];\n version = '1.0.0';\n type = 'standard' as const;\n dependencies: string[] = [];\n /**\n * init() probes the `data` engine ObjectQLPlugin provides for the\n * auto-bridge — order-if-present so the probe verdict is deterministic\n * (ADR-0116, #4471). Soft, not hard: without an engine the plugin\n * degrades on purpose (per-query lazy resolution / explicit\n * `executeAggregate`).\n */\n optionalDependencies: string[] = ['com.objectstack.engine.objectql'];\n\n private service?: AnalyticsService;\n private readonly options: AnalyticsServicePluginOptions;\n\n constructor(options: AnalyticsServicePluginOptions = {}) {\n this.options = options;\n }\n\n async init(ctx: PluginContext): Promise<void> {\n // Check if there is an existing analytics service (e.g. from dev-plugin)\n let fallbackService: IAnalyticsService | undefined;\n try {\n const existing = ctx.getService<IAnalyticsService>('analytics');\n if (existing && typeof existing.query === 'function') {\n fallbackService = existing;\n ctx.logger.debug('[Analytics] Found existing analytics service, using as fallback');\n }\n } catch {\n // No existing service — that's fine\n }\n\n // Auto-bridge: when caller did not supply executeAggregate, look up the\n // kernel's IDataEngine (registered as 'data' by ObjectQLPlugin) lazily and\n // translate AnalyticsStrategy's `{method, filter}` shape into the engine's\n // `{function, where}` shape. This lets users write\n // `new AnalyticsServicePlugin({ cubes })`\n // without re-implementing the bridge in every app.\n let executeAggregate = this.options.executeAggregate;\n let autoBridged = false;\n if (!executeAggregate) {\n const tryGetDataEngine = (): DataEngineLike | undefined => {\n try {\n const svc = ctx.getService<DataEngineLike>('data');\n return svc && typeof svc.aggregate === 'function' ? svc : undefined;\n } catch {\n return undefined;\n }\n };\n // Probe now (warn if missing) but resolve at call time so plugin order\n // does not matter as long as 'data' exists by the time a query runs.\n if (!tryGetDataEngine()) {\n ctx.logger.warn(\n '[Analytics] No \"data\" service registered yet at init; ' +\n 'will retry per-query. Register ObjectQLPlugin or pass executeAggregate.',\n );\n }\n executeAggregate = async (objectName, { groupBy, aggregations, filter, timezone, context }) => {\n const engine = tryGetDataEngine();\n if (!engine) {\n throw new Error(\n '[Analytics] Cannot execute aggregate: no IDataEngine (\"data\") service is registered. ' +\n 'Add ObjectQLPlugin to the kernel or supply AnalyticsServicePlugin({ executeAggregate }).',\n );\n }\n const rows = await engine.aggregate(objectName, {\n where: filter,\n groupBy,\n aggregations: aggregations?.map((a) => ({\n function: a.method,\n field: a.field,\n alias: a.alias,\n })),\n // ADR-0053 Phase 2: thread the reference tz so date buckets resolve on\n // that zone's calendar days (engine buckets in-memory when non-UTC).\n timezone,\n // ADR-0021 D-C (#3602): thread the caller's identity so the engine's\n // middleware chain scopes the read itself. `BaseEngineOptions.context`\n // is `.optional()`, so nothing ever forced this bridge to pass it —\n // and it did not, which is how an authenticated aggregate reached the\n // engine with no principal and plugin-security fell open (#3597).\n context,\n });\n return rows as Record<string, unknown>[];\n };\n autoBridged = true;\n }\n\n // Auto-bridge raw SQL when the data engine exposes `execute()` and the\n // caller did not supply their own `executeRawSql`. This unlocks\n // NativeSQLStrategy (priority 10) which can emit `LEFT JOIN`s for\n // dotted dimension/measure references like `account.industry`.\n let executeRawSql = this.options.executeRawSql;\n let autoBridgedRawSql = false;\n if (!executeRawSql) {\n const tryGetExecutor = (): DataEngineLike | undefined => {\n try {\n const svc = ctx.getService<DataEngineLike>('data');\n return svc && typeof svc.execute === 'function' ? svc : undefined;\n } catch {\n return undefined;\n }\n };\n // Always wire the bridge — resolution happens at call time, mirroring\n // the executeAggregate auto-bridge above. This way plugin-init order\n // does not matter as long as `data` exists by the time a query runs.\n executeRawSql = async (objectName, sql, params) => {\n const engine = tryGetExecutor();\n if (!engine || !engine.execute) {\n throw new Error(\n '[Analytics] Cannot execute raw SQL: no IDataEngine (\"data\") service with execute() is registered.',\n );\n }\n // NativeSQLStrategy emits `$1, $2, …` placeholders. Knex (used by\n // driver-sql) speaks `?` placeholders, so translate.\n const knexSql = sql.replace(/\\$(\\d+)/g, '?');\n // #5033 — `object` is ObjectQL's FIRST driver-selection key. This bridge\n // received the object name and dropped it, so every dataset raw-SQL read\n // fell through to the DEFAULT driver while the object-routed path\n // (`executeAggregate` → `engine.aggregate(objectName, …)`, right above)\n // resolved the object's own datasource. The two dataset execution paths\n // must give ONE answer to \"which datasource is this object in\": an\n // object routed elsewhere (ADR-0057 §3.6 telemetry split, an explicit\n // `object.datasource`, a `datasourceMapping` rule) read `no such table`\n // on the default DB and degraded to a confident `0` over live rows.\n const result = await engine.execute(knexSql, { args: params, object: objectName });\n // A driver that cannot run SQL (e.g. the in-memory driver) returns\n // null from execute(). Silently mapping that to [] made EVERY dataset\n // query on such environments report \"No rows\" while looking healthy\n // (HTTP 200, compiled SQL attached). Throw a TYPED error instead so\n // the orchestrator can fall back to an aggregate-based strategy —\n // never fabricate an empty result.\n if (result === null || result === undefined) {\n const err = new Error(\n '[Analytics] The \"data\" engine\\'s driver returned null for raw SQL — ' +\n 'this driver does not support SQL execution. The query will fall back ' +\n 'to an aggregate-based strategy when one is available.',\n ) as Error & { code: string };\n err.code = 'RAW_SQL_UNSUPPORTED';\n throw err;\n }\n if (Array.isArray(result)) return result as Record<string, unknown>[];\n if (typeof result === 'object' && 'rows' in (result as Record<string, unknown>)) {\n return (result as { rows: Record<string, unknown>[] }).rows;\n }\n return [];\n };\n autoBridgedRawSql = true;\n }\n\n // Default capabilities: when we have an aggregate bridge, advertise\n // ObjectQL support so ObjectQLStrategy is selected. Callers can still\n // override via options.queryCapabilities.\n const queryCapabilities = this.options.queryCapabilities\n ?? (() => ({\n nativeSql: !!executeRawSql,\n objectqlAggregate: !!executeAggregate,\n inMemory: false,\n }));\n\n // ADR-0021 D-C — wire the read-scope provider. Prefer an explicit option;\n // otherwise auto-bridge to a registered `'security'` service that exposes\n // `getReadFilter(object, context)` (resolved at call time so plugin-init\n // order does not matter). This keeps analytics decoupled from security.\n interface SecurityReadFilter {\n getReadFilter(\n object: string,\n context?: ExecutionContext,\n ):\n | FilterCondition\n | null\n | undefined\n | Promise<FilterCondition | null | undefined>;\n }\n let getReadScope = this.options.getReadScope;\n let autoBridgedReadScope = false;\n let securityPresentAtInit = false;\n if (!getReadScope) {\n const trySecurity = (): SecurityReadFilter | undefined => {\n try {\n const svc = ctx.getService<SecurityReadFilter>('security');\n return svc && typeof svc.getReadFilter === 'function' ? svc : undefined;\n } catch {\n return undefined;\n }\n };\n // ALWAYS wire the bridge — resolution happens at call time, mirroring the\n // executeAggregate / executeRawSql auto-bridges above. Gating the\n // ASSIGNMENT on an init-time probe (as this did) made analytics RLS\n // silently plugin-ORDER-DEPENDENT: a kernel that registers this plugin\n // before the security plugin got NO read-scope provider at all, so every\n // strategy ran unscoped and only a WARN marked it. The repo's own\n // `bootStack` harness registers in exactly that order, which is why no\n // dogfood test could ever observe analytics RLS.\n securityPresentAtInit = !!trySecurity();\n getReadScope = (object, context) => trySecurity()?.getReadFilter(object, context);\n autoBridgedReadScope = true;\n }\n\n // ADR-0021 — relationship → target-object resolver. A dataset's `include`\n // names lookup/master_detail FIELDS on the base object; the joined TABLE is\n // each field's `reference` target (which can differ from the field name,\n // e.g. lookup `account` → object `crm_account`). Resolve from the 'data'\n // engine's object schema at compile time so cross-object joins target the\n // right table. Resolved lazily so plugin-init order doesn't matter.\n const relationshipResolver = (baseObject: string, relationshipName: string): string | undefined => {\n const engine = (() => {\n try {\n const svc = ctx.getService<DataEngineLike>('data');\n return svc && typeof svc.getObject === 'function' ? svc : undefined;\n } catch { return undefined; }\n })();\n const obj = engine?.getObject?.(baseObject);\n const field = obj?.fields?.[relationshipName];\n if (field && (field.type === 'lookup' || field.type === 'master_detail') && field.reference) {\n return field.reference;\n }\n // Unknown to the schema — fall back to the relationship name as the table\n // (legacy same-name convention). Returning undefined would make the\n // compiler reject the dataset; the name-as-table fallback is safer for\n // engines that don't expose getObject.\n return engine ? undefined : relationshipName;\n };\n\n // ADR-0021 — dimension display-label resolution. `queryDataset` groups by a\n // dimension's raw stored value; for `select` fields the user-facing text is\n // the option label, and for `lookup`/`master_detail` fields it's the related\n // record's display name. Wire the two low-level capabilities the resolver\n // needs from the 'data' engine (resolved lazily so plugin-init order is free):\n // - field metadata (select options + lookup target), via getObject\n // - id→name pairs, via the executeAggregate bridge (group by id + name)\n const dataEngine = (): DataEngineLike | undefined => {\n try {\n const svc = ctx.getService<DataEngineLike>('data');\n return svc && typeof svc.getObject === 'function' ? svc : undefined;\n } catch { return undefined; }\n };\n const labelResolver: DimensionLabelDeps = {\n getObjectFields: (objectName) => dataEngine()?.getObject?.(objectName)?.fields,\n fetchRecordLabels: async (targetObject, ids, scope, context) => {\n const map = new Map<unknown, string>();\n const displayField = pickDisplayField(dataEngine()?.getObject?.(targetObject)?.fields);\n if (!displayField || !executeAggregate || ids.length === 0) return map;\n // #3680 — the sort-key pass hands over the PRE-window id set (every\n // grouped value, not just the displayed page), so a high-cardinality\n // lookup dimension can push thousands of ids through here. Chunk the\n // `$in` so the bound-parameter count stays under every driver's limit\n // (SQLite's historic floor is 999 variables).\n const CHUNK = 500;\n for (let i = 0; i < ids.length; i += CHUNK) {\n // #3602 — AND the referenced object's own read scope into the id filter,\n // with `$and` (never key-merge) so it cannot be displaced by the id\n // predicate — the same composition the strategy uses for the aggregate.\n // Without it this per-record read leaks display names the target's RLS\n // would hide (fires when the referenced object is stricter than the base).\n const idFilter: Record<string, unknown> = { id: { $in: ids.slice(i, i + CHUNK) } };\n const filter = scope ? { $and: [idFilter, scope] } : idFilter;\n // Group by (id, displayField) — one row per record — reusing the aggregate\n // bridge rather than adding a record-fetch capability. A count keeps engines\n // that require ≥1 aggregation happy; the count itself is unused.\n const rows = await executeAggregate(targetObject, {\n groupBy: ['id', displayField],\n aggregations: [{ field: 'id', method: 'count', alias: '_c' }],\n filter,\n // #3602 second belt — `scope` above is the analytics layer's own\n // predicate on this per-record read; the context makes the engine's\n // middleware scope it as well.\n context,\n });\n for (const r of rows) {\n if (r.id != null && r[displayField] != null) map.set(r.id, String(r[displayField]));\n }\n }\n return map;\n },\n };\n\n // ADR-0037 P3 — draft data preview: resolve the PENDING seed draft's rows\n // for an object via the kernel protocol (state:'draft' read — a published\n // seed's rows are already in the real table and must NOT overlay). Lazy\n // service lookup so plugin order doesn't matter; null ⇒ no pending seed ⇒\n // queryDataset falls through to live data.\n const draftRowsResolver = async (objectName: string): Promise<Record<string, unknown>[] | null> => {\n type ProtocolLike = {\n getMetaItems?(req: { type: string; previewDrafts?: boolean }): Promise<unknown>;\n getMetaItem?(req: { type: string; name: string; state?: string }): Promise<unknown>;\n };\n let protocol: ProtocolLike | undefined;\n try {\n protocol = ctx.getService<ProtocolLike>('protocol');\n } catch { return null; }\n if (!protocol?.getMetaItems || !protocol.getMetaItem) return null;\n const res = await protocol.getMetaItems({ type: 'seed', previewDrafts: true }).catch(() => null);\n const list = Array.isArray(res)\n ? res\n : (res && typeof res === 'object' && Array.isArray((res as { items?: unknown[] }).items)\n ? (res as { items: unknown[] }).items\n : []);\n const rows: Record<string, unknown>[] = [];\n let pending = false;\n for (const entry of list) {\n const body = ((entry as { item?: unknown })?.item ?? entry) as { name?: string; object?: string } | null;\n if (!body?.name || body.object !== objectName) continue;\n // Only a PENDING draft row qualifies; getMetaItem({state:'draft'})\n // throws no_draft when the seed is already published.\n const draft = await protocol.getMetaItem({ type: 'seed', name: body.name, state: 'draft' }).catch(() => null);\n const draftBody = (draft as { item?: { records?: unknown[] } } | null)?.item;\n if (!draftBody) continue;\n pending = true;\n for (const r of Array.isArray(draftBody.records) ? draftBody.records : []) {\n if (r && typeof r === 'object') rows.push(r as Record<string, unknown>);\n }\n }\n return pending ? rows : null;\n };\n\n // Temporal storage-form coercion (fixes the SQLite datetime \"No rows\" bug).\n // The raw-SQL strategy binds dashboard relative-date tokens (already expanded\n // to ISO strings) directly, bypassing the driver's CRUD coercion. Delegate to\n // the driver — the single source of truth for the on-disk storage convention —\n // so a `Field.datetime` ISO comparand becomes epoch ms on SQLite, while\n // `Field.date` text and native-timestamp (Postgres) columns pass through\n // unchanged. Resolved at call time so plugin-init order does not matter.\n const coerceTemporalFilterValue = (\n objectName: string,\n fieldName: string,\n value: unknown,\n ): unknown => {\n try {\n const svc = ctx.getService<DataEngineLike>('data');\n const driver = svc?.getDriverForObject?.(objectName);\n if (driver && typeof driver.temporalFilterValue === 'function') {\n return driver.temporalFilterValue(objectName, fieldName, value);\n }\n } catch {\n // No data engine / driver, or it doesn't support coercion — leave the\n // value as-is (today's behaviour; safe for text/native-timestamp paths).\n }\n return value;\n };\n\n // The column half of the same fix (#3912). A SQLite `Field.datetime` column\n // holds BOTH storage forms — INTEGER epoch from a `Date` write, ISO TEXT from\n // a REST/JSON write or a `NOW()` default — so coercing the comparand alone\n // matched whichever half the writer produced and returned an empty window for\n // the other. Ask the driver for the column expression that normalises both.\n const coerceTemporalFilterColumn = (\n objectName: string,\n fieldName: string,\n columnSql: string,\n ): string => {\n try {\n const svc = ctx.getService<DataEngineLike>('data');\n const driver = svc?.getDriverForObject?.(objectName);\n if (driver && typeof driver.temporalFilterColumnSql === 'function') {\n return driver.temporalFilterColumnSql(objectName, fieldName, columnSql);\n }\n } catch {\n // Same tiering as above — an unresolvable driver emits the bare column,\n // which is today's behaviour and correct on every non-mixed dialect.\n }\n return columnSql;\n };\n\n const config: AnalyticsServiceConfig = {\n cubes: this.options.cubes,\n logger: ctx.logger,\n queryCapabilities,\n executeRawSql,\n executeAggregate,\n fallbackService,\n getReadScope,\n getAllowedRelationships: this.options.getAllowedRelationships,\n coerceTemporalFilterValue,\n coerceTemporalFilterColumn,\n relationshipResolver,\n labelResolver,\n // [#8286] Passed through as authored — `undefined` is \"this host did not\n // choose\", which the service resolves to development-only. Defaulting it\n // here would be a second copy of that decision, drifting the moment one\n // of the two moves.\n debugSql: this.options.debugSql,\n // Source-field metadata behind the display chains on result columns:\n // ADR-0053 currency (`currencyConfig.defaultCurrency`) and percent scale\n // (`max`, which is what marks whole-percent storage — objectui#3136).\n sourceFieldMeta: (object: string, field: string) => {\n const f = dataEngine()?.getObject?.(object)?.fields?.[field] as\n | { type?: string; max?: number; currencyConfig?: { defaultCurrency?: string } }\n | undefined;\n return f ? { type: f.type, max: f.max, defaultCurrency: f.currencyConfig?.defaultCurrency } : undefined;\n },\n // #5033 — the datasource an object is bound to, used ONLY to name the\n // actual cause when a dataset's SQL references a table that is not on the\n // datasource the query was routed to. Undefined ⇒ the object rides the\n // default datasource (or the engine cannot answer), and the diagnostic\n // says so rather than inventing a name.\n //\n // [#5288] Asked of the ENGINE's resolver, not of the object's declaration.\n // `getObject(name).datasource` is the declared value — step 1 of the five\n // `getDriver` routes by — so an object placed by a `datasourceMapping`\n // rule, by the ADR-0057 §3.6 lifecycle split, or by its package's\n // `defaultDatasource` answered `'default'`, and the diagnostic named a\n // database the rows are not in. Recomputing those rules here instead would\n // be the second implementation `resolveMappedDatasource` (#4462) exists to\n // prevent: it drifts by one step, silently, and the drift only surfaces as\n // an error message pointing at the wrong database.\n getObjectDatasource: (objectName: string) => dataEngine()?.resolveEffectiveDatasource?.(objectName),\n // ADR-0062 D6 — a federated object carries an `external` block (ADR-0015).\n // Reported so NativeSQLStrategy declines it (its hand-compiled FROM would\n // hit the wrong physical table) and the driver-correct ObjectQL path runs.\n isExternalObject: (objectName: string) => {\n const obj = dataEngine()?.getObject?.(objectName);\n return !!(obj && obj.external != null);\n },\n // [#3867] Existence probe for the cube auto-inference gate. Reads the\n // same schema registry the data path's #3770 gate consults, through the\n // engine accessor this bridge already uses above — so \"which objects\n // exist\" has one answer across /data and /analytics.\n //\n // `dataEngine()` resolves lazily and may be absent entirely (analytics\n // installed without a data engine). Reporting `false` there would 404\n // every cube, so an unresolvable engine reports `true` — \"cannot answer,\n // do not block\" — mirroring the tiering #3770 took on the data path.\n isRegisteredObject: (name: string) => {\n const engine = dataEngine();\n if (!engine) return true;\n return engine.getObject?.(name) != null;\n },\n // [#4437, #5520] Field names for the two source-field gates — measures\n // (#4437) and dimensions/timeDimensions (#5520). Read from the\n // SAME schema registry `isRegisteredObject` above consults (and the data\n // path's #4315 gate reads), so \"which fields exist\" has one answer across\n // /data and /analytics. `undefined` — no engine, unknown object, or an\n // object with no field map (an external datasource whose columns are not\n // mirrored locally) — means \"cannot answer\", and the gate stands down.\n getObjectFieldNames: (objectName: string) => {\n const fields = dataEngine()?.getObject?.(objectName)?.fields;\n if (!fields || typeof fields !== 'object') return undefined;\n const names = Object.keys(fields);\n return names.length > 0 ? names : undefined;\n },\n draftRowsResolver,\n };\n\n if (autoBridgedReadScope && securityPresentAtInit) {\n ctx.logger.info('[Analytics] Auto-bridged getReadScope → \"security\" service (getReadFilter)');\n } else if (autoBridgedReadScope) {\n // The bridge IS wired and will resolve at call time — this is only a\n // heads-up that security had not registered yet at our init. It becomes a\n // real problem only if no security service ever appears.\n ctx.logger.info(\n '[Analytics] getReadScope bridged to the \"security\" service; that service is not ' +\n 'registered yet at init and will be resolved per query (plugin order is not significant).',\n );\n } else if (!getReadScope) {\n ctx.logger.warn(\n '[Analytics] No getReadScope configured and no \"security\" service with getReadFilter found — ' +\n 'analytics queries will NOT enforce tenant/RLS scoping (ADR-0021 D-C). ' +\n 'Supply getReadScope or register a security service in multi-tenant deployments.',\n );\n }\n\n if (autoBridged) {\n ctx.logger.info('[Analytics] Auto-bridged executeAggregate → \"data\" service (IDataEngine)');\n }\n if (autoBridgedRawSql) {\n ctx.logger.info('[Analytics] Auto-bridged executeRawSql → \"data\" service (IDataEngine.execute)');\n }\n\n this.service = new AnalyticsService(config);\n\n // Register or replace the analytics service\n if (fallbackService) {\n ctx.replaceService('analytics', this.service);\n } else {\n ctx.registerService('analytics', this.service);\n }\n\n if (this.options.debug) {\n ctx.hook('analytics:beforeQuery', async (query: unknown) => {\n ctx.logger.debug('[Analytics] Before query', { query });\n });\n }\n\n ctx.logger.info('[Analytics] Service initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n if (!this.service) return;\n\n // Notify other plugins that analytics is ready\n await ctx.trigger('analytics:ready', this.service);\n\n ctx.logger.info(\n `[Analytics] Service started with ${this.service.cubeRegistry.size} cubes: ` +\n `${this.service.cubeRegistry.names().join(', ') || '(none)'}`,\n );\n }\n\n async destroy(): Promise<void> {\n this.service = undefined;\n }\n}\n"],"mappings":"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","names":["import_data","import_ui","import_core","import_data","hit","v","nullSafeNegationOperand","assertDefinedComparands","assertNoFieldReferenceComparand","undefinedComparandError","nullValueSatisfiesOperator","operatorIsNullTotal","nullGuardForFieldSpec","import_core","import_data","import_core","n","import_data","import_data","import_core","aggregate","y","import_core","d"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/analytics-service.ts","../src/cube-registry.ts","../src/strategies/filter-normalizer.ts","../src/comparand-shape.ts","../src/like-pattern.ts","../src/read-scope-sql.ts","../src/dataset-refusal.ts","../src/strategies/native-sql-strategy.ts","../src/strategies/objectql-strategy.ts","../src/strategies/cross-object-rebucket.ts","../src/dataset-compiler.ts","../src/dataset-executor.ts","../src/dimension-labels.ts","../src/preview-evaluator.ts","../src/plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n// Core service\nexport { AnalyticsService } from './analytics-service.js';\nexport type { AnalyticsServiceConfig } from './analytics-service.js';\n\n// Kernel plugin\nexport { AnalyticsServicePlugin } from './plugin.js';\nexport type { AnalyticsServicePluginOptions } from './plugin.js';\n\n// Cube registry\nexport { CubeRegistry } from './cube-registry.js';\n\n// Dataset semantic layer (ADR-0021)\nexport { compileDataset } from './dataset-compiler.js';\nexport type {\n CompiledDataset,\n DatasetCompileOptions,\n DerivedMeasureSpec,\n RelationshipResolver,\n RelationshipTarget,\n} from './dataset-compiler.js';\n\nexport {\n resolveDimensionLabels,\n pickDisplayField,\n createOrderLabelResolver,\n withLabelFetchCache,\n} from './dimension-labels.js';\nexport type { DimensionLabelDeps, FieldMetaLite, OrderLabelResolver } from './dimension-labels.js';\nexport {\n DatasetExecutor,\n evaluateDerivedMeasures,\n combineFilters,\n shiftRange,\n mergeByDimensions,\n fillEmptyGroups,\n} from './dataset-executor.js';\nexport type { DatasetSelection, CompareTo } from './dataset-executor.js';\nexport { compileScopedFilterToSql } from './read-scope-sql.js';\n\n// Strategies\nexport { NativeSQLStrategy } from './strategies/native-sql-strategy.js';\nexport { ObjectQLStrategy } from './strategies/objectql-strategy.js';\nexport type { AnalyticsStrategy, StrategyContext, AnalyticsDriverCapabilities } from './strategies/types.js';\n\n// Note: InMemoryStrategy is exported from @objectstack/driver-memory\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n IAnalyticsService,\n AnalyticsQuery,\n AnalyticsResult,\n CubeMeta,\n DatasetSelection,\n} from '@objectstack/spec/contracts';\nimport { percentScaleOf, type Cube, type FilterCondition } from '@objectstack/spec/data';\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\nimport type { Dataset } from '@objectstack/spec/ui';\n// [#6761] The ONE shared `I18nLabel → string` resolver (#6765, maintainer\n// ruling B). Imported, never re-implemented: a private twin here is exactly the\n// fork the ruling exists to prevent — it would render the same authored map\n// differently from objectui's `pickLocalized` with neither end erroring.\nimport { resolveI18nLabel } from '@objectstack/spec/ui';\nimport type { Logger } from '@objectstack/spec/contracts';\nimport { createLogger, getEnv, bucketKeyToCalendarRange, zonedDateStartToUtcMs } from '@objectstack/core';\n// [#6615] The Postgres `\"x\" of relation \"y\"` phrase, owned once. This is the\n// only reason this package depends on `@objectstack/types` — see the module's\n// docblock for why the edge is acyclic and why it was worth adding.\nimport { matchMissingColumnOfRelation } from '@objectstack/types';\nimport { CubeRegistry } from './cube-registry.js';\nimport type { AnalyticsStrategy, AnalyticsDriverCapabilities, StrategyContext, DatasetScopedStrategyContext } from './strategies/types.js';\nimport { NativeSQLStrategy } from './strategies/native-sql-strategy.js';\nimport { ObjectQLStrategy } from './strategies/objectql-strategy.js';\n// [#5669] The `where` source-field gate reads the filter tree through the SAME\n// pair the strategies compile it with, so \"the field the gate saw\" and \"the\n// column that reached SQL\" cannot be two different things.\nimport {\n normalizeAnalyticsFilterTree,\n collectFilterLeaves,\n lowerAnalyticsWhere,\n conjunctFieldKeys,\n} from './strategies/filter-normalizer.js';\nimport { findCrossFieldComparand } from './comparand-shape.js';\nimport { compileDataset, type CompiledDataset, type RelationshipResolver } from './dataset-compiler.js';\nimport { DatasetExecutor, resolveDimensionGranularity, type DateGranularityValue } from './dataset-executor.js';\nimport {\n resolveDimensionLabels,\n createOrderLabelResolver,\n withLabelFetchCache,\n type DimensionLabelDeps,\n} from './dimension-labels.js';\nimport { evaluateAnalyticsQueryOverRows } from './preview-evaluator.js';\n// [#5918] The measure mint refuses a dotted member through the SAME constructor\n// the strategies' member-level refusals use — `INVALID_FIELD` / 400, naming the\n// member as the request spelled it (see `dataset-refusal.ts`'s header for why\n// that code and not `DATASET_INVALID`).\nimport { invalidMemberError } from './dataset-refusal.js';\n\n/**\n * Analytics result augmented with drill-through metadata (ADR-0021 D2; see\n * queryDataset). Carried alongside `rows` so the host can drill a clicked bucket\n * back to the underlying records without the renderer knowing field mappings.\n */\ntype AnalyticsResultWithDrill = AnalyticsResult & {\n /** The dataset's base object — the host drills into its records. */\n object?: string;\n /** Selected drillable dimension NAME → underlying object FIELD name. */\n dimensionFields?: Record<string, string>;\n /**\n * RAW grouped values per row, aligned to `rows` by index — each a map of\n * drillable dimension NAME → stored value (BEFORE label resolution rewrote\n * `rows[i][dim]` to the display label). The exact-match drill filter is built\n * from these, never from the display labels.\n */\n drillRawRows?: Array<Record<string, unknown>>;\n /**\n * RAW grouped values for the totals/subtotal rows (#3214), the totals-side\n * companion to `drillRawRows`: `drillRawTotals[i]` aligns to `result.totals[i]`\n * and `drillRawTotals[i][j]` to `result.totals[i].rows[j]`. Each map holds that\n * grouping's DRILLABLE dimension NAME → stored value, snapshotted in the SAME\n * pre-label-resolution pass (the totals loop below overwrites a subtotal row's\n * dimension value with its display label just like the data rows). Restricted\n * to the drillable dims present in the grouping, so the grand-total grouping\n * (`[]`) contributes an empty map per row — which keeps the index alignment\n * intact and correctly drills the whole (unfiltered) object.\n */\n drillRawTotals?: Array<Array<Record<string, unknown>>>;\n /**\n * #1752 — half-open date-range drill scope per row, the RANGE companion to\n * `drillRawRows` (which handles equality dims). A time-bucketed date\n * dimension (`dateGranularity`) groups a SPAN of records into one bucket\n * (\"2026-Q2\"), so its drill needs `[gte, lt)`, not equality — the humanized\n * bucket can't be exact-matched (which is why date dims are excluded from\n * `dimensionFields`/`drillRawRows`). Aligned to `rows` by index; each entry\n * maps a drillable date-dimension NAME → `{ field, gte, lt }` with `gte`\n * inclusive and `lt` exclusive (bounds as `YYYY-MM-DD`). Present only for\n * buckets whose boundaries are unambiguous — a `datetime` field under a\n * non-UTC reference timezone is omitted (host drills an unscoped superset)\n * until instant-boundary support lands.\n */\n drillRanges?: Array<Record<string, { field: string; gte: string; lt: string }>>;\n};\n\n/**\n * [#5717] Does this error carry an ADR-0112 envelope — i.e. did its PRODUCER\n * already classify it?\n *\n * The structural fact, read exactly as `rest-server.ts`'s\n * `/analytics/dataset/query` catch reads it (`envelopeStatus`/`envelopeCode`):\n * a numeric `status` plus a non-empty string `code`. Deliberately the SAME\n * predicate rather than a second dialect of \"looks enveloped\" — a producer that\n * ships half an envelope has a bug of its own and must be found, not guessed at\n * from either end of the wire.\n *\n * Status RANGE is deliberately not part of it. The 4xx case is the loud one\n * (#5717's own: a `DATASET_INVALID` / 400 refusal must reach the caller as a\n * 400, never as an empty grid), but a DECLARED 5xx — `read-scope-sql.ts`'s\n * `READ_SCOPE_COMPILE_FAILED` / 500 fail-closed refusals — is if anything worse\n * to swallow: an RLS lowering that failed closed, rendered as a confident empty\n * chart, is a server fault nobody is told about. Either way the producer has\n * ANSWERED the classification question, and {@link isMissingSourceError} — a\n * heuristic over DRIVER phrasing — has no business re-opening it.\n */\nfunction hasDeclaredErrorEnvelope(err: unknown): boolean {\n const e = err as { code?: unknown; status?: unknown } | null | undefined;\n return typeof e?.status === 'number' && typeof e?.code === 'string' && e.code.length > 0;\n}\n\n/**\n * [#6035] Postgres's MISSING COLUMN wording — the one driver phrase that is a\n * missing-SOURCE phrase by substring while meaning the opposite.\n *\n * `column \"label\" of relation \"acct\" does not exist` (SQLSTATE 42703)\n *\n * `relation \"acct\" does not exist` sits inside it verbatim, so both\n * {@link isMissingSourceError} and {@link missingSourceRelation} read it as\n * \"the table `acct` is gone\" — which would degrade the widget to an empty grid\n * (when `acct` is the dataset's own object) or report a cross-datasource\n * topology error (when it is a joined one). Neither is true: `acct` is right\n * there and a COLUMN NAME IS MISPELLED — precisely the class both docblocks\n * promise to leave as a hard failure.\n *\n * Subtracting it first is the shape `rest-server.ts`'s `mapDataError` has used\n * since #5352 (its `unknownColumn` probe extracts this same phrase ahead of the\n * unknown-object branch, so the REST face answers `400 INVALID_FIELD` rather\n * than `404`; the case is pinned in `rest.test.ts`). This is deliberately that\n * regex rather than a second dialect of it — the two faces must not disagree\n * about what counts as postgres saying \"column\".\n *\n * Both quotes are required because postgres always emits them here (its errmsg\n * template is `column \"%s\" of relation \"%s\" does not exist`), and requiring\n * them is the safe direction of error: a wording this misses merely keeps\n * today's verdict, while one it over-matches would turn a genuinely missing\n * table into a hard failure and regress #5033's deliberate leniency.\n *\n * [#6615] \"Deliberately that regex rather than a second dialect of it\" is now\n * enforced rather than asserted: the phrase moved to\n * {@link matchMissingColumnOfRelation} in `@objectstack/types`, which\n * `rest-server.ts`'s `mapDataError` and `metadata`'s `MISSING_TABLE.excludes`\n * also read. The two faces can no longer disagree about what postgres says by\n * one of them being edited. Same pattern, byte for byte — only its owner moved.\n */\nfunction isMissingColumnOfRelation(message: string): boolean {\n return matchMissingColumnOfRelation(message) !== undefined;\n}\n\n/**\n * Detect the \"backing object/table isn't present in this kernel\" class of\n * error so a dataset query can degrade to an empty result instead of failing\n * the widget with a 500. Matches the missing-relation signatures across the\n * drivers ObjectStack runs on (sqlite/libsql, postgres, mysql) plus the\n * framework's own unknown-object signal. Deliberately scoped to MISSING SOURCE\n * (table/object/relation) — not column/syntax errors, which stay hard failures\n * so real query bugs still surface.\n *\n * ⚠️ It is a heuristic over driver PHRASING, so it is the SECOND question the\n * degradation path asks, never the first: {@link hasDeclaredErrorEnvelope} runs\n * ahead of it (#5717), and only an error whose producer declared nothing is\n * classified by its words here.\n *\n * [#5717] The postgres limb is ANCHORED to postgres's actual wording\n * (`relation \"x\" does not exist`, relation name quoted or bare) instead of the\n * `includes('relation') && includes('does not exist')` conjunction it used to\n * be. That conjunction matched any sentence carrying both words — including\n * `dataset-compiler.ts`'s `… includes relationship \"R\" which does not exist on\n * object \"O\"`, where the \"relation\" is inside \"relationship\" and the missing\n * thing is a RELATIONSHIP, not a table. The anchor is the same pattern the\n * sibling {@link missingSourceRelation} already uses for postgres (and the same\n * shape as `metadata/src/utils/schema-sync-errors.ts`), so \"is something\n * missing\" and \"what is missing\" can no longer disagree on this limb.\n *\n * MEASURED over the wordings this repo actually carries — 13 strings: the three\n * driver families' phrasings (including sql-prefixed and schema-qualified\n * forms), the framework's not-registered signals, and this package's own\n * refusals — exactly ONE verdict moves, the compiler refusal above. No driver\n * wording changes, which is what makes this a narrowing rather than a\n * behaviour change for #5033's leniency.\n *\n * [#6035] The residue #5717 left and named here is now closed by\n * {@link isMissingColumnOfRelation}, subtracted BEFORE any limb below runs.\n * The anchor above cannot do it alone, for a reason worth stating plainly: the\n * missing-COLUMN wording literally CONTAINS a well-formed missing-relation\n * wording, so no tightening of \"does this say a relation is missing\" can ever\n * exclude it — only asking the more specific question FIRST can. That makes the\n * ORDER the fix, not the pattern.\n */\nfunction isMissingSourceError(err: unknown): boolean {\n const raw = String((err as { message?: unknown })?.message ?? err ?? '');\n // [#6035] Missing COLUMN is not missing SOURCE — the paragraph above promises\n // column errors stay hard failures, and this is where that promise is kept.\n if (isMissingColumnOfRelation(raw)) return false;\n const msg = raw.toLowerCase();\n return (\n msg.includes('no such table') || // sqlite / libsql\n /relation\\s+[`\"']?[A-Za-z0-9_$.]+[`\"']?\\s+does not exist/i.test(raw) || // postgres\n msg.includes(\"doesn't exist\") || // mysql (\"table ... doesn't exist\")\n msg.includes('not registered') || // framework: object not in registry\n msg.includes('unknown object') ||\n msg.includes('is not a registered object')\n );\n}\n\n/**\n * #5033 — the relation a missing-source error NAMES, when it names one.\n *\n * `isMissingSourceError` answers \"is something missing\"; this answers \"what\".\n * The distinction decides whether the widget may degrade: a dataset whose OWN\n * backing table is absent is a kernel that never mounted the object (degrade —\n * that is the case the graceful path exists for), while a dataset whose\n * *joined* table is absent on the datasource the base object routed to is a\n * cross-datasource dataset, i.e. a topology error that must be reported as\n * itself instead of hiding behind \"backing object … is unavailable\".\n *\n * Returns the bare relation name (schema/database qualifiers stripped —\n * `mydb.crm_account` → `crm_account`; Prime Directive #6 makes object name =\n * table name, so the result is comparable to a dataset's `object`), or\n * `undefined` when the driver's phrasing carries no name. Unparseable ⇒ the\n * caller keeps today's degradation, never a louder guess.\n *\n * [#6035] It subtracts {@link isMissingColumnOfRelation} for the same reason\n * its sibling does, and the reason is CONSISTENCY rather than a second bug:\n * measured on `origin/main`, the column wording made this function answer\n * `sys_team`, so fixing only \"is something missing\" would leave the pair\n * DISAGREEING — one saying nothing is missing, the other naming a table. That\n * disagreement is the exact defect #5717 closed on the postgres limb, and\n * re-opening it here would re-arm the same mine one edit away: today this\n * function is only ever called behind a true `isMissingSourceError`, so the\n * guard is unreachable, but \"unreachable\" is a property of the CALL ORDER at\n * one call site, not of this function. Guarding both keeps the two answers\n * derivable from the wording alone.\n */\nfunction missingSourceRelation(err: unknown): string | undefined {\n const msg = String((err as { message?: unknown })?.message ?? err ?? '');\n if (isMissingColumnOfRelation(msg)) return undefined;\n const patterns = [\n /no such table:\\s*[`\"'[]?([A-Za-z0-9_$.]+)/i, // sqlite / libsql\n /relation\\s+[`\"']?([A-Za-z0-9_$.]+)[`\"']?\\s+does not exist/i, // postgres\n /table\\s+[`\"']?([A-Za-z0-9_$.]+)[`\"']?\\s+doesn't exist/i, // mysql\n /(?:object|table)\\s+[`\"']([A-Za-z0-9_$.]+)[`\"']\\s+is not registered/i, // framework\n /unknown object:?\\s*[`\"']?([A-Za-z0-9_$.]+)/i,\n /[`\"']([A-Za-z0-9_$.]+)[`\"']\\s+is not a registered object/i,\n ];\n for (const re of patterns) {\n const m = re.exec(msg);\n if (m?.[1]) {\n const parts = m[1].split('.').filter(Boolean);\n const bare = parts[parts.length - 1];\n if (bare) return bare;\n }\n }\n return undefined;\n}\n\n/**\n * [#4437] A name that is a plain column/table identifier and nothing else.\n * Anything with a dot, a paren, whitespace or an operator is a SQL EXPRESSION\n * (or a cross-object reference) whose parts this layer cannot attribute to a\n * single field — such measures pass the source-field gate untouched.\n */\nconst BARE_IDENTIFIER = /^[a-z_][a-z0-9_]*$/i;\n\n/**\n * The `cube.dimensions` / `cube.measures` KEY a member resolves to, mirroring\n * the strategies' own `lookupMember` — including its deliberate LAST case: a\n * dotted member that matches no declared key is a synthetic relation traversal\n * handed to the JOIN machinery, which the source-field gates must not judge\n * (hence `undefined`, read as \"nothing to check\" rather than \"undeclared bare\n * column\").\n *\n * `kind` is which bags to consult, and it is NOT cosmetic — it is the difference\n * between the two callers' real resolution rules:\n *\n * - `'dimension'` — {@link AnalyticsService.assertDimensionFields}, matching\n * `NativeSQLStrategy.resolveDimensionSql` / `ObjectQLStrategy.resolveFieldName\n * (…, 'dimension')`, which look in `cube.dimensions` only.\n * - `'any'` — {@link AnalyticsService.assertWhereFields}, matching the FILTER\n * member resolution in `NativeSQLStrategy.resolveFieldSql` and\n * `ObjectQLStrategy.resolveFieldName(…, 'any')`, which fall through to\n * `cube.measures`. Consulting dimensions only would have made the where gate\n * reject a query that works on both strategies today: a cube declaring\n * `measures.revenue = {sql: 'annual_revenue'}` answers\n * `where: {revenue: {$gt: 100}}` as `annual_revenue > ?`, and a\n * dimensions-only lookup would have called `revenue` a missing column.\n *\n * Extracted from #5520's gate so #5669's second caller reads the tree the same\n * way — two open-coded copies of `lookupMember` in one file is exactly how\n * \"what the gate sees\" and \"what reaches SQL\" drift apart.\n */\nfunction declaredMemberEntry(\n cube: Cube,\n member: string,\n kind: 'dimension' | 'any',\n): { key: string; sql?: unknown } | undefined {\n const bags: Array<Record<string, { sql?: unknown } | undefined>> =\n kind === 'dimension'\n ? [cube.dimensions as Record<string, { sql?: unknown } | undefined>]\n : [\n cube.dimensions as Record<string, { sql?: unknown } | undefined>,\n cube.measures as Record<string, { sql?: unknown } | undefined>,\n ];\n // `key` is spread LAST in every arm: it is the bag key this member RESOLVED\n // to, and a `key` property on the cube entry itself must not shadow it.\n for (const bag of bags) {\n if (bag[member]) return { ...bag[member], key: member };\n if (member.includes('.')) {\n const [first, ...rest] = member.split('.');\n const tail = rest.join('.');\n if (first === cube.name && bag[tail]) return { ...bag[tail], key: tail };\n if (bag[tail]) return { ...bag[tail], key: tail };\n const flat = member.replace(/\\./g, '_');\n if (bag[flat]) return { ...bag[flat], key: flat };\n }\n }\n return undefined;\n}\n\n/**\n * The cube KEY a member resolves to (for the rejection's suggestion list) and\n * the bare COLUMN it compiles to — `source: null` meaning \"nothing this gate can\n * check\", which covers every deliberate stand-down at member level: an\n * expression `sql` (`CASE WHEN …`, `*`), and a dotted relation traversal whose\n * join target the gate cannot see.\n *\n * Shared by the dimension gate (#5520) and the `where` gate (#5669) — see\n * {@link declaredMemberEntry} for why `kind` differs between them.\n */\nfunction resolveMemberSource(\n cube: Cube,\n member: string,\n kind: 'dimension' | 'any',\n): { key: string; source: string | null } {\n const entry = declaredMemberEntry(cube, member, kind);\n if (entry) {\n const source = typeof entry.sql === 'string' ? entry.sql.trim() : '';\n return { key: entry.key, source: source && BARE_IDENTIFIER.test(source) ? source : null };\n }\n // Undeclared. A dotted spelling is the relation traversal above; a bare one IS\n // the column the strategies will emit.\n if (member.includes('.')) return { key: member, source: null };\n return { key: member, source: BARE_IDENTIFIER.test(member) ? member : null };\n}\n\n/**\n * Configuration for AnalyticsService.\n */\nexport interface AnalyticsServiceConfig {\n /** Pre-defined cube definitions (from manifest). */\n cubes?: Cube[];\n /** Logger instance. */\n logger?: Logger;\n /**\n * Probe driver capabilities for the object that backs a cube.\n * The service calls this function to decide which strategy can handle a query.\n */\n queryCapabilities?: (cubeName: string) => AnalyticsDriverCapabilities;\n /**\n * Execute raw SQL on the driver for a given object.\n * Required for NativeSQLStrategy.\n */\n executeRawSql?: (objectName: string, sql: string, params: unknown[]) => Promise<Record<string, unknown>[]>;\n /**\n * Execute an ObjectQL aggregate query.\n * Required for ObjectQLStrategy.\n */\n executeAggregate?: (objectName: string, options: {\n groupBy?: string[];\n aggregations?: Array<{ field: string; method: string; alias: string }>;\n filter?: Record<string, unknown>;\n /** Reference timezone (IANA) for date bucketing — ADR-0053 Phase 2. */\n timezone?: string;\n /**\n * ADR-0021 D-C (#3602) — the request's ExecutionContext. Bridges MUST\n * forward it to `engine.aggregate` so engine-side RLS applies; see\n * `StrategyContext.executeAggregate` for why this is a second belt rather\n * than a replacement for `getReadScope`.\n */\n context?: ExecutionContext;\n }) => Promise<Record<string, unknown>[]>;\n /**\n * Fallback IAnalyticsService (e.g. MemoryAnalyticsService).\n * Used by InMemoryStrategy.\n */\n fallbackService?: IAnalyticsService;\n /**\n * Custom strategies to add/replace the defaults.\n * They are merged with the built-in strategies and sorted by priority.\n */\n strategies?: AnalyticsStrategy[];\n /**\n * ADR-0021 D-C — context-aware per-object read scope (tenant + RLS). Supplied\n * by the runtime that owns the sharing middleware; receives the current\n * request's ExecutionContext and returns the RLS `FilterCondition` for the\n * object (exactly what `RLSCompiler` emits). The service binds the active\n * context per query and the strategy compiles the filter into alias-qualified\n * SQL injected into every base and joined table.\n *\n * MAY be async: the production bridge resolves RLS from the `security`\n * service's `getReadFilter`, which can hit the database. The service\n * pre-resolves the scope for every base + joined object of a query (before\n * the synchronous SQL builder runs), so a sync return still works unchanged.\n */\n getReadScope?: (\n objectName: string,\n context?: ExecutionContext,\n ) =>\n | FilterCondition\n | null\n | undefined\n | Promise<FilterCondition | null | undefined>;\n /**\n * ADR-0021 D-C — join allowlist per cube (the dataset's declared `include`).\n * Joins outside this set are rejected by the strategy. Compiled datasets\n * (via `queryDataset`/`registerDataset`) supply this automatically; this\n * config hook is a fallback for legacy hand-authored cubes.\n */\n getAllowedRelationships?: (cubeName: string) => Set<string> | undefined;\n /**\n * Coerce a filter comparand to a temporal column's storage form so a\n * relative-date / ISO-string value compares correctly on the active driver\n * (SQLite `Field.datetime` → epoch ms; `Field.date` / native timestamp →\n * unchanged). Threaded into the StrategyContext and consulted by\n * `NativeSQLStrategy` when binding filter values. See the contract docs on\n * `StrategyContext.coerceTemporalFilterValue` for the full rationale.\n */\n coerceTemporalFilterValue?: (objectName: string, fieldName: string, value: unknown) => unknown;\n /**\n * Normalise the COLUMN side of the same comparison to that storage form — the\n * other half of the fix, needed because a SQLite `Field.datetime` holds both an\n * INTEGER epoch (a `Date` write) and ISO TEXT (a REST/JSON write, a `NOW()`\n * default) at once, so coercing only the comparand matches one of them and\n * misses the other (#3912). See `StrategyContext.coerceTemporalFilterColumn`.\n */\n coerceTemporalFilterColumn?: (objectName: string, fieldName: string, columnSql: string) => string;\n /**\n * ADR-0062 D6 — report whether an object is federated (external datasource).\n * Threaded into the StrategyContext so `NativeSQLStrategy` declines external\n * objects (which it would otherwise query against the wrong physical table),\n * routing them to the driver-correct ObjectQL aggregate path instead. See\n * `StrategyContext.isExternalObject`.\n */\n isExternalObject?: (objectName: string) => boolean;\n /**\n * [#5033] The datasource `objectName` is bound to, or `undefined` when it\n * rides the default one (or nothing authoritative can answer).\n *\n * It never selects a driver (that is `engine.execute`'s `object` key, which\n * the `plugin.ts` bridge now passes). It exists so that when a dataset's SQL\n * references a table that is NOT on the datasource its base object routed to,\n * the failure can name the actual cause — *table X is not on datasource Y* —\n * instead of the misleading \"backing object … is unavailable\" that a\n * cross-datasource join used to produce.\n *\n * [#5115] The same probe now also gates COMPILATION: `registerDataset` hands\n * it to `compileDataset`, which rejects a dataset whose join crosses\n * datasources before any query is ever built. Absence keeps the pre-#5115\n * behaviour exactly (\"cannot answer, do not block\") — the query-time\n * diagnostic above stays as the backstop.\n *\n * [#5288] \"Bound to\" above is the whole contract, and it took until #5288 for\n * the built-in host to honour it: `plugin.ts` answered with the object's\n * DECLARED `datasource` — step 1 of the five `ObjectQL.getDriver` routes by —\n * so an object placed by a `datasourceMapping` rule, by the ADR-0057 §3.6\n * lifecycle split, or by its package's `defaultDatasource` reported\n * `'default'` and sent the message above to the wrong database. It now asks\n * `ObjectQL.resolveEffectiveDatasource`. A custom host owes the same answer:\n * the datasource an object is BOUND to, `undefined` when nothing binds it.\n */\n getObjectDatasource?: (objectName: string) => string | undefined;\n /**\n * [#3867] Is `name` a registered object in this kernel's schema registry?\n *\n * Consulted by {@link AnalyticsService.ensureCube} on the auto-inference\n * path only. When no Cube is registered under the queried name, the service\n * infers a minimal one whose `sql` IS that name — the intended \"metric over\n * an object\" path (an `object-metric` KPI widget queries `crm_account`\n * without anyone authoring a Cube). Without this hook that inference accepts\n * ANY string, so an arbitrary physical table name reached the driver: the\n * analytics-side twin of the data-path gap closed in #3770.\n *\n * Optional, and absence means \"skip the check\" — same tiering as #3770's\n * `assertObjectRegistered`: with no registry to consult the question cannot\n * be answered, and failing closed would break every embedding that runs\n * analytics without a data engine. The production bridge in `plugin.ts`\n * always wires it.\n */\n isRegisteredObject?: (name: string) => boolean;\n /**\n * [#4437] The FIELD NAMES `objectName` declares, or `undefined` when nothing\n * authoritative can answer.\n *\n * Consulted by {@link AnalyticsService.ensureCube} to validate the SOURCE\n * FIELD a measure resolves to BEFORE any SQL is built. `inferMeasure` maps a\n * suffix convention onto a field name (`ghost_sum` → `SUM(ghost)`) and used\n * to accept any spelling, so a typo'd measure reached the driver as a column\n * and came back as an opaque `500 SQLITE_ERROR` — a driver error class on the\n * wire for a caller-shaped mistake (ADR-0112). The DATA route already refuses\n * the same mistake with a `400 INVALID_FIELD` naming the field (#4315/#4254);\n * this hook is what lets the ANALYTICS route give the same answer.\n *\n * [#5520] The same probe now answers for DIMENSIONS too\n * ({@link AnalyticsService.assertDimensionFields}). #4437 gated only the\n * measure half, so the identical typo one key over — `dimensions:\n * ['bogus_dim']` — still reached the driver as a `GROUP BY` column and came\n * back as the same 500. One probe, one answer, both member kinds.\n *\n * [#5669] …and for the `where` members ({@link AnalyticsService.assertWhereFields}),\n * the third and last request key that carries a field name. `where:\n * {bogus_col: 'x'}` compiled straight into `WHERE bogus_col = $1` for exactly\n * as long as #4437 and #5520 had each closed only their own key. One probe now\n * answers for all three.\n *\n * Same tiering as {@link isRegisteredObject}: absence means \"skip the check\"\n * (registry-less hosts, engine doubles, external datasources whose columns\n * are not mirrored locally). The production bridge in `plugin.ts` wires it\n * from the same schema registry the data path's gate reads, so \"which fields\n * exist\" has ONE answer across `/data` and `/analytics`.\n */\n getObjectFieldNames?: (objectName: string) => readonly string[] | undefined;\n /**\n * ADR-0021 — optional object-graph resolver used when compiling datasets:\n * `(baseObject, relationshipName) => relatedObjectName | undefined`. When\n * provided, `queryDataset` validates that every declared `include` exists.\n */\n relationshipResolver?: RelationshipResolver;\n /**\n * Resolve the metadata of a dimension's or measure's SOURCE FIELD on the\n * dataset's base object — the one seam through which display semantics that\n * live on the field reach the result columns. `undefined` for an unknown\n * field. Feeds three chains:\n *\n * - ADR-0053 currency: a monetary measure that omits an explicit `currency`\n * falls back to the field's declared currency, then the tenant default\n * (`ctx.currency`). Non-`currency` fields never get a code.\n * - Percent scale (objectui#3136): a measure over a `percent` field inherits\n * that field's storage scale via `percentScaleOf`, so a renderer scales by\n * declared metadata instead of guessing from the value.\n * - Date bucketing: a date vs datetime dimension drills by the right bound.\n */\n sourceFieldMeta?: (object: string, field: string) => { type?: string; defaultCurrency?: string; max?: number } | undefined;\n /** Pre-defined datasets to compile + register at construction (ADR-0021). */\n datasets?: Dataset[];\n /**\n * ADR-0021 — resolve raw dimension values to human display labels. When\n * provided, `queryDataset` post-processes result rows so a `select` dimension\n * shows its option label (not the stored value) and a `lookup`/`master_detail`\n * dimension shows the related record's display name (not the FK id). Injected\n * by the plugin from the `data` engine; omit to keep raw values.\n */\n labelResolver?: DimensionLabelDeps;\n\n /**\n * ADR-0037 Phase 3 — draft data preview. Resolve the PENDING `seed` draft\n * rows for an object (returns null when the object has no pending seed).\n * When provided and `queryDataset` is called with `previewDrafts`, the\n * selection is evaluated over these rows in memory instead of the engine —\n * the Live Canvas charts real numbers from the drafted sample data, and\n * because publish materializes the SAME seed, the numbers are continuous\n * across the publish boundary. Reads only; never touches physical tables.\n */\n draftRowsResolver?: (\n objectName: string,\n context?: ExecutionContext,\n ) => Promise<Record<string, unknown>[] | null>;\n\n /**\n * [#8286] Echo the executed statement back to the CALLER in\n * `AnalyticsResult.sql`. **Off unless this host opts in.**\n *\n * The contract has always declared the echo debug-only —\n * `AnalyticsResultResponseSchema.data.sql` (`spec/api/analytics.zod.ts`) is\n * `optional()` and describes itself as \"Executed SQL (if debug enabled)\" —\n * but no implementation ever read a debug switch, so every `/analytics/query`\n * response carried the statement, on production deployments included. This\n * field is that switch: declared = enforced, restored at the one seam the\n * response leaves through ({@link AnalyticsService.query}).\n *\n * **Default** — `NODE_ENV === 'development'`, and nothing else. In\n * particular an UNSET `NODE_ENV` counts as production and the echo stays\n * off: that is the maintainer's 2026-08-06 ruling for machine-readable\n * environment answers (see `resolveDiscoveryEnvironment` and the note at\n * `runtime/src/http-dispatcher.ts`), and of the two ways to be wrong,\n * disclosing on a production deployment whose operator forgot the variable\n * is the dangerous one. `os start` forces `NODE_ENV='production'` when\n * unset, `os serve` resolves `NODE_ENV || 'production'`, `os doctor` derives\n * the same expression — this switch now reads the absence the same way.\n *\n * **Why not a request field.** There is none, deliberately: a caller-set\n * debug flag would let any tenant re-open the disclosure on demand, which is\n * the shape of the defect rather than a fix for it. The echo is a HOST\n * decision, and the caller-facing surface for \"show me the SQL\" already\n * exists as the dedicated dry-run route `/api/v1/analytics/sql`\n * (`generateSql`), which this switch does not touch.\n *\n * **Why not the plugin's `debug` (log) option.** Server-side log verbosity\n * and what travels to a caller are different decisions with different blast\n * radii; folding them together means a support engineer raising log level on\n * a live deployment silently reopens the disclosure. Two switches, named for\n * what they open.\n */\n debugSql?: boolean;\n}\n\n/**\n * Default capabilities when probing is not configured — assumes in-memory only.\n */\nconst DEFAULT_CAPABILITIES: AnalyticsDriverCapabilities = {\n nativeSql: false,\n objectqlAggregate: false,\n inMemory: true,\n};\n\n/**\n * AnalyticsService — Multi-driver analytics orchestrator.\n *\n * Implements `IAnalyticsService` by delegating to a priority-ordered\n * strategy chain:\n *\n * | Priority | Strategy | Condition |\n * |:---:|:---|:---|\n * | P1 (10) | NativeSQLStrategy | Driver supports raw SQL |\n * | P2 (20) | ObjectQLStrategy | Driver supports aggregate AST |\n * | P3 (30) | (custom / InMemoryStrategy from driver-memory) | Injected by user |\n *\n * When `fallbackService` is configured, an internal delegate strategy\n * is automatically appended at priority 30 as a safety net.\n *\n * The service also owns a `CubeRegistry` for metadata discovery and\n * auto-inference from object schemas.\n */\nexport class AnalyticsService implements IAnalyticsService {\n private readonly strategies: AnalyticsStrategy[];\n /** Context-independent part of the StrategyContext (no per-request scope). */\n private readonly baseCtx: DatasetScopedStrategyContext;\n /** Context-aware read-scope provider (bound to the request's context per call). */\n private readonly readScopeProvider?: AnalyticsServiceConfig['getReadScope'];\n /** Compiled datasets by name — feeds the join allowlist (D-C) and queryDataset. */\n private readonly datasetRegistry = new Map<string, CompiledDataset>();\n /** Optional object-graph resolver used when compiling datasets. */\n private readonly relationshipResolver?: RelationshipResolver;\n private readonly sourceFieldMeta?: AnalyticsServiceConfig['sourceFieldMeta'];\n /** Optional dimension display-label resolver (select options / lookup names). */\n private readonly labelResolver?: DimensionLabelDeps;\n /** ADR-0037 P3: pending-seed row resolver for draft data preview. */\n private readonly draftRowsResolver?: AnalyticsServiceConfig['draftRowsResolver'];\n /** [#3867] Schema-registry probe gating cube auto-inference. */\n private readonly isRegisteredObject?: AnalyticsServiceConfig['isRegisteredObject'];\n /** [#4437] Field-name probe gating measure source-field resolution. */\n private readonly getObjectFieldNames?: AnalyticsServiceConfig['getObjectFieldNames'];\n /**\n * [#5033] Datasource probe for the missing-source triage — and, since #5115,\n * for the compile-time cross-datasource join gate in `compileDataset`.\n */\n private readonly getObjectDatasource?: AnalyticsServiceConfig['getObjectDatasource'];\n /** ADR-0062 D6 — federated-object probe (strategy routing + #5115's gate). */\n private readonly isExternalObject?: AnalyticsServiceConfig['isExternalObject'];\n /** [#3867] One-shot flag for the {@link assertInferableCube} stand-down warning. */\n private warnedNoObjectRegistry = false;\n /**\n * [#8286] Does the executed statement travel back to the caller?\n * See {@link AnalyticsServiceConfig.debugSql} for the switch and its default.\n */\n private readonly debugSql: boolean;\n readonly cubeRegistry: CubeRegistry;\n private readonly logger: Logger;\n\n constructor(config: AnalyticsServiceConfig = {}) {\n this.logger = config.logger || createLogger({ level: 'info', format: 'pretty' });\n this.cubeRegistry = new CubeRegistry();\n\n // Register pre-defined cubes\n if (config.cubes) {\n this.cubeRegistry.registerAll(config.cubes);\n }\n\n this.readScopeProvider = config.getReadScope;\n this.relationshipResolver = config.relationshipResolver;\n this.sourceFieldMeta = config.sourceFieldMeta;\n this.labelResolver = config.labelResolver;\n this.draftRowsResolver = config.draftRowsResolver;\n this.isRegisteredObject = config.isRegisteredObject;\n this.getObjectFieldNames = config.getObjectFieldNames;\n this.getObjectDatasource = config.getObjectDatasource;\n this.isExternalObject = config.isExternalObject;\n // [#8286] Resolved ONCE, at construction, from the host's explicit choice\n // or from `NODE_ENV`. An unset `NODE_ENV` is not development — see the\n // field's doc for the ruling this inherits.\n this.debugSql = config.debugSql ?? (getEnv('NODE_ENV') === 'development');\n\n // Compile + register pre-defined datasets (ADR-0021).\n if (config.datasets) {\n for (const ds of config.datasets) {\n try {\n this.registerDataset(ds);\n } catch (e) {\n this.logger?.warn?.(`[Analytics] Failed to register dataset \"${ds?.name}\": ${String((e as Error)?.message ?? e)}`);\n }\n }\n }\n\n // Build the context-independent strategy context. `getReadScope` is bound\n // per query in `callCtx(context)` so it can resolve the active tenant.\n this.baseCtx = {\n getCube: (name) => this.cubeRegistry.get(name),\n queryCapabilities: config.queryCapabilities || (() => DEFAULT_CAPABILITIES),\n executeRawSql: config.executeRawSql,\n executeAggregate: config.executeAggregate,\n fallbackService: config.fallbackService,\n // Prefer a compiled dataset's declared relationships (D-C join allowlist);\n // fall back to any explicitly-configured provider for legacy cubes.\n getAllowedRelationships: (cubeName: string) =>\n this.datasetRegistry.get(cubeName)?.allowedRelationships\n ?? config.getAllowedRelationships?.(cubeName),\n // [#10298] The compiled dataset's definition-level filter and its\n // per-measure filters — the half of the declaration the Cube model has\n // no room for. Same shape and same registry as `getAllowedRelationships`\n // directly above: answered for a cube that IS a compiled dataset,\n // `undefined` for every other cube.\n getDatasetScope: (cubeName: string) => {\n const compiled = this.datasetRegistry.get(cubeName);\n if (!compiled) return undefined;\n return { filter: compiled.filter, measureFilters: compiled.measureFilters };\n },\n coerceTemporalFilterValue: config.coerceTemporalFilterValue,\n coerceTemporalFilterColumn: config.coerceTemporalFilterColumn,\n isExternalObject: config.isExternalObject,\n };\n\n // Build strategy chain (built-in + custom, sorted by priority)\n // InMemoryStrategy is NOT built-in — it lives in @objectstack/driver-memory\n // and should be passed via config.strategies when needed.\n // When fallbackService is configured, an internal delegate is added at P3.\n const builtIn: AnalyticsStrategy[] = [\n new NativeSQLStrategy(),\n new ObjectQLStrategy(),\n ];\n\n // Auto-add fallback delegate when fallbackService is provided\n if (config.fallbackService) {\n builtIn.push(new FallbackDelegateStrategy());\n }\n\n const custom = config.strategies || [];\n this.strategies = [...builtIn, ...custom].sort((a, b) => a.priority - b.priority);\n\n this.logger.info(\n `[Analytics] Initialized with ${this.cubeRegistry.size} cubes, ` +\n `${this.strategies.length} strategies: ${this.strategies.map(s => s.name).join(' → ')}`,\n );\n }\n\n /**\n * Build a per-call StrategyContext that binds the read-scope provider to the\n * current request's ExecutionContext (ADR-0021 D-C). The strategy then sees a\n * `getReadScope(objectName)` that already knows the active tenant.\n */\n private async callCtx(\n query: AnalyticsQuery,\n context?: ExecutionContext,\n ): Promise<DatasetScopedStrategyContext> {\n // #3602 — `context` rides along unconditionally. It is the ENGINE-side belt\n // (forwarded to `engine.aggregate`, where the middleware chain applies its\n // own RLS), so it must not be gated on the analytics-side belt being wired:\n // a deployment with no `getReadScope` provider is exactly the one that most\n // needs the engine to scope for it.\n if (!this.readScopeProvider) return { ...this.baseCtx, context };\n // Pre-resolve the read scope for every object the strategy will scan (base\n // + all declared joins) BEFORE the synchronous SQL builder runs, since the\n // provider may be async (the production `security.getReadFilter` bridge).\n // The strategy then reads each object's filter synchronously from the map.\n const scopes = await this.resolveReadScopes(query, context);\n return {\n ...this.baseCtx,\n context,\n getReadScope: (objectName: string) => scopes.get(objectName) ?? null,\n };\n }\n\n /**\n * Resolve the read scope (tenant + RLS `FilterCondition`) for the base object\n * AND every joined object of the query's cube, keyed by object name. This is\n * the async pre-pass that lets the synchronous strategy enforce scoping even\n * when the provider (security `getReadFilter`) resolves asynchronously.\n *\n * The object set is `cube.sql` (base) plus every `cube.joins[*].name` — a\n * SUPERSET of what the strategy actually scans (the strategy only joins along\n * declared relationships), so no scanned object is ever left unscoped.\n *\n * Fail-closed: if the provider throws for an object, the whole query is\n * rejected rather than emitting SQL with that object unscoped.\n */\n private async resolveReadScopes(\n query: AnalyticsQuery,\n context?: ExecutionContext,\n ): Promise<Map<string, FilterCondition>> {\n const map = new Map<string, FilterCondition>();\n const provider = this.readScopeProvider;\n if (!provider || !query.cube) return map;\n const cube = this.cubeRegistry.get(query.cube);\n if (!cube) return map;\n\n const objects = new Set<string>();\n if (typeof cube.sql === 'string' && cube.sql.trim()) {\n objects.add(cube.sql.trim());\n }\n const joins = (cube as { joins?: Record<string, { name?: string }> }).joins;\n if (joins) {\n for (const [alias, j] of Object.entries(joins)) {\n objects.add(j?.name ?? alias);\n }\n }\n\n for (const object of objects) {\n let filter: FilterCondition | null | undefined;\n try {\n filter = await provider(object, context);\n } catch (e) {\n // Deny the entire query — never fall through to unscoped SQL.\n this.logger.error?.(\n `[Analytics] read-scope resolution failed for object \"${object}\" — ` +\n `rejecting query (fail-closed, ADR-0021 D-C)`,\n e instanceof Error ? e : new Error(String(e)),\n );\n throw new Error(\n `[Analytics] read-scope resolution failed for \"${object}\"; query denied (fail-closed).`,\n );\n }\n if (filter != null) map.set(object, filter);\n }\n return map;\n }\n\n /**\n * Execute an analytical query by delegating to the first capable strategy.\n *\n * A strategy can discover only AT EXECUTION TIME that the underlying driver\n * cannot serve it — the canonical case is NativeSQLStrategy on an in-memory\n * driver, whose `execute()` returns null for raw SQL (the auto-bridge throws\n * `RAW_SQL_UNSUPPORTED`). That is a capability miss, not a query error: fall\n * back to the next capable strategy (e.g. ObjectQLStrategy over the\n * aggregate bridge) instead of failing — or worse, fabricating empty rows.\n * Any other error propagates untouched.\n */\n async query(query: AnalyticsQuery, context?: ExecutionContext): Promise<AnalyticsResult> {\n if (!query.cube) {\n throw new Error('Cube name is required in analytics query');\n }\n\n this.ensureCube(query);\n const ctx = await this.callCtx(query, context);\n let skip: Set<AnalyticsStrategy> | undefined;\n for (;;) {\n const strategy = this.resolveStrategy(query, ctx, skip);\n this.logger.debug(`[Analytics] Query on cube \"${query.cube}\" → ${strategy.name}`);\n try {\n // [#8286] ONE gate, every strategy. This is the single seam a strategy\n // result leaves the service through — `NativeSQLStrategy` returns the\n // statement it ran, `ObjectQLStrategy` renders a representative one,\n // and `FallbackDelegateStrategy` passes through whatever the delegated\n // service minted (e.g. `MemoryAnalyticsService`, which always echoes).\n // Gating any one of those would leave the others serving, which is the\n // shape the defect already had.\n return this.applySqlEchoPolicy(await strategy.execute(query, ctx));\n } catch (e) {\n if ((e as { code?: string })?.code === 'RAW_SQL_UNSUPPORTED') {\n this.logger.warn(\n `[Analytics] ${strategy.name} cannot run on this driver (raw SQL unsupported) — falling back to the next strategy.`,\n );\n (skip ??= new Set()).add(strategy);\n continue;\n }\n throw e;\n }\n }\n }\n\n /**\n * [#8286] Withhold the executed statement unless this host enabled the echo.\n *\n * Applied at {@link query}, which is the response-assembly seam for BOTH\n * faces that serve callers: `/api/v1/analytics/query` calls it directly, and\n * `queryDataset` reaches it through `DatasetExecutor`, so a dataset response\n * inherits the same verdict without a second gate to keep in step.\n * `generateSql` — the dedicated `/api/v1/analytics/sql` dry-run route — is\n * deliberately NOT gated: asking for the statement is that route's entire\n * purpose, and it is the surface a debugging author is meant to use.\n *\n * What the echo disclosed, and why \"it is only a table name\" understates it:\n * the statement carries the compiled read scope, i.e. the SHAPE of the\n * isolation predicate (`\"sys_user\".\"id\" IN ($2, $3, …)` rather than an\n * `organization_id` comparison) plus its bound-parameter arity, which counts\n * the caller's own org membership. No wall was breached by it — the echo is\n * information disclosure, and this is the disclosure closing.\n */\n private applySqlEchoPolicy(result: AnalyticsResult): AnalyticsResult {\n if (this.debugSql || result?.sql === undefined) return result;\n // Copy-and-delete rather than mutate: the strategy (or a delegated\n // fallback service) owns the object it returned, and a cached result on\n // the other side of that boundary must not lose a field because this\n // service handed it to a caller once.\n const withheld: AnalyticsResult = { ...result };\n delete withheld.sql;\n return withheld;\n }\n\n /**\n * Compile a `dataset` (ADR-0021) and register its Cube + join allowlist so it\n * can be queried by name. Idempotent (re-registering overwrites). Returns the\n * compiled dataset.\n */\n registerDataset(dataset: Dataset): CompiledDataset {\n // #5115 — the datasource/federation probes turn a cross-datasource join\n // from a query-time explosion into a registration-time rejection. Both are\n // optional and tiered \"cannot answer, do not block\" inside the compiler.\n const compiled = compileDataset(dataset, this.relationshipResolver, {\n getObjectDatasource: this.getObjectDatasource,\n isExternalObject: this.isExternalObject,\n });\n this.cubeRegistry.register(compiled.cube);\n this.datasetRegistry.set(dataset.name, compiled);\n return compiled;\n }\n\n /**\n * Execute a semantic-layer dataset (ADR-0021). Compiles the dataset (saved or\n * inline draft — Studio preview), registers its Cube + join allowlist, then\n * runs the selection through the `DatasetExecutor` with the request context so\n * tenant/RLS scoping (D-C) is applied. See {@link IAnalyticsService.queryDataset}.\n */\n async queryDataset(\n dataset: Dataset,\n selection: DatasetSelection,\n context?: ExecutionContext,\n options?: { previewDrafts?: boolean },\n ): Promise<AnalyticsResult> {\n const compiled = this.registerDataset(dataset);\n this.logger.debug(`[Analytics] queryDataset \"${dataset.name}\" (object=${dataset.object}, include=${(dataset.include ?? []).join(',') || '—'})`);\n\n // ── ADR-0037 P3 — draft data preview ────────────────────────────────────\n // When the request renders the as-if-published world AND the base object\n // has a PENDING seed draft, evaluate the selection over the seed's rows in\n // memory (a query-evaluating proxy feeds the unchanged DatasetExecutor, so\n // measure filters / compareTo / derived measures all behave identically).\n // No pending seed → fall through to the real engine: published objects\n // keep charting live data even inside a preview.\n if (options?.previewDrafts && this.draftRowsResolver) {\n let seedRows: Record<string, unknown>[] | null = null;\n try {\n seedRows = await this.draftRowsResolver(dataset.object, context);\n } catch (e) {\n this.logger.warn(`[Analytics] draft preview resolver failed for \"${dataset.object}\" — falling back to live data: ${String((e as Error)?.message ?? e)}`);\n }\n if (seedRows) {\n this.logger.debug(`[Analytics] queryDataset \"${dataset.name}\" → preview over ${seedRows.length} drafted seed row(s)`);\n const previewService = {\n query: async (q: AnalyticsQuery) => evaluateAnalyticsQueryOverRows(q, compiled.cube, seedRows!),\n } as IAnalyticsService;\n const previewResult = await new DatasetExecutor(previewService).execute(compiled, selection, context);\n // Label resolution is skipped on purpose: drafted seed rows reference\n // lookups by NAME (the seed convention), which already reads well.\n return previewResult;\n }\n }\n\n // [#6761] The audience's language for THIS request, and the only locale\n // either field-label enrichment site below is entitled to use.\n //\n // `ExecutionContext.locale` is the BCP-47 tag `resolveExecutionContext`\n // resolves per request — the caller's `Accept-Language` when it expressed a\n // preference, else the workspace `localization` setting. It is the same\n // context field the currency chain a few lines down already reads, so both\n // display decisions in this response answer to one request identity.\n //\n // `undefined` (no context, or an anonymous request that skips localization)\n // is passed through deliberately rather than defaulted here: the shared\n // resolver documents nullish as \"no locale known\" and answers `en`, the\n // platform's source language. Choosing a different default in this file\n // would be this service disagreeing with the renderer about the same map.\n const requestLocale = context?.locale;\n\n // #3602 — every label lookup in this request (sort keys below, display\n // labels further down) reads the REFERENCED object, so bind that object's\n // own read scope to this request once, up front.\n const provider = this.readScopeProvider;\n const resolveScope = provider\n ? (targetObject: string) => provider(targetObject, context)\n : undefined;\n // #3680 — per-request label-fetch cache. A selection that sorts by a\n // lookup dimension resolves labels twice (pre-window sort keys, then\n // post-window display); the cache makes the display pass reuse the ids the\n // sort already fetched, so label-ordering costs ONE id→name read total.\n const labelDeps = this.labelResolver ? withLabelFetchCache(this.labelResolver) : undefined;\n // #3680 — hand the executor the sort-key label hook so an `order` on a\n // select/lookup dimension sorts by the label the user reads. Built over\n // the SAME capabilities (and read scope) as the display resolution below.\n const orderLabels = labelDeps && dataset.dimensions?.length\n ? createOrderLabelResolver(\n dataset.object,\n dataset.dimensions\n .filter((d) => !!d.field)\n .map((d) => ({ name: d.name, field: d.field as string })),\n labelDeps,\n resolveScope,\n context,\n )\n : undefined;\n\n // Graceful degradation: a dashboard/report widget whose backing object or\n // table is not present in this kernel (e.g. a platform dashboard like\n // System Overview that charts `sys_audit_log`, opened in an environment\n // that never mounted the audit object) must render as \"no data\" — NOT\n // crash the widget with a 500. Datasets were the one read surface that\n // hard-failed on a missing source.\n //\n // #5033 — that leniency is scoped to the dataset's OWN source. Once the raw-SQL\n // bridge routes by object (`plugin.ts`), a dataset that JOINS across datasources\n // fails on the base object's datasource with the JOINED table missing. Reporting\n // that as \"backing object … is unavailable\" would be the misleading old shape\n // wearing a new cause: the base table is right there, and the widget would keep\n // rendering the confident `0` this issue is about. So triage by WHICH relation\n // the driver named, and let a cross-datasource dataset fail loudly.\n //\n // #5717 — and the leniency is scoped to errors NOBODY classified. The\n // triage below reads message text, so before it runs, an error that carries\n // an ADR-0112 envelope is re-thrown untouched: its producer already said\n // what it is, and a `DATASET_INVALID` / 400 turned into `{rows: []}` is the\n // #5033 symptom wearing the opposite disguise — the caller's own mistake\n // reported as \"no data\", with no exception, no 4xx, no 5xx, just a warn and\n // a confident empty chart. See {@link hasDeclaredErrorEnvelope}.\n let result: AnalyticsResult;\n try {\n result = await new DatasetExecutor(this, orderLabels).execute(compiled, selection, context);\n } catch (err) {\n // The producer answered the classification question — the route's\n // envelope reader serves it (4xx as itself, declared 5xx through the\n // `ANALYTICS_QUERY_FAILED` path). Nothing here may re-judge it by wording.\n if (hasDeclaredErrorEnvelope(err)) throw err;\n if (isMissingSourceError(err)) {\n const missing = missingSourceRelation(err);\n const detail = String((err as Error)?.message ?? err);\n // A named relation that is NOT the dataset's own object is a joined table.\n // If that object IS registered in this kernel, it exists — just not on the\n // datasource this query ran against: a topology error, not an absence.\n // (`isRegisteredObject` absent / unable to answer ⇒ treat as registered,\n // the same \"cannot answer, do not block\" tiering it carries elsewhere;\n // here the honest report is the loud one, since the base table resolved.)\n const joined = missing && missing.toLowerCase() !== dataset.object.toLowerCase() ? missing : undefined;\n if (joined && (this.isRegisteredObject?.(joined) ?? true)) {\n const baseDs = this.getObjectDatasource?.(dataset.object);\n const joinedDs = this.getObjectDatasource?.(joined);\n const where = baseDs ? `datasource \"${baseDs}\"` : 'the default datasource';\n const joinedWhere = joinedDs ? `datasource \"${joinedDs}\"` : 'the default datasource';\n throw new Error(\n `[Analytics] dataset \"${dataset.name}\" cannot be executed as one statement: table \"${joined}\" ` +\n `is not on ${where}, which is where its base object \"${dataset.object}\" lives — ` +\n `\"${joined}\" is registered on ${joinedWhere}. A dataset JOIN cannot cross datasources. ` +\n `Fix it by binding both objects to the same datasource, or by dropping the cross-datasource ` +\n `relationship from the dataset's \\`include\\`/dimensions. (driver said: ${detail})`,\n );\n }\n this.logger.warn(\n `[Analytics] dataset \"${dataset.name}\" backing object \"${dataset.object}\" is unavailable ` +\n `(${detail}); returning an empty result instead of failing the widget`,\n );\n return { rows: [], fields: [], totals: [] };\n }\n throw err;\n }\n\n // Selected dimensions resolved against the dataset definition — shared by\n // drill metadata, label resolution, and dimension field-label enrichment.\n const selectedDims = (selection.dimensions ?? [])\n .map((name) => dataset.dimensions?.find((d) => d.name === name))\n .filter((d): d is NonNullable<typeof d> => !!d);\n\n // ADR-0021 D2 — drill-through metadata. A host (dashboard/report) drills a\n // clicked bucket back to the underlying records, but it only knows the\n // dimension NAMES, and the label resolution below OVERWRITES the raw grouped\n // value in each row with its display label. So before that happens, snapshot\n // the raw grouped values into a PARALLEL array (aligned to `rows` by index —\n // the result rows are NOT mutated) and expose the dataset's `object` +\n // dimension→field mapping so the renderer can build an exact-match filter.\n // Date buckets are excluded — a humanized bucket (\"2026-06\") can't be\n // exact-matched against the stored timestamp, so they are not drillable.\n const drillDims = selectedDims.filter((d) => !!d.field && d.type !== 'date');\n if (drillDims.length && result.rows.length) {\n (result as AnalyticsResultWithDrill).object = dataset.object;\n (result as AnalyticsResultWithDrill).dimensionFields = Object.fromEntries(\n drillDims.map((d) => [d.name, d.field as string]),\n );\n (result as AnalyticsResultWithDrill).drillRawRows = result.rows.map((row) => {\n const raw: Record<string, unknown> = {};\n for (const d of drillDims) raw[d.name] = row[d.name];\n return raw;\n });\n // #3214 — the totals/subtotal rows (#1753) carry dimension values too and\n // go through the SAME label resolution below, so snapshot their raw\n // grouped values here in the same pre-label pass. Aligned to `result.totals`\n // by index; each grouping is restricted to the drillable dims it actually\n // groups by (the grand-total grouping `[]` keeps empty maps, so a subtotal\n // drill filters by the stored value while the grand total drills unfiltered).\n if (result.totals?.length) {\n (result as AnalyticsResultWithDrill).drillRawTotals = result.totals.map((total) => {\n const groupingDims = drillDims.filter((d) => total.dimensions.includes(d.name));\n return total.rows.map((row) => {\n const raw: Record<string, unknown> = {};\n for (const d of groupingDims) raw[d.name] = row[d.name];\n return raw;\n });\n });\n }\n }\n\n // #1752 — date-range drill scope. A `dateGranularity` dimension groups a\n // SPAN of records into one bucket, so drilling it needs a half-open range\n // `[gte, lt)`, which the equality `drillRawRows` sidecar can't express\n // (that's exactly why date dims are excluded from `drillDims` above). Emit\n // a parallel range sidecar computed — via the shared inverse util so server\n // and client agree on boundaries — from the canonical bucket KEY, which is\n // still in `rows[i][dim]` here (this runs BEFORE label resolution rewrites\n // it to a display label).\n const rangeTz = selection.timezone ?? context?.timezone ?? 'UTC';\n // Per drillable date+granularity dim, decide how to serialize its bounds\n // (ADR-0053 temporal semantics):\n // - `datetime` → the reference tz's MIDNIGHT INSTANT (ISO), because the\n // bucket is defined on that tz's calendar (works under any tz, incl. DST);\n // - `date` → `YYYY-MM-DD` calendar bounds, a tz-naive calendar day that is\n // exact under ANY reference tz;\n // - unknown field type → safe only under UTC (where the calendar day and\n // its instant coincide); under a non-UTC tz we can't tell whether to\n // shift, so the dim is omitted and the host drills a superset.\n // The bucket size to invert MUST be the one the query actually grouped by —\n // `selection.dateGranularity` overrides the dataset dimension's default\n // (#3588). Reading `d.dateGranularity` here meant a widget that bucketed by\n // quarter or year got its ranges computed from the dataset's month (or,\n // when the dataset declared none, dropped entirely) — so drilling a bucket\n // opened the wrong span, or the chart lost drill-through altogether.\n const rangeDims: Array<{ d: (typeof selectedDims)[number]; granularity: DateGranularityValue; instant: boolean }> = [];\n for (const d of selectedDims) {\n if (!d.field || d.type !== 'date') continue;\n const granularity = resolveDimensionGranularity(selection, d.name, d.dateGranularity);\n if (!granularity) continue;\n const ftype = this.sourceFieldMeta?.(dataset.object, d.field as string)?.type;\n if (ftype === 'datetime') rangeDims.push({ d, granularity, instant: true });\n else if (ftype === 'date') rangeDims.push({ d, granularity, instant: false });\n else if (rangeTz === 'UTC') rangeDims.push({ d, granularity, instant: false });\n // else: unknown field type under a non-UTC reference tz → omit (superset).\n }\n if (rangeDims.length && result.rows.length) {\n const bound = (ymd: string, instant: boolean): string =>\n instant ? new Date(zonedDateStartToUtcMs(ymd, rangeTz)).toISOString() : ymd;\n (result as AnalyticsResultWithDrill).drillRanges = result.rows.map((row) => {\n const ranges: Record<string, { field: string; gte: string; lt: string }> = {};\n for (const { d, granularity, instant } of rangeDims) {\n // A row in the empty bucket carries `null` here (#3839) and yields no\n // range, so that row simply gets no drill bound — the superset.\n const cal = bucketKeyToCalendarRange(row[d.name] as string | null, granularity);\n if (cal) {\n ranges[d.name] = { field: d.field as string, gte: bound(cal.start, instant), lt: bound(cal.end, instant) };\n }\n }\n return ranges;\n });\n // The equality drill block sets `object` only when a NON-date drill dim\n // exists; a report grouped ONLY by time still needs the base object so the\n // host can open its list. Safe to (re)set to the same dataset object.\n (result as AnalyticsResultWithDrill).object = dataset.object;\n }\n\n // ADR-0021 — resolve grouped dimension values to human display labels\n // (select option label, lookup related-record name). Charts render the\n // dimension key verbatim, so this is the single place that turns a stored\n // value / FK id into the text a user expects to read.\n if (labelDeps && selectedDims.length) {\n // Same single-source rule as the drill ranges above: a date bucket must be\n // LABELLED with the granularity it was actually grouped by (#3588).\n // Formatting a `year` bucket with the dataset's `month` default rendered\n // it as \"1970-01\" — the year key re-parsed as an epoch millisecond count.\n const dims = selectedDims\n .filter((d) => !!d.field)\n .map((d) => ({\n name: d.name,\n field: d.field,\n type: d.type,\n dateGranularity: resolveDimensionGranularity(selection, d.name, d.dateGranularity),\n }));\n if (dims.length) {\n // `resolveScope` (hoisted above) binds the referenced object's read\n // scope to THIS request so the label lookup (a per-record read of the\n // related object) cannot surface a record the referenced object's RLS\n // would hide (#3602). `labelDeps` is the per-request cache over the\n // configured resolver, so ids the sort-key pass (#3680) already fetched\n // are not fetched again here.\n try {\n // `context` rides alongside `resolveScope` — the label lookup's SECOND\n // belt. `resolveScope` is this layer's own predicate; the context lets\n // the engine's middleware scope the same per-record read itself.\n await resolveDimensionLabels(dataset.object, dims, result.rows, labelDeps, resolveScope, context);\n // Totals rows (#1753) carry dimension values too (a row subtotal is\n // keyed by its row bucket) — resolve each grouping's own subset.\n for (const total of result.totals ?? []) {\n const subset = dims.filter((d) => total.dimensions.includes(d.name));\n if (subset.length) {\n await resolveDimensionLabels(dataset.object, subset, total.rows, labelDeps, resolveScope, context);\n }\n }\n } catch (e) {\n this.logger?.warn?.(`[Analytics] dimension label resolution failed for \"${dataset.name}\": ${String((e as Error)?.message ?? e)}`);\n }\n }\n }\n\n // ADR-0021 — enrich measure columns with their display `label` + `format`\n // so presentations show \"Tasks\" / \"$616,000\" instead of the raw measure\n // name \"task_count\" / \"616000\". Carried on the result fields; the renderer\n // applies the format (it can't be baked into the numeric row value).\n //\n // [#6761] The label is resolved through the shared `I18nLabel → string`\n // resolver, so BOTH authorized forms reach the wire: a plain string, and the\n // inline locale map `I18nLabelSchema` has authorized since #5728. The old\n // `typeof m.label === 'string'` test dropped the map silently — a dataset\n // written the way the schema documents shipped a column with no header at\n // all. The wire type is unchanged (`fields[].label?: string`, both ends):\n // this resolves TO a string rather than widening the contract.\n if (result.fields?.length && dataset.measures?.length) {\n const measureByName = new Map(dataset.measures.map((m) => [m.name, m]));\n for (const f of result.fields) {\n const m = measureByName.get(f.name) ?? measureByName.get(f.name.replace(/__compare$/, ''));\n if (!m) continue;\n // `undefined` from the resolver means \"nothing was picked\" — an absent\n // label, or a map with no usable entry. Nothing is written in that case:\n // this enrichment describes columns, it never invents a header.\n if (f.label == null) {\n const label = resolveI18nLabel(m.label, requestLocale);\n if (label !== undefined) f.label = label;\n }\n if (f.format == null && m.format) f.format = m.format;\n // ADR-0053 currency chain. A MONETARY measure resolves its display\n // currency from: explicit measure `currency` → source-field\n // `currencyConfig.defaultCurrency` → tenant default (`ctx.currency`). A\n // measure is monetary if it declares a currency OR aggregates a\n // `currency`-type field; non-monetary measures (count, avg of a plain\n // number) never receive a currency code.\n const fc = f as { currency?: string };\n const mc = m as { currency?: string };\n const meta = m.field ? this.sourceFieldMeta?.(dataset.object, m.field) : undefined;\n if (fc.currency == null) {\n const monetary = !!mc.currency || meta?.type === 'currency';\n if (monetary) {\n const resolved = mc.currency ?? meta?.defaultCurrency ?? context?.currency;\n if (resolved) fc.currency = resolved;\n }\n }\n // Percent scale chain (objectui#3136) — the currency chain's sibling.\n // A `%` format says how to PRINT a number, not what scale it is on, and\n // the two readings collide at exactly 1 (\"100%\" vs \"1%\"). Both answers\n // are in metadata, so answer here instead of leaving the renderer to\n // guess from the value's magnitude: a `ratio` is a 0–1 fraction by\n // definition, and any aggregate of a `percent` field (avg/min/max —\n // sum is already rejected as incoherent) keeps that field's own scale.\n if (f.percentScale == null) {\n f.percentScale = m.derived?.op === 'ratio' ? 'fraction' : percentScaleOf(meta);\n }\n }\n }\n\n // Enrich DIMENSION columns with their display `label` too, so a grouped\n // table header reads \"Status\" instead of the raw field name \"status\". The\n // measure-only enrichment above left dimension headers bare (the renderer\n // then fell back to the raw dimension name).\n //\n // This ENRICHES existing entries and deliberately mints none: whether a\n // column exists at all is the query layer's answer, and inventing one here\n // would describe a column the rows may not carry. #5537 was that gap read\n // from the wrong end — a selection whose base measures were ALL\n // filter-scoped ran no primary query, so `DatasetExecutor.runMeasurePass`\n // assembled a grid with dimension columns in every row and no dimension\n // entry in `fields` for this pass to enrich. Fixed where the grid is\n // assembled (see that method's #5537 note), which is also the only place\n // that knows what the active strategy actually projected.\n //\n // #5688 — the set to describe FROM is wider than `selection.dimensions`. A\n // `timeDimensions` entry that resolves a granularity is GROUPED BY, so it is\n // a result column even when the caller never listed it under `dimensions`\n // (#4033's `projectedDimensions`) — and reading `selection.dimensions` alone\n // left exactly that column carrying a `type` and no `label`, the blind spot\n // #5537's PR pinned as a control case. Widening the LOOKUP is not the same\n // as widening the projection: the loop still enriches only entries\n // `result.fields` already carries, so an entry that stays a pure window\n // contributes no column and receives no descriptor.\n //\n // Kept out of `selectedDims` deliberately. Drill metadata and row-value\n // label resolution above answer a different question — which dimensions the\n // caller GROUPED THE GRID BY, i.e. what a click can be turned back into\n // records — and widening those would change drill payloads and row values,\n // not table headers.\n const describableDims = [...selectedDims];\n for (const t of selection.timeDimensions ?? []) {\n if (describableDims.some((d) => d.name === t.dimension)) continue;\n const d = dataset.dimensions?.find((x) => x.name === t.dimension);\n if (d) describableDims.push(d);\n }\n if (result.fields?.length && describableDims.length) {\n const dimByName = new Map(describableDims.map((d) => [d.name, d]));\n const dimByField = new Map(describableDims.filter((d) => !!d.field).map((d) => [d.field as string, d]));\n for (const f of result.fields) {\n if (f.label != null) continue;\n // Result fields may be keyed by the dataset dimension NAME or the\n // underlying cube FIELD depending on strategy — match either.\n const d = dimByName.get(f.name) ?? dimByField.get(f.name);\n if (!d) continue;\n // [#6761] Same resolver, same locale, same \"write nothing on a miss\"\n // rule as the measure enrichment above.\n const label = resolveI18nLabel(d.label, requestLocale);\n if (label !== undefined) f.label = label;\n }\n }\n return result;\n }\n\n /**\n * Get cube metadata for discovery.\n */\n async getMeta(cubeName?: string): Promise<CubeMeta[]> {\n // If a fallback service is configured, merge its metadata with the registry\n const cubes = cubeName\n ? [this.cubeRegistry.get(cubeName)].filter(Boolean) as Cube[]\n : this.cubeRegistry.getAll();\n\n return cubes.map(cube => ({\n name: cube.name,\n title: cube.title,\n measures: Object.entries(cube.measures).map(([key, measure]) => ({\n name: `${cube.name}.${key}`,\n type: measure.type,\n title: measure.label,\n })),\n dimensions: Object.entries(cube.dimensions).map(([key, dimension]) => ({\n name: `${cube.name}.${key}`,\n type: dimension.type,\n title: dimension.label,\n })),\n }));\n }\n\n /**\n * Generate SQL for a query without executing it (dry-run).\n */\n async generateSql(query: AnalyticsQuery, context?: ExecutionContext): Promise<{ sql: string; params: unknown[] }> {\n if (!query.cube) {\n throw new Error('Cube name is required for SQL generation');\n }\n\n this.ensureCube(query);\n const ctx = await this.callCtx(query, context);\n const strategy = this.resolveStrategy(query, ctx);\n this.logger.debug(`[Analytics] generateSql on cube \"${query.cube}\" → ${strategy.name}`);\n\n return strategy.generateSql(query, ctx);\n }\n\n // ── Internal ─────────────────────────────────────────────────────\n\n /**\n * Ensure a cube exists for the given query and that it knows about every\n * measure referenced by the query.\n *\n * - If no cube is registered for `query.cube`, infer a minimal cube from\n * the query so downstream strategies (which assume `cube.sql` exists)\n * don't crash.\n * - If a cube exists but the query references measures that aren't in\n * `cube.measures` (e.g. `amount_sum`, `amount_avg` emitted by dashboard\n * widget translators), inject suffix-inferred Metric entries so the\n * strategies pick the right aggregation function and field.\n *\n * It is also where the three SOURCE-FIELD gates run, on every path out of this\n * method and always BEFORE the (possibly augmented) cube is registered:\n * {@link assertMeasureFields} (#4437), {@link assertDimensionFields} (#5520)\n * and {@link assertWhereFields} (#5669) — one per request key that can carry a\n * field name. All three answer the same question — does the object actually\n * have the column this member resolves to — and all three must answer it here,\n * because from the strategy onwards the answer is the driver's `no such\n * column`.\n *\n * They run in request-key order (measures → dimensions/timeDimensions →\n * where), so a query that gets several wrong is answered about one at a time,\n * naming a real mistake either way.\n */\n private ensureCube(query: AnalyticsQuery): void {\n const name = query.cube!;\n let cube = this.cubeRegistry.get(name);\n\n if (!cube) {\n // [#3867] Auto-inference below sets `cube.sql = name`, so from here on\n // the queried string IS a physical table name. Verify it names a\n // registered object BEFORE that happens — otherwise `/analytics/query`\n // is a way to aggregate over any table the connection can see, exactly\n // the hole #3770 closed on the data path. A registered Cube needs no\n // such check: it was authored, and its `sql` is whatever it declares.\n this.assertInferableCube(name);\n cube = this.inferCubeFromQuery(query);\n // [#4437] Validate the inferred measures' SOURCE FIELDS before the cube\n // is registered — a rejected query must leave no trace in the registry\n // (same rule the #3867 gate above keeps), or a retry would find a\n // \"registered\" cube carrying the bogus measure and sail straight to SQL.\n this.assertMeasureFields(query, cube, Object.keys(cube.measures));\n // [#5520] …and the dimensions', for exactly the same reason. On this path\n // `cube.dimensions` was minted from the query moments ago, so the bogus\n // spelling is in there — which is why the suggestion list is computed by\n // subtraction inside the gate rather than echoed verbatim.\n this.assertDimensionFields(query, cube, Object.keys(cube.dimensions));\n // [#5669] …and the `where`'s, third and last of the three request keys that\n // carry a field name. Its members are read from the filter TREE, not from\n // `cube.dimensions` — which on this path was minted from this very query,\n // bogus spelling included.\n this.assertWhereFields(query, cube, Object.keys(cube.dimensions));\n this.cubeRegistry.register(cube);\n // A scalar query — only measures, no grouping (no `dimensions`/\n // `timeDimensions`) — is the first-class \"metric over an object\" path\n // (e.g. the `object-metric` KPI widget). Auto-inferring a count/sum cube\n // is the intended behaviour there, so log at debug. A query that groups\n // by an explicit dimension or time bucket almost certainly meant to hit a\n // registered cube; keep that at warn so a forgotten registration is loud.\n const isScalarMetric =\n (query.dimensions?.length ?? 0) === 0 && (query.timeDimensions?.length ?? 0) === 0;\n const message =\n `[Analytics] No cube registered for \"${name}\"; auto-inferred a minimal cube ` +\n `(sql=\"${name}\", measures=${Object.keys(cube.measures).join(',') || '(none)'}, ` +\n `dimensions=${Object.keys(cube.dimensions).join(',') || '(none)'}). ` +\n `Define an explicit Cube in your stack for full control.`;\n if (isScalarMetric) this.logger.debug(message);\n else this.logger.warn(message);\n return;\n }\n\n // Cube exists — check for unknown measures referenced by the query and\n // augment the cube with suffix-inferred Metric definitions so callers\n // that pass `<field>_sum` / `<field>_avg` etc. get the right aggregation.\n //\n // [#5918] This is the SECOND measure mint, and it judges a dotted spelling\n // exactly as the ad-hoc one does — `mintableMeasureKey` owns the rule. It\n // has to: the ad-hoc path REGISTERS what it infers, so from the second\n // request onwards a cube minted moments ago by `inferCubeFromQuery` is\n // \"registered\" and arrives here. Measured on `origin/main` `01faeb13a`,\n // one service, two queries:\n //\n // ① measures: ['count'] → SELECT COUNT(*) … (warms the registry)\n // ② measures: ['owner.region_count_distinct'] → SELECT COUNT(DISTINCT region) AS \"owner.region_count_distinct\"\n //\n // i.e. the silent wrong column #5918 reports, reached through this loop\n // instead of that one. Refusing in only one of the two would have closed the\n // cold request and left every warm one exactly as it was.\n //\n // A measure the cube DECLARES is never minted, so it never reaches the\n // rule — including a declared DOTTED key, which `lookupMember` resolves by\n // direct hit and which this loop must therefore check for verbatim FIRST or\n // it would refuse a cube's own authored vocabulary.\n const extraMeasures: Record<string, any> = {};\n for (const m of query.measures || []) {\n if (cube.measures[m] || extraMeasures[m]) continue;\n const key = mintableMeasureKey(m, name);\n if (cube.measures[key] || extraMeasures[key]) continue;\n extraMeasures[key] = inferMeasure(key);\n }\n if (Object.keys(extraMeasures).length > 0) {\n const augmented: Cube = {\n ...cube,\n measures: { ...cube.measures, ...extraMeasures },\n };\n // [#4437] The cube's DECLARED measures are the ones a caller may name;\n // the suffix-inferred entries just added are a convenience, not a\n // vocabulary. Snapshot the declared list BEFORE registering the augmented\n // cube so the rejection can suggest what the caller could have meant —\n // and so a rejected query leaves the registry as it found it.\n this.assertMeasureFields(query, augmented, Object.keys(cube.measures));\n // [#5520] Dimensions are never augmented (nothing infers one), so the\n // authored/compiled list IS the vocabulary a caller may name — and the one\n // the rejection suggests.\n this.assertDimensionFields(query, augmented, Object.keys(cube.dimensions));\n // [#5669] The `where` gate resolves a filter member through dimensions AND\n // measures (that is what the strategies do for a filter member), so it is\n // handed the AUGMENTED cube — a caller filtering on a suffix-inferred\n // measure must be judged against the same bag the strategy will read.\n this.assertWhereFields(query, augmented, Object.keys(cube.dimensions));\n this.cubeRegistry.register(augmented);\n this.logger.debug(\n `[Analytics] Augmented cube \"${name}\" with inferred measures: ${Object.keys(extraMeasures).join(',')}`,\n );\n } else {\n // No inference happened — every measure is declared. Still validate: an\n // authored cube can declare a measure over a field the object dropped.\n this.assertMeasureFields(query, cube, Object.keys(cube.measures));\n this.assertDimensionFields(query, cube, Object.keys(cube.dimensions));\n this.assertWhereFields(query, cube, Object.keys(cube.dimensions));\n }\n }\n\n /**\n * [#4437] Reject a measure whose SOURCE FIELD the backing object does not\n * have, BEFORE the strategy compiles it into SQL.\n *\n * `inferMeasure` maps a suffix convention onto a field name and has no way to\n * know whether that field exists: `ghost_sum` happily became `SUM(ghost)`, the\n * driver threw `no such column`, and the caller got\n * `500 {\"code\":\"SQLITE_ERROR\",\"message\":\"Internal server error\"}` — a driver\n * error class on the wire, and nothing actionable, for what is a plain typo.\n * The DATA route has refused the same mistake with a `400 INVALID_FIELD`\n * naming the field since #4315/#4254; this is the analytics half of that\n * answer, and it is deliberately the SAME envelope (`code`/`field`/`object`/\n * `param`) so one mistake has one shape across both routes.\n *\n * What it checks, and what it deliberately does not:\n *\n * - Only when the cube's `sql` is a bare OBJECT NAME. An authored cube whose\n * `sql` is a real SQL expression has no field list to check against.\n * - Only when {@link AnalyticsServiceConfig.getObjectFieldNames} answers.\n * Absent hook / unknown object → stand down (see the config field's doc).\n * - Only measures whose source is a BARE COLUMN. `count(*)` has no source\n * field, and a dotted reference (`account.industry`) resolves through a\n * join whose target this check cannot see — both pass through untouched.\n * - `id` / `created_at` / `updated_at` are admitted unconditionally, matching\n * the data path's `resolveQueryFields`: they are engine-assigned rather than\n * declared, and a gate stricter than the engine it guards would reject\n * queries that used to work.\n *\n * [#5918] Its `stripPrefix` below is deliberately NOT narrowed the way the two\n * MINTS were. This is a RESOLVER — it mirrors `lookupMember`'s tiers to answer\n * \"which Metric will the strategy read\", and that tier order did not change.\n * What changed is what can reach it: a dotted measure is now either a\n * `<cube>.` qualifier or a key the cube itself declares, because every other\n * dotted spelling is refused at the mint before this gate runs.\n */\n private assertMeasureFields(query: AnalyticsQuery, cube: Cube, declaredMeasures: string[]): void {\n const probe = this.getObjectFieldNames;\n if (!probe) return;\n const measures = query.measures ?? [];\n if (measures.length === 0) return;\n\n const object = typeof cube.sql === 'string' ? cube.sql.trim() : '';\n if (!object || !BARE_IDENTIFIER.test(object)) return;\n const fieldNames = probe(object);\n if (!fieldNames || fieldNames.length === 0) return;\n const known = new Set<string>([...fieldNames, 'id', 'created_at', 'updated_at']);\n\n const stripPrefix = (m: string) => (m.includes('.') ? m.split('.').slice(1).join('.') : m);\n /** The source field a measure aggregates, or null when there is nothing to check. */\n const sourceFieldOf = (measure: string): string | null => {\n const metric = cube.measures[stripPrefix(measure)] as { type?: string; sql?: unknown } | undefined;\n if (!metric) return null;\n // `count(*)` is the one legitimately field-less aggregate.\n if (metric.type === 'count' && (metric.sql === '*' || metric.sql == null)) return null;\n const source = typeof metric.sql === 'string' ? metric.sql.trim() : '';\n if (!source || source === '*' || !BARE_IDENTIFIER.test(source)) return null;\n return source;\n };\n\n // Two passes so the rejection can suggest the measures that WOULD have\n // worked. On the auto-inference path `cube.measures` already carries the\n // caller's own bogus spelling (it was inferred from the query moments ago),\n // so echoing the cube's measure list verbatim would offer the typo back as\n // a valid alternative — the one suggestion guaranteed to be wrong.\n const invalid = new Set<string>();\n for (const measure of measures) {\n const source = sourceFieldOf(measure);\n if (source && !known.has(source)) invalid.add(stripPrefix(measure));\n }\n if (invalid.size === 0) return;\n const usable = declaredMeasures.filter((m) => !invalid.has(m));\n\n for (const measure of measures) {\n const source = sourceFieldOf(measure);\n if (!source || known.has(source)) continue;\n\n const err = new Error(\n `Measure '${measure}' on cube '${cube.name}' aggregates field '${source}', which object ` +\n `'${object}' does not have. ` +\n `Valid measures: ${usable.join(', ') || '(none)'}. ` +\n `Other measures are inferred from the object's OWN fields as ` +\n `'<field>_sum' / '_avg' / '_min' / '_max' / '_count_distinct', so check the spelling of ` +\n `'${source}' — known fields: ${[...fieldNames].sort().join(', ')}.`,\n ) as Error & { code?: string; status?: number; field?: string; object?: string; param?: string; measure?: string };\n err.code = 'INVALID_FIELD';\n err.status = 400;\n err.field = source;\n err.object = object;\n err.param = 'measures';\n err.measure = measure;\n throw err;\n }\n }\n\n /**\n * [#5520] Reject a DIMENSION whose source field the backing object does not\n * have, BEFORE the strategy compiles it into `GROUP BY`.\n *\n * The symmetric half of {@link assertMeasureFields}. #4437 closed the measure\n * side and stopped there, so the identical mistake one request key over still\n * reached the driver:\n *\n * ```\n * POST /analytics/query {\"cube\":\"crm_account\",\"measures\":[\"account_count\"],\"dimensions\":[\"bogus_dim\"]}\n * → 500 {\"code\":\"SQLITE_ERROR\",\"message\":\"Internal server error\"}\n *\n * POST /analytics/dataset/query {\"selection\":{\"dimensions\":[\"bogus_dim\"],…}}\n * → 500 {\"code\":\"ANALYTICS_QUERY_FAILED\",\n * \"error\":\"SELECT bogus_dim AS \\\"bogus_dim\\\", … GROUP BY bogus_dim - no such column: bogus_dim\"}\n * ```\n *\n * A driver error class as the caller's `error.code` is the ADR-0112 violation\n * #4437 named, and the dataset face additionally echoed the generated\n * statement — physical table and column names — back to the caller. The\n * envelope here is deliberately the SAME as the measure gate's\n * (`INVALID_FIELD`/400 + `field`/`object`/`param`), because \"the query names a\n * field the object does not have\" is ONE mistake and must have one wire shape\n * whichever member kind carried it.\n *\n * What it checks, and what it deliberately does not:\n *\n * - **Both dimension keys.** `query.dimensions` and `query.timeDimensions`\n * land in the same `cube.dimensions` bag, are resolved by the same\n * `lookupMember`, and produced the same 500 (a bogus time dimension became\n * `date_trunc('month', bogus_at)`); `param` reports which key carried it.\n * - **An UNDECLARED but real field stays legal.** `dimensions: ['phone']` on a\n * cube that never declared `phone` groups by `phone` today — the dimension\n * twin of measure auto-inference, and an established contract. So the\n * question asked is \"does the OBJECT have this field\", never \"did the cube\n * declare this dimension\". An undeclared member is checked against the\n * object under the name the strategies would use as the column (their own\n * `resolveDimensionSql`/`resolveFieldName` fallback: the member itself).\n * - Only when the cube's `sql` is a bare OBJECT NAME, only when\n * {@link AnalyticsServiceConfig.getObjectFieldNames} answers, and only for\n * sources that are BARE COLUMNS — same three stand-downs as the measure\n * gate, for the same reasons (no field list to check against; nothing\n * authoritative to consult; a dotted reference resolves through a join whose\n * target this gate cannot see, so it belongs to the join allowlist).\n * - `id` / `created_at` / `updated_at` are admitted unconditionally, matching\n * the data path's `resolveQueryFields`.\n *\n * Runs after the measure gate and before the `where` gate on each `ensureCube`\n * path, so a query that gets several wrong is answered about its measure\n * first — one rejection at a time, naming a real mistake either way.\n */\n private assertDimensionFields(query: AnalyticsQuery, cube: Cube, declaredDimensions: string[]): void {\n const probe = this.getObjectFieldNames;\n if (!probe) return;\n /** Every dimension this query names, tagged with the request key it came from. */\n const members: Array<{ member: string; param: 'dimensions' | 'timeDimensions' }> = [\n ...(query.dimensions ?? []).map((member) => ({ member, param: 'dimensions' as const })),\n ...(query.timeDimensions ?? []).map((td) => ({ member: td.dimension, param: 'timeDimensions' as const })),\n ];\n if (members.length === 0) return;\n\n const object = typeof cube.sql === 'string' ? cube.sql.trim() : '';\n if (!object || !BARE_IDENTIFIER.test(object)) return;\n const fieldNames = probe(object);\n if (!fieldNames || fieldNames.length === 0) return;\n const known = new Set<string>([...fieldNames, 'id', 'created_at', 'updated_at']);\n\n /**\n * The `cube.dimensions` key a member resolves to (for the suggestion list)\n * and the column it groups by — `source: null` meaning \"nothing to check\".\n *\n * [#5669] The body moved to the module-level {@link resolveMemberSource},\n * shared with the `where` gate. `'dimension'` keeps this call site's\n * resolution exactly as #5520 wrote it: `cube.dimensions` only, matching\n * `resolveDimensionSql` / `resolveFieldName(…, 'dimension')`.\n */\n const resolve = (member: string): { key: string; source: string | null } =>\n resolveMemberSource(cube, member, 'dimension');\n\n // Two passes, for the reason the measure gate has two: on the auto-inference\n // path `cube.dimensions` was minted from this very query, so echoing its keys\n // verbatim would offer the caller their own typo back as a valid alternative.\n const invalid = new Set<string>();\n for (const { member } of members) {\n const { key, source } = resolve(member);\n if (source && !known.has(source)) invalid.add(key);\n }\n if (invalid.size === 0) return;\n const usable = declaredDimensions.filter((d) => !invalid.has(d));\n\n for (const { member, param } of members) {\n const { source } = resolve(member);\n if (!source || known.has(source)) continue;\n\n const kind = param === 'timeDimensions' ? 'Time dimension' : 'Dimension';\n const verb = param === 'timeDimensions' ? 'buckets' : 'groups by';\n const err = new Error(\n `${kind} '${member}' on cube '${cube.name}' ${verb} field '${source}', which object ` +\n `'${object}' does not have. ` +\n `Valid dimensions: ${usable.join(', ') || '(none)'}. ` +\n `Any of the object's OWN fields may also be used as a dimension without the cube ` +\n `declaring it, so check the spelling of ` +\n `'${source}' — known fields: ${[...fieldNames].sort().join(', ')}.`,\n ) as Error & { code?: string; status?: number; field?: string; object?: string; param?: string; dimension?: string };\n err.code = 'INVALID_FIELD';\n err.status = 400;\n err.field = source;\n err.object = object;\n err.param = param;\n err.dimension = member;\n throw err;\n }\n }\n\n /**\n * [#5669] Reject a `where` member whose source field the backing object does\n * not have, BEFORE the strategy compiles it into `WHERE`.\n *\n * The third and last param of one defect. #4437 gated `measures`, #5520 gated\n * `dimensions`/`timeDimensions`, and the filter face — the request key that\n * most often carries a hand-typed field name — had no gate at all:\n *\n * ```\n * POST /analytics/query {\"cube\":\"crm_account\",\"measures\":[\"count\"],\"where\":{\"bogus_col\":\"x\"}}\n * → SELECT COUNT(*) AS \"count\" FROM \"crm_account\" WHERE bogus_col = $1\n * → 500 {\"code\":\"SQLITE_ERROR\",\"message\":\"Internal server error\"}\n * ```\n *\n * Same envelope as its two siblings (`INVALID_FIELD`/400 + `field`/`object`/\n * `param`), because \"the query names a field the object does not have\" is ONE\n * mistake whichever request key carried it, and the DATA route has answered it\n * that way since #4315/#4254 (`resolveQueryFields`).\n *\n * # Where the field names come from: the SQL producer's own reader\n *\n * The members are collected through `normalizeAnalyticsFilterTree` +\n * `collectFilterLeaves` — the SAME pair both strategies call to build the\n * predicate. This is deliberate and is the whole reason this gate is not a\n * second filter-tree walker: a hand-rolled walk would have to re-derive\n * `$and`/`$or`/`$not` recursion, `$`-prefixed operator keys, `$between`\n * lowering, the nested-relation dot flattening (`{owner: {region: 'NA'}}` →\n * member `owner.region`) and the #5334 array lowering, and every divergence\n * would show up as \"the field the gate saw\" not being \"the column that reached\n * SQL\" — in either direction (a phantom rejection, or a hole).\n * `collectFilterLeaves` discards structure, which is exactly right here:\n * whether a predicate sits under an `$or` changes nothing about whether its\n * column exists. (Its doc's warning — never rebuild a predicate from this list\n * — does not apply; this gate builds nothing.)\n *\n * # Three stand-downs at query level, plus the per-member ones\n *\n * - No {@link AnalyticsServiceConfig.getObjectFieldNames}, cube `sql` that is\n * not a bare object name, or a probe that cannot answer for the object — the\n * same three tiers as the measure and dimension gates, for the same reasons.\n * - A `where` the normalizer REFUSES (an unknown operator, a non-array\n * `$and`, an unlowerable filter array) is not judged here: this gate stands\n * down and lets the refusal happen where it already does. Those inputs\n * already answer `INVALID_FILTER`/400 from the strategy (#5352/#5367's\n * geography, not this gate's), and pulling them forward into `ensureCube`\n * would newly refuse them on the draft-preview path too, whose\n * `matchesWhere` never consults the normalizer at all. A field gate that\n * cannot read the tree has nothing to say about it.\n * - Per member, {@link resolveMemberSource} stands down on an expression `sql`\n * and on a dotted relation traversal — for the dimension gate's reasons.\n *\n * # Array `where` IS gated, and #5353's fix did not change that\n *\n * Since #5334 an array `where` is lowered by `normalizeAnalyticsFilterTree`\n * and compiles to the identical predicate — a measured fact,\n * `where: [['bogus_col','=','x']]` and `where: {bogus_col: 'x'}` both produce\n * `WHERE bogus_col = $1` and hand `executeAggregate` the same\n * `{bogus_col: 'x'}`. Gating one spelling and not the other would answer one\n * mistake two ways, which is the split this whole gate family exists to close.\n *\n * `inferCubeFromQuery` used to skip an array `where` when minting the ad-hoc\n * cube's `dimensions` — a separate question (the cube's dimension VOCABULARY,\n * not which columns reach the driver), fixed by #5353 by lowering before\n * reading keys. Because this gate reads filter LEAVES rather than\n * `cube.dimensions`, that fix could not change its verdicts, and measurement\n * confirms it did not: the array where's keys now reach `cube.dimensions`, so\n * {@link resolveMemberSource} takes the DECLARED-dimension branch for those\n * members instead of the undeclared-bare-column one — and both branches yield\n * the same `source` for the same member, since the minted dimension's `sql` IS\n * the member name. What did change is the rejection's suggestion list, in the\n * direction that closes the split: `Valid filter members:` now reads the same\n * for both spellings of one filter.\n */\n private assertWhereFields(query: AnalyticsQuery, cube: Cube, declaredDimensions: string[]): void {\n const probe = this.getObjectFieldNames;\n if (!probe) return;\n const where = (query as { where?: unknown }).where;\n if (!where || typeof where !== 'object') return;\n\n const object = typeof cube.sql === 'string' ? cube.sql.trim() : '';\n if (!object || !BARE_IDENTIFIER.test(object)) return;\n const fieldNames = probe(object);\n if (!fieldNames || fieldNames.length === 0) return;\n const known = new Set<string>([...fieldNames, 'id', 'created_at', 'updated_at']);\n\n /** Every member the compiled predicate will bind against, structure discarded. */\n let members: string[];\n try {\n members = collectFilterLeaves(normalizeAnalyticsFilterTree(query)).map((leaf) => leaf.member);\n } catch {\n // A `where` this layer refuses outright — see the stand-down note above.\n return;\n }\n if (members.length === 0) return;\n\n // Two passes, for the reason the measure and dimension gates have two: on the\n // auto-inference path `inferCubeFromQuery` mints the `where`'s own field keys\n // into `cube.dimensions` — since #5353 for the array spelling too — so\n // echoing that bag verbatim would offer the caller their own typo back as a\n // valid filter member.\n const invalid = new Set<string>();\n for (const member of members) {\n const { key, source } = resolveMemberSource(cube, member, 'any');\n if (source && !known.has(source)) invalid.add(key);\n }\n if (invalid.size === 0) return;\n const usable = declaredDimensions.filter((d) => !invalid.has(d));\n\n for (const member of members) {\n const { source } = resolveMemberSource(cube, member, 'any');\n if (!source || known.has(source)) continue;\n\n const err = new Error(\n `Filter member '${member}' in 'where' on cube '${cube.name}' constrains field ` +\n `'${source}', which object '${object}' does not have. ` +\n `Valid filter members: ${usable.join(', ') || '(none)'}. ` +\n `Any of the object's OWN fields may also be filtered on without the cube ` +\n `declaring it, so check the spelling of ` +\n `'${source}' — known fields: ${[...fieldNames].sort().join(', ')}.`,\n ) as Error & { code?: string; status?: number; field?: string; object?: string; param?: string; member?: string };\n err.code = 'INVALID_FIELD';\n err.status = 400;\n err.field = source;\n err.object = object;\n err.param = 'where';\n err.member = member;\n throw err;\n }\n }\n\n /**\n * [#3867] Gate on the cube auto-inference path: a name with no registered\n * Cube may only be inferred into one if it is a registered object.\n *\n * Rejects with `status: 404` / `code: 'CUBE_NOT_FOUND'` so the HTTP boundary\n * answers \"no such cube\" instead of letting the name reach the driver as a\n * table and surfacing whatever the driver says about it. The message names\n * both ways the request could be made valid, because from here the two are\n * genuinely indistinguishable: register a Cube, or register the object.\n *\n * Skips when `isRegisteredObject` was not supplied — see the config field's\n * doc for why that tier is a deliberate stand-down and not a hole.\n */\n private assertInferableCube(name: string): void {\n const isRegisteredObject = this.isRegisteredObject;\n if (!isRegisteredObject) {\n if (!this.warnedNoObjectRegistry) {\n this.warnedNoObjectRegistry = true;\n this.logger.warn(\n '[Analytics] no object-registry hook configured — the cube-inference existence gate ' +\n '(#3867) is INACTIVE for this service; an unregistered cube name reaches the driver ' +\n 'as a raw table name.',\n );\n }\n return;\n }\n if (isRegisteredObject(name)) return;\n const err = new Error(\n `Cube '${name}' not found: no cube is registered under that name, and it is not a ` +\n `registered object either (a cube can only be auto-inferred from a registered object). ` +\n `Define a Cube in your stack, or check the object name.`,\n ) as Error & { code?: string; status?: number; cube?: string };\n err.code = 'CUBE_NOT_FOUND';\n err.status = 404;\n err.cube = name;\n throw err;\n }\n\n /** Build a minimal Cube from the fields referenced by an AnalyticsQuery. */\n private inferCubeFromQuery(query: AnalyticsQuery): Cube {\n const cubeName = query.cube!;\n const measures: Record<string, any> = {};\n const dimensions: Record<string, any> = {};\n\n // [#5739] Strip the `<cube>.` QUALIFIER, and nothing else.\n //\n // The predecessor (`stripPrefix`) dropped the first segment of ANY dotted\n // member, which conflated two different facts wearing the same punctuation:\n //\n // `deal.stage` — the canonical analytics QUALIFIER. `getMeta` hands\n // members out cube-prefixed and callers echo them back,\n // so the prefix is noise and stripping it is right.\n // `owner.region` — a relation TRAVERSAL. Stripping it minted\n // `dimensions.region = {sql: 'region'}`, a BASE-TABLE\n // column, and `lookupMember`'s \"plain second-segment\"\n // tier then found it BEFORE its synthetic-traversal tier\n // could hand the dotted path to the JOIN machinery. Where\n // the base table happened to carry a same-named column\n // that filtered/grouped the WRONG column with no error to\n // read; where it did not, the 400 named `region` for a\n // caller who wrote `owner.region`.\n //\n // Only the first is a qualifier, and only the first is stripped. Everything\n // else is minted VERBATIM (`{sql: 'owner.region'}`), which is precisely what\n // `lookupMember`'s synthetic tier already hands the strategies for an\n // undeclared dotted member — so the ad-hoc path now compiles the traversal\n // the array `where` spelling has compiled all along, and the two spellings\n // converge instead of disagreeing. Maintainer ruling, 2026-08-06 (#5739).\n //\n // Scope: this governs the DIMENSION-shaped mints (`dimensions`, the\n // `where`'s field keys, `timeDimensions`), which SERVE a traversal. The\n // measure mint applies the same qualifier rule but ends the other way —\n // `mintableMeasureKey` refuses a non-qualifier dot outright, because the\n // measure side has no traversal to serve (#5918, and the loop below).\n const stripCubeQualifier = (m: string): string => {\n const dot = m.indexOf('.');\n if (dot < 0) return m;\n return m.slice(0, dot) === cubeName ? m.slice(dot + 1) : m;\n };\n\n // Always provide a default `count` measure\n measures.count = { name: 'count', label: 'Count', type: 'count', sql: '*' };\n\n for (const m of query.measures || []) {\n // [#5918] MEASURES no longer take the blanket strip #5739 left them with.\n // That strip cast `owner.region_count_distinct` onto the BASE `region`\n // column — silently where the object had one, and as a #4437 400 naming\n // the stripped tail where it did not. Neither is the caller's query.\n //\n // The ruling here is NOT #5739's (mint the traversal verbatim): measures\n // have no traversal tier to converge on, so there is nothing correct to\n // converge to. It is a loud refusal instead — see `mintableMeasureKey`,\n // which owns the rule and the envelope, and which the augmentation mint in\n // `ensureCube` shares so the same spelling gets the same answer on a warm\n // registry. Maintainer ruling, 2026-08-07 (#5918).\n const key = mintableMeasureKey(m, cubeName);\n if (measures[key]) continue;\n const inferred = inferMeasure(key);\n measures[key] = inferred;\n }\n\n for (const d of query.dimensions || []) {\n const key = stripCubeQualifier(d);\n if (dimensions[key]) continue;\n dimensions[key] = { name: key, label: key, type: 'string', sql: key };\n }\n\n // The `where`'s field keys seed dimensions too. LOWER FIRST, then read keys:\n // the rule this bag has always followed is \"the `where`'s own top-level keys\n // are field names\", and the only reason an ARRAY `where` was skipped here is\n // that it was not a filter when the code was written.\n //\n // [#5353] The `!Array.isArray(query.where)` guard this replaces predates\n // #5334. Since #5334 an array `where` IS a filter — lowered by\n // `lowerAnalyticsWhere` and compiling to a byte-identical predicate — so\n // skipping it meant ONE filter, spelled two ways, minted two different\n // cubes: `{stage: 'won'}` seeded `dimensions.stage`, `[['stage','=','won']]`\n // seeded nothing. Lowering first makes the spelling stop mattering, which is\n // the same fix #5334 applied one layer down.\n let lowered: Record<string, unknown> | null = null;\n try {\n lowered = lowerAnalyticsWhere(query);\n } catch {\n // A `where` the lowering REFUSES (an unlowerable array) is not judged\n // here — the same stand-down `assertWhereFields` makes, for the same\n // reason: that refusal already happens in the strategy with an\n // `INVALID_FILTER`/400 envelope (#5352/#5367), and raising it from\n // `ensureCube` instead would move the answer's geography and would newly\n // refuse the draft-preview path, whose `matchesWhere` never consults the\n // normalizer at all. A cube minted without those keys is exactly what a\n // query that is about to be refused needs.\n }\n if (lowered) {\n // `conjunctFieldKeys` descends `$and` — which the LOWERING introduces for a\n // flat array (`[[a,…],[b,…]]` → `{$and: [{a…},{b…}]}`) — and not `$or` /\n // `$not`, which contribute no key on either spelling. Deliberately NOT\n // `collectFilterLeaves`: the leaves answer \"what does the compiled\n // predicate BIND\", this bag answers \"what may a caller NAME\", and the two\n // genuinely differ — `{stage: {$in: []}}` lowers to the boolean constant\n // FALSE, binding nothing while still naming `stage`.\n for (const key of conjunctFieldKeys(lowered)) {\n // [#5739] Dotted keys ride this loop too, and that is the FOLD #5353 left\n // for this issue. Until the ruling, a dotted key was skipped here and\n // re-minted from the RAW object `where` by a separate residue loop —\n // stripped to its tail, so one filter got one answer per spelling: the\n // object spelling mis-cast `owner.region` to base `region`, the array\n // spelling minted nothing and compiled the traversal. One loop over the\n // LOWERED condition mints both spellings identically, and\n // `stripCubeQualifier` keeps them a traversal instead of a base column,\n // so the cube AND the compiled SQL now match on either spelling.\n const minted = stripCubeQualifier(key);\n if (dimensions[minted] || measures[minted]) continue;\n dimensions[minted] = { name: minted, label: minted, type: 'string', sql: minted };\n }\n }\n\n for (const td of query.timeDimensions || []) {\n const key = stripCubeQualifier(td.dimension);\n if (dimensions[key]) continue;\n dimensions[key] = {\n name: key, label: key, type: 'time', sql: key,\n granularities: ['day', 'week', 'month', 'quarter', 'year'],\n };\n }\n\n return {\n name: cubeName,\n title: cubeName,\n sql: cubeName,\n measures,\n dimensions,\n public: false,\n };\n }\n\n /**\n * Walk the strategy chain and return the first strategy that can handle the\n * query. `skip` excludes strategies that already proved incapable at\n * execution time (see {@link query}'s RAW_SQL_UNSUPPORTED fallback).\n */\n private resolveStrategy(\n query: AnalyticsQuery,\n ctx: StrategyContext,\n skip?: Set<AnalyticsStrategy>,\n ): AnalyticsStrategy {\n for (const strategy of this.strategies) {\n if (skip?.has(strategy)) continue;\n if (strategy.canHandle(query, ctx)) {\n return strategy;\n }\n }\n // [#7598] Name the one decline that is about the QUERY rather than about\n // the deployment. Since the 2026-08-12 ruling `NativeSQLStrategy` declines\n // a cross-field `{ $field }` comparison so it routes to the engine path —\n // so a host advertising `nativeSql` WITHOUT an aggregate bridge now reaches\n // this exit for a filter it used to (wrongly) compile. The bare message\n // below would send that operator off to check their driver configuration,\n // which is not the problem: every other query on that cube still works.\n //\n // Reachability, measured: NOT from `AnalyticsServicePlugin`, whose default\n // `queryCapabilities` derives BOTH flags from the bridges it wired\n // (`objectqlAggregate: !!executeAggregate`) and auto-wires the aggregate\n // bridge from the engine — so a real deployment that has `nativeSql` has\n // `objectqlAggregate` too. Reachable only from a host that overrides\n // `queryCapabilities` by hand. Cheap to say, and the alternative is a dead\n // end that reads like a misconfiguration.\n const crossField = findCrossFieldComparand(lowerAnalyticsWhereQuietly(query));\n throw new Error(\n `[Analytics] No strategy can handle query for cube \"${query.cube}\". ` +\n `Checked: ${this.strategies.map(s => s.name).join(', ')}${skip?.size ? ` (skipped at runtime: ${[...skip].map((s) => s.name).join(', ')})` : ''}. ` +\n (crossField\n ? `This query's filter compares against the field reference ` +\n `{ \"$field\": \"${crossField.ref}\" } under \"${crossField.op}\" on \"${crossField.field}\", and ` +\n `NativeSQLStrategy DECLINES a cross-field comparison so that it routes to the ObjectQL ` +\n `engine path — whose driver compiles it and enforces the #5222 rulings with metadata it ` +\n `owns (#7598). No such path is configured here, so the capability is unavailable on this ` +\n `deployment: supply an \\`executeAggregate\\` bridge (the plugin auto-wires one from the ` +\n `engine), or compare against a literal value. Every other query on this cube is ` +\n `unaffected. `\n : '') +\n 'Ensure a compatible driver is configured or a fallback service is registered.',\n );\n }\n}\n\n/**\n * [#7598] A query's `where`, lowered — the same input the strategies scan, so\n * the diagnostic above describes the filter the decline actually saw.\n *\n * Quiet by construction: a `where` that cannot even be lowered is refused\n * downstream with its own message and envelope, and there is no reference to\n * find in one that does not lower.\n */\nfunction lowerAnalyticsWhereQuietly(query: AnalyticsQuery): unknown {\n try {\n return lowerAnalyticsWhere(query);\n } catch {\n return null;\n }\n}\n\n/**\n * [#5918] The `cube.measures` KEY a request's `measures` entry may be MINTED\n * under — or a loud refusal when the entry is a dotted member.\n *\n * Two mint sites feed {@link inferMeasure}, and both go through here: the\n * ad-hoc mint in {@link AnalyticsService.inferCubeFromQuery} (no cube\n * registered) and the suffix-augmentation loop in\n * {@link AnalyticsService.ensureCube} (a cube exists but does not declare the\n * measure). They are the same act — inventing a Metric out of a request\n * spelling — so they must judge the spelling the same way.\n *\n * ## What is refused, and why it is a refusal rather than a traversal\n *\n * A `<cube>.` QUALIFIER is stripped, exactly as `inferCubeFromQuery`'s\n * `stripCubeQualifier` does for the dimension-shaped mints (#5739): `getMeta`\n * hands members out cube-prefixed and\n * callers echo them back, so that prefix is noise. **Every other dot is\n * refused.** The predecessor dropped the first segment of ANY dotted member,\n * which meant `owner.region_count_distinct` minted\n * `measures.region_count_distinct = {type:'count_distinct', sql:'region'}` — the\n * BASE table's own `region` column — and then:\n *\n * ```\n * NativeSQL → SELECT COUNT(DISTINCT region) AS \"owner.region_count_distinct\" FROM \"crm_account\"\n * ObjectQL → aggregations: [{field:'region', method:'count_distinct', alias:'owner.region_count_distinct'}]\n * ```\n *\n * No JOIN, no error, and a response column LABELLED with a relation attribute\n * whose number came from the base table — a wrong answer the caller cannot see\n * (measured on `origin/main` `01faeb13a`). Where the base object had no\n * same-named column it degraded instead to #4437's `400 INVALID_FIELD` naming\n * the STRIPPED tail (`Measure 'owner.score_sum' … aggregates field 'score'`) —\n * honest about what reached SQL, but naming a string the caller never wrote.\n *\n * #5739 fixed the same punctuation on the three DIMENSION-shaped mints by\n * minting the traversal verbatim, and that ruling deliberately does NOT carry\n * over here: `lookupMember`'s synthetic relation-traversal tier is\n * dimension-only (`if (kind === 'dimension')`), so a dotted measure has no\n * correct traversal answer to converge on. Minting it verbatim would only\n * re-route it into ObjectQL's `cannot evaluate a cross-object measure` — which\n * would be the wrong diagnosis for a plain typo like `total.sum`. Maintainer\n * ruling, 2026-08-07 (#5918, option 3): refuse the dotted measure LOUDLY, with\n * the caller's own spelling in the envelope. A genuine traversal measure\n * (`SUM(\"owner\".\"amount\")` + LEFT JOIN) would be a capability with its own\n * justification, not a side effect of a strip.\n *\n * The refusal deliberately reaches BOTH the typo (`total.sum`) and the genuine\n * traversal intent (`owner.amount_sum`): the two are lexically indistinguishable\n * on this path, and telling them apart would need field metadata the ad-hoc\n * path does not have (`getObjectFieldNames` answers names, not types or\n * relation targets). One honest 400 for both beats a metadata capability nobody\n * has asked for.\n *\n * A measure the cube DECLARES under a dotted key is not this function's\n * business — it was authored, not minted, and `lookupMember` resolves it by\n * direct hit. Both call sites check that first.\n */\nfunction mintableMeasureKey(member: string, cubeName: string): string {\n const dot = member.indexOf('.');\n if (dot < 0) return member;\n if (member.slice(0, dot) === cubeName) return member.slice(dot + 1);\n\n throw invalidMemberError(\n `[Analytics] Measure '${member}' on cube '${cubeName}' is a DOTTED member, and ` +\n `measures do not traverse relationships — only dimensions do — so there is no ` +\n `related column for this to aggregate. Until #5918 the prefix was silently ` +\n `dropped, so the aggregate ran against '${cubeName}' itself while the result ` +\n `column kept the label '${member}'. Aggregate one of the object's OWN fields ` +\n `instead ('<field>_sum' / '_avg' / '_min' / '_max' / '_count_distinct'), or ` +\n `declare a Cube whose measure names the related column in its own 'sql'. The ` +\n `only dot a measure may carry is the '${cubeName}.' qualifier.`,\n { member, param: 'measures', cube: cubeName },\n );\n}\n\n/**\n * Infer a Metric definition from a measure key name.\n *\n * Recognised suffix conventions (matches dashboard widget translators that\n * emit measures like `<field>_sum`, `<field>_avg`):\n *\n * | Suffix | Aggregation |\n * |:-------------------|:----------------|\n * | `count` | `count(*)` |\n * | `_sum` | `sum(field)` |\n * | `_avg` / `_average`| `avg(field)` |\n * | `_min` | `min(field)` |\n * | `_max` | `max(field)` |\n * | `_count_distinct` | `count(distinct field)` |\n *\n * Anything else is treated as a `sum(<key>)` — best-effort default for an\n * unknown numeric measure.\n */\nexport function inferMeasure(key: string): { name: string; label: string; type: 'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'; sql: string } {\n if (key === 'count') {\n return { name: 'count', label: 'Count', type: 'count', sql: '*' };\n }\n const suffixes: Array<[string, 'sum' | 'avg' | 'min' | 'max' | 'count_distinct']> = [\n ['_count_distinct', 'count_distinct'],\n ['_sum', 'sum'],\n ['_avg', 'avg'],\n ['_average', 'avg'],\n ['_min', 'min'],\n ['_max', 'max'],\n ];\n for (const [suffix, type] of suffixes) {\n if (key.endsWith(suffix)) {\n const field = key.slice(0, -suffix.length) || '*';\n return { name: key, label: key, type, sql: field };\n }\n }\n return { name: key, label: key, type: 'sum', sql: key };\n}\n\n/**\n * FallbackDelegateStrategy — Internal strategy for fallback service delegation.\n *\n * Automatically added to the strategy chain when `fallbackService` is configured.\n * Not exported — consumers who need explicit in-memory support should use\n * `InMemoryStrategy` from `@objectstack/driver-memory`.\n */\nclass FallbackDelegateStrategy implements AnalyticsStrategy {\n readonly name = 'FallbackDelegateStrategy';\n readonly priority = 30;\n\n canHandle(query: AnalyticsQuery, ctx: StrategyContext): boolean {\n if (!query.cube) return false;\n return !!ctx.fallbackService;\n }\n\n async execute(query: AnalyticsQuery, ctx: StrategyContext): Promise<AnalyticsResult> {\n return ctx.fallbackService!.query(query);\n }\n\n async generateSql(query: AnalyticsQuery, ctx: StrategyContext): Promise<{ sql: string; params: unknown[] }> {\n if (ctx.fallbackService?.generateSql) {\n return ctx.fallbackService.generateSql(query);\n }\n return {\n sql: `-- FallbackDelegateStrategy: SQL generation not supported for cube \"${query.cube}\"`,\n params: [],\n };\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Cube } from '@objectstack/spec/data';\n\n/**\n * CubeRegistry — Central registry for analytics cube definitions.\n *\n * Cubes can be registered from two sources:\n * 1. **Manifest definitions** — Explicit cube definitions in `objectstack.config.ts`.\n * 2. **Object schema inference** — Auto-generated cubes from ObjectQL object schemas.\n *\n * The registry is the single source of truth for cube metadata discovery\n * (used by `getMeta()` and the strategy chain).\n */\nexport class CubeRegistry {\n private cubes = new Map<string, Cube>();\n\n /** Register a single cube definition. Overwrites if name already exists. */\n register(cube: Cube): void {\n this.cubes.set(cube.name, cube);\n }\n\n /** Register multiple cube definitions at once. */\n registerAll(cubes: Cube[]): void {\n for (const cube of cubes) {\n this.register(cube);\n }\n }\n\n /** Get a cube definition by name. */\n get(name: string): Cube | undefined {\n return this.cubes.get(name);\n }\n\n /** Check if a cube is registered. */\n has(name: string): boolean {\n return this.cubes.has(name);\n }\n\n /** Return all registered cubes. */\n getAll(): Cube[] {\n return Array.from(this.cubes.values());\n }\n\n /** Return all cube names. */\n names(): string[] {\n return Array.from(this.cubes.keys());\n }\n\n /** Number of registered cubes. */\n get size(): number {\n return this.cubes.size;\n }\n\n /** Remove all cubes. */\n clear(): void {\n this.cubes.clear();\n }\n\n /**\n * Auto-generate a cube definition from an object schema.\n *\n * Heuristic rules:\n * - `number` fields → `sum`, `avg`, `min`, `max` measures\n * - `boolean` fields → `count` measure (count where true)\n * - All non-computed fields → dimensions\n * - `date`/`datetime` fields → time dimensions with standard granularities\n * - A default `count` measure is always added\n *\n * @param objectName - The snake_case object name (used as table/cube name)\n * @param fields - Array of field descriptors `{ name, type, label? }`\n */\n inferFromObject(\n objectName: string,\n fields: Array<{ name: string; type: string; label?: string }>,\n ): Cube {\n const measures: Record<string, any> = {\n count: {\n name: 'count',\n label: 'Count',\n type: 'count',\n sql: '*',\n },\n };\n const dimensions: Record<string, any> = {};\n\n for (const field of fields) {\n const label = field.label || field.name;\n\n // All fields become dimensions\n const dimType = this.fieldTypeToDimensionType(field.type);\n dimensions[field.name] = {\n name: field.name,\n label,\n type: dimType,\n sql: field.name,\n ...(dimType === 'time'\n ? { granularities: ['day', 'week', 'month', 'quarter', 'year'] }\n : {}),\n };\n\n // Numeric fields also become aggregation measures\n if (field.type === 'number' || field.type === 'currency' || field.type === 'percent') {\n measures[`${field.name}_sum`] = {\n name: `${field.name}_sum`,\n label: `${label} (Sum)`,\n type: 'sum',\n sql: field.name,\n };\n measures[`${field.name}_avg`] = {\n name: `${field.name}_avg`,\n label: `${label} (Avg)`,\n type: 'avg',\n sql: field.name,\n };\n }\n }\n\n const cube: Cube = {\n name: objectName,\n title: objectName,\n sql: objectName,\n measures,\n dimensions,\n public: false,\n };\n\n this.register(cube);\n return cube;\n }\n\n private fieldTypeToDimensionType(fieldType: string): string {\n switch (fieldType) {\n case 'number':\n case 'currency':\n case 'percent':\n return 'number';\n case 'boolean':\n return 'boolean';\n case 'date':\n case 'datetime':\n return 'time';\n default:\n return 'string';\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Filter Normalization for the Analytics Layer\n *\n * The analytics endpoint accepts filters via the canonical `where`\n * field per the unified Query DSL (`spec/data/query.zod.ts`):\n *\n * - MongoDB-style FilterCondition: `{ field: value }` /\n * `{ field: { $op: value } }` / `{ $and: [...] }` — defined in\n * `spec/data/filter.zod.ts` and used by `find()`, dashboard\n * widget `filter`, RLS, etc.\n *\n * `normalizeAnalyticsFilters` flattens the FilterCondition tree into\n * the internal array form used by the SQL/Mongo pipeline strategies.\n * Strategies stay simple — they only need to know one shape — and the\n * spec is honoured: dashboard metadata is authored once in the\n * canonical MongoDB form and the server normalizes at the boundary.\n *\n * # Coverage — a dropped predicate WIDENS the query, so nothing is dropped\n *\n * Failing to map an operator is not \"not supporting\" it: the predicate simply\n * disappears, the compiled SQL stays valid, and the query returns rows the\n * author excluded. It reads as a chart drawn over the whole dataset (#3650's\n * symptom) and is invisible to any test that asserts the emitted SQL string.\n * `$between`, `$startsWith`, `$endsWith` and `$null` each sat broken that way\n * (#4128), so what this maps is now a complete capability claim over\n * `filter.zod.ts`'s authorable vocabulary:\n *\n * - mapped 1:1 — `$eq` `$ne` `$gt` `$gte` `$lt` `$lte` `$in` `$nin`\n * `$contains` `$notContains` `$startsWith` `$endsWith`;\n * - value-DEPENDENT, so resolved explicitly rather than through the map —\n * `$null` and `$exists`, whose meaning flips with their boolean;\n * - lowered — `$between`, which becomes its two bounds so each strategy's\n * existing upper-bound handling applies the calendar-day whole-day rule\n * (see the note at the lowering);\n * - structural — `$and` / `$or` / `$not`, carried as tree nodes;\n * - anything else THROWS. An operator outside the vocabulary is a caller\n * error, and a loud one beats a silently widened read — the call\n * driver-memory made for the same shape in #3948.\n *\n * `$or` / `$not` were the last of that family, and they were dropped for a\n * structural reason rather than an oversight: this module produced a flat\n * ARRAY, which cannot carry a disjunction. So an author's `{$or: […]}`\n * vanished from the WHERE clause and the widget drew every row. The output is\n * now a {@link NormalizedFilterNode} tree, and each strategy compiles it the\n * way its own backend expresses a disjunction.\n *\n * # `null` is TRUE, and TRUE is a VALUE — not \"nothing happened\" (#5325)\n *\n * {@link buildNode} returns `null` for a condition that constrains nothing\n * (`{}`, an all-`{}` `$and`). That `null` is the boolean constant TRUE, and the\n * two places a compiler forgets it are exactly where this one used to be wrong —\n * the same two squares `read-scope-sql.ts` was wrong on (#5297), because this\n * module was written from it:\n *\n * - TRUE is the AND identity, so dropping it from a `$and` is right — but it\n * ABSORBS a `$or`: one TRUE disjunct makes the whole disjunction TRUE.\n * Filtering it out (`{$or: [{}, {a: 1}]}` → `a = 1`) silently NARROWED a\n * widget's filter to its surviving branches.\n * - `NOT TRUE ≡ FALSE`, so `{$not: {}}` is the zero-row predicate. Producing\n * `null` for it meant no `WHERE` was emitted at all and the widget charted\n * the ENTIRE dataset — the #3650 / #4128 silent-widening class again.\n *\n * FALSE therefore has a spelling of its own ({@link NormalizedFilterNode}'s\n * `const` kind) instead of being representable only as silence. Every compiler\n * of this tree implements it: `native-sql-strategy.compileFilterNode`,\n * `objectql-strategy.filterNodeToCondition` and its display-SQL twin\n * `renderFilterNodeSql`.\n *\n * The EMPTY combinators complete the same boolean algebra (#5322 ruling):\n * `{$and: []}` is TRUE and `{$or: []}` is FALSE — this module used to refuse\n * both fail-closed while the five `FILTER_LOGIC_CASES` backends reduced them;\n * see the note inside {@link buildNode}'s combinator branch for the history\n * and the reasoning the ruling adopted.\n *\n * # `$not` is NULL-safe (#5146)\n *\n * SQL is three-valued and a `WHERE` keeps only TRUE, so a bare `NOT (col = ?)`\n * drops every row whose `col` is NULL — while `driver-memory`, `formula` and\n * (since #5296) `driver-sql` return those rows. One widget filter, two row sets,\n * chosen by whichever backend answered. #5146 ruled the JS answer canonical, and\n * {@link nullSafeNegationOperand} applies the same leaf-wise totalisation\n * `sql-driver.ts` and `read-scope-sql.ts` apply.\n *\n * The rewrite lives HERE rather than in `native-sql-strategy` on purpose: at\n * this layer the guard is STRUCTURE (one more `{col: {$null: false}}` conjunct),\n * not a SQL trick, so it survives `filterNodeToCondition` handing the tree to\n * the ObjectQL engine and holds on any driver behind it — including one that is\n * not NULL-safe by itself. Guarding only in the SQL strategy would make \"what\n * does this widget's `$not` mean\" depend on which backend caught it, which is\n * what #5146 spent a round eliminating. The cost is that the engine path can\n * guard twice (this rewrite, then `driver-sql`'s own); that is idempotent —\n * `NOT (c IS NOT NULL AND (c IS NOT NULL AND c = v))` is the same predicate —\n * so it buys portability for one redundant conjunct.\n *\n * # `$ne` / `$nin` / `$notContains` are NULL-safe too (#5298)\n *\n * Same rule, same reason, one ruling later. The operators that carry their OWN\n * negation had the defect #5146 fixed for `$not`: a bare `col <> ?` is UNKNOWN\n * for a NULL column and the `WHERE` drops the row, while the JS backends return\n * it. Measured on this package's own fixture before the fix (#5977), for\n * `{stage: {$ne: 'won'}}` over rows whose `stage` is NULL:\n *\n * | path | was | now (= JS family) |\n * |----------------------------------------|-----------|-------------------|\n * | `NativeSQLStrategy` (raw SQL) | `2` | `2,3,4` |\n * | `ObjectQLStrategy` display SQL echo | `2` | `2,3,4` |\n * | `ObjectQLStrategy` → engine condition | `2,3,4` | `2,3,4` |\n *\n * The engine column was already right, and that is the whole argument for\n * fixing it HERE: it was right because `driver-sql` guards for itself (#5962),\n * so the Cube face's answer depended on which compiler downstream caught the\n * leaf — three emitters, two answers. `fieldLeaves` now emits the guard as\n * STRUCTURE, an `or` of `notSet` with the comparison, so all three compile the\n * same predicate and none of them needs to know the rule. That is the same\n * trade the `$not` rewrite above took, including its cost: the engine path\n * guards twice, which is idempotent (`c IS NULL OR (c IS NULL OR c <> v)`).\n *\n * Which operators get the guard is NOT a new list — it is\n * {@link nullValueSatisfiesOperator} and {@link operatorIsNullTotal}, the same\n * pair `nullGuardForFieldSpec` consults for the `$not` rewrite, asked about one\n * operator instead of a whole field spec. A leaf is guarded exactly when a NULL\n * value SATISFIES the operator and the compiled leaf is not already total, which\n * is that pair's `allowNull` verdict. Hard-coding the three names would have put\n * a second polarity table in this file, free to drift from the first — and the\n * `$eq`/`$ne` arms of the existing one already turn on the COMPARAND (`$ne:\n * null` compiles to `set`, which is total and must never be widened), so a name\n * list would have been wrong as well as duplicated.\n *\n * # A `null` COMPARAND is a null predicate, not a value (#5332)\n *\n * `{stage: null}` compiled to `IS NULL` while `{stage: {$eq: null}}` compiled to\n * `stage = ''` — one meaning, two answers, inside this one file. The cause was\n * that the `$eq` / `$ne` pair fell through to {@link MONGO_TO_CUBE_OP} like any\n * other comparison and `stringifyForCube(null)` handed it the empty STRING, so a\n * \"stage is empty\" widget compared a real value against columns that are NULL\n * and charted zero rows — silently, with nothing for the author to read. On a\n * text column the `$ne` direction was worse than empty: `''` is a value rows\n * genuinely store, so \"stage is not empty\" EXCLUDED exactly the rows it was asked\n * to keep.\n *\n * The pair is not merely similar to `{$null: true|false}` — `driver-mongodb`\n * TRANSLATES `$null` into it — so {@link fieldLeaves} now emits the same\n * `notSet` / `set` leaves for all three spellings, and the #5146 guard table\n * moved in the same commit (see {@link nullValueSatisfiesOperator}); a guard that\n * still described the old emitter would have negated an always-false conjunction\n * and answered `{$not: {stage: {$eq: null}}}` with every row.\n *\n * # A comparand keeps its own TYPE — there is no round trip any more (#5526)\n *\n * A leaf's `values` used to be `string[]`, so every comparand was encoded to a\n * string on the way in (`stringifyForCube`) and GUESSED back into a type on the\n * way out (`recoverNumber`, behind `coerceFilterValueForSql` /\n * `coerceFilterValueForObjectQL`). An encoding whose alphabet is \"all strings\"\n * and whose decoder is \"does this string look like a number/boolean/null\" has no\n * escape, so author strings COLLIDED with the tokens the encoder wrote for other\n * types. Measured on `main` (#5526's table), for `{code: {$eq: v}}`:\n *\n * | author's `v` | bound (SQL) | bound (engine) |\n * |---|---|---|\n * | `'007'` | `7` (#5528: fixed) | `7` (#5528: fixed) |\n * | `'1.50'` | `1.5` (#5528: fixed)| `1.5` (#5528: fixed)|\n * | `'null'` | real `NULL` | real `null` |\n * | `'true'` | `1` | `true` |\n *\n * Every row of that table is one defect: a TEXT column storing the author's\n * spelling stops matching. `'007'` on SQLite compares an integer against a TEXT\n * column and is never equal; on Postgres `text = integer` is a type error. The\n * `'null'` row is worse than empty — a comparison against real NULL is UNKNOWN\n * for every row, so the widget can never draw anything. Zero-padded strings,\n * `'true'`/`'false'` as enum-ish codes and `'null'` as a literal label are all\n * ordinary business shapes (order numbers, SKUs, postcodes, dialling codes).\n *\n * #5528 narrowed the number half of the decoder (canonical spelling only) as a\n * STOPGAP and said so; this is the ruled fix. `values` is now `unknown[]`: the\n * comparand the author wrote travels through the tree untouched, and no\n * stringification happens at all except where a boundary genuinely demands it:\n *\n * - {@link toSqlBindValue} — the ONLY survivor, and it is one-way (a value →\n * its SQL bind form), never a decoder. It exists because a SQL driver cannot\n * bind every JS type: better-sqlite3 refuses a `boolean`, a `Date` and a\n * plain object. Nothing about it inspects a string.\n * - the LIKE family, whose comparand `filter.zod.ts` declares a `string`\n * (`$contains: z.string()`), so `like-pattern.ts` stringifies at the emitter\n * — the same `String(value)` `driver-sql`'s `applyLike` applies, which is\n * what keeps one `$contains` meaning one thing on both faces.\n *\n * The ObjectQL path needs NO conversion at all now: the engine compares against\n * the stored runtime type, and the value it receives is the author's own.\n *\n * Two shapes changed reading as a consequence, both toward fail-closed and both\n * pinned in `filter-value-type-fidelity.test.ts`:\n *\n * - `{name: {$contains: null}}` compiled to `LIKE '%%'` — matching EVERY\n * non-NULL row — because `stringifyForCube(null)` was `''`. It is now\n * `LIKE '%null%'`, which is what `driver-sql` has always compiled it to.\n * - `{amount: {$gt: null}}` compiled to `amount > ''`, a real comparison\n * against the empty string. It now binds NULL, so the predicate is UNKNOWN\n * and the widget draws nothing — the honest answer for an unordered\n * comparand, and the one `driver-memory` / `formula` give. (#5332 named this\n * comparand position as covered by no ruling and left the `''` placeholder\n * alone; deleting the encoder decides it by construction.)\n *\n * # A `where` ARRAY is lowered here, not dropped (#5334)\n *\n * `FilterArray` — `['stage', '=', 'won']`, `['and', […], […]]`, `[[…], […]]` —\n * is INPUT-ONLY authoring sugar (`spec/data/filter.zod.ts`, #5285), and #5158's\n * ruling C says every door into the runtime LOWERS it through the one\n * `parseFilterAST` sink before anything downstream sees a filter. #5329 closed\n * the engine's six entry points that way and deleted the four drivers' array\n * dialects. Analytics is the FIFTH door: it compiles `where` itself — to SQL\n * (`NativeSQLStrategy`) or to a `FilterCondition` for the engine\n * (`ObjectQLStrategy`) — so nothing upstream lowers for it.\n *\n * Until #5334 this function answered an array with `return null`: the WHOLE\n * `where` disappeared, no error, no trace, and the widget charted the entire\n * dataset — the #3650 / #4128 silent-widening class again, reached through the\n * array spelling. {@link normalizeAnalyticsFilterTree} now gives the same three\n * answers the engine door gives, so one query means one thing on every path.\n *\n * # Every refusal here is a 400, and SAYS so (#5352)\n *\n * All of the above only helps the author if the refusal REACHES them. Each\n * refusal in this module is a caller-shaped mistake — a misspelled operator, a\n * `$between` with one bound, a `{}` where an operator belongs — and ADR-0112's\n * rule is that such an error carries its own machine-readable semantics\n * (`code` + `status`) rather than leaving each consumer to guess from the\n * message text. Until #5352 only the #5334 array refusals did; the other seven\n * were bare `throw new Error(…)`, so `/analytics/dataset/query` had nothing to\n * read and answered `500 ANALYTICS_QUERY_FAILED` — \"the platform is broken\" for\n * what is a typo in a widget's filter, counted as a 5xx by ops alerting. The\n * same mistake on `find()` has answered `400 INVALID_FILTER` since #3948.\n *\n * So {@link invalidFilterError} is now the ONLY way this module refuses, and\n * `rest-server.ts`'s analytics catch reads that envelope before anything else.\n * #5352 changed the SHAPE of these errors and nothing about WHICH inputs are\n * refused — the refusal set is pinned input-by-input in\n * `filter-refusal-envelope.test.ts` precisely so that stays true.\n *\n * # An `undefined` COMPARAND is refused, not read seven different ways (#6386)\n *\n * #6050 ruled on 2026-08-07 (ruling B) that `undefined` sitting where a\n * comparand belongs is REFUSED, and landed that on `driver-sql` / `driver-turso`.\n * #6125 pushed it to this package's OTHER door, `read-scope-sql.ts` (PR #6390).\n * This door — the `where` the CALLER writes — had never been named by any of\n * those rulings, and it read the one shape seven ways. Measured on `origin/main`\n * (`5faa23ca3`) by calling `normalizeAnalyticsFilterTree({ where })` directly:\n *\n * | `where` | normalized to | reading |\n * |---|---|---|\n * | `{d: undefined}` | `null` | the WHOLE where dropped — the query ran with NO filter |\n * | `{stage:'won', d: undefined}` | `stage equals 'won'` | the `d` conjunct vanished in silence |\n * | `{$not: {d: undefined}}` | `NOT (d set)`, i.e. `d IS NULL` | a predicate the author never wrote |\n * | `{d: {$eq: undefined}}` | `d equals [null]` | a value comparison, NOT `$eq: null`'s `notSet` |\n * | `{d: {$gt: undefined}}` | `d gt [null]` | ditto |\n * | `{d: {$in: [undefined]}}` | `d in [null]` | ditto |\n * | `{d: {$ne: undefined}}` | `d notSet OR d notEquals [null]` | ditto |\n *\n * The first three are the whole argument. They WIDEN — which is the failure mode\n * the note at {@link MONGO_TO_CUBE_OP}'s miss branch has forbidden in this very\n * function since #4128 (\"NEVER drop: a missing predicate does not narrow the\n * query, it WIDENS it … That failure mode is #3650's\"). `buildNode`'s first line\n * was `if (raw === undefined) continue;`: the module did, at its entry, the exact\n * thing its own body refuses to do a few dozen lines further down.\n *\n * Row three is the strangest and is worth stating separately, because it is not\n * \"one conjunct fewer\": the #5146 rewrite splits the leaf into `{d: {$null:\n * false}} AND {d: undefined}`, the entry gate dropped the second half, and the\n * surviving guard was then negated — so a predicate GREW OUT of a discarded leaf.\n *\n * ⚠️ The direction is silently WRONG RESULTS, not a permission bypass. Read\n * scope is compiled by the other door (`read-scope-sql.ts` → `applyReadScope`)\n * and never passes through here, so a caller still saw only rows it was entitled\n * to — just more of them than it asked for. What was lost is the ANSWER: an\n * analytics figure, a report total, an aggregate, wrong with nothing to read.\n *\n * The refusal is {@link undefinedComparandError}, in this module's existing\n * envelope (`INVALID_FILTER` / 400) — the opposite attribution from\n * `read-scope-sql`'s 500, and deliberately so: that door compiles a platform\n * artifact, this one receives what the CALLER wrote.\n *\n * ⛔ `null` does not move, and that is the way this change could do harm: the two\n * live one `===` apart in every polarity table here. `{d: null}`, `{$eq: null}`,\n * `{$ne: null}`, `{$null: …}`, `{$exists: …}` and `$contains: null`'s `%null%`\n * (#5526) keep their exact lowering, pinned as a control group in\n * `filter-normalizer-undefined-comparand.test.ts`.\n *\n * # A field wrapper cannot MIX $-operators with non-$ members (#6444)\n *\n * The value-independent sibling of the #6386 defect, in the same function, ruled\n * Option A (refuse) by the maintainer on 2026-08-08. A field constraint object\n * that carries `$`-operator keys AND non-`$` keys at once used to compile its\n * operators and silently DROP every non-`$` sibling — `fieldLeaves`'s\n * `if (opKeys.length > 0) { …; return out; }` arm never looked at them, and the\n * nested-relation flatten sits after that early return. Measured on\n * `origin/main` (`1a53a0253`):\n *\n * | `where` | normalized to | reading |\n * |---|---|---|\n * | `{d: {$eq: 1, nested: 'x'}}` | `d equals [1]` | the `nested` conjunct vanished in silence |\n * | `{amount: {gte: 10, $lte: 20}}` | `amount lte 20` | the missing-`$` typo: the LOWER BOUND silently gone |\n * | `{$not: {d: {$null: true, nested: 'x'}}}` | `NOT(d set AND d notSet)` | a contradiction that negates to TRUE — EVERY row |\n *\n * Row two is the likeliest producer — a dropped `$` is a canonical agent typo —\n * and row three is the strangest: the #5146 guard was computed while the sibling\n * still existed (`requireValue`), the sibling then vanished inside\n * `fieldLeaves`, and the surviving conjunction was contradictory, so the\n * negation widened to the whole dataset. All three WIDEN — the #3650 failure\n * mode the note at {@link MONGO_TO_CUBE_OP}'s miss branch forbids in this very\n * function.\n *\n * The refusal is {@link mixedFieldWrapperError} via\n * {@link assertUnmixedFieldWrapper}, in this module's one envelope\n * (`INVALID_FILTER` / 400, #5352). The message must do one thing more than the\n * module's other refusals: the shape has TWO legitimate repairs answering two\n * intents this module cannot tell apart — an operator missing its `$`\n * (`gte` → `$gte`) and a nested-relation member that needs a wrapper of its own\n * — so the message names the offending key(s) and shows BOTH rewrites (ruling\n * requirement, #6444).\n *\n * Option B — flattening the non-`$` siblings as nested paths next to the\n * operators — was REJECTED by the same ruling: it would compile the likely-real\n * cause (a dropped `$`) into a predicate on a non-existent member `amount.gte`,\n * turning a diagnosable mistake into a harder one.\n *\n * ⛔ What does not move: a wrapper that is ALL non-`$` keys keeps flattening to\n * the dotted member (`{d: {nested: 'x'}}` → `d.nested`); a wrapper that is ALL\n * `$`-operators compiles exactly as before; `$null` / `$exists` flag semantics\n * (#5526 / #5332 / #5347) and {@link comparand} are untouched. The sibling door\n * `read-scope-sql.ts` already fails closed on this exact shape\n * (`compileField`'s non-`$`-key check) and is not touched — this change makes\n * the two doors give one answer.\n *\n * Row-result cover: `filter-operator-coverage.test.ts` for the operator\n * vocabulary, `native-sql-filter-logic-conformance.test.ts`, which runs the\n * SHARED combinator table (`FILTER_LOGIC_CASES`, #3774) that the SQL compiler,\n * the in-memory matcher, `formula` and `read-scope-sql` are already held to,\n * `filter-normalizer-not-null-safe.test.ts` for the two squares that table\n * deliberately does not carry (NULL handling, boolean identities),\n * `filter-array-lowering.test.ts` for the array door (#5334),\n * `filter-value-type-fidelity.test.ts` for what each comparand TYPE binds on both\n * consumers (#5526, carrying #5528's cases forward as end-to-end assertions),\n * `filter-normalizer-undefined-comparand.test.ts` for the `undefined` refusal and\n * its `null` control group (#6386), and\n * `filter-normalizer-mixed-wrapper.test.ts` for the mixed `$`/non-`$` wrapper\n * refusal and its pure-shape control groups (#6444).\n */\n\nimport { isFilterAST, parseFilterAST, VALID_AST_OPERATORS } from '@objectstack/spec/data';\nimport { StandardErrorCode } from '@objectstack/spec/api';\nimport {\n CROSS_FIELD_COMPARISON_OPERATORS,\n fieldReferenceBetweenBoundMessage,\n isBindableComparand,\n isFieldReference,\n isRenderableTextComparand,\n TEXT_PATTERN_OPERATORS,\n unbindableListMemberMessage,\n unrenderableTextComparandMessage,\n} from '../comparand-shape.js';\n\nexport interface NormalizedAnalyticsFilter {\n member: string;\n operator: string;\n /** The author's comparands, at their own types — see {@link NormalizedFilterNode}. */\n values: unknown[];\n}\n\n// ── [#5334 / #5352] The refusal envelope ─────────────────────────────────────\n\n/**\n * [#5334, generalised by #5352] A filter refusal in the ADR-0112 envelope every\n * sibling filter refusal in the repo speaks — `INVALID_FILTER` / 400.\n *\n * The twin of `driver-sql`'s and `driver-memory`'s `unsupportedFilterError`.\n * A caller that writes a filter this module cannot compile has made a\n * 400-class mistake, and a coded refusal is what lets the `/analytics` face\n * answer it as one instead of as an opaque 500.\n *\n * ⛔ **The only way this module refuses.** #5334 introduced it for the two\n * array-door refusals while the other seven sites stayed bare `Error`s, and a\n * half-enveloped module is indistinguishable from an unenveloped one at the\n * REST boundary: `error.code` was `undefined` for the operator typo that is by\n * far the commonest of the nine, so the whole family landed as\n * `500 ANALYTICS_QUERY_FAILED` (#5352). A new refusal added to this file must\n * be thrown through here; a bare `throw new Error` is the defect returning.\n *\n * It carries no `#5352`-specific wording on purpose — the envelope is the\n * contract, the message stays whatever the refusing site says.\n *\n * [#7598] EXPORTED, and the \"only way this module refuses\" invariant is\n * unchanged by it: that rule is about this file's own sites, and the export\n * exists so a sibling in this directory cannot invent a SECOND spelling of the\n * same envelope. `ObjectQLStrategy.generateSql` refuses a cross-field\n * comparison it cannot honestly render, and that refusal is a `where`-door\n * refusal in every respect that matters — the caller authored the input, the\n * repair is theirs — so it takes the `where` door's envelope rather than a\n * hand-rolled twin. (`read-scope-sql.ts` keeps its OWN local error factory\n * because its envelope genuinely differs: a read scope is not caller-authored,\n * hence `READ_SCOPE_COMPILE_FAILED` / 500 by the #5367 ruling.)\n */\nexport function invalidFilterError(message: string): Error {\n const err = new Error(message) as Error & { code?: string; status?: number };\n err.code = StandardErrorCode.enum.INVALID_FILTER;\n err.status = 400;\n return err;\n}\n\n/**\n * The value-INDEPENDENT operators: the pipeline name depends only on the key.\n *\n * `$null` and `$exists` are deliberately absent — their meaning flips with\n * their boolean value, which a key→name map cannot express. Putting `$exists`\n * here anyway is what made `{$exists: false}` compile to `IS NOT NULL`, the\n * exact inverse of what it asks for; both are handled explicitly below.\n */\nconst MONGO_TO_CUBE_OP: Record<string, string> = {\n $eq: 'equals',\n $ne: 'notEquals',\n $gt: 'gt',\n $gte: 'gte',\n $lt: 'lt',\n $lte: 'lte',\n $in: 'in',\n $nin: 'notIn',\n $contains: 'contains',\n $notContains: 'notContains',\n $startsWith: 'startsWith',\n $endsWith: 'endsWith',\n // [#6520] The case-INSENSITIVE twin, ASCII fold only. A separate cube operator\n // rather than a flag on `contains`, because the two compile to different SQL\n // and one name would make the renderers guess which was meant.\n $icontains: 'icontains',\n};\n\n/**\n * The comparand a leaf carries: the author's value, at the author's type.\n *\n * [#5526] This function is what used to be `stringifyForCube`, and the whole of\n * its former body is gone: `values` is `unknown[]`, so a comparand needs no\n * encoding and there is nothing for a decoder downstream to guess at. What\n * remains is one normalisation, and it is not a type conversion:\n *\n * `undefined` becomes `null`. JSON has no `undefined`, so no authored\n * `FilterCondition` can carry one — `{$eq: undefined}` is a key the author did\n * not mean to write (#5332's reading, unchanged here) — while a `values` entry\n * that IS `undefined` is a bind error on better-sqlite3 rather than a predicate.\n * `null` is the fail-closed reading: the comparison is UNKNOWN, so the widget\n * draws nothing instead of drawing rows chosen by an accident.\n *\n * Note what this does NOT do: `{$eq: undefined}` still compiles to an `equals`\n * leaf, not to `notSet`. Only `=== null` is the null PREDICATE (#5332's identity\n * test, which this module reads at the operator branch, above this function),\n * and widening it to `== null` here would re-decide that ruling sideways.\n *\n * ## Addendum (#6386): the `undefined` arm is now UNREACHABLE from this door\n *\n * {@link assertDefinedComparands} refuses an `undefined` before any comparand is\n * read, and it covers every call site of this function — the `$between` bounds,\n * the operator value and its array members, the bare-array `$in` and the implicit\n * `=` — so nothing can arrive here holding `undefined` any more. The refusal\n * tests enumerate exactly that set of positions, which is what makes the claim\n * checkable rather than asserted.\n *\n * ⛔ It is left in place ON PURPOSE, code untouched. Both statements this\n * function makes were RULED — `undefined` → `null` by #5526, and \"only `=== null`\n * is the null predicate\" by #5332 — and #6386 refuses an INPUT without reopening\n * either. Deleting a now-dead arm would be a semantic edit smuggled in as a\n * cleanup: it is #5526's call whether the normalisation still earns its place\n * once its last caller is gated, and that is a separate decision from this one.\n */\nfunction comparand(v: unknown): unknown {\n return v === undefined ? null : v;\n}\n\n/**\n * One node of the normalized filter TREE.\n *\n * A tree rather than the flat array this module used to produce, because a flat\n * array cannot express `$or` — and what it did with one was DROP it, which does\n * not narrow a query, it widens it to rows the author excluded (#3650's\n * symptom). The structure is the minimum that survives that: leaves carry the\n * pipeline's `{member, operator, values}` triple unchanged, and the combinators\n * are explicit so each strategy can compile them the way its own backend\n * expresses them — recursive SQL for the raw-SQL path, a passed-through\n * `$or`/`$not` for the engine path.\n *\n * The `const` kind is the boolean constant (#5325). The union carried only\n * `leaf | and | or | not`, so there was no way to SAY \"matches nothing\": a\n * `{$not: {}}` — whose meaning is exactly that — could only be expressed by\n * emitting nothing, which every compiler reads as \"no constraint\", i.e. the\n * opposite. TRUE keeps its existing spelling (`null` = no constraint, the AND\n * identity); FALSE needs a node because it must survive into the WHERE clause.\n *\n * [#5526] A leaf's `values` is `unknown[]`, not `string[]`. The author's\n * comparand travels at its own type: a number stays a number, a boolean a\n * boolean, and — the defect this fixed — a STRING stays the string the author\n * typed, so `'007'` is never the integer `7` and `'null'` is never real NULL.\n * The compilers of this tree convert only where their own boundary forces it\n * ({@link toSqlBindValue} for a SQL parameter, `like-pattern.ts` for the LIKE\n * family, whose comparand the spec declares a `string`); the ObjectQL engine path\n * converts nothing, because the engine compares against the stored runtime type\n * and the value it is handed is the author's own. See the module header.\n */\nexport type NormalizedFilterNode =\n | { kind: 'leaf'; member: string; operator: string; values: unknown[] }\n | { kind: 'const'; value: boolean }\n | { kind: 'and'; children: NormalizedFilterNode[] }\n | { kind: 'or'; children: NormalizedFilterNode[] }\n | { kind: 'not'; child: NormalizedFilterNode };\n\n/**\n * The SQL boolean constants the compilers of this tree emit for a `const` node.\n *\n * `1 = 0` / `1 = 1` are the spellings already used on both sides of the repo —\n * `read-scope-sql.ts` compiles an empty `$in` to `1 = 0`, `driver-sql`'s\n * `applyFalseConstant` emits the same (#5134), and Knex renders an empty\n * `whereIn` that way. They need no bindings, are valid on every dialect these\n * strategies target (unlike a bare `FALSE`), and keep the statement a normal\n * SELECT so `GROUP BY` / `LIMIT` still behave.\n */\nexport const SQL_CONST_FALSE = '1 = 0';\nexport const SQL_CONST_TRUE = '1 = 1';\n\n/** The tree's FALSE. A fresh object per call — nodes are never shared. */\nfunction falseNode(): NormalizedFilterNode {\n return { kind: 'const', value: false };\n}\n\n/**\n * `NOT` of a node, with `null` read as the constant TRUE it is.\n *\n * `NOT TRUE ≡ FALSE` is the whole point: `{$not: {}}` used to fall off the tree\n * here, taking the WHERE clause with it (#5325). A `NOT` of a constant folds to\n * the opposite constant, so `{$not: {$not: {}}}` is TRUE again rather than a\n * `NOT (1 = 0)` that only happens to evaluate right.\n */\nfunction notOf(inner: NormalizedFilterNode | null): NormalizedFilterNode {\n if (!inner) return falseNode();\n if (inner.kind === 'const') return { kind: 'const', value: !inner.value };\n return { kind: 'not', child: inner };\n}\n\n/** A node the normalizer can walk: a plain object, not `null` and not an array. */\nfunction isFilterObject(v: unknown): v is Record<string, unknown> {\n return v !== null && typeof v === 'object' && !Array.isArray(v) && !(v instanceof Date);\n}\n\n/** `null` means \"no constraint\" — an empty object contributes no predicate. */\nfunction andOf(children: NormalizedFilterNode[]): NormalizedFilterNode | null {\n if (children.length === 0) return null;\n if (children.length === 1) return children[0];\n return { kind: 'and', children };\n}\n\n/**\n * [#5234] The comparand-SHAPE gate for the analytics `where` door.\n *\n * This runs before a leaf exists, which is the whole reason it is here rather\n * than at the three emitters. {@link fieldLeaves} is the ONLY producer of leaf\n * nodes in this package, so one refusal here covers all three consumers of the\n * tree at once — `NativeSQLStrategy.buildFilterClause` (the statement that\n * executes), `ObjectQLStrategy.buildFilterClauseSql` (the `/analytics/sql` echo)\n * and `ObjectQLStrategy.convertFilter` (the engine path). Guarding the emitters\n * instead would have been three guards, three envelopes, and one of them —\n * `convertFilter`'s `String(v0)` — would still have LAUNDERED the object into\n * `'[object Object]'` before any driver could refuse it, so a strict driver\n * downstream could never see the shape it was strict about.\n *\n * Prime Directive #12, applied literally: refuse at the door, do not tolerate at\n * the consumer. `read-scope-sql.ts` is this package's OTHER door — it compiles a\n * `FilterCondition` that never passes through here — and carries the same two\n * checks in its own fail-closed envelope.\n *\n * Two shapes are refused, the two #5234 measured. `$eq` and friends keep\n * binding any OTHER object as JSON (`toSqlBindValue`), which remains a separate\n * account.\n *\n * ⚠️ [#7598, maintainer ruling 2026-08-12 Q1 = B] A THIRD arm briefly lived\n * here — a `{$field}` reference in the comparand of the six scalar comparison\n * operators, added by #7694 as the shipped interim while the routing question\n * was with the maintainer. It is GONE, and its removal is the point of the\n * ruling rather than a cleanup: this function runs inside {@link fieldLeaves},\n * which is the one producer of leaf nodes for ALL THREE consumers of this tree\n * — including `ObjectQLStrategy.convertFilter`, the ENGINE path. Refusing here\n * therefore refused the very execution B routes such a query to, so the arm and\n * the ruling cannot both stand. `NativeSQLStrategy.canHandle` now declines\n * instead (see the ruling recorded there), and `driver-sql` compiles the\n * comparison under the four #5222 rulings with the metadata it owns.\n *\n * What did NOT move is the `$between` arm — see\n * {@link assertNoFieldReferenceComparand}, which is now that arm alone.\n */\nfunction assertCompilableComparand(opKey: string, field: string, value: unknown): void {\n if (TEXT_PATTERN_OPERATORS.has(opKey)) {\n // An array reaches this door as `values[0]` — i.e. every member after the\n // first is silently DROPPED — while `read-scope-sql` and `driver-sql`\n // stringify the whole array. That split is why an array is refused and not\n // merely stringified consistently.\n if (!isRenderableTextComparand(value)) {\n throw invalidFilterError(`[analytics] ${unrenderableTextComparandMessage(opKey, field, value)}`);\n }\n return;\n }\n if ((opKey === '$in' || opKey === '$nin') && Array.isArray(value)) {\n value.forEach((member, index) => {\n if (!isBindableComparand(member)) {\n throw invalidFilterError(`[analytics] ${unbindableListMemberMessage(opKey, field, member, index)}`);\n }\n });\n }\n}\n\n/**\n * [#7598] A `{ $field: 'col' }` reference in a `$between` ENDPOINT — the one\n * position on this door where the gate is still load-bearing after the\n * 2026-08-12 ruling, and the one place its removal would silently CREATE a\n * capability rather than remove a refusal.\n *\n * ## Why this arm survived when the scalar-comparand arm did not\n *\n * The ruling (Q1 = B) moved the six scalar comparison operators OUT of this\n * door's business entirely: `NativeSQLStrategy.canHandle` declines a `where`\n * carrying one, the query routes to the engine, and `driver-sql` compiles the\n * comparison under the four #5222 rulings. Refusing them here would refuse the\n * execution the routing exists to reach, so that arm is gone.\n *\n * `$between` is the opposite case, because of a LOWERING this door performs and\n * the driver never sees. {@link fieldLeaves}'s `$between` branch splits\n * `{ $between: [a, b] }` into a `gte` leaf and an `lte` leaf, and\n * `ObjectQLStrategy.convertFilter` hands those to the engine as `{ $gte: a }` /\n * `{ $lte: b }`. So a reference in an endpoint would arrive at `driver-sql`\n * wearing a `$gte` it was never authored with — and `$gte` is a position #5222\n * COMPILES. The result would be that `{ amount: { $between: [{ $field:\n * 'budget' }, 100] } }` quietly SUCCEEDS on the analytics face while\n * `CROSS_FIELD_REFUSALS` pins it as refused on both SQL drivers, and while\n * #7596 has removed the position from `FieldReferenceSchema` altogether\n * (maintainer ruling 2026-08-11, ADR-0049 declared = enforced). One shape, two\n * answers, created by a laundering this module does on the way past — exactly\n * the class #7598 was filed about, spelled backwards.\n *\n * Refusing here therefore CONVERGES with `driver-sql`'s own #5222 refusal arm,\n * which is what every surviving `{$field}` refusal in this package now does:\n * the LIKE family through {@link assertCompilableComparand}'s\n * `isRenderableTextComparand` call, `$in` / `$nin` members through its\n * `isBindableComparand` one, a bare `{ field: { $field: … } }` as an unsupported\n * operator, and this. The scalar comparands are the only positions where the\n * two faces now differ, and they differ by the analytics face DECLINING to\n * serve them itself rather than by refusing them.\n *\n * Asserted under the `$between` name, not under the `gte` / `lte` the bounds\n * lower to, because the author wrote `$between` and that is the key they have\n * to repair.\n */\nfunction assertNoFieldReferenceComparand(opKey: string, field: string, value: unknown): void {\n if (opKey !== '$between' || !Array.isArray(value)) return;\n value.forEach((member, index) => {\n if (!isFieldReference(member)) return;\n throw invalidFilterError(\n `[analytics] ${fieldReferenceBetweenBoundMessage(opKey, field, member.$field, index)}`,\n );\n });\n}\n\n/**\n * [#6386, on #6050's ruling B] `undefined` in a COMPARAND position.\n *\n * ONE wording for every position (#5240 — one condition, one wording); only\n * `path` varies, because only the position does. The wording names BOTH measured\n * consequences rather than one, which is where this differs from\n * `read-scope-sql`'s twin: there all four cells failed the same way (a silent\n * NULL bind), here the same input is read two different ways depending on where\n * it sits — the key is DROPPED in the three positions `buildNode` used to skip,\n * and compiled as a comparison against `null` in the four {@link comparand}\n * normalises. An author who hits either one needs to be told which of their keys\n * is unreadable, and both halves of what it would otherwise have done.\n *\n * ## Why 400 here and 500 on the sibling door\n *\n * `read-scope-sql.ts` refuses the same shape as `READ_SCOPE_COMPILE_FAILED` /\n * 500 because a read scope is compiled by the PLATFORM from CEL and stored\n * policy — billing the caller for it would be wrong. This door is the mirror\n * image: `where` is what the caller itself passed to `AnalyticsService.query`,\n * so it is a 400-class mistake and takes the envelope every other refusal in\n * this module already carries (#5352).\n *\n * ## Why the producer named is the CALLER, and what shape to look for\n *\n * `undefined` cannot cross JSON, so neither REST door can carry it: it can only\n * come from in-process code building the object — `{ owner_id: ctx.user?.id }`,\n * the shape #6050 proved reachable on `driver-sql`. Note the platform's OWN\n * answer to that need is already fail-closed and is not this shape: a\n * `{current_user_id}` placeholder in a dataset / widget / report filter is\n * resolved by `resolveFilterTokens` (`@objectstack/core`), which throws\n * `FILTER_TOKEN_UNRESOLVED` / 400 rather than emitting `undefined`.\n */\nfunction undefinedComparandError(field: string, path: string): Error {\n return invalidFilterError(\n `[analytics] comparand at ${path} is undefined — refusing to compile this filter. ` +\n `@objectstack/spec FieldOperatorsSchema declares no undefined comparand, and in JavaScript a key ` +\n `whose value is undefined cannot be told apart from an ABSENT key — yet the two mean OPPOSITE ` +\n `things (a predicate versus no constraint at all), so there is no reading of it that is not a ` +\n `guess. It used to compile, two ways: in a FIELD position the key was dropped outright, so a ` +\n `single-key where ran with no filter at all and the chart was drawn over every row (#3650's ` +\n `widening, which this module refuses everywhere else); in an OPERATOR or list position it ` +\n `became a comparison against null, which is UNKNOWN for every row and charts nothing. ` +\n `Write null if the null predicate was meant ({ \"${field}\": null } or { \"${field}\": { \"$null\": true } }), ` +\n `or omit the key entirely when the value is genuinely absent — an omitted key is the same \"no ` +\n `constraint\" without the ambiguity. The producer to fix is whoever BUILT this where: undefined ` +\n `cannot cross JSON, so it is in-process code spreading a possibly-absent value into a filter ` +\n `object (#6050 ruling B, pushed down to this door by #6386).`,\n );\n}\n\n/**\n * [#6386] Refuse every `undefined` sitting in a comparand position of ONE field\n * constraint.\n *\n * The positions are enumerated rather than swept, because \"comparand\" is a\n * POSITION and not a type:\n *\n * - the DIRECT comparand — `{d: undefined}`, the implicit `=`. Reached for a\n * nested relation too, because {@link fieldLeaves} recurses into one with the\n * DOTTED member name, so `{profile: {verified: undefined}}` is refused as\n * `\"profile.verified\"` — the member the leaf would have carried, not the\n * relation. (`read-scope-sql`'s twin has no such case: it refuses nested\n * relations outright.)\n * - a MEMBER of the bare-array implicit `$in` — `{d: [1, undefined]}`. The\n * array itself is a legitimate comparand here, so its elements are comparands\n * in their own right. This is the deliberate divergence from that twin, which\n * refuses a bare array as a whole and so must not relabel it.\n * - an OPERATOR's comparand — `{d: {$gt: undefined}}`, `$eq`, `$ne`, the LIKE\n * family, every other single-value operator;\n * - a MEMBER of a list operator's array — `{d: {$in: [undefined]}}`, `$nin`,\n * and `$between`'s two bounds.\n *\n * `$null` / `$exists` are deliberately NOT swept, exactly as on the twin: their\n * comparand is a declared BOOLEAN — a flag, not a value to compare against — so\n * `undefined` there is not a comparand at all. ⚠️ This module reads that flag by\n * IDENTITY (`=== true` / `=== false`, see {@link fieldLeaves}) where the twin\n * reads it by truthiness, so `{$null: undefined}` lowers here to `set`\n * (`IS NOT NULL`). That is the boolean-DOMAIN question #5347 / #5369 opened and\n * #6387 is measuring on the sibling door; it is a different cell and is not\n * decided as a rider on this one.\n *\n * ## Why the gate sits HERE, and what that decides for `{$not: {d: undefined}}`\n *\n * {@link fieldLeaves} is the only producer of leaf nodes in this module, so one\n * gate covers all three consumers of the tree at once — the same argument\n * {@link assertCompilableComparand} makes one function below.\n *\n * That places it DOWNSTREAM of {@link nullSafeNegationOperand}, and for row three\n * of the header's table that choice is the whole question: a gate on the far side\n * of the #5146 rewrite refuses, while a rewrite that could swallow the leaf first\n * would leave a CHANGED SHAPE for the gate to bless. Measured rather than\n * assumed, because the same trap cost PR #6390 a lap on the sibling door — and\n * the reasoning there does NOT transfer, since the two modules' polarity tables\n * are spelled differently (that one is uniformly `=== null`; this one mixes\n * `=== null` for `$eq`/`$ne` with IDENTITY reads for `$null`/`$exists`). What the\n * measurement shows here is that the rewrite never drops a leaf: every guard\n * disposition — `requireValue` pushes `{k: {$null: false}}, {k: spec}`,\n * `allowNull` pushes `{$or: [{k: {$null: true}}, {k: spec}]}`, `none` writes\n * `out[k] = spec` — carries `spec` through by reference, so the author's\n * `undefined` always reaches this gate and always throws. Pinned in\n * `filter-normalizer-undefined-comparand.test.ts` as its own block: one case per\n * rewrite path that can carry a SWEPT comparand (`requireValue`, `allowNull`, and\n * the nested-relation recursion), plus the measured reason there is no third —\n * `none` needs every operator to satisfy {@link operatorIsNullTotal}, which is\n * false for an `undefined` comparand on every operator this gate sweeps, so the\n * only field specs that reach it holding one are the `$null` / `$exists` flags it\n * deliberately does not sweep.\n */\nfunction assertDefinedComparands(field: string, spec: unknown): void {\n const root = `\"${field}\"`;\n if (spec === undefined) throw undefinedComparandError(field, root);\n if (Array.isArray(spec)) {\n spec.forEach((member, index) => {\n if (member === undefined) throw undefinedComparandError(field, `${root}[${index}]`);\n });\n return;\n }\n if (!isFilterObject(spec)) return;\n for (const [op, opValue] of Object.entries(spec)) {\n if (!op.startsWith('$') || op === '$null' || op === '$exists') continue;\n const opPath = `${root}.${op}`;\n if (opValue === undefined) throw undefinedComparandError(field, opPath);\n if (!Array.isArray(opValue)) continue;\n opValue.forEach((member, index) => {\n if (member === undefined) throw undefinedComparandError(field, `${opPath}[${index}]`);\n });\n }\n}\n\n/**\n * [#6444, ruled Option A on 2026-08-08] A field wrapper mixing `$`-operator\n * keys with non-`$` sibling keys.\n *\n * ONE wording whatever the mix (#5240 — one condition, one wording); only the\n * field and the two key lists vary, because only those do. Where this message\n * has to do MORE than the module's other refusals: the shape has two\n * legitimate repairs answering two different intents, and the module cannot\n * tell which one the author held — that inability is exactly why the shape is\n * refused rather than read. So the message must present BOTH (ruling\n * requirement):\n *\n * - an OPERATOR missing its `$` — the canonical agent typo\n * `{amount: {gte: 10, $lte: 20}}` — repaired by the prefixed spelling\n * (`\"gte\" → \"$gte\"`);\n * - a NESTED-RELATION member that strayed into an operator wrapper —\n * repaired by giving it a wrapper of its own (`{ \"d\": { \"nested\": … } }`\n * compiles to the member `d.nested`), ANDed with the operator constraint\n * explicitly, since one JSON object cannot spell the same field key twice.\n *\n * Option B — flattening the sibling as a nested path beside the operators —\n * was rejected by the same ruling: it would compile the missing-`$` typo into\n * a predicate on a non-existent member `amount.gte`, converting a diagnosable\n * mistake into a harder one.\n */\nfunction mixedFieldWrapperError(field: string, opKeys: string[], nonOpKeys: string[]): Error {\n const offending = nonOpKeys.map((k) => `\"${k}\"`).join(', ');\n const rewrites = nonOpKeys.map((k) => `\"${k}\" → \"$${k}\"`).join(', ');\n const example = nonOpKeys[0];\n return invalidFilterError(\n `[analytics] \"${field}\" mixes $-operator keys (${opKeys.join(', ')}) with non-$ sibling key(s) ` +\n `${offending} in ONE field constraint — refusing to compile this filter. A $-prefixed key is an ` +\n `OPERATOR and a bare key is a NESTED-RELATION member; the two readings of ${offending} lead to ` +\n `different predicates and this module cannot tell which was meant, so any silent choice is a ` +\n `guess. If an operator missing its \"$\" was meant — the usual authoring slip — spell it with the ` +\n `prefix: ${rewrites}, as in { \"${field}\": { \"$${example}\": ... } }. If a nested-relation member ` +\n `was meant, give it a wrapper of its OWN with no $ siblings — { \"${field}\": { \"${example}\": ... } } ` +\n `compiles to the member \"${field}.${example}\" — and AND it with the operator constraint ` +\n `explicitly: { \"$and\": [{ \"${field}\": { \"$op\": ... } }, { \"${field}\": { \"${example}\": ... } }] }. ` +\n `This shape used to compile by silently DROPPING every non-$ sibling, and a dropped conjunct ` +\n `does not narrow the query, it WIDENS it: the chart included rows the author excluded, with ` +\n `nothing to read (#3650's failure mode, which this module refuses everywhere else). The sibling ` +\n `door in this package (read-scope-sql.ts) already fails closed on this exact shape — one shape, ` +\n `one answer (#6444).`,\n );\n}\n\n/**\n * [#6444] Refuse ONE field wrapper that mixes `$`-operator keys with non-`$`\n * sibling keys — the value-independent sibling of {@link assertDefinedComparands}.\n *\n * What made the mix silent: {@link fieldLeaves}'s operator arm iterates\n * `opKeys` only and returns, and the nested-relation flatten sits after that\n * early return — so with even one `$` key present, every non-`$` sibling was\n * simply never visited. Dropping a conjunct WIDENS (#3650), and inside a `$not`\n * it did worse than widen by one conjunct: {@link nullGuardForFieldSpec} judged\n * the wrapper while the sibling still existed (a non-`$` key never satisfies\n * {@link operatorIsNullTotal}, so the disposition was `requireValue` or\n * `allowNull`, never `none`), the sibling then vanished here, and for a\n * null-predicate operator the surviving guard was CONTRADICTORY —\n * `{$not: {d: {$null: true, nested: 'x'}}}` compiled to `NOT(d set AND d\n * notSet)`, which is TRUE for every row. That same never-`none` fact is what\n * guarantees the #5146 rewrite carries a mixed wrapper to this gate by\n * reference instead of swallowing it — pinned in\n * `filter-normalizer-mixed-wrapper.test.ts`'s rewrite block.\n *\n * ## Ordering against the neighbouring gates\n *\n * Runs in {@link fieldLeaves}'s wrapper arm, after the #5240 zero-operator\n * refusal (disjoint by construction: `{}` has no keys of either kind) and\n * after {@link assertDefinedComparands} at the function's entry — so\n * `{d: {$eq: undefined, nested: 'x'}}` is refused as an undefined comparand,\n * not as a mix. Both are refusals in the same envelope, so the REST face\n * answers 400 either way; the ordering is pinned as a measured fact, not a\n * contract.\n *\n * ## What is deliberately not judged here\n *\n * A wrapper that is ALL `$`-operators or ALL non-`$` members passes untouched —\n * this gate moves the refusal set by exactly the mixed shape. Whether a non-`$`\n * KEY is a real member of the modeled object is the schema's question at a\n * different layer, not this compiler's.\n */\nfunction assertUnmixedFieldWrapper(field: string, wrapper: Record<string, unknown>): void {\n const keys = Object.keys(wrapper);\n const opKeys = keys.filter((k) => k.startsWith('$'));\n if (opKeys.length === 0) return;\n const nonOpKeys = keys.filter((k) => !k.startsWith('$'));\n if (nonOpKeys.length === 0) return;\n throw mixedFieldWrapperError(field, opKeys, nonOpKeys);\n}\n\n/**\n * Compile one `field: value | { $op: … }` entry into its leaves.\n *\n * Multiple operators on one field AND together — the rule\n * `FILTER_LOGIC_CASES` pins for every other backend, and the one a range\n * `{ $gte, $lte }` depends on.\n */\nfunction fieldLeaves(key: string, raw: unknown): NormalizedFilterNode[] {\n // [#6386] `undefined` in a comparand position, refused before any leaf exists.\n // First statement of the only leaf producer, so no consumer of the tree can be\n // handed one — see {@link assertDefinedComparands} for the position list and\n // for why this side of the `$not` rewrite is the load-bearing choice.\n assertDefinedComparands(key, raw);\n\n const out: NormalizedFilterNode[] = [];\n const leaf = (operator: string, values: unknown[]): void => {\n out.push({ kind: 'leaf', member: key, operator, values });\n };\n\n if (raw === null) {\n leaf('notSet', []);\n return out;\n }\n\n if (typeof raw === 'object' && !Array.isArray(raw) && !(raw instanceof Date)) {\n const wrapper = raw as Record<string, unknown>;\n // A field constrained by ZERO operators, ruled on in #5240: REFUSE it, the\n // way `driver-sql`, `driver-memory` and `formula` now do. This module used\n // to produce no leaf for it — and \"no leaf\" is the constant TRUE, which is\n // load-bearing since #5325 made TRUE absorb a `$or`: left alone,\n // `{$or: [{a: {}}, {b: 2}]}` would have gone from `b = 2` to EVERY row.\n // Neither silent reading is the author's intent (the shape is an authoring\n // accident — a filter builder that recorded a field and never its operator),\n // and a loud refusal is the answer the rest of the repo already gives.\n if (Object.keys(wrapper).length === 0) {\n throw invalidFilterError(\n `[analytics] \"${key}\" carries a field constraint with zero operators ({}). ` +\n `Refusing rather than reading it as \"every row\" or \"no row\" — #5240 ruled this ` +\n `shape refused on every backend.`,\n );\n }\n // [#6444] A wrapper mixing $-operator keys with non-$ siblings is refused\n // BEFORE the operator arm below gets to iterate `opKeys` only and return —\n // that early return is exactly how the non-$ siblings used to vanish. See\n // {@link assertUnmixedFieldWrapper} for the two-intent message contract and\n // the `$not` interaction.\n assertUnmixedFieldWrapper(key, wrapper);\n const opKeys = Object.keys(wrapper).filter((k) => k.startsWith('$'));\n if (opKeys.length > 0) {\n for (const opKey of opKeys) {\n // `$between [min, max]` LOWERS to its two bounds rather than getting a\n // `between` operator of its own. Both strategies already carry the\n // calendar-day whole-day rule on their upper bound — NativeSQLStrategy\n // compiles a bare-day `lte` half-open (#3777), ObjectQLStrategy hands\n // `$lte` to the driver, which does the same — so a range's max\n // inherits that rule by construction instead of needing a second\n // implementation to keep in step. (The preview evaluator's `$between`\n // gap was closed the same way, sharing its `$lte` helper.)\n //\n // Before this, `$between` was simply absent from the operator map and\n // fell to the `continue` below: the predicate VANISHED from the WHERE\n // clause, so a dashboard widget carrying a range filter charted the\n // entire dataset — #3650's symptom, on the surface #3650 was about.\n // The temporal conformance matrix caught it as row results\n // (`native-sql-temporal-conformance.test.ts`).\n if (opKey === '$between') {\n const v = wrapper[opKey];\n if (!Array.isArray(v) || v.length !== 2) {\n // Never drop it: an unbounded read is the failure mode this whole\n // branch exists to prevent, and it is indistinguishable from a\n // legitimately wide query. Same stance driver-memory took for the\n // same shape (#3948).\n throw invalidFilterError(\n `[analytics] \"$between\" on \"${key}\" needs a two-element [min, max] array, got ` +\n `${JSON.stringify(v)}. Dropping the predicate would silently widen the query to every row.`,\n );\n }\n // [#7598] The endpoints are comparands in their own right, and this\n // branch RETURNS before `assertCompilableComparand` below — so a\n // reference in a `$between` bound was the one comparand position on\n // this door that no shape gate ever saw. Asserted under the `$between`\n // name, not under the `gte` / `lte` the bounds lower to, because the\n // author wrote `$between` and that is the key they have to repair.\n assertNoFieldReferenceComparand(opKey, key, v);\n leaf('gte', [comparand(v[0])]);\n leaf('lte', [comparand(v[1])]);\n continue;\n }\n\n // The two null predicates read their BOOLEAN, not just their key —\n // which is why neither can live in MONGO_TO_CUBE_OP. `$null: true`\n // asks for IS NULL (`notSet`), `$null: false` for IS NOT NULL\n // (`set`); `$exists` is the mirror image. `$null` is the shape the\n // console emits for an \"is empty\" / \"is not empty\" filter\n // (`is_null`/`is_not_null` normalise to it in `filter.zod.ts`), so\n // dropping it silently meant such a widget showed every row.\n if (opKey === '$null' || opKey === '$exists') {\n const isNull = opKey === '$null' ? wrapper[opKey] === true : wrapper[opKey] === false;\n leaf(isNull ? 'notSet' : 'set', []);\n continue;\n }\n\n // [#5332] A `null` COMPARAND is a null PREDICATE, not a value\n // comparison: `$eq: null` is `IS NULL` (`notSet`) and `$ne: null` is\n // `IS NOT NULL` (`set`) — the same two leaves the `raw === null` branch\n // above and `{$null: true|false}` beside it already produce. `$eq: null`\n // and `$null: true` are not merely similar spellings; `driver-mongodb`\n // TRANSLATES the latter into the former (`mongodb-filter.ts`'s `$null`\n // arm), so they are one predicate in the contract, and\n // `read-scope-sql.ts`'s `compileOperator`, `driver-sql`, `driver-memory`\n // and `formula` all compile them alike.\n //\n // Without this branch the pair fell through to MONGO_TO_CUBE_OP and\n // `stringifyForCube(null)` → `''`, i.e. `stage = ''` / `stage != ''`\n // (#5332). One meaning had two answers inside ONE file, and the wrong\n // one bound a real value a NULL column can never equal: an \"is empty\"\n // widget charted ZERO rows with no error to read, and on a text column —\n // where `''` is a value rows genuinely store — `$ne: null` additionally\n // EXCLUDED the empty-string rows it was asked to keep.\n //\n // Identity against `null`, matching `read-scope-sql` and `driver-sql`:\n // `null` is what an authored `FilterCondition` can carry (JSON has no\n // `undefined`, and `$eq: undefined` is a key the author did not mean to\n // write). [#5526] `comparand`'s `undefined` → `null` normalisation\n // deliberately does NOT widen this test to `== null`: it makes the VALUE\n // bindable, while this branch decides what the operator MEANS, and the\n // meaning is #5332's to change, not a side effect of deleting an encoder.\n if ((opKey === '$eq' || opKey === '$ne') && wrapper[opKey] === null) {\n leaf(opKey === '$eq' ? 'notSet' : 'set', []);\n continue;\n }\n\n // An EMPTY set is a boolean constant, not an absent predicate (#5134).\n // `buildFilterClause` returns `null` for a value-less `in`/`notIn`, and\n // a `null` clause is read as \"no constraint\" by every compiler of this\n // tree — so `{stage: {$in: []}}` charted every row instead of none. It\n // also has to be a CONSTANT rather than a dropped clause for the\n // NULL-safe `$not` rewrite below to stay correct: a dropped conjunct\n // inside a negation flips the whole negation's answer, while `1 = 0`\n // negates to `1 = 1` the way `read-scope-sql.ts` already has it.\n if ((opKey === '$in' || opKey === '$nin') && Array.isArray(wrapper[opKey]) && (wrapper[opKey] as unknown[]).length === 0) {\n out.push({ kind: 'const', value: opKey === '$nin' });\n continue;\n }\n\n const cubeOp = MONGO_TO_CUBE_OP[opKey];\n if (!cubeOp) {\n // NEVER drop: a missing predicate does not narrow the query, it\n // WIDENS it — the compiled SQL stays valid and simply returns rows\n // the author excluded, which is indistinguishable from a\n // legitimately broad query and invisible to any test that asserts\n // the emitted SQL. That failure mode is #3650's, and skipping\n // unmapped operators is how `$between` reproduced it (#4128).\n // driver-memory made the same call for the same reason in #3948.\n throw invalidFilterError(\n `[analytics] Unsupported filter operator \"${opKey}\" on \"${key}\". ` +\n `Supported: ${Object.keys(MONGO_TO_CUBE_OP).join(', ')}, $between, $null, $exists, ` +\n `and the $and/$or/$not combinators. ` +\n `Dropping it would silently widen the query to rows the filter excludes.`,\n );\n }\n const v = wrapper[opKey];\n // [#5234] The comparand SHAPE gate runs before anything reads `v` — see\n // {@link assertCompilableComparand} for why this door and not the three\n // emitters downstream of it.\n assertCompilableComparand(opKey, key, v);\n const values = Array.isArray(v) ? v.map(comparand) : [comparand(v)];\n // [#5298] The operators that carry their own negation are NULL-safe,\n // here as everywhere else — see the module header's section on it.\n if (nullValueSatisfiesOperator(opKey, v) && !operatorIsNullTotal(opKey, v)) {\n out.push({\n kind: 'or',\n children: [\n { kind: 'leaf', member: key, operator: 'notSet', values: [] },\n { kind: 'leaf', member: key, operator: cubeOp, values },\n ],\n });\n continue;\n }\n leaf(cubeOp, values);\n }\n return out;\n }\n // Nested relation (e.g. {profile: {verified: true}}). Flatten with\n // dot-prefixed keys so cube field path resolution still works.\n for (const [nestedKey, nestedVal] of Object.entries(wrapper)) {\n out.push(...fieldLeaves(`${key}.${nestedKey}`, nestedVal));\n }\n return out;\n }\n\n // Implicit equality / array → in. An empty array is the same constant its\n // explicit `{$in: []}` spelling is — see the note at that branch.\n if (Array.isArray(raw)) {\n if (raw.length === 0) out.push({ kind: 'const', value: false });\n else leaf('in', raw.map(comparand));\n } else leaf('equals', [comparand(raw)]);\n return out;\n}\n\n/**\n * Compile a `FilterCondition` object into a node. `null` = no constraint (TRUE).\n *\n * Every entry of one object ANDs with its siblings, at every depth — the rule\n * `filter-logic-conformance.ts` exists to hold each backend to (#3774). The\n * combinator handling deliberately mirrors `read-scope-sql.ts`'s\n * `compileNode` — the `{}`/`{$not: {}}` identities since #5325, and the EMPTY\n * `$and`/`$or` identities since the #5322 ruling (see the note at the\n * `length === 0` branch) — so the two SQL-producing paths in this package\n * cannot drift apart about what a filter MEANS.\n */\nfunction buildNode(cond: Record<string, unknown>): NormalizedFilterNode | null {\n const children: NormalizedFilterNode[] = [];\n\n for (const [key, raw] of Object.entries(cond)) {\n // [#6386] What used to be here — `if (raw === undefined) continue;` — was\n // the entry gate doing the one thing the rest of this file forbids: a key\n // dropped without trace, which does not narrow the query, it WIDENS it (see\n // the module header's table and the note at MONGO_TO_CUBE_OP's miss branch).\n // Removing it does NOT create four new refusals; every key kind now reaches\n // the branch that already had the truest thing to say about it:\n //\n // {d: undefined} → `fieldLeaves` → `assertDefinedComparands` (#6386)\n // {$and|$or: undefined} → \"requires an array of filter objects, got undefined\"\n // {$not: undefined} → \"requires a filter object, got undefined\"\n // {$other: undefined} → \"Unsupported top-level filter operator\"\n //\n // ⚠️ An absent `where` is untouched and still means \"no constraint\":\n // `lowerAnalyticsWhere` answers `null` for `{where: undefined}` before this\n // function runs. The refusal is about a KEY INSIDE a `where`, where dropping\n // it silently changes which rows the author gets.\n if (key === '$and' || key === '$or') {\n if (!Array.isArray(raw)) {\n throw invalidFilterError(\n `[analytics] \"${key}\" requires an array of filter objects, got ${JSON.stringify(raw)}. ` +\n `Dropping it would silently widen the query to rows the filter excludes.`,\n );\n }\n if (raw.length === 0) {\n // Boolean identity (#5322 ruling, 2026-08-04): the empty `$and` is the\n // AND identity — TRUE, no constraint — and the empty `$or` is the OR\n // identity — FALSE, zero rows. Until that ruling this function REFUSED\n // both; its error message argued, verbatim, that \"An empty combinator\n // has no defensible reading — dropping it widens the query, and\n // treating it as 'match nothing' silently empties a chart\" — while the\n // five FILTER_LOGIC_CASES backends already reduced them. The ruling\n // took the reduction: only a reduction can evaluate a NESTED tree (a\n // rejection must first reduce to judge `$and: []` as the third branch\n // of a `$or`, which concedes the point), and `{$or: []}` = zero rows\n // is fail-closed where it matters — a disjunct list that loops to zero\n // items hides every row instead of widening (#5134). Loud\n // AUTHORING-time rejection of the literal spellings is #5330's scope.\n // Note the guard above did NOT loosen: a non-array `$and`/`$or` still\n // throws, as do non-object branches below.\n if (key === '$or') children.push(falseNode());\n continue;\n }\n const branches = raw.map((sub) => {\n // A non-object element is refused rather than skipped: skipping it\n // NARROWS a `$or` to its remaining branches and, under the TRUE-absorbs\n // rule below, would otherwise have to be read as TRUE and widen the\n // query to every row. Neither is a defensible reading of garbage input —\n // `read-scope-sql.ts` refuses the same shape.\n if (!isFilterObject(sub)) {\n throw invalidFilterError(\n `[analytics] \"${key}\" branches must be filter objects, got ${JSON.stringify(sub)}. ` +\n `Skipping it would silently change which rows the filter admits.`,\n );\n }\n return buildNode(sub);\n });\n // A `null` branch is the constant TRUE. It is the AND identity, so it\n // drops out of a `$and` — but it ABSORBS a `$or`: one TRUE disjunct makes\n // the whole disjunction TRUE, so the group contributes NO constraint\n // rather than collapsing to its surviving branches. Collapsing is what\n // narrowed `{$or: [{}, {stage: 'won'}]}` to `stage = 'won'` (#5325).\n if (key === '$or' && branches.some((n) => n === null)) continue;\n const kept = branches.filter((n): n is NormalizedFilterNode => n !== null);\n if (kept.length === 0) continue;\n // `$and` folds into this object's own AND; `$or` becomes a node, since\n // OR is exactly the structure a flat list could not carry.\n if (key === '$and') children.push(...kept);\n else children.push(kept.length === 1 ? kept[0] : { kind: 'or', children: kept });\n continue;\n }\n\n if (key === '$not') {\n if (!isFilterObject(raw)) {\n // Same call as the branch elements above: a `$not` of garbage used to\n // vanish, which turns \"exclude these rows\" into \"exclude nothing\".\n throw invalidFilterError(\n `[analytics] \"$not\" requires a filter object, got ${JSON.stringify(raw)}. ` +\n `Dropping it would silently widen the query to rows the filter excludes.`,\n );\n }\n // NULL-safe negation (#5146): totalise the operand's leaves FIRST, so the\n // negation can never be UNKNOWN and this path admits the same rows\n // `driver-memory` / `formula` / `driver-sql` admit. The guard is added as\n // STRUCTURE here, which is what makes it survive into the ObjectQL engine\n // path too (see the module header).\n const inner = buildNode(nullSafeNegationOperand(raw));\n // `notOf` turns a TRUE operand into FALSE instead of nothing: `{$not: {}}`\n // is the zero-row filter, and emitting nothing for it charted every row.\n children.push(notOf(inner));\n continue;\n }\n\n if (key.startsWith('$')) {\n throw invalidFilterError(\n `[analytics] Unsupported top-level filter operator \"${key}\". ` +\n `Dropping it would silently widen the query to rows the filter excludes.`,\n );\n }\n\n children.push(...fieldLeaves(key, raw));\n }\n\n return andOf(children);\n}\n\n// ── [#5146 / #5325] NULL-safe `$not` ─────────────────────────────────────────\n\n/**\n * What one field constraint needs so the leaves it produces are TOTAL — TRUE or\n * FALSE for every row, never UNKNOWN.\n *\n * - `'none'` — already total (`set` / `notSet`, a boolean constant), or\n * a shape this normalizer refuses, which must keep refusing.\n * - `'requireValue'` — a NULL column does NOT satisfy it: `col IS NOT NULL AND (…)`.\n * - `'allowNull'` — a NULL column DOES satisfy it: `col IS NULL OR (…)`.\n */\ntype NullGuard = 'none' | 'requireValue' | 'allowNull';\n\n/**\n * Does a NULL column satisfy this one operator, under the semantics the JS\n * backends (`driver-memory`'s `match`, `formula`'s `matchesFilterCondition`)\n * give it? They evaluate a missing value in ordinary two-valued JS — `undefined\n * !== 'won'` is simply `true` — and #5146 ruled that answer canonical.\n *\n * This is `sql-driver.ts`'s and `read-scope-sql.ts`'s table, with the\n * differences that come from THIS module's emitter rather than from a different\n * reading of #5146 — each guard matches its own emitter, which is the invariant,\n * not the literal table:\n *\n * - `$null` / `$exists` are read by IDENTITY (`=== true` / `=== false`)\n * because {@link fieldLeaves} reads them that way, where `read-scope-sql`\n * uses truthiness because its emitter does. Immaterial in practice: both\n * compile to a null predicate, so they are total either way and never\n * reach the polarity question.\n * - `$between` exists in this vocabulary; it lowers to `gte` + `lte`, two\n * positive comparisons, so it takes the same default they do.\n *\n * `$eq` / `$ne` DO carry `read-scope-sql`'s `value === null` arms — since #5332,\n * and only since then. While {@link fieldLeaves} stringified a `null` comparand\n * to `''`, these two arms had to describe THAT emitter: `{$eq: null}` was an\n * ordinary value comparison here, the guard said so, and the TSDoc recorded the\n * `''` comparand as a separate defect deliberately left undecided. #5332 decided\n * it — the emitter now compiles the pair to `notSet` / `set` — so the arms moved\n * with it, in the same commit. The invariant is not \"copy the sibling table\", it\n * is \"each guard matches its OWN emitter\"; the two tables agreeing again is the\n * consequence of the emitters agreeing, not the reason for the edit.\n *\n * The default is the large positive-comparison family (`$gt` / `$in` /\n * `$contains` / …), every member of which answers `false` for a value that is\n * not there. An operator this module does not support also lands here; it is\n * guarded and then still THROWS from {@link fieldLeaves}, so fail-closed is\n * preserved.\n */\nfunction nullValueSatisfiesOperator(op: string, value: unknown): boolean {\n switch (op) {\n // [#5332] `$eq: null` IS the null predicate — a NULL column satisfies it,\n // and no other comparand does.\n case '$eq': return value === null;\n // Mirror image: `$ne: null` compiles to `set` (`IS NOT NULL`), which a NULL\n // column FAILS. Any other comparand is the two-valued JS `!==`, which an\n // absent value passes — the arm this used to be for every comparand.\n case '$ne': return value !== null;\n case '$null': return value === true;\n case '$exists': return value === false;\n // Negative-polarity set / substring tests hold vacuously for an absent value.\n case '$nin': return true;\n // `$notContains` is the one operator where the two JS backends disagree for\n // a null-valued field (`driver-memory` answers false, `formula` true).\n // `formula` is followed because `driver-sql` and `read-scope-sql` follow it,\n // so this module casts no vote on a disagreement that is filed elsewhere.\n case '$notContains': return true;\n default: return false;\n }\n}\n\n/** Is this operator's compiled leaf already total for a NULL column? */\nfunction operatorIsNullTotal(op: string, value: unknown): boolean {\n // [#7598, maintainer ruling 2026-08-12] A `{ $field }` comparand on any of the\n // six scalar comparison operators is TOTAL AT THE BACKEND, so this module must\n // add no guard of its own — and MEASURED, adding one changes the answer.\n //\n // Every other entry in this switch is total because THIS module compiles the\n // operator into a null predicate. This one is total because of where the leaf\n // ends up: since the ruling, a `where` carrying a reference is declined by\n // `NativeSQLStrategy.canHandle` and served on the engine path, where\n // `driver-sql`'s `applyCrossFieldComparison` emits a predicate written total\n // across NULLs by construction (it repeats both column expressions for exactly\n // that reason — see `cross-field-conformance-cases.ts`, whose rows 4-6 carry\n // every NULL arrangement a pair of columns can be in). `@objectstack/formula`\n // resolves the reference and then compares in two-valued JS. The two agree,\n // and the corpus's declared id lists are the third statement of it.\n //\n // ## What the guard did before this arm existed — measured on the wasm driver\n //\n // The `$ne` arm of {@link nullValueSatisfiesOperator} answers `true` for any\n // non-null comparand, so a reference took the negative-polarity totalisation\n // in {@link fieldLeaves} and `{ amount: { $ne: { $field: 'budget' } } }`\n // lowered to `{$or: [{amount: null}, {amount: {$ne: ref}}]}`. That admitted\n // fixture row 6 — BOTH columns NULL — where the corpus, both SQL drivers and\n // the memory evaluator all EXCLUDE it, because row 6 satisfies the inner\n // `$eq` and `$ne` is its exact complement. Six corpus cases moved: the three\n // `$ne` class-pair cases, `a column differs from itself on no row`, and the\n // two `$not`-of-`$eq` cases (which reach the same guard through\n // {@link nullGuardForFieldSpec}). Widening a `$ne`, on a shape whose producer\n // is an RLS rule, is the direction that matters.\n //\n // The guard is right for a LITERAL comparand and is untouched there: `{amount:\n // {$ne: 5}}` must still admit a NULL `amount`, which is #5298's ruling and the\n // JS backends' answer. What differs is only that a reference's NULL semantics\n // are already decided by the referent, not by the target column alone — so\n // there is nothing left for a guard to decide.\n if (CROSS_FIELD_COMPARISON_OPERATORS.has(op) && isFieldReference(value)) return true;\n switch (op) {\n // Compile to `set` / `notSet` — `IS NULL` / `IS NOT NULL`, two-valued by\n // construction, on every strategy that compiles this tree.\n case '$null':\n case '$exists':\n return true;\n // [#5332] A `null` comparand makes these null PREDICATES too — `notSet` /\n // `set`, not comparisons — so they are total by construction and take NO\n // guard. Left out, `{$not: {stage: {$eq: null}}}` wrapped `stage IS NOT NULL\n // AND stage IS NULL` (an always-false conjunction) and negated it to EVERY\n // row, for a filter meaning \"stage is not empty\".\n case '$eq':\n case '$ne':\n return value === null;\n // An EMPTY set compiles to a boolean CONSTANT (see `fieldLeaves`), and a\n // constant is total. Wrapping a guard around it would only add a redundant\n // conjunct to a predicate whose value is already decided.\n case '$in':\n case '$nin':\n return Array.isArray(value) && value.length === 0;\n default:\n return false;\n }\n}\n\n/**\n * The guard one field constraint needs. A constraint is the AND of its\n * operators, so it is total when every operator is, and a NULL column satisfies\n * it only when it satisfies all of them.\n */\nfunction nullGuardForFieldSpec(spec: unknown): NullGuard {\n // `{field: null}` compiles to `notSet` (`IS NULL`) — already total.\n if (spec === null) return 'none';\n // A bare array is an implicit `$in`; an EMPTY one is the FALSE constant.\n if (Array.isArray(spec)) return spec.length === 0 ? 'none' : 'requireValue';\n // A scalar / Date is an implicit `=`; a NULL column fails it.\n if (typeof spec !== 'object' || spec instanceof Date) return 'requireValue';\n const entries = Object.entries(spec as Record<string, unknown>);\n // `{field: {}}` is REFUSED by `fieldLeaves` (#5240). Passing it through\n // unrewritten is what keeps that refusal reachable — a guard wrapped around it\n // would only change which message the caller sees.\n if (entries.length === 0) return 'none';\n let total = true;\n let nullSatisfies = true;\n for (const [op, value] of entries) {\n if (!operatorIsNullTotal(op, value)) total = false;\n if (!nullValueSatisfiesOperator(op, value)) nullSatisfies = false;\n }\n if (total) return 'none';\n return nullSatisfies ? 'allowNull' : 'requireValue';\n}\n\n/**\n * Guard one `field: spec` entry, writing either the untouched entry into `out`\n * or its guarded form into `guarded`.\n *\n * A nested relation spec (`{account: {region: 'NA'}}`) is flattened with the\n * dotted key {@link fieldLeaves} would have produced, so the guard lands on the\n * SAME member as the leaf it protects — guarding `account` when the leaf reads\n * `account.region` would test a column that does not exist.\n */\nfunction guardFieldEntry(\n key: string,\n spec: unknown,\n out: Record<string, unknown>,\n guarded: unknown[],\n): void {\n if (\n isFilterObject(spec) &&\n Object.keys(spec).length > 0 &&\n !Object.keys(spec).some((k) => k.startsWith('$'))\n ) {\n for (const [nested, value] of Object.entries(spec)) {\n guardFieldEntry(`${key}.${nested}`, value, out, guarded);\n }\n return;\n }\n\n const guard = nullGuardForFieldSpec(spec);\n if (guard === 'none') {\n out[key] = spec;\n } else if (guard === 'requireValue') {\n // `col IS NOT NULL AND (…)` — both conjuncts of the enclosing node.\n guarded.push({ [key]: { $null: false } }, { [key]: spec });\n } else {\n // `col IS NULL OR (…)` — one conjunct, so the OR binds tighter than the AND\n // this node's keys form.\n guarded.push({ $or: [{ [key]: { $null: true } }, { [key]: spec }] });\n }\n}\n\n/**\n * [#5146] Rewrite the operand of a `$not` so every leaf compiles to a TOTAL\n * predicate — which is what makes `NOT (…)` mean here what it means in\n * `driver-memory`, `formula` and (since #5296) `driver-sql`.\n *\n * # Why the guard rides the LEAF, not the `NOT`\n *\n * For a flat operand `NOT (a IS NOT NULL AND a = ?)` and `NOT (a = ?) OR a IS\n * NULL` are the same predicate. They stop being the same as soon as the operand\n * nests: hoisting the guard above a `$not` whose operand is a `$or` re-admits\n * rows the JS backends exclude — a NULL `a` would satisfy the whole negation\n * even when the `$or`'s OTHER branch is satisfied. Totalising each leaf makes\n * the rewrite compositional instead: De Morgan is sound over two-valued leaves,\n * so `$and`, `$or` and a nested `$not` all stay correct with no special cases.\n *\n * # Why polarity is per operator\n *\n * A blanket `OR col IS NULL` would WIDEN the negative-polarity operators:\n * `{$not: {a: {$ne: 5}}}` means \"a is 5\", and both JS backends exclude a NULL\n * row from it. Adding an unconditional null escape there would hand back exactly\n * the rows the filter excludes. So each leaf is guarded in the direction its own\n * operator answers, per {@link nullValueSatisfiesOperator}.\n *\n * # Why it is a REWRITE of the condition, not of the tree\n *\n * The output is still a `FilterCondition`, so `buildNode` compiles it with no\n * new cases and — the point of doing it here rather than in the SQL strategy —\n * the guard reaches the ObjectQL engine as structure too. Running only inside a\n * `$not` keeps every other comparison's shape untouched, and a NESTED `$not` is\n * left alone on purpose: its own branch totalises its operand, and\n * `NOT <total>` is itself total, so recursing would stack a redundant guard on\n * the same column.\n */\nfunction nullSafeNegationOperand(node: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n const guarded: unknown[] = [];\n for (const [key, value] of Object.entries(node)) {\n if ((key === '$and' || key === '$or') && Array.isArray(value)) {\n // A non-object element is passed through so `buildNode` still refuses it\n // with its own message.\n out[key] = value.map((element) => (isFilterObject(element) ? nullSafeNegationOperand(element) : element));\n continue;\n }\n if (key.startsWith('$')) {\n // `$not` (handled by its own branch) and anything else `$`-prefixed keep\n // whatever this module does with them today — the rewrite rules on NULL,\n // not on the operator vocabulary, and an unknown one must still throw.\n out[key] = value;\n continue;\n }\n guardFieldEntry(key, value, out, guarded);\n }\n if (guarded.length > 0) {\n const existing = Array.isArray(out.$and) ? out.$and : [];\n out.$and = [...existing, ...guarded];\n }\n return out;\n}\n\n// ── [#5334] The FilterArray door ─────────────────────────────────────────────\n\n/**\n * [#5334] A `where` array this door cannot lower.\n *\n * Deliberately the same refusal the other doors give, in the same envelope:\n * `driver-sql` / `driver-memory` / `driver-mongodb`'s\n * `filterArrayReachedDriverError` (#5158/#5329) and the engine's own\n * `lowerWhereFilterArray`. The INFIX join form (`[condA, 'or', condB]`) is the\n * shape that makes this branch load-bearing — no schema declares it,\n * `FilterArraySchema` excludes it and `parseFilterAST` has no lowering for it,\n * so it can only be refused; silently dropping it returns the UNFILTERED\n * dataset, which is what this whole module exists to prevent.\n */\nfunction filterArrayNotLowerableError(where: unknown[]): Error {\n return invalidFilterError(\n `[analytics] received a 'where' array that is not a filter: ${JSON.stringify(where)}. ` +\n `A filter array is a comparison [field, operator, value], a logical node ` +\n `[\"and\"|\"or\", ...conditions], or a list of those — it is INPUT-ONLY sugar (spec ` +\n `'FilterArray'), lowered to a FilterCondition by @objectstack/spec parseFilterAST() at ` +\n `every door, this one included (#5158/#5334). This value cannot be lowered, and an ` +\n `unapplied filter would have charted the UNFILTERED dataset. Recognised operators: ` +\n `${[...VALID_AST_OPERATORS].sort().join(', ')}. Infix joins ([condA, \"or\", condB]) are ` +\n `NOT one of the shapes — write the prefix form [\"or\", condA, condB].`,\n );\n}\n\n/**\n * Lower an analytics query's `where` to the CANONICAL `FilterCondition` object,\n * before any node is built. `null` when the query carries no `where`.\n *\n * Extracted from {@link normalizeAnalyticsFilterTree} for #5353's second reader\n * (`inferCubeFromQuery`, which needs the lowered condition's own KEYS rather\n * than the compiled tree's leaves — see that function for why the two readers\n * want different views of one filter). The three arrival answers documented on\n * {@link normalizeAnalyticsFilterTree} are all decided HERE; that function is\n * now this lowering plus {@link buildNode}.\n *\n * Keeping the lowering in ONE place is the point of the extraction. The\n * alternative — a second `isFilterAST`/`parseFilterAST` call at the new reader —\n * is how \"the shape the cube was minted from\" and \"the shape that reached SQL\"\n * drift apart, and both refusal paths below would then have had to be\n * re-derived to stay in step.\n */\nexport function lowerAnalyticsWhere(\n query: { where?: unknown } | unknown,\n): Record<string, unknown> | null {\n if (!query || typeof query !== 'object') return null;\n const where = (query as { where?: unknown }).where;\n if (!where || typeof where !== 'object') return null;\n\n if (Array.isArray(where)) {\n // (1) `[]` is \"no filter\", not a failed filter.\n if (where.length === 0) return null;\n // (3) Not a shape `parseFilterAST` can express.\n if (!isFilterAST(where)) throw filterArrayNotLowerableError(where);\n // (2) The declared path.\n const condition = parseFilterAST(where);\n if (!condition || typeof condition !== 'object' || Array.isArray(condition)) {\n // Unreachable by construction — `isFilterAST` accepted the shape, so\n // `parseFilterAST` has a lowering for it. Loud rather than silent for the\n // same reason the engine door is: the failure mode of the two spec\n // functions disagreeing is a dropped predicate, i.e. every row.\n throw invalidFilterError(\n `[analytics] filter array ${JSON.stringify(where)} passed isFilterAST() but ` +\n `parseFilterAST() lowered it to ${JSON.stringify(condition)}. Refusing rather than ` +\n `charting the dataset unfiltered (#5158/#5334).`,\n );\n }\n return condition as Record<string, unknown>;\n }\n\n return where as Record<string, unknown>;\n}\n\n/**\n * The FIELD KEYS a lowered `FilterCondition` names in its top-level\n * CONJUNCTION — `$and` descended through, `$or` / `$not` deliberately not.\n *\n * # Why a conjunction walker and not {@link collectFilterLeaves}\n *\n * This answers a VOCABULARY question, not a predicate question: #5353's caller\n * mints an ad-hoc cube's `dimensions` from the `where`, and the rule that bag\n * has always followed is \"the `where`'s own top-level keys are field names\".\n * `collectFilterLeaves` answers a different question (every member the compiled\n * predicate binds, structure discarded) and substituting it here would have\n * changed three behaviours #5353 does not ask about — see the caller's note.\n *\n * `$and` is descended because the LOWERING ITSELF introduces it: a flat filter\n * array `[[a,…],[b,…]]` is the array spelling of the object `{a…, b…}`, and\n * `parseFilterAST` lowers it to `{$and: [{a…}, {b…}]}`. Without descending, the\n * two spellings of one filter would still mint two different cubes — the whole\n * defect. Conjunction is associative and flat, so nested `$and`s are descended\n * too; recursing at all is safe here precisely because every entry of one\n * object ANDs with its siblings at every depth (`buildNode`'s rule).\n *\n * `$or` / `$not` are NOT descended, and both spellings agree on that today:\n * `{$or: […]}` and `[\"or\", …]` each contribute no key. Reading a disjunction's\n * branches as cube dimensions is a separate question from #5353's asymmetry —\n * and answering it would force a policy on DOTTED members that #5739 owns.\n */\nexport function conjunctFieldKeys(condition: Record<string, unknown>): string[] {\n const keys: string[] = [];\n const walk = (cond: Record<string, unknown>): void => {\n for (const [key, value] of Object.entries(cond)) {\n if (key === '$and' && Array.isArray(value)) {\n for (const child of value) {\n if (isFilterObject(child)) walk(child);\n }\n continue;\n }\n // Every other `$` key is a combinator this walk does not enter (`$or`,\n // `$not`) or an operator that belongs to a field ENTRY, not to the\n // condition — neither names a field here.\n if (key.startsWith('$')) continue;\n keys.push(key);\n }\n };\n walk(condition);\n return keys;\n}\n\n/**\n * Normalize an analytics query's `where` into the tree the strategies compile.\n * `null` when the query carries no `where` — i.e. no constraint.\n *\n * `where` is declared a `FilterCondition` (`AnalyticsQuerySchema`), and the\n * object form is the whole of the contract downstream. An ARRAY nevertheless\n * arrives — it is the `FilterArray` authoring sugar four published contracts\n * teach, and analytics is a door into the runtime like any other — so it is\n * LOWERED by {@link lowerAnalyticsWhere} (#5334, on #5158's ruling C), giving\n * the same three answers `ObjectQL`'s six entry points give since #5329:\n *\n * 1. `[]` — \"no filter\", not a failed filter: `null`, the same reading every\n * layer gives it (the engine door DELETES the key; `parseFilterAST([])` is\n * `undefined`). No predicate is emitted and no error is raised.\n * 2. A well-formed `FilterArray` — lowered through `parseFilterAST` and\n * compiled by {@link buildNode}, so the author gets the SAME rows either\n * spelling produces. `isFilterAST` gates first so the operator vocabulary is\n * checked before `parseFilterAST`'s lenient `$${op}` fallback can turn a\n * misspelling into a `$sounds_like` condition nothing executes.\n * 3. Anything else array-shaped — REFUSED, loudly. Before #5334 all three of\n * these arrivals answered the same way: `return null`, which for (2) and (3)\n * means the predicate VANISHED and the chart was drawn over every row.\n *\n * Lowering rather than refusing outright is what keeps ONE dashboard's\n * metadata meaning one thing: the same `where` on a plain `find()` already\n * lowers at the engine door (#5329), so refusing it here would have forked the\n * product by which face read the metadata.\n */\nexport function normalizeAnalyticsFilterTree(\n query: { where?: unknown } | unknown,\n): NormalizedFilterNode | null {\n const condition = lowerAnalyticsWhere(query);\n if (!condition) return null;\n return buildNode(condition);\n}\n\n/**\n * Every leaf in the tree, structure discarded.\n *\n * For asking WHICH MEMBERS a filter touches — the cross-object envelope check\n * is the caller. Never for building a predicate: the leaves of an `$or` read\n * as a conjunction here, so compiling from this list would turn `a OR b` into\n * `a AND b`. Use {@link normalizeAnalyticsFilterTree} for that.\n */\nexport function collectFilterLeaves(\n node: NormalizedFilterNode | null,\n): NormalizedAnalyticsFilter[] {\n if (!node) return [];\n if (node.kind === 'leaf') return [{ member: node.member, operator: node.operator, values: node.values }];\n // A boolean constant names no member — it constrains rows, not columns — so\n // it contributes nothing to the cross-object envelope check.\n if (node.kind === 'const') return [];\n if (node.kind === 'not') return collectFilterLeaves(node.child);\n return node.children.flatMap(collectFilterLeaves);\n}\n\n/**\n * [#5526] Put one comparand into a form a SQL driver can BIND. One-way, and the\n * only stringification left in this module's value path.\n *\n * This replaces `coerceFilterValueForSql` / `coerceFilterValueForObjectQL`, and\n * the difference is the whole of #5526: those two were DECODERS — they received a\n * string and guessed which type it had been before `stringifyForCube` flattened\n * it, so `'007'` became `7`, `'null'` became real NULL and `'true'` became `1`,\n * whatever the author meant. Nothing here inspects a string. A `string` comparand\n * is returned untouched, always, because a `string` is already bindable; only the\n * JS types a driver CANNOT bind are converted, each to the one form SQL has for\n * it:\n *\n * - `boolean` → `1` / `0`. better-sqlite3 refuses a JS boolean outright\n * (\"can only bind numbers, strings, bigints, buffers, and null\"), and `1`/`0`\n * is how every dialect these strategies target spells a bit. The ObjectQL\n * path deliberately does NOT do this — the engine compares against the\n * STORED boolean, where `1` never matches `true` (the regression\n * `objectql-strategy-boolean-filter.test.ts` guards).\n * - `Date` → canonical UTC ISO text. A comparand on a temporal column has\n * normally been through `StrategyContext.coerceTemporalFilterValue` (the\n * driver's own storage convention, ADR-0053 D-A2) before it gets here; this\n * arm is the fallback for the hookless / non-temporal case, where an\n * unbindable object would otherwise reach the driver.\n * - any other object / array → JSON text. Not a meaningful comparison on any\n * column, but the shape `filter.zod.ts` cannot exclude, and a driver-level\n * bind error tells the author nothing about their filter.\n *\n * `number`, `bigint`, `null` and `string` pass through — `null` included, and\n * that is deliberate: `col > NULL` is UNKNOWN, so the widget draws nothing. It is\n * the honest answer for an unordered comparand and the one the JS backends give;\n * the `''` this used to bind was a real comparison against the empty string,\n * which on a text column silently matched rows (see the module header).\n */\nexport function toSqlBindValue(v: unknown): unknown {\n if (typeof v === 'boolean') return v ? 1 : 0;\n if (v instanceof Date) return v.toISOString();\n if (v !== null && typeof v === 'object') return JSON.stringify(v);\n return v;\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport {\n isUninterpretableTemporalComparand,\n type TemporalComparandKind,\n} from '@objectstack/core';\n\n/**\n * Which comparand SHAPES this package's filter compilers can express (#5234).\n *\n * Two questions, asked of every value that reaches a predicate:\n *\n * 1. can it become a bound parameter at all ({@link isBindableComparand})?\n * 2. does it have a faithful rendering as the TEXT of a `LIKE` pattern\n * ({@link isRenderableTextComparand})?\n *\n * They are different questions about the same value — a binary buffer binds\n * fine and renders to nothing meaningful — and they are asked at different\n * operators, so they are two predicates rather than one with a flag.\n *\n * ## Why this file exists at all\n *\n * `driver-sql`'s `applyLike` and this package's {@link likePattern} both reached\n * their comparand through `String(value)`, and `String({})` is the literal\n * `'[object Object]'`. The result was never an error: it was a parameterised,\n * syntactically perfect `LIKE '%[object Object]%'` — a pattern the author never\n * wrote. Measured against a row whose text really is `[object Object]`, that\n * pattern MATCHED it, and `$notContains` EXCLUDED it. The `$in` / `$nin` half is\n * quieter still: an object member binds, compares equal to nothing, and the list\n * silently loses an entry — so `{status: {$nin: [{…}]}}` excludes nothing while\n * claiming to exclude something.\n *\n * ## The fence is an ALLOW-list, and it is measured\n *\n * An allow-list because a deny-list silently re-admits whatever value form is\n * invented next — the lesson `driver-turso`'s `RemoteTransport` wrote down when\n * it refused these same two shapes in remote mode (cloud#1004 / #1058), which is\n * also the precedent this rule follows rather than inventing a second policy.\n *\n * What stays IN the fence was measured across every face before being kept, not\n * assumed:\n *\n * | comparand | `driver-sql` | `driver-memory` | analytics (both doors) |\n * |---|---|---|---|\n * | `{$contains: 5}` | `%5%` | `%5%` | `%5%` |\n * | `{$contains: null}` | `%null%` | no match | `%null%` (#5526 pinned) |\n * | `{$contains: {}}` | matched a row reading `[object Object]` | same | same |\n * | `{$contains: ['al','be']}` | `%al,be%` | — | `%al,be%` (read scope) / `%al%` (`where` door) |\n *\n * The primitives agree, so refusing them would BREAK agreement — #5526 kept\n * `{$contains: 5}` deliberately for exactly that reason. The last row is the\n * opposite case: an array comparand already answered two different ways inside\n * this one package, so refusing it closes a live split.\n *\n * ## [#8186] The TYPE membership is IMPORTED; only the local extras are mirrored\n *\n * These predicates used to restate `driver-sql`'s `isBindableComparand` /\n * `isRenderableTextComparand` in full, because `service-analytics` depends on no\n * driver (see its `package.json` — only `@objectstack/core`,\n * `@objectstack/spec` and `@objectstack/types`) and those are module-private\n * functions with no export to reach for. The set itself no longer needs\n * reaching for: #7872 promoted it to the shared comparand-type door in\n * `@objectstack/spec/data`, `driver-sql` and `driver-turso` consume it there,\n * and since #8186 so does this file — `isAcceptedFilterComparand` for the six\n * types, {@link ACCEPTED_FILTER_COMPARAND_TYPES_SENTENCE} for the sentence the\n * refusals quote. The old comment's own prescription, applied: \"a THIRD\n * hand-copy is the thing to refuse: import from one of the two.\"\n *\n * What is still mirrored is the part the door deliberately does not carry —\n * each face's LOCAL extras (this package's `undefined` and binary arms), which\n * `driver-sql` records at its own use sites for the same reasons. Those stay\n * held by `__tests__/like-metacharacter-escape.test.ts`, which asserts both\n * predicates against the driver's post-#7872 expressions over a shared value\n * table, and by `__tests__/comparand-door-single-source.test.ts`, which pins the\n * end-to-end accept/refuse matrix this reconciliation had to leave untouched.\n *\n * ⛔ The ENVELOPES and the position logic below are this package's own and were\n * deliberately NOT moved: a caller-authored `where` refuses with a 400\n * `INVALID_FILTER`, a read scope fails closed with a 500 (ADR-0021 D-C), and\n * only the type membership and the shared sentence come from the door.\n *\n * ## ⚠️ [#7598] What the mirror does NOT cover: a position no gate ever reached\n *\n * `{ $field: 'col' }` is the shape the two predicates above are most often\n * assumed to handle, and they do not — not because they drifted, but because\n * they are only ASKED about two of the positions a comparand can sit in. Both\n * still classify a reference object exactly as `driver-sql`'s twins do (an\n * object is neither bindable nor renderable), and both doors call them for the\n * LIKE family and for `$in`/`$nin`/`$between` MEMBERS only. The whole comparand\n * of a scalar comparison — `{ amount: { $gt: { $field: 'budget' } } }` — was\n * asked of neither, so it was BOUND: measured on `origin/main` (`5823d593d`),\n * the read-scope door compiled `\"t\".\"amount\" > ?` with the reference OBJECT in\n * the bind list and the analytics `where` door compiled the same predicate with\n * the JSON TEXT `{\"$field\":\"budget\"}`. Nothing refused, nothing logged, and the\n * predicate compares a column against a value no row can hold.\n *\n * That is why this file gained a THIRD question — {@link isFieldReference} —\n * rather than a widened answer to the first two: the defect was never a\n * misclassification, so tightening `isBindableComparand` would have changed\n * cells that were already right (and broken the mirror) while leaving the\n * unasked position unasked.\n *\n * ## …and what the answer to that question is now — maintainer ruling 2026-08-12\n *\n * #7694 stopped the bind by REFUSING the shape on both doors, as the shipped\n * interim while the routing question sat with the maintainer. The ruling (Q1 =\n * B) replaced that with routing: `NativeSQLStrategy.canHandle` DECLINES a query\n * whose `where` or read scope carries a scalar reference, the query falls\n * through to the ObjectQL/engine path, and `driver-sql` compiles the comparison\n * under the four #5222 rulings using the `initObjects` metadata it owns. The\n * capability is therefore AVAILABLE on the analytics face, and the four security\n * rulings live in exactly one place — the option-A alternative (a\n * `StrategyContext` enumeration hook plus a second copy of those rulings here)\n * was rejected precisely because a guard that exists twice is a guard that will\n * disagree with itself.\n *\n * What this file contributes to that is {@link findCrossFieldComparand}, the\n * routing predicate, alongside the two refusal sentences that survive it:\n * {@link fieldReferenceComparandMessage} for the `/analytics/sql` echo, which\n * cannot honestly RENDER a predicate it does not emit, and\n * {@link fieldReferenceBetweenBoundMessage} for a `$between` endpoint, which no\n * backend serves and which #7596 removed from the spec.\n */\n\nimport {\n isAcceptedFilterComparand,\n ACCEPTED_FILTER_COMPARAND_TYPES_SENTENCE,\n} from '@objectstack/spec/data';\n\n/**\n * Can this value be handed to a driver as a bound parameter at all?\n *\n * [#8186] The TYPE membership is the shared comparand-type door's\n * (`isAcceptedFilterComparand`, `@objectstack/spec/data`), not this file's. It\n * used to be spelled out here, and identically again in\n * {@link isRenderableTextComparand} — three copies of one six-type set counting\n * the door itself, which is the drift risk #8186 was filed on rather than a\n * defect: the copies AGREED with the door, cell for cell, right up to this\n * change (`__tests__/comparand-door-single-source.test.ts` measured the whole\n * matrix on `origin/main` first, then re-ran it unchanged after).\n *\n * `driver-sql`'s twin was reconciled the same way by #7872 and this mirrors its\n * post-#7872 spelling, so the two are still a value-for-value mirror — see the\n * \"Mirrored, not imported\" section above, whose subject is now the LOCAL EXTRAS\n * rather than the set.\n *\n * ## The one package-local extra here: binary\n *\n * `ArrayBuffer.isView` (which covers `Buffer`, a `Uint8Array`) is a bindable the\n * engine-level door does not admit, and it is deliberately kept: a blob column\n * really is comparable on this driver family, and the read path measured it\n * accepted in every bind position. It is `driver-sql`'s own recorded extra too.\n */\nexport function isBindableComparand(value: unknown): boolean {\n // `undefined` — see {@link isRenderableTextComparand}'s note; it is admitted\n // here for the same reason and is just as unreachable through either door.\n if (value === undefined) return true;\n return isAcceptedFilterComparand(value) || ArrayBuffer.isView(value);\n}\n\n/**\n * Does this value have a faithful rendering as the text of a `LIKE` pattern?\n *\n * [#8186] The bindable set minus binary — and, like its sibling, the six-type\n * membership is now the shared door's (`isAcceptedFilterComparand`,\n * `@objectstack/spec/data`) rather than a local re-spelling. Binary binds but\n * renders to nothing a caller meant, which is why the two questions are two\n * predicates rather than one with a flag.\n *\n * ## The package-local extra here: `undefined` — kept, and unreachable\n *\n * `undefined` is inside the fence because it is not authorable (JSON has no\n * `undefined`) and `filter-normalizer.ts`'s `comparand()` normalises it to\n * `null` rather than refusing it (#5526, #5332).\n *\n * ⚠️ It no longer REACHES either predicate from either door, and the two\n * refusals arrived separately:\n *\n * | door | what refuses an `undefined` comparand first | envelope |\n * |---|---|---|\n * | read scope | `read-scope-sql.ts`, per #6050 ruling B pushed down by #6125 | `READ_SCOPE_COMPILE_FAILED` / 500 |\n * | analytics `where` | `assertDefinedComparands` (#6386, same ruling) | `INVALID_FILTER` / 400 |\n *\n * So `comparand()`'s normalise-to-`null` is itself a deliberately-kept dead arm\n * (its own TSDoc says so, and says reopening it is #5526's call, not a\n * cleanup's) — and this branch is one too. ⛔ Neither is evidence that this\n * package TOLERATES `undefined` where the shared door refuses it: measured\n * through both doors, `undefined` is REFUSED here exactly as the door would\n * refuse it (`__tests__/comparand-door-single-source.test.ts` pins that row at\n * both doors, in all three comparand positions). The branch stays because these\n * predicates are a value-for-value mirror of `driver-sql`'s twins, which keep\n * theirs for the identical reason — refused upstream there too, since #6050.\n * Narrowing the fence here would break the mirror without removing a reachable\n * answer.\n */\nexport function isRenderableTextComparand(value: unknown): boolean {\n return value === undefined || isAcceptedFilterComparand(value);\n}\n\n/**\n * [#7598] Is this comparand a `{ $field: 'col' }` reference — the shape\n * `FieldReferenceSchema` declares and `compileCelToFilter` really produces for a\n * field-to-field comparison in a CEL permission / RLS rule?\n *\n * Character for character `driver-sql`'s module-private `fieldReferenceOf`\n * (`sql-driver.ts`), read as a boolean: a plain object, not an array, carrying a\n * `$field` whose value is a STRING. Two deliberate consequences of mirroring\n * that spelling rather than inventing a third:\n *\n * - **Extra keys do not disqualify it.** `{ $field: 'budget', extra: 1 }` IS a\n * reference on all three faces, because `@objectstack/formula`'s\n * `resolveValue` reads `'$field' in raw` and ignores the remainder. A\n * narrower reading here would let the remainder be re-bound as a literal on\n * one face and resolved on another — the split this whole file exists to\n * close (#5222 measured the same cell driver-side and moved its own test).\n * - **A non-string `$field` is NOT one.** `{ $field: 5 }` falls through to the\n * ordinary object-comparand account — `driver-sql` binds it as JSON there\n * and so does this package (#5234 left `{$eq: {…}}` alone on purpose). That\n * cell is untouched here; changing it would be a different ruling, not a\n * rider on this one.\n *\n * ⚠️ `@objectstack/formula` is the WIDER of the two (`'$field' in raw`, any\n * value type). The driver's spelling is mirrored because this file's contract is\n * to be a value-for-value mirror of `driver-sql`, and because the wider reading\n * would refuse a shape the drivers bind — a new divergence in a change that\n * exists to remove one.\n */\nexport function isFieldReference(value: unknown): value is { $field: string } {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n return typeof (value as Record<string, unknown>).$field === 'string';\n}\n\n/**\n * [#7598] The comparison operators whose whole comparand `driver-sql` compiles\n * into a same-table column-to-column comparison since #5222 — and exactly the\n * positions where a `{ $field }` silently BOUND on this package's two doors.\n *\n * A mirror of `driver-sql`'s module-private `CROSS_FIELD_COMPARISON_OPERATORS`,\n * held by `__tests__/cross-field-reference-refusal.test.ts` rather than by this\n * comment: that suite drives the SHARED corpus (`CROSS_FIELD_CASES`, exported\n * from `@objectstack/driver-sql` precisely so a second face can be held to the\n * same table), so an operator the driver starts or stops compiling shows up as a\n * corpus case this package answers differently.\n *\n * Every OTHER position a reference can occupy was already refused on both doors\n * and is deliberately left alone, wording included — the LIKE family through\n * {@link isRenderableTextComparand}, `$in` / `$nin` members through\n * {@link isBindableComparand}, and a bare `{ field: { $field: … } }` as an\n * unsupported operator. Those refusals CONVERGE with `driver-sql`, which refuses\n * the same positions in its own #5222 refusal arm; only this set diverged.\n */\nexport const CROSS_FIELD_COMPARISON_OPERATORS: ReadonlySet<string> = new Set([\n '$eq', '$ne', '$gt', '$gte', '$lt', '$lte',\n]);\n\n/**\n * [#7598, maintainer ruling 2026-08-12 Q1 = B] The first `{ $field }` reference\n * sitting in a position `driver-sql` COMPILES since #5222 — or `null`.\n *\n * ## What this is FOR, which is not what the two predicates above are for\n *\n * {@link isBindableComparand} and {@link isRenderableTextComparand} answer\n * \"may this ONE value reach that ONE position\". This walks a WHOLE filter —\n * an analytics `where` (already lowered by `lowerAnalyticsWhere`, so the\n * authored array sugar arrives here as a `FilterCondition`) or an RLS read\n * scope — and answers a routing question instead: **does serving this query\n * require the cross-field capability?** `NativeSQLStrategy.canHandle` reads it\n * to DECLINE, so the query falls through to the ObjectQL/engine path, where\n * `driver-sql` compiles the comparison and enforces the four #5222 rulings\n * with the `initObjects` metadata it owns. See that method for the ruling.\n *\n * ## Why only the six scalar operators, when the ruling says \"carries `$field`\"\n *\n * Every OTHER position a reference can occupy is refused IDENTICALLY on both\n * sides of the routing decision — the LIKE family and `$in` / `$nin` members\n * through this file's two predicates here and through `driver-sql`'s own #5222\n * refusal arm, a `$between` endpoint through\n * `filter-normalizer.ts`'s surviving gate, a bare `{ field: { $field: … } }` as\n * an unsupported operator. Declining for those would swap one refusal for\n * another refusal a package further away, trading this package's precise\n * wording for the driver's without changing a single outcome. The scalar\n * comparands are the whole of what routing BUYS, so they are the whole of what\n * it tests.\n *\n * The walk is structural and total: it descends into `$and` / `$or` arrays,\n * `$not` operands, nested relation objects and any other nesting, because a\n * reference three combinators deep still needs the engine path. It is\n * deliberately blind to whether the referenced column is DECLARED, is the\n * tenant column, or has a comparable type — those are the four rulings, they\n * live in exactly one place (`driver-sql`), and re-asking them here is the\n * duplicated-guard the ruling rejected as option A.\n */\nexport function findCrossFieldComparand(\n filter: unknown,\n): { op: string; field: string; ref: string } | null {\n return findIn(filter, '');\n}\n\nfunction findIn(\n node: unknown,\n field: string,\n): { op: string; field: string; ref: string } | null {\n if (!node || typeof node !== 'object') return null;\n if (Array.isArray(node)) {\n for (const child of node) {\n const hit = findIn(child, field);\n if (hit) return hit;\n }\n return null;\n }\n if (node instanceof Date || ArrayBuffer.isView(node)) return null;\n for (const [key, value] of Object.entries(node as Record<string, unknown>)) {\n if (CROSS_FIELD_COMPARISON_OPERATORS.has(key) && isFieldReference(value)) {\n return { op: key, field, ref: value.$field };\n }\n // A `$`-prefixed key is an operator or a combinator, so the FIELD in scope\n // does not change; anything else names a field (or a nested relation\n // member) and becomes the new scope. Only used for the message.\n const hit = findIn(value, key.startsWith('$') ? field : key);\n if (hit) return hit;\n }\n return null;\n}\n\n/**\n * [#8690, maintainer ruling 2026-08-15] The first comparand a declared TEMPORAL\n * member's storage rule cannot read — or `null`.\n *\n * ## What it is FOR: the raw-SQL bypass named in the ruling\n *\n * The refusal itself lives at the ObjectQL engine's filter collection point\n * (`@objectstack/objectql`, `temporal-comparand-door.ts`), which is the one\n * seam that holds a comparand and the field's declared type at the same moment.\n * `NativeSQLStrategy` never reaches it: it compiles its own\n * `SELECT … WHERE col >= $N` and binds the comparand directly, so a raw-SQL\n * deployment would keep answering the silent zero the engine door now refuses.\n * The ruling closes that by name:\n *\n * > `NativeSQLStrategy.canHandle` must **decline** an uninterpretable temporal\n * > comparand so raw-SQL paths fall through to the engine door.\n *\n * So this answers a ROUTING question, exactly as {@link findCrossFieldComparand}\n * does one seam over: not \"is this filter legal\" but \"does serving it correctly\n * need the path that judges it\". Declining sends the query to the ObjectQL\n * strategy, whose `engine.aggregate` passes through that door — one refusal,\n * one wording, one place, whichever strategy the deployment's driver selects.\n *\n * ## Why the KIND is supplied by the caller\n *\n * This package holds no field map — it depends on `core`, `spec` and `types`,\n * and on no driver. The temporal fact therefore has to arrive with the query,\n * and it already does: a cube DIMENSION declares `type: 'time'` (compiled from\n * the dataset's `type: 'date'`), and `resolveStorageTarget`/`lookupMember`\n * already map a filter member to it. `kindOf` is that lookup, passed in, so\n * this walk stays a pure function of the filter and the caller's classification.\n *\n * ⚠️ Consequence, recorded rather than hidden: a temporal column filtered\n * WITHOUT being declared as a time dimension on the cube is not classified\n * here, so it is not declined and keeps today's behaviour on the raw-SQL path.\n * That is a strictly smaller hole than \"every raw-SQL query bypasses the door\",\n * it fails in the safe direction (a missed decline degrades to today's\n * behaviour, never to a NEW wrong answer), and closing it fully would take a\n * field map this package deliberately does not have.\n *\n * The walk is structural and total for the same reason its sibling's is: a\n * comparand three combinators deep still needs the engine path.\n */\nexport function findUninterpretableTemporalMember(\n filter: unknown,\n kindOf: (member: string) => TemporalComparandKind | null,\n): { field: string; kind: TemporalComparandKind; value: string } | null {\n return findUninterpretableIn(filter, '', kindOf);\n}\n\nfunction findUninterpretableIn(\n node: unknown,\n field: string,\n kindOf: (member: string) => TemporalComparandKind | null,\n): { field: string; kind: TemporalComparandKind; value: string } | null {\n if (!node || typeof node !== 'object') return null;\n if (Array.isArray(node)) {\n for (const child of node) {\n const hit = findUninterpretableIn(child, field, kindOf);\n if (hit) return hit;\n }\n return null;\n }\n if (node instanceof Date || ArrayBuffer.isView(node)) return null;\n // A reference is not a literal — the same position this file's sibling walk\n // routes on, and never a value any storage rule reads.\n if (isFieldReference(node)) return null;\n for (const [key, value] of Object.entries(node as Record<string, unknown>)) {\n // `$`-prefixed keys are operators and combinators: the field in scope does\n // not change. Anything else names a member and becomes the new scope.\n const scope = key.startsWith('$') ? field : key;\n const kind = scope ? kindOf(scope) : null;\n if (kind) {\n const hit = judgeTemporalLiterals(value, scope, kind);\n if (hit) return hit;\n continue;\n }\n const hit = findUninterpretableIn(value, scope, kindOf);\n if (hit) return hit;\n }\n return null;\n}\n\n/**\n * Every literal reachable in one classified member's value position — the\n * comparand itself, an operator bag's comparands, and each MEMBER of a list\n * operator's array, which is a comparand in its own right.\n */\nfunction judgeTemporalLiterals(\n value: unknown,\n field: string,\n kind: TemporalComparandKind,\n): { field: string; kind: TemporalComparandKind; value: string } | null {\n if (Array.isArray(value)) {\n for (const member of value) {\n const hit = judgeTemporalLiterals(member, field, kind);\n if (hit) return hit;\n }\n return null;\n }\n if (value && typeof value === 'object') {\n if (value instanceof Date || ArrayBuffer.isView(value) || isFieldReference(value)) return null;\n for (const nested of Object.values(value as Record<string, unknown>)) {\n const hit = judgeTemporalLiterals(nested, field, kind);\n if (hit) return hit;\n }\n return null;\n }\n return isUninterpretableTemporalComparand(kind, value)\n ? { field, kind, value: value as string }\n : null;\n}\n\n/**\n * The Filter Protocol operators whose comparand becomes the text of a `LIKE`\n * pattern — the ones every compiler in this package routes through\n * {@link likePattern}.\n *\n * `$regex` is absent because this package's operator vocabulary does not carry\n * it: `MONGO_TO_CUBE_OP` has no entry and `read-scope-sql` refuses it by name.\n * (`driver-sql` DOES list it, because the better-auth adapter emits it there for\n * a substring search.)\n *\n * [#7693] `$icontains` belongs here for the same reason its four siblings do,\n * and was missing for the ordinary reason a set goes stale: the operator\n * arrived AFTER the fence. #6520 added it to `MONGO_TO_CUBE_OP` and gave\n * `read-scope-sql`'s arm its `assertRenderableText` call, but not this entry —\n * so the analytics `where` door, this set's only reader, applied NO\n * comparand-shape gate to it at all. Measured on `origin/main` @ `b54aaab`:\n *\n * | door | `{name: {$icontains: {foo: 1}}}` |\n * |---|---|\n * | analytics `where` | compiled — `NativeSQLStrategy` bound `'%[object Object]%'` into its `LIKE` |\n * | `read-scope-sql` | REFUSED (`READ_SCOPE_COMPILE_FAILED` / 500) |\n *\n * One operator, two answers inside one package — #5234's defect verbatim, at\n * the operator its fence was never extended to. The ASCII fold `$icontains`\n * adds rides ON TOP of the pattern text (`likeShape` maps it to `'contains'`\n * on both executing compilers), so the question this set asks of a comparand\n * is the same question and the answer had no business differing. `driver-sql`'s\n * own `TEXT_PATTERN_OPERATORS` has listed it since #6520; this entry closes the\n * third and last face, after #7158 closed the objectql `having` one.\n */\nexport const TEXT_PATTERN_OPERATORS: ReadonlySet<string> = new Set([\n '$contains', '$notContains', '$startsWith', '$endsWith', '$icontains',\n]);\n\n/** A short, non-throwing rendering of an offending comparand for a message. */\nexport function shapePreview(value: unknown): string {\n try {\n const json = JSON.stringify(value);\n if (typeof json !== 'string') return typeof value;\n return json.length > 80 ? `${json.slice(0, 77)}...` : json;\n } catch {\n return typeof value;\n }\n}\n\n/**\n * The sentence both doors say about an object where a `LIKE` pattern's text\n * belongs, so the analytics `where` door and the read-scope lowering do not\n * describe one rule two ways. Each door wraps it in its OWN envelope — a 400\n * `INVALID_FILTER` for a caller-authored filter, a fail-closed compile refusal\n * for a read scope — because the envelope is what differs between them, not the\n * diagnosis.\n */\nexport function unrenderableTextComparandMessage(op: string, field: string, value: unknown): string {\n return (\n `\"${op}\" on \"${field}\" matches against the TEXT of a pattern, but its comparand is ` +\n `${Array.isArray(value) ? 'an array' : 'an object'} (${shapePreview(value)}). filter.zod.ts ` +\n `declares it a string (StringOperatorSchema); ${ACCEPTED_FILTER_COMPARAND_TYPES_SENTENCE} is ` +\n `accepted. Refusing rather than stringifying it: String({}) is \"[object Object]\", so the ` +\n `pattern that ran would be one nobody wrote — and a row storing that literal text matches it.`\n );\n}\n\n/**\n * [#7598] What `read-scope-sql` says about a `{ $field }` comparand it cannot\n * lower — the ONE surviving caller of this sentence after the 2026-08-12 ruling,\n * and the reason it now reads as a RENDERING boundary rather than a capability\n * one.\n *\n * ## What changed under the ruling, and why the wording had to follow\n *\n * Until that ruling this sentence was said by both doors and meant \"the platform\n * will not serve this here\". It no longer means that. Q1 = B routes a query\n * whose `where` or read scope carries a reference to the ObjectQL/engine path,\n * where `driver-sql` compiles the comparison and enforces the four #5222 rulings\n * with metadata it owns — so `/analytics/query` SERVES these queries and returns\n * rows. What is left is `compileScopedFilterToSql`, and the caller that still\n * reaches it with such a scope is `ObjectQLStrategy.generateSql`: the\n * `/analytics/sql` ECHO, a display string for an execution it does not perform.\n * There is no honest rendering of a total column-to-column predicate available\n * to that renderer, and the ruling's answer for the echo was explicit —\n * 「一致的响亮答案,不半渲染」 (one consistent, loud answer; no half-rendering).\n *\n * So the message tells a reader three things it could not tell them before: the\n * query itself is fine, the ECHO is what declined, and the rows are one call\n * away on `/analytics/query`. Telling them instead to \"compare against a literal\"\n * would send them to repair a rule that works.\n *\n * It still names what USED to happen, because that is the part a reader cannot\n * reconstruct: the reference was BOUND. The predicate was syntactically perfect,\n * the query ran, and a column was compared against a value no row can hold — no\n * error, no log line, an admin's read scope quietly answering the wrong row set.\n * That is the #3650 / #5234 class, and naming it is what stops the next reader\n * from \"restoring\" the old tolerance as a convenience.\n *\n * ⛔ `position` is no longer passed by any caller for a `$between` endpoint —\n * that arm says {@link fieldReferenceBetweenBoundMessage} instead, because it is\n * refused permanently and everywhere rather than declined by one renderer. The\n * parameter stays for a caller that needs to locate a reference inside a nested\n * scope.\n */\nexport function fieldReferenceComparandMessage(\n op: string,\n field: string,\n ref: string,\n position?: string,\n): string {\n return (\n `\"${op}\" on \"${field}\"${position ? ` (${position})` : ''} compares against the field reference ` +\n `{ \"$field\": \"${ref}\" }, which this compiler does not lower into a column-to-column ` +\n `comparison. Refusing rather than binding it: the reference object used to become the BOUND ` +\n `VALUE of the comparison, so the emitted predicate compared \"${field}\" against the reference ` +\n `itself — a value no row can hold — and a read scope built from it answered the wrong row set ` +\n `with nothing to read. ⚠️ This is NOT the platform declining the rule. @objectstack/spec ` +\n `declares this shape (FieldReferenceSchema), @objectstack/formula resolves it per record in ` +\n `memory, driver-sql / driver-sqlite-wasm compile it to a same-table column comparison for the ` +\n `six scalar operators since #5222, and since the 2026-08-12 ruling on #7598 the analytics ` +\n `native-SQL strategy DECLINES such a query so it routes to the ObjectQL engine path and runs ` +\n `there — the driver enforcing declared-only enumeration, the tenant-isolation ban and the ` +\n `comparison class with metadata it owns. What refuses here is this SQL lowering, whose only ` +\n `remaining caller is the /analytics/sql display echo; it has no faithful rendering of the ` +\n `predicate the engine path actually runs, and half-rendering one would describe a query that ` +\n `returns different rows. Run the query itself (/analytics/query) to get its rows (#7598).`\n );\n}\n\n/**\n * [#7598] A `{ $field }` in a `$between` ENDPOINT — a separate sentence from\n * {@link fieldReferenceComparandMessage} because it is a separate condition,\n * and #5240's rule cuts the other way here: two shapes with two repairs must\n * not share one wording.\n *\n * The scalar comparands above are SERVED, one path over. A `$between` endpoint\n * is not served anywhere and is not going to be: `driver-sql` and\n * `driver-sqlite-wasm` refuse it (`CROSS_FIELD_REFUSALS` pins both endpoints),\n * the memory evaluator has no reading of it either — `resolveValue` returns an\n * array unchanged, so the bounds are ordered against the raw reference OBJECT —\n * and #7596 removed the position from `FieldReferenceSchema` outright under\n * ADR-0049 declared = enforced (maintainer ruling 2026-08-11). Pointing that\n * author at the engine path would point them at another refusal.\n *\n * It matters most on THIS door, which is why the arm exists here at all: the\n * analytics `where` lowering splits `$between` into a `gte` leaf and an `lte`\n * leaf, so an endpoint reference would reach the driver wearing an operator\n * #5222 COMPILES — succeeding on the analytics face alone, in defiance of both\n * the driver corpus and the schema. See `filter-normalizer.ts`'s\n * `assertNoFieldReferenceComparand`.\n */\nexport function fieldReferenceBetweenBoundMessage(\n op: string,\n field: string,\n ref: string,\n index: number,\n): string {\n return (\n `\"${op}\" on \"${field}\" has the field reference { \"$field\": \"${ref}\" } at index ${index} of its ` +\n `[min, max] bounds. A range BOUND may not be a field reference on any backend: driver-sql and ` +\n `driver-sqlite-wasm refuse both endpoints (#5222), @objectstack/formula does not resolve a ` +\n `reference inside a list either — it orders the bounds against the raw reference object, which ` +\n `no value compares meaningfully to — and @objectstack/spec no longer declares the position at ` +\n `all (#7596 removed FieldReferenceSchema from the $between endpoint union, ADR-0049 declared = ` +\n `enforced). Refusing rather than lowering it: this compiler splits $between into its two ` +\n `bounds, so the reference would arrive at the driver under a \"$gte\" / \"$lte\" the author never ` +\n `wrote — a position the SQL drivers DO compile — and the range would quietly succeed here ` +\n `while the identical filter is refused everywhere else. Use a literal bound, or spell the ` +\n `comparison you meant as a scalar one ({ \"${field}\": { \"$gte\": { \"$field\": \"${ref}\" } } }), ` +\n `which IS served — on the ObjectQL engine path, where the driver enforces the #5222 rulings ` +\n `(#7598).`\n );\n}\n\n/**\n * The sentence both doors say about a list member that cannot be bound. See\n * {@link unrenderableTextComparandMessage} for why the message is shared and the\n * envelope is not.\n *\n * [#8186] The accepted-set clause is {@link ACCEPTED_FILTER_COMPARAND_TYPES_SENTENCE},\n * with binary kept as this package's own parenthetical extra — the exact shape\n * `driver-sql`'s twin took when #7872 reconciled it, so the two faces describe\n * one rule in one wording again. The hand-copy it replaces read \"a string,\n * number, boolean, null, Date or binary value\", which had silently gone WRONG\n * in the quieter direction: it omitted `bigint`, a type both predicates here\n * have always accepted and both doors have always compiled. Quoting the door\n * fixes the omission as a side effect of removing the copy — the accepted set\n * itself does not move (`__tests__/comparand-door-single-source.test.ts`).\n */\nexport function unbindableListMemberMessage(\n op: string,\n field: string,\n value: unknown,\n index: number,\n): string {\n return (\n `\"${op}\" on \"${field}\" has a value at index ${index} of its list that cannot be bound as a SQL ` +\n `parameter: ${shapePreview(value)}. Every member of an $in/$nin/$between list is a comparand ` +\n `in its own right — use ${ACCEPTED_FILTER_COMPARAND_TYPES_SENTENCE} (or a binary value). ` +\n `Refusing rather than binding it: the member can equal no stored value, so the list silently ` +\n `loses that entry (and a $nin loses the exclusion the caller wrote).`\n );\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * LIKE pattern construction for this package's three SQL compilers (#5567).\n *\n * A `$contains` / `$notContains` / `$startsWith` / `$endsWith` comparand is a\n * LITERAL the author typed. Concatenating it straight into a wildcard position\n * silently reinterprets it as a pattern, because `_` is LIKE's single-character\n * wildcard and `%` its multi-character one:\n *\n * - `{name: {$contains: '_admin'}}` matched `xyadmin` as well as `x_admin`;\n * - `{name: {$contains: '50%'}}` matched `off 5012 now` as well as `off 50% now`.\n *\n * Both directions are WIDENING, and one of the three call sites is\n * `read-scope-sql.ts` — the ADR-0021 D-C read-scope (tenant + RLS) lowering,\n * where a wider predicate is over-reach rather than a loose filter (#5347 /\n * #5324, on that same file). Prime Directive #3 forces machine names to\n * `snake_case`, so essentially every machine-name comparand carries a `_` and\n * hits this silently.\n *\n * ## The two halves are one fix\n *\n * Escaping the value and declaring the escape character are not independent\n * steps — either alone is a different bug:\n *\n * - Escaping alone: `%\\_admin%` with no escape character in force is a search\n * for a literal backslash. SQLite has NO default escape character, so this\n * would return zero rows there.\n * - The clause alone: nothing in the pattern is escaped, so nothing changes.\n *\n * Hence {@link likePattern} always produces a pattern escaped for\n * {@link LIKE_ESCAPE_CHAR}, and every emitter pairs it with an `ESCAPE` binding.\n *\n * ## Why the escape character is BOUND, never written as a literal\n *\n * Every emitter here binds it as an ordinary placeholder (`LIKE ? ESCAPE ?`)\n * rather than writing `ESCAPE '\\'` into the SQL text. Two reasons, both load-bearing:\n *\n * 1. **The literal spelling is not portable.** MySQL applies C escape syntax\n * inside string literals — \"If you want a LIKE string to contain a literal\n * `\\`, you must double it\" — so the backslash escape character is spelled\n * `'\\\\'` there and `'\\'` on SQLite/Postgres. These compilers do not know\n * which dialect will run their output. A bound value is escaped by the\n * driver for its own dialect, so there is exactly one spelling here.\n * 2. **It rides the existing placeholder plumbing.** `read-scope-sql.ts` emits\n * `?` and BOTH of its consumers (`NativeSQLStrategy.applyReadScope`,\n * `ObjectQLStrategy.generateSql`) renumber `?` → `$N` while pushing the\n * matching value. Because the escape character is a bound value it is\n * carried by that rewrite with no change at the upper layer — which answers\n * the \"which layer does the ESCAPE clause belong to\" question in the issue:\n * the predicate layer, entirely, because nothing above it has to know.\n *\n * Dialect support for the clause itself, confirmed against the vendors' own\n * reference manuals (quoted in PR for #5567): Postgres defaults to backslash and\n * accepts `ESCAPE`; MySQL assumes `\\` unless `NO_BACKSLASH_ESCAPES` is set and\n * accepts `ESCAPE` with an argument that \"must evaluate as a constant at\n * execution time\" (a bound placeholder is); SQLite honours NO default escape\n * character at all, which is the reason the explicit clause is required rather\n * than merely tidy.\n *\n * ## Relationship to `driver-sql`'s `applyLike`\n *\n * This is deliberately the same transform `SqlDriver.applyLike`\n * (`packages/drivers/driver-sql/src/sql-driver.ts`) applies — same escaped\n * character class, same three wildcard shapes, same bound `ESCAPE` — and its\n * TSDoc points back here. It is a SECOND implementation on purpose, not an\n * oversight:\n *\n * - `service-analytics` depends on no driver (see its `package.json`: only\n * `@objectstack/core` and `@objectstack/spec`), and `applyLike` is a private\n * method on a knex builder — it takes a builder and a field, not a string,\n * so there is nothing importable even if the dependency existed.\n * - Promoting it to a shared package would add a new public surface to\n * `@objectstack/core` for three call sites inside one package. Not worth a\n * new export until a fourth consumer outside this package needs it.\n *\n * What keeps the two from drifting is not these comments: it is\n * `__tests__/like-metacharacter-escape.test.ts`, which asserts\n * {@link escapeLikePattern} against `applyLike`'s expression character for\n * character. A third hand-copy of this logic anywhere is the thing to refuse —\n * import from here, or add a consumer to that test.\n *\n * ## [#6518] Case sensitivity: why this file still emits a plain `LIKE`\n *\n * #4706 Q2 = A rules the `$contains` family case-SENSITIVE on every backend, and\n * #6518 moved the driver family onto that answer — `SqlDriver`'s\n * `textMatchPredicate` now picks the construct per DIALECT, because `LIKE` folds\n * ASCII on SQLite and follows the collation on MySQL. The obvious question is\n * why the compilers here did not move with it, and the answer is measured\n * rather than assumed:\n *\n * 1. **These compilers emit Postgres-shaped SQL, and on Postgres `LIKE` is\n * already exactly the ruled semantics.** Both consumers number their\n * placeholders `$1`, `$2`, … (`native-sql-strategy.ts`'s `buildFilterClause`\n * and `objectql-strategy.ts`'s filter render), and `applyReadScope` /\n * `generateSql` rewrite this file's `?` into `$N` on the way out;\n * identifiers are `\"double quoted\"`. Measured on a live PostgreSQL 16\n * against the shared nine-row fixture: `LIKE '%acme%'` answers row 2 alone\n * and `LIKE '%ACME%'` answers row 1 alone — case-exact, which is the\n * contract. So there is no divergence to close HERE, and changing the\n * construct would create one.\n * 2. **The RLS fork the issue warned about does not open.** #6518's concern\n * was that a driver-only fix would compile one permission rule into two row\n * sets. It does not, because the two paths meet only on Postgres — where\n * `textMatchPredicate`'s postgres arm is also a plain `LIKE`, unchanged.\n *\n * What that reasoning DEPENDS on is the dialect, so it is the thing to re-open\n * rather than the code: **if these compilers ever emit for SQLite or MySQL, this\n * file is wrong and `$contains` silently over-matches there** — on\n * `read-scope-sql.ts`'s output that is ADR-0021 read-scope over-reach, not a\n * loose filter (#3948). Two things would have to arrive together: a dialect\n * input reaching these three compilers, and the per-dialect construct table\n * `textMatchPredicate` already carries. Neither exists today and neither is\n * invented here on speculation. `__tests__/like-metacharacter-escape.test.ts`\n * pins both halves of the claim — that the emitted statement is\n * Postgres-shaped, and that the family is compiled case-EXACT — so this\n * paragraph goes red rather than merely stale.\n *\n * `$icontains` IS implemented here since #6520, and it is a separate construct\n * rather than a flag on the family above: it folds ASCII case on BOTH sides via\n * {@link asciiLowerSqlExpr}, while the `$contains` family stays case-EXACT. The\n * two must not be collapsed — a shared \"case-insensitive\" path would give the\n * `$contains` family the fold the ruling took away from it.\n *\n * ## `String(value)` is safe here because nothing unrenderable reaches it (#5234)\n *\n * The `String()` below used to be the whole defect on the other side: `String({})`\n * is the literal `'[object Object]'`, so an object comparand built a parameterised\n * `LIKE '%[object Object]%'` — valid SQL, a pattern nobody wrote, and one that\n * MATCHED a row whose text really was `[object Object]`. This function is NOT the\n * place that was fixed. Both of this package's doors refuse an object comparand\n * before a pattern is built — `filter-normalizer.ts`'s `fieldLeaves` for the\n * analytics `where` path and `read-scope-sql.ts`'s `compileOperator` for the RLS\n * lowering — using the one rule in `comparand-shape.ts`, and `driver-sql`'s\n * `assertCompilableComparand` does the same for `applyLike`.\n *\n * So `escapeLikePattern` keeps its `unknown` parameter and its unconditional\n * `String()` on purpose: what arrives is a string, number, bigint, boolean,\n * `null`, `undefined` or `Date`, each of which `String()` renders faithfully, and\n * a number comparand (`{$contains: 5}` → `%5%`) is deliberately still accepted —\n * it agrees across every face and #5526 kept it. Do NOT add a tolerant reading of\n * an object here; add it to neither door either.\n */\n\n/**\n * Where the wildcard sits relative to the comparand. Named exactly as\n * `driver-sql`'s `applyLike` names its `shape` parameter, so the two read alike:\n * `contains` → `%v%`, `starts` → `v%`, `ends` → `%v`.\n */\nexport type LikeShape = 'contains' | 'starts' | 'ends';\n\n/**\n * The escape character every emitter in this package binds into its `ESCAPE`\n * clause. A single backslash — the value `driver-sql` binds, and the default\n * Postgres and MySQL already assume.\n */\nexport const LIKE_ESCAPE_CHAR = '\\\\';\n\n/**\n * Escape the LIKE metacharacters (`%`, `_`) and the escape character itself\n * (`\\`) so a comparand matches literally.\n *\n * Character for character the expression `driver-sql`'s `applyLike` uses; the\n * shared test holds them to each other.\n */\nexport function escapeLikePattern(value: unknown): string {\n return String(value).replace(/[\\\\%_]/g, '\\\\$&');\n}\n\n/**\n * Build the LIKE pattern for one comparand: escaped, then wrapped in the\n * wildcards `shape` calls for.\n *\n * The result MUST be bound together with {@link LIKE_ESCAPE_CHAR} as the\n * predicate's `ESCAPE` argument — see the escaping-alone note in this file's\n * header for what happens on SQLite otherwise.\n */\nexport function likePattern(shape: LikeShape, value: unknown): string {\n const escaped = escapeLikePattern(value);\n return shape === 'starts' ? `${escaped}%` : shape === 'ends' ? `%${escaped}` : `%${escaped}%`;\n}\n\n/** `A`..`Z` and the `a`..`z` they fold onto — the #4706 Q1 = A domain, as data. */\nconst ASCII_UPPER_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';\nconst ASCII_LOWER_LETTERS = 'abcdefghijklmnopqrstuvwxyz';\n\n/**\n * [#6520] Wrap a SQL expression in `$icontains`' ASCII-ONLY case fold.\n *\n * Character for character `driver-sql`'s postgres arm\n * (`textMatchPredicate` → `translate(expr, 'ABC…', 'abc…')`), and shared by this\n * package's three compilers for the same reason `likePattern` is: the escaping\n * and the fold interact, and a second copy of either is how two compilers of one\n * filter tree start describing different queries (#5333).\n *\n * ## Why `translate()` and not `LOWER()`\n *\n * Postgres' `LOWER()` is locale-aware — it folds `É` to `é`, and the contract\n * says it must not (#4706 Q1 = A, because SQLite folds ASCII only and three of\n * the five drivers are SQLite underneath). `translate()` with an explicit\n * 26-character domain folds exactly `A-Z` and leaves every other code point\n * alone, so it is the fold the ruling names rather than the one the database\n * happens to offer.\n *\n * ## The dialect this assumes, stated so it can go red rather than stale\n *\n * Postgres, like everything else these three compilers emit — the claim this\n * file's header already makes and `__tests__/like-metacharacter-escape.test.ts`\n * pins. `translate()` is Postgres/Oracle; SQLite has no such function. So the\n * warning in the header applies to this helper WORD FOR WORD: if these compilers\n * ever emit for SQLite or MySQL, this expression does not merely over-match, it\n * fails to parse. The remedy is the per-dialect construct table `driver-sql`'s\n * `textMatchPredicate` already carries — nested `REPLACE` for the dialects\n * without `translate` — not a quiet fallback to `LOWER()`, which would silently\n * restore the Unicode fold this function exists to avoid.\n *\n * The caller must apply it to BOTH sides of the comparison. Folding only the\n * comparand compares a folded needle against a raw column and matches just the\n * rows that were already lower-case.\n */\nexport function asciiLowerSqlExpr(expr: string): string {\n return `translate(${expr}, '${ASCII_UPPER_LETTERS}', '${ASCII_LOWER_LETTERS}')`;\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { FilterCondition } from '@objectstack/spec/data';\nimport type { RegisteredErrorCode } from '@objectstack/spec/api';\nimport { likePattern, LIKE_ESCAPE_CHAR, asciiLowerSqlExpr } from './like-pattern.js';\nimport {\n CROSS_FIELD_COMPARISON_OPERATORS,\n fieldReferenceBetweenBoundMessage,\n fieldReferenceComparandMessage,\n isBindableComparand,\n isFieldReference,\n isRenderableTextComparand,\n unbindableListMemberMessage,\n unrenderableTextComparandMessage,\n} from './comparand-shape.js';\n\n/**\n * Compile an RLS / tenant read-scope `FilterCondition` into a parameterized,\n * alias-qualified SQL predicate (ADR-0021 D-C).\n *\n * This is the single, security-critical translation point between the\n * canonical Mongo-style filter the `RLSCompiler` emits and the raw SQL the\n * analytics `NativeSQLStrategy` runs. It is deliberately:\n *\n * - **Fail-closed.** Any operator, value shape, or identifier it cannot\n * translate THROWS. A read-scope predicate must never be silently dropped —\n * dropping it would run the query unscoped and leak cross-tenant data.\n * - **Injection-safe.** Field/alias identifiers are validated against a strict\n * snake_case pattern and every value is bound as a `?` placeholder (the\n * strategy renumbers `?` → `$N`). No value is ever interpolated into SQL.\n * - **Alias-qualified.** Bare fields become `\"alias\".\"field\"` so the same\n * predicate applies to the base table or any joined table.\n *\n * Supports the operators the RLS layer and common policies emit: implicit\n * equality, `$eq/$ne/$gt/$gte/$lt/$lte/$in/$nin/$between/$contains/$notContains/\n * $startsWith/$endsWith/$null/$exists`, and `$and/$or/$not` combinators.\n *\n * ## `''` means TRUE, and that is a value — not \"nothing happened\"\n *\n * `compileNode` returns `''` for a node that constrains nothing (`{}`, an\n * all-TRUE `$and`). That empty string is the boolean constant TRUE, and the two\n * places where a compiler forgets it are exactly where this one used to be\n * wrong (#5297):\n *\n * - TRUE is the AND identity, so dropping it from a `$and` is correct — but it\n * ABSORBS a `$or`, so one TRUE disjunct makes the whole `$or` TRUE. Filtering\n * it out of `$or` (`{$or: [{}, {a: 1}]}` → `a = 1`) silently NARROWED.\n * - `NOT TRUE ≡ FALSE`, so `{$not: {}}` is the zero-row predicate. Emitting\n * nothing for it made `compileScopedFilterToSql` return `''`, and\n * `applyReadScope`'s `if (!sql) return;` then added no `WHERE` at all — a\n * read scope that should have shown zero rows exposed the whole table. In an\n * RLS lowering that is a permission bypass, not a rounding error.\n *\n * So a group is now compiled into its own bind buffer and only committed when it\n * survives, and FALSE has a spelling ({@link FALSE_CLAUSE}) instead of being\n * representable only as silence.\n *\n * ## Empty combinators are boolean identities (#5322)\n *\n * `{$and: []}` is TRUE (every row), `{$or: []}` is FALSE (zero rows), and\n * `{$not: {}}` is `NOT TRUE` — FALSE. This compiler used to refuse the empty\n * arrays fail-closed while the five `FILTER_LOGIC_CASES` backends reduced\n * them; the 2026-08-04 #5322 ruling aligned this file and the analytics\n * `filter-normalizer` with the reduction (see the note at the `length === 0`\n * branch in {@link compileNode} for why). Reduction happens structurally over\n * the whole tree, and it composes with the #5146 NULL-safe `$not` rewrite as\n * \"reduce first\": {@link nullSafeNegationOperand} maps combinator arrays\n * element-wise (an empty array stays empty, a `{}` leaf has no field to\n * guard), so the identity a constant reduces to is untouched by the rewrite\n * and the rewrite only ever guards leaves that survive it.\n *\n * ## `$not` is NULL-safe (#5146)\n *\n * SQL is three-valued and a `WHERE` keeps only TRUE, so a bare `NOT (col = ?)`\n * drops every row whose `col` is NULL — while `driver-memory` and `formula`\n * (and, since #5296, `driver-sql`) return those rows. One read scope, two\n * visible sets, chosen by which backend answered. #5146 ruled the JS answer\n * canonical; {@link nullSafeNegationOperand} here is the same rewrite\n * `sql-driver.ts` applies, so an analytics query and an ordinary `find()` scope\n * the same rows.\n *\n * ## The LIKE family compares LITERALS (#5567)\n *\n * `_` is LIKE's single-character wildcard and `%` its multi-character one, so a\n * comparand concatenated straight into a pattern position stops meaning what the\n * author wrote: `{owner_name: {$contains: '_admin'}}` also admitted `xyadmin`,\n * and `{$contains: '50%'}` also admitted `off 5012 now`. Every LIKE arm below\n * therefore binds an ESCAPED pattern plus its escape character — see\n * `like-pattern.ts` for the transform, for why the escape character is a bound\n * value rather than a SQL literal, and for its correspondence with `driver-sql`'s\n * `applyLike`.\n *\n * On THIS compiler that widening was the #5347 / #5324 shape again: a read scope\n * admitting rows the policy did not is over-reach, not a degraded filter. Note\n * the file was fail-closed everywhere else — the LIKE family was the one place an\n * author's literal was silently reinterpreted rather than refused.\n *\n * ## Every refusal here is a SERVER fault, and says so (#5367, maintainer ruling 2026-08-06)\n *\n * The ten fail-closed refusals below were bare `throw new Error(…)`, and\n * `/analytics/dataset/query` classified them by matching `read-scope-sql` in the\n * message text — the last surviving entry of the hardcoded substring list #5352\n * introduced. It answered `400 DATASET_INVALID`, which was wrong twice:\n *\n * - **Wrong attribution.** Neither input is the caller's. `filter` is the RLS\n * `FilterCondition` the security service compiles from an ADMIN-authored\n * sharing rule / permission set; `alias` is a join alias the DATASET COMPILER\n * generated. The caller's own predicate travels a different road entirely\n * (`filter-normalizer.ts`, `INVALID_FILTER` / 400 since #5352). So the two\n * things that can land here are an administrator's broken policy and drift\n * between two of OUR components (#5557's `$regex` was exactly the second) —\n * and for the caller of this request both are a server fault. `400` told them\n * to fix a request that was never the problem, and hid the fault from the 5xx\n * alerting that should have seen it.\n * - **Wrong disclosure.** A 400 echoed the message verbatim, so\n * `unsafe field identifier \"…\"` / `unsupported operator \"$x\" on \"owner\"`\n * handed the caller the FIELD NAMES AND COMPARANDS OF THE RLS POLICY — the\n * one document a tenant must not be able to read out of an error body.\n *\n * {@link readScopeCompileError} is now the only way this module refuses:\n * `READ_SCOPE_COMPILE_FAILED` / **500**. The status is what makes the retirement\n * safe — `rest-server.ts`'s envelope branch is 4xx-only, so a declared 5xx falls\n * through to the `ANALYTICS_QUERY_FAILED` envelope BY DECLARATION rather than by\n * nothing having been declared, and that route withholds the message of any\n * producer that declares a server fault (the full text goes to `logError`).\n *\n * ⚠️ The withhold is NOT inherited from `looksLikeInternalErrorLeak`. That\n * predicate is a heuristic over SQL/driver PHRASING, and measured, every message\n * below returns FALSE from it — so retiring the route's message list on its own\n * would have moved the policy content from a 400 body into a 500 body instead of\n * out of the response. Teaching the heuristic to recognise `[read-scope-sql]`\n * would have been more message sniffing, which is the mechanism #5367 exists to\n * remove; the route keys on the DECLARATION instead.\n *\n * The code is what a machine reads: `dispatcher-plugin.errorResponseBase`, the\n * sibling `/analytics/query` exit, puts a thrown `err.code` on the wire at\n * `error.code` (#3842), so `READ_SCOPE_COMPILE_FAILED` is legible there without\n * anyone parsing prose.\n *\n * ⚠️ At `error.code` — NOT `error.details.code`, which is where this note\n * pointed until #6123 corrected it. `errorResponseBase` only STAGES the code in\n * a `details` object; `buildApiError` then runs `splitSemanticCode`\n * (`@objectstack/runtime`, `src/error-envelope.ts:117`), which PROMOTES it into\n * the declared `ApiErrorSchema` field and returns the now-empty `details` as\n * `undefined` — so the key is omitted from the body and `error.details.code` is\n * never present to read. The measured 500 body is exactly:\n *\n * ```json\n * {\"success\":false,\"error\":{\"code\":\"READ_SCOPE_COMPILE_FAILED\",\n * \"message\":\"Internal server error\",\"httpStatus\":500}}\n * ```\n *\n * Pinned end-to-end in `@objectstack/runtime`'s\n * `analytics-query-read-scope-withhold.test.ts`, which asserts the code at\n * `error.code` against a real `AnalyticsService` on a real mounted route.\n *\n * ⚠️ Deliberately NOT a 4xx of any flavour, including a 422. Option A on the\n * decision card was `READ_SCOPE_INVALID` / 422 (\"not your fault, not a crash\");\n * it was rejected because no consumer reads a code on this path (so a new\n * vocabulary had no measured pull), because a 4xx cannot be fixed by the client\n * and therefore misreports the condition, and because 422 would have left the\n * disclosure question to be re-decided message by message.\n *\n * ## An `undefined` comparand is refused, not bound (#6125, PM ruling 2026-08-07)\n *\n * That makes ELEVEN refusing sites; the envelope above is what all eleven carry,\n * and {@link undefinedComparandError} is the eleventh. #6050 ruled on 2026-08-07\n * that `undefined` in a comparand position is refused everywhere (ruling B), and\n * implemented it on `driver-sql` / `driver-turso` — the surfaces where the shape\n * was PROVEN reachable. This module was measured in the same round and answered\n * a fourth way again: legal SQL, one bound NULL, and not a single log line.\n *\n * The #6125 ruling scoped the push-down to THIS file and kept its own envelope\n * (`READ_SCOPE_COMPILE_FAILED` / 500 — see above): a read scope is compiled by\n * the platform from CEL and stored metadata, so telling the caller to fix their\n * request would name the wrong author. `@objectstack/formula` reads the same\n * value as a THIRD semantics (\"the key is absent from the record\") and is\n * deliberately left alone — deciding it here would settle #5299's\n * key-missing-vs-value-null question as a side effect — and `driver-memory` /\n * `driver-mongodb` stay pin-only under the #5499 freeze.\n *\n * The eleventh message was measured against `looksLikeInternalErrorLeak` before\n * being added, because the section above turns on that predicate answering FALSE\n * for this family: it does for all four positions, so the new refusal is\n * withheld from the response BY DECLARATION exactly like the other ten, and no\n * message-sniffing list learns a twelfth phrase.\n *\n * ## A non-boolean `$null` / `$exists` is refused too (#6387, applying #5347 / #5369)\n *\n * TWELVE refusing sites, and {@link nonBooleanFlagComparandError} is the\n * twelfth's — ONE message for BOTH operators, because both failed the same way\n * (see that function for the measured table and the #5240 argument). #5347 and\n * #5369 refused a non-boolean comparand for these two operators on `driver-sql`;\n * #6387 measured that neither ruling had been pushed down here, where the\n * emitter still read them by plain TRUTHINESS. The consequence was sharper than\n * on the driver face: `{ $exists: \"false\" }`, written to scope to rows with NO\n * owner, compiled to `IS NOT NULL` — the rows that HAVE one. In a module whose\n * contract is fail-closed, that is a WIDENING, which is why this cell was graded\n * above #6125's silent-zero-rows one even though its reachability is narrower\n * (measured: not reachable from stored metadata — the CEL lowering emits `$null`\n * only with hard-coded booleans and `$exists` never; reachable only from an\n * in-process `getReadScope` producer).\n *\n * The twelfth site is one gate over TWO triggers, exactly like `quoteIdent`'s\n * alias-vs-field split, and the refusal-envelope inventory lists it as two rows\n * over one site for that reason. The message was measured against\n * `looksLikeInternalErrorLeak` too — FALSE, like the other eleven — so it is\n * withheld from the response BY DECLARATION and teaches no sniffing list a new\n * phrase.\n *\n * ## A `{ $field }` comparand is refused, not bound (#7598)\n *\n * THIRTEEN refusing sites, and {@link assertNoFieldReferenceComparand} is the\n * thirteenth's — the first whose shape is not wrong everywhere, only\n * uncompilable HERE. #5222 taught `driver-sql` / `driver-sqlite-wasm` to compile\n * `{ amount: { $gt: { $field: 'budget' } } }` into a same-table column-to-column\n * comparison, under four maintainer rulings (same-table only, declared-only\n * enumeration, tenant-isolation column forbidden on both sides, same comparison\n * class). This compiler was measured in the same family and answered a fifth way\n * again: it BOUND the reference object, so an admin's RLS predicate compared a\n * column against a value no row can hold — see that function for the measured\n * table and for why the four rulings cannot be enforced from here at all\n * (`StrategyContext` exposes neither an object's declared field set nor its\n * tenant-isolation column, so the enumeration the rulings turn on does not\n * exist on this side).\n *\n * ⚠️ [UPDATED — maintainer ruling 2026-08-12, #7598 Q1 = B] When this section\n * was written the refusal was the whole answer, and it said so: \"it does not\n * make the capability available\". **It does now, by getting out of the way.**\n * `NativeSQLStrategy.canHandle` DECLINES a query whose read scope carries a\n * reference, so the query falls through to the ObjectQL/engine path — where\n * `ObjectQLStrategy` ANDs the scope into the `FilterCondition` it hands\n * `engine.aggregate`, the reference reaches `driver-sql` intact, and the driver\n * compiles it under all four #5222 rulings using its own `initObjects`\n * metadata. A field-to-field RLS rule is therefore SERVED on the analytics\n * face, and the security rules live in exactly one place rather than two.\n *\n * What that leaves for the gate below is a narrower but still live job:\n * `applyReadScope` (`native-sql-strategy.ts`) no longer reaches it — the\n * decline runs first — but `ObjectQLStrategy.generateSql` does. That is the\n * `/analytics/sql` ECHO, a display string for an execution it does not perform,\n * and it has no faithful rendering of the total column-to-column predicate the\n * engine path runs. The ruling answered that face explicitly —\n * 「一致的响亮答案,不半渲染」 (one consistent, loud answer; no half-rendering) —\n * so the refusal here IS the echo's decline. `compileScopedFilterToSql` is also\n * a public export of this package (`index.ts`), so the gate additionally holds\n * for any consumer outside these two.\n *\n * ⛔ The ENVELOPE is untouched, and deliberately: Q2 = A kept the #5367 ruling\n * verbatim — `READ_SCOPE_COMPILE_FAILED` / 500 with the message withheld. No new\n * ADR-0112 code for the unsupported-rule class (option C was declined for zero\n * measured pull; #5367 recorded that no consumer reads a code on this path), and\n * no 4xx (option B reintroduces both defects #5367 closed). The paragraph above\n * beginning \"⚠️ Deliberately NOT a 4xx of any flavour\" is that ruling's own text\n * and is not to be rewritten.\n */\n\nconst IDENT = /^[a-z_][a-z0-9_]*$/i;\n\n/**\n * `READ_SCOPE_COMPILE_FAILED`, pinned against the ADR-0112 D3 ledger.\n *\n * Typed as `RegisteredErrorCode` so dropping the ledger row (or misspelling the\n * code here) fails `tsc` instead of shipping a code `ApiErrorSchema` rejects.\n */\nconst READ_SCOPE_COMPILE_FAILED: RegisteredErrorCode = 'READ_SCOPE_COMPILE_FAILED';\n\n/**\n * [#5367] A read-scope lowering failure in the ADR-0112 envelope —\n * `READ_SCOPE_COMPILE_FAILED` / 500.\n *\n * ⛔ **The only way this module refuses.** Module-local for the same reason\n * `filter-normalizer.ts`'s `invalidFilterError` is: every refusing site lives in\n * this one file, so a shared module would buy nothing and a second spelling\n * would cost the invariant. A bare `throw new Error` added below is the defect\n * returning — and a half-enveloped module is indistinguishable from an\n * unenveloped one at the HTTP boundary (the lesson #5352 paid for when seven of\n * `filter-normalizer.ts`'s nine sites stayed bare).\n *\n * The message stays whatever the refusing site says: it is for the operator's\n * log, which after #5367 is its only destination.\n */\nfunction readScopeCompileError(message: string): Error {\n const err = new Error(message) as Error & { code?: string; status?: number };\n err.code = READ_SCOPE_COMPILE_FAILED;\n err.status = 500;\n return err;\n}\n\n/**\n * The FALSE constant. `''` is this compiler's TRUE, so FALSE needs a spelling of\n * its own — `1 = 0` is already what an empty `$in` lowers to, and what\n * `driver-sql` emits for the same identity (#5243).\n */\nconst FALSE_CLAUSE = '1 = 0';\n\n/** A node the compiler can walk: a plain object, not `null` and not an array. */\nfunction isFilterNode(v: unknown): v is Record<string, unknown> {\n return v !== null && typeof v === 'object' && !Array.isArray(v);\n}\n\nfunction quoteIdent(name: string, kind: string): string {\n if (typeof name !== 'string' || !IDENT.test(name)) {\n throw readScopeCompileError(`[read-scope-sql] unsafe ${kind} identifier \"${String(name)}\" — refusing to build read scope (fail-closed).`);\n }\n return `\"${name}\"`;\n}\n\nexport function compileScopedFilterToSql(\n filter: FilterCondition,\n alias: string,\n): { sql: string; params: unknown[] } {\n const quotedAlias = quoteIdent(alias, 'alias');\n const params: unknown[] = [];\n const sql = compileNode(filter, quotedAlias, params);\n return { sql, params };\n}\n\n/**\n * Compile a child node into its OWN bind buffer.\n *\n * A group can turn out to be a boolean identity only after its children have\n * been compiled — and compiling them appends to `params`. Binding straight into\n * the parent's array and then discarding the clause would leave those values\n * behind with no `?` to consume them, shifting every later placeholder onto the\n * wrong value: a read scope that binds the wrong tenant id is worse than one\n * that is merely too wide. Buffer per child, commit only what survives.\n */\nfunction compileSub(node: unknown, qAlias: string): { sql: string; params: unknown[] } {\n const params: unknown[] = [];\n const sql = compileNode(node, qAlias, params);\n return { sql, params };\n}\n\n/** Compile a filter node into a boolean SQL expression ('' = TRUE, no constraint). */\nfunction compileNode(node: unknown, qAlias: string, params: unknown[]): string {\n if (!isFilterNode(node)) {\n throw readScopeCompileError('[read-scope-sql] read scope must be a filter object (fail-closed).');\n }\n const clauses: string[] = [];\n for (const [key, value] of Object.entries(node)) {\n if (key === '$and' || key === '$or') {\n if (!Array.isArray(value)) {\n throw readScopeCompileError(`[read-scope-sql] \"${key}\" requires an array (fail-closed).`);\n }\n if (value.length === 0) {\n // Boolean identity (#5322 ruling, 2026-08-04): the empty `$and` is the\n // AND identity — TRUE, no constraint — and the empty `$or` is the OR\n // identity — FALSE, zero rows. Until that ruling this compiler REFUSED\n // both (\"requires a non-empty array (fail-closed)\"), while the five\n // FILTER_LOGIC_CASES backends reduced them; #5322 took the reduction:\n // it is the only reading that lets a nested tree be evaluated at all\n // (a rejection cannot answer what `$and: []` means as the third branch\n // of a `$or`), and `{$or: []}` = zero rows is itself fail-closed for an\n // RLS scope — a disjunct list that loops to zero items hides every row\n // instead of exposing the table (#5134). Authoring-time loud rejection\n // of the literal spelling is tracked separately (#5330).\n if (key === '$or') clauses.push(FALSE_CLAUSE);\n continue;\n }\n const compiled = (value as unknown[]).map((child) => compileSub(child, qAlias));\n // A `''` branch is the constant TRUE. It ABSORBS a disjunction — one TRUE\n // disjunct makes the whole `$or` TRUE — so the group contributes nothing\n // rather than collapsing to its remaining branches, which would have\n // narrowed `{$or: [{}, {a: 1}]}` to `a = 1` (#5297).\n if (key === '$or' && compiled.some((c) => c.sql.length === 0)) continue;\n // For `$and` the same constant is the identity, so it just drops out.\n const kept = compiled.filter((c) => c.sql.length > 0);\n if (kept.length === 0) continue;\n for (const part of kept) params.push(...part.params);\n const joiner = key === '$and' ? ' AND ' : ' OR ';\n clauses.push(`(${kept.map((c) => c.sql).join(joiner)})`);\n } else if (key === '$not') {\n // NULL-safe negation (#5146): totalise the operand's leaves first, so\n // `NOT (…)` can never be UNKNOWN and this compiler admits the same rows\n // `driver-sql` / `driver-memory` / `formula` admit. A non-node operand is\n // left alone so `compileNode` still rejects it with its own message.\n const operand = isFilterNode(value) ? nullSafeNegationOperand(value) : value;\n const inner = compileSub(operand, qAlias);\n if (inner.sql.length === 0) {\n // `NOT TRUE ≡ FALSE`. Emitting nothing here is what let a `{$not: {}}`\n // read scope through `applyReadScope`'s `if (!sql) return;` and ran the\n // analytics query completely unscoped (#5297).\n clauses.push(FALSE_CLAUSE);\n } else {\n params.push(...inner.params);\n clauses.push(`NOT (${inner.sql})`);\n }\n } else if (key.startsWith('$')) {\n throw readScopeCompileError(`[read-scope-sql] unsupported top-level operator \"${key}\" (fail-closed).`);\n } else {\n clauses.push(compileField(key, value, qAlias, params));\n }\n }\n return clauses.join(' AND ');\n}\n\n/** Compile a single `field: value | { $op: ... }` entry. */\nfunction compileField(field: string, value: unknown, qAlias: string, params: unknown[]): string {\n const col = `${qAlias}.${quoteIdent(field, 'field')}`;\n\n // [#6125] `undefined` in a comparand position, refused before anything binds —\n // and after `quoteIdent`, so an unsafe identifier (the injection vector) keeps\n // its own message and its precedence. See {@link assertDefinedComparands} for\n // why this one call site covers the whole tree.\n assertDefinedComparands(field, value);\n\n // [#6387] …and the two comparands that are NOT positions but DOMAINS: `$null`\n // and `$exists` take a declared boolean. Deliberately a second call rather\n // than a widened first one — the two gates gate different things, and their\n // domains are disjoint by construction (`assertDefinedComparands` skips these\n // two operators by name), so neither can shadow the other's message.\n assertBooleanFlagComparands(field, value);\n\n // [#7598] …and the comparand that is neither a position nor a domain problem\n // but a CAPABILITY one: a `{ $field }` reference the SQL drivers compile since\n // #5222 and this compiler cannot. Third call rather than a widened first, for\n // the reason the second one is separate — the three gates gate different\n // things, and their triggers are disjoint by construction (a reference is\n // never `undefined`, and `$null` / `$exists` are outside this gate's operator\n // set), so none can shadow another's message. Runs AFTER both, so a\n // `{ $gt: undefined }` keeps being an undefined comparand rather than becoming\n // \"not a field reference\".\n assertNoFieldReferenceComparand(field, value);\n\n // Scalar / null → implicit equality.\n if (value === null) return `${col} IS NULL`;\n if (typeof value !== 'object' || value instanceof Date) {\n params.push(value);\n return `${col} = ?`;\n }\n if (Array.isArray(value)) {\n throw readScopeCompileError(`[read-scope-sql] bare array value for \"${field}\" — use { $in: [...] } (fail-closed).`);\n }\n\n const ops = value as Record<string, unknown>;\n const keys = Object.keys(ops);\n // A value object must be ALL operators; a non-$ key means a nested relation,\n // which a flat read scope cannot join — fail closed.\n if (keys.length === 0 || keys.some((k) => !k.startsWith('$'))) {\n throw readScopeCompileError(`[read-scope-sql] \"${field}\" has a nested/relation value which is not supported in a read scope (fail-closed).`);\n }\n\n const parts: string[] = [];\n for (const op of keys) {\n parts.push(compileOperator(col, op, ops[op], field, params));\n }\n return parts.length === 1 ? parts[0] : `(${parts.join(' AND ')})`;\n}\n\nfunction bind(params: unknown[], v: unknown): string {\n params.push(v);\n return '?';\n}\n\n/**\n * [#5567] Bind a LIKE pattern together with its escape character: `? ESCAPE ?`.\n *\n * Both are ordinary bound values, so this whole concern stays inside the\n * predicate: `applyReadScope` (`native-sql-strategy.ts`) and `generateSql`\n * (`objectql-strategy.ts`) rewrite `?` → `$N` while pushing the matching value\n * from `params`, and they carry the escape character for free — neither consumer\n * needed a change. A SQL literal `ESCAPE '\\'` would have pushed the problem up a\n * layer AND been unportable: MySQL strips one backslash inside a string literal,\n * so the literal spelling differs per dialect while a bound value does not.\n *\n * The clause is not optional decoration. SQLite honours no default escape\n * character, so the escaped pattern alone would search for a literal backslash\n * there and match nothing — the two halves are one fix (see `like-pattern.ts`).\n */\nfunction bindLike(params: unknown[], pattern: string): string {\n // Left-to-right evaluation of the template puts the pattern in `params` before\n // the escape character, which is the order the `?` appear.\n return `${bind(params, pattern)} ESCAPE ${bind(params, LIKE_ESCAPE_CHAR)}`;\n}\n\n/**\n * [#5298] Wrap a negative-polarity value test so a row whose column has no value\n * SATISFIES it: `(col IS NULL OR <test>)`.\n *\n * The read-scope twin of `driver-sql`'s `applyNullSafeNegative`, and the reason\n * this compiler had to move in the same PR rather than a later one: an RLS rule\n * is authored once and evaluated on BOTH sides — this file lowers it for the\n * read path while `formula`'s `matchesFilterCondition` evaluates it for the\n * write-side `check`. Leaving the two on different answers for `$ne` is one\n * permission rule admitting two different row sets, which is the security\n * defect #5146 named for `$not` and #5298 ruled for the rest.\n *\n * OR-expansion rather than `IS DISTINCT FROM` / `IS NOT` / `<=>`, for the three\n * reasons recorded on the driver-side twin: `NOT LIKE` has no such form, the\n * SQLite spelling depends on an engine version nothing here pins, and the\n * measured query plans are identical either way.\n *\n * The parentheses are not optional. {@link compileField} joins a field's\n * operators with bare ` AND `, so an unwrapped `col IS NULL OR …` would bind\n * looser than that AND and silently widen the whole scope.\n */\nfunction nullSafeNegative(col: string, test: string): string {\n return `(${col} IS NULL OR ${test})`;\n}\n\n/**\n * [#5234] The comparand-SHAPE gate for this door.\n *\n * `compileScopedFilterToSql` takes a `FilterCondition` that never passes through\n * `filter-normalizer`'s `fieldLeaves`, so this module needs the two checks in\n * its own right — same rule, stated once in `comparand-shape.ts`, wrapped in\n * THIS module's envelope. The envelope difference is the point: a read scope is\n * compiled from a policy, not authored by the caller, so an uncompilable\n * comparand here is a 500 fail-closed refusal (see the header) rather than a 400.\n *\n * The direction matters more here than anywhere else this rule lands. A\n * read-scope `{$nin: [{…}]}` compiled to `NOT IN ('[object Object]')`, which\n * excludes NOTHING — the scope's exclusion silently did not happen, which is\n * over-reach on a tenant/RLS predicate rather than a loose filter. That is the\n * same reading #5347 / #5324 made on this very file, and the reason the #5234\n * issue's \"fail-closed, so lower risk\" framing does not survive contact with the\n * `$nin` / `$notContains` half.\n */\nfunction assertCompilableMembers(op: string, field: string, members: unknown[]): void {\n members.forEach((member, index) => {\n if (!isBindableComparand(member)) {\n throw readScopeCompileError(`[read-scope-sql] ${unbindableListMemberMessage(op, field, member, index)}`);\n }\n });\n}\n\n/** [#5234] See {@link assertCompilableMembers}; this is the LIKE-family half. */\nfunction assertRenderableText(op: string, field: string, val: unknown): void {\n if (isRenderableTextComparand(val)) return;\n throw readScopeCompileError(`[read-scope-sql] ${unrenderableTextComparandMessage(op, field, val)}`);\n}\n\n/**\n * [#6125, PM ruling 2026-08-07] `undefined` in a COMPARAND position.\n *\n * ONE wording for all four positions #6125 measured (#5240 — one condition, one\n * wording); only `path` varies, because only the position does. What the four\n * had in common is why a shared sentence is right rather than merely shorter:\n * every one of them compiled to legal SQL with the JS value `undefined` in the\n * bind list, which the external driver renders as NULL — and every comparison\n * against NULL is UNKNOWN, so the scope matched ZERO rows and said nothing.\n *\n * Re-measured on `origin/main` (`d8e8d9cbc`) with the refusal disabled, alias\n * `t`, field `d` — the same four rows #6125's table recorded on `cba7454df`:\n *\n * | read scope | compiled to | bind list |\n * |---|---|---|\n * | `{ d: undefined }` | `\"t\".\"d\" = ?` | `[undefined]` |\n * | `{ d: { $gt: undefined } }` | `\"t\".\"d\" > ?` | `[undefined]` |\n * | `{ d: { $in: [undefined] } }` | `\"t\".\"d\" IN (?)` | `[undefined]` |\n * | `{ $not: { d: undefined } }` | `NOT ((\"t\".\"d\" IS NOT NULL AND \"t\".\"d\" = ?))` | `[undefined]` |\n *\n * ⚠️ `[undefined]`, not `[null]` — one correction to the issue's table. Nothing\n * in this package coerces it: `applyReadScope` (`native-sql-strategy.ts`) pushes\n * `scopeParams[i]` into the driver's bind array verbatim while it renumbers\n * `?` → `$N`. So the NULL is the DRIVER's reading of a JS `undefined`, which is\n * also why the same cell reads as a bare `Undefined binding(s)` crash on the\n * drivers that refuse to guess (#6050's LOCAL column). Two failure modes from\n * one bind, decided by which driver the datasource happens to be — the reason\n * this is refused at the compiler and not repaired at any one consumer.\n *\n * ⛔ What deliberately does NOT move: `null`. `{ d: null }`, `{ $eq: null }`,\n * `{ $ne: null }`, `$null` and `$exists` keep their exact lowering — `null` IS a\n * declared comparand and IS the null predicate, and the whole point of this\n * refusal is the JS value that cannot be told apart from an ABSENT key. ⚠️ Read\n * `$null` / `$exists` there as \"with their declared BOOLEAN comparand\": #6387\n * later refused every other comparand for those two, `{ $null: null }` included,\n * on the separate domain grounds {@link assertBooleanFlagComparands} states. The\n * `null` this paragraph promises not to move is `null` in a COMPARAND position,\n * which is untouched by both changes and still pinned row for row. Pinned\n * as its own control group in `read-scope-undefined-comparand.test.ts`, because\n * refusing `null` along with `undefined` is the way this change could do harm.\n *\n * ## Why the direction here is not #6050's direction\n *\n * On `driver-sql` the same shape was over-reach: `{ owner_id: ctx.user?.id }`\n * with a missing id compiled to `IS NULL` on Turso's remote transport and\n * matched every env-wide row. Here it is fail-CLOSED — zero rows, never extra\n * rows — so this is not a latent permission bypass and was not graded as one.\n * It is refused anyway because a read scope that answers a question nobody asked,\n * with no log line, is indistinguishable from one that worked: the value of this\n * change is turning silence into noise, which is exactly the grading #6125's\n * ruling recorded.\n */\nfunction undefinedComparandError(field: string, path: string): Error {\n return readScopeCompileError(\n `[read-scope-sql] comparand at ${path} is undefined — refusing to build read scope (fail-closed). ` +\n `@objectstack/spec FieldOperatorsSchema declares no undefined comparand, and in JavaScript a key ` +\n `whose value is undefined cannot be told apart from an ABSENT key — yet the two mean OPPOSITE ` +\n `things (a predicate versus no constraint at all), so there is no reading of it that is not a ` +\n `guess. It used to compile: undefined went into the bind list, the driver read it as SQL NULL, ` +\n `every comparison against NULL is UNKNOWN, and the scope matched ZERO rows in silence. Write null if the null ` +\n `predicate was meant ({ \"${field}\": null } or { \"${field}\": { \"$null\": true } }), or omit the key ` +\n `when the value is genuinely absent. The producer to fix is whoever BUILT this read scope — an ` +\n `admin-authored sharing rule / permission set, its CEL lowering, or the in-process code that ` +\n `assembled the FilterCondition — never the caller of this query, who cannot author it (#6050 ` +\n `ruling B, pushed down to this compiler by #6125).`,\n );\n}\n\n/**\n * [#6125] Refuse every `undefined` sitting in a comparand position of ONE field\n * constraint.\n *\n * The positions are enumerated rather than swept, because \"comparand\" is a\n * POSITION and not a type:\n *\n * - the DIRECT comparand — `{ d: undefined }`, the implicit `=`;\n * - an OPERATOR's comparand — `{ d: { $gt: undefined } }`, `$eq`, `$ne`, the\n * LIKE family, every other single-value operator;\n * - a MEMBER of a list operator's array — `{ d: { $in: [undefined] } }`,\n * `$nin`, `$between`. The array itself IS `$in`'s legitimate comparand;\n * each element is a comparand in its own right, which is the same split\n * {@link assertCompilableMembers} already makes.\n *\n * Three positions are deliberately NOT swept, each because this module already\n * refuses the enclosing shape with a TRUER diagnosis — #5240's rule read in the\n * direction that matters here, since a second wording for a shape that is\n * refused either way only sends the operator to the wrong repair:\n *\n * - `$null` / `$exists`. Their comparand is a declared BOOLEAN — a flag, not a\n * value to compare against — so `undefined` there is not a comparand at all.\n * `driver-sql`'s twin skips them for the same reason. ✅ [#6387] And it now\n * skips them the way that twin does: to a boolean-DOMAIN gate,\n * {@link assertBooleanFlagComparands}, which #6387 pushed down from #5347 /\n * #5369. When this note was written that gate did not exist here, so\n * `{ $null: undefined }` lowered by truthiness to `IS NOT NULL` and\n * `{ $null: \"false\" }` — the STRING, which is truthy — landed on the side\n * opposite the `false` it was written to mean. Both are refused today, and\n * `undefined` is refused there rather than here on purpose: outside the\n * declared domain is a truer diagnosis for a flag than \"this comparand\n * position is undefined\".\n * - a bare ARRAY in direct comparand position (`{ d: [1, undefined] }`).\n * {@link compileField} refuses the array as a whole (\"use `{ $in: [...] }`\"),\n * and inspecting its members here would relabel a shape refused either way.\n * - a NON-`$` key inside the constraint object (`{ owner: { manager_id:\n * undefined } }`). That is a nested relation, which {@link compileField}\n * refuses outright; answering \"the comparand is undefined\" would send the\n * operator to write `null` there, and `{ owner: { manager_id: null } }` does\n * not compile either. This is the one deliberate divergence from\n * `driver-sql`'s twin, and it comes from THIS module refusing nested\n * relations — not from a different reading of #6050.\n *\n * ## Why the call site is {@link compileField} and not a pre-pass\n *\n * `driver-sql` refuses on its separate validating walk because its emitter\n * short-circuits: a boolean identity can resolve an enclosing node before a\n * malformed sibling is ever visited, so a gate in the emitter would be\n * conditional on evaluation order. THIS compiler has no such blind spot —\n * {@link compileNode} `.map()`s every `$and`/`$or` child into its own buffer\n * BEFORE any identity is applied (the `$or` TRUE-absorption and the `$and`\n * identity filter both read the fully-compiled list), and\n * {@link nullSafeNegationOperand} rewrites a `$not` operand without dropping a\n * single leaf. Every comparand therefore reaches `compileField`, which is also\n * the only path to {@link bind} — one gate, on the one road.\n *\n * The other half of `driver-sql`'s \"runs FIRST\" argument does not transfer\n * either, and that is worth stating rather than copying: there, the refusal had\n * to precede the `$not` rewrite because the polarity tables spelled `=== null`\n * while the `$ne` emitter spelled `== null`, so the two disagreed about\n * `undefined` itself. Here {@link nullValueSatisfiesOperator},\n * {@link operatorIsNullTotal} and every arm of {@link compileOperator} spell it\n * `=== null` alike, so the tables and the emitter agree that `undefined` is \"a\n * value\" — the rewrite for a `{ $not: … }` operand runs, produces a leaf, and\n * that leaf is refused. Nothing inconsistent is being outrun; the silent NULL\n * bind is.\n */\nfunction assertDefinedComparands(field: string, spec: unknown): void {\n const root = `\"${field}\"`;\n if (spec === undefined) throw undefinedComparandError(field, root);\n if (!isFilterNode(spec)) return;\n for (const [op, opValue] of Object.entries(spec)) {\n if (!op.startsWith('$') || op === '$null' || op === '$exists') continue;\n const opPath = `${root}.${op}`;\n if (opValue === undefined) throw undefinedComparandError(field, opPath);\n if (!Array.isArray(opValue)) continue;\n opValue.forEach((member, index) => {\n if (member === undefined) throw undefinedComparandError(field, `${opPath}[${index}]`);\n });\n }\n}\n\n/**\n * [#6387, applying #5347 / #5369] `$null` / `$exists` whose comparand is not a\n * boolean.\n *\n * ## ONE wording for BOTH operators (#5240), and why that is right here\n *\n * `driver-sql` gives its twins two messages, because each names the direction\n * ITS OWN emitter defaulted to and those directions differ. This module had one\n * emitter rule covering both — plain TRUTHINESS — so both operators failed the\n * same way, in the same sentence, and #5240's rule applies in the direction it\n * usually does: one condition, one wording. Only the operator NAME and the\n * `path` vary, and `read-scope-boolean-flag-comparand.test.ts` pins that \"only\n * those vary\" so a later change cannot give one of them a bespoke phrasing.\n *\n * ## What it used to do — measured on `origin/main` (`5faa23ca3`), alias `t`\n *\n * The emitter read `val ? … : …`, so every non-boolean was sorted by JS\n * truthiness into one of the two declared answers:\n *\n * | read scope | compiled to | |\n * |---|---|---|\n * | `{ owner_id: { $null: \"false\" } }` | `\"t\".\"owner_id\" IS NULL` | ⛔ the OPPOSITE of what was written |\n * | `{ owner_id: { $null: \"true\" } }` | `\"t\".\"owner_id\" IS NULL` | |\n * | `{ owner_id: { $null: 0 } }` | `\"t\".\"owner_id\" IS NOT NULL` | |\n * | `{ owner_id: { $null: null } }` | `\"t\".\"owner_id\" IS NOT NULL` | |\n * | `{ owner_id: { $null: undefined } }`| `\"t\".\"owner_id\" IS NOT NULL` | |\n * | `{ owner_id: { $exists: \"false\" } }`| `\"t\".\"owner_id\" IS NOT NULL` | ⛔ the OPPOSITE of what was written |\n * | `{ owner_id: { $exists: 0 } }` | `\"t\".\"owner_id\" IS NULL` | |\n * | `{ owner_id: { $exists: \"no\" } }` | `\"t\".\"owner_id\" IS NOT NULL` | |\n *\n * The string `\"false\"` is TRUTHY, so the two rows marked ⛔ are the ones that\n * matter: a scope written to say \"rows with NO owner\" compiled to \"rows that\n * HAVE one\". Unlike #6125's cell — which was fail-CLOSED, zero rows, merely\n * silent — this direction ADMITS the rows the policy meant to exclude, in a\n * module whose own contract is \"a read-scope predicate must never be silently\n * dropped\". That is why the disposition needed no new judgement: #5347 (`$null`)\n * and #5369 (`$exists`) already refused this shape on `driver-sql`, and their\n * stated reason transfers word for word.\n *\n * ## ⚠️ Reachability, measured — and the half that came back NEGATIVE\n *\n * #6387 asked for a decisive answer to \"can `{ $null: <non-boolean> }` travel\n * from STORED metadata to this compiler\". Measured on `5faa23ca3`, it cannot —\n * three independent gates close that road, and this is recorded because the\n * issue's severity argument rested on it:\n *\n * 1. `RowLevelSecurityPolicySchema` declares `using` / `check` as `z.string()`\n * — a CEL predicate, not a `FilterCondition`. A stored object is rejected\n * at write (\"expected string, received object\").\n * 2. The CEL lowering never emits this shape. `@objectstack/formula`'s\n * `cel-to-filter.ts` emits `$null` at exactly two sites, both with a\n * HARD-CODED boolean (`== null` → `{ $null: true }`, `!= null` →\n * `{ $null: false }`), and emits `$exists` nowhere at all. An unresolved\n * `current_user.*` yields `unresolved-variable` → the policy drops → the\n * deny sentinel, never a stray comparand.\n * 3. Even bypassing the schema, a raw object predicate throws inside\n * `sqlPredicateToCel` (`expression.replace is not a function`), and\n * `getReadFilter`'s catch turns that into `RLS_DENY_FILTER`. A JSON STRING\n * of a FilterCondition stores fine and then fails to parse as CEL → `null`\n * → deny. Both roads end fail-closed.\n *\n * The other read-scope producers cannot emit it either: the Layer 0 tenant\n * filter, `plugin-sharing`'s `buildReadFilter` (`{owner: id}` / `$in` / `$or` /\n * `{id:'__deny_all__'}`), the controlled-by-parent filter (`{fk: {$in: […]}}`)\n * and `RLS_DENY_FILTER` contain no `$null` or `$exists` at all.\n *\n * ⚠️ What IS open, and why this gate is still worth having: `getReadScope` is a\n * DOCUMENTED public option on `AnalyticsPluginOptions` (`plugin.ts`), so a host\n * that supplies its own read scope — from JSON config, or from JS where the\n * `FilterCondition` type is not checked — is a live producer with no gate\n * between it and here. #6387 also confirmed the issue's other measurement:\n * `plugin-security` performs no `FilterConditionSchema` / `safeParse` anywhere\n * on this path. So the shape is not reachable from stored metadata TODAY, and\n * nothing structural stops the next producer; refusing it at the compiler is\n * what makes \"declared boolean\" mean enforced boolean regardless of who writes\n * the scope. Graded on that measurement, not on the issue's opening wording.\n */\nfunction nonBooleanFlagComparandError(op: string, field: string, path: string): Error {\n return readScopeCompileError(\n `[read-scope-sql] comparand for \"${op}\" at ${path} is not a boolean — refusing to build read scope ` +\n `(fail-closed). @objectstack/spec FieldOperatorsSchema declares both $null and $exists as ` +\n `z.boolean(), and this compiler used to read the comparand by TRUTHINESS instead — so a ` +\n `non-boolean was silently sorted into one of the two declared answers rather than refused. The ` +\n `string \"false\" is TRUTHY, which is the case that matters: it landed on the side OPPOSITE the ` +\n `false it was written to mean, turning \"rows with no ${field}\" into \"rows that have one\" — a ` +\n `read scope that ADMITS the rows the policy excludes. Write the boolean itself (true or false), ` +\n `not a string, a number, null or undefined. The producer to fix is whoever BUILT this read ` +\n `scope — an admin-authored sharing rule / permission set, its CEL lowering, or the in-process ` +\n `code (a getReadScope option) that assembled the FilterCondition — never the caller of this ` +\n `query, who cannot author it (#5347 / #5369, pushed down to this compiler by #6387).`,\n );\n}\n\n/**\n * [#6387] Refuse a non-boolean `$null` / `$exists` comparand on ONE field\n * constraint.\n *\n * `hasOwnProperty` rather than `in`, so an inherited key can never trip the\n * gate, and rather than `Object.hasOwn` to match `driver-sql`'s twin\n * (`reduceFilterKey`) line for line. `{ $null: undefined }` DOES count: the key\n * is own and enumerable, and `undefined` is one of the comparands #6387\n * measured a flip on — it lowered to `IS NOT NULL`, which\n * `read-scope-undefined-comparand.test.ts` pinned as \"the cell #6125\n * deliberately left alone\". This is the ruling that picks it up. Refusing it\n * here rather than in {@link assertDefinedComparands} keeps that gate's claim\n * honest — `undefined` is refused as a value OUTSIDE the declared BOOLEAN\n * DOMAIN, which is a truer diagnosis than \"a comparand position is undefined\"\n * for a flag that was never a comparand position.\n *\n * ## Why this call site, and not the `$not` pre-pass\n *\n * Same reason {@link assertDefinedComparands} sits here: {@link compileField} is\n * the one road every field constraint travels, because {@link compileNode}\n * `.map()`s every child into its own buffer BEFORE any boolean identity is\n * applied, so no sibling can absorb a malformed one. It runs AFTER\n * {@link nullSafeNegationOperand} for a `$not` operand — harmless, and worth\n * stating: that rewrite consults {@link nullValueSatisfiesOperator}, which now\n * reads these two by identity, so a non-boolean is classified before it is\n * refused. The classification is DISCARDED either way (the leaf still reaches\n * `compileField` and still throws), and the rewrite's own synthesised leaves\n * (`{ $null: false }`, `{ $null: true }`) are literal booleans by construction.\n */\nfunction assertBooleanFlagComparands(field: string, spec: unknown): void {\n if (!isFilterNode(spec)) return;\n for (const op of ['$null', '$exists'] as const) {\n if (!Object.prototype.hasOwnProperty.call(spec, op)) continue;\n if (typeof spec[op] === 'boolean') continue;\n throw nonBooleanFlagComparandError(op, field, `\"${field}\".${op}`);\n }\n}\n\n/**\n * [#7598] A `{ $field: 'col' }` reference in a comparand position this compiler\n * BOUND instead of refusing — the THIRTEENTH refusing site, and the first one\n * whose shape is executed correctly somewhere else.\n *\n * ## The measured cell, on `origin/main` (`5823d593d`), alias `person`\n *\n * | read scope | compiled to | bind list |\n * |---|---|---|\n * | `{ amount: { $gt: { $field: 'budget' } } }` | `\"person\".\"amount\" > ?` | `[{\"$field\":\"budget\"}]` |\n * | `{ amount: { $eq: { $field: 'budget' } } }` | `\"person\".\"amount\" = ?` | `[{\"$field\":\"budget\"}]` |\n *\n * The reference OBJECT goes into the bind list verbatim — `applyReadScope`\n * (`native-sql-strategy.ts`) pushes `scopeParams[i]` into the driver's array\n * while it renumbers `?` → `$N`, exactly as #6125 measured for `undefined`. What\n * the driver then does with a plain object is its own business: JSON text on the\n * `toSqlBindValue` drivers, a bare `Undefined binding(s)`-class crash on the ones\n * that refuse to guess. Either way an admin's RLS predicate compared a column\n * against a value no row can hold, silently. In a module whose contract is\n * \"a read-scope predicate must never be silently dropped\" a predicate that is\n * silently MEANINGLESS is the same defect one step further on — and unlike\n * #6125's cell it is not reliably fail-closed, because the comparison it\n * degrades to depends on the driver rather than on the scope.\n *\n * ## Which positions this gate covers, and why the others keep their wording\n *\n * ONLY the positions that were BOUND: the whole comparand of the six scalar\n * comparison operators ({@link CROSS_FIELD_COMPARISON_OPERATORS}) and the two\n * `$between` endpoints. Everything else a reference can occupy already refused\n * here BEFORE this change, with a diagnosis of its own, and each of those\n * refusals converges with `driver-sql`'s own #5222 refusal arm — so widening\n * this gate over them would restate a rule that is already right in a second\n * wording (#5240, read in the direction that matters: a second sentence for a\n * shape refused either way only sends the operator to the wrong repair):\n *\n * - the LIKE family → {@link assertRenderableText} (\"matches against the TEXT\n * of a pattern\"), and `driver-sql` refuses a reference there too — a\n * column-side LIKE pattern cannot be metacharacter-escaped portably;\n * - `$in` / `$nin` members → {@link assertCompilableMembers} (\"cannot be bound\n * as a SQL parameter\"), and `driver-sql` refuses those members as well,\n * because the memory evaluator does not resolve a reference inside a list\n * either;\n * - a bare `{ field: { $field: … } }` → `unsupported operator \"$field\"` from\n * {@link compileOperator}'s default arm.\n *\n * `$between` is in the covered set even though {@link assertCompilableMembers}\n * would also refuse its endpoints, because this gate runs FIRST and the truer\n * diagnosis wins: \"a range bound may not be a field reference on any backend\"\n * tells the policy author what to write, where \"cannot be bound as a SQL\n * parameter\" describes a consequence of the shape rather than the shape. The two\n * covered positions now say DIFFERENT sentences, because the 2026-08-12 ruling\n * made them different conditions — see {@link fieldReferenceComparandMessage}\n * (a rendering boundary on a rule the platform SERVES) versus\n * {@link fieldReferenceBetweenBoundMessage} (a position refused everywhere, and\n * removed from the spec by #7596).\n *\n * ## What reaches this gate after the 2026-08-12 ruling\n *\n * Not `applyReadScope`. `NativeSQLStrategy.canHandle` declines a query whose\n * read scope carries a scalar reference before that method runs, so the scope\n * is served on the engine path instead (module header). What DOES reach it is\n * `ObjectQLStrategy.generateSql` — the `/analytics/sql` echo — plus any external\n * consumer of the `compileScopedFilterToSql` export. The gate is therefore the\n * echo's decline, which is what the ruling asked that face for.\n *\n * ## Envelope: unchanged, deliberately (#5367 ruling 2026-08-06, re-affirmed as\n * #7598 Q2 = A on 2026-08-12)\n *\n * `READ_SCOPE_COMPILE_FAILED` / 500, like the other twelve. The two arguments\n * #5367 gave apply to this shape verbatim rather than by analogy: the producer\n * is an ADMIN-authored sharing rule / permission set and its CEL lowering —\n * `compileCelToFilter` is exactly what emits `{ $field: path }` — so a 4xx would\n * bill the caller for a document they cannot author, and a 4xx echoes the\n * message, which here names the POLICY's field names. #7598 put the question to\n * the maintainer and it was answered A: keep #5367 as it stands, add no new\n * ADR-0112 code for the unsupported-rule class (option C had zero measured pull\n * — #5367 recorded that no consumer reads a code on this path — and a zero-pull\n * vocabulary is recorded, not built), and do not move to 4xx.\n */\nfunction assertNoFieldReferenceComparand(field: string, spec: unknown): void {\n if (!isFilterNode(spec)) return;\n for (const [op, opValue] of Object.entries(spec)) {\n if (CROSS_FIELD_COMPARISON_OPERATORS.has(op) && isFieldReference(opValue)) {\n throw readScopeCompileError(\n `[read-scope-sql] ${fieldReferenceComparandMessage(op, field, opValue.$field)}`,\n );\n }\n if (op !== '$between' || !Array.isArray(opValue)) continue;\n opValue.forEach((member, index) => {\n if (!isFieldReference(member)) return;\n throw readScopeCompileError(\n `[read-scope-sql] ${fieldReferenceBetweenBoundMessage(op, field, member.$field, index)}`,\n );\n });\n }\n}\n\nfunction compileOperator(col: string, op: string, val: unknown, field: string, params: unknown[]): string {\n switch (op) {\n case '$eq': return val === null ? `${col} IS NULL` : `${col} = ${bind(params, val)}`;\n // [#5298] `$ne: null` stays `IS NOT NULL` — already total, and \"has any\n // value\" is false for a row that has none. Only the comparison is guarded.\n case '$ne': return val === null ? `${col} IS NOT NULL` : nullSafeNegative(col, `${col} <> ${bind(params, val)}`);\n case '$gt': return `${col} > ${bind(params, val)}`;\n case '$gte': return `${col} >= ${bind(params, val)}`;\n case '$lt': return `${col} < ${bind(params, val)}`;\n case '$lte': return `${col} <= ${bind(params, val)}`;\n case '$in': {\n if (!Array.isArray(val)) throw readScopeCompileError(`[read-scope-sql] $in for \"${field}\" needs an array (fail-closed).`);\n if (val.length === 0) return FALSE_CLAUSE; // IN () matches nothing — safe\n assertCompilableMembers(op, field, val);\n return `${col} IN (${val.map((v) => bind(params, v)).join(', ')})`;\n }\n case '$nin': {\n if (!Array.isArray(val)) throw readScopeCompileError(`[read-scope-sql] $nin for \"${field}\" needs an array (fail-closed).`);\n if (val.length === 0) return '1 = 1'; // NOT IN () excludes nothing\n assertCompilableMembers(op, field, val);\n // [#5298] NULL-safe: \"not among this list\" holds vacuously for a value\n // that is not there.\n return nullSafeNegative(col, `${col} NOT IN (${val.map((v) => bind(params, v)).join(', ')})`);\n }\n case '$between': {\n if (!Array.isArray(val) || val.length !== 2) throw readScopeCompileError(`[read-scope-sql] $between for \"${field}\" needs [min,max] (fail-closed).`);\n assertCompilableMembers(op, field, val);\n return `${col} BETWEEN ${bind(params, val[0])} AND ${bind(params, val[1])}`;\n }\n // [#5567] The comparand is a LITERAL, so it is escaped and the escape\n // character is bound with it. See {@link bindLike}.\n // [#5234] …and it must be a value `String()` can render, which is asserted\n // BEFORE `likePattern` sees it — see {@link assertRenderableText}.\n case '$contains': assertRenderableText(op, field, val); return `${col} LIKE ${bindLike(params, likePattern('contains', val))}`;\n /**\n * [#6520] `$icontains` on the READ-SCOPE lowering — the one compiler in this\n * package where a wrong answer is an ADR-0021 scope over-reach rather than a\n * loose chart filter, which is why the fold is the spec's ruled one and not\n * `LOWER()`.\n *\n * `assertRenderableText` first, exactly as its case-exact twin above: the\n * comparand has to be something `String()` renders faithfully before a\n * pattern is built from it (#5234).\n *\n * The fold wraps BOTH the column and the bound pattern. Folding one side\n * only would compare a folded needle against a raw column — matching just\n * the rows already lower-case — and on a read scope that is a row set the\n * policy author never wrote, in the narrowing direction here but in the\n * WIDENING direction under a `$not`.\n */\n case '$icontains': {\n assertRenderableText(op, field, val);\n // The two binds are spelled out rather than taken from `bindLike`, because\n // only the PATTERN placeholder is folded and the `ESCAPE` one must not be.\n // Left-to-right, so the values land in `params` in placeholder order —\n // the ordering invariant `bindLike`'s own comment states.\n const patternRef = asciiLowerSqlExpr(bind(params, likePattern('contains', val)));\n return `${asciiLowerSqlExpr(col)} LIKE ${patternRef} ESCAPE ${bind(params, LIKE_ESCAPE_CHAR)}`;\n }\n // [#5298] NULL-safe: `NOT LIKE` is UNKNOWN for a NULL column, and \"does not\n // contain\" is true of a value that is not there.\n case '$notContains': assertRenderableText(op, field, val); return nullSafeNegative(col, `${col} NOT LIKE ${bindLike(params, likePattern('contains', val))}`);\n case '$startsWith': assertRenderableText(op, field, val); return `${col} LIKE ${bindLike(params, likePattern('starts', val))}`;\n case '$endsWith': assertRenderableText(op, field, val); return `${col} LIKE ${bindLike(params, likePattern('ends', val))}`;\n // [#6387] `val` is a boolean here — {@link assertBooleanFlagComparands}\n // refused anything else at {@link compileField}, before this emitter runs.\n // So `=== true` is an exhaustive TWO-WAY choice over the declared domain,\n // not the \"anything truthy is IS NULL\" rule it used to be. That old rule is\n // what put the STRING `\"false\"` on the side opposite the `false` it was\n // written to mean; the identity spelling cannot, and it is the spelling\n // {@link nullValueSatisfiesOperator} now mirrors (#5146 / #5298).\n case '$null': return val === true ? `${col} IS NULL` : `${col} IS NOT NULL`;\n case '$exists': return val === true ? `${col} IS NOT NULL` : `${col} IS NULL`;\n default:\n throw readScopeCompileError(`[read-scope-sql] unsupported operator \"${op}\" on \"${field}\" (fail-closed).`);\n }\n}\n\n// ── [#5146] NULL-safe `$not` ─────────────────────────────────────────────────\n\n/**\n * What one field constraint needs so its compiled SQL is TOTAL — TRUE or FALSE\n * for every row, never UNKNOWN.\n *\n * - `'none'` — already total (`IS NULL` / `IS NOT NULL`), or a shape\n * this compiler refuses outright, which must keep refusing.\n * - `'requireValue'` — a NULL column does NOT satisfy it: `col IS NOT NULL AND (…)`.\n * - `'allowNull'` — a NULL column DOES satisfy it: `col IS NULL OR (…)`.\n */\ntype NullGuard = 'none' | 'requireValue' | 'allowNull';\n\n/**\n * Does a NULL column satisfy this one operator, under the semantics the JS\n * backends (`driver-memory`'s `match`, `formula`'s `matchesFilterCondition`)\n * give it? They evaluate a missing value in ordinary two-valued JS — `undefined\n * !== 'won'` is simply `true` — and #5146 ruled that answer canonical.\n *\n * This is `sql-driver.ts`'s `nullValueSatisfiesOperator` table, entry for entry,\n * with ONE deliberate difference that comes from THIS file's emitter rather than\n * from a different reading of #5146:\n *\n * - `$between` exists in this compiler and not in that table; it is a\n * positive comparison, so it takes the default (a value that is not there\n * does not lie between two bounds) exactly as the other comparisons do.\n *\n * ⚠️ [#6387] There used to be a SECOND difference, and its removal is half of\n * that change rather than a tidy-up. `$null` / `$exists` were read here by\n * TRUTHINESS — `Boolean(value)` / `!value` — because {@link compileOperator}\n * wrote them as `val ? … : …`, while `driver-sql` read them by identity because\n * its emitter did. That was correct under the invariant #5146 / #5298 state:\n * each polarity table pins the spelling of ITS OWN emitter, not the other\n * file's. So when the emitter stopped guessing at a non-boolean, these two arms\n * had to move WITH it in the same change — leaving them truthy would have\n * broken the invariant silently, at its own definition, with nothing red. The\n * divergence is gone now because its cause is: both emitters read the declared\n * boolean domain, so both tables spell it by identity, and the two files agree\n * on every arm for the first time.\n *\n * The default is the large positive-comparison family (`$gt`/`$in`/`$contains`/\n * …), every member of which answers `false` for a value that is not there. An\n * operator this compiler does not support also lands here; it is guarded and\n * then still throws from {@link compileOperator}, so fail-closed is preserved.\n */\nfunction nullValueSatisfiesOperator(op: string, value: unknown): boolean {\n switch (op) {\n // `$eq: null` IS the null predicate; any other comparand is a value test.\n case '$eq': return value === null;\n // Mirror image: `$ne: null` compiles to `IS NOT NULL`, which a NULL fails.\n case '$ne': return value !== null;\n // [#6387] Identity, matching this file's emitter (see the note above).\n // `assertBooleanFlagComparands` refuses anything but `true` / `false` before\n // this table is consulted, so each arm is an exhaustive TWO-WAY choice over\n // the declared domain — and the strict spelling is chosen over the lenient\n // one it replaces for the reason #5347 gave: `Boolean(value)` and\n // `value === true` are equivalent only while the gate upstream holds, and\n // the lenient spelling would quietly resume answering for shapes nobody\n // ruled on if that gate were ever moved. A NULL column satisfies `$null`\n // exactly when the author asked for null…\n case '$null': return value === true;\n // …and satisfies `$exists` exactly when the author asked for \"no value\".\n // `$null: true` and `$exists: false` are the same question, so these two\n // arms are correctly each other's MIRROR, not each other's copy (#5369).\n case '$exists': return value === false;\n // Negative-polarity set / substring tests hold vacuously for an absent value.\n case '$nin': return true;\n // `$notContains` is the one operator where the two JS backends disagree for\n // a null-valued field (`driver-memory` answers false, `formula` true).\n // `formula` is followed because `driver-sql` follows it, so this compiler\n // does not cast a vote on a disagreement that is filed elsewhere.\n case '$notContains': return true;\n default: return false;\n }\n}\n\n/** Is this operator's compiled SQL already total for a NULL column? */\nfunction operatorIsNullTotal(op: string, value: unknown): boolean {\n switch (op) {\n // Compile to `IS NULL` / `IS NOT NULL` — two-valued by construction.\n case '$null':\n case '$exists':\n return true;\n // A null comparand makes these null PREDICATES too, not comparisons.\n case '$eq':\n case '$ne':\n return value === null;\n default:\n return false;\n }\n}\n\n/**\n * The guard one field constraint needs. A constraint is the AND of its\n * operators, so it is total when every operator is, and a NULL column satisfies\n * it only when it satisfies all of them.\n */\nfunction nullGuardForFieldSpec(spec: unknown): NullGuard {\n // `{ field: null }` compiles to `IS NULL` — already total.\n if (spec === null) return 'none';\n // A scalar / Date is an implicit `=`; a NULL column fails it. A bare array is\n // REFUSED by `compileField`; classifying it here keeps that refusal reachable\n // (the unrewritten `{field: […]}` conjunct still throws its own message).\n if (typeof spec !== 'object' || spec instanceof Date || Array.isArray(spec)) return 'requireValue';\n const entries = Object.entries(spec as Record<string, unknown>);\n // `{ field: {} }` and any non-`$` key are shapes `compileField` throws on.\n // Passing them through unrewritten is what preserves the exact error; a guard\n // wrapped around them would only change which message the caller sees.\n if (entries.length === 0) return 'none';\n let total = true;\n let nullSatisfies = true;\n for (const [op, value] of entries) {\n if (!operatorIsNullTotal(op, value)) total = false;\n if (!nullValueSatisfiesOperator(op, value)) nullSatisfies = false;\n }\n if (total) return 'none';\n return nullSatisfies ? 'allowNull' : 'requireValue';\n}\n\n/**\n * [#5146] Rewrite the operand of a `$not` so every leaf compiles to a TOTAL\n * predicate — which is what makes `NOT (…)` mean here what it means in\n * `driver-memory`, `formula` and (since #5296) `driver-sql`.\n *\n * # Why the guard rides the LEAF, not the `NOT`\n *\n * For a flat operand `NOT (a IS NOT NULL AND a = ?)` and `NOT (a = ?) OR a IS\n * NULL` are the same predicate. They stop being the same as soon as the operand\n * nests: hoisting the guard above a `$not` whose operand is a `$or` re-admits\n * rows the JS backends exclude — a NULL `a` would satisfy the whole negation\n * even when the `$or`'s OTHER branch is satisfied. Totalising each leaf makes\n * the rewrite compositional instead: De Morgan is sound over two-valued leaves,\n * so `$and`, `$or` and a nested `$not` all stay correct with no special cases.\n * On an RLS lowering that difference is rows a policy excludes becoming visible,\n * so it is the whole reason this is a rewrite and not a suffix.\n *\n * # Why polarity is per operator\n *\n * A blanket `OR col IS NULL` would WIDEN the negative-polarity operators:\n * `{$not: {a: {$ne: 5}}}` means \"a is 5\", and both JS backends exclude a NULL\n * row from it. Adding an unconditional null escape there would hand back exactly\n * the rows the scope excludes. So each leaf is guarded in the direction its own\n * operator answers, per {@link nullValueSatisfiesOperator}.\n *\n * The rewrite runs ONLY inside a `$not`; an ordinary comparison's SQL is\n * untouched, so nothing outside a negation changes shape. A nested `$not` is\n * left alone on purpose — its own branch totalises its operand, and\n * `NOT <total>` is itself total, so recursing would stack a redundant guard on\n * the same column.\n */\nfunction nullSafeNegationOperand(node: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n const guarded: unknown[] = [];\n for (const [key, value] of Object.entries(node)) {\n if ((key === '$and' || key === '$or') && Array.isArray(value)) {\n // A non-node element is passed through so `compileNode` still rejects it.\n out[key] = value.map((element) => (isFilterNode(element) ? nullSafeNegationOperand(element) : element));\n continue;\n }\n if (key.startsWith('$')) {\n // `$not` (handled by its own branch) and anything else `$`-prefixed keep\n // whatever this compiler does with them today — the rewrite rules on NULL,\n // not on the operator vocabulary, and an unknown one must still throw.\n out[key] = value;\n continue;\n }\n const guard = nullGuardForFieldSpec(value);\n if (guard === 'none') {\n out[key] = value;\n } else if (guard === 'requireValue') {\n // `col IS NOT NULL AND (…)` — both conjuncts of the enclosing node.\n guarded.push({ [key]: { $null: false } }, { [key]: value });\n } else {\n // `col IS NULL OR (…)` — one conjunct, so the OR binds tighter than the\n // AND this node's keys form.\n guarded.push({ $or: [{ [key]: { $null: true } }, { [key]: value }] });\n }\n }\n if (guarded.length > 0) {\n const existing = Array.isArray(out.$and) ? out.$and : [];\n out.$and = [...existing, ...guarded];\n }\n return out;\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * [#5367] The dataset refusals this service raises, in the ADR-0112 envelope.\n *\n * ## Why this file exists\n *\n * `/analytics/dataset/query` classifies a thrown error by reading its `code` +\n * 4xx `status` (#5352 / PR #5366). Five refusals in this package were still bare\n * `throw new Error(…)`, so the route could not read them at all — and they only\n * kept answering `400 DATASET_INVALID` because the catch carried a hardcoded\n * list of message SUBSTRINGS as a transitional fallback:\n *\n * ```\n * /not declared in the dataset|not backed by a declared relationship|\n * not supported by the v1 dataset runtime|read-scope-sql|\n * not a selected dimension or measure|is not a subset of the selected dimensions/\n * ```\n *\n * Prime Directive #12 allows an accommodation like that only while it is\n * declared, loud, tested **and removable on a schedule**. #5366 delivered the\n * first three and nothing carried the fourth, which made the HTTP status of five\n * error families a property of their WORDING: rephrasing\n * \"is not declared in the dataset's `include`\" — no logic change, no test red, no\n * gate red — silently moved that refusal from 400 to 500, i.e. re-opened #5352\n * for a different family. #5367 is that schedule; this constructor is how the\n * five families leave the list.\n *\n * ## The envelope, and why it is `DATASET_INVALID` / 400\n *\n * Same shape as `filter-normalizer.ts`'s `invalidFilterError`\n * (`INVALID_FILTER` / 400) and `analytics-service.ts`'s dimension/measure gates\n * (`INVALID_FIELD` / 400): the code names the caller-shaped mistake, the status\n * says whose fault it is, and the message stays whatever the refusing site says.\n * `DATASET_INVALID` is not a new code — it is what the route's fallback list has\n * answered for these five families since #5352, registered in\n * `ERROR_CODE_LEDGER` (ADR-0112 D3). Producing it HERE, rather than deriving it\n * there from message text, is the whole change: one condition, one wire shape,\n * chosen by the producer that knows.\n *\n * The `RegisteredErrorCode` annotation is load-bearing rather than decorative —\n * it is what makes an unregistered code a compile error instead of a string that\n * only fails when some route happens to parse its own response body.\n *\n * ## [#5716] The second constructor, and how to choose between them\n *\n * #5352 named six refusal families and #5367 enveloped five of them. Reading\n * every `throw` in this package afterwards turned up NINE more sites of exactly\n * the same kind — caller- or author-shaped refusals that never entered the\n * route's message list at all, so they were answering `500` with nobody's\n * regex to rescue them — plus the `objectql-strategy.ts` `planCrossObject`\n * family (PM ruling on #5716, 2026-08-06). What decides the CODE is not which\n * file throws but what the refusal is a verdict ABOUT:\n *\n * - {@link datasetInvalidError} — a verdict about the DATASET or the whole\n * SELECTION: an `include` path that cannot be joined, an aggregate v1 cannot\n * lower, a `compareTo` with no window to shift, a `dateRange` that is not a\n * date. The caller fixes the dataset definition or the selection.\n * - {@link invalidMemberError} — a verdict about ONE MEMBER the request named:\n * a measure the cube does not declare, a member this engine cannot join to.\n * The caller fixes (or drops) that member.\n *\n * The member family is `INVALID_FIELD` / 400 rather than a second\n * `DATASET_INVALID` for two measured reasons. First, the three shipped analytics\n * gates already answer `INVALID_FIELD` / 400 to the NEIGHBOURING member-level\n * mistakes on the very same request keys — `measures` (#4437), `dimensions` /\n * `timeDimensions` (#5520), `where` (#5669) — so a caller who mistypes a member\n * and a caller who names one the engine cannot serve would otherwise get two\n * wire shapes for one class of mistake, which is the defect ADR-0112 exists to\n * remove. Second, these sites are NOT dataset-only: `planCrossObject` and the\n * undeclared-measure refusal fire on `/analytics/query` too, where there is no\n * dataset at all — `DATASET_INVALID` would name a document the caller never\n * sent, while `INVALID_FIELD` reads correctly on both faces.\n *\n * ## What deliberately does NOT go through here\n *\n * Not every `throw` in this package is the caller's mistake, and enveloping one\n * that isn't would be the mirror-image defect — an internal fault re-labelled\n * `400`, which hides it from ops alerting and tells the author to fix something\n * they did not write. Three families are deliberately NOT `DATASET_INVALID`:\n *\n * - **`read-scope-sql.ts`'s ten fail-closed refusals** — a SERVER fault, and\n * since the maintainer's 2026-08-06 ruling they say so: that module's own\n * `readScopeCompileError` gives all ten `READ_SCOPE_COMPILE_FAILED` / **500**.\n * Its inputs are an RLS `FilterCondition` the security service compiled from\n * an admin-authored policy and a join alias the dataset compiler generated —\n * neither is caller input, so `400` both misattributed the fault and echoed\n * policy field names back to the tenant. (This bullet said \"stays bare,\n * verdict pending\" until that ruling; the route's message list is now gone\n * entirely and #5367's retirement schedule is paid off.)\n * - **Internal invariants** — e.g. `dataset-compiler.ts`'s \"non-derived measure\n * has no aggregate\", which the spec refinement already guarantees. An\n * arrival there is our bug; an undeclared `500` is the honest answer, and\n * staying bare keeps it readable in the response (#5667's tiering) instead of\n * withheld like a declared server fault. [#5716] `native-sql-strategy.ts`'s\n * \"measure … has unrecognised type\" joins this bullet after measurement, and\n * against #5716's own list, which had it down as author-shaped: `Metric.type`\n * is the CLOSED `AggregationMetricType` enum, `metric-type-coverage.test.ts`\n * pins that every member of it is handled (its second case is literally \"leaves\n * no metric type to the unrecognised-type throw\"), the dataset compiler maps\n * only `SUPPORTED_AGGREGATES` into a cube, and `inferMeasure` mints six known\n * types. So no spec-valid cube can reach it — an arrival is our own drift or a\n * host registering an unparsed cube object, which is the same 500 tier as the\n * line above, not the author's 400.\n * - **Producer/consumer drift between two of OUR tables** — the posture\n * `objectql-strategy.ts`'s display-SQL renderer already states explicitly\n * (\"Deliberately NOT `invalidFilterError`'s 400 envelope: this is drift\n * between two of our own tables, not a caller-shaped mistake\", #5333).\n *\n * So this module is deliberately NOT \"the only way this package refuses\" — the\n * claim `invalidFilterError` can make about `filter-normalizer.ts`, and\n * `readScopeCompileError` about `read-scope-sql.ts`. It is the way this package\n * refuses **the caller**.\n */\n\nimport type { RegisteredErrorCode, StandardErrorCode } from '@objectstack/spec/api';\n\n/**\n * `DATASET_INVALID`, pinned against the ledger.\n *\n * Typed as `RegisteredErrorCode` so removing the ledger row (or misspelling the\n * code here) fails `tsc` rather than shipping a code `ApiErrorSchema` rejects.\n */\nconst DATASET_INVALID: RegisteredErrorCode = 'DATASET_INVALID';\n\n/**\n * [#5716] `INVALID_FIELD`, pinned against the STANDARD catalog.\n *\n * Same load-bearing annotation as `DATASET_INVALID` above, one tier over: this\n * code is platform-wide (`StandardErrorCode`), not registered per package, which\n * is precisely why the member-level refusals use it — see the module header.\n */\nconst INVALID_FIELD: StandardErrorCode = 'INVALID_FIELD';\n\n/**\n * [#5716] Which request key named the member — the analytics vocabulary, spelled\n * exactly as the shipped source-field gates spell it in `err.param`.\n */\nexport type AnalyticsRequestKey = 'measures' | 'dimensions' | 'timeDimensions' | 'where';\n\n/**\n * A dataset refusal in the ADR-0112 envelope — `DATASET_INVALID` / 400.\n *\n * Use it for a refusal the CALLER can fix by changing the request or the dataset\n * definition they authored: a selection that names something the dataset does not\n * declare, a dataset whose fields traverse an undeclared relationship, an\n * aggregate the v1 runtime does not implement. See this module's header for the\n * families that deliberately stay bare `Error`s.\n */\nexport function datasetInvalidError(message: string): Error {\n const err = new Error(message) as Error & { code?: string; status?: number };\n err.code = DATASET_INVALID;\n err.status = 400;\n return err;\n}\n\n/**\n * [#5716] A refusal about ONE MEMBER the request named — `INVALID_FIELD` / 400.\n *\n * Use it when the verdict is about a single `measures` / `dimensions` /\n * `timeDimensions` / `where` entry rather than about the dataset or the whole\n * selection: a measure the cube does not declare (#4157), a member this engine\n * cannot evaluate because it traverses a relationship the driver cannot join\n * (`planCrossObject`). The message stays whatever the refusing site says — every\n * one of these already names the member and how to fix it, and #5923's tests\n * assert that wording.\n *\n * `member` is the entry AS THE REQUEST SPELLED IT — `revenue`, not the\n * `account.balance` it resolved to — because that is the string the caller can\n * find in the body they sent; the resolved form stays in the message, which is\n * where the explanation lives. `member` / `param` / `cube` mirror the diagnostic\n * fields the three shipped gates attach\n * (`err.field`/`err.param`/`err.measure`…). `field` is deliberately\n * NOT among them: those gates resolve a member to a base COLUMN and name the\n * column that is missing, while here either there is no such column (an\n * undeclared measure) or the column exists and is perfectly fine on another\n * driver (a cross-object member). Naming one would be inventing a fact.\n */\nexport function invalidMemberError(\n message: string,\n meta: { member: string; param?: AnalyticsRequestKey; cube?: string },\n): Error {\n const err = new Error(message) as Error & {\n code?: string;\n status?: number;\n member?: string;\n param?: string;\n cube?: string;\n };\n err.code = INVALID_FIELD;\n err.status = 400;\n err.member = meta.member;\n if (meta.param) err.param = meta.param;\n if (meta.cube) err.cube = meta.cube;\n return err;\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { AnalyticsQuery, AnalyticsResult } from '@objectstack/spec/contracts';\nimport type { Cube } from '@objectstack/spec/data';\nimport type { AnalyticsStrategy, StrategyContext, DatasetScopedStrategyContext } from './types.js';\nimport {\n lowerAnalyticsWhere,\n normalizeAnalyticsFilterTree,\n toSqlBindValue,\n SQL_CONST_FALSE,\n SQL_CONST_TRUE,\n type NormalizedFilterNode,\n} from './filter-normalizer.js';\nimport { findCrossFieldComparand, findUninterpretableTemporalMember } from '../comparand-shape.js';\nimport { compileScopedFilterToSql } from '../read-scope-sql.js';\nimport { datasetInvalidError, invalidMemberError } from '../dataset-refusal.js';\nimport { likePattern, LIKE_ESCAPE_CHAR, asciiLowerSqlExpr, type LikeShape } from '../like-pattern.js';\nimport { nextUtcCalendarDay } from '@objectstack/core';\n\n/**\n * The SQL wrapper for each aggregate a measure's `type` can name.\n *\n * A table rather than a `switch` so its coverage is *assertable*: the aggregate\n * vocabulary lives in `@objectstack/spec` (`AggregationFunction`), the dataset\n * compiler subtracts whatever it cannot lower (`UNSUPPORTED_AGGREGATES` — empty\n * since #6188 retired its two members, `array_agg` and `string_agg`, from the\n * spec itself), and `aggregation-lockstep.test.ts` checks that what remains is\n * exactly the keys below. A `switch` gave that no purchase — the missing case fell to\n * `default: COUNT(*)`, so an aggregate the spec grew would have returned a row\n * count instead of the number the author asked for, silently. objectui#2945.\n *\n * Non-aggregate metric types (`number`/`string`/`boolean`) are deliberately\n * absent — they are handled by {@link EXPRESSION_METRIC_TYPES}, which emits the\n * author's expression rather than wrapping it.\n */\nconst AGGREGATE_SQL: Record<string, (col: string) => string> = {\n // [#10298] `count` takes its COLUMN when the measure declares one. The\n // wrapper used to discard `col` and always emit `COUNT(*)`, so a measure\n // written `{ aggregate: 'count', field: 'resolved_by_article' }` counted\n // ROWS instead of non-null values — and a deflection rate built as\n // `kb_resolved_count / closed_count` read 100% where the truth was 12.5%,\n // with the numerator and denominator printed beside it as 8 and 8. `*` is\n // still `COUNT(*)`: the compiler writes `sql: m.field ?? '*'`, so the star\n // IS the \"no field declared\" spelling and must keep counting rows.\n 'count': (col) => (col === '*' ? 'COUNT(*)' : `COUNT(${col})`),\n 'sum': (col) => `SUM(${col})`,\n 'avg': (col) => `AVG(${col})`,\n 'min': (col) => `MIN(${col})`,\n 'max': (col) => `MAX(${col})`,\n 'count_distinct': (col) => `COUNT(DISTINCT ${col})`,\n};\n\n/**\n * The same six aggregates, restricted to the rows a measure's own `filter`\n * admits (#10298).\n *\n * Spelled `CASE WHEN` rather than SQL-standard `FILTER (WHERE …)` on purpose:\n * `FILTER` is Postgres and SQLite ≥ 3.30 only — MySQL has never had it — and\n * this strategy hand-compiles ONE statement for whichever SQL driver owns the\n * object. A portable conditional aggregate is the only form that cannot answer\n * a syntax error on one supported driver and a number on another.\n *\n * `count` over `*` counts a constant, because `COUNT(CASE WHEN p THEN * END)`\n * is not a thing; over a real column it counts that column's non-null values\n * among the admitted rows, which composes the two corrections this card makes.\n *\n * Keyed identically to {@link AGGREGATE_SQL} — `aggregation-lockstep.test.ts`\n * pins the two key sets equal, so an aggregate added to one and not the other\n * fails a test instead of silently losing its measure filter.\n */\nconst CONDITIONAL_AGGREGATE_SQL: Record<string, (col: string, pred: string) => string> = {\n 'count': (col, pred) => `COUNT(CASE WHEN ${pred} THEN ${col === '*' ? '1' : col} END)`,\n 'sum': (col, pred) => `SUM(CASE WHEN ${pred} THEN ${col} END)`,\n 'avg': (col, pred) => `AVG(CASE WHEN ${pred} THEN ${col} END)`,\n 'min': (col, pred) => `MIN(CASE WHEN ${pred} THEN ${col} END)`,\n 'max': (col, pred) => `MAX(CASE WHEN ${pred} THEN ${col} END)`,\n 'count_distinct': (col, pred) => `COUNT(DISTINCT CASE WHEN ${pred} THEN ${col} END)`,\n};\n\n/** Exported for the lockstep guard — the aggregates this strategy can lower. */\nexport const SUPPORTED_AGGREGATE_SQL_KEYS = Object.keys(AGGREGATE_SQL);\n\n/**\n * Exported for the same guard — the aggregates this strategy can lower WITH a\n * measure-scoped filter (#10298). Equal to {@link SUPPORTED_AGGREGATE_SQL_KEYS}\n * by construction and pinned equal by the lockstep suite: an aggregate present\n * in one table only would silently drop the author's `filter` rather than fail.\n */\nexport const CONDITIONAL_AGGREGATE_SQL_KEYS = Object.keys(CONDITIONAL_AGGREGATE_SQL);\n\n/**\n * Metric types that are a custom SQL *expression*, not an aggregate to wrap.\n *\n * `AggregationMetricType` (`data/analytics.zod.ts`) documents these three as\n * \"Custom SQL expression returning a number / string / boolean\" — the measure's\n * `sql` IS the whole computation (a ratio, a `CASE`, a window function), so the\n * only correct emission is the expression itself. They used to fall through to\n * `resolveMeasureSql`'s `COUNT(*)` fallback, which threw the expression away and\n * returned a row count. #4157.\n *\n * Named rather than derived as \"everything that is not an aggregate\": deriving it\n * would silently classify a *new* aggregate the spec grows (`median`, …) as an\n * expression and emit a bare column. `metric-type-coverage.test.ts` asserts these\n * two sets partition `AggregationMetricType`, so a new member fails a test\n * instead of picking a default.\n */\nexport const EXPRESSION_METRIC_TYPES = new Set(['number', 'string', 'boolean']);\n\n/**\n * A dot-separated chain of bare identifiers — `amount`, `account.amount`,\n * `account.owner.region`. Distinguishes a relationship PATH, which\n * {@link NativeSQLStrategy.qualifyAndRegisterJoin} lowers into joins, from a SQL\n * expression that merely contains a dot. #4157.\n */\nconst IDENTIFIER_PATH = /^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)*$/;\n\n/**\n * NativeSQLStrategy — Priority 1\n *\n * Pushes the analytics query down to the database as a native SQL statement.\n * This is the most efficient path and is preferred whenever the backing driver\n * supports raw SQL execution (e.g. Postgres, MySQL, SQLite).\n *\n * `resolveMeasureSql` used to answer `COUNT(*)` to three different questions it\n * could not otherwise answer — an undeclared measure, a custom-SQL-expression\n * metric type, and an unrecognised type. All three returned a plausible number\n * for a query that asked for something else. They now emit the expression or\n * throw; see that method. #4157.\n */\nexport class NativeSQLStrategy implements AnalyticsStrategy {\n readonly name = 'NativeSQLStrategy';\n readonly priority = 10;\n\n canHandle(query: AnalyticsQuery, ctx: StrategyContext): boolean {\n if (!query.cube) return false;\n // This strategy groups by the raw column expression (`GROUP BY <col>`) and\n // emits no `date_trunc` — it cannot bucket a date dimension to a coarser\n // granularity, nor resolve buckets on a non-UTC calendar. When the query\n // asks for granularity bucketing we therefore DECLINE so the lower-priority\n // ObjectQLStrategy handles it via `engine.aggregate` (native date_trunc when\n // UTC-safe, else uniform in-memory bucketing). Without this, a date-bucketed\n // query silently grouped by the raw timestamp — one bucket per row — and a\n // non-UTC reference timezone was ignored entirely (ADR-0053 Phase 2, #1982).\n if (query.timeDimensions?.some((td) => !!td.granularity)) return false;\n // ADR-0062 D6 — DECLINE federated (external-datasource) objects. This\n // strategy hand-compiles `FROM \"<object>\"` and bare column references, which\n // bypass the driver's physical-table resolution (`external.remoteName` /\n // `remoteSchema` / `columnMap`) and would query the WRONG table. Routing the\n // query to the lower-priority ObjectQL aggregate path keeps it correct —\n // that path goes through the driver's `getBuilder` (#2138/#2149). Applies to\n // the base object AND any joined object (a join would also hit the wrong\n // table). Until native-SQL learns the driver's resolution, \"disabled\" beats\n // \"silently wrong\".\n if (typeof ctx.isExternalObject === 'function') {\n const cube = ctx.getCube(query.cube);\n if (cube) {\n if (ctx.isExternalObject(this.extractObjectName(cube))) return false;\n const joinTargets = cube.joins ? Object.values(cube.joins) : [];\n for (const j of joinTargets) {\n const joinedObject = (j as { name?: string })?.name;\n if (joinedObject && ctx.isExternalObject(joinedObject)) return false;\n }\n }\n }\n // ── [#7598] DECLINE a `{ $field }` cross-field comparison ───────────────\n //\n // ## The maintainer ruling this implements (2026-08-12, Q1 = B)\n //\n // 「`NativeSQLStrategy.canHandle` 对携带 `$field` 的 `where` / read scope\n // **decline**,落回 ObjectQL/engine 路径,由 driver 用它自有的 metadata 强制\n // 全部四条 #5222 裁定 —— 安全规则只存在一处,不复制、不新增\n // `StrategyContext` 钩子、不动 `packages/spec`。⚠️ canHandle 依据 filter\n // 内容路由是新行为 —— 认可并接受,实现时在 canHandle 处注释记录本裁定。」\n //\n // (Q1 = B; option A — `StrategyContext.getDeclaredFields` / `getTenantColumn`\n // hooks plus a SECOND implementation of the four rulings inside this package\n // — was explicitly rejected: it builds an enumeration surface with no\n // measured consumer, and its fallback when a host omits a hook is either\n // \"refuse\" or \"skip the check\", and skipping the check is the defect #7598\n // exists to close. Q2 = A: `read-scope-sql`'s envelope is untouched.)\n //\n // ## What is new here, and why it is sound\n //\n // Every other decline above turns on the query's SHAPE (a granularity, a\n // federated object). This one turns on filter CONTENT, which is new\n // behaviour for `canHandle` — named as such in the ruling and accepted\n // there. It is the same mechanism ADR-0062 D6 already uses one branch up:\n // when this strategy cannot compile something CORRECTLY, routing to the\n // lower-priority ObjectQL path is better than compiling it anyway. What it\n // cannot compile correctly here is a column-to-column comparison, because\n // the four #5222 rulings (same-table columns only, declared-only\n // enumeration, tenant-isolation column forbidden on BOTH sides, same\n // comparison class) each turn on metadata `StrategyContext` does not expose\n // — an object's declared field set, its declared types, its\n // tenant-isolation column. `driver-sql` reads all four out of its own\n // `initObjects` capture, so declining puts the query in front of the one\n // component that can enforce them, instead of enforcing them twice.\n //\n // ## Both inputs, because a read scope is not the caller's `where`\n //\n // The caller's `where` and the RLS read scope are separate producers and\n // either can carry a reference — `compileCelToFilter` emits `{ $field }`\n // for a field-to-field comparison in an ADMIN-authored CEL rule, which is\n // the read-scope half and the one #5041 measured. The scopes of the JOINED\n // objects are read too, for the same reason `generateSql` injects them:\n // `applyReadScope` would compile each of them through `read-scope-sql`.\n //\n // `lowerAnalyticsWhere` rather than `query.where` raw, so the authored\n // ARRAY sugar (`['amount', '=', { $field: 'budget' }]`) is seen after\n // `parseFilterAST` has lowered it (#7597). A THROW from that lowering is\n // not this gate's to answer — the filter is malformed either way and\n // `normalizeAnalyticsFilterTree` refuses it a moment later with the message\n // and envelope it has always had — so it is caught and read as \"no\n // reference found\".\n if (this.carriesCrossFieldComparison(query, ctx)) return false;\n // ── [#8690] DECLINE an uninterpretable TEMPORAL comparand ───────────────\n //\n // ## The maintainer ruling this implements (2026-08-15, option B)\n //\n // > refuse the uninterpretable temporal comparand at the ObjectQL engine's\n // > single filter collection point … Includes the measured gap:\n // > `NativeSQLStrategy.canHandle` must **decline** an uninterpretable\n // > temporal comparand so raw-SQL paths fall through to the engine door.\n //\n // The refusal itself is NOT here and must not be: judging \"can this column\n // read this comparand\" needs the field's declared TYPE, which only the\n // engine's filter collection point holds (this package depends on no\n // driver and carries no field map). What is here is the ROUTING half —\n // without it a raw-SQL deployment binds `WHERE col >= 'last_30_days'`\n // directly, never reaches the door, and keeps answering 200 with zero rows.\n //\n // Same mechanism, same direction, as the two declines above and the #7598\n // one below it: when this strategy cannot serve something CORRECTLY,\n // routing to the lower-priority ObjectQL path beats compiling it anyway.\n // Content-based rather than shape-based, which #7598's ruling already\n // named as new-but-accepted behaviour for `canHandle`.\n //\n // ⚠️ Deliberately NO fail-closed backstop at the emitter, unlike #7598's.\n // There the routing gate's failure mode was a NEW wrong answer (a bound\n // `{\"$field\":…}` object); here a missed decline degrades to exactly\n // today's behaviour, and a throw at the emitter would answer 500 for a\n // filter the engine door answers 400 for — two envelopes for one mistake,\n // which is the drift this card exists to remove.\n if (this.carriesUninterpretableTemporalComparand(query, ctx)) return false;\n const caps = ctx.queryCapabilities(query.cube);\n return caps.nativeSql && typeof ctx.executeRawSql === 'function';\n }\n\n /**\n * [#8690] Does the query's `where` compare a declared TIME dimension against\n * a value no temporal storage rule can read? See the ruling at\n * {@link canHandle}.\n *\n * The classification comes from the CUBE, the only metadata this package has:\n * a dimension declares `type: 'time'` (compiled from the dataset dimension's\n * `type: 'date'`), and {@link lookupMember} is the same resolution every other\n * member lookup in this strategy uses, so \"the member the gate classified\"\n * and \"the member the compiler emits\" cannot drift apart.\n *\n * A `time` dimension is read with the DATETIME rule — the permissive one of\n * the three. That is the right direction because this is a routing decision,\n * not a verdict: the engine door re-judges with the field's real declared\n * type and has the final say, so under-classifying an exotic spelling merely\n * leaves today's behaviour, while over-classifying would silently move a\n * working dashboard off the fast path. The comparands this card measured\n * (`last_30_days`, `not-a-date-at-all`) are unreadable under all three rules,\n * so the decline fires for them whichever backing type the dimension has.\n *\n * `lowerAnalyticsWhere` rather than `query.where` raw, so the authored ARRAY\n * sugar is seen after `parseFilterAST` has lowered it; a THROW from that\n * lowering is not this gate's to answer — the filter is malformed either way\n * and `normalizeAnalyticsFilterTree` refuses it a moment later with the\n * message and envelope it has always had.\n */\n private carriesUninterpretableTemporalComparand(\n query: AnalyticsQuery,\n ctx: StrategyContext,\n ): boolean {\n const cube = query.cube ? ctx.getCube(query.cube) : undefined;\n if (!cube) return false;\n let where: unknown = null;\n try {\n where = lowerAnalyticsWhere(query);\n } catch {\n return false;\n }\n if (!where) return false;\n return findUninterpretableTemporalMember(\n where,\n (member) => (this.lookupMember(cube, member, 'dimension')?.type === 'time' ? 'datetime' : null),\n ) !== null;\n }\n\n /**\n * [#7598] Does serving this query require the cross-field capability this\n * strategy declines? See the ruling recorded at {@link canHandle}.\n *\n * ⚠️ This and {@link assertNoCrossFieldComparison} read the SAME inputs\n * through the SAME detector, which is what makes the decline and the\n * fail-closed backstop unable to drift: a shape one of them recognises is a\n * shape the other recognises.\n */\n private carriesCrossFieldComparison(query: AnalyticsQuery, ctx: StrategyContext): boolean {\n return this.crossFieldComparisonIn(query, ctx) !== null;\n }\n\n private crossFieldComparisonIn(\n query: AnalyticsQuery,\n ctx: StrategyContext,\n ): { source: string; op: string; field: string; ref: string } | null {\n let where: unknown = null;\n try {\n where = lowerAnalyticsWhere(query);\n } catch {\n // A `where` this compiler cannot even lower is refused downstream, with\n // its own message. Nothing to route.\n return null;\n }\n const inWhere = findCrossFieldComparand(where);\n if (inWhere) return { source: 'the query\\'s `where`', ...inWhere };\n\n if (typeof ctx.getReadScope !== 'function') return null;\n const cube = query.cube ? ctx.getCube(query.cube) : undefined;\n if (!cube) return null;\n const objects = [this.extractObjectName(cube)];\n for (const alias of Object.keys(cube.joins ?? {})) {\n objects.push(cube.joins?.[alias]?.name ?? alias);\n }\n for (const objectName of objects) {\n const scope = ctx.getReadScope(objectName);\n if (scope === undefined || scope === null) continue;\n const inScope = findCrossFieldComparand(scope);\n if (inScope) return { source: `the read scope of \"${objectName}\"`, ...inScope };\n }\n return null;\n }\n\n /**\n * [#7598] The fail-closed backstop at the door that BINDS.\n *\n * ⚠️ **Unreachable by construction, and kept deliberately** — saying so\n * because #7598's brief asks that a refusal arm which has become unreachable\n * be named rather than left to be re-discovered. {@link canHandle} declines\n * every query this would fire on, and it declines using\n * {@link crossFieldComparisonIn} — the same walk over the same two inputs —\n * so `resolveStrategy` cannot hand this strategy a query carrying one.\n *\n * It is kept because of what the failure mode is if that ever stops being\n * true. The defect #7598 measured was not a missing error: it was a SILENT\n * BIND — `toSqlBindValue` JSON-stringifies the reference object, so the\n * statement compiled perfectly and compared a column against the text\n * `{\"$field\":\"budget\"}`, a value no row can hold. A routing gate that misses\n * a shape therefore degrades to a wrong ANSWER rather than to an error, and\n * that is the one class this package refuses to leave to a single guard\n * (Prime Directive #12 — refuse at the door, do not tolerate at the\n * consumer). One line, no measurable cost, and it turns a routing regression\n * into a loud refusal instead of an empty chart.\n *\n * Deliberately BARE — an undeclared 500, not `INVALID_FILTER` / 400 — for the\n * reason `buildFilterClauseSql`'s #5333 exit in `objectql-strategy.ts` gives\n * for the same class: the caller's filter is legal and is served on the\n * engine path, so an arrival here is drift between our own routing gate and\n * our own emitter. Billing the caller 400 for that would hide a platform bug\n * from 5xx alerting and tell a dashboard user to fix a filter that is fine.\n * Same tier as `resolveMeasureSql`'s unrecognised-`Metric.type` throw below.\n */\n private assertNoCrossFieldComparison(query: AnalyticsQuery, ctx: StrategyContext): void {\n const hit = this.crossFieldComparisonIn(query, ctx);\n if (!hit) return;\n throw new Error(\n `[native-sql-strategy] ${hit.source} carries a field reference ` +\n `{ \"$field\": \"${hit.ref}\" } under \"${hit.op}\" on \"${hit.field}\", which this strategy does not ` +\n `compile into a column-to-column comparison — it would BIND the reference object as the ` +\n `comparison's value and answer a wrong row set silently (#7598). \\`canHandle\\` declines such a ` +\n `query so it routes to the ObjectQL/engine path, whose driver compiles it and enforces the ` +\n `#5222 rulings with metadata it owns; reaching this throw means the decline and this emitter ` +\n `stopped agreeing, which is our bug and must never degrade to a silent answer.`,\n );\n }\n\n async execute(query: AnalyticsQuery, ctx: StrategyContext): Promise<AnalyticsResult> {\n const { sql, params } = await this.generateSql(query, ctx);\n const cube = ctx.getCube(query.cube!)!;\n const objectName = this.extractObjectName(cube);\n\n const rows = await ctx.executeRawSql!(objectName, sql, params);\n\n // Build field metadata\n const fields = this.buildFieldMeta(query, cube);\n\n return { rows, fields, sql };\n }\n\n async generateSql(query: AnalyticsQuery, ctx: StrategyContext): Promise<{ sql: string; params: unknown[] }> {\n const cube = ctx.getCube(query.cube!);\n if (!cube) {\n throw new Error(`Cube not found: ${query.cube}`);\n }\n\n // [#7598] Unreachable by construction — `canHandle` declined this query.\n // See {@link assertNoCrossFieldComparison} for why it is asserted anyway.\n this.assertNoCrossFieldComparison(query, ctx);\n\n const params: unknown[] = [];\n const selectClauses: string[] = [];\n const groupByClauses: string[] = [];\n const tableName = this.extractObjectName(cube);\n // Map of relation alias → JOIN clause. Populated lazily as dotted\n // dimensions/measures/filters are resolved.\n const joins = new Map<string, string>();\n\n // Build SELECT for dimensions\n if (query.dimensions && query.dimensions.length > 0) {\n for (const dim of query.dimensions) {\n const colExpr = this.resolveDimensionSql(cube, dim, tableName, joins);\n selectClauses.push(`${colExpr} AS \"${dim}\"`);\n groupByClauses.push(colExpr);\n }\n }\n\n // ── #10298 — the half of a compiled dataset the Cube cannot carry ──────\n // A dataset's definition-level `filter` and each measure's own scoped\n // `filter` live beside the Cube, in the dataset registry. `DatasetExecutor`\n // read them; this strategy did not — so `/api/v1/analytics/query`, which\n // addresses the registered Cube directly, answered UNFILTERED aggregates\n // under the author's measure names while the dashboard answered filtered\n // ones, for the same cube. `undefined` for any cube that is not a compiled\n // dataset, which is why an inferred or manifest cube compiles unchanged.\n const datasetScope = (ctx as DatasetScopedStrategyContext).getDatasetScope?.(query.cube!);\n\n // Build SELECT for measures\n if (query.measures && query.measures.length > 0) {\n for (const measure of query.measures) {\n // The measure's own filter becomes a CONDITIONAL aggregate rather than\n // a `WHERE` conjunct: the statement carries several measures at once and\n // a `WHERE` would narrow ALL of them. Compiled here, inside the SELECT\n // loop, so its bound values are pushed onto `params` in the order the\n // placeholders appear in the statement — the SELECT list precedes the\n // WHERE clause, and `$n` is positional.\n const measureFilter = datasetScope?.measureFilters?.[measure];\n const predicate = measureFilter\n ? this.compileFilterNode(\n normalizeAnalyticsFilterTree({ where: measureFilter }),\n cube,\n tableName,\n joins,\n params,\n ctx,\n )\n : null;\n const aggExpr = this.resolveMeasureSql(cube, measure, tableName, joins, predicate);\n selectClauses.push(`${aggExpr} AS \"${measure}\"`);\n }\n }\n\n // Build WHERE clause. The filter is a TREE, so it compiles recursively —\n // a flat loop can only ever AND, which is precisely why an author's `$or`\n // used to be dropped instead of compiled.\n const whereClauses: string[] = [];\n const filterSql = this.compileFilterNode(\n normalizeAnalyticsFilterTree(query),\n cube,\n tableName,\n joins,\n params,\n ctx,\n );\n if (filterSql) whereClauses.push(filterSql);\n\n // [#10298] The dataset's OWN scope, for the door that never went through\n // `DatasetExecutor`. Applied as a plain conjunct because it narrows the\n // whole statement — every measure in it — which is exactly what the\n // definition-level filter means. Redundant on the dataset door (the\n // executor already merged it into `where`) and idempotent there: ANDing a\n // predicate with itself selects the same rows.\n if (datasetScope?.filter) {\n const scopeSql = this.compileFilterNode(\n normalizeAnalyticsFilterTree({ where: datasetScope.filter }),\n cube,\n tableName,\n joins,\n params,\n ctx,\n );\n if (scopeSql) whereClauses.push(scopeSql);\n }\n\n // Build time dimension filters\n if (query.timeDimensions && query.timeDimensions.length > 0) {\n for (const td of query.timeDimensions) {\n const colExpr = this.resolveFieldSql(cube, td.dimension, tableName, joins);\n if (td.dateRange) {\n const range = Array.isArray(td.dateRange) ? td.dateRange : [td.dateRange, td.dateRange];\n if (range.length === 2) {\n // Same epoch-vs-text root cause as buildFilterClause: a dateRange on a\n // SQLite `Field.datetime` column compares ISO TEXT against an INTEGER\n // epoch and matches nothing. Coerce both bounds to the storage form —\n // and normalise the column to that form too, because the column holds\n // BOTH forms at once and coercing only the bounds still empties the\n // half the writer stored the other way (#3912).\n const td2 = this.resolveStorageTarget(cube, td.dimension, tableName);\n const column = this.temporalColumn(ctx, td2, colExpr);\n // A bare-day window end means \"through that whole day\" (#3777). A\n // BETWEEN's inclusive upper bound anchors a bare `YYYY-MM-DD` to\n // midnight on a datetime column, dropping the final day's rows, so\n // the window compiles half-open — `>= start AND < end+1day` — the\n // same `[gte, lt)` the drill ranges emit. Equivalent to the old\n // BETWEEN for a `date` column (plain `YYYY-MM-DD` ordering), which\n // is what lets this path stay column-type-blind.\n const nextDay = nextUtcCalendarDay(range[1]);\n params.push(this.coerceTemporal(ctx, td2, range[0]));\n const lower = `${column} >= $${params.length}`;\n if (nextDay != null) {\n params.push(this.coerceTemporal(ctx, td2, nextDay));\n whereClauses.push(`(${lower} AND ${column} < $${params.length})`);\n } else {\n params.push(this.coerceTemporal(ctx, td2, range[1]));\n whereClauses.push(`(${lower} AND ${column} <= $${params.length})`);\n }\n }\n }\n }\n }\n\n // ── ADR-0021 D-C — enforce the join allowlist + inject per-object RLS ──\n // 1. Reject any join not backed by a relationship the dataset declared.\n const allowed = ctx.getAllowedRelationships?.(query.cube!);\n if (allowed) {\n for (const alias of joins.keys()) {\n if (!allowed.has(alias)) {\n // [#5367] `DATASET_INVALID` / 400 — verified caller-shaped before\n // enveloping. Every join in `joins` was registered by\n // `qualifyAndRegisterJoin`, and on the dataset route the only inputs\n // that can register one OUTSIDE the allowlist are the REQUEST's own:\n // `lookupMember`'s synthetic relation fallback mints a dotted\n // dimension nobody declared, so `selection.dimensions`,\n // `selection.timeDimensions` and a `runtimeFilter` member spelled\n // `account.name` each land here. The dataset's OWN dimensions and\n // measures cannot: `compileDataset`'s `assertDeclared` refuses an\n // undeclared relationship path at compile time (also 400\n // `DATASET_INVALID`, so the two agree rather than diverge), and\n // `resolveMeasureSql` has no synthetic fallback at all.\n //\n // The one non-caller trigger is the legacy\n // `config.getAllowedRelationships` hook for hand-authored cubes,\n // where a mismatch is the host's configuration rather than the\n // caller's query. It is unreachable from\n // `/analytics/dataset/query`: `queryDataset` registers the compiled\n // dataset first, so `getAllowedRelationships` answers from\n // `datasetRegistry` and never falls through to the hook.\n throw datasetInvalidError(\n `[NativeSQLStrategy] join \"${alias}\" is not backed by a declared relationship on ` +\n `cube \"${query.cube}\". v1 only joins along relationships listed in the dataset's \\`include\\`.`,\n );\n }\n }\n }\n // 2. Inject the tenant/RLS read scope for the base table AND every joined\n // object — this is the predicate the raw-SQL path would otherwise skip.\n this.applyReadScope(this.extractObjectName(cube), tableName, ctx, whereClauses, params);\n for (const alias of joins.keys()) {\n // The joined OBJECT (for the RLS lookup) is the target table from the\n // cube's join map; the ALIAS is how it's referenced in SQL. These differ\n // for namespaced objects (alias `account` → object `crm_account`).\n const joinedObject = cube.joins?.[alias]?.name ?? alias;\n this.applyReadScope(joinedObject, alias, ctx, whereClauses, params);\n }\n\n let sql = `SELECT ${selectClauses.join(', ')} FROM \"${tableName}\"`;\n if (joins.size > 0) {\n sql += ' ' + Array.from(joins.values()).join(' ');\n }\n if (whereClauses.length > 0) {\n sql += ` WHERE ${whereClauses.join(' AND ')}`;\n }\n if (groupByClauses.length > 0) {\n sql += ` GROUP BY ${groupByClauses.join(', ')}`;\n }\n if (query.order && Object.keys(query.order).length > 0) {\n const orderClauses = Object.entries(query.order).map(([f, d]) => `\"${f}\" ${d.toUpperCase()}`);\n sql += ` ORDER BY ${orderClauses.join(', ')}`;\n }\n if (query.limit != null) {\n sql += ` LIMIT ${query.limit}`;\n }\n if (query.offset != null) {\n sql += ` OFFSET ${query.offset}`;\n }\n\n return { sql, params };\n }\n\n // ── Helpers ──────────────────────────────────────────────────────\n\n /**\n * ADR-0021 D-C — inject an object's read scope (tenant + RLS predicate) into\n * the WHERE clause. The scope is a canonical `FilterCondition` (what the\n * RLSCompiler emits); `compileScopedFilterToSql` turns it into alias-qualified,\n * parameterized SQL (fail-closed — it throws rather than drop a predicate).\n * The `?` placeholders are then renumbered into the strategy's `$N` scheme.\n * No-op when the runtime provides no scope hook (the caller is then\n * responsible for isolation — see contract note).\n */\n private applyReadScope(\n objectName: string,\n alias: string,\n ctx: StrategyContext,\n whereClauses: string[],\n params: unknown[],\n ): void {\n if (typeof ctx.getReadScope !== 'function') return;\n const filter = ctx.getReadScope(objectName);\n if (filter === undefined || filter === null) return;\n const { sql, params: scopeParams } = compileScopedFilterToSql(filter, alias);\n if (!sql) return;\n let i = 0;\n const rendered = sql.replace(/\\?/g, () => {\n params.push(scopeParams[i++]);\n return `$${params.length}`;\n });\n whereClauses.push(`(${rendered})`);\n }\n\n /** SQL-safe join alias for a relationship path (dots → `__`); single-segment\n * paths are unchanged. Mirrors the dataset compiler's `cube.joins` keying so\n * alias, allowlist, and per-hop RLS all agree on one valid identifier. */\n private joinAlias(path: string): string {\n return path.replace(/\\./g, '__');\n }\n\n /**\n * Resolve a dimension/measure/filter SQL expression that may reference a\n * related table via dot notation (e.g. `account.industry`).\n *\n * A dotted `sql` is a relationship PATH (ADR-0071 multi-hop): every segment\n * but the last is a to-one relationship hop, the last is the column. Each hop\n * synthesises a `LEFT JOIN` aliased by its full path prefix, chained\n * parent→child. The convention (matching the auto-cube generator and\n * ObjectStack object schemas) for a single hop is:\n *\n * <parentTable>.<lookupField> = <lookupField>.id\n *\n * i.e. the lookup field name on the parent table equals the related\n * table name. This holds for all `Field.lookup({ object: '...' })`\n * declarations where the field is named after its target object.\n *\n * Returns the qualified SQL reference (e.g. `\"account\".\"industry\"`).\n * Pure column references (no dot) are returned as-is.\n */\n private qualifyAndRegisterJoin(\n rawSql: string,\n parentTable: string,\n joins: Map<string, string>,\n cube?: Cube,\n ): string {\n if (!rawSql.includes('.')) {\n // Base-table column. When the cube can join other tables, a bare column\n // that also exists on a joined table (e.g. base `status` vs joined\n // `account.status`) makes the SQL engine raise \"ambiguous column name\".\n // Qualify plain identifiers with the base table; leave SQL expressions\n // and `*` untouched. Single-object cubes (no joins) keep bare columns so\n // their generated SQL is byte-for-byte unchanged.\n const canJoin = !!cube?.joins && Object.keys(cube.joins).length > 0;\n if (canJoin && /^[A-Za-z_][A-Za-z0-9_]*$/.test(rawSql)) {\n return `\"${parentTable}\".\"${rawSql}\"`;\n }\n return rawSql;\n }\n // A dot does not by itself mean \"relationship path\". `SUM(account.amount)`\n // is one SQL EXPRESSION that happens to contain a dot, and splitting it as a\n // path produced `\"SUM(account\".\"amount)\"` plus a phantom\n // `LEFT JOIN \"SUM(account\"` — invalid SQL and a join to a table that does not\n // exist. Only qualify when every segment is a bare identifier; otherwise the\n // author wrote an expression and it is returned as-is. #4157.\n if (!IDENTIFIER_PATH.test(rawSql)) return rawSql;\n // Multi-hop (ADR-0071): the dotted path IS the join chain. Every segment but\n // the last is a relationship hop; the last is the column. The join ALIAS at\n // each hop is the full path PREFIX (`account`, then `account.owner`), which\n // encodes its own parent (the prefix minus its last segment) and FK column\n // (that segment). Register one LEFT JOIN per prefix, chaining parent→child.\n const segments = rawSql.split('.');\n const column = segments[segments.length - 1];\n const hops = segments.slice(0, -1);\n if (hops.length === 0 || !column) return rawSql;\n let parentAlias = parentTable;\n let prefix = '';\n for (const seg of hops) {\n prefix = prefix ? `${prefix}.${seg}` : seg;\n const alias = this.joinAlias(prefix);\n if (!joins.has(alias)) {\n // The joined TABLE is resolved from the Cube's `joins` map (emitted by\n // the dataset compiler, keyed by the same alias); fall back to the alias\n // as the table for legacy/same-name cubes.\n const joinTable = cube?.joins?.[alias]?.name ?? alias;\n // Only emit an explicit alias when the table differs from it; when they\n // match, `LEFT JOIN \"account\" ON …` is cleaner (and back-compat).\n const tableRef = joinTable === alias ? `\"${alias}\"` : `\"${joinTable}\" \"${alias}\"`;\n joins.set(\n alias,\n `LEFT JOIN ${tableRef} ON \"${parentAlias}\".\"${seg}\" = \"${alias}\".\"id\"`,\n );\n }\n parentAlias = alias;\n }\n return `\"${parentAlias}\".\"${column}\"`;\n }\n\n /**\n * Resolve a member reference (dimension, measure, or filter field) to its\n * cube definition.\n *\n * Accepts three naming conventions:\n * 1. `<cube>.<field>` — the canonical analytics qualifier (stripped to `<field>`).\n * 2. `<lookup>.<field>` — a relation traversal (e.g. `account.industry`).\n * First tried as the literal key, then as the underscore-flattened\n * key (`account_industry`), and finally returned as a synthetic\n * definition whose `sql` is the dotted reference so the JOIN\n * machinery can pick it up.\n * 3. `<field>` — a bare field name on the cube's table.\n */\n private lookupMember(\n cube: Cube,\n member: string,\n kind: 'dimension' | 'measure',\n ): { sql: string; type?: string } | undefined {\n const bag = kind === 'dimension' ? cube.dimensions : cube.measures;\n // Direct hit on the registered key (handles `cube.field` and exact dotted keys).\n if (bag[member]) return bag[member];\n if (member.includes('.')) {\n const [first, ...rest] = member.split('.');\n const tail = rest.join('.');\n // `<cube>.<field>` style.\n if (first === cube.name && bag[tail]) return bag[tail];\n // Plain second-segment lookup (legacy behaviour).\n if (bag[tail]) return bag[tail];\n // Underscore-flattened relation lookup (e.g. `account_industry`).\n const flat = member.replace(/\\./g, '_');\n if (bag[flat]) return bag[flat];\n // Synthetic relation traversal — let qualifyAndRegisterJoin handle it.\n if (kind === 'dimension') {\n return { sql: member, type: 'string' };\n }\n } else if (bag[member]) {\n return bag[member];\n }\n return undefined;\n }\n\n private resolveDimensionSql(\n cube: Cube,\n member: string,\n parentTable: string,\n joins: Map<string, string>,\n ): string {\n const dim = this.lookupMember(cube, member, 'dimension');\n const raw = dim ? dim.sql : (member.includes('.') ? member.split('.')[1] : member);\n return this.qualifyAndRegisterJoin(raw, parentTable, joins, cube);\n }\n\n /**\n * @param predicate - The measure's own scoped filter, already compiled to a\n * SQL boolean (`null` = the measure declares none, or declares one that\n * constrains nothing — `compileFilterNode`'s TRUE). #10298.\n */\n private resolveMeasureSql(\n cube: Cube,\n member: string,\n parentTable: string,\n joins: Map<string, string>,\n predicate: string | null = null,\n ): string {\n const measure = this.lookupMember(cube, member, 'measure') as\n | { sql: string; type: string }\n | undefined;\n // `lookupMember`'s synthetic relation fallback is dimension-only, so an\n // undeclared measure name lands here — a typo, or a query naming a metric\n // this cube does not have. It used to return `COUNT(*)`: the caller asked\n // for revenue and got a row count, aliased AS \"revenue\". #4157.\n if (!measure) {\n const declared = Object.keys(cube.measures ?? {});\n // [#5716] `INVALID_FIELD` / 400, naming the member — the request's\n // `measures` entry is the only input, and #4437's gate already answers\n // exactly this code for the measure one character away (a measure whose\n // SOURCE FIELD the object lacks). Two spellings of \"your `measures` entry\n // is wrong\" must not get two wire shapes. `DATASET_INVALID` would be wrong\n // on the other face this fires on: `/analytics/query` names a cube, not a\n // dataset.\n throw invalidMemberError(\n `[native-sql-strategy] cube \"${cube.name}\" declares no measure \"${member}\"` +\n (declared.length ? ` (declared: ${declared.join(', ')})` : ' (it declares none)'),\n { member, param: 'measures', cube: cube.name },\n );\n }\n\n const col = measure.sql === '*'\n ? '*'\n : this.qualifyAndRegisterJoin(measure.sql, parentTable, joins, cube);\n\n if (predicate !== null) {\n const wrapConditional = CONDITIONAL_AGGREGATE_SQL[measure.type];\n if (wrapConditional) return wrapConditional(col, predicate);\n // [#10298] Deliberately BARE — an undeclared 500, same tier and same\n // reasoning as the \"unrecognised type\" throw below. A measure filter only\n // ever arrives here from a COMPILED DATASET, and `DatasetMeasure.aggregate`\n // is `AggregationFunction`, whose every member is a key of the table\n // above — so an expression metric type (`number`/`string`/`boolean`,\n // where `sql` IS the whole computation and there is no aggregate to make\n // conditional) cannot carry one. What would reach here is our own drift.\n // Emitting the unfiltered aggregate instead is precisely the defect this\n // card closes: a 200 carrying different arithmetic than the author declared.\n throw new Error(\n `[native-sql-strategy] measure \"${member}\" on cube \"${cube.name}\" carries a ` +\n `scoped filter, but its type \"${measure.type}\" has no conditional form ` +\n `(conditional: ${CONDITIONAL_AGGREGATE_SQL_KEYS.join(', ')}).`,\n );\n }\n\n const wrap = AGGREGATE_SQL[measure.type];\n if (wrap) return wrap(col);\n // A custom SQL expression: the measure's `sql` IS the computation, so emit\n // it unwrapped. In a grouped query the expression must itself be\n // aggregate-shaped — measures never join `GROUP BY` (only dimensions do), so\n // a scalar expression there is invalid SQL. That is the author's contract to\n // keep; silently substituting `COUNT(*)` did not keep it for them.\n if (EXPRESSION_METRIC_TYPES.has(measure.type)) return col;\n\n // [#5716] Deliberately BARE — an undeclared 500, and the one site on that\n // issue's list of nine that is NOT the author's mistake. `Metric.type` is the\n // CLOSED `AggregationMetricType` enum; `metric-type-coverage.test.ts` pins\n // that {@link AGGREGATE_SQL} ∪ {@link EXPRESSION_METRIC_TYPES} partitions it\n // exactly, `dataset-compiler` only ever writes a `SUPPORTED_AGGREGATES`\n // member into a cube, and `inferMeasure` mints six known types. So no\n // spec-valid cube can arrive here: what does is our own drift or a host\n // registering a cube object that never met `CubeSchema`. Answering the\n // CALLER 400 for that would hide a platform bug from ops alerting and tell a\n // dashboard user to fix metadata they cannot see. Same tier as\n // `dataset-compiler`'s \"non-derived measure has no aggregate\"; the reasoning\n // is written once in `dataset-refusal.ts`'s header.\n throw new Error(\n `[native-sql-strategy] measure \"${member}\" on cube \"${cube.name}\" has ` +\n `unrecognised type \"${measure.type}\" — expected an aggregate ` +\n `(${SUPPORTED_AGGREGATE_SQL_KEYS.join(', ')}) or a custom-expression type ` +\n `(${[...EXPRESSION_METRIC_TYPES].join(', ')}).`,\n );\n }\n\n private resolveFieldSql(\n cube: Cube,\n member: string,\n parentTable: string,\n joins: Map<string, string>,\n ): string {\n const dim = this.lookupMember(cube, member, 'dimension');\n if (dim) return this.qualifyAndRegisterJoin(dim.sql, parentTable, joins, cube);\n const measure = this.lookupMember(cube, member, 'measure');\n if (measure) return this.qualifyAndRegisterJoin(measure.sql, parentTable, joins, cube);\n const fieldName = member.includes('.') ? member.split('.')[1] : member;\n return fieldName;\n }\n\n /**\n * Resolve the (object, column) a filter member binds against, so its\n * comparand can be coerced to that column's on-disk storage form.\n *\n * Mirrors `resolveFieldSql`'s `sql` resolution but yields the *logical*\n * target rather than the qualified SQL:\n * - A dotted column (`account.region`, emitted for a relation traversal)\n * belongs to the JOINED object — resolve the alias → target table via the\n * cube's `joins` map (alias `account` → object `crm_account` when\n * namespaced) and take the tail as the column.\n * - Otherwise the column lives on the cube's BASE table. Use the dimension's\n * resolved `sql` (the real column, which may differ from the member name,\n * e.g. dimension `assessed` → column `assessed_at`) rather than the member.\n */\n private resolveStorageTarget(\n cube: Cube,\n member: string,\n baseTable: string,\n ): { object: string; field: string } {\n const dim = this.lookupMember(cube, member, 'dimension');\n const measure = dim ? undefined : this.lookupMember(cube, member, 'measure');\n const rawSql = dim?.sql ?? measure?.sql ?? (member.includes('.') ? member.split('.').slice(1).join('.') : member);\n\n if (rawSql.includes('.')) {\n // Multi-hop (ADR-0071): the column's owning object is the join at the\n // relationship PATH (all segments but the last); the column is the last.\n const segments = rawSql.split('.');\n const field = segments[segments.length - 1];\n const relPath = segments.slice(0, -1).join('.');\n const object = cube.joins?.[this.joinAlias(relPath)]?.name ?? relPath;\n return { object, field };\n }\n return { object: baseTable, field: rawSql };\n }\n\n /**\n * Apply the storage-form coercion for a single comparand. Prefers the\n * driver-backed `coerceTemporalFilterValue` hook (single source of truth for\n * the date/datetime storage convention — see StrategyContext); when the hook\n * is absent, or returns the value unchanged (the field is not a temporal\n * column, or the dialect stores it as a native timestamp), falls back to\n * {@link toSqlBindValue} so an unbindable JS type still reaches the driver as\n * something it can bind.\n *\n * [#5526] `value` is `unknown`, not `string`, because a leaf now carries the\n * author's comparand at its own type. Both halves of this method were already\n * `unknown`-typed for it: the hook's contract is\n * `coerceTemporalFilterValue(object, field, value: unknown)` and the fallback\n * converts only what a driver cannot bind. What CHANGED is that a string is no\n * longer re-typed on the way out — the fallback used to be\n * `coerceFilterValueForSql`, which read `'007'` as the integer `7`.\n */\n private coerceTemporal(\n ctx: StrategyContext,\n target: { object: string; field: string },\n value: unknown,\n ): unknown {\n if (typeof ctx.coerceTemporalFilterValue === 'function') {\n const coerced = ctx.coerceTemporalFilterValue(target.object, target.field, value);\n // Hook returns the value untouched for non-temporal / native-timestamp\n // columns; only short-circuit when it actually changed the value.\n if (coerced !== value) return coerced;\n }\n return toSqlBindValue(value);\n }\n\n /**\n * The column side of {@link coerceTemporal}: normalise the reference so it\n * reads in the storage form the comparand was coerced into.\n *\n * A SQLite `Field.datetime` column carries an INTEGER epoch (a `Date` write)\n * and ISO TEXT (a REST/JSON write, a `NOW()` default — including the platform's\n * own `created_at`) at the SAME time, so coercing the value alone fixes one half\n * and empties the other. That is #3912: a `dateRange: last_30_days` on\n * `created_date` read 0 with 29 rows in range. Every other column and dialect\n * gets its reference back verbatim.\n */\n private temporalColumn(\n ctx: StrategyContext,\n target: { object: string; field: string },\n col: string,\n ): string {\n if (typeof ctx.coerceTemporalFilterColumn !== 'function') return col;\n return ctx.coerceTemporalFilterColumn(target.object, target.field, col) || col;\n }\n\n /**\n * Compile a normalized filter node into a boolean SQL expression, recursing\n * through the combinators. `null` = no constraint.\n *\n * Leaves go through {@link buildFilterClause} exactly as they did when this\n * was a flat loop, so the storage-form coercion and the calendar-day\n * upper-bound rule (#3777) apply at every depth — including inside an `$or`,\n * where a second, combinator-aware implementation would have been free to\n * drift from the first.\n *\n * Parenthesisation is explicit rather than left to SQL's precedence: `AND`\n * does bind tighter than `OR`, so `a AND b OR c` happens to be right, but\n * being right by construction is what keeps a future edit from making it\n * wrong.\n *\n * # `null` is the constant TRUE, and TRUE absorbs a disjunction (#5325)\n *\n * A `null` return means \"constrains nothing\", which is the boolean TRUE — the\n * AND identity, so it drops out of an `and`, but the OR ABSORBER, so one TRUE\n * disjunct makes the whole `or` TRUE. Filtering it out of an `or` narrowed the\n * query to the surviving branches. `NOT TRUE ≡ FALSE`, so a negation whose\n * operand constrains nothing compiles to the FALSE constant rather than\n * disappearing (which added no `WHERE` and charted every row).\n *\n * # The invariant that keeps `params` aligned\n *\n * **A call that returns `null` leaves `params` exactly as it found it.** It\n * has to: a value bound with no `$n` to consume it shifts every later\n * placeholder onto the wrong value, and a filter that binds the WRONG comparand\n * is worse than one that is merely too wide (#5297). Leaves decide emptiness\n * before they bind, and the absorbing `or` — the one place a clause that HAS\n * bound is discarded — truncates back to the length it started at, so the\n * invariant holds inductively for every node kind.\n */\n private compileFilterNode(\n node: NormalizedFilterNode | null,\n cube: Cube,\n parentTable: string,\n joins: Map<string, string>,\n params: unknown[],\n ctx: StrategyContext,\n ): string | null {\n if (!node) return null;\n\n if (node.kind === 'const') {\n return node.value ? SQL_CONST_TRUE : SQL_CONST_FALSE;\n }\n\n if (node.kind === 'leaf') {\n const colExpr = this.resolveFieldSql(cube, node.member, parentTable, joins);\n // Resolve the (object, column) this member binds against so the value\n // can be coerced to the column's storage form (see buildFilterClause).\n const target = this.resolveStorageTarget(cube, node.member, parentTable);\n return this.buildFilterClause(colExpr, node.operator, node.values, params, ctx, target);\n }\n\n if (node.kind === 'not') {\n const inner = this.compileFilterNode(node.child, cube, parentTable, joins, params, ctx);\n // `NOT TRUE ≡ FALSE`. Returning `null` here is what made `{$not: {}}` emit\n // no `WHERE` at all — a filter meaning \"no rows\" that showed all of them.\n // The normalizer already folds that case into a `const` node; this arm is\n // the same identity applied to anything else that constrains nothing.\n return inner ? `NOT (${inner})` : SQL_CONST_FALSE;\n }\n\n // Everything committed before this group, so an absorbed `or` can put both\n // back exactly as they were.\n const paramBase = params.length;\n const joinBase = new Map(joins);\n const parts: string[] = [];\n for (const child of node.children) {\n const clause = this.compileFilterNode(child, cube, parentTable, joins, params, ctx);\n if (clause === null) {\n // TRUE: the AND identity, the OR absorber.\n if (node.kind !== 'or') continue;\n params.length = paramBase;\n joins.clear();\n for (const [alias, clauseSql] of joinBase) joins.set(alias, clauseSql);\n return null;\n }\n parts.push(clause);\n }\n if (parts.length === 0) return null;\n if (parts.length === 1) return parts[0];\n return `(${parts.join(node.kind === 'or' ? ' OR ' : ' AND ')})`;\n }\n\n private buildFilterClause(\n rawCol: string,\n operator: string,\n // [#5526] `unknown[]`: the author's comparands, at their own types. Every\n // conversion below is one a BOUNDARY demands — `likePattern` because\n // `filter.zod.ts` declares the LIKE comparand a `string`, `coerceTemporal`\n // because a driver cannot bind every JS type — never a guess about which\n // type a string \"really\" was.\n values: unknown[] | undefined,\n params: unknown[],\n ctx: StrategyContext,\n target: { object: string; field: string },\n ): string | null {\n const opMap: Record<string, string> = {\n equals: '=', notEquals: '!=', gt: '>', gte: '>=', lt: '<', lte: '<=',\n contains: 'LIKE', notContains: 'NOT LIKE',\n startsWith: 'LIKE', endsWith: 'LIKE',\n // [#6520] `$icontains` — `LIKE` like its neighbours; what separates it is\n // the ASCII fold applied below, not the keyword.\n icontains: 'LIKE',\n };\n /**\n * Where each string operator puts the wildcard. [#5567] The pattern itself is\n * built by the shared `likePattern`, which ESCAPES the comparand — `_` and\n * `%` are LIKE wildcards, so the old inline table quietly turned an author's\n * literal into a pattern (`$contains: '_admin'` also matched `xyadmin`).\n * `objectql-strategy.ts`'s `LIKE_SQL_OPS` carries the same table for the\n * `/analytics/sql` echo of this statement; they move together.\n */\n const likeShape: Record<string, LikeShape> = {\n contains: 'contains', notContains: 'contains',\n startsWith: 'starts', endsWith: 'ends',\n // [#6520] Same wildcard placement as `contains`; the case fold is what\n // differs, and it is applied to both sides of the comparison below.\n icontains: 'contains',\n };\n\n // Null predicates and the LIKE family read the column as stored — the former\n // is storage-independent, the latter is a substring match on the raw text —\n // so only the value comparisons take the normalised reference.\n if (operator === 'set') return `${rawCol} IS NOT NULL`;\n if (operator === 'notSet') return `${rawCol} IS NULL`;\n\n if (operator === 'in' || operator === 'notIn') {\n if (!values || values.length === 0) return null;\n // Dates can legitimately appear in an `in`/`notIn` set (e.g. a multi-day\n // KPI), so coerce each element to the column's storage form too — same\n // SQLite epoch-vs-text root cause as the scalar operators below.\n const placeholders = values.map(v => { params.push(this.coerceTemporal(ctx, target, v)); return `$${params.length}`; }).join(', ');\n return `${this.temporalColumn(ctx, target, rawCol)} ${operator === 'in' ? 'IN' : 'NOT IN'} (${placeholders})`;\n }\n\n const sqlOp = opMap[operator];\n if (!sqlOp || !values || values.length === 0) return null;\n\n // The LIKE family reads the column as stored — a substring/prefix/suffix\n // match is on the raw text — so it keeps the un-normalised reference.\n const shape = likeShape[operator];\n if (shape) {\n // [#5567] Escaped pattern AND an explicit `ESCAPE`, bound together: the\n // escaping alone would search for a literal backslash on SQLite (no\n // default escape character there), the clause alone would change nothing.\n params.push(likePattern(shape, values[0]));\n const patternRef = `$${params.length}`;\n params.push(LIKE_ESCAPE_CHAR);\n // [#6520] `$icontains` folds ASCII case on BOTH sides. Only this operator\n // folds: the rest of the family is case-EXACT by ruling (#4706 Q2 = A),\n // and `objectql-strategy.ts`'s echo of this statement carries the same\n // `fold` flag on the same single row so the two keep describing one query.\n if (operator === 'icontains') {\n return `${asciiLowerSqlExpr(rawCol)} ${sqlOp} ${asciiLowerSqlExpr(patternRef)} ESCAPE $${params.length}`;\n }\n return `${rawCol} ${sqlOp} ${patternRef} ESCAPE $${params.length}`;\n }\n\n // A bare-day `lte` bound means \"through that whole day\" (#3777): compile\n // half-open (`< day+1`) so a datetime column keeps the final day's rows.\n // Equivalent to `<=` for a `date` column, so no column-type lookup needed.\n if (operator === 'lte') {\n const nextDay = nextUtcCalendarDay(values[0]);\n if (nextDay != null) {\n params.push(this.coerceTemporal(ctx, target, nextDay));\n return `${this.temporalColumn(ctx, target, rawCol)} < $${params.length}`;\n }\n }\n\n // Coerce so booleans/numbers bind as their native SQL types AND so a\n // relative-date / ISO-string comparand on a SQLite `Field.datetime`\n // column is converted to its INTEGER epoch storage form. Without this a\n // dashboard filter like `assessed_at >= '2025-06-18'` compiles to a\n // TEXT-vs-INTEGER affinity compare that is always false → \"No rows\",\n // even though the rows exist (the confirmed time-series chart bug).\n params.push(this.coerceTemporal(ctx, target, values[0]));\n return `${this.temporalColumn(ctx, target, rawCol)} ${sqlOp} $${params.length}`;\n }\n\n private extractObjectName(cube: Cube): string {\n return cube.sql.trim();\n }\n\n private buildFieldMeta(query: AnalyticsQuery, cube: Cube): Array<{ name: string; type: string }> {\n const fields: Array<{ name: string; type: string }> = [];\n if (query.dimensions) {\n for (const dim of query.dimensions) {\n const d = this.lookupMember(cube, dim, 'dimension');\n fields.push({ name: dim, type: d?.type || 'string' });\n }\n }\n if (query.measures) {\n for (const m of query.measures) {\n fields.push({ name: m, type: 'number' });\n }\n }\n return fields;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { AnalyticsQuery, AnalyticsResult } from '@objectstack/spec/contracts';\nimport type { Cube } from '@objectstack/spec/data';\n// [#8220] The read-scope provenance mark: `withReadScope` below is one of the\n// two merge boundaries that stamp it.\nimport { markFilterSubtreeProvenance } from '@objectstack/spec/data';\nimport type { AnalyticsStrategy, StrategyContext, DatasetScopedStrategyContext } from './types.js';\nimport {\n invalidFilterError,\n lowerAnalyticsWhere,\n normalizeAnalyticsFilterTree,\n collectFilterLeaves,\n SQL_CONST_FALSE,\n SQL_CONST_TRUE,\n type NormalizedFilterNode,\n} from './filter-normalizer.js';\nimport { findCrossFieldComparand, isFieldReference } from '../comparand-shape.js';\nimport { compileScopedFilterToSql } from '../read-scope-sql.js';\nimport { invalidMemberError } from '../dataset-refusal.js';\nimport { likePattern, LIKE_ESCAPE_CHAR, asciiLowerSqlExpr, type LikeShape } from '../like-pattern.js';\nimport { nextUtcCalendarDay } from '@objectstack/core';\nimport {\n rebucketCrossObject,\n RECOMBINABLE_METHODS,\n type CrossObjectDim,\n type MeasureRecombine,\n type RecombinableMethod,\n} from './cross-object-rebucket.js';\n\n/**\n * [#10861] Where a member in the cross-object envelope's inventory came from.\n *\n * Two producers put predicates in front of `engine.aggregate` on this path: the\n * caller's own `where`, and — since PR #10758 — the compiled dataset's\n * definition-level `filter`. Both are judged by the same envelope check; only\n * the DIAGNOSTIC differs, because only one of them names a key the caller sent.\n */\ntype FilterMemberOrigin = 'where' | 'dataset-filter';\n\n/** Scalar analytics operators → their SQL spelling (display SQL only). */\nconst SCALAR_SQL_OPS: Record<string, string> = {\n equals: '=', notEquals: '!=', gt: '>', gte: '>=', lt: '<', lte: '<=',\n};\n\n/**\n * The LIKE family: SQL spelling + where each one puts the wildcard.\n *\n * Deliberately the same pair of tables `NativeSQLStrategy.buildFilterClause`\n * carries (`opMap` / `likeShape`), because this file renders a description of\n * the statement THAT compiler produces. Keeping them as one table here is the\n * point of #5333: `startsWith` / `endsWith` were in neither the branch above nor\n * `SCALAR_SQL_OPS`, so they fell to the unmapped exit and the predicate vanished\n * from the echo while the query it documents ran `LIKE 'w%'`.\n *\n * [#5567] The pattern comes from the shared `likePattern`, which ESCAPES the\n * comparand, and the renderer binds an explicit `ESCAPE` alongside it. That is\n * not cosmetic for an echo: the execution this file describes goes through the\n * engine to `driver-sql`, whose `applyLike` has always escaped and bound\n * `ESCAPE`. Rendering the raw comparand meant the echoed statement was WIDER\n * than the query it claims to reproduce whenever the comparand carried a `_` or\n * `%` — the #3601 / #3602 / #3650 failure this render block exists to prevent.\n */\nconst LIKE_SQL_OPS: Record<string, { sql: string; shape: LikeShape; fold?: boolean }> = {\n contains: { sql: 'LIKE', shape: 'contains' },\n notContains: { sql: 'NOT LIKE', shape: 'contains' },\n startsWith: { sql: 'LIKE', shape: 'starts' },\n endsWith: { sql: 'LIKE', shape: 'ends' },\n // [#6520] `$icontains`: the same escaped pattern and bound `ESCAPE` as its\n // four case-EXACT neighbours, with `fold` adding the ASCII-only case fold to\n // both sides of the comparison. The flag is on this row alone — the family\n // above it is case-sensitive by ruling (#4706 Q2 = A).\n icontains: { sql: 'LIKE', shape: 'contains', fold: true },\n};\n\n/** One cross-object grouping dimension planned for FK-expand (#3654). */\ninterface CrossObjectPlanDim {\n /** The caller's dimension name (output key), e.g. `region`. */\n outputName: string;\n /** The base lookup FK column to group the base aggregate by, e.g. `account`. */\n fkField: string;\n /** The related object's attribute to resolve the FK to, e.g. `region`. */\n attr: string;\n /** The related object name (join target), e.g. `crm_account`. */\n refObject: string;\n}\n\ninterface CrossObjectPlan {\n crossDims: CrossObjectPlanDim[];\n}\n\n/**\n * ObjectQLStrategy — Priority 2\n *\n * Translates an analytics query into an ObjectQL `engine.aggregate()` call.\n * This path works with any driver that supports the ObjectQL aggregate AST\n * (Postgres, Mongo, SQLite, etc.) without requiring raw SQL access.\n */\nexport class ObjectQLStrategy implements AnalyticsStrategy {\n readonly name = 'ObjectQLStrategy';\n readonly priority = 20;\n\n canHandle(query: AnalyticsQuery, ctx: StrategyContext): boolean {\n if (!query.cube) return false;\n const caps = ctx.queryCapabilities(query.cube);\n return caps.objectqlAggregate && typeof ctx.executeAggregate === 'function';\n }\n\n async execute(query: AnalyticsQuery, ctx: StrategyContext): Promise<AnalyticsResult> {\n const cube = ctx.getCube(query.cube!)!;\n const objectName = this.extractObjectName(cube);\n\n // Build groupBy from dimensions, honouring `timeDimensions` granularity.\n // A date dimension with a granularity becomes a STRUCTURED groupBy item\n // `{ field, dateGranularity }` — which `engine.aggregate()` buckets (driver\n // date_trunc or in-memory). Without this the ObjectQL path grouped raw\n // timestamps (one bucket per row) and date-bucketed dataset widgets never\n // matched their legacy `categoryGranularity` counterpart.\n type GroupByItem = string | { field: string; dateGranularity: string };\n const granByDim = new Map<string, string>();\n for (const td of query.timeDimensions ?? []) {\n if (td.granularity) granByDim.set(td.dimension, td.granularity);\n }\n const groupBy: GroupByItem[] = [];\n if (query.dimensions && query.dimensions.length > 0) {\n for (const dim of query.dimensions) {\n const field = this.resolveFieldName(cube, dim, 'dimension');\n const gran = granByDim.get(dim);\n groupBy.push(gran ? { field, dateGranularity: gran } : field);\n granByDim.delete(dim);\n }\n }\n // Time dimensions not also listed in `dimensions` still bucket + group.\n for (const [dim, gran] of granByDim) {\n groupBy.push({ field: this.resolveFieldName(cube, dim, 'dimension'), dateGranularity: gran });\n }\n\n // Build aggregations from measures\n const aggregations: Array<{ field: string; method: string; alias: string }> = [];\n if (query.measures && query.measures.length > 0) {\n for (const measure of query.measures) {\n const { field, method } = this.resolveMeasureAggregation(cube, measure);\n aggregations.push({ field, method, alias: measure });\n }\n }\n\n // Build the engine filter. Every predicate — the caller's `where` and the\n // time-dimension windows alike — is contributed through\n // `mergeFilterOperand`, because one field routinely carries MULTIPLE\n // operators (a range `{$gte, $lte}` on `close_date`) and a plain assignment\n // would keep only the last.\n const filter: Record<string, unknown> = {};\n // Operands that cannot merge into their field's entry without one silently\n // replacing the other; ANDed in below so the engine intersects them.\n const conjuncts: Record<string, unknown>[] = [];\n this.applyFilterNode(normalizeAnalyticsFilterTree(query), cube, filter, conjuncts);\n // #3650 — and the time-dimension WINDOWS, through the SAME merge, so a\n // `dateRange` and a caller `where` bound on one field compose instead of\n // clobbering each other.\n for (const { field, bounds } of this.dateRangeBounds(cube, query)) {\n const extra = this.mergeFilterOperand(filter, field, bounds);\n if (extra) conjuncts.push(extra);\n }\n // [#10413 phase 1] The compiled dataset's OWN scope — the half of the\n // declaration the Cube model has no room for, read from the same channel\n // `NativeSQLStrategy` reads it from (#10298). This door addresses the\n // REGISTERED CUBE directly and never passes through `DatasetExecutor`, so\n // the definition-level `filter` reached `engine.aggregate` nowhere: the call\n // went out with no `filter` key at all and every measure aggregated the\n // whole table, while the dashboard door — same cube, same measure names —\n // answered the scoped numbers. `undefined` for a cube that is not a compiled\n // dataset, which is why an inferred or manifest cube compiles unchanged.\n //\n // ANDed as its own conjunct rather than merged key by key, for the reason\n // `withReadScope` gives below: the caller's `where` and the dataset's scope\n // can name the SAME field, and a spread would let one silently overwrite the\n // other. Placed before the `$and` fold, so it travels on the cross-object\n // path (`executeCrossObject`) as well as the direct one.\n //\n // PHASE 1 ONLY. `datasetScope.measureFilters` is deliberately NOT read here:\n // an aggregation is `{ field, method, alias }`, so a per-measure predicate\n // cannot be expressed on this contract at all, and folding one into this\n // whole-call filter would narrow EVERY measure — trading a wrong `won_count`\n // for a wrong `opp_count` as well. Widening the aggregate contract is\n // #10576; lowering the measure filters into it is phase 2 of #10413, and\n // `objectql-dataset-filter.test.ts` pins the gap open until then.\n const datasetScope = (ctx as DatasetScopedStrategyContext).getDatasetScope?.(query.cube!);\n if (datasetScope?.filter) {\n // `null` = constrains nothing, which is the AND identity — nothing to add,\n // and nothing invented for a filter that says nothing.\n const scopeCondition = this.filterNodeToCondition(\n normalizeAnalyticsFilterTree({ where: datasetScope.filter }),\n cube,\n );\n if (scopeCondition) conjuncts.push(scopeCondition);\n }\n if (conjuncts.length > 0) {\n filter.$and = [...(Array.isArray(filter.$and) ? filter.$and : []), ...conjuncts];\n }\n\n // #3654 — classify cross-object references. A cross-object DIMENSION within\n // the supported envelope is served by an FK-expand (`executeCrossObject`);\n // everything the engine cannot serve (cross-object measures/filters,\n // multi-hop, non-recombinable measures) is REJECTED by `planCrossObject` —\n // the engine has no join, and a silent mis-bucket is worse than a loud\n // error. `null` ⇒ the query is base-only and takes the direct path below.\n //\n // [#10759] Judged on {@link filterMemberView} — EVERY member the `where`\n // touches — and NOT on the engine filter built above. The engine filter is\n // the wrong instrument for this question: an AND-ed leaf lands at its top\n // level and is seen, but anything structural (an `$or`, a `$not`, a nested\n // `$and` that cannot merge) is folded into `filter.$and`, so the only key\n // `planCrossObject` could see for it was the literal `$and` — never a\n // cross-object field name. A cross-object reference inside a combinator was\n // therefore invisible HERE while `generateSql()` — which has always asked\n // the flattened question — refused it, and the two doors answered\n // differently for one query. `/analytics/query` reached `engine.aggregate`\n // with a predicate the engine cannot join and silently mis-bucketed it,\n // which is the exact outcome #3654's loud refusal exists to prevent.\n const plan = this.planCrossObject(cube, query, this.filterMemberView(cube, query, ctx));\n if (plan) {\n return this.executeCrossObject(cube, query, aggregations, filter, plan, ctx);\n }\n\n // ADR-0021 D-C — the base object's read scope (tenant + RLS) MUST be ANDed\n // in before the query leaves the strategy (#3597). A base-only query has a\n // single object in play, so one base-object scope is sufficient here.\n const rows = await ctx.executeAggregate!(objectName, {\n // Structured groupBy items ({field, dateGranularity}) pass through the\n // executeAggregate bridge to engine.aggregate, which buckets them. The\n // contract types groupBy as string[]; the cast carries the richer shape.\n groupBy: groupBy.length > 0 ? (groupBy as unknown as string[]) : undefined,\n aggregations: aggregations.length > 0 ? aggregations : undefined,\n filter: this.withReadScope(objectName, filter, ctx),\n // ADR-0053 Phase 2 (D2): forward the reference tz so date buckets resolve\n // on that zone's calendar days. A non-UTC zone makes the engine bucket\n // in-memory (uniform across drivers); UTC/unset keeps the DB fast path.\n timezone: query.timezone,\n // ADR-0021 D-C (#3602): the second belt. `withReadScope` above is this\n // layer's own scoping; handing the engine the context makes ITS middleware\n // inject RLS too, so a future strategy that forgets `withReadScope` still\n // cannot read across tenants. Without it the operation reaches the engine\n // principal-less and plugin-security falls open — the #3597 shape.\n context: ctx.context,\n });\n\n // Remap short field names back to cube-qualified names. Driven by\n // `projectedDimensions`, so a `timeDimensions`-only bucket — grouped by\n // just above, and therefore present in `row` — reaches the caller instead\n // of being silently dropped (#4033).\n const mappedRows = rows.map(row => {\n const mapped: Record<string, unknown> = {};\n for (const dim of this.projectedDimensions(query)) {\n const shortName = this.resolveFieldName(cube, dim, 'dimension');\n if (shortName in row) mapped[dim] = row[shortName];\n }\n if (query.measures) {\n for (const m of query.measures) {\n // Alias was set to the full measure name\n if (m in row) mapped[m] = row[m];\n }\n }\n return mapped;\n });\n\n const fields = this.buildFieldMeta(query, cube);\n // Echo a representative SQL alongside the rows (#3588). `NativeSQLStrategy`\n // returns the statement it actually ran, and dataset responses surface that\n // string — it is how an author checks what their widget compiled to. This\n // path builds an AST, so it had nothing to echo, and the `sql` field simply\n // vanished from the response whenever a query was date-bucketed (native SQL\n // declines granularity, handing those queries here). An author reading the\n // response then couldn't tell \"bucketing is not implemented\" from \"this\n // strategy doesn't report\". Best-effort: rendering is a debugging aid and\n // must never fail a query that already ran.\n let sql: string | undefined;\n try {\n sql = (await this.generateSql(query, ctx)).sql;\n } catch {\n sql = undefined;\n }\n return sql ? { rows: mappedRows, fields, sql } : { rows: mappedRows, fields };\n }\n\n /**\n * Render a REPRESENTATIVE SQL string for an ObjectQL aggregate query.\n *\n * This path executes through `engine.aggregate()`, not raw SQL, so the string\n * is documentation rather than the literal statement — but it must be an\n * honest account of what the query does, because dataset responses echo it\n * and authors read it to verify their widget options landed (#3588). It\n * therefore renders date bucketing (`date_trunc`), the WHERE predicate,\n * ordering, and the row window.\n *\n * Filter VALUES are rendered as `$n` placeholders and returned in `params`,\n * never inlined: the echoed statement travels to the browser, and a filter\n * comparand can carry tenant data.\n */\n async generateSql(query: AnalyticsQuery, ctx: StrategyContext): Promise<{ sql: string; params: unknown[] }> {\n const cube = ctx.getCube(query.cube!);\n if (!cube) {\n throw new Error(`Cube not found: ${query.cube}`);\n }\n\n // [#7598, maintainer ruling 2026-08-12] The echo DECLINES a cross-field\n // comparison — 「`/analytics/sql` 的 echo 同样 decline(一致的响亮答案,\n // 不半渲染)」.\n //\n // This renderer describes an execution it does not perform, and there is no\n // honest description of a cross-field comparison available to it. The\n // reference reaches `engine.aggregate` intact and `driver-sql` compiles it\n // into a TOTAL column-to-column predicate — several repetitions of both\n // column expressions, so the answer matches the memory evaluator across\n // NULLs. What this file's `buildFilterClauseSql` can render is `amount >\n // $1` with the reference OBJECT in `params`: not a simplification of that\n // predicate but a different one, comparing a column against a value no row\n // can hold. Rendering it would hand a debugger SQL that reproduces NONE of\n // the rows the query returned — the #3601 / #3602 / #3650 failure this\n // whole render block exists to prevent, in its worst direction.\n //\n // Note what this does NOT affect: `execute()` calls `generateSql` inside a\n // `try`/`catch` precisely because the echo is a debugging aid that must\n // never fail a query that already ran, so `/analytics/query` still serves\n // these queries and returns rows — the response simply carries no `sql`\n // string. Only the dry-run face (`/analytics/sql`) refuses, which is the\n // \"one consistent, loud answer\" the ruling asked for.\n //\n // The READ SCOPE half needs no arm of its own: `compileScopedFilterToSql`\n // below still refuses a reference in its own fail-closed envelope\n // (`READ_SCOPE_COMPILE_FAILED` / 500, #5367 ruling kept verbatim by Q2 = A),\n // and that refusal is now reached from HERE rather than from\n // `NativeSQLStrategy.applyReadScope` — see `read-scope-sql.ts`'s header.\n const crossField = findCrossFieldComparand(this.loweredWhere(query));\n if (crossField) {\n throw invalidFilterError(\n `[analytics] cannot render display SQL for the field reference ` +\n `{ \"$field\": \"${crossField.ref}\" } under \"${crossField.op}\" on \"${crossField.field}\". ` +\n `The query itself is SERVED — \\`NativeSQLStrategy.canHandle\\` declines a cross-field ` +\n `comparison so it routes to the ObjectQL engine path, where driver-sql compiles it into a ` +\n `column-to-column predicate written TOTAL across NULLs and enforces the #5222 rulings ` +\n `(#7598, maintainer ruling 2026-08-12). This renderer has no faithful rendering of that ` +\n `predicate: what it can emit is a comparison against the reference object as a bound VALUE, ` +\n `which reproduces none of the rows the query returns. Refusing rather than half-rendering — ` +\n `an echo that contradicts execution is worse than no echo (#3601 / #3602 / #3650). Run the ` +\n `query itself (/analytics/query) to get its rows.`,\n );\n }\n\n const selectParts: string[] = [];\n const groupByParts: string[] = [];\n const params: unknown[] = [];\n\n // Date-bucketed dimensions render as `date_trunc('<granularity>', col)` —\n // the SQL shape the driver's own bucketing implements — so a `month` trend\n // no longer reads as if it grouped by the raw column.\n const granByDim = new Map<string, string>();\n for (const td of query.timeDimensions ?? []) {\n if (td.granularity) granByDim.set(td.dimension, td.granularity);\n }\n const tableName = this.extractObjectName(cube);\n // #3654 — plan cross-object dims (throws for out-of-envelope, so\n // `/analytics/sql` and `execute()` accept/reject the SAME set). An in-envelope\n // cross-object dim renders as a LEFT JOIN — its logical shape; `execute()`\n // serves it via FK-expand.\n // EVERY member the filter touches, including ones nested in an `$or` —\n // the envelope check rejects cross-object filters, so a member it cannot\n // see is a filter it cannot reject. [#10759] The same\n // {@link filterMemberView} `execute()` is judged on, as one expression\n // rather than two copies: \"the preview accepts/rejects the same set\" is an\n // invariant between two call sites, and two copies of a view can drift\n // apart while each stays individually correct — which is how they drifted.\n const plan = this.planCrossObject(cube, query, this.filterMemberView(cube, query, ctx));\n const crossByDim = new Map((plan?.crossDims ?? []).map((cd) => [cd.outputName, cd]));\n const joinClauses: string[] = [];\n const dimExpr = (dim: string): string => {\n const cd = crossByDim.get(dim);\n if (cd) {\n joinClauses.push(\n `LEFT JOIN \"${cd.refObject}\" ON \"${tableName}\".\"${cd.fkField}\" = \"${cd.refObject}\".\"id\"`,\n );\n return `\"${cd.refObject}\".\"${cd.attr}\"`;\n }\n const col = this.resolveFieldName(cube, dim, 'dimension');\n const gran = granByDim.get(dim);\n return gran ? `date_trunc('${gran}', ${col})` : col;\n };\n\n if (query.dimensions) {\n for (const dim of query.dimensions) {\n const expr = dimExpr(dim);\n selectParts.push(`${expr} AS \"${dim}\"`);\n groupByParts.push(expr);\n }\n }\n // A time dimension that is bucketed but not also listed in `dimensions`\n // still groups (see `execute`), so it belongs in the rendered GROUP BY too.\n for (const [dim] of granByDim) {\n if (query.dimensions?.includes(dim)) continue;\n const expr = dimExpr(dim);\n selectParts.push(`${expr} AS \"${dim}\"`);\n groupByParts.push(expr);\n }\n if (query.measures) {\n for (const m of query.measures) {\n const { field, method } = this.resolveMeasureAggregation(cube, m);\n const aggSql = method === 'count'\n ? 'COUNT(*)'\n : method === 'count_distinct'\n ? `COUNT(DISTINCT ${field})`\n : `${method.toUpperCase()}(${field})`;\n selectParts.push(`${aggSql} AS \"${m}\"`);\n }\n }\n\n // ADR-0021 D-C (#3602) — render the READ SCOPE too, not just the caller's\n // own filters (#3652 added those). Without it this string still reads as an\n // unscoped table scan while the real aggregate is scoped (#3601), so anyone\n // debugging a \"why is this row missing\" gets SQL that cannot reproduce the\n // result. Nothing leaks — the string is never executed, and scope VALUES\n // stay in `params`, which `execute()`'s echo discards — but a rendering\n // that contradicts execution is worse than no rendering.\n //\n // The cross-object guard runs here for the same reason: this must not\n // render SQL for a query `execute()` would reject outright (#3654).\n //\n // Faithfulness cuts both ways: the time-dimension WINDOWS render too, from\n // the same `dateRangeBounds` lowering `execute()` sends to the engine\n // (#3650). This comment used to explain why a BETWEEN was deliberately\n // absent — because `execute()` dropped the window and rendering one would\n // have invented a predicate. Now that it applies the window, omitting it\n // here would be the lie in the other direction.\n // (The cross-object envelope was already enforced by `planCrossObject` above,\n // so `/analytics/sql` rejects the same out-of-envelope set `execute()` does.)\n\n const whereParts: string[] = [];\n // Recursive, so the echoed statement carries the same disjunctions the\n // engine filter does — the echo exists to REPRODUCE execution, and an\n // `$or` rendered as a conjunction (or dropped) is exactly the lie this\n // block's comment above warns about, in the other direction.\n const filterClause = this.renderFilterNodeSql(\n normalizeAnalyticsFilterTree(query),\n cube,\n params,\n );\n if (filterClause) whereParts.push(filterClause);\n // [#10413] The dataset's own scope renders too, for the reason the read\n // scope does further down: `execute()` really applies this predicate now, and\n // an echo that OMITS an applied predicate is the same lie as one that invents\n // a predicate (#3601 / #3602 / #3650). Rendered from the same lowering\n // `execute()` uses, so the two cannot drift.\n const echoedDatasetFilter =\n (ctx as DatasetScopedStrategyContext).getDatasetScope?.(query.cube!)?.filter;\n if (echoedDatasetFilter) {\n const scopeSql = this.renderFilterNodeSql(\n normalizeAnalyticsFilterTree({ where: echoedDatasetFilter }),\n cube,\n params,\n );\n if (scopeSql) whereParts.push(scopeSql);\n }\n // Bounds bind as `$n` placeholders like every other comparand: this string\n // travels to the browser, and a window can carry tenant-derived dates.\n // A bare-day upper bound renders half-open (`< day+1`) because that is\n // what `execute()`'s driver actually runs for it on a datetime column\n // (#3777) — rendering the BETWEEN would hand a debugger SQL that drops\n // the final day's rows and cannot reproduce the result.\n for (const { field, bounds } of this.dateRangeBounds(cube, query)) {\n const nextDay = nextUtcCalendarDay(bounds.$lte);\n params.push(bounds.$gte, nextDay ?? bounds.$lte);\n whereParts.push(\n `(${field} >= $${params.length - 1} AND ${field} ${nextDay ? '<' : '<='} $${params.length})`,\n );\n }\n // Read scope last, so it reads as the outermost constraint. Compiled by the\n // same fail-closed compiler `NativeSQLStrategy` uses — it throws rather than\n // drop a predicate, which is the correct posture even for a display string:\n // silently omitting the scope is exactly the misleading output being fixed.\n const scope = ctx.getReadScope?.(tableName);\n if (scope != null) {\n const { sql: scopeSql, params: scopeParams } = compileScopedFilterToSql(scope, tableName);\n if (scopeSql) {\n let i = 0;\n // `compileScopedFilterToSql` emits `?`; renumber into this builder's $N.\n const rendered = scopeSql.replace(/\\?/g, () => {\n params.push(scopeParams[i++]);\n return `$${params.length}`;\n });\n whereParts.push(`(${rendered})`);\n }\n }\n\n let sql = `SELECT ${selectParts.join(', ')} FROM \"${tableName}\"`;\n if (joinClauses.length > 0) sql += ' ' + joinClauses.join(' ');\n if (whereParts.length > 0) {\n sql += ` WHERE ${whereParts.join(' AND ')}`;\n }\n if (groupByParts.length > 0) {\n sql += ` GROUP BY ${groupByParts.join(', ')}`;\n }\n if (query.order && Object.keys(query.order).length > 0) {\n const orderClauses = Object.entries(query.order).map(([f, d]) => `\"${f}\" ${d.toUpperCase()}`);\n sql += ` ORDER BY ${orderClauses.join(', ')}`;\n }\n if (query.limit != null) sql += ` LIMIT ${query.limit}`;\n if (query.offset != null) sql += ` OFFSET ${query.offset}`;\n\n return { sql, params };\n }\n\n // ── Helpers ──────────────────────────────────────────────────────\n\n /**\n * ADR-0021 D-C (#3597) — AND the object's read scope (tenant + RLS) into the\n * filter handed to `engine.aggregate`.\n *\n * This path used to drop the scope entirely, and the engine could not make up\n * for it: the aggregate bridge passes no `ExecutionContext`, so the security\n * middleware's principal-less fall-open skipped its own RLS injection. Both\n * belts were off at once — an authenticated caller received aggregates\n * computed over EVERY tenant's rows.\n *\n * Composed with `$and`, never by key merge: the query's own filter and the\n * scope can name the SAME field (e.g. a dashboard filtering `organization_id`),\n * and a spread would let caller input silently overwrite the security\n * predicate. `$and` makes that structurally impossible.\n */\n private withReadScope(\n objectName: string,\n filter: Record<string, unknown>,\n ctx: StrategyContext,\n ): Record<string, unknown> | undefined {\n // [#8220, A of #7929] This is the second read-scope MERGE BOUNDARY (the\n // first is plugin-security's CRUD injection), so the provenance mark is\n // stamped here: the scope is `'policy'` — a cross-field refusal from\n // inside it keeps the #7929 redaction — and the strategy-built user filter\n // is `'author'`: every name in it came from the caller's own query\n // (dimensions, measures, `where`, time windows) through this class's own\n // compilation, which is exactly the vouch the mark declares. Unmarked\n // content anywhere else stays withheld downstream, by the mark's declared\n // fail direction.\n const userFilter =\n Object.keys(filter).length > 0 ? markFilterSubtreeProvenance(filter, 'author') : undefined;\n if (typeof ctx.getReadScope !== 'function') return userFilter;\n const scope = ctx.getReadScope(objectName);\n if (scope === undefined || scope === null) return userFilter;\n const scopeFilter = markFilterSubtreeProvenance(scope as Record<string, unknown>, 'policy');\n if (!userFilter) return scopeFilter;\n return { $and: [userFilter, scopeFilter] };\n }\n\n /** Is `field` a resolved cross-object (relationship-traversal) reference? */\n private isCrossObjectField(cube: Cube, field: string, baseObject: string): boolean {\n if (!field.includes('.')) return false;\n const alias = field.split('.')[0];\n const joinedObject = cube.joins?.[alias]?.name ?? alias;\n return joinedObject !== baseObject;\n }\n\n /**\n * The member view {@link planCrossObject} judges a filter by: EVERY member\n * that will end up in the engine's predicate, structure discarded, keyed by\n * RESOLVED field name (#10759), valued by WHERE THE MEMBER CAME FROM\n * (#10861).\n *\n * Both call sites — `execute()` and `generateSql()` — are handed this and\n * nothing else, which is what makes the invariant `planCrossObject` states\n * for itself (\"the preview accepts/rejects the same set\") structural rather\n * than a coincidence maintained by hand. They used to build the view\n * separately: the echo flattened the tree, `execute()` passed the ENGINE\n * FILTER, and a filter record answers a different question — it is a\n * predicate to evaluate, not an inventory of members. An `$or`, a `$not` or\n * an unmergeable nested `$and` travels in it as one opaque `$and` entry, so\n * the members inside were unreadable from the outside and the envelope check\n * could not reject what it could not see.\n *\n * ## Two producers, one inventory (#10861)\n *\n * The caller's `where` is not the only thing that reaches `engine.aggregate`\n * as a predicate. Since PR #10758 the compiled dataset's own definition-level\n * `filter` is lowered onto `execute()`'s `conjuncts` and rendered by\n * `generateSql()`, so a dataset declaring `filter: { 'account.region': 'West' }`\n * sent `{\"$and\":[{\"account.region\":\"West\"}]}` to an engine that cannot join —\n * measured on both doors, which AGREED in accepting it, so #10759's\n * preview/execution symmetry had nothing to restore. Refusing it is a\n * widening of the refusal set, ruled by the maintainer on 2026-08-22 (Option\n * A, query-time refusal): fold the scope's leaves in HERE, where driver\n * capability is known, rather than in `dataset-compiler.ts`, which cannot see\n * which driver will serve the dataset and would refuse a dataset that is\n * perfectly legal on a native-SQL deployment.\n *\n * Structure is discarded on purpose — a member is cross-object or it is not,\n * and which branch of a disjunction it sits in cannot make\n * `engine.aggregate` able to join it. PROVENANCE is not discarded, because it\n * decides what the refusal can tell the caller to go fix: `AnalyticsRequestKey`\n * is the analytics REQUEST vocabulary and a dataset's `filter` is not in it,\n * so a scope-borne member must not be reported as `param: 'where'` — see\n * `planCrossObject`. The value slot carries that and nothing else; it never\n * reaches a driver.\n *\n * Dataset leaves are inserted FIRST so a member named by BOTH producers keeps\n * the caller's provenance (last write wins on a duplicate key): if it is in\n * the request too, the request is the actionable place to fix it.\n *\n * Time-dimension WINDOWS are deliberately absent (they live in\n * `dateRangeBounds`, not in `where`). They need no arm here: a cross-object\n * time dimension is refused by `planCrossObject`'s own first loop, over\n * `query.timeDimensions`, and refused as the time dimension the author wrote\n * rather than as the lowered predicate it becomes — which is the better\n * diagnostic and the reason that loop runs first.\n */\n private filterMemberView(\n cube: Cube,\n query: AnalyticsQuery,\n ctx: StrategyContext,\n ): Record<string, FilterMemberOrigin> {\n // Read from the SAME channel both doors lower the scope from, so the view\n // and the predicate cannot disagree about what the engine will receive.\n const datasetFilter = (ctx as DatasetScopedStrategyContext).getDatasetScope?.(query.cube!)?.filter;\n const leaves = (node: ReturnType<typeof normalizeAnalyticsFilterTree>, origin: FilterMemberOrigin) =>\n collectFilterLeaves(node).map(\n (f) => [this.resolveFieldName(cube, f.member, 'any'), origin] as const,\n );\n return Object.fromEntries([\n ...(datasetFilter\n ? leaves(normalizeAnalyticsFilterTree({ where: datasetFilter }), 'dataset-filter')\n : []),\n ...leaves(normalizeAnalyticsFilterTree(query), 'where'),\n ]);\n }\n\n /**\n * Plan how to serve cross-object references on this join-less path (#3654).\n *\n * `engine.aggregate()` cannot join. A cross-object DIMENSION within a\n * supported envelope is served by an FK-expand (`executeCrossObject`): group\n * the base aggregate on the lookup FK, resolve the FK to the related attribute\n * with a SCOPED read, re-bucket in memory. Returns `null` for a base-only\n * query (direct path), a plan for an in-envelope cross-object query.\n *\n * THROWS for anything outside the envelope — a cross-object MEASURE or FILTER\n * (needs a real join to evaluate), a cross-object leaf in the DATASET's own\n * definition-level `filter` (#10861 — same join it does not have, arriving\n * from the producer PR #10758 added), a MULTI-HOP dimension (`a.b.c`), or a\n * non-recombinable measure (`avg`/`count_distinct`, whose sub-bucket values\n * cannot be merged). A loud error beats the silent mis-bucket #3654 kills.\n * `generateSql()` calls this too, so the preview accepts/rejects the same set\n * — and since #10759 both callers derive `filter` from the one\n * {@link filterMemberView}, so that sentence is enforced by construction\n * instead of restated at two call sites.\n *\n * [#5716] All five refusals below are `invalidMemberError` — `INVALID_FIELD` /\n * 400, naming the member — and the four that predate #10861 keep their\n * MESSAGES unchanged (they are good diagnostics, and #5923's tests read\n * them). Each is decided by two facts and nothing else: a member that will\n * reach the engine's predicate, and whether that member resolves across a\n * join. Neither is an internal invariant — a cube where the member exists and\n * a driver that could serve it are both perfectly ordinary, which is exactly\n * what the \"run this on a native-SQL driver\" half of every message says. They\n * are member-level rather than dataset-level (hence not `datasetInvalidError`)\n * because the fix is always to change or drop ONE named member, and because\n * four of them fire on `/analytics/query` where no dataset exists.\n *\n * [#10861] The fifth is the exception that proves the rule and is written to\n * it: it can only fire where a dataset DOES exist, and it is the one refusal\n * here whose member no request key named — so it carries `cube` and no\n * `param`, and says in its own words which document to go and edit. It stays\n * `INVALID_FIELD` rather than becoming `DATASET_INVALID` because the verdict\n * is the same physical one as its neighbour — this engine cannot join this\n * member — and splitting the code by PROVENANCE would make a caller branch on\n * two wire shapes for one capability limit.\n *\n * Detection is on RESOLVED field names, so a dotted dimension the cube\n * flattens to a real column is treated as base, not cross-object.\n */\n private planCrossObject(\n cube: Cube,\n query: AnalyticsQuery,\n filter: Record<string, FilterMemberOrigin>,\n ): CrossObjectPlan | null {\n const baseObject = this.extractObjectName(cube);\n\n // A date bucket over a related object's field is not supported. Checked\n // FIRST: since #3650 a `dateRange` also lands in `filter`, so a cross-object\n // time dimension would otherwise be reported as a \"cross-object filter\" —\n // true of the lowered predicate, but not what the author wrote.\n for (const td of query.timeDimensions ?? []) {\n const field = this.resolveFieldName(cube, td.dimension, 'dimension');\n if (this.isCrossObjectField(cube, field, baseObject)) {\n throw invalidMemberError(\n `[Analytics] ObjectQLStrategy cannot bucket a cross-object time dimension (\"${field}\").`,\n { member: td.dimension, param: 'timeDimensions', cube: cube.name },\n );\n }\n }\n\n // A cross-object MEASURE or FILTER can only be evaluated with a real join.\n // [#5716] `member` is the entry AS THE REQUEST SPELLED IT (`revenue`), which\n // is what a caller can act on; `field` is what it RESOLVED to\n // (`account.balance`), which is what the message explains the refusal with.\n // The measure's request spelling used to be dropped here — the map kept only\n // the resolved field — so the envelope had nothing to name.\n const nonDim = [\n ...(query.measures ?? []).map((m) => ({\n where: 'measure', member: m, field: this.resolveMeasureAggregation(cube, m).field,\n })),\n ...Object.entries(filter)\n .filter(([, origin]) => origin === 'where')\n .map(([f]) => ({ where: 'filter', member: f, field: f })),\n ].filter((r) => this.isCrossObjectField(cube, r.field, baseObject));\n if (nonDim.length > 0) {\n throw invalidMemberError(\n `[Analytics] ObjectQLStrategy cannot evaluate a cross-object ${nonDim[0].where} ` +\n `(\"${nonDim[0].field}\") — the engine cannot join in an aggregate. Run this ` +\n `query on a native-SQL driver, or remove the cross-object ${nonDim[0].where}.`,\n {\n member: nonDim[0].member,\n // The two kinds share one throw, so the request key follows the kind\n // rather than being guessed by the reader of the message.\n param: nonDim[0].where === 'measure' ? 'measures' : 'where',\n cube: cube.name,\n },\n );\n }\n\n // [#10861] The same verdict for a member the CALLER never named: a leaf of\n // the compiled dataset's own definition-level `filter`. Checked after the\n // caller's own members, so a request that names a cross-object member keeps\n // the diagnostic it can act on directly, and so every shape that was\n // refused before this card is refused with the identical message.\n //\n // Same family and same envelope as the arm above — `INVALID_FIELD` / 400 —\n // because it is the same physical verdict about the same member: this\n // engine has no join. A caller should not have to branch on two codes for\n // one capability limit.\n //\n // `param` is deliberately ABSENT rather than `'where'`. `AnalyticsRequestKey`\n // is the analytics REQUEST vocabulary, and `AnalyticsQuerySchema` is strict:\n // there is no `filter` key on a query (its `guidance` sends `filters` to\n // `where`), and the request's `where` may be empty or absent entirely. Both\n // `param: 'where'` and a widened `param: 'filter'` would name a key the\n // caller cannot go and edit — the exact \"never suggest a key the schema\n // cannot accept\" trap `strict-object.ts` documents. `cube` carries the\n // locator that IS actionable: the dataset whose definition holds the leaf.\n const scopeCross = Object.entries(filter)\n .filter(([field, origin]) =>\n origin === 'dataset-filter' && this.isCrossObjectField(cube, field, baseObject))\n .map(([field]) => field);\n if (scopeCross.length > 0) {\n throw invalidMemberError(\n `[Analytics] ObjectQLStrategy cannot evaluate the cross-object filter ` +\n `(\"${scopeCross[0]}\") that dataset \"${cube.name}\" declares at its definition ` +\n `level — the engine cannot join in an aggregate, so this predicate matches ` +\n `nothing and the answer would be neither the scoped number nor the unscoped ` +\n `one. Nothing in the request names it: remove the cross-object leaf from the ` +\n `dataset's own \\`filter\\`, or serve this dataset on a native-SQL driver, ` +\n `where the same definition is valid.`,\n { member: scopeCross[0], cube: cube.name },\n );\n }\n\n // Collect cross-object DIMENSIONS (single-hop only).\n const crossDims: CrossObjectPlanDim[] = [];\n for (const dim of query.dimensions ?? []) {\n const field = this.resolveFieldName(cube, dim, 'dimension');\n if (!this.isCrossObjectField(cube, field, baseObject)) continue;\n const [alias, ...rest] = field.split('.');\n const attr = rest.join('.');\n if (attr.includes('.')) {\n throw invalidMemberError(\n `[Analytics] ObjectQLStrategy supports only single-hop cross-object ` +\n `dimensions; \"${field}\" traverses more than one relationship.`,\n { member: dim, param: 'dimensions', cube: cube.name },\n );\n }\n crossDims.push({ outputName: dim, fkField: alias, attr, refObject: cube.joins?.[alias]?.name ?? alias });\n }\n\n if (crossDims.length === 0) return null;\n\n // Every measure must re-combine across the intermediate FK sub-buckets.\n for (const m of query.measures ?? []) {\n const { method } = this.resolveMeasureAggregation(cube, m);\n if (!RECOMBINABLE_METHODS.has(method)) {\n throw invalidMemberError(\n `[Analytics] ObjectQLStrategy cannot group by a cross-object dimension ` +\n `with a \"${method}\" measure (\"${m}\") — its value cannot be recombined ` +\n `across the intermediate FK grouping. Use sum/count/min/max, or run on ` +\n `a native-SQL driver.`,\n { member: m, param: 'measures', cube: cube.name },\n );\n }\n }\n\n return { crossDims };\n }\n\n /**\n * Serve a cross-object-dimension query by FK-expand (#3654). The pure\n * re-bucketing step lives in `cross-object-rebucket.ts`.\n */\n private async executeCrossObject(\n cube: Cube,\n query: AnalyticsQuery,\n aggregations: Array<{ field: string; method: string; alias: string }>,\n filter: Record<string, unknown>,\n plan: CrossObjectPlan,\n ctx: StrategyContext,\n ): Promise<AnalyticsResult> {\n const baseObject = this.extractObjectName(cube);\n const crossByDim = new Map(plan.crossDims.map((cd) => [cd.outputName, cd]));\n\n // Rewrite group-by: a cross-object dim becomes its base FK column; base and\n // time dims pass through. `baseDimFields` are the group keys carried into\n // the re-bucket verbatim (the FK columns are replaced by resolved attrs).\n type GroupByItem = string | { field: string; dateGranularity: string };\n const granByDim = new Map<string, string>();\n for (const td of query.timeDimensions ?? []) {\n if (td.granularity) granByDim.set(td.dimension, td.granularity);\n }\n const groupBy: GroupByItem[] = [];\n const baseDimFields: string[] = [];\n for (const dim of query.dimensions ?? []) {\n const cd = crossByDim.get(dim);\n if (cd) {\n groupBy.push(cd.fkField);\n continue;\n }\n const field = this.resolveFieldName(cube, dim, 'dimension');\n const gran = granByDim.get(dim);\n groupBy.push(gran ? { field, dateGranularity: gran } : field);\n baseDimFields.push(field);\n granByDim.delete(dim);\n }\n for (const [dim, gran] of granByDim) {\n const field = this.resolveFieldName(cube, dim, 'dimension');\n groupBy.push({ field, dateGranularity: gran });\n baseDimFields.push(field);\n }\n\n // Base aggregate, grouped by the FK, scoped to the base object. Threads the\n // ExecutionContext for the engine-side second belt too (#3602).\n const baseRows = await ctx.executeAggregate!(baseObject, {\n groupBy: groupBy.length > 0 ? (groupBy as unknown as string[]) : undefined,\n aggregations: aggregations.length > 0 ? aggregations : undefined,\n filter: this.withReadScope(baseObject, filter, ctx),\n timezone: query.timezone,\n context: ctx.context,\n });\n\n // Resolve each cross-object dim's FK → attribute, SCOPED to the referenced\n // object: a related record the caller cannot read never yields its\n // attribute, so it buckets as RESTRICTED (no leak; ADR-0021 D-C / #3602).\n const resolvedDims: CrossObjectDim[] = [];\n for (const cd of plan.crossDims) {\n const fkValues = [...new Set(baseRows.map((r) => r[cd.fkField]).filter((v) => v != null))];\n const fkToAttr = await this.resolveFkAttr(cd.refObject, cd.attr, fkValues, ctx);\n resolvedDims.push({ outputName: cd.outputName, fkField: cd.fkField, fkToAttr });\n }\n\n const measures: MeasureRecombine[] = (query.measures ?? []).map((m) => ({\n alias: m,\n // planCrossObject already asserted every measure is recombinable.\n method: this.resolveMeasureAggregation(cube, m).method as RecombinableMethod,\n }));\n\n const merged = rebucketCrossObject(baseRows, baseDimFields, resolvedDims, measures);\n\n // Map resolved group keys back to the caller's dimension names.\n const mappedRows = merged.map((row) => {\n const out: Record<string, unknown> = {};\n // Same projection set as the direct path and `buildFieldMeta`\n // ({@link projectedDimensions}) — a cross-object dimension carries the\n // caller's name already, everything else is remapped from its short name.\n for (const dim of this.projectedDimensions(query)) {\n if (crossByDim.has(dim)) {\n if (dim in row) out[dim] = row[dim];\n } else {\n const field = this.resolveFieldName(cube, dim, 'dimension');\n if (field in row) out[dim] = row[field];\n }\n }\n for (const m of query.measures ?? []) {\n if (m in row) out[m] = row[m];\n }\n return out;\n });\n\n return { rows: mappedRows, fields: this.buildFieldMeta(query, cube) };\n }\n\n /**\n * Resolve `fkValues` (ids of `refObject`) to their `attr` values, applying the\n * referenced object's OWN read scope (#3654 / #3602). Reuses the aggregate\n * bridge — `group by (id, attr)` is one row per record. Ids the scope hides\n * are simply absent from the map (⇒ RESTRICTED bucket downstream).\n */\n private async resolveFkAttr(\n refObject: string,\n attr: string,\n fkValues: unknown[],\n ctx: StrategyContext,\n ): Promise<Map<unknown, unknown>> {\n const map = new Map<unknown, unknown>();\n if (fkValues.length === 0 || typeof ctx.executeAggregate !== 'function') return map;\n const idFilter: Record<string, unknown> = { id: { $in: fkValues } };\n const scope = typeof ctx.getReadScope === 'function' ? ctx.getReadScope(refObject) : null;\n // [#8220] Same boundary family as `withReadScope`: the scope arm is policy.\n // `idFilter` is this method's own plumbing, not the caller's text — it\n // stays unmarked, which withholds, and that is correct for a filter no\n // author typed.\n if (scope != null) markFilterSubtreeProvenance(scope, 'policy');\n const filter = scope != null ? { $and: [idFilter, scope] } : idFilter;\n const rows = await ctx.executeAggregate(refObject, {\n groupBy: ['id', attr],\n aggregations: [{ field: 'id', method: 'count', alias: '_c' }],\n filter,\n context: ctx.context,\n });\n for (const r of rows) {\n if (r.id != null) map.set(r.id, r[attr]);\n }\n return map;\n }\n\n /**\n * Render one normalized filter as a display SQL predicate for `generateSql`.\n *\n * Mirrors `NativeSQLStrategy.buildFilterClause`'s operator vocabulary so the\n * two previews read alike, but binds the comparand VERBATIM: the value shown is\n * the one THIS path actually hands the engine (a real boolean, not SQL's 1/0).\n *\n * [#5526] \"Verbatim\" is now literal. This used to bind through\n * `coerceFilterValueForObjectQL`, which decoded the string a `string[]` leaf\n * carried back into a type — so an echo could show `7` for a filter the author\n * wrote as `'007'`. A leaf carries the author's value at its own type, so the\n * echo needs no conversion at all to stay honest about execution. The LIKE\n * family is still the one exception, for the reason `filter.zod.ts` gives: its\n * comparand is declared a `string`, and what binds is the PATTERN.\n *\n * `null` means \"this leaf carries no predicate\" — a value-less scalar leaf,\n * which `execute()` and `NativeSQLStrategy` drop too. It does NOT mean \"I could\n * not render that operator\": #5333 was exactly that conflation, and an\n * unrenderable operator now THROWS (see the exit below).\n */\n private buildFilterClauseSql(\n col: string,\n operator: string,\n values: unknown[] | undefined,\n params: unknown[],\n ): string | null {\n if (operator === 'set') return `${col} IS NOT NULL`;\n if (operator === 'notSet') return `${col} IS NULL`;\n\n if (!values || values.length === 0) return null;\n\n if (operator === 'in' || operator === 'notIn') {\n const placeholders = values\n .map((v) => { params.push(v); return `$${params.length}`; })\n .join(', ');\n return `${col} ${operator === 'in' ? 'IN' : 'NOT IN'} (${placeholders})`;\n }\n\n // The LIKE family binds its PATTERN, which is text by construction — same\n // reason `NativeSQLStrategy` keeps the un-normalised column reference for\n // these: a prefix/suffix/substring match reads the column as stored.\n const like = LIKE_SQL_OPS[operator];\n if (like) {\n // [#5567] Escaped pattern + an explicit `ESCAPE`, matching what\n // `driver-sql`'s `applyLike` binds for the same operator — so an author\n // who copies this statement out runs the predicate that ran.\n params.push(likePattern(like.shape, values[0]));\n const patternRef = `$${params.length}`;\n params.push(LIKE_ESCAPE_CHAR);\n // [#6520] The fold, when the operator carries one, wraps BOTH sides:\n // folding only the comparand compares a folded needle against a raw column\n // and returns just the rows that were already lower-case — a wrong row set\n // that looks like a working predicate.\n const lhs = like.fold ? asciiLowerSqlExpr(col) : col;\n const rhs = like.fold ? asciiLowerSqlExpr(patternRef) : patternRef;\n return `${lhs} ${like.sql} ${rhs} ESCAPE $${params.length}`;\n }\n\n const op = SCALAR_SQL_OPS[operator];\n if (!op) {\n // [#5333] THROW rather than `return null`. `renderFilterNodeSql` reads a\n // `null` as \"this node constrains nothing\", so the old exit deleted the\n // predicate from the echoed statement — and a rendering WIDER than\n // execution is the failure this whole render block exists to prevent\n // (#3601 / #3602 / #3650): the author runs it to reproduce a result, gets\n // more rows, and concludes the filter never applied.\n //\n // It can throw because the vocabulary upstream is CLOSED: `fieldLeaves`\n // in `filter-normalizer.ts` is the only producer of leaf nodes, and it\n // refuses an operator outside `MONGO_TO_CUBE_OP` with `INVALID_FILTER` /\n // 400 before a leaf exists. So no caller-authored filter can land here —\n // an arrival means the normalizer's table gained an entry this renderer\n // has no arm for, which is our bug, not the caller's, and the one answer\n // that must never be given for it is a silently wider query. Same call\n // `convertFilter`'s `default:` arm made when it stopped reading an\n // unmapped operator as equality (#4128). Deliberately NOT\n // `invalidFilterError`'s 400 envelope: this is drift between two of our\n // own tables, not a caller-shaped mistake.\n throw new Error(\n `[analytics] ObjectQLStrategy cannot render display SQL for filter operator ` +\n `\"${operator}\" (on \"${col}\"). The analytics operator vocabulary is closed — ` +\n `filter-normalizer.ts refuses anything it cannot map — so this means a new ` +\n `operator reached the normalizer without an arm here. Add one rather than ` +\n `dropping the predicate: an echo without it describes a WIDER query than the ` +\n `one that ran (#5333).`,\n );\n }\n params.push(values[0]);\n return `${col} ${op} $${params.length}`;\n }\n\n /**\n * Resolve a member ref to a `{ sql, type? }` definition.\n *\n * Mirrors `NativeSQLStrategy.lookupMember` so the two strategies\n * accept the same naming conventions:\n * 1. `<cube>.<field>` — canonical analytics qualifier.\n * 2. `<lookup>.<field>` — relation traversal (e.g. `account.industry`).\n * Tries literal key, then underscore-flattened key, then falls\n * back to a synthetic dim whose `sql` is the dotted path so the\n * ObjectQL aggregate engine can traverse it via the lookup field.\n * 3. `<field>` — bare column on the cube's table.\n */\n private lookupMember(\n cube: Cube,\n member: string,\n kind: 'dimension' | 'measure',\n ): { sql: string; type?: string } | undefined {\n const bag = kind === 'dimension' ? cube.dimensions : cube.measures;\n if (bag[member]) return bag[member];\n if (member.includes('.')) {\n const [first, ...rest] = member.split('.');\n const tail = rest.join('.');\n if (first === cube.name && bag[tail]) return bag[tail];\n if (bag[tail]) return bag[tail];\n const flat = member.replace(/\\./g, '_');\n if (bag[flat]) return bag[flat];\n if (kind === 'dimension') return { sql: member, type: 'string' };\n } else if (bag[member]) {\n return bag[member];\n }\n return undefined;\n }\n\n private resolveFieldName(cube: Cube, member: string, kind: 'dimension' | 'measure' | 'any'): string {\n if (kind === 'dimension' || kind === 'any') {\n const dim = this.lookupMember(cube, member, 'dimension');\n if (dim) return dim.sql.replace(/^\\$/, '');\n }\n if (kind === 'measure' || kind === 'any') {\n const measure = this.lookupMember(cube, member, 'measure');\n if (measure) return measure.sql.replace(/^\\$/, '');\n }\n return member.includes('.') ? member.split('.')[1] : member;\n }\n\n private resolveMeasureAggregation(cube: Cube, measureName: string): { field: string; method: string } {\n const direct = this.lookupMember(cube, measureName, 'measure') as\n | { sql: string; type: string }\n | undefined;\n if (direct) {\n return {\n field: direct.sql.replace(/^\\$/, ''),\n method: direct.type === 'count_distinct' ? 'count_distinct' : direct.type,\n };\n }\n // Accept `${field}_${type}` aliases (e.g. 'amount_sum') for measures whose\n // canonical name is just `${field}` (e.g. measure 'amount' of type 'sum').\n // This matches the convention used by clients that build measure names\n // from (field, function) pairs (e.g. the data-objectstack adapter).\n const fieldName = measureName.includes('.') ? measureName.split('.')[1] : measureName;\n const aggTypes = ['count', 'sum', 'avg', 'min', 'max', 'count_distinct'];\n for (const type of aggTypes) {\n const suffix = `_${type}`;\n if (fieldName.endsWith(suffix)) {\n const baseField = fieldName.slice(0, -suffix.length);\n const candidate = cube.measures[baseField];\n if (candidate && candidate.type === type) {\n return {\n field: candidate.sql.replace(/^\\$/, ''),\n method: candidate.type === 'count_distinct' ? 'count_distinct' : candidate.type,\n };\n }\n }\n }\n return { field: '*', method: 'count' };\n }\n\n /**\n * AND one more operand onto `filter[field]`, merging operator objects rather\n * than overwriting them. Returns a standalone conjunct when the two cannot\n * share one entry, or `null` when the merge absorbed the operand.\n *\n * Every predicate this strategy contributes goes through here — the caller's\n * `where` and the time-dimension `dateRange` alike. Two operands on one field\n * are the normal case (`{$gte}` from a `where` plus `{$gte,$lte}` from a\n * window on `close_date`), and a plain assignment would keep only the last:\n * that is how a range used to lose a bound.\n *\n * Spreading is sound only while the operands name DIFFERENT operators. Where\n * they collide — two `$gte` bounds on one field, which a window makes routine\n * and which a `where` can already produce on its own through `$and` — the\n * spread keeps whichever came last and WIDENS the query. Same for a bare\n * equality meeting an operator object: neither can absorb the other. Those\n * are handed back for the caller to AND in separately, so the engine\n * intersects them instead of the strategy picking a winner.\n */\n /**\n * Fold a normalized filter node into the engine filter being built.\n *\n * AND-ed LEAVES merge per field through {@link mergeFilterOperand}, exactly\n * as the flat loop this replaced did — so a query without combinators still\n * produces byte-identical engine input. Anything structural (`$or`, `$not`,\n * a nested `$and` that cannot merge) becomes its own conjunct, which the\n * caller ANDs in. The engine speaks these combinators natively\n * (`FilterCondition` declares them and every driver compiles them), so this\n * path hands them over rather than lowering them.\n */\n private applyFilterNode(\n node: NormalizedFilterNode | null,\n cube: Cube,\n filter: Record<string, unknown>,\n conjuncts: Record<string, unknown>[],\n ): void {\n if (!node) return;\n\n if (node.kind === 'leaf') {\n const fieldName = this.resolveFieldName(cube, node.member, 'any');\n const extra = this.mergeFilterOperand(filter, fieldName, this.convertFilter(node.operator, node.values));\n if (extra) conjuncts.push(extra);\n return;\n }\n\n if (node.kind === 'and') {\n for (const child of node.children) this.applyFilterNode(child, cube, filter, conjuncts);\n return;\n }\n\n const rendered = this.filterNodeToCondition(node, cube);\n if (rendered) conjuncts.push(rendered);\n }\n\n /**\n * A node as a standalone `FilterCondition` the engine can consume.\n *\n * `null` = no constraint, which is the boolean TRUE — the AND identity but the\n * OR ABSORBER, so a `null` branch makes the whole disjunction unconstrained\n * instead of collapsing it to its surviving branches (#5325). FALSE is handed\n * to the engine as `{$not: {}}`, the spelling `driver-sql`, `formula` and\n * `driver-memory`'s matcher all already pin as the zero-row filter (#5134) —\n * this strategy invents no second one.\n */\n private filterNodeToCondition(\n node: NormalizedFilterNode | null,\n cube: Cube,\n ): Record<string, unknown> | null {\n if (!node) return null;\n\n if (node.kind === 'const') {\n return node.value ? null : { $not: {} };\n }\n\n if (node.kind === 'not') {\n const inner = this.filterNodeToCondition(node.child, cube);\n // `NOT TRUE ≡ FALSE` — a negation of nothing is the zero-row filter, not\n // the absence of a filter (which is what let `{$not: {}}` chart every row).\n return inner ? { $not: inner } : { $not: {} };\n }\n\n if (node.kind === 'or') {\n const branches = node.children.map((child) => this.filterNodeToCondition(child, cube));\n // One TRUE disjunct absorbs the disjunction.\n if (branches.some((c) => c === null)) return null;\n const kept = branches.filter((c): c is Record<string, unknown> => !!c);\n return kept.length > 0 ? { $or: kept } : null;\n }\n\n // `leaf` and `and` share the merge path so one field carrying several\n // operators composes here the same way it does at the top level.\n const filter: Record<string, unknown> = {};\n const conjuncts: Record<string, unknown>[] = [];\n this.applyFilterNode(node, cube, filter, conjuncts);\n if (conjuncts.length > 0) {\n filter.$and = [...(Array.isArray(filter.$and) ? filter.$and : []), ...conjuncts];\n }\n return Object.keys(filter).length > 0 ? filter : null;\n }\n\n /**\n * Render a normalized filter node as the display SQL `/analytics/sql`\n * echoes. Values still bind as `$n` placeholders — the echo travels to the\n * browser, so a comparand is never inlined.\n *\n * The boolean identities render too (#5325). This string exists to REPRODUCE\n * execution: a `{$not: {}}` filter that runs as zero rows but echoes SQL with\n * no `WHERE` hands whoever is debugging \"why is this chart empty\" a statement\n * that returns the whole table. Same reason the absorbed `$or` branch and the\n * `params` truncation below match {@link NativeSQLStrategy.compileFilterNode}\n * exactly — including the invariant that a `null` return leaves `params`\n * untouched, so no comparand is left with no placeholder to consume it.\n */\n private renderFilterNodeSql(\n node: NormalizedFilterNode | null,\n cube: Cube,\n params: unknown[],\n ): string | null {\n if (!node) return null;\n\n if (node.kind === 'const') {\n return node.value ? SQL_CONST_TRUE : SQL_CONST_FALSE;\n }\n\n if (node.kind === 'leaf') {\n return this.buildFilterClauseSql(\n this.resolveFieldName(cube, node.member, 'any'),\n node.operator,\n node.values,\n params,\n );\n }\n\n if (node.kind === 'not') {\n const inner = this.renderFilterNodeSql(node.child, cube, params);\n return inner ? `NOT (${inner})` : SQL_CONST_FALSE;\n }\n\n const paramBase = params.length;\n const parts: string[] = [];\n for (const child of node.children) {\n const clause = this.renderFilterNodeSql(child, cube, params);\n if (clause === null) {\n if (node.kind !== 'or') continue;\n params.length = paramBase;\n return null;\n }\n parts.push(clause);\n }\n if (parts.length === 0) return null;\n if (parts.length === 1) return parts[0];\n return `(${parts.join(node.kind === 'or' ? ' OR ' : ' AND ')})`;\n }\n\n private mergeFilterOperand(\n filter: Record<string, unknown>,\n field: string,\n operand: unknown,\n ): Record<string, unknown> | null {\n const existing = filter[field];\n if (existing === undefined) {\n filter[field] = operand;\n return null;\n }\n const mergeable = (v: unknown): v is Record<string, unknown> =>\n !!v && typeof v === 'object' && !Array.isArray(v);\n if (!mergeable(existing) || !mergeable(operand)) return { [field]: operand };\n if (Object.keys(operand).some((op) => op in existing)) return { [field]: operand };\n filter[field] = { ...existing, ...operand };\n return null;\n }\n\n /**\n * Lower `timeDimensions[].dateRange` into resolved-field bounds (#3650).\n *\n * `dateRange` states a WINDOW on a time dimension; it is a SIBLING of `where`,\n * never folded into it. `normalizeAnalyticsFilters` reads only `where`, so\n * this path used to drop the window on the floor — no error, just every row\n * ever recorded. Nor is that a corner case: `NativeSQLStrategy.canHandle`\n * declines any query carrying a `granularity`, so a date-bucketed trend lands\n * HERE on every driver — and \"bucketed trend\" is precisely the shape that also\n * carries a range (\"last 12 months\", \"this quarter\").\n *\n * Bounds are inclusive on both ends — logically \"from day X through day Y\".\n * The `$lte` end is left as the bare calendar day on purpose: the driver's\n * filter compiler owns the calendar-day → instant translation, compiling a\n * bare-day `$lte` on a `datetime` column into the half-open `< nextDay`\n * (#3777) while a `date` column keeps the plain `<=`. `NativeSQLStrategy`\n * performs the same half-open translation itself because it binds into raw\n * SQL, so one dashboard reads the same on every driver.\n *\n * [#5526] Bounds are forwarded at the type `dateRange` is DECLARED with —\n * `string` (`AnalyticsQuerySchema`'s `timeDimensions[].dateRange: string[]`) —\n * and nothing re-types them. They used to pass through\n * `coerceFilterValueForObjectQL`, whose TSDoc advertised that \"an epoch-ms\n * bound recovers as a number\"; that was a lenient CONSUMER rescuing a shape the\n * contract does not declare, and the same guess is what read a `'007'` filter\n * comparand as `7` (Prime Directive #12 — the producer or the spec is where an\n * epoch-ms window would have to be declared, not here). An author who wants an\n * instant window writes it as one; a declared `string` binds as a string. No\n * STORAGE coercion happens here either, deliberately: `NativeSQLStrategy` needs\n * `coerceTemporal` because it binds into raw SQL and had to learn that a\n * SQLite `Field.datetime` is an INTEGER epoch (#2034); this path goes through\n * `engine.aggregate()`, where the driver's own CRUD filter coercion applies —\n * the very coercion that already makes a `where` bound on that same column\n * work today.\n *\n * A bare-string `dateRange` degenerates to the single point `[s, s]`, matching\n * `NativeSQLStrategy`. Relative phrases (\"Last 7 days\") are NOT resolved here;\n * neither SQL path resolves them, and inventing a second interpretation on the\n * driver-independent path is how the two would drift apart again.\n *\n * An oddly-sized array (the schema types `dateRange` as a plain `string[]`)\n * takes its first two entries, a one-entry array degenerating to a point.\n * `NativeSQLStrategy` drops such a window entirely — but \"drop the window\"\n * means \"plot all of history\", which is the very failure this fixes, so the\n * fallback here errs toward the narrower query instead.\n */\n private dateRangeBounds(\n cube: Cube,\n query: AnalyticsQuery,\n ): Array<{ field: string; bounds: Record<string, unknown> }> {\n const out: Array<{ field: string; bounds: Record<string, unknown> }> = [];\n for (const td of query.timeDimensions ?? []) {\n if (!td.dateRange) continue;\n const range = Array.isArray(td.dateRange) ? td.dateRange : [td.dateRange, td.dateRange];\n const [start, end = start] = range;\n if (start == null) continue;\n out.push({\n field: this.resolveFieldName(cube, td.dimension, 'dimension'),\n bounds: { $gte: start, $lte: end },\n });\n }\n return out;\n }\n\n /**\n * One leaf as the operand the engine's `FilterCondition` expects.\n *\n * [#5526] The comparand is passed through UNCONVERTED. That is the whole of\n * this path's share of the fix: the engine compares against the value as\n * STORED, and a leaf now carries the value the author wrote, so `'007'` stays\n * `'007'`, `true` stays `true` and `7` stays `7` with nothing in between to\n * re-type them. The two `coerceFilterValueForObjectQL` calls this replaced\n * existed only to undo `stringifyForCube`, and undoing it required guessing.\n *\n * The four LIKE-family arms are the exception, and a contract one:\n * `filter.zod.ts` declares `$contains` / `$notContains` / `$startsWith` /\n * `$endsWith` as `z.string()`, so this PRODUCER must hand the engine a real\n * string — `String(…)`, the same normalisation `like-pattern.ts` applies at the\n * two SQL emitters and `driver-sql`'s `applyLike` applies at the driver, so one\n * `$contains` means one thing on every face (#5567's invariant).\n *\n * [#5234] Those four `String(…)` calls now only ever see a value that renders\n * faithfully: `fieldLeaves` refuses an object comparand on this family before a\n * leaf exists. That ordering is load-bearing rather than incidental — this arm\n * is a PRODUCER for the engine, so stringifying an object here would have\n * laundered it into `'[object Object]'` and handed a driver a perfectly\n * well-typed string. A strict driver downstream could never have seen the shape\n * it was strict about, which is why the guard sits at the door and not here.\n */\n private convertFilter(operator: string, values?: unknown[]): unknown {\n if (operator === 'set') return { $ne: null };\n if (operator === 'notSet') return null;\n if (!values || values.length === 0) return undefined;\n\n const v0 = values[0];\n // A COPY, not the leaf's own array: the `$in` / `$nin` operand below travels\n // into the filter object the engine receives, and a node of this tree is\n // never shared (see `falseNode`). The old `values.map(coerce…)` copied as a\n // side effect of converting; dropping the conversion must not drop the copy.\n const all = [...values];\n switch (operator) {\n // [#7598] IMPLICIT equality for a literal, EXPLICIT `$eq` for a field\n // reference — the branch is on the COMPARAND, not on the operator, which\n // is the same fix and the same reasoning #7597 applied to\n // `parseFilterAST`, the spec's own lowering sink.\n //\n // `{ amount: 5 }` is implicit equality and every backend reads it that\n // way. `{ amount: { $field: 'budget' } }` is NOT: it is a field-spec\n // object whose only key is `$field`, which no backend reads as an\n // equality — `driver-sql` sees an unrecognised operator key and the\n // memory evaluator sees a comparand it never resolves. So the bare return\n // was correct for four years' worth of literals and silently wrong for\n // the one comparand the 2026-08-12 ruling routes HERE on purpose: with it,\n // `{ amount: { $eq: { $field: 'budget' } } }` — the shape\n // `compileCelToFilter` emits for a field-to-field CEL rule, and the shape\n // `canHandle` now declines native SQL for — would arrive at the driver as\n // something the driver cannot read, so the capability B exists to serve\n // would fail on its single most important spelling. Its five siblings\n // (`$ne`/`$gt`/`$gte`/`$lt`/`$lte`) were never affected: they emit their\n // operator explicitly two lines down.\n case 'equals': return isFieldReference(v0) ? { $eq: v0 } : v0;\n case 'notEquals': return { $ne: v0 };\n case 'gt': return { $gt: v0 };\n case 'gte': return { $gte: v0 };\n case 'lt': return { $lt: v0 };\n case 'lte': return { $lte: v0 };\n // [#5557] `contains` was `{ $regex: values[0] }` — the comparand dropped\n // VERBATIM into a regex position while its three siblings below already\n // passed as canonical spec operators. Three things were wrong with that,\n // and none of them waits on #4706's ruling about what `$regex` should\n // mean:\n //\n // 1. `$regex` is not in `filter.zod.ts`'s `FILTER_OPERATORS`, so this\n // was a PRODUCER emitting an operator the contract does not declare\n // (Prime Directive #12 — fix the producer, not the consumers).\n // 2. `compileScopedFilterToSql` in this very package is a\n // `FilterCondition` consumer and fails closed on `$regex`, so one\n // filter tree no longer travelled between two consumers of the same\n // contract sitting in the same directory.\n // 3. On a backend that reads `$regex` as a real regex — driver-memory's\n // `memory-matcher.ts` does, deliberately, for plugin-auth's adapter\n // — an unescaped comparand changes what the author asked for:\n // `a.b` also matched `axb`, and `50% (+)` did not compile at all, so\n // the `catch { return false }` answered zero rows in silence.\n // `driver-sql` meanwhile compiles `$regex` to a substring LIKE, so\n // the same widget returned different row sets per driver.\n //\n // `MONGO_TO_CUBE_OP` maps `$contains` → `contains` and nothing else does,\n // so returning `$contains` here is the round trip of the author's own key.\n case 'contains': return { $contains: String(v0) };\n // `notContains` had no arm and fell to the `default` below, which returns\n // a BARE VALUE — i.e. `{field: 'x'}`, an equality. \"does not contain x\"\n // was compiled as \"equals x\". These three pass through as the canonical\n // spec operators every driver implements directly, so an anchored match\n // stays anchored rather than depending on regex dialect (#4128).\n case 'notContains': return { $notContains: String(v0) };\n case 'startsWith': return { $startsWith: String(v0) };\n case 'endsWith': return { $endsWith: String(v0) };\n case 'in': return { $in: all };\n case 'notIn': return { $nin: all };\n default:\n // Was `return v0` — a silent reinterpretation of the operator as an\n // equality, the write-side twin of the normalizer's dropped predicate\n // (#4128). Every operator `normalizeAnalyticsFilters` can emit is\n // handled above, so reaching here means the two drifted apart.\n throw new Error(\n `[analytics] ObjectQL strategy cannot express filter operator \"${operator}\". ` +\n `Treating it as an equality would silently query something the author did not ask for.`,\n );\n }\n }\n\n private extractObjectName(cube: Cube): string {\n return cube.sql.trim();\n }\n\n /**\n * [#7598] The query's `where`, lowered — the same input\n * `NativeSQLStrategy.canHandle` scans, so the strategy that DECLINED and the\n * echo that refuses read one shape rather than two.\n *\n * A throw from the lowering is swallowed for the same reason it is there: the\n * `where` is malformed either way and `normalizeAnalyticsFilterTree` below\n * refuses it with the message and envelope it has always had. This helper's\n * only job is finding a reference, and there is none to find in a filter that\n * does not lower.\n */\n private loweredWhere(query: AnalyticsQuery): unknown {\n try {\n return lowerAnalyticsWhere(query);\n } catch {\n return null;\n }\n }\n\n /**\n * The dimensions this query PROJECTS, in the order the result carries them:\n * every `dimensions` entry, then every granular `timeDimensions` entry that\n * is not already one of them.\n *\n * `timeDimensions` is not merely a filter carrier. An entry with a\n * `granularity` is GROUPED BY — see the `td.granularity` sites that build\n * groupBy here, in `generateSql` and in the cross-object path — so its\n * bucket is a COLUMN of the result; an entry without one only contributes a\n * `dateRange` predicate and must NOT be projected.\n *\n * Grouping, row mapping and {@link buildFieldMeta} have to agree on exactly\n * that set. When they did not, a bucketed query returned rows carrying only\n * the measures and a `fields` list that never mentioned the bucket — a trend\n * chart got N values and no x-axis (#4033) — even though the SQL had\n * selected `date_trunc(…) AS \"<dim>\"` all along. One definition, every\n * consumer.\n */\n private projectedDimensions(query: AnalyticsQuery): string[] {\n const out = [...(query.dimensions ?? [])];\n for (const td of query.timeDimensions ?? []) {\n if (td.granularity && !out.includes(td.dimension)) out.push(td.dimension);\n }\n return out;\n }\n\n private buildFieldMeta(query: AnalyticsQuery, cube: Cube): Array<{ name: string; type: string }> {\n const fields: Array<{ name: string; type: string }> = [];\n for (const dim of this.projectedDimensions(query)) {\n const d = this.lookupMember(cube, dim, 'dimension');\n fields.push({ name: dim, type: d?.type || 'string' });\n }\n if (query.measures) {\n for (const m of query.measures) {\n fields.push({ name: m, type: 'number' });\n }\n }\n return fields;\n }\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Cross-object dimension re-bucketing (#3654 capability).\n *\n * `engine.aggregate()` cannot join, so the ObjectQL path cannot group directly\n * by a related object's attribute (`account.region`). Instead the strategy:\n * 1. groups the base aggregate by the LOOKUP FK column (`account`), which the\n * engine CAN do (it is a plain base column), and\n * 2. resolves each FK id to the related attribute (`region`) with a SCOPED\n * read of the referenced object, then\n * 3. re-buckets the base aggregate by that attribute here, in memory,\n * recombining the measures.\n *\n * This module is the pure, deterministic step (3): given the base rows, the\n * FK→attribute maps, and the measures' aggregation methods, produce the rows a\n * direct cross-object grouping would have. It is unit-tested in isolation\n * because a wrong re-combination silently corrupts totals — exactly the class of\n * bug #3654 exists to kill.\n *\n * A base row whose FK does not resolve (the referenced record is hidden by the\n * referenced object's own RLS) buckets under {@link RESTRICTED_BUCKET}: its\n * measure still counts, so grand totals are preserved, but the hidden record's\n * attribute value never appears (no leak — ADR-0021 D-C, the #3602 class).\n */\n\n/** Only aggregation methods that re-combine across sub-buckets are supported. */\nexport type RecombinableMethod = 'sum' | 'count' | 'min' | 'max';\n\nexport const RECOMBINABLE_METHODS: ReadonlySet<string> = new Set<RecombinableMethod>([\n 'sum',\n 'count',\n 'min',\n 'max',\n]);\n\n/** Sentinel bucket for base rows whose referenced record the caller cannot read. */\nexport const RESTRICTED_BUCKET = '(restricted)';\n\nexport interface CrossObjectDim {\n /** Output key for the resolved attribute (the original dimension name), e.g. `region`. */\n outputName: string;\n /** The base FK column the base aggregate was grouped by, e.g. `account`. */\n fkField: string;\n /** `fkValue → attributeValue`. An FK absent from the map buckets as RESTRICTED. */\n fkToAttr: Map<unknown, unknown>;\n}\n\nexport interface MeasureRecombine {\n /** The measure's output key in each base row. */\n alias: string;\n method: RecombinableMethod;\n}\n\n/**\n * Order two measure values. A number orders as itself; a `Date` or an ISO\n * timestamp orders as its instant, so a `min`/`max` over a temporal measure\n * compares correctly instead of collapsing to `NaN` (#3797). `NaN` means \"not\n * orderable\" and the caller keeps the other side.\n */\nfunction orderableValue(v: unknown): number {\n if (v == null) return NaN;\n if (typeof v === 'number') return v;\n if (v instanceof Date) return v.getTime();\n const n = Number(v);\n if (Number.isFinite(n)) return n;\n return Date.parse(String(v));\n}\n\n/**\n * Combine two measure values under an aggregation method (either may be\n * undefined).\n *\n * `sum`/`count` are numeric by construction and stay so. `min`/`max` return the\n * winning ORIGINAL value rather than a number: the value they pick is a value\n * OF the column, so a temporal measure has to come back out in the same shape\n * the driver presented it (#3797) — coercing it to a number here would put the\n * epoch leak back one layer up, and on any dialect whose driver returns an ISO\n * string it would produce `NaN` outright.\n */\nfunction recombine(method: RecombinableMethod, acc: unknown, next: unknown): unknown {\n if (method === 'min' || method === 'max') {\n if (acc === undefined) return next ?? 0;\n const a = orderableValue(acc);\n const n = orderableValue(next);\n if (Number.isNaN(n)) return acc;\n if (Number.isNaN(a)) return next;\n const nextWins = method === 'min' ? n < a : n > a;\n return nextWins ? next : acc;\n }\n const n = Number(next ?? 0);\n return acc === undefined ? n : Number(acc) + n;\n}\n\n/**\n * Re-bucket base aggregate rows by resolved cross-object attributes.\n *\n * @param baseRows rows grouped by `baseDimFields` + every `crossDims[*].fkField`.\n * @param baseDimFields the NON-cross-object group keys carried through unchanged\n * (base columns and date buckets), keyed as in `baseRows`.\n * @param crossDims one entry per cross-object dimension (its FK→attr map).\n * @param measures measure keys + their (recombinable) aggregation method.\n * @returns rows keyed by `baseDimFields` + each `crossDims[*].outputName` + measures.\n */\nexport function rebucketCrossObject(\n baseRows: Record<string, unknown>[],\n baseDimFields: string[],\n crossDims: CrossObjectDim[],\n measures: MeasureRecombine[],\n): Record<string, unknown>[] {\n const buckets = new Map<string, Record<string, unknown>>();\n\n for (const row of baseRows) {\n // Resolve each cross-object FK to its attribute (or RESTRICTED).\n const resolved: Record<string, unknown> = {};\n for (const cd of crossDims) {\n const fk = row[cd.fkField];\n resolved[cd.outputName] = cd.fkToAttr.has(fk) ? cd.fkToAttr.get(fk) : RESTRICTED_BUCKET;\n }\n\n // Bucket key = base dims (unchanged) + resolved attributes. `\\u0001` is a\n // separator no group value contains, matching the engine's own convention.\n const keyParts: string[] = [];\n // JSON-encoded, so the empty bucket (`null` on both aggregation paths since\n // #3839) stays distinct from a row whose value is the literal string\n // `\"null\"` — plain interpolation renders both as `null` and would merge two\n // real groups into one. Only this composite id is affected; the emitted\n // bucket keeps the row's own value verbatim below.\n for (const f of baseDimFields) keyParts.push(`${f}=${JSON.stringify(row[f] ?? null)}`);\n for (const cd of crossDims) keyParts.push(`${cd.outputName}=${String(resolved[cd.outputName])}`);\n const key = keyParts.join('\\u0001');\n\n let bucket = buckets.get(key);\n if (!bucket) {\n bucket = {};\n for (const f of baseDimFields) bucket[f] = row[f];\n for (const cd of crossDims) bucket[cd.outputName] = resolved[cd.outputName];\n buckets.set(key, bucket);\n }\n for (const m of measures) {\n bucket[m.alias] = recombine(m.method, bucket[m.alias], row[m.alias]);\n }\n }\n\n return [...buckets.values()];\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Cube, Metric, Dimension as CubeDimension, CubeJoin } from '@objectstack/spec/data';\nimport { AggregationFunction } from '@objectstack/spec/data';\nimport type { Dataset, DatasetMeasure, DatasetDimension } from '@objectstack/spec/ui';\nimport { resolveI18nLabel } from '@objectstack/spec/ui';\nimport type { FilterCondition } from '@objectstack/spec/data';\nimport { datasetInvalidError } from './dataset-refusal.js';\n\n/**\n * Dataset → Cube compiler (ADR-0021 D-A=(c), WS2).\n *\n * Lowers a declarative `dataset` (base object + included relationships +\n * declared dimensions/measures + derived measures) into the existing Cube\n * analytics runtime model. The author never writes an `ON` clause: joins are\n * DERIVED from the `include` relationship names and the dotted `relationship.field`\n * references on dimensions/measures, matching the NativeSQLStrategy convention\n * `<parentTable>.<relationship> = <relationship>.id`.\n *\n * Safety (D-C): every dotted field reference must point at a relationship that\n * the dataset explicitly declared in `include`; otherwise the compile fails.\n * The returned `allowedRelationships` set is the join allowlist the strategy\n * enforces at SQL-build time.\n */\n\n/**\n * Aggregates v1 does NOT compile to the Cube SQL switch — surfaced as a clear error.\n *\n * **EMPTY since #6188, and deliberately kept.** It named `array_agg` and\n * `string_agg`: the two aggregates the spec declared and this runtime could not\n * lower. ADR-0049 resolved that the honest way round — both were retired from\n * `AggregationFunction` itself, so they are now refused one layer earlier, by\n * the parse, with a prescription that tells the author what to do instead.\n * `count_distinct` was the third unlowered function on the SQL drivers and was\n * NOT retired (maintainer ruling, 2026-08-07): this compiler lowers it already,\n * and the driver-side implementation follows on its own card.\n *\n * The set stays because it is one half of an arithmetic the lockstep tests\n * enforce (`SUPPORTED = spec vocabulary − this`), and that arithmetic is what\n * stops the next aggregate added to the spec from reaching the strategy's\n * `default` and returning a row count in place of the requested value. Empty is\n * the correct current reading — every declared aggregate is lowered — not a\n * leftover.\n */\nexport const UNSUPPORTED_AGGREGATES = new Set<string>();\n\n/**\n * What v1 *can* lower — derived from the spec's vocabulary rather than restated.\n *\n * The list used to be hand-written prose inside the error message below, which\n * made it a third copy of one vocabulary (after `AggregationFunction` and the\n * `native-sql-strategy` switch) with nothing keeping the three in step. An\n * aggregate added to the spec would have passed this gate, been reported as\n * supported by that message, and then hit the strategy's `default` — returning\n * a row count in place of the requested number. objectui#2945.\n */\nexport const SUPPORTED_AGGREGATES: string[] = AggregationFunction.options\n .filter((a: string) => !UNSUPPORTED_AGGREGATES.has(a));\n\nexport interface DerivedMeasureSpec {\n name: string;\n op: 'ratio' | 'sum' | 'difference' | 'product';\n of: string[];\n}\n\nexport interface CompiledDataset {\n /** The Cube the dataset compiles to (consumed by the strategy chain). */\n cube: Cube;\n /**\n * Every join alias the dataset may use — each declared `include` path AND its\n * intermediate prefixes (ADR-0071). The join allowlist (D-C): the\n * NativeSQLStrategy rejects any join alias not in this set.\n */\n allowedRelationships: Set<string>;\n /** Derived measures, computed post-aggregation by the executor (Q1). */\n derived: DerivedMeasureSpec[];\n /** Definition-level filter (the dataset's intrinsic scope). */\n filter?: FilterCondition;\n /** Per-measure scoped filters, keyed by measure name (applied by executor). */\n measureFilters: Record<string, FilterCondition>;\n}\n\n/**\n * The related object reached by traversing a relationship: its logical object\n * name (used to resolve the NEXT hop in a multi-hop chain — ADR-0071) and its\n * physical table name (the join target).\n */\nexport interface RelationshipTarget {\n object: string;\n table: string;\n}\n\n/**\n * Resolves a relationship name on a base object to the related object/table,\n * using the runtime's object graph. Optional: when omitted the compiler trusts\n * the declared `include` names (the NativeSQLStrategy convention assumes the\n * relationship name equals the related table name).\n *\n * May return a bare table-name `string` (legacy single-hop: object name is\n * assumed equal to the table) or a {@link RelationshipTarget} (required to\n * traverse further along a multi-hop path, where object differs from table for\n * namespaced objects).\n */\nexport type RelationshipResolver = (\n baseObject: string,\n relationshipName: string,\n) => string | RelationshipTarget | undefined;\n\n/**\n * Optional probes the compiler consults to reject metadata that is decidable\n * BEFORE any query runs. Every probe is optional and every one of them is\n * tiered \"cannot answer, do not block\" (the same stand-down as\n * `isRegisteredObject` / `getObjectFieldNames` on `AnalyticsServiceConfig`):\n * a host without a data engine compiles exactly as it did before.\n */\nexport interface DatasetCompileOptions {\n /**\n * [#5115] The datasource `objectName` is BOUND to, or `undefined` when nothing\n * authoritative can answer (no data engine, unknown object) — or when nothing\n * binds the object at all and it rides the deployment's default datasource.\n *\n * With it the compiler can settle at COMPILE time what #5033 could only\n * report at QUERY time: a dataset whose join crosses datasources declares a\n * statement no driver can execute, because the analytics engine lowers the\n * whole dataset into ONE SQL statement on the base object's datasource.\n *\n * [#5288] What the host supplies here changed shape, the rule below did not.\n * It used to be the object's DECLARED `datasource` — step 1 of the five\n * `ObjectQL.getDriver` routes by — so an object placed by a\n * `datasourceMapping` rule, by the ADR-0057 §3.6 lifecycle split, or by its\n * package's `defaultDatasource` answered `'default'` and was read here as\n * unanswered. The built-in host (`plugin.ts`) now asks the engine's own\n * resolver instead, so those three placements ARE visible from here and a join\n * between two objects bound to two different datasources is decidable\n * whichever mechanism bound them.\n *\n * Still deliberately UNANSWERED: `'default'`, and the object that no rule\n * places anywhere. The deployment's default driver keeps its natural name\n * (#3826), so \"rides the default\" is reported as `undefined` rather than as a\n * name — which means a join from a bound object to a default-riding one stays\n * undecidable here and remains the query-time diagnostic's business (#5288\n * records this boundary; widening it is #5115's follow-up, not this rule's).\n * See {@link compileDataset}.\n */\n getObjectDatasource?: (objectName: string) => string | undefined;\n /**\n * ADR-0062 D6 — is `objectName` federated (bound to an external datasource)?\n *\n * A federated participant is EXEMPT from the cross-datasource rejection:\n * `NativeSQLStrategy.canHandle` already declines a cube whose base or joined\n * object is external, so such a dataset is served by the ObjectQL FK-expand\n * path (two reads, joined in memory) — which crosses datasources by\n * construction. Rejecting it here would break a path that works today.\n */\n isExternalObject?: (objectName: string) => boolean;\n}\n\n/** Map a dataset measure's aggregate to the Cube metric `type`. */\nfunction aggregateToMetricType(m: DatasetMeasure): Metric['type'] {\n // Only reached for non-derived measures, where the spec refinement guarantees\n // an aggregate; guard defensively so the type narrows from `optional`.\n if (!m.aggregate) {\n throw new Error(`[dataset-compiler] non-derived measure \"${m.name}\" has no aggregate`);\n }\n if (UNSUPPORTED_AGGREGATES.has(m.aggregate)) {\n // [#5367] `DATASET_INVALID` / 400 — the aggregate is the dataset author's\n // choice, and the message already names the ones that would work.\n //\n // Unreachable while `UNSUPPORTED_AGGREGATES` is empty (#6188 retired its two\n // members from the spec, which now refuses them at parse). Kept as the\n // landing site for the next aggregate the spec declares before this runtime\n // can lower it: without it that aggregate reaches the strategy's `default`\n // and comes back as a row count. The lockstep suite is what decides which\n // of the two states we are in, so this branch cannot rot unnoticed.\n throw datasetInvalidError(\n `[dataset-compiler] measure \"${m.name}\" uses aggregate \"${m.aggregate}\" which is ` +\n `not supported by the v1 dataset runtime (supported: ${SUPPORTED_AGGREGATES.join(', ')}).`,\n );\n }\n return m.aggregate as Metric['type'];\n}\n\n/** Map a dataset dimension type to the Cube dimension `type`. */\nfunction dimensionType(d: DatasetDimension): CubeDimension['type'] {\n switch (d.type) {\n case 'date': return 'time';\n case 'number': return 'number';\n case 'boolean': return 'boolean';\n case 'lookup': return 'string';\n case 'string': return 'string';\n default: return 'string';\n }\n}\n\n/** The relationship PATH a dotted field traverses — all segments but the final\n * column — or null for a base-object field. E.g. `account.owner.region` →\n * `account.owner`; `account.region` → `account`; `region` → null. */\nfunction fieldRelationshipPath(field: string): string | null {\n const idx = field.lastIndexOf('.');\n return idx > 0 ? field.slice(0, idx) : null;\n}\n\n/** Max relationship hops in one `include` path — base → 3 hops = 4 objects\n * (ADR-0071; Salesforce-report-type parity). To-one chains never fan out, so\n * this is a performance/complexity guard, not a correctness limit. */\nconst MAX_JOIN_HOPS = 3;\n\n/** SQL-safe join alias for a relationship PATH. The dotted path is the author-\n * facing form; the alias replaces dots with `__` (Cube.js convention) so each\n * prefix is one valid identifier — quoted dotted identifiers are rejected by\n * the read-scope SQL guard (fail-closed). Single-segment paths are unchanged,\n * so single-hop joins stay byte-for-byte identical. */\nconst joinAlias = (path: string): string => path.replace(/\\./g, '__');\n\n/**\n * [#6761] The locale this compiler resolves an inline-locale-map label at:\n * **none**, i.e. the platform source language `en` per `resolveI18nLabel`'s\n * documented nullish-tolerance.\n *\n * This is a decision, not an omission, and it is spelled as a named constant so\n * it stays visible and greppable rather than reading as a forgotten argument\n * (the resolver takes `locale` positionally for exactly that reason).\n *\n * **A compiled Cube is a REGISTRY artifact, not a response.** `registerDataset`\n * writes it into `CubeRegistry` under the dataset's name, `queryDataset`\n * re-registers on every call, and `getMeta()` — the `/analytics/meta` face —\n * reads it back with **no execution context at all** (`IAnalyticsService.getMeta`\n * takes `cubeName?` and nothing else, and the route calls it without one). So\n * the request locale must NOT be baked in here: one `zh-CN` query would leave a\n * Chinese-labelled cube in a registry every later reader shares, and\n * `/analytics/meta` would answer whoever queried last. Request-scoped\n * resolution belongs where a request is in hand — `queryDataset`'s two field\n * enrichment sites, which read `context.locale`.\n *\n * **The fallback stays `d.name`, and that is safe against the `f.label == null`\n * guard** (#5199 route A / #6761). `Metric.label` and `Dimension.label` are\n * REQUIRED strings in `analytics.zod.ts`, so an unresolvable label must still\n * produce one, and the machine name is what this compiler already wrote. It\n * cannot pre-empt the document-sourced label downstream because a cube label\n * never reaches `AnalyticsResult.fields[]`: both strategies' `buildFieldMeta`,\n * the draft preview evaluator, and `DatasetExecutor`'s #5537 descriptor\n * adoption all emit `{ name, type }` only. The enrichment sites therefore still\n * see `f.label == null` and write the locale-resolved label over nothing.\n */\nconst REGISTRY_LOCALE: string | undefined = undefined;\n\nexport function compileDataset(\n dataset: Dataset,\n resolver?: RelationshipResolver,\n options?: DatasetCompileOptions,\n): CompiledDataset {\n const include = dataset.include ?? [];\n\n // ── #5115 — cross-datasource joins are rejected HERE, at compile time ──────\n //\n // A dataset lowers to ONE SQL statement executed on the base object's\n // datasource (`plugin.ts` routes raw SQL by object since #5033). So a join\n // whose target lives on a DIFFERENT datasource is not a query that sometimes\n // fails — it is metadata that can never execute, and the question \"which\n // datasource is each participant bound to\" is fully answerable the moment the\n // dataset is compiled. #5033 made that failure loud at QUERY time (in front of\n // whoever opened the dashboard); this gate moves the same verdict to\n // registration, where the AUTHOR is still holding the metadata.\n //\n // Tiering — \"cannot answer, do not block\": no probe, no answer for the base\n // object, or no answer for a target ⇒ compile as before and let #5033's\n // query-time defence report it. A false ALLOW costs a loud runtime error that\n // already exists; a false REJECT would blank a working dashboard on upgrade,\n // so this gate fires ONLY on a conflict the metadata itself proves.\n //\n // What counts as an ANSWER: a non-`'default'` datasource NAME for the object.\n // Two objects bound to two different names are provably in two databases.\n //\n // `'default'` is not one. It is what `ObjectSchema.datasource` defaults to and\n // what `ObjectQL.getDriver` reads as \"no explicit binding, keep looking\", so\n // treating it as \"the primary DB\" would reject a dataset whose two objects a\n // mapping rule in fact lands on the SAME datasource, and would make the\n // verdict depend on whether the object happened to be Zod-parsed (which\n // materializes the default). It is read as UNANSWERED, as is `undefined`.\n //\n // [#5288] The probe used to report only step 1 of that resolution — the\n // DECLARED value — which left every object placed by a `datasourceMapping`\n // rule, by the ADR-0057 §3.6 lifecycle split (audit/telemetry/event → the\n // `telemetry` datasource), or by its package's `defaultDatasource` answering\n // `'default'` and therefore unanswerable here. The built-in host now asks\n // `ObjectQL.resolveEffectiveDatasource`, so this rule — unchanged — sees those\n // placements too. What it still cannot see is the object nothing binds at all:\n // that one rides the deployment's default driver and is reported as\n // `undefined`, so a join from a bound object to a default-riding one is not\n // decidable here and stays with #5033's query-time diagnostic.\n const declaredDatasource = (objectName: string): string | undefined => {\n const declared = options?.getObjectDatasource?.(objectName);\n return declared && declared.toLowerCase() !== 'default' ? declared : undefined;\n };\n const isExternal = (objectName: string): boolean =>\n options?.isExternalObject?.(objectName) ?? false;\n const baseDatasource = declaredDatasource(dataset.object);\n // Datasource ids are compared case-insensitively: an id differing only in case\n // is not evidence of two different databases, and an uncertain answer must\n // not reject.\n const sameDatasource = (a: string, b: string) => a.toLowerCase() === b.toLowerCase();\n const baseIsFederated = isExternal(dataset.object);\n const assertSameDatasource = (targetObject: string, path: string): void => {\n // Judgeable only when the BASE side is placed and non-federated; it is the\n // side every comparison is made against, so an unplaceable base means no\n // join can be judged (never reject every join for want of the base).\n if (!baseDatasource || baseIsFederated) return;\n if (isExternal(targetObject)) return; // served by the FK-expand path, not by one statement\n const targetDatasource = declaredDatasource(targetObject);\n if (!targetDatasource) return; // cannot answer for this target\n if (sameDatasource(targetDatasource, baseDatasource)) return;\n // [#5716] `DATASET_INVALID` / 400 — the AUTHOR's verdict, decided entirely\n // from metadata before any query runs: the dataset's `include` path and the\n // two objects' `datasource` bindings. Both are things the author (or an\n // admin) can change, and the message already names both fixes. Nothing here\n // is a runtime fault, so a 500 told the author \"the platform is broken\"\n // about a document they wrote.\n throw datasetInvalidError(\n `[dataset-compiler] dataset \"${dataset.name}\" declares a JOIN that crosses datasources: ` +\n `its base object \"${dataset.object}\" is on datasource \"${baseDatasource}\", but the joined ` +\n `object \"${targetObject}\" — reached via the \\`include\\` path \"${path}\" — is on datasource ` +\n `\"${targetDatasource}\". A dataset JOIN cannot cross datasources: the whole dataset is ` +\n `executed as ONE statement on the base object's datasource, so \"${targetObject}\" is simply ` +\n `not there. Fix it by binding both objects to the same datasource, or by dropping \"${path}\" ` +\n `from the dataset's \\`include\\` (and every dimension/measure that references it).`,\n );\n };\n\n // Resolve each declared relationship PATH into its ordered join chain, emitting\n // one Cube join per PATH PREFIX (ADR-0071 multi-hop, to-one only). The join\n // ALIAS is the full dotted path (`account.owner`), which self-describes the\n // chain: the parent alias is the path minus its last segment, the FK column is\n // that last segment. So declaring `account.owner` auto-adds the intermediate\n // `account` join, and the strategy can rebuild every `ON` from the alias alone.\n // Without a resolver, each segment's relationship name is assumed to equal both\n // the related object and its table (legacy convention / unit tests).\n const resolveHop = (fromObject: string, rel: string): RelationshipTarget => {\n if (!resolver) return { object: rel, table: rel };\n const resolved = resolver(fromObject, rel);\n if (!resolved) {\n // [#5716] `DATASET_INVALID` / 400 — the dataset's own `include` names a\n // relationship the object graph does not have: a typo or a stale dataset,\n // fixable only by the author. Sibling of the already-enveloped \"…is not\n // declared in the dataset's `include`\" (#5367), one step earlier in the\n // same resolution.\n //\n // ⚠️ The WORDING is left exactly as it was, and that is not an oversight:\n // it contains both \"relation\"(ship) and \"does not exist\", so\n // `analytics-service.ts`'s `isMissingSourceError` matches it — the mine\n // #5717 filed. Enveloping it does not disarm that sniffer (it reads the\n // message, not the envelope) and does not arm it either (this throw is\n // still OUTSIDE `queryDataset`'s try, which is the only reason the mine\n // has never gone off). What it DOES do is make #5717's option B — \"never\n // degrade an error that declares a 4xx envelope to an empty result\" —\n // able to cover this site, which before today it could not.\n throw datasetInvalidError(\n `[dataset-compiler] dataset \"${dataset.name}\" includes relationship \"${rel}\" ` +\n `which does not exist on object \"${fromObject}\".`,\n );\n }\n return typeof resolved === 'string' ? { object: resolved, table: resolved } : resolved;\n };\n const joins: Record<string, CubeJoin> = {};\n for (const path of include) {\n const segments = path.split('.');\n if (segments.length > MAX_JOIN_HOPS) {\n // [#5716] `DATASET_INVALID` / 400 — a limit of the v1 runtime, reported\n // against a path the author wrote. Same family as the aggregate refusal a\n // few lines up (\"not supported by the v1 dataset runtime\", #5367): what\n // the runtime cannot do is stated as a property of the dataset, because\n // that is what the author has to change.\n throw datasetInvalidError(\n `[dataset-compiler] dataset \"${dataset.name}\" include path \"${path}\" exceeds the ` +\n `${MAX_JOIN_HOPS}-hop limit (${segments.length} hops). Deeper traversal is not supported.`,\n );\n }\n let fromObject = dataset.object;\n let parentAlias = dataset.object;\n let prefix = '';\n for (const seg of segments) {\n prefix = prefix ? `${prefix}.${seg}` : seg;\n const target = resolveHop(fromObject, seg);\n // #5115 — every hop is a join target in the single statement, so each one\n // (not just the last segment of a path) must sit on the base datasource.\n assertSameDatasource(target.object, prefix);\n const alias = joinAlias(prefix);\n if (!joins[alias]) {\n // KEY is the SQL-safe alias; `name` carries the join TABLE; the strategy\n // rebuilds the ON clause from the alias convention (`<parent>.<seg> = <alias>.id`).\n joins[alias] = {\n name: target.table,\n relationship: 'many_to_one',\n sql: `${parentAlias}.${seg} = ${prefix}.id`,\n };\n }\n fromObject = target.object;\n parentAlias = prefix;\n }\n }\n\n // The join allowlist (D-C) is every registered alias — each declared path AND\n // its intermediate prefixes — so a multi-hop field's intermediate joins pass.\n const allowedRelationships = new Set(Object.keys(joins));\n\n // Assert any dotted field only traverses a DECLARED relationship PATH (D-C).\n const assertDeclared = (field: string, ownerKind: string, ownerName: string) => {\n const relPath = fieldRelationshipPath(field);\n if (relPath && !joins[joinAlias(relPath)]) {\n // [#5367] `DATASET_INVALID` / 400 — a dimension/measure traversing a\n // relationship the same document never declared in `include` is the\n // dataset author's mistake, and the fix is in the document they hold.\n throw datasetInvalidError(\n `[dataset-compiler] ${ownerKind} \"${ownerName}\" references relationship path \"${relPath}\" ` +\n `via \"${field}\", but \"${relPath}\" is not declared in the dataset's \\`include\\`. ` +\n `Only fields along a declared relationship path are joinable.`,\n );\n }\n };\n\n // Compile dimensions.\n const dimensions: Record<string, CubeDimension> = {};\n for (const d of dataset.dimensions) {\n assertDeclared(d.field, 'dimension', d.name);\n const dim: CubeDimension = {\n name: d.name,\n // [#6761] An inline locale map is a label, not a missing one. Before this,\n // the `typeof === 'string'` test dropped the map and substituted the\n // machine name, which `/analytics/meta` then published as a display title\n // (`title: 'owner'` for a dimension labelled `{ en: 'Owner', … }`).\n label: resolveI18nLabel(d.label, REGISTRY_LOCALE) ?? d.name,\n type: dimensionType(d),\n sql: d.field,\n };\n if (dim.type === 'time') {\n dim.granularities = d.dateGranularity\n ? [d.dateGranularity]\n : ['day', 'week', 'month', 'quarter', 'year'];\n }\n dimensions[d.name] = dim;\n }\n\n // Compile measures (non-derived → Cube metrics; derived → sidecar).\n const measures: Record<string, Metric> = {};\n const derived: DerivedMeasureSpec[] = [];\n const measureFilters: Record<string, FilterCondition> = {};\n\n for (const m of dataset.measures) {\n if (m.derived) {\n derived.push({ name: m.name, op: m.derived.op, of: m.derived.of });\n continue;\n }\n if (m.field) assertDeclared(m.field, 'measure', m.name);\n const metric: Metric = {\n name: m.name,\n // [#6761] Same as the dimension label above — see {@link REGISTRY_LOCALE}.\n label: resolveI18nLabel(m.label, REGISTRY_LOCALE) ?? m.name,\n type: aggregateToMetricType(m),\n // `count` with no field aggregates over rows (*).\n sql: m.field ?? '*',\n };\n if (typeof m.format === 'string') metric.format = m.format;\n measures[m.name] = metric;\n if (m.filter) measureFilters[m.name] = m.filter;\n }\n\n const cube: Cube = {\n name: dataset.name,\n // [#6761] The cube's own display title, same rule. `Cube.title` is optional\n // in the schema, but an absent dataset label already produced the machine\n // name here and that is not what this card changes — only the map case moves.\n title: resolveI18nLabel(dataset.label, REGISTRY_LOCALE) ?? dataset.name,\n sql: dataset.object,\n measures,\n dimensions,\n public: false,\n };\n if (Object.keys(joins).length > 0) cube.joins = joins;\n\n return {\n cube,\n allowedRelationships,\n derived,\n filter: dataset.filter,\n measureFilters,\n };\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n IAnalyticsService,\n AnalyticsQuery,\n AnalyticsResult,\n DatasetSelection,\n DatasetCompareTo,\n} from '@objectstack/spec/contracts';\nimport { emptyGroupValueFor, type FilterCondition } from '@objectstack/spec/data';\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\nimport { bucketKeyToCalendarRange, filterTokenContextFrom, resolveFilterTokens } from '@objectstack/core';\nimport type { CompiledDataset, DerivedMeasureSpec } from './dataset-compiler.js';\nimport { datasetInvalidError } from './dataset-refusal.js';\nimport type { OrderLabelResolver } from './dimension-labels.js';\n\n// Re-export the shared protocol shapes so existing importers keep working.\nexport type { DatasetSelection } from '@objectstack/spec/contracts';\n/** @deprecated use DatasetCompareTo from @objectstack/spec/contracts */\nexport type CompareTo = DatasetCompareTo;\n\n/**\n * Dataset executor (ADR-0021 WS2).\n *\n * Turns a compiled dataset + a presentation's selection (dimensions, measures,\n * runtime filter, compareTo) into one or more `AnalyticsQuery`s against the Cube\n * runtime, then post-processes the results:\n * - resolves the base measures a selection needs (including derived deps),\n * - applies measure-scoped filters via supplementary grouped queries — in\n * EVERY window it runs, the `compareTo` one included (#4820) — and keeps the\n * assembled grid's `fields` describing its DIMENSION columns even when there\n * was no primary query to carry them (#5537),\n * - fills the empty-group value into columns no query reported, by aggregate\n * kind (#4708) — a count/sum over an excluded group is 0, avg/min/max null,\n * - evaluates derived measures (ratio/sum/difference/product) row-by-row (Q1),\n * - shifts the queries for `compareTo` (previousPeriod / previousYear) and\n * attaches `<measure>__compare` columns, re-running the same measure pass\n * so a filtered measure means the same thing in both columns, and — when\n * the shifted dimension is the grid's own time axis — restating the\n * comparison rows' bucket keys in CURRENT-period terms so the two grids\n * merge into one row per bucket instead of stacking two windows on one\n * axis (#6007),\n * - computes server-side totals (`selection.totals.groupings`, #1753) by\n * re-running the selection per dimension subset, so matrix subtotals and\n * the grand total use each measure's true aggregate,\n * - orders and windows the final grid (`order` / `limit` / `offset`, #3588).\n *\n * **Where ordering happens, and why here.** `order`/`limit`/`offset` are applied\n * to the ASSEMBLED grid — after measure-scoped sub-queries are merged in, after\n * `compareTo` columns are attached, and after derived measures are computed —\n * never by forwarding them blindly to every sub-query. Two reasons:\n *\n * 1. **Correctness.** A supplementary measure-scoped query selects ONE measure;\n * forwarding `ORDER BY <other_measure>` to it emits SQL referencing a column\n * that query never selects, and forwarding `LIMIT` truncates it before the\n * merge, so rows silently vanish from the grid. A derived measure has no SQL\n * column at all, yet is a perfectly reasonable sort key.\n * 2. **Coverage.** Only `NativeSQLStrategy` honours `order`/`limit`; the\n * ObjectQL aggregate path has nowhere to put them (`EngineAggregateOptions`\n * has no ordering grammar), and date-bucketed queries are *forced* down that\n * path because native SQL declines granularity. Sorting here makes ordering\n * work identically on every driver and strategy.\n *\n * The single-query case still pushes `order`/`limit`/`offset` DOWN into the SQL\n * (see `canPushDownWindow`) so the database does the work and the echoed `sql`\n * shows it; the post-pass is then a no-op re-sort of already-sorted rows.\n *\n * **What the sort key IS for a label-bearing dimension (#3680).** An order key\n * naming a `select` or `lookup`/`master_detail` dimension sorts by the DISPLAY\n * label the response will carry (option label / related record name), not the\n * stored value — a \"sort by Account\" ordered by opaque FK ids presents as\n * arbitrary once the labels render. The mapping comes through an injected\n * {@link OrderLabelResolver} (built by `queryDataset` over the same\n * label-resolution capabilities the display pass uses); rows keep their raw\n * values — only the COMPARISON substitutes the label — so drill metadata still\n * snapshots stored values downstream. Such keys are never pushed into SQL (the\n * label is not a column there), and the label fetch happens BEFORE `applyWindow`\n * so a \"top 10 by account name\" truncates the right ten.\n *\n * RLS/tenant scoping is NOT handled here — it is enforced inside the strategy\n * via the StrategyContext read-scope hook (D-C). This layer is pure query\n * shaping + arithmetic; the order-label hook is an injected interface, not an\n * engine dependency.\n */\n\n/**\n * Expand `{filter-placeholder}` values across everything a dataset query\n * compares on (framework#3582): the dataset's intrinsic `filter`, the\n * presentation's `runtimeFilter` (a dashboard widget's own scope), every\n * measure-scoped filter, and the `dateRange` bounds of the selection's time\n * dimensions.\n *\n * The dashboard path needs its own call rather than inheriting the ObjectQL\n * engine's: `NativeSQLStrategy` compiles a raw `SELECT … WHERE` and binds\n * comparands directly, so a widget filtered on `{current_year_start}` never\n * passes through `engine.find()` at all — which is exactly why the token\n * reached SQLite as the literal text and every such widget rendered zero.\n *\n * Inputs are treated as immutable: a `CompiledDataset` lives in the service's\n * registry across requests, so resolving in place would bake one request's\n * user id (and one day's dates) into every later render. New objects are\n * allocated only when the tree actually held a placeholder.\n */\nfunction resolveSelectionTokens(\n compiled: CompiledDataset,\n selection: DatasetSelection,\n context?: ExecutionContext,\n): { compiled: CompiledDataset; selection: DatasetSelection } {\n // One instant for the whole call: the intrinsic filter, the runtime filter\n // and each measure filter are resolved in separate passes, and a query whose\n // pieces disagreed about \"now\" could straddle a period boundary — the primary\n // grid scoped to this month while a measure-scoped sub-query saw the next.\n const tokenCtx = filterTokenContextFrom(context, new Date());\n const resolve = <T>(v: T): T => resolveFilterTokens(v, tokenCtx);\n\n const filter = resolve(compiled.filter);\n const measureFilters = resolve(compiled.measureFilters);\n const runtimeFilter = resolve(selection.runtimeFilter);\n const timeDimensions = selection.timeDimensions?.map((td) =>\n td.dateRange == null ? td : { ...td, dateRange: resolve(td.dateRange) },\n );\n\n const compiledChanged =\n filter !== compiled.filter || measureFilters !== compiled.measureFilters;\n const selectionChanged =\n runtimeFilter !== selection.runtimeFilter ||\n (timeDimensions !== undefined &&\n timeDimensions.some((td, i) => td !== selection.timeDimensions![i]));\n\n return {\n compiled: compiledChanged ? { ...compiled, filter, measureFilters } : compiled,\n selection: selectionChanged ? { ...selection, runtimeFilter, timeDimensions } : selection,\n };\n}\n\n/** AND two optional FilterConditions into one (MongoDB-style). */\nexport function combineFilters(\n a?: FilterCondition,\n b?: FilterCondition,\n): FilterCondition | undefined {\n if (a && b) return { $and: [a, b] } as FilterCondition;\n return a ?? b;\n}\n\n/**\n * Partition base measures into those the dataset scopes with their own\n * measure-level `filter` and those it does not — the single place that answers\n * \"does this measure carry its own filter?\".\n *\n * Paired with {@link DatasetExecutor.runMeasurePass}, this is what keeps ONE\n * definition of \"how a measure filter is applied\" for every grouped pass the\n * executor runs: the current period, each `totals` subset, and the `compareTo`\n * window. `compareTo` used to issue a single shifted query over all base\n * measures with only the base filter, consulting `measureFilters` nowhere on\n * that path — so a measure declared `filter: { stage: 'closed_won' }` was\n * scoped in its own column and unscoped in `<measure>__compare`: two different\n * measures rendered side by side under one label, and biased the worst way\n * (the comparison window is inflated by exactly the rows the measure exists to\n * exclude, so \"won deals vs. last month\" reads as a collapse). #4820.\n *\n * The remedy is deliberately NOT a second copy of the filter logic on the\n * compare path — two implementations of one rule diverge again at the next\n * change. Both paths call the same split and the same pass.\n */\nexport function splitMeasuresByFilter(\n measures: Iterable<string>,\n measureFilters: Record<string, FilterCondition | undefined>,\n): { unfiltered: string[]; filtered: string[] } {\n const unfiltered: string[] = [];\n const filtered: string[] = [];\n for (const m of measures) (measureFilters[m] ? filtered : unfiltered).push(m);\n return { unfiltered, filtered };\n}\n\n/**\n * Evaluate derived measures on each aggregated row, mutating a shallow copy.\n * Division by zero (and missing operands) yields `null` rather than Infinity/NaN.\n */\nexport function evaluateDerivedMeasures(\n rows: Record<string, unknown>[],\n derived: DerivedMeasureSpec[],\n): Record<string, unknown>[] {\n if (derived.length === 0) return rows;\n return rows.map((row) => {\n const out = { ...row };\n for (const d of derived) {\n out[d.name] = computeDerived(d, out);\n }\n return out;\n });\n}\n\n/**\n * Fill the EMPTY-GROUP value into every measure column the assembled grid\n * LISTS but no query REPORTED — by aggregate kind (#4708, objectui#3136).\n *\n * The grid is assembled from several results: the primary query, one\n * supplementary query per measure-scoped filter, and (for `compareTo`) a\n * shifted pass. {@link mergeByDimensions} writes a measure's column only onto\n * rows its source result returned, and a `GROUP BY` over a filtered row set\n * emits NO group at all for a dimension value the filter excludes entirely.\n * The column therefore comes back **absent**, not `0` — and absent renders as\n * \"no data for this row\", which for a count is the opposite of what the row\n * means. A derived ratio over it goes null as well ({@link computeDerived}\n * treats a missing operand as unknowable), so the blank spreads.\n *\n * The bias runs the worst possible way: the rows that blank are the ones whose\n * numerator the filter excluded — the WORST-performing rows. A `lead_source`\n * that won nothing renders as \"no data\" while one that won everything renders\n * fine.\n *\n * **Filled strictly by aggregate kind**, never wholesale. `count` /\n * `count_distinct` over an excluded group is unambiguously `0` (\"how many rows\n * matched\" has an exact answer when the answer is none), and `sum` over the\n * empty set is its identity `0`. `avg` / `min` / `max` are genuinely null —\n * there is nothing to average — and flattening those to `0` would trade this\n * lie for the opposite one, reporting a measurement nobody made. The\n * kind→identity mapping is `emptyGroupValueFor` in `@objectstack/spec/data`,\n * shared with the authoring-side coherence checks so the two cannot drift.\n *\n * **Only rows that already exist are touched** — no group is invented. A\n * dimension value no query reported at all has genuinely no data and stays out\n * of the grid; this fills the cell, never the row.\n *\n * Deliberately NOT a `?? 0` in the widget or a `coalesce` in the measure: a\n * consumer-side patch must be repeated by every author of every ratio widget\n * forever, and forgetting it is silent. Only the executor knows which aggregate\n * produced the gap, so only the executor can tell `0` from unknown.\n *\n * Mutates `rows` in place (they are already this pipeline's own copies) and\n * returns them for chaining.\n *\n * @param columnAggregates - Grid column → the aggregate that produced it.\n * Includes `<measure>__compare` columns, which merge through the same seam.\n */\nexport function fillEmptyGroups(\n rows: Record<string, unknown>[],\n columnAggregates: Record<string, string | undefined>,\n): Record<string, unknown>[] {\n for (const [column, aggregate] of Object.entries(columnAggregates)) {\n const empty = emptyGroupValueFor(aggregate);\n if (empty === undefined) continue;\n for (const row of rows) if (row[column] == null) row[column] = empty;\n }\n return rows;\n}\n\nfunction num(v: unknown): number | null {\n if (v == null) return null;\n const n = typeof v === 'number' ? v : Number(v);\n return Number.isFinite(n) ? n : null;\n}\n\nfunction computeDerived(d: DerivedMeasureSpec, row: Record<string, unknown>): number | null {\n const vals = d.of.map((name) => num(row[name]));\n if (vals.some((v) => v === null)) return null;\n const nums = vals as number[];\n switch (d.op) {\n case 'ratio': {\n if (nums.length < 2 || nums[1] === 0) return null;\n return nums[0] / nums[1];\n }\n case 'difference':\n return nums.slice(1).reduce((acc, v) => acc - v, nums[0]);\n case 'sum':\n return nums.reduce((acc, v) => acc + v, 0);\n case 'product':\n return nums.reduce((acc, v) => acc * v, 1);\n default:\n return null;\n }\n}\n\n// ── date bucketing (#3588) ───────────────────────────────────────────────────\n\n/** The date-bucket vocabulary shared by the dataset, the selection, and the\n * bucketing utilities in `@objectstack/core`. */\nexport type DateGranularityValue = NonNullable<DatasetSelection['dateGranularity']>;\n\n/**\n * The EFFECTIVE bucket size for one date dimension of a selection — the single\n * source of truth for granularity precedence.\n *\n * Precedence, per dimension:\n * 1. a `granularity` already stated on that dimension's `timeDimensions`\n * entry — never overridden;\n * 2. `selection.dateGranularity` — the presentation's choice, so a widget can\n * bucket by month without the dataset committing every consumer to it;\n * 3. `datasetDefault` — the dataset dimension's own `dateGranularity`.\n *\n * The unit of precedence is the GRANULARITY, not the entry: a `timeDimensions`\n * entry carrying only a `dateRange` (what `compareTo` needs) states a WINDOW,\n * not a bucket size, and so cannot VETO a bucket size stated elsewhere for a\n * dimension that is being grouped.\n *\n * This answers \"how big is the bucket\", never \"is this dimension bucketed at\n * all\" — a separate question, decided per query by `buildQuery`'s\n * `bucketsUnstatedEntry` (#5688), because a window-only entry for a dimension\n * nobody grouped must stay a filter. Every caller here is already past that\n * question: `buildQuery` asks it first, and the post-processing sites in\n * `analytics-service` iterate `selection.dimensions`, which are grouped by\n * definition.\n *\n * **Why this is exported.** The bucket size chosen here decides three things\n * that MUST agree: the `GROUP BY` the query compiles to, the humanized label\n * each bucket key is rendered as, and the half-open `[gte, lt)` range a bucket\n * drills into. When the query layer resolved granularity and the post-processing\n * in `analytics-service` read the dataset default instead, they silently\n * disagreed for every selection that overrode it — a `year` query came back\n * labelled `1970-01` (a year bucket re-formatted as a month), a `day` query\n * collapsed to duplicate month labels, and `quarter`/`year` lost their drill\n * ranges entirely. One function, called from all three sites, is what stops\n * that drift recurring.\n */\nexport function resolveDimensionGranularity(\n selection: Pick<DatasetSelection, 'timeDimensions' | 'dateGranularity'>,\n dimension: string,\n datasetDefault?: string,\n): DateGranularityValue | undefined {\n // `timeDimensions[].granularity` and the compiled cube's `granularities` are\n // both typed as bare strings by their own layers (Cube.js heritage), but the\n // only values that reach here come from the dataset/selection granularity\n // vocabulary — the same five the bucketing utilities accept.\n const stated = (selection.timeDimensions ?? []).find((t) => t.dimension === dimension)?.granularity;\n if (stated) return stated as DateGranularityValue;\n return selection.dateGranularity ?? (datasetDefault as DateGranularityValue | undefined);\n}\n\n// ── ordering + windowing (#3588) ─────────────────────────────────────────────\n\n/**\n * Compare two grouped-cell values for ORDER BY, ascending.\n *\n * Nulls sort LAST regardless of direction (the SQL `NULLS LAST` convention, and\n * the one users expect: an empty bucket shouldn't win a \"top 10 by revenue\").\n * The caller negates the result for `desc`, so the null branch deliberately\n * returns its verdict BEFORE that negation can flip it — see `compareRows`.\n *\n * Numbers (and numeric strings, which is how some drivers return SUM results)\n * compare numerically so 9 sorts below 10; everything else compares as a string.\n * Dates arrive here already bucketed to sort-stable keys (\"2026-04\", \"2026-Q2\"),\n * so lexicographic ordering is chronological for them too.\n */\nfunction compareValues(a: unknown, b: unknown): number {\n const aNull = a == null || a === '';\n const bNull = b == null || b === '';\n if (aNull || bNull) return aNull && bNull ? 0 : aNull ? 1 : -1;\n if (a instanceof Date || b instanceof Date) {\n return Number(a instanceof Date ? a.getTime() : a) - Number(b instanceof Date ? b.getTime() : b);\n }\n if (typeof a === 'boolean' || typeof b === 'boolean') {\n return Number(a) - Number(b);\n }\n const an = typeof a === 'number' ? a : Number(a);\n const bn = typeof b === 'number' ? b : Number(b);\n if (Number.isFinite(an) && Number.isFinite(bn)) return an - bn;\n return String(a).localeCompare(String(b));\n}\n\n/**\n * Order rows by each key in `order`, in the object's own key order (first key is\n * the primary sort). Returns a NEW array; the input is not mutated. Null/empty\n * cells stay last in both directions (see {@link compareValues}).\n *\n * `sortKeys` substitutes the COMPARED value per key (#3680): when it holds a map\n * for an order key, each cell compares by its mapped value — the display label a\n * label-bearing dimension will render as — falling back to the raw cell where\n * unmapped (an orphaned id or RLS-hidden record renders raw too, so sort and\n * display stay consistent). The rows themselves are never rewritten here.\n */\nexport function applyOrdering(\n rows: Record<string, unknown>[],\n order: Record<string, 'asc' | 'desc'> | undefined,\n sortKeys?: Record<string, Map<unknown, unknown>>,\n): Record<string, unknown>[] {\n const keys = Object.entries(order ?? {});\n if (keys.length === 0 || rows.length < 2) return rows;\n // Array.prototype.sort is stable (ES2019+), so equal rows keep the order the\n // grouping produced — an important property for reproducible LIMITs.\n return [...rows].sort((ra, rb) => {\n for (const [key, dir] of keys) {\n const map = sortKeys?.[key];\n const av = map?.get(ra[key]) ?? ra[key];\n const bv = map?.get(rb[key]) ?? rb[key];\n const aNull = av == null || av === '';\n const bNull = bv == null || bv === '';\n // Nulls last in BOTH directions — decided before `desc` negation.\n if (aNull || bNull) {\n if (aNull && bNull) continue;\n return aNull ? 1 : -1;\n }\n const c = compareValues(av, bv);\n if (c !== 0) return dir === 'desc' ? -c : c;\n }\n return 0;\n });\n}\n\n/** Apply `offset`/`limit` to an already-ordered grid. */\nexport function applyWindow(\n rows: Record<string, unknown>[],\n limit?: number,\n offset?: number,\n): Record<string, unknown>[] {\n const start = offset != null && offset > 0 ? offset : 0;\n if (start === 0 && limit == null) return rows;\n return rows.slice(start, limit != null ? start + limit : undefined);\n}\n\n/**\n * Validate `order` keys and resolve the EFFECTIVE ordering for a selection.\n *\n * A key must name something the caller actually selected — a dimension, a\n * measure, or a `<measure>__compare` column. An unknown key throws rather than\n * being dropped: silently ignoring `sortBy` is precisely the failure mode this\n * change exists to remove (#3588), and a mistyped sort key that quietly returns\n * arbitrarily-ordered rows is worse than a loud 400.\n *\n * When `limit`/`offset` is requested WITHOUT an order, the selected dimensions\n * ascending become the implicit ordering, so the truncated window is\n * reproducible instead of \"whatever the group-by happened to emit\".\n *\n * Failing both, a selected TIME dimension defaults to ASCENDING (#3916). A time\n * axis has one order a reader expects — chronological — and until this default\n * existed nothing supplied it anywhere in the stack: the aggregate path has no\n * ordering grammar, so buckets came back in Map-insertion order, and the pivot\n * builds its column headers in row-arrival order. A month-bucketed matrix\n * therefore rendered `2026-07-01, 2026-07-05, …, 2026-07-02`. Bucket keys are\n * minted sort-stable for exactly this (`2026-07`, `2026-Q3`, `2026-W31`), so\n * ascending IS chronological. An explicit `order` still wins outright — this is\n * a default, not a policy — and non-time dimensions keep whatever order the\n * grouping produced unless the caller asks.\n *\n * @param timeDimensions - The selected dimensions the cube types as `time`, in\n * selection order. The executor resolves these (it owns the cube); passing\n * them in keeps this function pure and directly testable.\n */\nexport function resolveOrdering(\n selection: DatasetSelection,\n dimensions: string[],\n timeDimensions: string[] = [],\n): Record<string, 'asc' | 'desc'> | undefined {\n const order = selection.order;\n if (order && Object.keys(order).length > 0) {\n const selectable = new Set<string>([\n ...dimensions,\n ...selection.measures,\n ...selection.measures.map((m) => `${m}__compare`),\n ]);\n const unknown = Object.keys(order).filter((k) => !selectable.has(k));\n if (unknown.length) {\n // [#5367] `DATASET_INVALID` / 400 — `selection.order` is request input and\n // the message already lists what was selectable, so the caller can fix it.\n throw datasetInvalidError(\n `[dataset-executor] order key(s) ${unknown.map((k) => `\"${k}\"`).join(', ')} — ` +\n `not a selected dimension or measure. Selectable here: ` +\n `${[...selectable].join(', ') || '(none)'}.`,\n );\n }\n return order;\n }\n // Implicit, deterministic ordering so a bare `limit` is reproducible.\n if ((selection.limit != null || selection.offset != null) && dimensions.length > 0) {\n return Object.fromEntries(dimensions.map((d) => [d, 'asc' as const]));\n }\n // #3916 — chronological by default on the time axis.\n const timeKeys = timeDimensions.filter((d) => dimensions.includes(d));\n if (timeKeys.length > 0) {\n return Object.fromEntries(timeKeys.map((d) => [d, 'asc' as const]));\n }\n return undefined;\n}\n\n// ── compareTo date math (deterministic — no Date.now) ────────────────────────\n\nfunction parseUTC(date: string): number {\n // Accepts 'YYYY-MM-DD' (and ISO datetimes); interpreted as UTC.\n const ms = Date.parse(date.length === 10 ? `${date}T00:00:00Z` : date);\n // [#5716] `DATASET_INVALID` / 400 — the string comes from the REQUEST\n // (`selection.timeDimensions[].dateRange`, usually a dashboard's date filter),\n // reaches here only through `shiftRange`'s `compareTo` math, and no schema\n // refines it into a date. A caller who sends an unparseable bound gets told\n // which bound it was; nothing about it is a server fault.\n if (Number.isNaN(ms)) {\n throw datasetInvalidError(`[dataset-executor] invalid date in dateRange: \"${date}\"`);\n }\n return ms;\n}\n\nconst DAY_MS = 86_400_000;\n\nfunction toISODate(ms: number): string {\n return new Date(ms).toISOString().slice(0, 10);\n}\n\nfunction shiftYear(date: string, years: number): string {\n const d = new Date(parseUTC(date));\n d.setUTCFullYear(d.getUTCFullYear() + years);\n return toISODate(d.getTime());\n}\n\n/**\n * Resolve which time dimension a `compareTo` shifts (#5011).\n *\n * `DatasetCompareTo.dimension` is optional, and this is the rule that makes the\n * omission deterministic. It is an EXECUTOR resolution rule, not consumer-side\n * tolerance (PD #12): the choice is made once, here, at the producer of the\n * comparison — so a dashboard widget, a report, and a raw `queryDataset` caller\n * that all omit it get the same dimension or the same error, and no renderer is\n * ever tempted to guess one on their behalf.\n *\n * Candidates use the executor's own long-standing criterion, unchanged: a\n * `timeDimensions` entry that carries a `dateRange`. That is exactly the set\n * {@link shiftRange} can act on — a time dimension with no window has nothing to\n * shift — so the resolution can never select a dimension the shift then fails on.\n *\n * Ambiguity is loud, never silently first-wins: picking `created_at` when the\n * author meant `close_date` produces a comparison column that is wrong rather\n * than missing, which is the failure mode nobody audits. The message names every\n * candidate so the fix is a copy-paste.\n */\nfunction resolveCompareDimension(selection: DatasetSelection): string {\n const cmp = selection.compareTo!;\n const shiftable = (selection.timeDimensions ?? []).filter(\n (t) => (t as { dateRange?: unknown }).dateRange != null,\n );\n const names = shiftable.map((t) => t.dimension);\n\n if (cmp.dimension != null) {\n if (!names.includes(cmp.dimension)) {\n // [#5716] `DATASET_INVALID` / 400 for all three refusals in this function.\n // Every one of them is a verdict about the SELECTION as a whole — which\n // `timeDimensions` carry a `dateRange`, and whether `compareTo` can pick\n // one — so it is `datasetInvalidError`, not the member-level\n // `invalidMemberError`: the fix is to add a window or drop `compareTo`,\n // not to correct a misspelled member. Same request, same document, same\n // family as the order-key and totals refusals already enveloped here\n // (#5367).\n throw datasetInvalidError(\n `[dataset-executor] compareTo requires a timeDimension \"${cmp.dimension}\" with a dateRange. `\n + (names.length > 0\n ? `This selection dates ${names.map((n) => `\"${n}\"`).join(', ')} — name one of those, or omit compareTo.dimension to let the executor choose when there is only one.`\n : 'This selection declares no timeDimension with a dateRange, so there is no window to shift; give the dimension a dateRange (a dashboard date-range filter is the usual source).'),\n );\n }\n return cmp.dimension;\n }\n\n if (names.length === 1) return names[0];\n\n if (names.length === 0) {\n throw datasetInvalidError(\n '[dataset-executor] compareTo needs a dated window to shift, but this selection declares no '\n + 'timeDimension with a dateRange. Give the time dimension a dateRange (a dashboard date-range '\n + 'filter is the usual source), or drop compareTo — a period-over-period comparison is only '\n + 'defined against a bounded window.',\n );\n }\n\n throw datasetInvalidError(\n `[dataset-executor] compareTo.dimension is ambiguous: ${names.length} time dimensions carry a `\n + `dateRange (${names.map((n) => `\"${n}\"`).join(', ')}). Name the one to shift — `\n + `compareTo: { kind: '${cmp.kind}', dimension: '${names[0]}' }.`,\n );\n}\n\n/** Compute the comparison window for a [start,end] range. */\nexport function shiftRange(range: [string, string], kind: CompareTo['kind']): [string, string] {\n const [start, end] = range;\n if (kind === 'previousYear') {\n return [shiftYear(start, -1), shiftYear(end, -1)];\n }\n // previousPeriod — the equal-length window ending the day before `start`.\n const startMs = parseUTC(start);\n const endMs = parseUTC(end);\n const lengthDays = Math.round((endMs - startMs) / DAY_MS) + 1;\n const prevEndMs = startMs - DAY_MS;\n const prevStartMs = prevEndMs - (lengthDays - 1) * DAY_MS;\n return [toISODate(prevStartMs), toISODate(prevEndMs)];\n}\n\n// ── compareTo bucket alignment (#6007) ───────────────────────────────────────\n\n/**\n * The ISO-8601 week label (`2026-W23`) of the UTC calendar day at `ms`.\n *\n * Mirrors the week branch of `@objectstack/objectql`'s `bucketDateValue` — the\n * function that MINTS the bucket keys this executor then has to realign. It is\n * copied rather than imported because `service-analytics` does not depend on\n * `objectql` (it talks to the runtime through `IAnalyticsService`), and the\n * copy is not a blind one: {@link bucketKeyAtOrdinal} is pinned round-trip\n * against `bucketKeyToCalendarRange` — `@objectstack/core`'s exported INVERSE\n * of the same vocabulary, which rejects an impossible week outright — so a\n * drift in either direction fails a test rather than mislabelling a bucket.\n */\nfunction isoWeekKeyOfUtcMs(ms: number): string {\n const target = new Date(ms);\n const dayNum = (target.getUTCDay() + 6) % 7; // Mon=0..Sun=6\n target.setUTCDate(target.getUTCDate() - dayNum + 3); // that week's Thursday\n const firstThursday = new Date(Date.UTC(target.getUTCFullYear(), 0, 4));\n const weekNo =\n 1 +\n Math.round(\n ((target.getTime() - firstThursday.getTime()) / DAY_MS - 3 + ((firstThursday.getUTCDay() + 6) % 7)) / 7,\n );\n return `${target.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`;\n}\n\n/**\n * The ORDINAL of the bucket a UTC calendar day falls in: a monotone integer\n * that advances by exactly 1 per bucket, at every granularity.\n *\n * This is what makes \"the same relative position in the other window\" a\n * computable thing rather than an array index. Counting POSITIONS in the two\n * result sets would have been the obvious implementation and is wrong for the\n * reason every gap-sensitive alignment is wrong: a bucket the current window\n * reported no rows for is missing from that array, so every later bucket\n * silently shifts by one and the comparison column lands on its neighbour.\n * Ordinals are computed from the CALENDAR, so a gap costs nothing.\n *\n * @param ymd - a `YYYY-MM-DD` UTC calendar day.\n */\nexport function bucketOrdinalOfDay(ymd: string, granularity: DateGranularityValue): number {\n const ms = parseUTC(ymd);\n const d = new Date(ms);\n const y = d.getUTCFullYear();\n const m = d.getUTCMonth(); // 0-11\n switch (granularity) {\n case 'year':\n return y;\n case 'quarter':\n return y * 4 + Math.floor(m / 3);\n case 'month':\n return y * 12 + m;\n // 1970-01-01 was a Thursday, so shifting by 3 days puts the Monday boundary\n // on a multiple of 7 and the ordinal advances exactly at each ISO week start.\n case 'week':\n return Math.floor((ms + 3 * DAY_MS) / (7 * DAY_MS));\n case 'day':\n default:\n return Math.floor(ms / DAY_MS);\n }\n}\n\n/**\n * The canonical bucket KEY at an ordinal — the inverse of\n * {@link bucketOrdinalOfDay}, and the only place this package mints a bucket key\n * of its own.\n *\n * The keys produced here MUST be byte-identical to the ones the runtime's\n * grouping produced for the primary pass, because they are compared as merge\n * keys: `2026-01`, `2026-Q1`, `2026`, `2026-01-07`, `2026-W03`. That equality is\n * pinned round-trip against `bucketKeyToCalendarRange` rather than asserted by\n * eye — see `dataset-compare-bucket-alignment.test.ts`.\n */\nexport function bucketKeyAtOrdinal(ordinal: number, granularity: DateGranularityValue): string {\n switch (granularity) {\n case 'year':\n return String(ordinal);\n case 'quarter':\n return `${Math.floor(ordinal / 4)}-Q${(ordinal % 4) + 1}`;\n case 'month':\n return `${Math.floor(ordinal / 12)}-${String((ordinal % 12) + 1).padStart(2, '0')}`;\n case 'week':\n return isoWeekKeyOfUtcMs(ordinal * 7 * DAY_MS - 3 * DAY_MS);\n case 'day':\n default:\n return toISODate(ordinal * DAY_MS);\n }\n}\n\n/**\n * Restate ONE comparison bucket key in CURRENT-period terms (#6007) — the\n * maintainer's ruling of 2026-08-07, direction 1.\n *\n * ## What goes wrong without it\n *\n * When the dimension `compareTo` anchors on is ALSO a grid dimension — a trend\n * chart's own time axis, the standard \"this period vs the same period last\n * year\" shape — the comparison pass groups the SHIFTED window, so its rows key\n * to shifted buckets. {@link mergeByDimensions} keys on `selection.dimensions`,\n * and `2025-01` is not `2026-01`, so not one comparison row ever merged: every\n * one of them was APPENDED as a new row. The grid then read half real value and\n * half confident `0` on every row (`fillEmptyGroups` filling the halves each\n * pass never reported), with the shifted buckets sitting in it as rows from\n * outside the very window the caller filtered to. A 2×2 answer came back as\n * 4 rows × 2 columns with a zero in each.\n *\n * ## The two shift-backs\n *\n * - **`previousYear`** — the shift is a calendar year, so the inverse is a\n * calendar year forward, applied to the bucket's own first day and re-bucketed.\n * `2025-01` → `2026-01`, `2025-Q1` → `2026-Q1`, `2025-W03` → `2026-W03`\n * (the week one year on, which is how a year-over-year weekly trend reads).\n * It is deliberately the exact inverse of the {@link shiftRange} arithmetic\n * that produced the window, `setUTCFullYear` rollover included, so window and\n * key can never disagree about what \"one year\" meant.\n * - **`previousPeriod`** — an arbitrary-length day window has no calendar\n * counterpart, so alignment is by **bucket ordinal**: the n-th bucket of the\n * previous window is the n-th bucket of this one, with `n` counted from each\n * window's own start. This is a semantic the ruling states, not one derived\n * from anything already here.\n *\n * ## Fail-closed, in both directions\n *\n * Returns `null` — meaning \"leave this row's key alone\", i.e. exactly the\n * pre-#6007 behaviour — whenever the alignment is not certain:\n *\n * - the key is not a string (the empty bucket keys as `null` on both\n * aggregation paths, #3839; the two passes' empty buckets already merge with\n * each other, and \"one year after nothing\" is not a date);\n * - the key is not a bucket key of this granularity, so there is no span to\n * shift (a raw timestamp from an unbucketed date dimension lands here);\n * - the shifted-back bucket falls outside the CURRENT window. Two equal-length\n * day windows can tile into different bucket counts (a 31-day window\n * straddling a month boundary yields two month buckets, its neighbour one),\n * and the ordinal rule then has a last bucket with no counterpart. Moving it\n * to a bucket the caller did not ask for would trade a visibly foreign row\n * for a plausible-looking wrong one; it keeps its own key and appends, as it\n * did before.\n *\n * @param currentRange - the selection's own window for the anchor dimension.\n * @param shiftedRange - what {@link shiftRange} made of it.\n */\nexport function alignedCompareBucketKey(\n key: unknown,\n granularity: DateGranularityValue,\n kind: CompareTo['kind'],\n currentRange: [string, string],\n shiftedRange: [string, string],\n): string | null {\n if (typeof key !== 'string' || key.length === 0) return null;\n const span = bucketKeyToCalendarRange(key, granularity);\n if (!span) return null;\n\n const targetOrdinal =\n kind === 'previousYear'\n ? bucketOrdinalOfDay(shiftYear(span.start, 1), granularity)\n : bucketOrdinalOfDay(span.start, granularity) +\n (bucketOrdinalOfDay(currentRange[0], granularity) -\n bucketOrdinalOfDay(shiftedRange[0], granularity));\n\n const first = bucketOrdinalOfDay(currentRange[0], granularity);\n const last = bucketOrdinalOfDay(currentRange[1], granularity);\n if (targetOrdinal < first || targetOrdinal > last) return null;\n return bucketKeyAtOrdinal(targetOrdinal, granularity);\n}\n\nexport class DatasetExecutor {\n /**\n * @param service - The analytics service the executor issues its queries to.\n * @param orderLabels - Optional sort-key label hook (#3680). When provided,\n * an order key naming a label-bearing (`select`/`lookup`) dimension sorts\n * by its display label instead of the stored value. Omit to sort by stored\n * values everywhere (e.g. the draft-preview path, whose seed rows already\n * carry display names).\n */\n constructor(\n private readonly service: IAnalyticsService,\n private readonly orderLabels?: OrderLabelResolver,\n ) {}\n\n /**\n * Execute a dataset selection and return the shaped rows (+ field metadata).\n *\n * @param context - The request's ExecutionContext, threaded into every\n * underlying `IAnalyticsService.query` so the tenant/RLS read scope is\n * applied per request (ADR-0021 D-C).\n */\n async execute(\n compiledInput: CompiledDataset,\n selectionInput: DatasetSelection,\n context?: ExecutionContext,\n ): Promise<AnalyticsResult> {\n // framework#3582 — expand `{current_quarter_start}` / `{current_user_id}`\n // placeholders BEFORE any query is shaped, once for the whole call so every\n // sub-query (measure-scoped, totals, compareTo) shares one instant.\n const { compiled, selection } = resolveSelectionTokens(compiledInput, selectionInput, context);\n\n const result = await this.executeSelection(compiled, selection, context);\n\n // Server-side totals (#1753) — re-run the selection grouped by each\n // requested dimension subset, so a subtotal/grand total is the measure's\n // TRUE aggregate over the underlying rows (an avg total is the average of\n // all rows, not of bucket averages). Re-running the full pipeline keeps\n // measure-scoped filters, derived measures, and compareTo consistent with\n // the primary grid. order/limit/offset are dropped: totals cover the whole\n // selection, and an order key may reference a dimension the grouping drops.\n const groupings = selection.totals?.groupings;\n if (groupings?.length) {\n const selected = new Set(selection.dimensions ?? []);\n const totals: NonNullable<AnalyticsResult['totals']> = [];\n for (const grouping of groupings) {\n const unknown = grouping.filter((d) => !selected.has(d));\n if (unknown.length) {\n // [#5367] `DATASET_INVALID` / 400 — `selection.totals.groupings` is\n // request input, judged against the caller's own `selection.dimensions`.\n throw datasetInvalidError(\n `[dataset-executor] totals grouping [${grouping.join(', ')}] is not a subset of the selected dimensions — unknown: ${unknown.join(', ')}.`,\n );\n }\n const sub = await this.executeSelection(compiled, {\n ...selection,\n dimensions: grouping,\n totals: undefined,\n order: undefined,\n limit: undefined,\n offset: undefined,\n }, context);\n totals.push({ dimensions: grouping, rows: sub.rows });\n }\n result.totals = totals;\n }\n\n return result;\n }\n\n private async executeSelection(\n compiled: CompiledDataset,\n selection: DatasetSelection,\n context?: ExecutionContext,\n ): Promise<AnalyticsResult> {\n const derivedByName = new Map(compiled.derived.map((d) => [d.name, d]));\n const selectedDerived = selection.measures\n .map((m) => derivedByName.get(m))\n .filter((d): d is DerivedMeasureSpec => !!d);\n\n // Base measures = selected non-derived + dependencies of selected derived.\n const baseMeasures = new Set<string>();\n for (const m of selection.measures) {\n if (!derivedByName.has(m)) baseMeasures.add(m);\n }\n for (const d of selectedDerived) {\n for (const dep of d.of) baseMeasures.add(dep);\n }\n\n // Split measures into those with a scoped filter and those without.\n const { unfiltered, filtered } = splitMeasuresByFilter(baseMeasures, compiled.measureFilters);\n\n const baseFilter = combineFilters(compiled.filter, selection.runtimeFilter);\n const dimensions = selection.dimensions ?? [];\n\n // Effective ordering — validated against what this selection projects, with\n // a deterministic dimension order synthesized for a bare `limit` (#3588) and\n // an ascending default on the time axis (#3916).\n const order = resolveOrdering(selection, dimensions, this.timeDimensionsOf(compiled, dimensions));\n\n // #3680 — order keys naming a select/lookup dimension sort by the DISPLAY\n // label the response will carry, not the stored value / FK id. Resolved\n // over the assembled grid below; identified up front because such a key\n // also disqualifies SQL pushdown (the label is not a column the database\n // could ORDER BY, and a SQL LIMIT would truncate the wrong window).\n const labelOrderKeys = this.orderLabels\n ? Object.keys(order ?? {}).filter(\n (k) => dimensions.includes(k) && this.orderLabels!.isLabelBearing(k),\n )\n : [];\n\n // Push `order`/`limit`/`offset` down into the SQL only when this selection\n // is ONE query whose columns can satisfy them. With supplementary\n // measure-scoped queries, a compareTo pass, or derived measures in play, the\n // grid is assembled from several results — a sub-query LIMIT would drop rows\n // before the merge and an ORDER BY would name a column that sub-query never\n // selects. Those cases order in memory below instead.\n const singleQuery = filtered.length === 0 && !selection.compareTo && selectedDerived.length === 0;\n const pushDownKeys = new Set<string>([...dimensions, ...unfiltered]);\n const canPushDownWindow =\n singleQuery && labelOrderKeys.length === 0 &&\n Object.keys(order ?? {}).every((k) => pushDownKeys.has(k));\n const windowQuery = canPushDownWindow\n ? { order, limit: selection.limit, offset: selection.offset }\n : undefined;\n\n // The current-period pass: unfiltered base measures in one query plus one\n // supplementary query per measure-scoped filter, merged by dimension key.\n const result = await this.runMeasurePass(compiled, selection, {\n measures: [...baseMeasures],\n dimensions,\n baseFilter,\n window: windowQuery,\n context,\n });\n\n // compareTo — run the SAME pass over the shifted window and attach.\n if (selection.compareTo) {\n const compareRows = await this.runCompare(compiled, selection, [...baseMeasures], dimensions, baseFilter, context);\n result.rows = mergeByDimensions(\n result.rows,\n compareRows,\n dimensions,\n [...baseMeasures].map((m) => `${m}__compare`),\n );\n for (const m of baseMeasures) result.fields.push({ name: `${m}__compare`, type: 'number' });\n }\n\n // Empty-group fill (#4708) — a group a query never reported reads `0` for a\n // count/sum and stays null for avg/min/max. See {@link fillEmptyGroups} for\n // why this belongs to the executor and not to every widget author.\n //\n // Placed after EVERY merge and before the derived pass, because each merge\n // is a place a column can go missing and `mergeByDimensions` APPENDS rows:\n // - a supplementary measure-scoped query omits the groups its filter\n // excluded (the \"won nothing\" rows — the original defect);\n // - a later supplementary query can append rows for dimension keys no\n // earlier query saw, and those need the same fill;\n // - the compareTo pass appends a row for every bucket that existed in the\n // PREVIOUS window and not in this one, on which *every* base measure is\n // absent — including unfiltered ones, which is why the fill covers all\n // base measures rather than only the filter-scoped ones. Since #6007\n // that append is once again the EDGE it was always documented to be\n // (\"this group sold last month and nothing this month\"): when the\n // shifted dimension is the grid's own time axis the comparison keys are\n // realigned onto the current period first, so the append no longer\n // fires on every single row and no longer carries a bucket from outside\n // the caller's window.\n // Running it before the compare merge left that last class blank, so a lead\n // source that sold last month and nothing this month rendered as \"no data\"\n // instead of 0 — the same worst-row bias, one merge later.\n //\n // Derived measures are evaluated AFTER, so a ratio over a filled 0 computes\n // (0%) instead of being poisoned by an absent operand.\n const fillColumns: Record<string, string | undefined> = {};\n for (const m of baseMeasures) {\n const aggregate = compiled.cube.measures?.[m]?.type;\n fillColumns[m] = aggregate;\n if (selection.compareTo) fillColumns[`${m}__compare`] = aggregate;\n }\n fillEmptyGroups(result.rows, fillColumns);\n\n // Derived measures (computed from base + compare columns already present).\n result.rows = evaluateDerivedMeasures(result.rows, selectedDerived);\n for (const d of selectedDerived) result.fields.push({ name: d.name, type: 'number' });\n\n // Order + window the assembled grid (#3588). Every column the caller may\n // sort by exists by now — merged measure-scoped values, `__compare`\n // columns, and derived measures included. When the window was already\n // pushed into SQL this re-sorts an already-sorted grid (a no-op) and\n // re-slices an already-sliced one; when it could not be (the ObjectQL\n // aggregate path has no ordering grammar, and date-bucketed queries are\n // forced down it), this is what makes `sortBy` work at all.\n //\n // #3680 — for label-bearing order keys, substitute the display label as\n // the SORT KEY, resolved over the grid's distinct values BEFORE the window\n // (a \"top 10 by account name\" must pick the ten by name). Rows keep their\n // raw values — display rewriting stays in `queryDataset`, after the drill\n // metadata snapshots the stored values. A select dimension resolves from\n // field metadata (no query); a lookup costs one batched id→name read.\n let sortKeys: Record<string, Map<unknown, unknown>> | undefined;\n for (const key of labelOrderKeys) {\n const values = [...new Set(result.rows.map((r) => r[key]).filter((v) => v != null))];\n if (values.length === 0) continue;\n const labels = await this.orderLabels!.resolveLabels(key, values);\n if (labels && labels.size > 0) (sortKeys ??= {})[key] = labels;\n }\n result.rows = applyOrdering(result.rows, order, sortKeys);\n result.rows = applyWindow(result.rows, selection.limit, selection.offset);\n\n return result;\n }\n\n /**\n * Run ONE grouped pass over a set of base measures, honouring each measure's\n * own scoped `filter`: the unfiltered measures in a single query, plus one\n * supplementary query per filter-scoped measure, merged back by dimension key.\n *\n * **This is the executor's only implementation of \"how a measure filter is\n * applied\", and every window goes through it** — the current period, each\n * `totals` subset (which re-enters via `executeSelection`), and the\n * `compareTo` window. Before #4820 the comparison window had its own,\n * simpler answer: one shifted query over all base measures with only the\n * base filter, so `compiled.measureFilters` was never read on that path.\n * `won_count` counted won deals and `won_count__compare` counted every deal,\n * under one label, in adjacent columns. Only measures carrying a filter were\n * wrong — which is what made it survive: the unfiltered ones next to them\n * compared correctly.\n *\n * The caller supplies the `selection` this pass queries under, which is how\n * the comparison window differs at all: same measures, same dimensions, same\n * filters — a `timeDimensions` shifted by {@link shiftRange}. Nothing else\n * about the two passes may drift, because anything that does becomes a\n * discrepancy between two columns the reader is invited to subtract.\n *\n * Cost: one extra query per filter-scoped measure when `compareTo` is set.\n * The alternative — declaring the discrepancy in the response — is not one,\n * since the two columns exist to be directly comparable.\n *\n * @param window - Ordering/window to push into the SQL. Only ever set for a\n * selection the caller proved is a single self-sufficient query; a pass\n * that fans out must return its whole grid for the merge.\n */\n private async runMeasurePass(\n compiled: CompiledDataset,\n selection: DatasetSelection,\n opts: {\n measures: string[];\n dimensions: string[];\n baseFilter?: FilterCondition;\n window?: { order?: Record<string, 'asc' | 'desc'>; limit?: number; offset?: number };\n context?: ExecutionContext;\n },\n ): Promise<AnalyticsResult> {\n const { measures, dimensions, baseFilter, window, context } = opts;\n const { unfiltered, filtered } = splitMeasuresByFilter(measures, compiled.measureFilters);\n\n // Primary query: all unfiltered base measures in one pass. When every base\n // measure is filter-scoped, the supplementary queries below build the grid.\n const primary = unfiltered.length > 0 || filtered.length === 0;\n let result: AnalyticsResult;\n if (primary) {\n result = await this.service.query(this.buildQuery(compiled, {\n measures: unfiltered,\n dimensions,\n where: baseFilter,\n selection,\n contextTimezone: context?.timezone,\n window,\n }), context);\n } else {\n result = { rows: [], fields: [] };\n }\n\n // Supplementary queries: one per measure-scoped filter, merged by dimension key.\n const measureNames = new Set(measures);\n for (const [i, m] of filtered.entries()) {\n const mFilter = combineFilters(baseFilter, compiled.measureFilters[m]);\n const sub = await this.service.query(this.buildQuery(compiled, {\n measures: [m], dimensions, where: mFilter, selection,\n contextTimezone: context?.timezone,\n }), context);\n result.rows = mergeByDimensions(result.rows, sub.rows, dimensions, [m]);\n // #5537 — with NO primary query there is nothing carrying the grid's\n // DIMENSION descriptors, and this loop only ever appends MEASURE ones. The\n // dimension columns are still in every row (they are the merge key), so\n // the grid silently described only half of itself: a consumer reading\n // column metadata got no `label` and no `type` for the grouped column and\n // fell back to humanizing the raw key — \"owner\" where the dataset declares\n // \"Owner\". Adopt them from the FIRST supplementary result, which projects\n // exactly the same dimensions this pass groups by.\n //\n // Deliberately taken from the sub-result rather than re-derived from\n // `compiled.cube` here. `buildFieldMeta` (both strategies) and the\n // draft-preview evaluator are the three producers of this shape, and each\n // decides its own projected set — every `dimensions` entry plus each\n // granular `timeDimensions` entry not already among them (#4033's \"one\n // definition, every consumer\"). An executor-side reconstruction would be a\n // fourth copy of that rule, free to drift from the very rows it labels;\n // reading the sub-result makes the two paths converge BY CONSTRUCTION, on\n // whatever the active strategy projects. Non-measure entries only, so the\n // measure descriptor stays the one this loop appends.\n if (!primary && i === 0) {\n for (const f of sub.fields ?? []) {\n if (!measureNames.has(f.name)) result.fields.push(f);\n }\n }\n result.fields.push({ name: m, type: 'number' });\n }\n\n return result;\n }\n\n /**\n * The selected dimensions the compiled cube types as `time`, in selection\n * order (#3916) — the axis {@link resolveOrdering} defaults to ascending.\n *\n * Membership is decided by the DIMENSION's declared type, not by whether the\n * selection happens to bucket it: a `date` dimension left ungranulated groups\n * raw timestamps, and those want chronological order every bit as much as\n * month buckets do. (Both sort correctly — `compareValues` compares Dates and\n * ISO strings chronologically, and bucket keys are minted sort-stable.)\n */\n private timeDimensionsOf(compiled: CompiledDataset, dimensions: string[]): string[] {\n return dimensions.filter((d) => compiled.cube.dimensions[d]?.type === 'time');\n }\n\n /**\n * The EFFECTIVE bucket size one dimension is grouped at for this selection,\n * or `undefined` when it is not a date dimension or nothing states a size (in\n * which case the runtime groups the raw column).\n *\n * One definition, two readers, deliberately: {@link buildQuery} uses it to\n * decide the `GROUP BY`, and {@link runCompare} uses it to realign the\n * comparison pass's bucket keys (#6007). Those two MUST agree — realigning\n * `month` keys a query grouped by `quarter` would move every comparison value\n * onto a bucket that does not exist — and the way to make them agree is to\n * have one of them, not two that look alike.\n */\n private granularityOf(\n compiled: CompiledDataset,\n selection: DatasetSelection,\n name: string,\n ): DateGranularityValue | undefined {\n const cd = compiled.cube.dimensions[name];\n if (cd?.type !== 'time') return undefined;\n const datasetDefault = cd.granularities?.length === 1 ? String(cd.granularities[0]) : undefined;\n return resolveDimensionGranularity(selection, name, datasetDefault);\n }\n\n private buildQuery(\n compiled: CompiledDataset,\n opts: {\n measures: string[];\n dimensions: string[];\n where?: FilterCondition;\n selection: DatasetSelection;\n contextTimezone?: string;\n /**\n * Ordering/window to push DOWN into this query. Set only for a selection\n * the caller proved is a single self-sufficient query (see\n * `canPushDownWindow`); omitted for supplementary/compare sub-queries,\n * which must return their full grid for the merge.\n */\n window?: { order?: Record<string, 'asc' | 'desc'>; limit?: number; offset?: number };\n },\n ): AnalyticsQuery {\n const q: AnalyticsQuery = {\n cube: compiled.cube.name,\n measures: opts.measures,\n dimensions: opts.dimensions,\n // Precedence: explicit selection tz → request's reference tz\n // (ExecutionContext.timezone, ADR-0053 Phase 2) → UTC.\n timezone: opts.selection.timezone ?? opts.contextTimezone ?? 'UTC',\n };\n if (opts.where) q.where = opts.where as Record<string, unknown>;\n // Bucket selected date dimensions. Without this a date dimension groups by\n // the raw timestamp — one bucket per ROW, which is why a \"new accounts by\n // month\" bar chart drew one bar per account instead of one per month\n // (#3588).\n //\n // Granularity precedence, per dimension:\n // 1. a `granularity` already stated on that dimension's\n // `selection.timeDimensions` entry — never overridden;\n // 2. `selection.dateGranularity` — the PRESENTATION's choice, so a widget\n // can bucket by month without the dataset committing every consumer to\n // that granularity;\n // 3. the dataset dimension's own default (the compiler lowers an explicit\n // `dateGranularity` to a single-entry `granularities`; the 5-entry\n // \"all granularities\" list means the dataset stated no default).\n //\n // Note the unit of precedence is the GRANULARITY, not the entry: a stated\n // `granularity` is never overridden, and an entry that states none does not\n // veto a bucket size the selection or the dataset supplies for a dimension\n // that IS being grouped. That is `resolveDimensionGranularity`'s job, and it\n // is unchanged — see `granularityFor` below.\n //\n // What an entry that carries only a `dateRange` decides is something else:\n // whether the dimension is grouped AT ALL (#5688). It is a WINDOW — a\n // filter, and a dashboard date-range picker is the usual source. A filter\n // removes rows; it does not add a grid column or split rows. Backfilling the\n // dataset's default bucket size onto such an entry turned \"by Owner, this\n // quarter\" into \"by Owner × month\": one extra column nobody selected, and\n // one row per owner PER MONTH, with a KPI card then reading the first of\n // them. So the backfill is scoped by {@link bucketsUnstatedEntry} to the\n // entries that are genuinely asking for a bucket.\n //\n // `compareTo` alignment survives that narrowing BY CONSTRUCTION, and this is\n // the pairing to keep in mind before widening or narrowing it again\n // (#3588/#4870 vs #5688 — the two demands meet exactly here):\n //\n // - the comparison pass re-enters this method through `runMeasurePass`\n // with the SAME `opts.dimensions` and the same `selection.dateGranularity`,\n // differing only in the shifted `dateRange`. Both passes therefore reach\n // the same verdict for the same entry, so the two grids are bucketed\n // alike or not at all — never one of each, which is the state #4870\n // fixed (a month-bucketed primary grid merged against raw-timestamp\n // comparison rows shared no dimension key, and every `__compare` column\n // came back empty);\n // - an anchor that IS grouped (listed in `dimensions`, or carrying its own\n // `granularity`) keeps its bucket on both passes — #3588/#4870 intact;\n // - an anchor used only as a window is grouped on NEITHER pass, so it is\n // not a column on either side and the two grids align on the dimensions\n // `mergeByDimensions` actually keys by. That merge has always keyed on\n // `dimensions` alone, so the bucket column the backfill added was never\n // part of the key: with several month-split rows per group the\n // comparison value landed on whichever one the index happened to hold\n // last and the rest read a confident `0`. Not bucketing the window\n // restores the alignment rather than weakening it.\n const selTimeDims = opts.selection.timeDimensions ?? [];\n const selDims = new Set(selTimeDims.map((t) => t.dimension));\n const groupedDims = new Set(opts.dimensions);\n const granularityFor = (name: string): string | undefined =>\n this.granularityOf(compiled, opts.selection, name);\n /**\n * Does a caller-supplied entry that stated NO granularity get one filled in?\n *\n * Only when something in the request says this date is being bucketed:\n * - the dimension is one of the grid dimensions this query groups by (so\n * it is a column regardless, and leaving it unbucketed would group raw\n * timestamps — one bucket per row, the #3588 defect); or\n * - `selection.dateGranularity` is set, which is the presentation stating\n * a bucket size for its date axes.\n * An entry carrying its own `granularity` never reaches here (nothing to\n * fill in), and stays grouped.\n *\n * The dataset dimension's own `dateGranularity` is deliberately NOT such a\n * signal on its own: it is how this date renders WHEN grouped, not a request\n * to group by it. Reading it as one is what made a date-range filter behave\n * like a second GROUP BY.\n */\n const bucketsUnstatedEntry = (dimension: string): boolean =>\n groupedDims.has(dimension) || opts.selection.dateGranularity != null;\n // Fill in a bucket size for caller-supplied entries that named none — for\n // the entries that are asking to be bucketed at all.\n const resolvedTimeDims = selTimeDims.map((t) => {\n if (t.granularity) return t;\n if (!bucketsUnstatedEntry(t.dimension)) return t;\n const granularity = granularityFor(t.dimension);\n return granularity ? { ...t, granularity } : t;\n });\n const explicitTimeDims: Array<{ dimension: string; granularity: string }> = [];\n for (const name of opts.dimensions) {\n if (selDims.has(name)) continue;\n const granularity = granularityFor(name);\n if (granularity) explicitTimeDims.push({ dimension: name, granularity });\n }\n const mergedTimeDims = [...resolvedTimeDims, ...explicitTimeDims];\n if (mergedTimeDims.length > 0) q.timeDimensions = mergedTimeDims as AnalyticsQuery['timeDimensions'];\n // Ordering/window: pushed down ONLY when the caller vouched for it. The\n // executor always re-applies both over the assembled grid, so omitting them\n // here costs correctness nothing — it only moves the work to memory.\n if (opts.window?.order && Object.keys(opts.window.order).length > 0) q.order = opts.window.order;\n if (opts.window?.limit != null) q.limit = opts.window.limit;\n if (opts.window?.offset != null) q.offset = opts.window.offset;\n return q;\n }\n\n private async runCompare(\n compiled: CompiledDataset,\n selection: DatasetSelection,\n measures: string[],\n dimensions: string[],\n baseFilter: FilterCondition | undefined,\n context?: ExecutionContext,\n ): Promise<Record<string, unknown>[]> {\n const cmp = selection.compareTo!;\n // `dimension` is optional since #5011; resolve it (or fail loudly) before\n // touching a window. Both the \"which one?\" and the \"with a dateRange\"\n // questions are answered in one place — see `resolveCompareDimension`.\n const dimension = resolveCompareDimension(selection);\n const td = (selection.timeDimensions ?? []).find((t) => t.dimension === dimension)!;\n const range: [string, string] = Array.isArray(td.dateRange)\n ? [td.dateRange[0], td.dateRange[1] ?? td.dateRange[0]]\n : [td.dateRange as string, td.dateRange as string];\n const shifted = shiftRange(range, cmp.kind);\n const shiftedTd = (selection.timeDimensions ?? []).map((t) =>\n t.dimension === dimension ? { ...t, dateRange: shifted } : t,\n );\n // Run the SAME pass the current period ran, over the shifted window: same\n // measures, same dimensions, same base filter, and — since #4820 — the same\n // measure-scoped filters, applied by the same supplementary sub-queries.\n // Issuing one flat query here instead is what made `<measure>__compare`\n // report a different measure than the column beside it.\n //\n // Going through `runMeasurePass` (and so `buildQuery`) also keeps the\n // comparison pass bucketing its date dimensions EXACTLY like the primary\n // pass. Hand-rolling the query here skipped granularity resolution, so a\n // bucketed primary grid (\"2026-04\") was merged against raw-timestamp\n // comparison rows and no dimension key ever matched — every `__compare`\n // column came back empty. The shifted `timeDimensions` still win for their\n // own dimension (rule 1 of the precedence chain); `window` is deliberately\n // omitted — the comparison grid must stay whole for the merge.\n const sub = await this.runMeasurePass(\n compiled,\n { ...selection, timeDimensions: shiftedTd },\n { measures, dimensions, baseFilter, context },\n );\n\n // #6007 — when the anchor is ALSO a grid dimension, the comparison rows key\n // to the SHIFTED buckets they were grouped into, and `mergeByDimensions`\n // keys on `dimensions`: `2025-01` never equals `2026-01`, so every\n // comparison row was appended instead of merged and the grid came back with\n // twice the rows, a confident `0` in each, and the shifted buckets showing\n // as rows from outside the caller's own window. Restate each comparison\n // bucket key in current-period terms BEFORE the merge — see\n // {@link alignedCompareBucketKey} for the two shift-backs and for every\n // case it deliberately declines to align.\n //\n // Scoped to exactly the shape that is broken, and no wider:\n // - the anchor must be a GRID dimension. A window-only anchor is not a\n // column on either pass, so the two grids already align on the\n // dimensions the merge keys by (#5688) and there is nothing to move;\n // - it must be BUCKETED. An ungrouped date dimension groups raw\n // timestamps, which are instants and not bucket keys — there is no\n // \"same bucket, one year on\" for them, so they are left alone.\n // Both passes read the bucket size through the same `granularityOf`, so the\n // size realigned here is by construction the size grouped by.\n const granularity = dimensions.includes(dimension)\n ? this.granularityOf(compiled, selection, dimension)\n : undefined;\n\n // Rename measure columns to `<measure>__compare` so they merge alongside primary.\n return sub.rows.map((row) => {\n const out: Record<string, unknown> = {};\n for (const dim of dimensions) out[dim] = row[dim];\n if (granularity) {\n const aligned = alignedCompareBucketKey(row[dimension], granularity, cmp.kind, range, shifted);\n if (aligned != null) out[dimension] = aligned;\n }\n for (const m of measures) out[`${m}__compare`] = row[m];\n return out;\n });\n }\n}\n\n/**\n * Key segment for a dimension whose value is null/undefined (#4821).\n *\n * Every other segment {@link dimensionKeyOf} emits begins with a decimal digit\n * (its length prefix), so a segment beginning with anything else cannot be\n * produced by any real value — which is the entire requirement for a sentinel.\n *\n * `undefined` keys the same as `null` deliberately: a row that OMITS the column\n * and a row carrying an explicit null both mean \"no value here\", and drivers do\n * omit null columns from row objects. Splitting on that would re-introduce, one\n * level down, exactly the cross-query split this key exists to prevent.\n */\nconst NULL_DIMENSION_SEGMENT = '~';\n\n/**\n * The composite index key for one row's dimension tuple — **length-prefixed**,\n * so no two distinct tuples can share a key and no character is reserved\n * (#4821).\n *\n * ## What was actually wrong\n *\n * The previous key was `dimensions.map((d) => String(row[d] ?? '')).join(SOH)`,\n * where SOH was a **raw U+0001 byte written literally into the source**. #4821\n * was filed against it reading `join('')`, because a raw control byte renders as\n * nothing — in a terminal, in a GitHub issue body, and in this file. So the\n * headline defect it reports (`['ab','c']` and `['a','bc']` both keying `\"abc\"`)\n * did not in fact reproduce: the separator was present, merely invisible. Two\n * things did:\n *\n * 1. `?? ''` keys a genuinely NULL dimension identically to an empty-string\n * one, so \"unassigned\" merges into \"blank\" — one real group absorbing\n * another's measures, silently, which is the outcome #4821 describes,\n * reached through its second mechanism rather than its first.\n * 2. A single-character separator is unambiguous only while no dimension VALUE\n * contains that character. Dimension values are user data (text fields,\n * imported records), so that is an assumption, not a guarantee — and it\n * fails exactly as silently as the issue predicted.\n *\n * A length prefix settles (2) by construction — `2:ab1:c` and `1:a2:bc` differ\n * for every possible input, nothing is reserved, and no invisible byte is left\n * in the source for the next reader to misread (this comment's own issue was\n * filed because of one). (1) is settled separately and explicitly, by\n * {@link NULL_DIMENSION_SEGMENT}.\n *\n * ## Why each segment is still `String()`-coerced\n *\n * Deliberately — and it is NOT the trade-off {@link rebucketCrossObject} makes\n * one file over, whose key JSON-encodes each part. That function re-buckets the\n * rows of ONE aggregate result: every value in a column came back from a single\n * query and so carries a single JS type, which makes JSON free there and buys a\n * real distinction (the empty bucket `null` vs. the literal string `\"null\"`).\n *\n * This key does the opposite job — it ALIGNS rows across **different queries**:\n * the primary pass against each measure-scoped supplementary pass, and (since\n * #4870) the current window against the shifted `compareTo` window, which now\n * fans out per measure the same way. Nothing guarantees two queries type the\n * same group identically, and {@link compareValues} records this executor's own\n * encounter with it: \"numeric strings, which is how some drivers return SUM\n * results\". `String()` per segment is what keys numeric `1` and string `'1'` the\n * same, so such rows keep merging. A `JSON.stringify` key renders them `1` vs\n * `\"1\"` and would split a group that merges correctly today — trading one silent\n * defect for a new one. Pinned by test; do not \"simplify\" it away.\n */\nfunction dimensionKeyOf(row: Record<string, unknown>, dimensions: string[]): string {\n let key = '';\n for (const d of dimensions) {\n const value = row[d];\n if (value == null) {\n key += NULL_DIMENSION_SEGMENT;\n continue;\n }\n const s = String(value);\n key += `${s.length}:${s}`;\n }\n return key;\n}\n\n/**\n * Left-merge `extra` rows onto `base` rows by their dimension-key tuple,\n * copying the listed value columns. Rows in `extra` with no base match are\n * appended (outer-ish merge so comparison-only buckets still surface).\n *\n * Rows are matched by {@link dimensionKeyOf} — read its notes before changing\n * how the key is built. Both the ambiguity it removes and the type coercion it\n * keeps are load-bearing, and both fail silently when got wrong.\n */\nexport function mergeByDimensions(\n base: Record<string, unknown>[],\n extra: Record<string, unknown>[],\n dimensions: string[],\n valueColumns: string[],\n): Record<string, unknown>[] {\n const keyOf = (row: Record<string, unknown>) => dimensionKeyOf(row, dimensions);\n const index = new Map<string, Record<string, unknown>>();\n for (const row of base) index.set(keyOf(row), row);\n\n for (const row of extra) {\n const key = keyOf(row);\n const target = index.get(key);\n if (target) {\n for (const c of valueColumns) target[c] = row[c];\n } else {\n const fresh: Record<string, unknown> = {};\n for (const d of dimensions) fresh[d] = row[d];\n for (const c of valueColumns) fresh[c] = row[c];\n index.set(key, fresh);\n base.push(fresh);\n }\n }\n return base;\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Dimension display-label resolution (ADR-0021).\n *\n * Analytics groups by the raw stored value of a dimension field. For two field\n * kinds that value is NOT human-readable:\n *\n * - **select** — grouped by the stored option `value` (e.g. `backlog`), but the\n * user-facing text is the option `label` (e.g. `Backlog`).\n * - **lookup / master_detail** — grouped by the foreign-key `id` (e.g.\n * `8eqtuKI4G9IhUsPS`), but the user-facing text is the related record's\n * display field (its name/title).\n *\n * `resolveDimensionLabels` post-processes the result rows IN PLACE, replacing the\n * raw value at `row[dimension.name]` with its display label when one is found.\n * Unresolved values are left untouched so an orphaned id still renders as itself\n * rather than blanking out. Date / number / plain-string dimensions are no-ops.\n *\n * The resolution LOGIC lives here (and is unit-tested); the low-level capabilities\n * — reading an object's field map and fetching id→label pairs — are injected via\n * {@link DimensionLabelDeps} so this module stays free of any engine dependency.\n */\n\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\n\n/** The minimal field shape this resolver needs. */\nexport interface FieldMetaLite {\n type?: string;\n /** Lookup / master_detail target object name. */\n reference?: string;\n /** Select options — the value→label source. */\n options?: Array<{ value: unknown; label?: string }>;\n}\n\n/** Capabilities the resolver needs from the runtime (injected by the plugin). */\nexport interface DimensionLabelDeps {\n /** Return the field map for an object, or `undefined` if unknown. */\n getObjectFields(objectName: string): Record<string, FieldMetaLite> | undefined;\n /**\n * Fetch a map of `id → display label` for the given ids of a target object.\n * The implementation chooses the target's display field. Returning an empty\n * map (e.g. no display field, no data access) leaves the ids unresolved.\n *\n * `scope` (ADR-0021 D-C, #3602) is the TARGET object's own read scope — the\n * RLS/tenant `FilterCondition` the implementation must AND into the label\n * lookup so this never reveals a related record the target object's RLS would\n * hide. The label lookup is a per-record read (`group by id`) dressed as an\n * aggregate; without the scope it leaks display names whenever the referenced\n * object is more restricted than the base object whose rows carry the id.\n * `undefined` means \"no scope for this object\" (global table / unrestricted\n * caller) — the same contract as the read-scope provider.\n *\n * `context` is the request's ExecutionContext — the SECOND belt on the same\n * read (#3602). `scope` is the analytics layer's own predicate; forwarding the\n * context lets the ENGINE's middleware chain scope this per-record read\n * itself, so it stays scoped even if a caller ever reaches this hook without\n * a resolved `scope`. Implementations bridging to an ObjectQL engine MUST\n * forward it; a bridge with nowhere to put it may ignore it.\n */\n fetchRecordLabels(\n targetObject: string,\n ids: unknown[],\n scope?: Record<string, unknown>,\n context?: ExecutionContext,\n ): Promise<Map<unknown, string>>;\n}\n\n/**\n * Resolve the TARGET object's read scope for a label lookup (#3602). Returns the\n * object's RLS/tenant `FilterCondition`, `null`/`undefined` when the object is\n * unscoped, or a rejected promise when the scope cannot be resolved — in which\n * case the resolver fails CLOSED (skips that dimension's labels) rather than\n * fetching unscoped names.\n */\nexport type LabelScopeResolver = (\n targetObject: string,\n) => Promise<Record<string, unknown> | null | undefined> | Record<string, unknown> | null | undefined;\n\nconst LOOKUP_TYPES = new Set(['lookup', 'master_detail']);\n\n/**\n * Sort-key label resolution for `DatasetSelection.order` (#3680).\n *\n * The executor sorts the assembled grid BEFORE `queryDataset` rewrites stored\n * dimension values into display labels, so an order key naming a `select` or\n * `lookup`/`master_detail` dimension used to sort by the stored value / FK id —\n * an order that presents as arbitrary once the labels render. This hook hands\n * the executor JUST the value→label mapping for such a dimension so it can sort\n * by what the user will actually read, while the rows keep their raw values\n * (drill metadata depends on them) and ordering + windowing stay one adjacent\n * step. The executor stays engine-free: it sees this interface, never the\n * engine behind it.\n */\nexport interface OrderLabelResolver {\n /**\n * Whether the dimension's stored value differs from the label it renders as\n * (`select` options, `lookup`/`master_detail` FK ids). Synchronous — the\n * executor consults it when deciding whether the window may be pushed into\n * SQL, before any query runs.\n */\n isLabelBearing(dimension: string): boolean;\n /**\n * Map the given raw stored values of one dimension to display labels.\n * Values missing from the map sort by their raw form — the same thing the\n * user will see rendered for them.\n */\n resolveLabels(dimension: string, values: unknown[]): Promise<Map<unknown, string> | undefined>;\n}\n\n/**\n * Build the executor's {@link OrderLabelResolver} from the dataset's dimension\n * list and the injected label capabilities. Mirrors the classification in\n * {@link resolveDimensionLabels}: a dimension is label-bearing when its field\n * carries select `options` or is a lookup/master_detail with a `reference`.\n *\n * - `select` resolves from field metadata — no query at all.\n * - `lookup`/`master_detail` costs ONE batched id→name read over the distinct\n * grouped values, scoped to the REFERENCED object's own RLS (#3602). Fail\n * closed: an unresolvable scope degrades to sorting by the stored id rather\n * than fetching unscoped — consistent with the display pass, which renders\n * the raw id in that case too.\n */\nexport function createOrderLabelResolver(\n baseObject: string,\n dims: Array<{ name: string; field: string }>,\n deps: DimensionLabelDeps,\n resolveScope?: LabelScopeResolver,\n context?: ExecutionContext,\n): OrderLabelResolver {\n const dimByName = new Map(dims.map((d) => [d.name, d]));\n const metaFor = (dimension: string): FieldMetaLite | undefined => {\n const dim = dimByName.get(dimension);\n return dim ? deps.getObjectFields(baseObject)?.[dim.field] : undefined;\n };\n return {\n isLabelBearing(dimension) {\n const meta = metaFor(dimension);\n if (!meta) return false;\n if (Array.isArray(meta.options) && meta.options.length > 0) return true;\n return !!(meta.type && LOOKUP_TYPES.has(meta.type) && meta.reference);\n },\n async resolveLabels(dimension, values) {\n const meta = metaFor(dimension);\n if (!meta) return undefined;\n if (Array.isArray(meta.options) && meta.options.length > 0) {\n const labelByValue = new Map<unknown, string>();\n for (const opt of meta.options) {\n if (opt && opt.label != null) labelByValue.set(opt.value, String(opt.label));\n }\n return labelByValue;\n }\n if (meta.type && LOOKUP_TYPES.has(meta.type) && meta.reference) {\n let scope: Record<string, unknown> | null | undefined;\n if (resolveScope) {\n try {\n scope = await resolveScope(meta.reference);\n } catch {\n return undefined;\n }\n }\n return deps.fetchRecordLabels(meta.reference, values, scope ?? undefined, context);\n }\n return undefined;\n },\n };\n}\n\n/**\n * Wrap a {@link DimensionLabelDeps} so repeated `fetchRecordLabels` calls\n * within ONE request fetch each id at most once. A selection that sorts by a\n * lookup dimension resolves labels twice — once PRE-window for the sort keys\n * (#3680, over the full grid's ids), once post-window for display (a subset of\n * the same ids) — so with this cache the display pass costs no extra query.\n *\n * Per-request only: entries are keyed by target object alone, which is safe\n * because an object's read scope is constant within one request. Never share\n * an instance across requests.\n */\nexport function withLabelFetchCache(deps: DimensionLabelDeps): DimensionLabelDeps {\n // Per target object: id → label, with `null` marking \"fetched, no label\"\n // (RLS-hidden or orphaned) so unresolvable ids are not re-fetched every call.\n const cache = new Map<string, Map<unknown, string | null>>();\n return {\n getObjectFields: (objectName) => deps.getObjectFields(objectName),\n async fetchRecordLabels(targetObject, ids, scope, context) {\n let known = cache.get(targetObject);\n if (!known) {\n known = new Map();\n cache.set(targetObject, known);\n }\n const missing = ids.filter((id) => !known.has(id));\n if (missing.length > 0) {\n const fetched = await deps.fetchRecordLabels(targetObject, missing, scope, context);\n for (const id of missing) known.set(id, fetched.get(id) ?? null);\n }\n const out = new Map<unknown, string>();\n for (const id of ids) {\n const label = known.get(id);\n if (label != null) out.set(id, label);\n }\n return out;\n },\n };\n}\n\n/** Date-dimension granularity (mirrors the dataset `dateGranularity` enum). */\nexport type DateGranularity = 'day' | 'week' | 'month' | 'quarter' | 'year';\n\nconst pad = (n: number) => String(n).padStart(2, '0');\n\n/**\n * Format a raw date value (epoch-ms number, numeric string, ISO string, or\n * Date) to a human, sort-stable bucket label per granularity. Returns the input\n * unchanged when it isn't a parseable date, so a non-date value never blanks.\n *\n * year → \"2026\"\n * quarter → \"2026-Q2\"\n * month → \"2026-04\"\n * week → \"2026-04-13\" (ISO date of the bucket)\n * day → \"2026-04-15\"\n *\n * Intentionally UTC-only (ADR-0053 Phase 2): timezone bucketing happens\n * upstream in `bucketDate` / `bucketDateValue`, so by the time a value reaches\n * here it is *already* the reference-zone bucket (often a label string like\n * \"2026-Q2\"). Re-applying a timezone here would shift an already-correct\n * `YYYY-MM-DD` day bucket by a day — this is a pure, idempotent re-labeler.\n */\nexport function formatDateBucket(value: unknown, granularity?: DateGranularity | string): unknown {\n if (value == null || value instanceof Date === false) {\n if (typeof value !== 'number' && typeof value !== 'string') return value;\n }\n // A YEAR bucket's canonical key IS the bare year (\"2026\" / 2026) — which the\n // epoch heuristic below would read as 2026 milliseconds and relabel \"1970\".\n // Being idempotent over already-formatted bucket keys is this function's whole\n // contract, and every other granularity's key already survives the round trip\n // (\"2026-Q2\", \"2026-07\", \"2026-07-15\" all fail the pure-digit test); only the\n // year key collides with it. Recognised before parsing, for both the string\n // and numeric forms drivers return.\n if (granularity === 'year') {\n const y = typeof value === 'number' ? value : Number(String(value).trim());\n if (Number.isInteger(y) && y >= 1000 && y <= 9999) return String(y);\n }\n let d: Date;\n if (value instanceof Date) d = value;\n else if (typeof value === 'number') d = new Date(value);\n else {\n const s = String(value).trim();\n // Pure-digit strings are epoch millis (or seconds); otherwise let Date parse ISO.\n d = /^\\d+$/.test(s) ? new Date(Number(s) < 1e12 ? Number(s) * 1000 : Number(s)) : new Date(s);\n }\n if (Number.isNaN(d.getTime())) return value;\n const y = d.getUTCFullYear();\n const m = d.getUTCMonth(); // 0-11\n switch (granularity) {\n case 'year': return String(y);\n case 'quarter': return `${y}-Q${Math.floor(m / 3) + 1}`;\n case 'month': return `${y}-${pad(m + 1)}`;\n case 'week':\n case 'day':\n default: return `${y}-${pad(m + 1)}-${pad(d.getUTCDate())}`;\n }\n}\n\n/**\n * Replace raw dimension values with display labels, in place.\n *\n * @param baseObject - the dataset's base object (where the dimension fields live)\n * @param dims - selected dimensions as `{ name, field, type?, dateGranularity? }`\n * (row key = `name`)\n * @param rows - result rows, mutated in place\n * @param deps - injected runtime capabilities\n * @param resolveScope - (ADR-0021 D-C, #3602) resolves the referenced object's\n * own read scope for a lookup/master_detail dimension's label fetch. When it\n * throws, that dimension's labels are SKIPPED (fail-closed — the raw id renders\n * instead) rather than fetched unscoped. Omit when no read-scope provider is\n * configured (labels then fetch unscoped, as before — no security in play).\n * @param context - the request's ExecutionContext, forwarded to\n * {@link DimensionLabelDeps.fetchRecordLabels} so the engine's own middleware\n * scopes the per-record label read too — the second belt beside `resolveScope`\n * (#3602)\n */\nexport async function resolveDimensionLabels(\n baseObject: string,\n dims: Array<{ name: string; field: string; type?: string; dateGranularity?: DateGranularity | string }>,\n rows: Record<string, unknown>[],\n deps: DimensionLabelDeps,\n resolveScope?: LabelScopeResolver,\n context?: ExecutionContext,\n): Promise<void> {\n if (!rows.length || !dims.length) return;\n const fields = deps.getObjectFields(baseObject);\n if (!fields) return;\n\n for (const dim of dims) {\n const meta = fields[dim.field];\n\n // ── date: epoch / ISO → human bucket label ────────────────────────\n // A date dimension's grouped value is a raw timestamp (or a bucket start);\n // either way it must render as a readable date, not epoch millis.\n if (dim.type === 'date' || (meta && meta.type === 'date')) {\n for (const row of rows) {\n const formatted = formatDateBucket(row[dim.name], dim.dateGranularity);\n if (formatted != null) row[dim.name] = formatted;\n }\n continue;\n }\n\n if (!meta) continue;\n\n // ── select: value → option label ──────────────────────────────────\n if (Array.isArray(meta.options) && meta.options.length > 0) {\n const labelByValue = new Map<unknown, string>();\n for (const opt of meta.options) {\n if (opt && opt.label != null) labelByValue.set(opt.value, String(opt.label));\n }\n if (labelByValue.size === 0) continue;\n for (const row of rows) {\n const raw = row[dim.name];\n const label = labelByValue.get(raw);\n if (label != null) row[dim.name] = label;\n }\n continue;\n }\n\n // ── lookup / master_detail: id → related record display name ───────\n if (meta.type && LOOKUP_TYPES.has(meta.type) && meta.reference) {\n const ids = Array.from(\n new Set(rows.map((r) => r[dim.name]).filter((v) => v != null)),\n );\n if (ids.length === 0) continue;\n // #3602 — the label lookup reads the REFERENCED object by id. Scope it to\n // that object's own RLS so it never surfaces a related record the target's\n // RLS would hide (leak fires when the referenced object is stricter than\n // the base). Fail closed: if the scope can't be resolved, skip this\n // dimension's labels (raw id renders) rather than fetch unscoped.\n let scope: Record<string, unknown> | null | undefined;\n if (resolveScope) {\n try {\n scope = await resolveScope(meta.reference);\n } catch {\n continue;\n }\n }\n const labelById = await deps.fetchRecordLabels(meta.reference, ids, scope ?? undefined, context);\n if (!labelById || labelById.size === 0) continue;\n for (const row of rows) {\n const label = labelById.get(row[dim.name]);\n if (label != null) row[dim.name] = label;\n }\n }\n }\n}\n\n/**\n * Pick the display field for an object from its field map, by convention:\n * an explicit `name`/`title`/`label` field, else the first text-like field.\n * Returns `undefined` when nothing suitable exists.\n */\nexport function pickDisplayField(\n fields: Record<string, FieldMetaLite> | undefined,\n): string | undefined {\n if (!fields) return undefined;\n for (const preferred of ['name', 'title', 'label']) {\n if (fields[preferred]) return preferred;\n }\n for (const [name, meta] of Object.entries(fields)) {\n if (meta.type === 'text' || meta.type === 'string') return name;\n }\n return undefined;\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n//\n// ADR-0037 Phase 3 — draft data preview: evaluate an AnalyticsQuery over an\n// in-memory row set (the pending `seed` draft's records) instead of the real\n// data engine. This is what lets a Live Canvas dashboard chart REAL numbers\n// from the DRAFTED sample data before anything is published — and because\n// publish materializes the *same* seed, the numbers are continuous across\n// the publish boundary.\n//\n// Scope (deliberately the dataset-query subset, not a general engine):\n// • Mongo-style `where` filters ($eq implicit, $ne/$gt/$gte/$lt/$lte/\n// $between/$in/$nin/$contains, $and/$or/$not)\n// • timeDimensions date-range filtering + granularity bucketing\n// (day/week/month/quarter/year)\n// • group-by dimensions; count / countDistinct / sum / avg / min / max\n// • order + limit/offset\n// Anything beyond (joins via `include`, raw SQL) falls back to the caller's\n// normal execution path — the preview simply doesn't claim it.\n\nimport { calendarPartsInTzOrUtc, nextUtcCalendarDay, utcInstantMs } from '@objectstack/core';\nimport type { AnalyticsQuery, AnalyticsResult } from '@objectstack/spec/contracts';\nimport type { Cube } from '@objectstack/spec/data';\n\ntype Row = Record<string, unknown>;\n\n// ── Filters (the unified Query DSL subset) ──────────────────────────────────\n\n/**\n * Order two operands the way every other filter backend orders them.\n *\n * The `Date` arm is load-bearing rather than defensive: `String(new Date())`\n * is `'Mon Jul 27 2026 …'`, which under the plain string ordering below sorts\n * AFTER every `'2026-…'` comparand — so a preview row carrying an instant both\n * disappeared from windows it belongs in and appeared in ones it does not.\n * Measured against the shared matrix, 10 of 16 cases diverged, and unlike the\n * cross-type silence on the drivers this direction ADDS rows: a drafted chart\n * showed numbers no published chart would.\n *\n * The population is real. `Field.datetime`'s storage form is a BSON `Date` on\n * `driver-mongodb` (ADR-0053 D-E2), so rows fetched from a mongo-backed dataset\n * arrive here as `Date` objects, while the comparands are wire text.\n * {@link utcInstantMs} is the same primitive `formula`'s write-side evaluator\n * uses for the same pairing, so the two type-blind surfaces cannot drift.\n *\n * Deliberately narrow: the lift runs only when one side is a `Date` and both\n * read as instants, so string-vs-string keeps ISO lexicographic ordering and a\n * `Field.time` wall clock — which denotes no instant — is left untouched.\n */\nfunction compare(a: unknown, b: unknown): number {\n if (typeof a === 'number' && typeof b === 'number') return a - b;\n if (a instanceof Date || b instanceof Date) {\n const ai = utcInstantMs(a);\n const bi = utcInstantMs(b);\n if (ai !== null && bi !== null) return ai - bi;\n }\n return String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0;\n}\n\n/**\n * The inclusive-upper-bound comparison, with the calendar-day rule (#3777): a\n * bare-day bound means \"through that whole day\", so it is evaluated half-open\n * against the next day. String ordering makes `< nextDay` equivalent to\n * `<= day` for plain date values, so this needs no field-type lookup — which\n * matters here, because the preview sees drafted rows with no schema.\n *\n * Shared by `$lte` and the max of `$between` so the two cannot drift apart.\n */\nfunction lteBound(value: unknown, bound: unknown): boolean {\n const nextDay = nextUtcCalendarDay(bound);\n if (nextDay != null) return compare(value, nextDay) < 0;\n return compare(value, bound) <= 0;\n}\n\nfunction matchOp(value: unknown, op: string, expected: unknown): boolean {\n switch (op) {\n case '$eq': return value === expected || String(value) === String(expected);\n case '$ne': return !(value === expected || String(value) === String(expected));\n case '$gt': return value != null && compare(value, expected) > 0;\n case '$gte': return value != null && compare(value, expected) >= 0;\n case '$lt': return value != null && compare(value, expected) < 0;\n case '$lte': {\n if (value == null) return false;\n // A bare-day upper bound means \"through that whole day\" (#3777): the SQL\n // paths compile it half-open (`< day+1`), and the preview must agree or\n // a drafted chart shows different numbers than the published one. String\n // ordering makes `< nextDay` equivalent to `<= day` for plain date\n // values, so no type lookup is needed here either.\n return lteBound(value, expected);\n }\n case '$between': {\n // Was absent, so it fell to the permissive `default` and matched EVERY\n // row — a drafted chart with a range filter silently charted the whole\n // dataset, then changed at publish (found by the ADR-0053 D-A3 matrix,\n // #4081). The max takes the same whole-day rule as `$lte`.\n if (value == null || !Array.isArray(expected) || expected.length !== 2) return false;\n const [min, max] = expected;\n if (min == null || max == null) return false;\n return compare(value, min) >= 0 && lteBound(value, max);\n }\n case '$in': return Array.isArray(expected) && expected.some((e) => value === e || String(value) === String(e));\n case '$nin': return Array.isArray(expected) && !expected.some((e) => value === e || String(value) === String(e));\n case '$contains': return String(value ?? '').toLowerCase().includes(String(expected ?? '').toLowerCase());\n default: return true; // unknown operator — permissive (preview, reads only)\n }\n}\n\nexport function matchesWhere(row: Row, where: Record<string, unknown> | undefined): boolean {\n if (!where) return true;\n for (const [key, cond] of Object.entries(where)) {\n if (key === '$and') {\n if (!(cond as Row[]).every((c) => matchesWhere(row, c as Row))) return false;\n } else if (key === '$or') {\n if (!(cond as Row[]).some((c) => matchesWhere(row, c as Row))) return false;\n } else if (key === '$not') {\n if (matchesWhere(row, cond as Row)) return false;\n } else if (cond !== null && typeof cond === 'object' && !Array.isArray(cond)) {\n for (const [op, expected] of Object.entries(cond as Row)) {\n if (!matchOp(row[key], op, expected)) return false;\n }\n } else if (!(row[key] === cond || String(row[key]) === String(cond))) {\n return false; // implicit equality\n }\n }\n return true;\n}\n\n// ── Time bucketing ──────────────────────────────────────────────────────────\n\nexport function bucketDate(value: unknown, granularity: string, timezone?: string): string | null {\n const d = new Date(String(value));\n if (Number.isNaN(d.getTime())) return null;\n // ADR-0053 Phase 2: resolve the calendar day in the reference zone so an\n // instant near a tz day-boundary buckets where a user in that zone expects.\n // Unset / 'UTC' / invalid keeps the historical UTC bucketing.\n const { year: y, month, day: dayNum } = calendarPartsInTzOrUtc(d, timezone);\n const m = `${month}`.padStart(2, '0');\n const day = `${dayNum}`.padStart(2, '0');\n switch (granularity) {\n case 'year': return `${y}`;\n case 'quarter': return `${y}-Q${Math.floor((month - 1) / 3) + 1}`;\n case 'month': return `${y}-${m}`;\n case 'week': {\n // Build a UTC date from the zone-shifted parts, then step back to Monday.\n const monday = new Date(Date.UTC(y, month - 1, dayNum));\n const dow = (monday.getUTCDay() + 6) % 7; // Monday=0\n monday.setUTCDate(monday.getUTCDate() - dow);\n return monday.toISOString().slice(0, 10);\n }\n case 'day':\n default:\n return `${y}-${m}-${day}`;\n }\n}\n\n// ── Aggregation ─────────────────────────────────────────────────────────────\n\nfunction aggregate(rows: Row[], metricType: string, field: string): number {\n if (metricType === 'count' || field === '*') {\n if (metricType === 'countDistinct') {\n return new Set(rows.map((r) => r[field]).filter((v) => v != null)).size;\n }\n return rows.length;\n }\n const nums = rows.map((r) => Number(r[field])).filter((n) => Number.isFinite(n));\n switch (metricType) {\n case 'countDistinct': return new Set(rows.map((r) => r[field]).filter((v) => v != null)).size;\n case 'sum': return nums.reduce((a, b) => a + b, 0);\n case 'avg': return nums.length ? nums.reduce((a, b) => a + b, 0) / nums.length : 0;\n case 'min': return nums.length ? Math.min(...nums) : 0;\n case 'max': return nums.length ? Math.max(...nums) : 0;\n default: return nums.length ? nums.reduce((a, b) => a + b, 0) : rows.length;\n }\n}\n\n/**\n * Evaluate `query` over `rows` using the cube's measure/dimension specs.\n * Mirrors the engine strategies' output contract: rows keyed by bare\n * measure/dimension names, `fields` describing each output column.\n */\nexport function evaluateAnalyticsQueryOverRows(\n query: AnalyticsQuery,\n cube: Cube,\n rows: Row[],\n): AnalyticsResult {\n // 1. Row-level filters: `where`, then timeDimension dateRanges.\n let filtered = rows.filter((r) => matchesWhere(r, query.where));\n const timeDims = query.timeDimensions ?? [];\n for (const td of timeDims) {\n const dim = cube.dimensions?.[td.dimension];\n const field = String(dim?.sql ?? td.dimension);\n if (!td.dateRange) continue;\n const [start, end] = Array.isArray(td.dateRange) ? td.dateRange : [td.dateRange, td.dateRange];\n filtered = filtered.filter((r) => {\n const v = String(r[field] ?? '');\n // Bare-day end → half-open `< day+1`, the same translation the SQL\n // paths apply (#3777); a full-timestamp end keeps the historical\n // `'~'`-suffix trick (inclusive of that instant's own sub-values).\n const nextDay = nextUtcCalendarDay(end);\n const inUpper = nextDay != null ? v < nextDay : v <= `${end}~`;\n return v >= String(start) && inUpper;\n });\n }\n\n // 2. Grouping keys: each selected dimension (time dims bucketed).\n const dimensions = query.dimensions ?? [];\n const timezone = query.timezone; // ADR-0053 Phase 2: reference tz for bucketing\n const granByDim = new Map(timeDims.filter((t) => t.granularity).map((t) => [t.dimension, t.granularity!]));\n const keyOf = (r: Row): { key: string; values: Row } => {\n const values: Row = {};\n for (const name of dimensions) {\n const dim = cube.dimensions?.[name];\n const field = String(dim?.sql ?? name);\n const raw = r[field];\n const gran = granByDim.get(name) ?? (dim?.type === 'time' && dim.granularities?.length === 1 ? String(dim.granularities[0]) : undefined);\n values[name] = gran ? bucketDate(raw, gran, timezone) : (raw ?? null);\n }\n return { key: JSON.stringify(values), values };\n };\n\n const groups = new Map<string, { values: Row; rows: Row[] }>();\n for (const r of filtered) {\n const { key, values } = keyOf(r);\n const g = groups.get(key) ?? { values, rows: [] };\n g.rows.push(r);\n groups.set(key, g);\n }\n // No dimensions → a single overall group (even over zero rows: count = 0).\n if (dimensions.length === 0 && groups.size === 0) {\n groups.set('{}', { values: {}, rows: [] });\n }\n\n // 3. Aggregate each measure per group.\n const out: Row[] = [];\n for (const g of groups.values()) {\n const row: Row = { ...g.values };\n for (const m of query.measures) {\n const metric = cube.measures?.[m];\n row[m] = aggregate(g.rows, String(metric?.type ?? 'count'), String(metric?.sql ?? '*'));\n }\n out.push(row);\n }\n\n // 4. Order + paging.\n for (const [col, dir] of Object.entries(query.order ?? {}).reverse()) {\n out.sort((a, b) => (dir === 'desc' ? -1 : 1) * compare(a[col], b[col]));\n }\n const offset = query.offset ?? 0;\n const limited = out.slice(offset, query.limit != null ? offset + query.limit : undefined);\n\n return {\n rows: limited,\n fields: [\n ...dimensions.map((d) => ({ name: d, type: 'string' })),\n ...query.measures.map((m) => ({ name: m, type: 'number' })),\n ],\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { Cube, FilterCondition } from '@objectstack/spec/data';\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\nimport type { IAnalyticsService, IDataDriver } from '@objectstack/spec/contracts';\nimport { AnalyticsService } from './analytics-service.js';\nimport type { AnalyticsServiceConfig } from './analytics-service.js';\nimport type { AnalyticsDriverCapabilities } from './strategies/types.js';\nimport { pickDisplayField, type DimensionLabelDeps } from './dimension-labels.js';\n\n/**\n * Minimal IDataEngine surface required for the auto-bridge.\n * ObjectQL exposes:\n * - `aggregate(object, { where, groupBy, aggregations: [{ function, field, alias }] })`\n * - `execute(sql, options)` for raw SQL pass-through (enables NativeSQLStrategy\n * and lets the analytics layer emit JOINs for relation traversal).\n */\ninterface DataEngineLike {\n aggregate(object: string, options: {\n where?: Record<string, unknown>;\n groupBy?: string[];\n aggregations?: Array<{ function: string; field: string; alias: string }>;\n /** Reference timezone (IANA) for date bucketing — ADR-0053 Phase 2. */\n timezone?: string;\n /**\n * `BaseEngineOptions.context` — identity/tenant of the request. The engine\n * merges it into the operation context (`mergeReadContext`), which is what\n * lets its middleware chain inject RLS into `opCtx.ast.where` (#3602).\n */\n context?: ExecutionContext;\n }): Promise<unknown[]>;\n /**\n * Raw command pass-through (SQL on driver-sql). The options bag is spelled out\n * rather than left as `Record<string, unknown>` because **`object` is load-bearing**:\n * `ObjectQL.execute()` picks its driver in the order\n * `options.object` → `getDriver(object)`, then `options.datasource`, then the\n * default driver. A command that reads an object and omits `object` therefore\n * runs against the DEFAULT datasource — which is how every dataset backed by a\n * telemetry/audit-routed object read `0` from a populated table (#5033).\n */\n execute?(command: unknown, options?: {\n /** Bound parameters for the command. */\n args?: unknown[];\n /** The object this command reads — routes to that object's own datasource. */\n object?: string;\n }): Promise<unknown>;\n /** Return the registered object schema (relationship → target + display-label resolution). */\n getObject?(name: string): {\n fields?: Record<string, {\n type?: string;\n reference?: string;\n options?: Array<{ value: unknown; label?: string }>;\n }>;\n /** Federation marker (ADR-0015): set on objects bound to an external datasource. */\n external?: unknown;\n } | undefined;\n /**\n * [#5288] The datasource an object's rows actually live on, by NAME — the\n * engine's own five-step resolution (explicit `datasource` →\n * `datasourceMapping` → the ADR-0057 §3.6 lifecycle split → the owning\n * package's `defaultDatasource` → the deployment default), not the value the\n * object declares.\n *\n * The declared value used to be read straight off `getObject().datasource`,\n * and it is only step 1 of those five: `ObjectSchema.datasource` defaults to\n * `'default'`, which in the engine means \"no explicit binding, keep looking\".\n * So every object routed by steps 2-4 — `sys_audit_log` among them, routed by\n * `lifecycle.class: 'audit'` — answered `'default'` and pointed diagnostics at\n * a database its rows are not in.\n *\n * `undefined` ⇒ nothing binds the object anywhere and it rides the\n * deployment's default datasource (or this engine cannot answer). Optional\n * because the analytics service runs against engines other than ObjectQL;\n * absent, the probe below simply never answers, which is the same \"cannot\n * answer, do not block\" tiering it already carries.\n */\n resolveEffectiveDatasource?(objectName: string): string | undefined;\n /**\n * Resolve the storage driver backing an object (public ObjectQL accessor).\n * Used to delegate temporal storage-form coercion to the driver, which is the\n * single source of truth for how a `Field.date`/`Field.datetime` is stored on\n * the active dialect. When the hooks are absent, values and column SQL pass\n * through untouched — the contract's identity semantics.\n */\n getDriverForObject?(objectName: string): TemporalDriverSurface | undefined;\n}\n\n/**\n * The slice of the `IDataDriver` CONTRACT the analytics layer consumes —\n * `temporalFilterValue` / `temporalFilterColumnSql` are first-class contract\n * members since ADR-0053 D-A2, no longer a duck-typed local invention. Picked\n * (rather than using `IDataDriver` whole) because `getDriverForObject` hands\n * back whatever the engine registered, and this seam only needs the temporal\n * surface; the runtime `typeof` guards below remain the correct way to consume\n * an optional contract member.\n */\ntype TemporalDriverSurface = Pick<\n IDataDriver,\n 'temporalFilterValue' | 'temporalFilterColumnSql'\n>;\n\n/**\n * Configuration for AnalyticsServicePlugin.\n */\nexport interface AnalyticsServicePluginOptions {\n /** Pre-defined cube definitions (from manifest). */\n cubes?: Cube[];\n /**\n * Probe driver capabilities for a given cube.\n * When omitted, defaults to in-memory only.\n */\n queryCapabilities?: (cubeName: string) => AnalyticsDriverCapabilities;\n /**\n * Execute raw SQL on a driver. Enables NativeSQLStrategy.\n */\n executeRawSql?: (objectName: string, sql: string, params: unknown[]) => Promise<Record<string, unknown>[]>;\n /**\n * Execute ObjectQL aggregate. Enables ObjectQLStrategy.\n */\n executeAggregate?: (objectName: string, options: {\n groupBy?: string[];\n aggregations?: Array<{ field: string; method: string; alias: string }>;\n filter?: Record<string, unknown>;\n /** Reference timezone (IANA) for date bucketing — ADR-0053 Phase 2. */\n timezone?: string;\n /**\n * ADR-0021 D-C (#3602) — the request's ExecutionContext. A custom bridge\n * MUST forward it to its engine so engine-side RLS applies; dropping it is\n * what made the built-in bridge fall open in #3597.\n */\n context?: ExecutionContext;\n }) => Promise<Record<string, unknown>[]>;\n /**\n * ADR-0021 D-C — context-aware per-object read scope (tenant + RLS). The\n * runtime supplies this from its sharing middleware so the analytics raw-SQL\n * path cannot bypass tenant isolation. Receives the request's ExecutionContext\n * and returns the RLS `FilterCondition` for the object (what `RLSCompiler`\n * emits). When omitted, the plugin auto-bridges to a registered `'security'`\n * service exposing `getReadFilter(object, context)` if one is present.\n */\n getReadScope?: (\n objectName: string,\n context?: ExecutionContext,\n ) =>\n | FilterCondition\n | null\n | undefined\n | Promise<FilterCondition | null | undefined>;\n /**\n * ADR-0021 D-C — join allowlist per cube (the dataset's declared `include`).\n * Typically wired from the dataset registry's compiled `allowedRelationships`.\n */\n getAllowedRelationships?: (cubeName: string) => Set<string> | undefined;\n /** Enable debug logging. */\n debug?: boolean;\n /**\n * [#8286] Echo the executed statement back to CALLERS in\n * `AnalyticsResult.sql` (`/api/v1/analytics/query`).\n *\n * Distinct from {@link AnalyticsServicePluginOptions.debug} above, which is\n * server-side log verbosity only: raising log level must never widen what\n * travels to a tenant. Default and rationale live with the service config —\n * see `AnalyticsServiceConfig.debugSql`. Undefined here means \"no host\n * choice\", which the service resolves to development-only.\n */\n debugSql?: boolean;\n}\n\n/**\n * AnalyticsServicePlugin — Kernel plugin for multi-driver analytics.\n *\n * Lifecycle:\n * 1. **init** — Creates `AnalyticsService`, registers as `'analytics'` service.\n * If an existing analytics service is already registered (e.g. MemoryAnalyticsService\n * from dev-plugin), it is captured as the `fallbackService`.\n * 2. **start** — Triggers `'analytics:ready'` hook so other plugins can\n * register cubes or extend the service.\n * 3. **destroy** — Cleans up references.\n *\n * @example\n * ```ts\n * import { LiteKernel } from '@objectstack/core';\n * import { AnalyticsServicePlugin } from '@objectstack/service-analytics';\n *\n * const kernel = new LiteKernel();\n * kernel.use(new AnalyticsServicePlugin({\n * cubes: [ordersCube],\n * queryCapabilities: (cube) => ({ nativeSql: true, objectqlAggregate: true, inMemory: false }),\n * executeRawSql: async (obj, sql, params) => pgPool.query(sql, params).then(r => r.rows),\n * }));\n * await kernel.bootstrap();\n *\n * const analytics = kernel.getService<IAnalyticsService>('analytics');\n * const result = await analytics.query({ cube: 'orders', measures: ['orders.count'] });\n * ```\n */\nexport class AnalyticsServicePlugin implements Plugin {\n name = 'com.objectstack.service-analytics';\n /**\n * Services init() registers on every path (ADR-0116, #4131) — lets the\n * kernel name this plugin when a consumer requires one before it inits.\n */\n providesServices = ['analytics'];\n version = '1.0.0';\n type = 'standard' as const;\n dependencies: string[] = [];\n /**\n * init() probes the `data` engine ObjectQLPlugin provides for the\n * auto-bridge — order-if-present so the probe verdict is deterministic\n * (ADR-0116, #4471). Soft, not hard: without an engine the plugin\n * degrades on purpose (per-query lazy resolution / explicit\n * `executeAggregate`).\n */\n optionalDependencies: string[] = ['com.objectstack.engine.objectql'];\n\n private service?: AnalyticsService;\n private readonly options: AnalyticsServicePluginOptions;\n\n constructor(options: AnalyticsServicePluginOptions = {}) {\n this.options = options;\n }\n\n async init(ctx: PluginContext): Promise<void> {\n // Check if there is an existing analytics service (e.g. from dev-plugin)\n let fallbackService: IAnalyticsService | undefined;\n try {\n const existing = ctx.getService<IAnalyticsService>('analytics');\n if (existing && typeof existing.query === 'function') {\n fallbackService = existing;\n ctx.logger.debug('[Analytics] Found existing analytics service, using as fallback');\n }\n } catch {\n // No existing service — that's fine\n }\n\n // Auto-bridge: when caller did not supply executeAggregate, look up the\n // kernel's IDataEngine (registered as 'data' by ObjectQLPlugin) lazily and\n // translate AnalyticsStrategy's `{method, filter}` shape into the engine's\n // `{function, where}` shape. This lets users write\n // `new AnalyticsServicePlugin({ cubes })`\n // without re-implementing the bridge in every app.\n let executeAggregate = this.options.executeAggregate;\n let autoBridged = false;\n if (!executeAggregate) {\n const tryGetDataEngine = (): DataEngineLike | undefined => {\n try {\n const svc = ctx.getService<DataEngineLike>('data');\n return svc && typeof svc.aggregate === 'function' ? svc : undefined;\n } catch {\n return undefined;\n }\n };\n // Probe now (warn if missing) but resolve at call time so plugin order\n // does not matter as long as 'data' exists by the time a query runs.\n if (!tryGetDataEngine()) {\n ctx.logger.warn(\n '[Analytics] No \"data\" service registered yet at init; ' +\n 'will retry per-query. Register ObjectQLPlugin or pass executeAggregate.',\n );\n }\n executeAggregate = async (objectName, { groupBy, aggregations, filter, timezone, context }) => {\n const engine = tryGetDataEngine();\n if (!engine) {\n throw new Error(\n '[Analytics] Cannot execute aggregate: no IDataEngine (\"data\") service is registered. ' +\n 'Add ObjectQLPlugin to the kernel or supply AnalyticsServicePlugin({ executeAggregate }).',\n );\n }\n const rows = await engine.aggregate(objectName, {\n where: filter,\n groupBy,\n aggregations: aggregations?.map((a) => ({\n function: a.method,\n field: a.field,\n alias: a.alias,\n })),\n // ADR-0053 Phase 2: thread the reference tz so date buckets resolve on\n // that zone's calendar days (engine buckets in-memory when non-UTC).\n timezone,\n // ADR-0021 D-C (#3602): thread the caller's identity so the engine's\n // middleware chain scopes the read itself. `BaseEngineOptions.context`\n // is `.optional()`, so nothing ever forced this bridge to pass it —\n // and it did not, which is how an authenticated aggregate reached the\n // engine with no principal and plugin-security fell open (#3597).\n context,\n });\n return rows as Record<string, unknown>[];\n };\n autoBridged = true;\n }\n\n // Auto-bridge raw SQL when the data engine exposes `execute()` and the\n // caller did not supply their own `executeRawSql`. This unlocks\n // NativeSQLStrategy (priority 10) which can emit `LEFT JOIN`s for\n // dotted dimension/measure references like `account.industry`.\n let executeRawSql = this.options.executeRawSql;\n let autoBridgedRawSql = false;\n if (!executeRawSql) {\n const tryGetExecutor = (): DataEngineLike | undefined => {\n try {\n const svc = ctx.getService<DataEngineLike>('data');\n return svc && typeof svc.execute === 'function' ? svc : undefined;\n } catch {\n return undefined;\n }\n };\n // Always wire the bridge — resolution happens at call time, mirroring\n // the executeAggregate auto-bridge above. This way plugin-init order\n // does not matter as long as `data` exists by the time a query runs.\n executeRawSql = async (objectName, sql, params) => {\n const engine = tryGetExecutor();\n if (!engine || !engine.execute) {\n throw new Error(\n '[Analytics] Cannot execute raw SQL: no IDataEngine (\"data\") service with execute() is registered.',\n );\n }\n // NativeSQLStrategy emits `$1, $2, …` placeholders. Knex (used by\n // driver-sql) speaks `?` placeholders, so translate.\n const knexSql = sql.replace(/\\$(\\d+)/g, '?');\n // #5033 — `object` is ObjectQL's FIRST driver-selection key. This bridge\n // received the object name and dropped it, so every dataset raw-SQL read\n // fell through to the DEFAULT driver while the object-routed path\n // (`executeAggregate` → `engine.aggregate(objectName, …)`, right above)\n // resolved the object's own datasource. The two dataset execution paths\n // must give ONE answer to \"which datasource is this object in\": an\n // object routed elsewhere (ADR-0057 §3.6 telemetry split, an explicit\n // `object.datasource`, a `datasourceMapping` rule) read `no such table`\n // on the default DB and degraded to a confident `0` over live rows.\n const result = await engine.execute(knexSql, { args: params, object: objectName });\n // A driver that cannot run SQL (e.g. the in-memory driver) returns\n // null from execute(). Silently mapping that to [] made EVERY dataset\n // query on such environments report \"No rows\" while looking healthy\n // (HTTP 200, compiled SQL attached). Throw a TYPED error instead so\n // the orchestrator can fall back to an aggregate-based strategy —\n // never fabricate an empty result.\n if (result === null || result === undefined) {\n const err = new Error(\n '[Analytics] The \"data\" engine\\'s driver returned null for raw SQL — ' +\n 'this driver does not support SQL execution. The query will fall back ' +\n 'to an aggregate-based strategy when one is available.',\n ) as Error & { code: string };\n err.code = 'RAW_SQL_UNSUPPORTED';\n throw err;\n }\n if (Array.isArray(result)) return result as Record<string, unknown>[];\n if (typeof result === 'object' && 'rows' in (result as Record<string, unknown>)) {\n return (result as { rows: Record<string, unknown>[] }).rows;\n }\n return [];\n };\n autoBridgedRawSql = true;\n }\n\n // Default capabilities: when we have an aggregate bridge, advertise\n // ObjectQL support so ObjectQLStrategy is selected. Callers can still\n // override via options.queryCapabilities.\n const queryCapabilities = this.options.queryCapabilities\n ?? (() => ({\n nativeSql: !!executeRawSql,\n objectqlAggregate: !!executeAggregate,\n inMemory: false,\n }));\n\n // ADR-0021 D-C — wire the read-scope provider. Prefer an explicit option;\n // otherwise auto-bridge to a registered `'security'` service that exposes\n // `getReadFilter(object, context)` (resolved at call time so plugin-init\n // order does not matter). This keeps analytics decoupled from security.\n interface SecurityReadFilter {\n getReadFilter(\n object: string,\n context?: ExecutionContext,\n ):\n | FilterCondition\n | null\n | undefined\n | Promise<FilterCondition | null | undefined>;\n }\n let getReadScope = this.options.getReadScope;\n let autoBridgedReadScope = false;\n let securityPresentAtInit = false;\n if (!getReadScope) {\n const trySecurity = (): SecurityReadFilter | undefined => {\n try {\n const svc = ctx.getService<SecurityReadFilter>('security');\n return svc && typeof svc.getReadFilter === 'function' ? svc : undefined;\n } catch {\n return undefined;\n }\n };\n // ALWAYS wire the bridge — resolution happens at call time, mirroring the\n // executeAggregate / executeRawSql auto-bridges above. Gating the\n // ASSIGNMENT on an init-time probe (as this did) made analytics RLS\n // silently plugin-ORDER-DEPENDENT: a kernel that registers this plugin\n // before the security plugin got NO read-scope provider at all, so every\n // strategy ran unscoped and only a WARN marked it. The repo's own\n // `bootStack` harness registers in exactly that order, which is why no\n // dogfood test could ever observe analytics RLS.\n securityPresentAtInit = !!trySecurity();\n getReadScope = (object, context) => trySecurity()?.getReadFilter(object, context);\n autoBridgedReadScope = true;\n }\n\n // ADR-0021 — relationship → target-object resolver. A dataset's `include`\n // names lookup/master_detail FIELDS on the base object; the joined TABLE is\n // each field's `reference` target (which can differ from the field name,\n // e.g. lookup `account` → object `crm_account`). Resolve from the 'data'\n // engine's object schema at compile time so cross-object joins target the\n // right table. Resolved lazily so plugin-init order doesn't matter.\n const relationshipResolver = (baseObject: string, relationshipName: string): string | undefined => {\n const engine = (() => {\n try {\n const svc = ctx.getService<DataEngineLike>('data');\n return svc && typeof svc.getObject === 'function' ? svc : undefined;\n } catch { return undefined; }\n })();\n const obj = engine?.getObject?.(baseObject);\n const field = obj?.fields?.[relationshipName];\n if (field && (field.type === 'lookup' || field.type === 'master_detail') && field.reference) {\n return field.reference;\n }\n // Unknown to the schema — fall back to the relationship name as the table\n // (legacy same-name convention). Returning undefined would make the\n // compiler reject the dataset; the name-as-table fallback is safer for\n // engines that don't expose getObject.\n return engine ? undefined : relationshipName;\n };\n\n // ADR-0021 — dimension display-label resolution. `queryDataset` groups by a\n // dimension's raw stored value; for `select` fields the user-facing text is\n // the option label, and for `lookup`/`master_detail` fields it's the related\n // record's display name. Wire the two low-level capabilities the resolver\n // needs from the 'data' engine (resolved lazily so plugin-init order is free):\n // - field metadata (select options + lookup target), via getObject\n // - id→name pairs, via the executeAggregate bridge (group by id + name)\n const dataEngine = (): DataEngineLike | undefined => {\n try {\n const svc = ctx.getService<DataEngineLike>('data');\n return svc && typeof svc.getObject === 'function' ? svc : undefined;\n } catch { return undefined; }\n };\n const labelResolver: DimensionLabelDeps = {\n getObjectFields: (objectName) => dataEngine()?.getObject?.(objectName)?.fields,\n fetchRecordLabels: async (targetObject, ids, scope, context) => {\n const map = new Map<unknown, string>();\n const displayField = pickDisplayField(dataEngine()?.getObject?.(targetObject)?.fields);\n if (!displayField || !executeAggregate || ids.length === 0) return map;\n // #3680 — the sort-key pass hands over the PRE-window id set (every\n // grouped value, not just the displayed page), so a high-cardinality\n // lookup dimension can push thousands of ids through here. Chunk the\n // `$in` so the bound-parameter count stays under every driver's limit\n // (SQLite's historic floor is 999 variables).\n const CHUNK = 500;\n for (let i = 0; i < ids.length; i += CHUNK) {\n // #3602 — AND the referenced object's own read scope into the id filter,\n // with `$and` (never key-merge) so it cannot be displaced by the id\n // predicate — the same composition the strategy uses for the aggregate.\n // Without it this per-record read leaks display names the target's RLS\n // would hide (fires when the referenced object is stricter than the base).\n const idFilter: Record<string, unknown> = { id: { $in: ids.slice(i, i + CHUNK) } };\n const filter = scope ? { $and: [idFilter, scope] } : idFilter;\n // Group by (id, displayField) — one row per record — reusing the aggregate\n // bridge rather than adding a record-fetch capability. A count keeps engines\n // that require ≥1 aggregation happy; the count itself is unused.\n const rows = await executeAggregate(targetObject, {\n groupBy: ['id', displayField],\n aggregations: [{ field: 'id', method: 'count', alias: '_c' }],\n filter,\n // #3602 second belt — `scope` above is the analytics layer's own\n // predicate on this per-record read; the context makes the engine's\n // middleware scope it as well.\n context,\n });\n for (const r of rows) {\n if (r.id != null && r[displayField] != null) map.set(r.id, String(r[displayField]));\n }\n }\n return map;\n },\n };\n\n // ADR-0037 P3 — draft data preview: resolve the PENDING seed draft's rows\n // for an object via the kernel protocol (state:'draft' read — a published\n // seed's rows are already in the real table and must NOT overlay). Lazy\n // service lookup so plugin order doesn't matter; null ⇒ no pending seed ⇒\n // queryDataset falls through to live data.\n const draftRowsResolver = async (objectName: string): Promise<Record<string, unknown>[] | null> => {\n type ProtocolLike = {\n getMetaItems?(req: { type: string; previewDrafts?: boolean }): Promise<unknown>;\n getMetaItem?(req: { type: string; name: string; state?: string }): Promise<unknown>;\n };\n let protocol: ProtocolLike | undefined;\n try {\n protocol = ctx.getService<ProtocolLike>('protocol');\n } catch { return null; }\n if (!protocol?.getMetaItems || !protocol.getMetaItem) return null;\n const res = await protocol.getMetaItems({ type: 'seed', previewDrafts: true }).catch(() => null);\n const list = Array.isArray(res)\n ? res\n : (res && typeof res === 'object' && Array.isArray((res as { items?: unknown[] }).items)\n ? (res as { items: unknown[] }).items\n : []);\n const rows: Record<string, unknown>[] = [];\n let pending = false;\n for (const entry of list) {\n const body = ((entry as { item?: unknown })?.item ?? entry) as { name?: string; object?: string } | null;\n if (!body?.name || body.object !== objectName) continue;\n // Only a PENDING draft row qualifies; getMetaItem({state:'draft'})\n // throws no_draft when the seed is already published.\n const draft = await protocol.getMetaItem({ type: 'seed', name: body.name, state: 'draft' }).catch(() => null);\n const draftBody = (draft as { item?: { records?: unknown[] } } | null)?.item;\n if (!draftBody) continue;\n pending = true;\n for (const r of Array.isArray(draftBody.records) ? draftBody.records : []) {\n if (r && typeof r === 'object') rows.push(r as Record<string, unknown>);\n }\n }\n return pending ? rows : null;\n };\n\n // Temporal storage-form coercion (fixes the SQLite datetime \"No rows\" bug).\n // The raw-SQL strategy binds dashboard relative-date tokens (already expanded\n // to ISO strings) directly, bypassing the driver's CRUD coercion. Delegate to\n // the driver — the single source of truth for the on-disk storage convention —\n // so a `Field.datetime` ISO comparand becomes epoch ms on SQLite, while\n // `Field.date` text and native-timestamp (Postgres) columns pass through\n // unchanged. Resolved at call time so plugin-init order does not matter.\n const coerceTemporalFilterValue = (\n objectName: string,\n fieldName: string,\n value: unknown,\n ): unknown => {\n try {\n const svc = ctx.getService<DataEngineLike>('data');\n const driver = svc?.getDriverForObject?.(objectName);\n if (driver && typeof driver.temporalFilterValue === 'function') {\n return driver.temporalFilterValue(objectName, fieldName, value);\n }\n } catch {\n // No data engine / driver, or it doesn't support coercion — leave the\n // value as-is (today's behaviour; safe for text/native-timestamp paths).\n }\n return value;\n };\n\n // The column half of the same fix (#3912). A SQLite `Field.datetime` column\n // holds BOTH storage forms — INTEGER epoch from a `Date` write, ISO TEXT from\n // a REST/JSON write or a `NOW()` default — so coercing the comparand alone\n // matched whichever half the writer produced and returned an empty window for\n // the other. Ask the driver for the column expression that normalises both.\n const coerceTemporalFilterColumn = (\n objectName: string,\n fieldName: string,\n columnSql: string,\n ): string => {\n try {\n const svc = ctx.getService<DataEngineLike>('data');\n const driver = svc?.getDriverForObject?.(objectName);\n if (driver && typeof driver.temporalFilterColumnSql === 'function') {\n return driver.temporalFilterColumnSql(objectName, fieldName, columnSql);\n }\n } catch {\n // Same tiering as above — an unresolvable driver emits the bare column,\n // which is today's behaviour and correct on every non-mixed dialect.\n }\n return columnSql;\n };\n\n const config: AnalyticsServiceConfig = {\n cubes: this.options.cubes,\n logger: ctx.logger,\n queryCapabilities,\n executeRawSql,\n executeAggregate,\n fallbackService,\n getReadScope,\n getAllowedRelationships: this.options.getAllowedRelationships,\n coerceTemporalFilterValue,\n coerceTemporalFilterColumn,\n relationshipResolver,\n labelResolver,\n // [#8286] Passed through as authored — `undefined` is \"this host did not\n // choose\", which the service resolves to development-only. Defaulting it\n // here would be a second copy of that decision, drifting the moment one\n // of the two moves.\n debugSql: this.options.debugSql,\n // Source-field metadata behind the display chains on result columns:\n // ADR-0053 currency (`currencyConfig.defaultCurrency`) and percent scale\n // (`max`, which is what marks whole-percent storage — objectui#3136).\n sourceFieldMeta: (object: string, field: string) => {\n const f = dataEngine()?.getObject?.(object)?.fields?.[field] as\n | { type?: string; max?: number; currencyConfig?: { defaultCurrency?: string } }\n | undefined;\n return f ? { type: f.type, max: f.max, defaultCurrency: f.currencyConfig?.defaultCurrency } : undefined;\n },\n // #5033 — the datasource an object is bound to, used ONLY to name the\n // actual cause when a dataset's SQL references a table that is not on the\n // datasource the query was routed to. Undefined ⇒ the object rides the\n // default datasource (or the engine cannot answer), and the diagnostic\n // says so rather than inventing a name.\n //\n // [#5288] Asked of the ENGINE's resolver, not of the object's declaration.\n // `getObject(name).datasource` is the declared value — step 1 of the five\n // `getDriver` routes by — so an object placed by a `datasourceMapping`\n // rule, by the ADR-0057 §3.6 lifecycle split, or by its package's\n // `defaultDatasource` answered `'default'`, and the diagnostic named a\n // database the rows are not in. Recomputing those rules here instead would\n // be the second implementation `resolveMappedDatasource` (#4462) exists to\n // prevent: it drifts by one step, silently, and the drift only surfaces as\n // an error message pointing at the wrong database.\n getObjectDatasource: (objectName: string) => dataEngine()?.resolveEffectiveDatasource?.(objectName),\n // ADR-0062 D6 — a federated object carries an `external` block (ADR-0015).\n // Reported so NativeSQLStrategy declines it (its hand-compiled FROM would\n // hit the wrong physical table) and the driver-correct ObjectQL path runs.\n isExternalObject: (objectName: string) => {\n const obj = dataEngine()?.getObject?.(objectName);\n return !!(obj && obj.external != null);\n },\n // [#3867] Existence probe for the cube auto-inference gate. Reads the\n // same schema registry the data path's #3770 gate consults, through the\n // engine accessor this bridge already uses above — so \"which objects\n // exist\" has one answer across /data and /analytics.\n //\n // `dataEngine()` resolves lazily and may be absent entirely (analytics\n // installed without a data engine). Reporting `false` there would 404\n // every cube, so an unresolvable engine reports `true` — \"cannot answer,\n // do not block\" — mirroring the tiering #3770 took on the data path.\n isRegisteredObject: (name: string) => {\n const engine = dataEngine();\n if (!engine) return true;\n return engine.getObject?.(name) != null;\n },\n // [#4437, #5520] Field names for the two source-field gates — measures\n // (#4437) and dimensions/timeDimensions (#5520). Read from the\n // SAME schema registry `isRegisteredObject` above consults (and the data\n // path's #4315 gate reads), so \"which fields exist\" has one answer across\n // /data and /analytics. `undefined` — no engine, unknown object, or an\n // object with no field map (an external datasource whose columns are not\n // mirrored locally) — means \"cannot answer\", and the gate stands down.\n getObjectFieldNames: (objectName: string) => {\n const fields = dataEngine()?.getObject?.(objectName)?.fields;\n if (!fields || typeof fields !== 'object') return undefined;\n const names = Object.keys(fields);\n return names.length > 0 ? names : undefined;\n },\n draftRowsResolver,\n };\n\n if (autoBridgedReadScope && securityPresentAtInit) {\n ctx.logger.info('[Analytics] Auto-bridged getReadScope → \"security\" service (getReadFilter)');\n } else if (autoBridgedReadScope) {\n // The bridge IS wired and will resolve at call time — this is only a\n // heads-up that security had not registered yet at our init. It becomes a\n // real problem only if no security service ever appears.\n ctx.logger.info(\n '[Analytics] getReadScope bridged to the \"security\" service; that service is not ' +\n 'registered yet at init and will be resolved per query (plugin order is not significant).',\n );\n } else if (!getReadScope) {\n ctx.logger.warn(\n '[Analytics] No getReadScope configured and no \"security\" service with getReadFilter found — ' +\n 'analytics queries will NOT enforce tenant/RLS scoping (ADR-0021 D-C). ' +\n 'Supply getReadScope or register a security service in multi-tenant deployments.',\n );\n }\n\n if (autoBridged) {\n ctx.logger.info('[Analytics] Auto-bridged executeAggregate → \"data\" service (IDataEngine)');\n }\n if (autoBridgedRawSql) {\n ctx.logger.info('[Analytics] Auto-bridged executeRawSql → \"data\" service (IDataEngine.execute)');\n }\n\n this.service = new AnalyticsService(config);\n\n // Register or replace the analytics service\n if (fallbackService) {\n ctx.replaceService('analytics', this.service);\n } else {\n ctx.registerService('analytics', this.service);\n }\n\n if (this.options.debug) {\n ctx.hook('analytics:beforeQuery', async (query: unknown) => {\n ctx.logger.debug('[Analytics] Before query', { query });\n });\n }\n\n ctx.logger.info('[Analytics] Service initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n if (!this.service) return;\n\n // Notify other plugins that analytics is ready\n await ctx.trigger('analytics:ready', this.service);\n\n ctx.logger.info(\n `[Analytics] Service started with ${this.service.cubeRegistry.size} cubes: ` +\n `${this.service.cubeRegistry.names().join(', ') || '(none)'}`,\n );\n }\n\n async destroy(): Promise<void> {\n this.service = undefined;\n }\n}\n"],"mappings":"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","names":["import_data","import_ui","import_core","import_data","hit","v","nullSafeNegationOperand","assertDefinedComparands","assertNoFieldReferenceComparand","undefinedComparandError","nullValueSatisfiesOperator","operatorIsNullTotal","nullGuardForFieldSpec","import_core","import_data","import_core","n","import_data","import_data","import_core","aggregate","y","import_core","d"]}
|