@objectstack/service-analytics 17.0.0-rc.0 → 17.0.0-rc.2
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 +3093 -0
- package/dist/index.cjs +615 -172
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +258 -15
- package/dist/index.d.ts +258 -15
- package/dist/index.js +606 -164
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { IAnalyticsService, Logger,
|
|
2
|
-
export { AnalyticsStrategy, DatasetSelection,
|
|
1
|
+
import { IAnalyticsService, Logger, AnalyticsDriverCapabilities, AnalyticsStrategy, AnalyticsQuery, AnalyticsResult, DatasetSelection, CubeMeta, DatasetCompareTo, StrategyContext } from '@objectstack/spec/contracts';
|
|
2
|
+
export { AnalyticsDriverCapabilities, AnalyticsStrategy, DatasetSelection, StrategyContext } from '@objectstack/spec/contracts';
|
|
3
3
|
import { Cube, FilterCondition } from '@objectstack/spec/data';
|
|
4
4
|
import { ExecutionContext } from '@objectstack/spec/kernel';
|
|
5
5
|
import { Dataset } from '@objectstack/spec/ui';
|
|
@@ -268,7 +268,7 @@ interface AnalyticsServiceConfig {
|
|
|
268
268
|
* Probe driver capabilities for the object that backs a cube.
|
|
269
269
|
* The service calls this function to decide which strategy can handle a query.
|
|
270
270
|
*/
|
|
271
|
-
queryCapabilities?: (cubeName: string) =>
|
|
271
|
+
queryCapabilities?: (cubeName: string) => AnalyticsDriverCapabilities;
|
|
272
272
|
/**
|
|
273
273
|
* Execute raw SQL on the driver for a given object.
|
|
274
274
|
* Required for NativeSQLStrategy.
|
|
@@ -336,6 +336,14 @@ interface AnalyticsServiceConfig {
|
|
|
336
336
|
* `StrategyContext.coerceTemporalFilterValue` for the full rationale.
|
|
337
337
|
*/
|
|
338
338
|
coerceTemporalFilterValue?: (objectName: string, fieldName: string, value: unknown) => unknown;
|
|
339
|
+
/**
|
|
340
|
+
* Normalise the COLUMN side of the same comparison to that storage form — the
|
|
341
|
+
* other half of the fix, needed because a SQLite `Field.datetime` holds both an
|
|
342
|
+
* INTEGER epoch (a `Date` write) and ISO TEXT (a REST/JSON write, a `NOW()`
|
|
343
|
+
* default) at once, so coercing only the comparand matches one of them and
|
|
344
|
+
* misses the other (#3912). See `StrategyContext.coerceTemporalFilterColumn`.
|
|
345
|
+
*/
|
|
346
|
+
coerceTemporalFilterColumn?: (objectName: string, fieldName: string, columnSql: string) => string;
|
|
339
347
|
/**
|
|
340
348
|
* ADR-0062 D6 — report whether an object is federated (external datasource).
|
|
341
349
|
* Threaded into the StrategyContext so `NativeSQLStrategy` declines external
|
|
@@ -362,6 +370,26 @@ interface AnalyticsServiceConfig {
|
|
|
362
370
|
* always wires it.
|
|
363
371
|
*/
|
|
364
372
|
isRegisteredObject?: (name: string) => boolean;
|
|
373
|
+
/**
|
|
374
|
+
* [#4437] The FIELD NAMES `objectName` declares, or `undefined` when nothing
|
|
375
|
+
* authoritative can answer.
|
|
376
|
+
*
|
|
377
|
+
* Consulted by {@link AnalyticsService.ensureCube} to validate the SOURCE
|
|
378
|
+
* FIELD a measure resolves to BEFORE any SQL is built. `inferMeasure` maps a
|
|
379
|
+
* suffix convention onto a field name (`ghost_sum` → `SUM(ghost)`) and used
|
|
380
|
+
* to accept any spelling, so a typo'd measure reached the driver as a column
|
|
381
|
+
* and came back as an opaque `500 SQLITE_ERROR` — a driver error class on the
|
|
382
|
+
* wire for a caller-shaped mistake (ADR-0112). The DATA route already refuses
|
|
383
|
+
* the same mistake with a `400 INVALID_FIELD` naming the field (#4315/#4254);
|
|
384
|
+
* this hook is what lets the ANALYTICS route give the same answer.
|
|
385
|
+
*
|
|
386
|
+
* Same tiering as {@link isRegisteredObject}: absence means "skip the check"
|
|
387
|
+
* (registry-less hosts, engine doubles, external datasources whose columns
|
|
388
|
+
* are not mirrored locally). The production bridge in `plugin.ts` wires it
|
|
389
|
+
* from the same schema registry the data path's gate reads, so "which fields
|
|
390
|
+
* exist" has ONE answer across `/data` and `/analytics`.
|
|
391
|
+
*/
|
|
392
|
+
getObjectFieldNames?: (objectName: string) => readonly string[] | undefined;
|
|
365
393
|
/**
|
|
366
394
|
* ADR-0021 — optional object-graph resolver used when compiling datasets:
|
|
367
395
|
* `(baseObject, relationshipName) => relatedObjectName | undefined`. When
|
|
@@ -369,15 +397,23 @@ interface AnalyticsServiceConfig {
|
|
|
369
397
|
*/
|
|
370
398
|
relationshipResolver?: RelationshipResolver;
|
|
371
399
|
/**
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
400
|
+
* Resolve the metadata of a dimension's or measure's SOURCE FIELD on the
|
|
401
|
+
* dataset's base object — the one seam through which display semantics that
|
|
402
|
+
* live on the field reach the result columns. `undefined` for an unknown
|
|
403
|
+
* field. Feeds three chains:
|
|
404
|
+
*
|
|
405
|
+
* - ADR-0053 currency: a monetary measure that omits an explicit `currency`
|
|
406
|
+
* falls back to the field's declared currency, then the tenant default
|
|
407
|
+
* (`ctx.currency`). Non-`currency` fields never get a code.
|
|
408
|
+
* - Percent scale (objectui#3136): a measure over a `percent` field inherits
|
|
409
|
+
* that field's storage scale via `percentScaleOf`, so a renderer scales by
|
|
410
|
+
* declared metadata instead of guessing from the value.
|
|
411
|
+
* - Date bucketing: a date vs datetime dimension drills by the right bound.
|
|
377
412
|
*/
|
|
378
|
-
|
|
413
|
+
sourceFieldMeta?: (object: string, field: string) => {
|
|
379
414
|
type?: string;
|
|
380
415
|
defaultCurrency?: string;
|
|
416
|
+
max?: number;
|
|
381
417
|
} | undefined;
|
|
382
418
|
/** Pre-defined datasets to compile + register at construction (ADR-0021). */
|
|
383
419
|
datasets?: Dataset[];
|
|
@@ -428,13 +464,15 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
428
464
|
private readonly datasetRegistry;
|
|
429
465
|
/** Optional object-graph resolver used when compiling datasets. */
|
|
430
466
|
private readonly relationshipResolver?;
|
|
431
|
-
private readonly
|
|
467
|
+
private readonly sourceFieldMeta?;
|
|
432
468
|
/** Optional dimension display-label resolver (select options / lookup names). */
|
|
433
469
|
private readonly labelResolver?;
|
|
434
470
|
/** ADR-0037 P3: pending-seed row resolver for draft data preview. */
|
|
435
471
|
private readonly draftRowsResolver?;
|
|
436
472
|
/** [#3867] Schema-registry probe gating cube auto-inference. */
|
|
437
473
|
private readonly isRegisteredObject?;
|
|
474
|
+
/** [#4437] Field-name probe gating measure source-field resolution. */
|
|
475
|
+
private readonly getObjectFieldNames?;
|
|
438
476
|
/** [#3867] One-shot flag for the {@link assertInferableCube} stand-down warning. */
|
|
439
477
|
private warnedNoObjectRegistry;
|
|
440
478
|
readonly cubeRegistry: CubeRegistry;
|
|
@@ -511,6 +549,35 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
511
549
|
* strategies pick the right aggregation function and field.
|
|
512
550
|
*/
|
|
513
551
|
private ensureCube;
|
|
552
|
+
/**
|
|
553
|
+
* [#4437] Reject a measure whose SOURCE FIELD the backing object does not
|
|
554
|
+
* have, BEFORE the strategy compiles it into SQL.
|
|
555
|
+
*
|
|
556
|
+
* `inferMeasure` maps a suffix convention onto a field name and has no way to
|
|
557
|
+
* know whether that field exists: `ghost_sum` happily became `SUM(ghost)`, the
|
|
558
|
+
* driver threw `no such column`, and the caller got
|
|
559
|
+
* `500 {"code":"SQLITE_ERROR","message":"Internal server error"}` — a driver
|
|
560
|
+
* error class on the wire, and nothing actionable, for what is a plain typo.
|
|
561
|
+
* The DATA route has refused the same mistake with a `400 INVALID_FIELD`
|
|
562
|
+
* naming the field since #4315/#4254; this is the analytics half of that
|
|
563
|
+
* answer, and it is deliberately the SAME envelope (`code`/`field`/`object`/
|
|
564
|
+
* `param`) so one mistake has one shape across both routes.
|
|
565
|
+
*
|
|
566
|
+
* What it checks, and what it deliberately does not:
|
|
567
|
+
*
|
|
568
|
+
* - Only when the cube's `sql` is a bare OBJECT NAME. An authored cube whose
|
|
569
|
+
* `sql` is a real SQL expression has no field list to check against.
|
|
570
|
+
* - Only when {@link AnalyticsServiceConfig.getObjectFieldNames} answers.
|
|
571
|
+
* Absent hook / unknown object → stand down (see the config field's doc).
|
|
572
|
+
* - Only measures whose source is a BARE COLUMN. `count(*)` has no source
|
|
573
|
+
* field, and a dotted reference (`account.industry`) resolves through a
|
|
574
|
+
* join whose target this check cannot see — both pass through untouched.
|
|
575
|
+
* - `id` / `created_at` / `updated_at` are admitted unconditionally, matching
|
|
576
|
+
* the data path's `resolveQueryFields`: they are engine-assigned rather than
|
|
577
|
+
* declared, and a gate stricter than the engine it guards would reject
|
|
578
|
+
* queries that used to work.
|
|
579
|
+
*/
|
|
580
|
+
private assertMeasureFields;
|
|
514
581
|
/**
|
|
515
582
|
* [#3867] Gate on the cube auto-inference path: a name with no registered
|
|
516
583
|
* Cube may only be inferred into one if it is a registered object.
|
|
@@ -545,7 +612,7 @@ interface AnalyticsServicePluginOptions {
|
|
|
545
612
|
* Probe driver capabilities for a given cube.
|
|
546
613
|
* When omitted, defaults to in-memory only.
|
|
547
614
|
*/
|
|
548
|
-
queryCapabilities?: (cubeName: string) =>
|
|
615
|
+
queryCapabilities?: (cubeName: string) => AnalyticsDriverCapabilities;
|
|
549
616
|
/**
|
|
550
617
|
* Execute raw SQL on a driver. Enables NativeSQLStrategy.
|
|
551
618
|
*/
|
|
@@ -617,9 +684,22 @@ interface AnalyticsServicePluginOptions {
|
|
|
617
684
|
*/
|
|
618
685
|
declare class AnalyticsServicePlugin implements Plugin {
|
|
619
686
|
name: string;
|
|
687
|
+
/**
|
|
688
|
+
* Services init() registers on every path (ADR-0116, #4131) — lets the
|
|
689
|
+
* kernel name this plugin when a consumer requires one before it inits.
|
|
690
|
+
*/
|
|
691
|
+
providesServices: string[];
|
|
620
692
|
version: string;
|
|
621
693
|
type: "standard";
|
|
622
694
|
dependencies: string[];
|
|
695
|
+
/**
|
|
696
|
+
* init() probes the `data` engine ObjectQLPlugin provides for the
|
|
697
|
+
* auto-bridge — order-if-present so the probe verdict is deterministic
|
|
698
|
+
* (ADR-0116, #4471). Soft, not hard: without an engine the plugin
|
|
699
|
+
* degrades on purpose (per-query lazy resolution / explicit
|
|
700
|
+
* `executeAggregate`).
|
|
701
|
+
*/
|
|
702
|
+
optionalDependencies: string[];
|
|
623
703
|
private service?;
|
|
624
704
|
private readonly options;
|
|
625
705
|
constructor(options?: AnalyticsServicePluginOptions);
|
|
@@ -637,6 +717,50 @@ declare function combineFilters(a?: FilterCondition, b?: FilterCondition): Filte
|
|
|
637
717
|
* Division by zero (and missing operands) yields `null` rather than Infinity/NaN.
|
|
638
718
|
*/
|
|
639
719
|
declare function evaluateDerivedMeasures(rows: Record<string, unknown>[], derived: DerivedMeasureSpec[]): Record<string, unknown>[];
|
|
720
|
+
/**
|
|
721
|
+
* Fill the EMPTY-GROUP value into every measure column the assembled grid
|
|
722
|
+
* LISTS but no query REPORTED — by aggregate kind (#4708, objectui#3136).
|
|
723
|
+
*
|
|
724
|
+
* The grid is assembled from several results: the primary query, one
|
|
725
|
+
* supplementary query per measure-scoped filter, and (for `compareTo`) a
|
|
726
|
+
* shifted pass. {@link mergeByDimensions} writes a measure's column only onto
|
|
727
|
+
* rows its source result returned, and a `GROUP BY` over a filtered row set
|
|
728
|
+
* emits NO group at all for a dimension value the filter excludes entirely.
|
|
729
|
+
* The column therefore comes back **absent**, not `0` — and absent renders as
|
|
730
|
+
* "no data for this row", which for a count is the opposite of what the row
|
|
731
|
+
* means. A derived ratio over it goes null as well ({@link computeDerived}
|
|
732
|
+
* treats a missing operand as unknowable), so the blank spreads.
|
|
733
|
+
*
|
|
734
|
+
* The bias runs the worst possible way: the rows that blank are the ones whose
|
|
735
|
+
* numerator the filter excluded — the WORST-performing rows. A `lead_source`
|
|
736
|
+
* that won nothing renders as "no data" while one that won everything renders
|
|
737
|
+
* fine.
|
|
738
|
+
*
|
|
739
|
+
* **Filled strictly by aggregate kind**, never wholesale. `count` /
|
|
740
|
+
* `count_distinct` over an excluded group is unambiguously `0` ("how many rows
|
|
741
|
+
* matched" has an exact answer when the answer is none), and `sum` over the
|
|
742
|
+
* empty set is its identity `0`. `avg` / `min` / `max` are genuinely null —
|
|
743
|
+
* there is nothing to average — and flattening those to `0` would trade this
|
|
744
|
+
* lie for the opposite one, reporting a measurement nobody made. The
|
|
745
|
+
* kind→identity mapping is `emptyGroupValueFor` in `@objectstack/spec/data`,
|
|
746
|
+
* shared with the authoring-side coherence checks so the two cannot drift.
|
|
747
|
+
*
|
|
748
|
+
* **Only rows that already exist are touched** — no group is invented. A
|
|
749
|
+
* dimension value no query reported at all has genuinely no data and stays out
|
|
750
|
+
* of the grid; this fills the cell, never the row.
|
|
751
|
+
*
|
|
752
|
+
* Deliberately NOT a `?? 0` in the widget or a `coalesce` in the measure: a
|
|
753
|
+
* consumer-side patch must be repeated by every author of every ratio widget
|
|
754
|
+
* forever, and forgetting it is silent. Only the executor knows which aggregate
|
|
755
|
+
* produced the gap, so only the executor can tell `0` from unknown.
|
|
756
|
+
*
|
|
757
|
+
* Mutates `rows` in place (they are already this pipeline's own copies) and
|
|
758
|
+
* returns them for chaining.
|
|
759
|
+
*
|
|
760
|
+
* @param columnAggregates - Grid column → the aggregate that produced it.
|
|
761
|
+
* Includes `<measure>__compare` columns, which merge through the same seam.
|
|
762
|
+
*/
|
|
763
|
+
declare function fillEmptyGroups(rows: Record<string, unknown>[], columnAggregates: Record<string, string | undefined>): Record<string, unknown>[];
|
|
640
764
|
/** Compute the comparison window for a [start,end] range. */
|
|
641
765
|
declare function shiftRange(range: [string, string], kind: CompareTo['kind']): [string, string];
|
|
642
766
|
declare class DatasetExecutor {
|
|
@@ -660,6 +784,48 @@ declare class DatasetExecutor {
|
|
|
660
784
|
*/
|
|
661
785
|
execute(compiledInput: CompiledDataset, selectionInput: DatasetSelection, context?: ExecutionContext): Promise<AnalyticsResult>;
|
|
662
786
|
private executeSelection;
|
|
787
|
+
/**
|
|
788
|
+
* Run ONE grouped pass over a set of base measures, honouring each measure's
|
|
789
|
+
* own scoped `filter`: the unfiltered measures in a single query, plus one
|
|
790
|
+
* supplementary query per filter-scoped measure, merged back by dimension key.
|
|
791
|
+
*
|
|
792
|
+
* **This is the executor's only implementation of "how a measure filter is
|
|
793
|
+
* applied", and every window goes through it** — the current period, each
|
|
794
|
+
* `totals` subset (which re-enters via `executeSelection`), and the
|
|
795
|
+
* `compareTo` window. Before #4820 the comparison window had its own,
|
|
796
|
+
* simpler answer: one shifted query over all base measures with only the
|
|
797
|
+
* base filter, so `compiled.measureFilters` was never read on that path.
|
|
798
|
+
* `won_count` counted won deals and `won_count__compare` counted every deal,
|
|
799
|
+
* under one label, in adjacent columns. Only measures carrying a filter were
|
|
800
|
+
* wrong — which is what made it survive: the unfiltered ones next to them
|
|
801
|
+
* compared correctly.
|
|
802
|
+
*
|
|
803
|
+
* The caller supplies the `selection` this pass queries under, which is how
|
|
804
|
+
* the comparison window differs at all: same measures, same dimensions, same
|
|
805
|
+
* filters — a `timeDimensions` shifted by {@link shiftRange}. Nothing else
|
|
806
|
+
* about the two passes may drift, because anything that does becomes a
|
|
807
|
+
* discrepancy between two columns the reader is invited to subtract.
|
|
808
|
+
*
|
|
809
|
+
* Cost: one extra query per filter-scoped measure when `compareTo` is set.
|
|
810
|
+
* The alternative — declaring the discrepancy in the response — is not one,
|
|
811
|
+
* since the two columns exist to be directly comparable.
|
|
812
|
+
*
|
|
813
|
+
* @param window - Ordering/window to push into the SQL. Only ever set for a
|
|
814
|
+
* selection the caller proved is a single self-sufficient query; a pass
|
|
815
|
+
* that fans out must return its whole grid for the merge.
|
|
816
|
+
*/
|
|
817
|
+
private runMeasurePass;
|
|
818
|
+
/**
|
|
819
|
+
* The selected dimensions the compiled cube types as `time`, in selection
|
|
820
|
+
* order (#3916) — the axis {@link resolveOrdering} defaults to ascending.
|
|
821
|
+
*
|
|
822
|
+
* Membership is decided by the DIMENSION's declared type, not by whether the
|
|
823
|
+
* selection happens to bucket it: a `date` dimension left ungranulated groups
|
|
824
|
+
* raw timestamps, and those want chronological order every bit as much as
|
|
825
|
+
* month buckets do. (Both sort correctly — `compareValues` compares Dates and
|
|
826
|
+
* ISO strings chronologically, and bucket keys are minted sort-stable.)
|
|
827
|
+
*/
|
|
828
|
+
private timeDimensionsOf;
|
|
663
829
|
private buildQuery;
|
|
664
830
|
private runCompare;
|
|
665
831
|
}
|
|
@@ -681,6 +847,12 @@ declare function compileScopedFilterToSql(filter: FilterCondition, alias: string
|
|
|
681
847
|
* Pushes the analytics query down to the database as a native SQL statement.
|
|
682
848
|
* This is the most efficient path and is preferred whenever the backing driver
|
|
683
849
|
* supports raw SQL execution (e.g. Postgres, MySQL, SQLite).
|
|
850
|
+
*
|
|
851
|
+
* `resolveMeasureSql` used to answer `COUNT(*)` to three different questions it
|
|
852
|
+
* could not otherwise answer — an undeclared measure, a custom-SQL-expression
|
|
853
|
+
* metric type, and an unrecognised type. All three returned a plausible number
|
|
854
|
+
* for a query that asked for something else. They now emit the expression or
|
|
855
|
+
* throw; see that method. #4157.
|
|
684
856
|
*/
|
|
685
857
|
declare class NativeSQLStrategy implements AnalyticsStrategy {
|
|
686
858
|
readonly name = "NativeSQLStrategy";
|
|
@@ -767,6 +939,34 @@ declare class NativeSQLStrategy implements AnalyticsStrategy {
|
|
|
767
939
|
* correctly.
|
|
768
940
|
*/
|
|
769
941
|
private coerceTemporal;
|
|
942
|
+
/**
|
|
943
|
+
* The column side of {@link coerceTemporal}: normalise the reference so it
|
|
944
|
+
* reads in the storage form the comparand was coerced into.
|
|
945
|
+
*
|
|
946
|
+
* A SQLite `Field.datetime` column carries an INTEGER epoch (a `Date` write)
|
|
947
|
+
* and ISO TEXT (a REST/JSON write, a `NOW()` default — including the platform's
|
|
948
|
+
* own `created_at`) at the SAME time, so coercing the value alone fixes one half
|
|
949
|
+
* and empties the other. That is #3912: a `dateRange: last_30_days` on
|
|
950
|
+
* `created_date` read 0 with 29 rows in range. Every other column and dialect
|
|
951
|
+
* gets its reference back verbatim.
|
|
952
|
+
*/
|
|
953
|
+
private temporalColumn;
|
|
954
|
+
/**
|
|
955
|
+
* Compile a normalized filter node into a boolean SQL expression, recursing
|
|
956
|
+
* through the combinators. `null` = no constraint.
|
|
957
|
+
*
|
|
958
|
+
* Leaves go through {@link buildFilterClause} exactly as they did when this
|
|
959
|
+
* was a flat loop, so the storage-form coercion and the calendar-day
|
|
960
|
+
* upper-bound rule (#3777) apply at every depth — including inside an `$or`,
|
|
961
|
+
* where a second, combinator-aware implementation would have been free to
|
|
962
|
+
* drift from the first.
|
|
963
|
+
*
|
|
964
|
+
* Parenthesisation is explicit rather than left to SQL's precedence: `AND`
|
|
965
|
+
* does bind tighter than `OR`, so `a AND b OR c` happens to be right, but
|
|
966
|
+
* being right by construction is what keeps a future edit from making it
|
|
967
|
+
* wrong.
|
|
968
|
+
*/
|
|
969
|
+
private compileFilterNode;
|
|
770
970
|
private buildFilterClause;
|
|
771
971
|
private extractObjectName;
|
|
772
972
|
private buildFieldMeta;
|
|
@@ -895,6 +1095,26 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
895
1095
|
* are handed back for the caller to AND in separately, so the engine
|
|
896
1096
|
* intersects them instead of the strategy picking a winner.
|
|
897
1097
|
*/
|
|
1098
|
+
/**
|
|
1099
|
+
* Fold a normalized filter node into the engine filter being built.
|
|
1100
|
+
*
|
|
1101
|
+
* AND-ed LEAVES merge per field through {@link mergeFilterOperand}, exactly
|
|
1102
|
+
* as the flat loop this replaced did — so a query without combinators still
|
|
1103
|
+
* produces byte-identical engine input. Anything structural (`$or`, `$not`,
|
|
1104
|
+
* a nested `$and` that cannot merge) becomes its own conjunct, which the
|
|
1105
|
+
* caller ANDs in. The engine speaks these combinators natively
|
|
1106
|
+
* (`FilterCondition` declares them and every driver compiles them), so this
|
|
1107
|
+
* path hands them over rather than lowering them.
|
|
1108
|
+
*/
|
|
1109
|
+
private applyFilterNode;
|
|
1110
|
+
/** A node as a standalone `FilterCondition` the engine can consume. */
|
|
1111
|
+
private filterNodeToCondition;
|
|
1112
|
+
/**
|
|
1113
|
+
* Render a normalized filter node as the display SQL `/analytics/sql`
|
|
1114
|
+
* echoes. Values still bind as `$n` placeholders — the echo travels to the
|
|
1115
|
+
* browser, so a comparand is never inlined.
|
|
1116
|
+
*/
|
|
1117
|
+
private renderFilterNodeSql;
|
|
898
1118
|
private mergeFilterOperand;
|
|
899
1119
|
/**
|
|
900
1120
|
* Lower `timeDimensions[].dateRange` into resolved-field bounds (#3650).
|
|
@@ -907,9 +1127,13 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
907
1127
|
* HERE on every driver — and "bucketed trend" is precisely the shape that also
|
|
908
1128
|
* carries a range ("last 12 months", "this quarter").
|
|
909
1129
|
*
|
|
910
|
-
* Bounds are inclusive on both ends —
|
|
911
|
-
* `
|
|
912
|
-
*
|
|
1130
|
+
* Bounds are inclusive on both ends — logically "from day X through day Y".
|
|
1131
|
+
* The `$lte` end is left as the bare calendar day on purpose: the driver's
|
|
1132
|
+
* filter compiler owns the calendar-day → instant translation, compiling a
|
|
1133
|
+
* bare-day `$lte` on a `datetime` column into the half-open `< nextDay`
|
|
1134
|
+
* (#3777) while a `date` column keeps the plain `<=`. `NativeSQLStrategy`
|
|
1135
|
+
* performs the same half-open translation itself because it binds into raw
|
|
1136
|
+
* SQL, so one dashboard reads the same on every driver.
|
|
913
1137
|
*
|
|
914
1138
|
* Comparands are coerced by the SAME helper the `where` path uses, so an
|
|
915
1139
|
* epoch-ms bound recovers as a number and an ISO string stays a string. No
|
|
@@ -934,7 +1158,26 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
934
1158
|
private dateRangeBounds;
|
|
935
1159
|
private convertFilter;
|
|
936
1160
|
private extractObjectName;
|
|
1161
|
+
/**
|
|
1162
|
+
* The dimensions this query PROJECTS, in the order the result carries them:
|
|
1163
|
+
* every `dimensions` entry, then every granular `timeDimensions` entry that
|
|
1164
|
+
* is not already one of them.
|
|
1165
|
+
*
|
|
1166
|
+
* `timeDimensions` is not merely a filter carrier. An entry with a
|
|
1167
|
+
* `granularity` is GROUPED BY — see the `td.granularity` sites that build
|
|
1168
|
+
* groupBy here, in `generateSql` and in the cross-object path — so its
|
|
1169
|
+
* bucket is a COLUMN of the result; an entry without one only contributes a
|
|
1170
|
+
* `dateRange` predicate and must NOT be projected.
|
|
1171
|
+
*
|
|
1172
|
+
* Grouping, row mapping and {@link buildFieldMeta} have to agree on exactly
|
|
1173
|
+
* that set. When they did not, a bucketed query returned rows carrying only
|
|
1174
|
+
* the measures and a `fields` list that never mentioned the bucket — a trend
|
|
1175
|
+
* chart got N values and no x-axis (#4033) — even though the SQL had
|
|
1176
|
+
* selected `date_trunc(…) AS "<dim>"` all along. One definition, every
|
|
1177
|
+
* consumer.
|
|
1178
|
+
*/
|
|
1179
|
+
private projectedDimensions;
|
|
937
1180
|
private buildFieldMeta;
|
|
938
1181
|
}
|
|
939
1182
|
|
|
940
|
-
export { AnalyticsService, type AnalyticsServiceConfig, AnalyticsServicePlugin, type AnalyticsServicePluginOptions, type CompareTo, type CompiledDataset, CubeRegistry, DatasetExecutor, type DerivedMeasureSpec, type DimensionLabelDeps, type FieldMetaLite, NativeSQLStrategy, ObjectQLStrategy, type OrderLabelResolver, type RelationshipResolver, type RelationshipTarget, combineFilters, compileDataset, compileScopedFilterToSql, createOrderLabelResolver, evaluateDerivedMeasures, mergeByDimensions, pickDisplayField, resolveDimensionLabels, shiftRange, withLabelFetchCache };
|
|
1183
|
+
export { AnalyticsService, type AnalyticsServiceConfig, AnalyticsServicePlugin, type AnalyticsServicePluginOptions, type CompareTo, type CompiledDataset, CubeRegistry, DatasetExecutor, type DerivedMeasureSpec, type DimensionLabelDeps, type FieldMetaLite, NativeSQLStrategy, ObjectQLStrategy, type OrderLabelResolver, type RelationshipResolver, type RelationshipTarget, combineFilters, compileDataset, compileScopedFilterToSql, createOrderLabelResolver, evaluateDerivedMeasures, fillEmptyGroups, mergeByDimensions, pickDisplayField, resolveDimensionLabels, shiftRange, withLabelFetchCache };
|