|
| 1 | +--- |
| 2 | +'@objectstack/spec': minor |
| 3 | +--- |
| 4 | + |
| 5 | +**BREAKING** — retire `CubeJoin.sql` and `CubeJoin.relationship`. A cube join declares |
| 6 | +WHICH object it reaches; the ON clause is derived from the declared relationship between |
| 7 | +the two cubes' objects and is never authored. |
| 8 | + |
| 9 | +`CubeJoin.sql` was **required** and described itself as the `ON` clause, and nothing ever |
| 10 | +read it. Both analytics strategies synthesise the join: `NativeSQLStrategy` emits |
| 11 | +`LEFT JOIN <name> <alias> ON "<parent>"."<segment>" = "<alias>"."id"` from the dotted member |
| 12 | +path alone, and `ObjectQLStrategy` resolves the join through `cube.joins?.[alias]?.name` and |
| 13 | +lowers it to a relationship traversal with no `ON` clause at all. So an authored join |
| 14 | +condition was not ignored — it was **replaced**, under a `200`, by an equality the author had |
| 15 | +not asked for, with a plausible number attached. `relationship` is the same shape one key |
| 16 | +over: it carried a `.default('many_to_one')`, nothing dispatched on the cardinality, and |
| 17 | +`one_to_many` parsed, changed no SQL and kept the many-to-one arithmetic. |
| 18 | + |
| 19 | +ADR-0049 enforce-or-remove; maintainer ruling 2026-09-18 (director batch #154 item 4, |
| 20 | +letter 2). The ruling declined the other remedy — executing the author's SQL — as a new |
| 21 | +capability whose first design question is an injection boundary, for zero authors today. A |
| 22 | +custom join condition, if a customer needs one, is a capability card with that boundary |
| 23 | +decided first. |
| 24 | + |
| 25 | +## FROM → TO |
| 26 | + |
| 27 | +| you wrote (17.4 and earlier) | write instead | |
| 28 | +| --- | --- | |
| 29 | +| `joins: { account: { name: 'crm_account', relationship: 'many_to_one', sql: '${orders}.account = ${crm_account}.id' } }` | `joins: { account: { name: 'crm_account' } }` — delete both keys | |
| 30 | +| `joins: { a: { name: 'b', relationship: 'one_to_many' } }` | `joins: { a: { name: 'b' } }` — the cardinality was never read; declare it on the object's own relationship field | |
| 31 | +| `joins: { a: { name: 'b', on: '…' } }` | `joins: { a: { name: 'b' } }` — `on` was the curated alias for `sql` and is retired with it | |
| 32 | + |
| 33 | +**The one-line fix:** delete `sql` and `relationship` from every `joins` entry; keep `name`. |
| 34 | + |
| 35 | +Nothing regresses by deleting them: neither key ever reached a query. What decides the join |
| 36 | +is `name` (the joined object, which is also what the per-object RLS/tenant read scope is |
| 37 | +computed for) and the declared relationship the runtime derives the equality from. |
| 38 | + |
| 39 | +## The retirement kit |
| 40 | + |
| 41 | +- **Strict deletion plus a `guidance` prescription, not a `retiredKey()` tombstone.** Every |
| 42 | + cube shape is a `strictObject`, so the key leaves the walked shape entirely and the |
| 43 | + refusal carries the upgrade: writing `sql`, `relationship` or `on` on a join is an |
| 44 | + `unrecognized_keys` rejection whose message names the key and states that the `ON` clause |
| 45 | + is DERIVED from the declared relationship between the two cubes' objects. Same route |
| 46 | + `MetricSchema.filters` took one shape over in this same file. |
| 47 | +- **`on` is no longer an alias.** It pointed at `sql`; an alias naming a key the shape |
| 48 | + cannot accept answers an author with a second rejection, so it became a `guidance` entry |
| 49 | + of its own and the rename suggestion is gone. Pinned in both directions. |
| 50 | +- **ADR-0087: a D3 SEMANTIC entry**, `cube-join-sql-and-relationship-retired`, plus the two |
| 51 | + exact-key registrations `data/CubeJoin:sql` and `data/CubeJoin:relationship` in |
| 52 | + `RETIRED_KEYS_BY_MAJOR[18]`. Not a D2 conversion: the ruling's census found zero authored |
| 53 | + cube joins outside this repository, so there is no consumer source to rewrite, and a |
| 54 | + mechanical strip would delete the key without recording which cube lost it — an author who |
| 55 | + wrote a non-FK `sql` wanted a join the runtime does not perform, and that want needs a |
| 56 | + decision rather than a rewrite. The `list-view-navigation-view-retired` entry took the same |
| 57 | + route for the same reason. |
| 58 | +- **No `os migrate meta` sentence** in either prescription: that sentence is owed only where |
| 59 | + an ADR-0087 conversion covers the surface, and none does. |
| 60 | +- **The liveness ledger rows went WITH the keys** (`liveness/analytics_cube.json`), which is |
| 61 | + the strict-deletion route's disposition — the opposite of the tombstone route, which keeps |
| 62 | + the row because `retiredKey()` keeps the key in the walked shape. `analytics_cube` drops |
| 63 | + from 12 `dead` to 10. |
| 64 | +- **The one in-repo producer is fixed in the same diff.** `examples/app-showcase`'s |
| 65 | + `DeliveryCube` authored both keys, including an `ON` clause the runtime was replacing; |
| 66 | + `dataset-compiler.ts` minted them as two constants no reader consulted. |
| 67 | + |
| 68 | +Clause-②: yes (narrowing) |
| 69 | + |
| 70 | +<!-- adr-0087: registered cube-join-sql-and-relationship-retired --> |
0 commit comments