@objectstack/service-analytics 17.2.0 → 17.4.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 +899 -0
- package/dist/index.cjs +746 -133
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +275 -34
- package/dist/index.d.ts +275 -34
- package/dist/index.js +739 -115
- package/dist/index.js.map +1 -1
- package/package.json +16 -10
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { IAnalyticsService, Logger, AnalyticsDriverCapabilities, AnalyticsStrategy, AnalyticsQuery, AnalyticsResult, DatasetSelection, CubeMeta, DatasetCompareTo, StrategyContext } from '@objectstack/spec/contracts';
|
|
2
2
|
export { AnalyticsDriverCapabilities, AnalyticsStrategy, DatasetSelection, StrategyContext } from '@objectstack/spec/contracts';
|
|
3
|
-
import { Cube, FilterCondition } from '@objectstack/spec/data';
|
|
3
|
+
import { Cube, FilterCondition, AggregationFunction } from '@objectstack/spec/data';
|
|
4
4
|
import { ExecutionContext } from '@objectstack/spec/kernel';
|
|
5
5
|
import { Dataset } from '@objectstack/spec/ui';
|
|
6
6
|
import { Plugin, PluginContext } from '@objectstack/core';
|
|
@@ -8,12 +8,26 @@ import { Plugin, PluginContext } from '@objectstack/core';
|
|
|
8
8
|
/**
|
|
9
9
|
* CubeRegistry — Central registry for analytics cube definitions.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* The registry is the single source of truth for cube metadata discovery:
|
|
12
|
+
* `getMeta()` maps every registered cube's measure/dimension `label` onto the
|
|
13
|
+
* `CubeMeta` titles served by `GET /api/v1/analytics/meta`, and the strategy
|
|
14
|
+
* chain resolves a query's cube through it.
|
|
14
15
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
16
|
+
* Three sources write to it, all of them from `AnalyticsService`:
|
|
17
|
+
* 1. **Manifest definitions** — `AnalyticsServiceConfig.cubes` (`registerAll`),
|
|
18
|
+
* i.e. explicit cube definitions authored in `objectstack.config.ts`.
|
|
19
|
+
* 2. **Compiled datasets** (ADR-0021) — `compileDataset()`'s Cube, registered
|
|
20
|
+
* under the dataset's name by `queryDataset`.
|
|
21
|
+
* 3. **Ad-hoc query inference** — `ensureCube` / `inferCubeFromQuery` mints a
|
|
22
|
+
* minimal Cube from the members an `AnalyticsQuery` references, once
|
|
23
|
+
* `assertInferableCube` (#3867) has confirmed the name is a registered
|
|
24
|
+
* object. It infers from the QUERY, never from the object's field schema.
|
|
25
|
+
*
|
|
26
|
+
* This list used to read "two sources: manifest definitions, and object schema
|
|
27
|
+
* inference". Neither half was right: sources 2 and 3 were missing, and object
|
|
28
|
+
* schema inference is `inferFromObject` below, which no path in this repository
|
|
29
|
+
* calls (#15019). It is described at the method rather than advertised here,
|
|
30
|
+
* because listing it would promise a source the platform does not deliver.
|
|
17
31
|
*/
|
|
18
32
|
declare class CubeRegistry {
|
|
19
33
|
private cubes;
|
|
@@ -34,14 +48,36 @@ declare class CubeRegistry {
|
|
|
34
48
|
/** Remove all cubes. */
|
|
35
49
|
clear(): void;
|
|
36
50
|
/**
|
|
37
|
-
* Auto-generate a cube definition from an object
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
51
|
+
* Auto-generate a cube definition from an object's FIELD SCHEMA, and register
|
|
52
|
+
* it under `objectName`.
|
|
53
|
+
*
|
|
54
|
+
* ⚠️ Nothing in this repository calls this — the only in-tree caller is a unit
|
|
55
|
+
* test, and every cube the platform registers itself comes from one of the
|
|
56
|
+
* three sources named on the class above (#15019). That is not the same thing
|
|
57
|
+
* as unreachable: `CubeRegistry` is exported from the package entry and
|
|
58
|
+
* `AnalyticsService.cubeRegistry` is public, so a consumer of
|
|
59
|
+
* `@objectstack/service-analytics` can call it, and what it mints does reach
|
|
60
|
+
* the wire — `getMeta()` serves the labels below as `CubeMeta` titles. Whether
|
|
61
|
+
* this published method is removed or wired up as a real cube source is #15019.
|
|
62
|
+
*
|
|
63
|
+
* Heuristic rules, measured by driving the built package (the list this
|
|
64
|
+
* replaces claimed three behaviours the code does not have — `min`/`max`
|
|
65
|
+
* measures, a `count` measure for booleans, and a computed-field exclusion):
|
|
66
|
+
* - `number` / `currency` / `percent` fields → one `sum` and one `avg` measure
|
|
67
|
+
* each, labelled with the field's label plus ` (Sum)` / ` (Avg)`. No `min`
|
|
68
|
+
* or `max` measure is minted.
|
|
69
|
+
* - EVERY field becomes a dimension; there is no computed-field exclusion (the
|
|
70
|
+
* `fields` parameter carries no flag one could exclude on).
|
|
71
|
+
* - `boolean` fields become a `boolean` DIMENSION and nothing else — no count
|
|
72
|
+
* measure is minted for them.
|
|
73
|
+
* - `date` / `datetime` fields → `time` dimensions granulated
|
|
74
|
+
* day/week/month/quarter/year.
|
|
75
|
+
* - A default `count` measure labelled `Count` is always added.
|
|
76
|
+
*
|
|
77
|
+
* Those three defaults (`Count`, and the two composites) are English literals
|
|
78
|
+
* with no i18n hook; #14492's ruling listed the `Count` one as a site to carry
|
|
79
|
+
* the `builtinAggregate` discriminator, and it was left alone because no
|
|
80
|
+
* in-repo path reaches it.
|
|
45
81
|
*
|
|
46
82
|
* @param objectName - The snake_case object name (used as table/cube name)
|
|
47
83
|
* @param fields - Array of field descriptors `{ name, type, label? }`
|
|
@@ -328,10 +364,28 @@ interface AnalyticsServiceConfig {
|
|
|
328
364
|
*/
|
|
329
365
|
executeAggregate?: (objectName: string, options: {
|
|
330
366
|
groupBy?: string[];
|
|
367
|
+
/**
|
|
368
|
+
* The local mirror of `StrategyContext.executeAggregate`'s aggregation
|
|
369
|
+
* entries (`packages/spec/src/contracts/analytics-service.ts`), kept in
|
|
370
|
+
* lockstep with it member by member. The two that lockstep is load-bearing
|
|
371
|
+
* for:
|
|
372
|
+
*
|
|
373
|
+
* - `filter` (#10576, the #10413 phase-2 contract field) — a bridge that
|
|
374
|
+
* reconstructs the aggregation entries (as `AnalyticsServicePlugin`'s
|
|
375
|
+
* auto-bridge does, to rename `method` → the engine's `function`) MUST
|
|
376
|
+
* forward this field or a measure-scoped filter `ObjectQLStrategy`
|
|
377
|
+
* lowers never reaches storage.
|
|
378
|
+
* - `method` is the spec's OWN six-value `AggregationFunction`, not
|
|
379
|
+
* `string`: #12776 narrowed the contract, #12940 brought this mirror
|
|
380
|
+
* back into line. Widening it here again would not be a local matter —
|
|
381
|
+
* a bridge author types their handler against THIS declaration, so what
|
|
382
|
+
* they would get is a vocabulary the contract no longer has.
|
|
383
|
+
*/
|
|
331
384
|
aggregations?: Array<{
|
|
332
385
|
field: string;
|
|
333
|
-
method:
|
|
386
|
+
method: AggregationFunction;
|
|
334
387
|
alias: string;
|
|
388
|
+
filter?: Record<string, unknown>;
|
|
335
389
|
}>;
|
|
336
390
|
filter?: Record<string, unknown>;
|
|
337
391
|
/** Reference timezone (IANA) for date bucketing — ADR-0053 Phase 2. */
|
|
@@ -502,6 +556,24 @@ interface AnalyticsServiceConfig {
|
|
|
502
556
|
defaultCurrency?: string;
|
|
503
557
|
max?: number;
|
|
504
558
|
} | undefined;
|
|
559
|
+
/**
|
|
560
|
+
* [#15684] The SQL dialect of the datasource backing `object` — `'sqlite'`,
|
|
561
|
+
* `'postgres'`, `'mysql'`, or `undefined` when the host cannot answer.
|
|
562
|
+
*
|
|
563
|
+
* The three SQL compilers need it for ONE thing: the case-EXACT text family
|
|
564
|
+
* (`$contains` / `$notContains` / `$startsWith` / `$endsWith`, #4706 Q2 = A)
|
|
565
|
+
* has no construct that is case-exact and parses on every dialect. A plain
|
|
566
|
+
* `LIKE` is case-exact on Postgres alone — SQLite folds ASCII case
|
|
567
|
+
* unconditionally, so the query's own `where` AND the RLS read scope
|
|
568
|
+
* admitted rows the predicate excludes there (over-reach, #3948). The
|
|
569
|
+
* per-dialect table lives in `text-match-sql.ts`.
|
|
570
|
+
*
|
|
571
|
+
* Answered by the plugin from the driver that will EXECUTE the statement, so
|
|
572
|
+
* the driver stays the single source of truth for its own dialect. A host
|
|
573
|
+
* that wires nothing keeps the `LIKE` the compilers always emitted —
|
|
574
|
+
* "cannot answer, do not block".
|
|
575
|
+
*/
|
|
576
|
+
sqlDialect?: (object: string) => string | undefined;
|
|
505
577
|
/** Pre-defined datasets to compile + register at construction (ADR-0021). */
|
|
506
578
|
datasets?: Dataset[];
|
|
507
579
|
/**
|
|
@@ -619,6 +691,27 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
619
691
|
* `getReadScope(objectName)` that already knows the active tenant.
|
|
620
692
|
*/
|
|
621
693
|
private callCtx;
|
|
694
|
+
/**
|
|
695
|
+
* [#12230] Copy-on-write expansion of filter placeholders across everything
|
|
696
|
+
* a DIRECT analytics query compares on: `where` and each time dimension's
|
|
697
|
+
* `dateRange` — the same positions `DatasetExecutor.resolveSelectionTokens`
|
|
698
|
+
* covers for the dashboard door, minus the dataset-only channels it alone
|
|
699
|
+
* carries (measure filters ride the dataset-scope getter below).
|
|
700
|
+
*
|
|
701
|
+
* The input is never mutated: a query object can be caller-owned metadata
|
|
702
|
+
* (a saved report definition, a flow node's config) reused across requests,
|
|
703
|
+
* and resolving in place would bake one request's user id into every later
|
|
704
|
+
* render. Returns the SAME object when nothing resolved.
|
|
705
|
+
*/
|
|
706
|
+
private resolveQueryTokens;
|
|
707
|
+
/**
|
|
708
|
+
* [#12230] A per-request `getDatasetScope` whose answers have their filter
|
|
709
|
+
* placeholders resolved against THIS caller. See `callCtx` for why the
|
|
710
|
+
* registry's copy cannot be handed out raw. Token-free scopes pass through
|
|
711
|
+
* by reference — `resolveFilterTokens` returns its input unchanged when the
|
|
712
|
+
* tree holds no placeholder, so the common case allocates nothing.
|
|
713
|
+
*/
|
|
714
|
+
private resolvedDatasetScopeGetter;
|
|
622
715
|
/**
|
|
623
716
|
* Resolve the read scope (tenant + RLS `FilterCondition`) for the base object
|
|
624
717
|
* AND every joined object of the query's cube, keyed by object name. This is
|
|
@@ -644,7 +737,7 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
644
737
|
* aggregate bridge) instead of failing — or worse, fabricating empty rows.
|
|
645
738
|
* Any other error propagates untouched.
|
|
646
739
|
*/
|
|
647
|
-
query(
|
|
740
|
+
query(queryInput: AnalyticsQuery, context?: ExecutionContext): Promise<AnalyticsResult>;
|
|
648
741
|
/**
|
|
649
742
|
* [#8286] Withhold the executed statement unless this host enabled the echo.
|
|
650
743
|
*
|
|
@@ -679,6 +772,42 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
679
772
|
queryDataset(dataset: Dataset, selection: DatasetSelection, context?: ExecutionContext, options?: {
|
|
680
773
|
previewDrafts?: boolean;
|
|
681
774
|
}): Promise<AnalyticsResult>;
|
|
775
|
+
/**
|
|
776
|
+
* The dataset dimensions this selection GROUPED THE GRID BY, resolved against
|
|
777
|
+
* the dataset definition. Shared by drill metadata, row-value label
|
|
778
|
+
* resolution and — through {@link enrichResultColumns} — the dimension column
|
|
779
|
+
* headers, so all three answer "which dimensions" the same way.
|
|
780
|
+
*/
|
|
781
|
+
private selectedDimensions;
|
|
782
|
+
/**
|
|
783
|
+
* ADR-0021 — describe the result's COLUMNS from the dataset's own authored
|
|
784
|
+
* definition: a measure's `label` / `format` / `currency` / `percentScale` /
|
|
785
|
+
* `builtinAggregate` and the `type` its aggregate really returns, then a
|
|
786
|
+
* dimension column's header `label`.
|
|
787
|
+
*
|
|
788
|
+
* **Every key here is read off the DATASET** (the authored measure or
|
|
789
|
+
* dimension) **and `sourceFieldMeta`** (the source object's declared field
|
|
790
|
+
* metadata). Not one is read off `result.rows`. That is what makes this one
|
|
791
|
+
* seam serve both paths that produce a dataset response — the live engine
|
|
792
|
+
* query and the ADR-0037 P3 draft-data preview — and it is why #16097 was a
|
|
793
|
+
* defect rather than a deliberate omission: the preview branch returns ~250
|
|
794
|
+
* lines before this ran, so a response over drafted seed rows carried none of
|
|
795
|
+
* these keys and a renderer fell back to humanizing the raw measure name and
|
|
796
|
+
* guessing a percent scale from magnitude — the exact failures #5537,
|
|
797
|
+
* objectui#3136 and #14492 each closed on the live path.
|
|
798
|
+
*
|
|
799
|
+
* Extracted rather than copied onto the second path, for the reason the
|
|
800
|
+
* `type` correction below already gives for living here at all: this is ONE
|
|
801
|
+
* rule holding both halves of the question, and a per-path copy would be two
|
|
802
|
+
* implementations of it, free to drift.
|
|
803
|
+
*
|
|
804
|
+
* ⛔ Not here, and deliberately: dimension VALUE label resolution
|
|
805
|
+
* ({@link resolveDimensionLabels}), which rewrites the grouped value in each
|
|
806
|
+
* ROW. That reads the rows, it is the one enrichment a seed-draft row set can
|
|
807
|
+
* make unnecessary, and the preview path skips it on purpose — see the note
|
|
808
|
+
* at that early return.
|
|
809
|
+
*/
|
|
810
|
+
private enrichResultColumns;
|
|
682
811
|
/**
|
|
683
812
|
* Get cube metadata for discovery.
|
|
684
813
|
*/
|
|
@@ -686,7 +815,7 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
686
815
|
/**
|
|
687
816
|
* Generate SQL for a query without executing it (dry-run).
|
|
688
817
|
*/
|
|
689
|
-
generateSql(
|
|
818
|
+
generateSql(queryInput: AnalyticsQuery, context?: ExecutionContext): Promise<{
|
|
690
819
|
sql: string;
|
|
691
820
|
params: unknown[];
|
|
692
821
|
}>;
|
|
@@ -922,10 +1051,28 @@ interface AnalyticsServicePluginOptions {
|
|
|
922
1051
|
*/
|
|
923
1052
|
executeAggregate?: (objectName: string, options: {
|
|
924
1053
|
groupBy?: string[];
|
|
1054
|
+
/**
|
|
1055
|
+
* The CUSTOM bridge's view of the aggregation entries — an app author's
|
|
1056
|
+
* own `executeAggregate`, as opposed to the auto-bridge below. Mirrors
|
|
1057
|
+
* `StrategyContext.executeAggregate`
|
|
1058
|
+
* (`packages/spec/src/contracts/analytics-service.ts`) and must stay in
|
|
1059
|
+
* lockstep with it; the two members that lockstep is load-bearing for:
|
|
1060
|
+
*
|
|
1061
|
+
* - `filter` (#10576, the #10413 contract field) — a custom bridge MUST
|
|
1062
|
+
* forward it to the real engine the same way the auto-bridge does, or a
|
|
1063
|
+
* measure-scoped filter this plugin lowers onto the aggregation
|
|
1064
|
+
* silently never reaches storage.
|
|
1065
|
+
* - `method` is the spec's OWN six-value `AggregationFunction`, not
|
|
1066
|
+
* `string`: #12776 narrowed the contract, #12940 brought this mirror
|
|
1067
|
+
* back into line. This is the declaration a custom-bridge author types
|
|
1068
|
+
* their handler against, so it is where the compile-time vocabulary
|
|
1069
|
+
* #12776 bought for strategy authors reaches them too.
|
|
1070
|
+
*/
|
|
925
1071
|
aggregations?: Array<{
|
|
926
1072
|
field: string;
|
|
927
|
-
method:
|
|
1073
|
+
method: AggregationFunction;
|
|
928
1074
|
alias: string;
|
|
1075
|
+
filter?: Record<string, unknown>;
|
|
929
1076
|
}>;
|
|
930
1077
|
filter?: Record<string, unknown>;
|
|
931
1078
|
/** Reference timezone (IANA) for date bucketing — ADR-0053 Phase 2. */
|
|
@@ -1164,7 +1311,37 @@ declare class DatasetExecutor {
|
|
|
1164
1311
|
*/
|
|
1165
1312
|
declare function mergeByDimensions(base: Record<string, unknown>[], extra: Record<string, unknown>[], dimensions: string[], valueColumns: string[]): Record<string, unknown>[];
|
|
1166
1313
|
|
|
1167
|
-
|
|
1314
|
+
/**
|
|
1315
|
+
* [#14079] What a caller can tell this compiler about the columns a read scope
|
|
1316
|
+
* names, beyond the scope itself.
|
|
1317
|
+
*/
|
|
1318
|
+
interface ReadScopeCompileOptions {
|
|
1319
|
+
/**
|
|
1320
|
+
* Is `field` a column whose STORED value is never text — a declared numeric
|
|
1321
|
+
* or boolean scalar (`NON_TEXT_STORED_VALUE_TYPES`, `@objectstack/spec`)?
|
|
1322
|
+
* When it answers `true`, a text operator over that column compiles to the
|
|
1323
|
+
* contract's constant (`1 = 0` for the positive operators, `1 = 1` for
|
|
1324
|
+
* `$notContains`) instead of a `LIKE` that coerces on SQLite and is refused
|
|
1325
|
+
* at query time on Postgres (SQLSTATE 42883). Absent, or answering `false`,
|
|
1326
|
+
* keeps the `LIKE` — the answer for a text column and for a caller that
|
|
1327
|
+
* cannot classify one (`non-text-column.ts` carries the argument).
|
|
1328
|
+
*/
|
|
1329
|
+
nonTextColumn?: (field: string) => boolean;
|
|
1330
|
+
/**
|
|
1331
|
+
* [#15684] The SQL dialect that will EXECUTE this scope — `'sqlite'` /
|
|
1332
|
+
* `'postgres'` / `'mysql'`. Anything else, including absent, is `'unknown'`
|
|
1333
|
+
* and keeps the plain `LIKE` this compiler always emitted.
|
|
1334
|
+
*
|
|
1335
|
+
* The case-EXACT text family (#4706 Q2 = A) needs it: SQLite's `LIKE` folds
|
|
1336
|
+
* ASCII case unconditionally, so a policy written `{ name: { $contains:
|
|
1337
|
+
* 'acme' } }` ADMITTED `ACME Corp` there — rows the predicate excludes,
|
|
1338
|
+
* which on a read scope is over-reach (#3948), not a loose filter. The
|
|
1339
|
+
* per-dialect construct table lives in `text-match-sql.ts`; both of this
|
|
1340
|
+
* compiler's consumers fill this in from the driver that owns the object.
|
|
1341
|
+
*/
|
|
1342
|
+
dialect?: string;
|
|
1343
|
+
}
|
|
1344
|
+
declare function compileScopedFilterToSql(filter: FilterCondition, alias: string, options?: ReadScopeCompileOptions): {
|
|
1168
1345
|
sql: string;
|
|
1169
1346
|
params: unknown[];
|
|
1170
1347
|
};
|
|
@@ -1465,7 +1642,7 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
1465
1642
|
* the members inside were unreadable from the outside and the envelope check
|
|
1466
1643
|
* could not reject what it could not see.
|
|
1467
1644
|
*
|
|
1468
|
-
* ##
|
|
1645
|
+
* ## Three producers, one inventory (#10861, #11461)
|
|
1469
1646
|
*
|
|
1470
1647
|
* The caller's `where` is not the only thing that reaches `engine.aggregate`
|
|
1471
1648
|
* as a predicate. Since PR #10758 the compiled dataset's own definition-level
|
|
@@ -1480,6 +1657,36 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
1480
1657
|
* which driver will serve the dataset and would refuse a dataset that is
|
|
1481
1658
|
* perfectly legal on a native-SQL deployment.
|
|
1482
1659
|
*
|
|
1660
|
+
* [#11461] #10413 phase 2 then added a THIRD producer with the same reach and
|
|
1661
|
+
* none of the coverage: a compiled measure's own `filter`, lowered onto that
|
|
1662
|
+
* measure's `aggregations[].filter` entry (#10576). This view enumerated two
|
|
1663
|
+
* origins, so the third was invisible to the envelope check and the arm of
|
|
1664
|
+
* `planCrossObject` that inspects `query.measures` reads only each measure's
|
|
1665
|
+
* resolved FIELD, never its filter. Measured on the unfixed tree, one fixture,
|
|
1666
|
+
* both doors:
|
|
1667
|
+
*
|
|
1668
|
+
* ```
|
|
1669
|
+
* BEFORE execute() ACCEPTED -> aggregations: [{field:"*",method:"count",
|
|
1670
|
+
* alias:"west_count",
|
|
1671
|
+
* filter:{"account.region":"West"}}]
|
|
1672
|
+
* -> rows [{stage:"won",total_count:3,west_count:0}]
|
|
1673
|
+
* (the truthful west_count is 2; total_count
|
|
1674
|
+
* is right, so the wrong number arrived in
|
|
1675
|
+
* the same response shape as the right one)
|
|
1676
|
+
* generateSql() ACCEPTED -> COUNT(CASE WHEN account.region = $1 THEN 1 END)
|
|
1677
|
+
* over a FROM with no join in it at all
|
|
1678
|
+
* AFTER both doors REFUSED INVALID_FIELD / 400, engine never reached
|
|
1679
|
+
* ```
|
|
1680
|
+
*
|
|
1681
|
+
* The same maintainer ruling covers it — same hazard, same physical verdict,
|
|
1682
|
+
* one more producer — so it folds in HERE for the #10861 reason and not into
|
|
1683
|
+
* `dataset-compiler.ts`, which still cannot see which driver will serve the
|
|
1684
|
+
* dataset. Only the REQUESTED measures are folded: both doors' aggregation
|
|
1685
|
+
* loops read `measureFilters[m]` for `m of query.measures` and nothing else,
|
|
1686
|
+
* so a filter declared on a measure this query never asks for reaches no
|
|
1687
|
+
* engine, and refusing on it would reject a query for a member that was never
|
|
1688
|
+
* going to be evaluated.
|
|
1689
|
+
*
|
|
1483
1690
|
* Structure is discarded on purpose — a member is cross-object or it is not,
|
|
1484
1691
|
* and which branch of a disjunction it sits in cannot make
|
|
1485
1692
|
* `engine.aggregate` able to join it. PROVENANCE is not discarded, because it
|
|
@@ -1489,9 +1696,12 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
1489
1696
|
* `planCrossObject`. The value slot carries that and nothing else; it never
|
|
1490
1697
|
* reaches a driver.
|
|
1491
1698
|
*
|
|
1492
|
-
*
|
|
1493
|
-
*
|
|
1494
|
-
*
|
|
1699
|
+
* Insertion order is measure-filter, then dataset-filter, then `where`, and
|
|
1700
|
+
* last write wins on a duplicate key. Two things follow, in that order of
|
|
1701
|
+
* importance. A member named by the request too keeps the CALLER's provenance,
|
|
1702
|
+
* because if it is in the request that is the actionable place to fix it. And
|
|
1703
|
+
* every shape that was refused before #11461 keeps the exact message it had:
|
|
1704
|
+
* the new origin can only ever win a key no older producer names.
|
|
1495
1705
|
*
|
|
1496
1706
|
* Time-dimension WINDOWS are deliberately absent (they live in
|
|
1497
1707
|
* `dateRangeBounds`, not in `where`). They need no arm here: a cross-object
|
|
@@ -1513,18 +1723,20 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
1513
1723
|
* THROWS for anything outside the envelope — a cross-object MEASURE or FILTER
|
|
1514
1724
|
* (needs a real join to evaluate), a cross-object leaf in the DATASET's own
|
|
1515
1725
|
* definition-level `filter` (#10861 — same join it does not have, arriving
|
|
1516
|
-
* from the producer PR #10758 added), a
|
|
1517
|
-
*
|
|
1518
|
-
*
|
|
1726
|
+
* from the producer PR #10758 added), a cross-object leaf in ONE MEASURE's own
|
|
1727
|
+
* `filter` (#11461 — the same join again, arriving from the producer #10413
|
|
1728
|
+
* phase 2 added), a MULTI-HOP dimension (`a.b.c`), or a non-recombinable
|
|
1729
|
+
* measure (`avg`/`count_distinct`, whose sub-bucket values cannot be merged).
|
|
1730
|
+
* A loud error beats the silent mis-bucket #3654 kills.
|
|
1519
1731
|
* `generateSql()` calls this too, so the preview accepts/rejects the same set
|
|
1520
1732
|
* — and since #10759 both callers derive `filter` from the one
|
|
1521
1733
|
* {@link filterMemberView}, so that sentence is enforced by construction
|
|
1522
1734
|
* instead of restated at two call sites.
|
|
1523
1735
|
*
|
|
1524
|
-
* [#5716] All
|
|
1736
|
+
* [#5716] All six refusals below are `invalidMemberError` — `INVALID_FIELD` /
|
|
1525
1737
|
* 400, naming the member — and the four that predate #10861 keep their
|
|
1526
1738
|
* MESSAGES unchanged (they are good diagnostics, and #5923's tests read
|
|
1527
|
-
* them). Each is decided by two facts and nothing else: a member that will
|
|
1739
|
+
* them); so does #10861's own, which #11461 left untouched beside it. Each is decided by two facts and nothing else: a member that will
|
|
1528
1740
|
* reach the engine's predicate, and whether that member resolves across a
|
|
1529
1741
|
* join. Neither is an internal invariant — a cube where the member exists and
|
|
1530
1742
|
* a driver that could serve it are both perfectly ordinary, which is exactly
|
|
@@ -1533,14 +1745,15 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
1533
1745
|
* because the fix is always to change or drop ONE named member, and because
|
|
1534
1746
|
* four of them fire on `/analytics/query` where no dataset exists.
|
|
1535
1747
|
*
|
|
1536
|
-
* [#10861] The fifth
|
|
1537
|
-
* it:
|
|
1538
|
-
* here whose member no request key named — so
|
|
1539
|
-
* `param`, and says in its own words which document to
|
|
1748
|
+
* [#10861, #11461] The fifth and sixth are the exceptions that prove the rule
|
|
1749
|
+
* and are written to it: they can only fire where a dataset DOES exist, and
|
|
1750
|
+
* they are the two refusals here whose member no request key named — so each
|
|
1751
|
+
* carries `cube` and no `param`, and says in its own words which document to
|
|
1752
|
+
* go and edit, the sixth naming the MEASURE inside it as well. Both stay
|
|
1540
1753
|
* `INVALID_FIELD` rather than becoming `DATASET_INVALID` because the verdict
|
|
1541
|
-
* is the same physical one as
|
|
1754
|
+
* is the same physical one as their neighbours — this engine cannot join this
|
|
1542
1755
|
* member — and splitting the code by PROVENANCE would make a caller branch on
|
|
1543
|
-
*
|
|
1756
|
+
* three wire shapes for one capability limit.
|
|
1544
1757
|
*
|
|
1545
1758
|
* Detection is on RESOLVED field names, so a dotted dimension the cube
|
|
1546
1759
|
* flattens to a real column is treated as base, not cross-object.
|
|
@@ -1558,6 +1771,23 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
1558
1771
|
* are simply absent from the map (⇒ RESTRICTED bucket downstream).
|
|
1559
1772
|
*/
|
|
1560
1773
|
private resolveFkAttr;
|
|
1774
|
+
/**
|
|
1775
|
+
* A measure's aggregate, restricted to the rows its own `filter` admits
|
|
1776
|
+
* (#10413 phase 2) — the same six functions `generateSql`'s unconditional
|
|
1777
|
+
* branch renders, wrapped in a `CASE WHEN`.
|
|
1778
|
+
*
|
|
1779
|
+
* Spelled `CASE WHEN` rather than SQL-standard `FILTER (WHERE …)`, mirroring
|
|
1780
|
+
* `NativeSQLStrategy.CONDITIONAL_AGGREGATE_SQL`: this string is DOCUMENTATION
|
|
1781
|
+
* of an execution that really goes through `engine.aggregate`'s per-driver
|
|
1782
|
+
* `aggregations[].filter` lowering (#10576), not a statement this class runs
|
|
1783
|
+
* itself, so there is no reason to pick a dialect-restricted spelling over
|
|
1784
|
+
* the portable one the SQL-executing sibling already settled on.
|
|
1785
|
+
*
|
|
1786
|
+
* `count` over `*` counts a constant (`COUNT(CASE WHEN p THEN 1 END)`, since
|
|
1787
|
+
* `COUNT(CASE WHEN p THEN * END)` is not valid SQL); over a real column it
|
|
1788
|
+
* counts that column's non-null values among the admitted rows.
|
|
1789
|
+
*/
|
|
1790
|
+
private conditionalAggregateSql;
|
|
1561
1791
|
/**
|
|
1562
1792
|
* Render one normalized filter as a display SQL predicate for `generateSql`.
|
|
1563
1793
|
*
|
|
@@ -1592,6 +1822,17 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
1592
1822
|
* 3. `<field>` — bare column on the cube's table.
|
|
1593
1823
|
*/
|
|
1594
1824
|
private lookupMember;
|
|
1825
|
+
/**
|
|
1826
|
+
* [#14079] The (object, field) a filter member binds against — the echo's
|
|
1827
|
+
* copy of `NativeSQLStrategy.resolveStorageTarget`, kept beside the
|
|
1828
|
+
* `LIKE_SQL_OPS` table for the same reason that table is a copy: this file
|
|
1829
|
+
* renders a description of the statement THAT compiler produces, and the
|
|
1830
|
+
* declared-type test both apply is keyed by object and field. A dotted
|
|
1831
|
+
* `sql` is a relationship path (ADR-0071): every segment but the last is a
|
|
1832
|
+
* hop whose join alias is the dot-to-`__` spelling the dataset compiler keys
|
|
1833
|
+
* `cube.joins` by, the last is the column.
|
|
1834
|
+
*/
|
|
1835
|
+
private resolveStorageTarget;
|
|
1595
1836
|
private resolveFieldName;
|
|
1596
1837
|
private resolveMeasureAggregation;
|
|
1597
1838
|
/**
|