@objectstack/service-analytics 17.0.0-rc.2 → 17.0.0-rc.4
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 +1539 -0
- package/dist/index.cjs +1063 -167
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +322 -13
- package/dist/index.d.ts +322 -13
- package/dist/index.js +1057 -161
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -96,7 +96,46 @@ interface RelationshipTarget {
|
|
|
96
96
|
* namespaced objects).
|
|
97
97
|
*/
|
|
98
98
|
type RelationshipResolver = (baseObject: string, relationshipName: string) => string | RelationshipTarget | undefined;
|
|
99
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Optional probes the compiler consults to reject metadata that is decidable
|
|
101
|
+
* BEFORE any query runs. Every probe is optional and every one of them is
|
|
102
|
+
* tiered "cannot answer, do not block" (the same stand-down as
|
|
103
|
+
* `isRegisteredObject` / `getObjectFieldNames` on `AnalyticsServiceConfig`):
|
|
104
|
+
* a host without a data engine compiles exactly as it did before.
|
|
105
|
+
*/
|
|
106
|
+
interface DatasetCompileOptions {
|
|
107
|
+
/**
|
|
108
|
+
* [#5115] The datasource `objectName` DECLARES (`object.datasource`), or
|
|
109
|
+
* `undefined` when nothing authoritative can answer (no data engine, unknown
|
|
110
|
+
* object).
|
|
111
|
+
*
|
|
112
|
+
* With it the compiler can settle at COMPILE time what #5033 could only
|
|
113
|
+
* report at QUERY time: a dataset whose join crosses datasources declares a
|
|
114
|
+
* statement no driver can execute, because the analytics engine lowers the
|
|
115
|
+
* whole dataset into ONE SQL statement on the base object's datasource.
|
|
116
|
+
*
|
|
117
|
+
* IMPORTANT — `'default'` is not an answer. In `ObjectQL.getDriver`'s
|
|
118
|
+
* resolution order an explicit `object.datasource` other than `'default'`
|
|
119
|
+
* wins outright (step 1); `'default'` is the schema's DEFAULT value and means
|
|
120
|
+
* only "no explicit binding", after which routing is decided by
|
|
121
|
+
* `datasourceMapping` rules, the ADR-0057 §3.6 lifecycle split, and the
|
|
122
|
+
* owning package's `defaultDatasource` — none of which are visible from here.
|
|
123
|
+
* The compiler therefore treats `'default'`/`undefined` as UNANSWERED. See
|
|
124
|
+
* {@link compileDataset}.
|
|
125
|
+
*/
|
|
126
|
+
getObjectDatasource?: (objectName: string) => string | undefined;
|
|
127
|
+
/**
|
|
128
|
+
* ADR-0062 D6 — is `objectName` federated (bound to an external datasource)?
|
|
129
|
+
*
|
|
130
|
+
* A federated participant is EXEMPT from the cross-datasource rejection:
|
|
131
|
+
* `NativeSQLStrategy.canHandle` already declines a cube whose base or joined
|
|
132
|
+
* object is external, so such a dataset is served by the ObjectQL FK-expand
|
|
133
|
+
* path (two reads, joined in memory) — which crosses datasources by
|
|
134
|
+
* construction. Rejecting it here would break a path that works today.
|
|
135
|
+
*/
|
|
136
|
+
isExternalObject?: (objectName: string) => boolean;
|
|
137
|
+
}
|
|
138
|
+
declare function compileDataset(dataset: Dataset, resolver?: RelationshipResolver, options?: DatasetCompileOptions): CompiledDataset;
|
|
100
139
|
|
|
101
140
|
/**
|
|
102
141
|
* Dimension display-label resolution (ADR-0021).
|
|
@@ -352,6 +391,24 @@ interface AnalyticsServiceConfig {
|
|
|
352
391
|
* `StrategyContext.isExternalObject`.
|
|
353
392
|
*/
|
|
354
393
|
isExternalObject?: (objectName: string) => boolean;
|
|
394
|
+
/**
|
|
395
|
+
* [#5033] The datasource `objectName` is bound to, or `undefined` when it
|
|
396
|
+
* rides the default one (or nothing authoritative can answer).
|
|
397
|
+
*
|
|
398
|
+
* It never selects a driver (that is `engine.execute`'s `object` key, which
|
|
399
|
+
* the `plugin.ts` bridge now passes). It exists so that when a dataset's SQL
|
|
400
|
+
* references a table that is NOT on the datasource its base object routed to,
|
|
401
|
+
* the failure can name the actual cause — *table X is not on datasource Y* —
|
|
402
|
+
* instead of the misleading "backing object … is unavailable" that a
|
|
403
|
+
* cross-datasource join used to produce.
|
|
404
|
+
*
|
|
405
|
+
* [#5115] The same probe now also gates COMPILATION: `registerDataset` hands
|
|
406
|
+
* it to `compileDataset`, which rejects a dataset whose join crosses
|
|
407
|
+
* datasources before any query is ever built. Absence keeps the pre-#5115
|
|
408
|
+
* behaviour exactly ("cannot answer, do not block") — the query-time
|
|
409
|
+
* diagnostic above stays as the backstop.
|
|
410
|
+
*/
|
|
411
|
+
getObjectDatasource?: (objectName: string) => string | undefined;
|
|
355
412
|
/**
|
|
356
413
|
* [#3867] Is `name` a registered object in this kernel's schema registry?
|
|
357
414
|
*
|
|
@@ -383,6 +440,18 @@ interface AnalyticsServiceConfig {
|
|
|
383
440
|
* the same mistake with a `400 INVALID_FIELD` naming the field (#4315/#4254);
|
|
384
441
|
* this hook is what lets the ANALYTICS route give the same answer.
|
|
385
442
|
*
|
|
443
|
+
* [#5520] The same probe now answers for DIMENSIONS too
|
|
444
|
+
* ({@link AnalyticsService.assertDimensionFields}). #4437 gated only the
|
|
445
|
+
* measure half, so the identical typo one key over — `dimensions:
|
|
446
|
+
* ['bogus_dim']` — still reached the driver as a `GROUP BY` column and came
|
|
447
|
+
* back as the same 500. One probe, one answer, both member kinds.
|
|
448
|
+
*
|
|
449
|
+
* [#5669] …and for the `where` members ({@link AnalyticsService.assertWhereFields}),
|
|
450
|
+
* the third and last request key that carries a field name. `where:
|
|
451
|
+
* {bogus_col: 'x'}` compiled straight into `WHERE bogus_col = $1` for exactly
|
|
452
|
+
* as long as #4437 and #5520 had each closed only their own key. One probe now
|
|
453
|
+
* answers for all three.
|
|
454
|
+
*
|
|
386
455
|
* Same tiering as {@link isRegisteredObject}: absence means "skip the check"
|
|
387
456
|
* (registry-less hosts, engine doubles, external datasources whose columns
|
|
388
457
|
* are not mirrored locally). The production bridge in `plugin.ts` wires it
|
|
@@ -473,6 +542,13 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
473
542
|
private readonly isRegisteredObject?;
|
|
474
543
|
/** [#4437] Field-name probe gating measure source-field resolution. */
|
|
475
544
|
private readonly getObjectFieldNames?;
|
|
545
|
+
/**
|
|
546
|
+
* [#5033] Datasource probe for the missing-source triage — and, since #5115,
|
|
547
|
+
* for the compile-time cross-datasource join gate in `compileDataset`.
|
|
548
|
+
*/
|
|
549
|
+
private readonly getObjectDatasource?;
|
|
550
|
+
/** ADR-0062 D6 — federated-object probe (strategy routing + #5115's gate). */
|
|
551
|
+
private readonly isExternalObject?;
|
|
476
552
|
/** [#3867] One-shot flag for the {@link assertInferableCube} stand-down warning. */
|
|
477
553
|
private warnedNoObjectRegistry;
|
|
478
554
|
readonly cubeRegistry: CubeRegistry;
|
|
@@ -547,6 +623,19 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
547
623
|
* `cube.measures` (e.g. `amount_sum`, `amount_avg` emitted by dashboard
|
|
548
624
|
* widget translators), inject suffix-inferred Metric entries so the
|
|
549
625
|
* strategies pick the right aggregation function and field.
|
|
626
|
+
*
|
|
627
|
+
* It is also where the three SOURCE-FIELD gates run, on every path out of this
|
|
628
|
+
* method and always BEFORE the (possibly augmented) cube is registered:
|
|
629
|
+
* {@link assertMeasureFields} (#4437), {@link assertDimensionFields} (#5520)
|
|
630
|
+
* and {@link assertWhereFields} (#5669) — one per request key that can carry a
|
|
631
|
+
* field name. All three answer the same question — does the object actually
|
|
632
|
+
* have the column this member resolves to — and all three must answer it here,
|
|
633
|
+
* because from the strategy onwards the answer is the driver's `no such
|
|
634
|
+
* column`.
|
|
635
|
+
*
|
|
636
|
+
* They run in request-key order (measures → dimensions/timeDimensions →
|
|
637
|
+
* where), so a query that gets several wrong is answered about one at a time,
|
|
638
|
+
* naming a real mistake either way.
|
|
550
639
|
*/
|
|
551
640
|
private ensureCube;
|
|
552
641
|
/**
|
|
@@ -578,6 +667,132 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
578
667
|
* queries that used to work.
|
|
579
668
|
*/
|
|
580
669
|
private assertMeasureFields;
|
|
670
|
+
/**
|
|
671
|
+
* [#5520] Reject a DIMENSION whose source field the backing object does not
|
|
672
|
+
* have, BEFORE the strategy compiles it into `GROUP BY`.
|
|
673
|
+
*
|
|
674
|
+
* The symmetric half of {@link assertMeasureFields}. #4437 closed the measure
|
|
675
|
+
* side and stopped there, so the identical mistake one request key over still
|
|
676
|
+
* reached the driver:
|
|
677
|
+
*
|
|
678
|
+
* ```
|
|
679
|
+
* POST /analytics/query {"cube":"crm_account","measures":["account_count"],"dimensions":["bogus_dim"]}
|
|
680
|
+
* → 500 {"code":"SQLITE_ERROR","message":"Internal server error"}
|
|
681
|
+
*
|
|
682
|
+
* POST /analytics/dataset/query {"selection":{"dimensions":["bogus_dim"],…}}
|
|
683
|
+
* → 500 {"code":"ANALYTICS_QUERY_FAILED",
|
|
684
|
+
* "error":"SELECT bogus_dim AS \"bogus_dim\", … GROUP BY bogus_dim - no such column: bogus_dim"}
|
|
685
|
+
* ```
|
|
686
|
+
*
|
|
687
|
+
* A driver error class as the caller's `error.code` is the ADR-0112 violation
|
|
688
|
+
* #4437 named, and the dataset face additionally echoed the generated
|
|
689
|
+
* statement — physical table and column names — back to the caller. The
|
|
690
|
+
* envelope here is deliberately the SAME as the measure gate's
|
|
691
|
+
* (`INVALID_FIELD`/400 + `field`/`object`/`param`), because "the query names a
|
|
692
|
+
* field the object does not have" is ONE mistake and must have one wire shape
|
|
693
|
+
* whichever member kind carried it.
|
|
694
|
+
*
|
|
695
|
+
* What it checks, and what it deliberately does not:
|
|
696
|
+
*
|
|
697
|
+
* - **Both dimension keys.** `query.dimensions` and `query.timeDimensions`
|
|
698
|
+
* land in the same `cube.dimensions` bag, are resolved by the same
|
|
699
|
+
* `lookupMember`, and produced the same 500 (a bogus time dimension became
|
|
700
|
+
* `date_trunc('month', bogus_at)`); `param` reports which key carried it.
|
|
701
|
+
* - **An UNDECLARED but real field stays legal.** `dimensions: ['phone']` on a
|
|
702
|
+
* cube that never declared `phone` groups by `phone` today — the dimension
|
|
703
|
+
* twin of measure auto-inference, and an established contract. So the
|
|
704
|
+
* question asked is "does the OBJECT have this field", never "did the cube
|
|
705
|
+
* declare this dimension". An undeclared member is checked against the
|
|
706
|
+
* object under the name the strategies would use as the column (their own
|
|
707
|
+
* `resolveDimensionSql`/`resolveFieldName` fallback: the member itself).
|
|
708
|
+
* - Only when the cube's `sql` is a bare OBJECT NAME, only when
|
|
709
|
+
* {@link AnalyticsServiceConfig.getObjectFieldNames} answers, and only for
|
|
710
|
+
* sources that are BARE COLUMNS — same three stand-downs as the measure
|
|
711
|
+
* gate, for the same reasons (no field list to check against; nothing
|
|
712
|
+
* authoritative to consult; a dotted reference resolves through a join whose
|
|
713
|
+
* target this gate cannot see, so it belongs to the join allowlist).
|
|
714
|
+
* - `id` / `created_at` / `updated_at` are admitted unconditionally, matching
|
|
715
|
+
* the data path's `resolveQueryFields`.
|
|
716
|
+
*
|
|
717
|
+
* Runs after the measure gate and before the `where` gate on each `ensureCube`
|
|
718
|
+
* path, so a query that gets several wrong is answered about its measure
|
|
719
|
+
* first — one rejection at a time, naming a real mistake either way.
|
|
720
|
+
*/
|
|
721
|
+
private assertDimensionFields;
|
|
722
|
+
/**
|
|
723
|
+
* [#5669] Reject a `where` member whose source field the backing object does
|
|
724
|
+
* not have, BEFORE the strategy compiles it into `WHERE`.
|
|
725
|
+
*
|
|
726
|
+
* The third and last param of one defect. #4437 gated `measures`, #5520 gated
|
|
727
|
+
* `dimensions`/`timeDimensions`, and the filter face — the request key that
|
|
728
|
+
* most often carries a hand-typed field name — had no gate at all:
|
|
729
|
+
*
|
|
730
|
+
* ```
|
|
731
|
+
* POST /analytics/query {"cube":"crm_account","measures":["count"],"where":{"bogus_col":"x"}}
|
|
732
|
+
* → SELECT COUNT(*) AS "count" FROM "crm_account" WHERE bogus_col = $1
|
|
733
|
+
* → 500 {"code":"SQLITE_ERROR","message":"Internal server error"}
|
|
734
|
+
* ```
|
|
735
|
+
*
|
|
736
|
+
* Same envelope as its two siblings (`INVALID_FIELD`/400 + `field`/`object`/
|
|
737
|
+
* `param`), because "the query names a field the object does not have" is ONE
|
|
738
|
+
* mistake whichever request key carried it, and the DATA route has answered it
|
|
739
|
+
* that way since #4315/#4254 (`resolveQueryFields`).
|
|
740
|
+
*
|
|
741
|
+
* # Where the field names come from: the SQL producer's own reader
|
|
742
|
+
*
|
|
743
|
+
* The members are collected through `normalizeAnalyticsFilterTree` +
|
|
744
|
+
* `collectFilterLeaves` — the SAME pair both strategies call to build the
|
|
745
|
+
* predicate. This is deliberate and is the whole reason this gate is not a
|
|
746
|
+
* second filter-tree walker: a hand-rolled walk would have to re-derive
|
|
747
|
+
* `$and`/`$or`/`$not` recursion, `$`-prefixed operator keys, `$between`
|
|
748
|
+
* lowering, the nested-relation dot flattening (`{owner: {region: 'NA'}}` →
|
|
749
|
+
* member `owner.region`) and the #5334 array lowering, and every divergence
|
|
750
|
+
* would show up as "the field the gate saw" not being "the column that reached
|
|
751
|
+
* SQL" — in either direction (a phantom rejection, or a hole).
|
|
752
|
+
* `collectFilterLeaves` discards structure, which is exactly right here:
|
|
753
|
+
* whether a predicate sits under an `$or` changes nothing about whether its
|
|
754
|
+
* column exists. (Its doc's warning — never rebuild a predicate from this list
|
|
755
|
+
* — does not apply; this gate builds nothing.)
|
|
756
|
+
*
|
|
757
|
+
* # Three stand-downs at query level, plus the per-member ones
|
|
758
|
+
*
|
|
759
|
+
* - No {@link AnalyticsServiceConfig.getObjectFieldNames}, cube `sql` that is
|
|
760
|
+
* not a bare object name, or a probe that cannot answer for the object — the
|
|
761
|
+
* same three tiers as the measure and dimension gates, for the same reasons.
|
|
762
|
+
* - A `where` the normalizer REFUSES (an unknown operator, a non-array
|
|
763
|
+
* `$and`, an unlowerable filter array) is not judged here: this gate stands
|
|
764
|
+
* down and lets the refusal happen where it already does. Those inputs
|
|
765
|
+
* already answer `INVALID_FILTER`/400 from the strategy (#5352/#5367's
|
|
766
|
+
* geography, not this gate's), and pulling them forward into `ensureCube`
|
|
767
|
+
* would newly refuse them on the draft-preview path too, whose
|
|
768
|
+
* `matchesWhere` never consults the normalizer at all. A field gate that
|
|
769
|
+
* cannot read the tree has nothing to say about it.
|
|
770
|
+
* - Per member, {@link resolveMemberSource} stands down on an expression `sql`
|
|
771
|
+
* and on a dotted relation traversal — for the dimension gate's reasons.
|
|
772
|
+
*
|
|
773
|
+
* # Array `where` IS gated, and #5353's fix did not change that
|
|
774
|
+
*
|
|
775
|
+
* Since #5334 an array `where` is lowered by `normalizeAnalyticsFilterTree`
|
|
776
|
+
* and compiles to the identical predicate — a measured fact,
|
|
777
|
+
* `where: [['bogus_col','=','x']]` and `where: {bogus_col: 'x'}` both produce
|
|
778
|
+
* `WHERE bogus_col = $1` and hand `executeAggregate` the same
|
|
779
|
+
* `{bogus_col: 'x'}`. Gating one spelling and not the other would answer one
|
|
780
|
+
* mistake two ways, which is the split this whole gate family exists to close.
|
|
781
|
+
*
|
|
782
|
+
* `inferCubeFromQuery` used to skip an array `where` when minting the ad-hoc
|
|
783
|
+
* cube's `dimensions` — a separate question (the cube's dimension VOCABULARY,
|
|
784
|
+
* not which columns reach the driver), fixed by #5353 by lowering before
|
|
785
|
+
* reading keys. Because this gate reads filter LEAVES rather than
|
|
786
|
+
* `cube.dimensions`, that fix could not change its verdicts, and measurement
|
|
787
|
+
* confirms it did not: the array where's keys now reach `cube.dimensions`, so
|
|
788
|
+
* {@link resolveMemberSource} takes the DECLARED-dimension branch for those
|
|
789
|
+
* members instead of the undeclared-bare-column one — and both branches yield
|
|
790
|
+
* the same `source` for the same member, since the minted dimension's `sql` IS
|
|
791
|
+
* the member name. What did change is the rejection's suggestion list, in the
|
|
792
|
+
* direction that closes the split: `Valid filter members:` now reads the same
|
|
793
|
+
* for both spellings of one filter.
|
|
794
|
+
*/
|
|
795
|
+
private assertWhereFields;
|
|
581
796
|
/**
|
|
582
797
|
* [#3867] Gate on the cube auto-inference path: a name with no registered
|
|
583
798
|
* Cube may only be inferred into one if it is a registered object.
|
|
@@ -833,6 +1048,10 @@ declare class DatasetExecutor {
|
|
|
833
1048
|
* Left-merge `extra` rows onto `base` rows by their dimension-key tuple,
|
|
834
1049
|
* copying the listed value columns. Rows in `extra` with no base match are
|
|
835
1050
|
* appended (outer-ish merge so comparison-only buckets still surface).
|
|
1051
|
+
*
|
|
1052
|
+
* Rows are matched by {@link dimensionKeyOf} — read its notes before changing
|
|
1053
|
+
* how the key is built. Both the ambiguity it removes and the type coercion it
|
|
1054
|
+
* keeps are load-bearing, and both fail silently when got wrong.
|
|
836
1055
|
*/
|
|
837
1056
|
declare function mergeByDimensions(base: Record<string, unknown>[], extra: Record<string, unknown>[], dimensions: string[], valueColumns: string[]): Record<string, unknown>[];
|
|
838
1057
|
|
|
@@ -934,9 +1153,17 @@ declare class NativeSQLStrategy implements AnalyticsStrategy {
|
|
|
934
1153
|
* driver-backed `coerceTemporalFilterValue` hook (single source of truth for
|
|
935
1154
|
* the date/datetime storage convention — see StrategyContext); when the hook
|
|
936
1155
|
* is absent, or returns the value unchanged (the field is not a temporal
|
|
937
|
-
* column, or the dialect stores it as a native timestamp), falls back to
|
|
938
|
-
*
|
|
939
|
-
*
|
|
1156
|
+
* column, or the dialect stores it as a native timestamp), falls back to
|
|
1157
|
+
* {@link toSqlBindValue} so an unbindable JS type still reaches the driver as
|
|
1158
|
+
* something it can bind.
|
|
1159
|
+
*
|
|
1160
|
+
* [#5526] `value` is `unknown`, not `string`, because a leaf now carries the
|
|
1161
|
+
* author's comparand at its own type. Both halves of this method were already
|
|
1162
|
+
* `unknown`-typed for it: the hook's contract is
|
|
1163
|
+
* `coerceTemporalFilterValue(object, field, value: unknown)` and the fallback
|
|
1164
|
+
* converts only what a driver cannot bind. What CHANGED is that a string is no
|
|
1165
|
+
* longer re-typed on the way out — the fallback used to be
|
|
1166
|
+
* `coerceFilterValueForSql`, which read `'007'` as the integer `7`.
|
|
940
1167
|
*/
|
|
941
1168
|
private coerceTemporal;
|
|
942
1169
|
/**
|
|
@@ -965,6 +1192,25 @@ declare class NativeSQLStrategy implements AnalyticsStrategy {
|
|
|
965
1192
|
* does bind tighter than `OR`, so `a AND b OR c` happens to be right, but
|
|
966
1193
|
* being right by construction is what keeps a future edit from making it
|
|
967
1194
|
* wrong.
|
|
1195
|
+
*
|
|
1196
|
+
* # `null` is the constant TRUE, and TRUE absorbs a disjunction (#5325)
|
|
1197
|
+
*
|
|
1198
|
+
* A `null` return means "constrains nothing", which is the boolean TRUE — the
|
|
1199
|
+
* AND identity, so it drops out of an `and`, but the OR ABSORBER, so one TRUE
|
|
1200
|
+
* disjunct makes the whole `or` TRUE. Filtering it out of an `or` narrowed the
|
|
1201
|
+
* query to the surviving branches. `NOT TRUE ≡ FALSE`, so a negation whose
|
|
1202
|
+
* operand constrains nothing compiles to the FALSE constant rather than
|
|
1203
|
+
* disappearing (which added no `WHERE` and charted every row).
|
|
1204
|
+
*
|
|
1205
|
+
* # The invariant that keeps `params` aligned
|
|
1206
|
+
*
|
|
1207
|
+
* **A call that returns `null` leaves `params` exactly as it found it.** It
|
|
1208
|
+
* has to: a value bound with no `$n` to consume it shifts every later
|
|
1209
|
+
* placeholder onto the wrong value, and a filter that binds the WRONG comparand
|
|
1210
|
+
* is worse than one that is merely too wide (#5297). Leaves decide emptiness
|
|
1211
|
+
* before they bind, and the absorbing `or` — the one place a clause that HAS
|
|
1212
|
+
* bound is discarded — truncates back to the length it started at, so the
|
|
1213
|
+
* invariant holds inductively for every node kind.
|
|
968
1214
|
*/
|
|
969
1215
|
private compileFilterNode;
|
|
970
1216
|
private buildFilterClause;
|
|
@@ -1035,6 +1281,17 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
1035
1281
|
* cannot be merged). A loud error beats the silent mis-bucket #3654 kills.
|
|
1036
1282
|
* `generateSql()` calls this too, so the preview accepts/rejects the same set.
|
|
1037
1283
|
*
|
|
1284
|
+
* [#5716] All four refusals below are `invalidMemberError` — `INVALID_FIELD` /
|
|
1285
|
+
* 400, naming the member — and the MESSAGES are unchanged (they are good
|
|
1286
|
+
* diagnostics, and #5923's tests read them). Each is decided by two caller-side
|
|
1287
|
+
* facts and nothing else: a member the query named, and whether that member
|
|
1288
|
+
* resolves across a join. Neither is an internal invariant — a cube where the
|
|
1289
|
+
* member exists and a driver that could serve it are both perfectly ordinary,
|
|
1290
|
+
* which is exactly what the "run this on a native-SQL driver" half of each
|
|
1291
|
+
* message says. They are member-level rather than dataset-level (hence not
|
|
1292
|
+
* `datasetInvalidError`) because the fix is always to change or drop ONE named
|
|
1293
|
+
* member, and because they fire on `/analytics/query` where no dataset exists.
|
|
1294
|
+
*
|
|
1038
1295
|
* Detection is on RESOLVED field names, so a dotted dimension the cube
|
|
1039
1296
|
* flattens to a real column is treated as base, not cross-object.
|
|
1040
1297
|
*/
|
|
@@ -1055,10 +1312,21 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
1055
1312
|
* Render one normalized filter as a display SQL predicate for `generateSql`.
|
|
1056
1313
|
*
|
|
1057
1314
|
* Mirrors `NativeSQLStrategy.buildFilterClause`'s operator vocabulary so the
|
|
1058
|
-
* two previews read alike, but binds
|
|
1059
|
-
* the
|
|
1060
|
-
*
|
|
1061
|
-
*
|
|
1315
|
+
* two previews read alike, but binds the comparand VERBATIM: the value shown is
|
|
1316
|
+
* the one THIS path actually hands the engine (a real boolean, not SQL's 1/0).
|
|
1317
|
+
*
|
|
1318
|
+
* [#5526] "Verbatim" is now literal. This used to bind through
|
|
1319
|
+
* `coerceFilterValueForObjectQL`, which decoded the string a `string[]` leaf
|
|
1320
|
+
* carried back into a type — so an echo could show `7` for a filter the author
|
|
1321
|
+
* wrote as `'007'`. A leaf carries the author's value at its own type, so the
|
|
1322
|
+
* echo needs no conversion at all to stay honest about execution. The LIKE
|
|
1323
|
+
* family is still the one exception, for the reason `filter.zod.ts` gives: its
|
|
1324
|
+
* comparand is declared a `string`, and what binds is the PATTERN.
|
|
1325
|
+
*
|
|
1326
|
+
* `null` means "this leaf carries no predicate" — a value-less scalar leaf,
|
|
1327
|
+
* which `execute()` and `NativeSQLStrategy` drop too. It does NOT mean "I could
|
|
1328
|
+
* not render that operator": #5333 was exactly that conflation, and an
|
|
1329
|
+
* unrenderable operator now THROWS (see the exit below).
|
|
1062
1330
|
*/
|
|
1063
1331
|
private buildFilterClauseSql;
|
|
1064
1332
|
/**
|
|
@@ -1107,12 +1375,29 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
1107
1375
|
* path hands them over rather than lowering them.
|
|
1108
1376
|
*/
|
|
1109
1377
|
private applyFilterNode;
|
|
1110
|
-
/**
|
|
1378
|
+
/**
|
|
1379
|
+
* A node as a standalone `FilterCondition` the engine can consume.
|
|
1380
|
+
*
|
|
1381
|
+
* `null` = no constraint, which is the boolean TRUE — the AND identity but the
|
|
1382
|
+
* OR ABSORBER, so a `null` branch makes the whole disjunction unconstrained
|
|
1383
|
+
* instead of collapsing it to its surviving branches (#5325). FALSE is handed
|
|
1384
|
+
* to the engine as `{$not: {}}`, the spelling `driver-sql`, `formula` and
|
|
1385
|
+
* `driver-memory`'s matcher all already pin as the zero-row filter (#5134) —
|
|
1386
|
+
* this strategy invents no second one.
|
|
1387
|
+
*/
|
|
1111
1388
|
private filterNodeToCondition;
|
|
1112
1389
|
/**
|
|
1113
1390
|
* Render a normalized filter node as the display SQL `/analytics/sql`
|
|
1114
1391
|
* echoes. Values still bind as `$n` placeholders — the echo travels to the
|
|
1115
1392
|
* browser, so a comparand is never inlined.
|
|
1393
|
+
*
|
|
1394
|
+
* The boolean identities render too (#5325). This string exists to REPRODUCE
|
|
1395
|
+
* execution: a `{$not: {}}` filter that runs as zero rows but echoes SQL with
|
|
1396
|
+
* no `WHERE` hands whoever is debugging "why is this chart empty" a statement
|
|
1397
|
+
* that returns the whole table. Same reason the absorbed `$or` branch and the
|
|
1398
|
+
* `params` truncation below match {@link NativeSQLStrategy.compileFilterNode}
|
|
1399
|
+
* exactly — including the invariant that a `null` return leaves `params`
|
|
1400
|
+
* untouched, so no comparand is left with no placeholder to consume it.
|
|
1116
1401
|
*/
|
|
1117
1402
|
private renderFilterNodeSql;
|
|
1118
1403
|
private mergeFilterOperand;
|
|
@@ -1135,9 +1420,16 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
1135
1420
|
* performs the same half-open translation itself because it binds into raw
|
|
1136
1421
|
* SQL, so one dashboard reads the same on every driver.
|
|
1137
1422
|
*
|
|
1138
|
-
*
|
|
1139
|
-
*
|
|
1140
|
-
*
|
|
1423
|
+
* [#5526] Bounds are forwarded at the type `dateRange` is DECLARED with —
|
|
1424
|
+
* `string` (`AnalyticsQuerySchema`'s `timeDimensions[].dateRange: string[]`) —
|
|
1425
|
+
* and nothing re-types them. They used to pass through
|
|
1426
|
+
* `coerceFilterValueForObjectQL`, whose TSDoc advertised that "an epoch-ms
|
|
1427
|
+
* bound recovers as a number"; that was a lenient CONSUMER rescuing a shape the
|
|
1428
|
+
* contract does not declare, and the same guess is what read a `'007'` filter
|
|
1429
|
+
* comparand as `7` (Prime Directive #12 — the producer or the spec is where an
|
|
1430
|
+
* epoch-ms window would have to be declared, not here). An author who wants an
|
|
1431
|
+
* instant window writes it as one; a declared `string` binds as a string. No
|
|
1432
|
+
* STORAGE coercion happens here either, deliberately: `NativeSQLStrategy` needs
|
|
1141
1433
|
* `coerceTemporal` because it binds into raw SQL and had to learn that a
|
|
1142
1434
|
* SQLite `Field.datetime` is an INTEGER epoch (#2034); this path goes through
|
|
1143
1435
|
* `engine.aggregate()`, where the driver's own CRUD filter coercion applies —
|
|
@@ -1156,6 +1448,23 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
1156
1448
|
* fallback here errs toward the narrower query instead.
|
|
1157
1449
|
*/
|
|
1158
1450
|
private dateRangeBounds;
|
|
1451
|
+
/**
|
|
1452
|
+
* One leaf as the operand the engine's `FilterCondition` expects.
|
|
1453
|
+
*
|
|
1454
|
+
* [#5526] The comparand is passed through UNCONVERTED. That is the whole of
|
|
1455
|
+
* this path's share of the fix: the engine compares against the value as
|
|
1456
|
+
* STORED, and a leaf now carries the value the author wrote, so `'007'` stays
|
|
1457
|
+
* `'007'`, `true` stays `true` and `7` stays `7` with nothing in between to
|
|
1458
|
+
* re-type them. The two `coerceFilterValueForObjectQL` calls this replaced
|
|
1459
|
+
* existed only to undo `stringifyForCube`, and undoing it required guessing.
|
|
1460
|
+
*
|
|
1461
|
+
* The four LIKE-family arms are the exception, and a contract one:
|
|
1462
|
+
* `filter.zod.ts` declares `$contains` / `$notContains` / `$startsWith` /
|
|
1463
|
+
* `$endsWith` as `z.string()`, so this PRODUCER must hand the engine a real
|
|
1464
|
+
* string — `String(…)`, the same normalisation `like-pattern.ts` applies at the
|
|
1465
|
+
* two SQL emitters and `driver-sql`'s `applyLike` applies at the driver, so one
|
|
1466
|
+
* `$contains` means one thing on every face (#5567's invariant).
|
|
1467
|
+
*/
|
|
1159
1468
|
private convertFilter;
|
|
1160
1469
|
private extractObjectName;
|
|
1161
1470
|
/**
|
|
@@ -1180,4 +1489,4 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
|
|
|
1180
1489
|
private buildFieldMeta;
|
|
1181
1490
|
}
|
|
1182
1491
|
|
|
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 };
|
|
1492
|
+
export { AnalyticsService, type AnalyticsServiceConfig, AnalyticsServicePlugin, type AnalyticsServicePluginOptions, type CompareTo, type CompiledDataset, CubeRegistry, type DatasetCompileOptions, 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 };
|