Skip to content

Commit f522e95

Browse files
fix(service-analytics): executeRawSql bridge routes by object, not to the default datasource (#5033) (#5119)
`AnalyticsServicePlugin`'s `executeRawSql` auto-bridge received the object name and dropped it: `engine.execute(knexSql, { args: params })`. `ObjectQL.execute()` selects its driver in the order `options.object` -> `getDriver(object)`, then `options.datasource`, then the default driver, so rule 1 could never fire and every dataset raw-SQL read landed on the DEFAULT datasource. Any object routed elsewhere (ADR-0057 3.6 telemetry split, an explicit `object.datasource`, a `datasourceMapping` rule) raised `no such table`, which the widget-level graceful degradation turned into a confident `0` over live rows. Measured on the showcase: `sys_audit_log` returned 49 records object-routed and `{"rows":[]}` through the dataset raw-SQL path, on one running kernel. The bridge now passes `{ args: params, object: objectName }`, matching the `executeAggregate` bridge beside it, so both dataset execution paths give one answer to "which datasource is this object in". `DataEngineLike.execute`'s options bag is spelled out instead of `Record<string, unknown>` so the load-bearing key is visible at the call site. Accepted behaviour change: a dataset whose SQL joins across datasources now runs on the base object's own datasource and fails there rather than silently reading the wrong database. It must fail as ITSELF -- reporting it as "backing object is unavailable" would keep the confident `0` alive under a new cause -- so the missing-source triage now asks WHICH relation the driver named: - the dataset's own object -> degrade to an empty result + WARN (unchanged) - a joined table whose object -> degrade (genuine absence, unchanged shape) is not registered here - a joined table whose object -> throw, naming table X, the datasource its IS registered base object lives on, where X actually is, and the remedy `AnalyticsServiceConfig` gains one optional diagnostics-only hook, `getObjectDatasource(objectName)`, wired in `plugin.ts` from the engine's schema registry. It never selects a driver. Tests: `src/__tests__/raw-sql-object-routing.test.ts` drives the real plugin wiring against an engine double that resolves its driver the way `execute()` documents -- the defect was invisible to every test that stubbed `executeRawSql` directly. Covers the issue's exact shape (raw SQL == object-routed rows, no degradation WARN), default-datasource objects unchanged, the cross-datasource loud failure and its wording, and both surviving degradation paths. Compile-time rejection of cross-datasource dataset joins is deliberately not in this PR; filed as #5115. Fixes #5033 Claude-Session: https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX Co-authored-by: Claude <noreply@anthropic.com>
1 parent 78adc2e commit f522e95

4 files changed

Lines changed: 528 additions & 4 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
"@objectstack/service-analytics": patch
3+
---
4+
5+
fix(service-analytics): the dataset raw-SQL bridge routes by object, so datasets over non-default datasources stop reading `0` (#5033)
6+
7+
`AnalyticsServicePlugin`'s `executeRawSql` auto-bridge received the object name
8+
and threw it away: `engine.execute(knexSql, { args: params })`. `ObjectQL.execute()`
9+
picks its driver in the order `options.object` → `getDriver(object)`, then
10+
`options.datasource`, then the default driver — so rule 1 could never fire and
11+
**every dataset raw-SQL read landed on the default datasource**. Any object routed
12+
elsewhere (the ADR-0057 §3.6 telemetry split for `lifecycle.class ∈ {audit,
13+
telemetry, event}`, an explicit `object.datasource`, a `datasourceMapping` rule)
14+
raised `no such table`, which the widget-level graceful degradation then turned
15+
into an empty result — a confident `0` over live rows, on a green dashboard.
16+
Measured: `sys_audit_log` returned 49 records through the object-routed read and
17+
`{"rows":[]}` through the dataset raw-SQL read, on the same running kernel.
18+
19+
The bridge now passes `{ args: params, object: objectName }`, matching the
20+
`executeAggregate` bridge beside it (`engine.aggregate(objectName, …)`), so both
21+
dataset execution paths give **one** answer to "which datasource is this object in".
22+
No configuration change is needed; misrouted dashboards start reading real data.
23+
24+
**Behaviour change worth knowing about.** A dataset whose SQL `LEFT JOIN`s (what
25+
`NativeSQLStrategy` emits for a dotted dimension such as `account.industry`) across
26+
two datasources previously ran against the default datasource and silently read the
27+
wrong database. It now runs on the base object's own datasource, where the joined
28+
table genuinely is not — and **fails loudly** instead of degrading, because the base
29+
table resolved fine and reporting it as "unavailable" would keep the confident `0`
30+
alive under a new cause. The error names the actual cause and the remedy:
31+
32+
```
33+
[Analytics] dataset "audit_by_actor" cannot be executed as one statement:
34+
table "account" is not on datasource "telemetry", which is where its base object
35+
"sys_audit_log" lives — "account" is registered on the default datasource.
36+
A dataset JOIN cannot cross datasources. Fix it by binding both objects to the
37+
same datasource, or by dropping the cross-datasource relationship from the
38+
dataset's `include`/dimensions.
39+
```
40+
41+
Graceful degradation is unchanged for genuine absence: a dataset whose own backing
42+
object (or a joined object that this kernel never registered) has no table still
43+
renders as "no data" with the existing server-side `warn`, rather than failing the
44+
widget. `AnalyticsServiceConfig` gains one optional, diagnostics-only hook —
45+
`getObjectDatasource(objectName)` — used solely to name the datasources in that
46+
message; it never selects a driver.

0 commit comments

Comments
 (0)