docs: document dbt pull primary key detection and reverse joins - #11424
Conversation
Describe the three-tier primary key ladder (dbt constraints → strict unique + not_null tests → column-name suffixes), the new pull options, and add troubleshooting for missing/incorrect keys and one-directional joins. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
Claude finished @MikeNitsenko's task in 2m 0s —— View job Docs review — approve with 1 correctness fix; 1 medium, 3 low, 5 inline commentsFull review
OverallGood change. The old row genuinely was misleading ("Marks Note that the dbt pull implementation isn't in this OSS monorepo, so I could not verify the behavioral claims (tier order, strictness rules, entity-name preference, reverse-join type) against code. The findings below are internal-consistency and dbt-semantics issues visible from the page itself. Findings
Smaller points, not worth inline threads
Not applicableSecurity, performance, and test-coverage checks don't apply — this is a single |
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
- "Every cube needs a primary_key" — a cube can end up without one - constraints snippet now includes the enforced model contract dbt requires, plus a pointer to the test-based path for projects without contracts - name the parent option instead of "the option above"/"below" - attribute pull-time behavior to "the pull", not "Cube" Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @MikeNitsenko's task in 2m 14s —— View job Re-review of
|
| Prior finding | Status |
|---|---|
"Every cube gets a primary_key" contradicted the warning at the end of the section |
Fixed — now "needs a primary_key", and "The pull looks for it in three tiers" |
| "regardless of the two options below" pointed the wrong way | Fixed — "even when both detection options are off" |
| "Only shown when the option above is on" was positional | Fixed — names Auto-detect primary keys from column names |
| The composite-key snippet had no model contract | Fixed — snippet now carries config.contract.enforced plus the data_type entries dbt requires, with a link to dbt's contract reference, and a fallback sentence for projects that don't use contracts |
| "Cube looks for it" / "Cube prefers" blurred product vs. pull-time behavior (smaller point) | Fixed — both now read "the pull" |
| Only-tier-1-yields-composite-keys stated three times (smaller point) | Fixed — the troubleshooting bullet is trimmed to "declare the key in dbt and the pull will use it verbatim" |
Those four threads are resolved. The contract link (https://docs.getdbt.com/reference/resource-configs/contract) and both claims around it — constraints need an enforced contract, an enforced contract needs data_type on every column — match dbt's documented behavior.
Remaining
Low — reverse join type is still hardcoded (dbt.mdx:153, :414). The direction wording improved, but the type didn't: line 412 says the forward relationship can be one_to_one, whose inverse is one_to_one, not one_to_many. Left the thread open with a suggestion — if the emitter always writes one_to_many regardless of the forward type, the current text is correct as-is and the thread can just be closed.
Question, not a blocker. The snippet now shows the enforced-contract path, which is right for a reader authoring constraints in dbt. Worth confirming whether Cube's tier 1 actually requires enforcement, or whether it reads constraints out of manifest.json regardless — dbt parses constraint blocks into the manifest even when it warns about the missing contract. If the latter, the current phrasing over-prescribes slightly for readers who don't want contracts (though the tier-2 fallback sentence softens that). Also still unverified: whether composite column order is preserved as written ([account_sk, valid_from]), since that affects join semantics.
Scope
As before, the dbt pull implementation isn't in this OSS monorepo, so the behavioral claims (tier order, strictness rules, entity-name preference, reverse-join type, constraint ordering) can't be checked against code — the findings above are internal-consistency and dbt-semantics issues visible from the page. Security, performance, and test-coverage checks don't apply to a single .mdx content change; docs.json needs no update (no new page); anchors and <Accordion> usage match the rest of the page. The Mintlify preview deployment is green.
Generated joins are always many_to_one from the cube owning the foreign key; one_to_many/one_to_one were never emitted for forward joins. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @MikeNitsenko's task in 45s —— View job Re-review of
|
| Location | Claim |
|---|---|
dbt.mdx:153 |
reverse join adds one_to_many back to the referencing cube |
dbt.mdx:412-414 |
forward join is always many_to_one from the FK owner |
dbt.mdx:414-416 |
reverse join emits one_to_many |
No contradiction remains, so the type is safe to name concretely in both spots rather than describing it as an inverse. I resolved my thread on line 153 — its premise (a forward type that varies) doesn't hold.
Also verified after the earlier round: the #primary-key-detection anchor still resolves from both the settings table and the troubleshooting accordion; the composite-key snippet carries config.contract.enforced with data_type on both columns and the no-contracts fallback sentence; both new accordions match the page's symptom-titled style. No docs.json change needed.
Remaining
Nothing blocking, and nothing worth a new thread. The only optional item left from the first round is the untouched Limitations section — "single-column keys unless dbt declares a composite constraint" could be a bullet there, but the new section already covers it and the page is long. Your call.
Scope
The dbt pull implementation isn't in this OSS monorepo, so I took your source reads (forward always many_to_one, reverse always one_to_many, column order carries no meaning) as given — I could only check internal consistency and dbt semantics from the page. Security, performance, and test-coverage checks don't apply to a single .mdx content change. The Mintlify preview deployment is green.
Looks good to merge.
· Branch docs/dbt-primary-key-detection ·
Verified the page against the implementation: document that join target columns are keyed regardless of the tiers, that a primary_key constraint naming an undocumented column is skipped entirely, and that foreign-key avoidance is a preference with a fallback. Order the pull options to match the settings card. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @MikeNitsenko's task in 1m 40s —— View job Re-review of
|
| Change | Assessment |
|---|---|
Join target columns are always keyed on the target cube (:177-178) |
Genuinely new information, and the "Cube can't resolve the join otherwise" justification is the right one-liner. But it interacts badly with two existing sentences — see below. |
FK avoidance softened from "avoids" to "deprioritizes … unless that would leave no candidate" (:174-176) |
Good correction. The 1:1 satellite parenthetical earns its space — it's the case a reader would otherwise file as a bug. |
Constraints are all-or-nothing when a column isn't documented (:700-703) |
Strong troubleshooting entry. "Rather than emitting a narrower, wrong grain" explains why it's skipped, which is what makes the behavior non-surprising. |
Settings table reordered to match the UI card (:147-149) |
Fine. The tests row now sits below the column-name rows, i.e. tier 3 above tier 2 — but "Outranks the column-name guess" was added to that row, which covers it, and the suffixes row stays adjacent to the parent it depends on. Matching the UI is the better tradeoff. |
Findings
Medium — dbt.mdx:177-178. "A column that another cube joins to is always marked as a key on the target cube" is unconditional, and two nearby sentences assume it isn't:
- Line 204, "a declared key is never widened or overridden by a guess" — if a cube declares
primary_key: [account_sk]and another cube joins tolegacy_id, keyinglegacy_idtoo is widening the declared key. - Line 204–205 and the "No key at all" troubleshooting bullet (
:697) — if join targets are always keyed, a cube involved in a join can't end up keyless, so that warning only ever fires for cubes with no joins.
Both readings (join-target marking as fallback vs. additive on top of a declared key) are supportable from the text, and they produce different grains. One clause naming the precedence resolves it. Inline thread posted.
Nit — dbt.mdx:703, :705. Both new bullets point at "the pull log", but the page never says where that is — the only pull output it describes is the progress toast at line 322. Line 205 leans on log output too. In a troubleshooting accordion that's the actionable half. Inline thread posted.
Thread hygiene
All six prior threads on this PR are resolved and none of their concerns resurface in this commit — nothing to reopen, nothing to re-affirm. The two threads above are new lines with new root causes, so no dedup conflict.
Consistency re-check
The join-type story from 0210d31 is untouched and still lines up (:153 reverse one_to_many, :414-418 forward always many_to_one from the FK owner). The #primary-key-detection anchor still resolves from the settings table (:147) and both troubleshooting exits (:697, :709). The composite-key snippet still carries config.contract.enforced with data_type on both columns and the no-contracts fallback. No docs.json change needed — no new page.
Scope
The dbt pull implementation isn't in this OSS monorepo, so the behavioral claims this commit adds (join-target keying, all-or-nothing constraints, FK-deprioritization fallback, what the log emits) can't be checked against code — I took your source reads as given and checked internal consistency and dbt semantics from the page. Security, performance, and test-coverage checks don't apply to a single .mdx content change. The Mintlify preview deployment is green.
Neither finding blocks merge; the medium one is worth a sentence before you do.
· Branch docs/dbt-primary-key-detection ·
Join-referenced columns are additive on top of the tiers and apply to the referenced cube only, so the FK owner is what typically ends up keyless. Replace pointers to pull log output, which isn't surfaced to users, with the compile error and the generated .yml. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @MikeNitsenko's task in 1m 24s —— View job Re-review of
|
| Prior finding | Status |
|---|---|
| "A column that another cube joins to is always marked as a key" collided with "never widened by a guess" and the keyless-cube warning | Fixed. The paragraph now says the marking is additive, lands on the referenced cube, and that the FK owner gets nothing from it — "which is the usual reason a cube in a join ends up without one". That closes the loop I couldn't close from the text: my second inference (a cube in a join can't be keyless) was wrong for the reason you gave, and the page now states it. Line 208 was reworded to match — "never overridden or widened by the guessing tiers — only a joined-to column can add to it". |
| Three bullets pointed at a "pull log" the page never located | Fixed, and better than a pointer. Since the warnings only reach server-side observability, removing them was the right call: the keyless case now names the compile error users actually hit, the ignored-constraint bullet says what to check in the dbt YAML, and the accordion closes on the generated .yml — primary_key: true on a dimension is something a reader can actually go look at. |
Both threads resolved. All eight threads on this PR are now resolved; nothing to reopen and no dedup conflict with the finding below (new lines, new root cause).
Finding
Low — dbt.mdx:208-212, the compile-error claim is unconditional. This is the one new claim I could verify against this repo, and it's almost right. JoinGraph.buildJoinEdges (packages/cubejs-schema-compiler/src/compiler/JoinGraph.ts:135-143) only reports primary key for '<cube>' is required… when the keyless cube has a multiplied measure — count, sum, avg, or number. With Add default measures on (the default) every generated cube carries a count, so "the data model won't compile" holds. With it off, no error fires and the join edge is dropped silently — the worse symptom, because nothing surfaces it. A half-clause covers it.
Also: the quoted string is a prefix. The full message ends …in order to make aggregates work properly. Searchable either way, but an … would signal truncation. Inline thread posted.
Consistency re-check
- The additive rule at
:177-182and "only a joined-to column can add to it" at:208now agree, and both agree with the keyless-cube case at:704. - The join-type story from
0210d31is untouched and still lines up (:153reverseone_to_many, forward alwaysmany_to_onefrom the FK owner). #primary-key-detectionstill resolves from:147and:715. Composite-key snippet still carriesconfig.contract.enforcedwithdata_typeon both columns plus the no-contracts fallback. Both accordions keep the page's symptom-titled style. Nodocs.jsonchange needed — no new page.- The inline code span at
:210-211wraps across a newline; Markdown collapses that to a space, so it renders as one line. Fine.
Scope
The dbt pull implementation isn't in this OSS monorepo, so its behavioral claims (additive join-target keying, all-or-nothing constraints, tier order, FK deprioritization) rest on your source reads — I checked internal consistency and dbt semantics from the page. The one claim that does touch this repo, the compile error, I verified in JoinGraph.ts; hence the finding above. Security, performance, and test-coverage checks don't apply to a single .mdx content change. The Mintlify preview deployment is green.
The finding doesn't block merge — it's a qualifier on a sentence that's correct under default settings.
· Branch docs/dbt-primary-key-detection ·
The compile error only fires when the cube has an aggregatable measure; without one the join is dropped with no error. Quote the full message. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @MikeNitsenko's task in 1m 14s —— View job Re-review of
|
| Claim | Check |
|---|---|
| Error only fires when the keyless cube has an aggregatable measure | Confirmed — JoinGraph.ts:135-143, guard is !primaryKeys[cube].length && multipliedMeasures.length > 0, and it's applied to both the from and to cube, so "a cube that participates in a join" is the right scope |
Add default measures puts a qualifying count on every cube |
Confirmed — count is in the type list at :117-122, and the settings row at :150 says the option adds one to every cube |
| Without one, the join is dropped | Confirmed — the guard return falses out of the .filter, so the edge never reaches the graph |
| Full error message | Matches JoinGraph.ts:126 verbatim |
Thread resolved. All nine threads on this PR are now resolved; the finding below is on new lines with a different root cause, so no dedup conflict.
Finding
Low — dbt.mdx:215-216, the no-measure case isn't silent, it's deferred. "There's no error" holds at compile time, but once the edge is dropped, a query touching both cubes reaches buildJoin and throws Can't find join path to join '<cube>', '<cube>' (JoinGraph.ts:196). The reader does get a message — later, and phrased as a missing join path rather than a missing key, which is precisely why it's hard to trace back to this section. Naming that string makes the bullet searchable the same way the compile error above it is.
Same thread, secondary: "aggregatable measures" is a touch wider than getMultipliedMeasures, which matches sum / avg / count / number (plus raw count(*) sql) — min and max are aggregates that don't trigger the guard. Immaterial with Add default measures on; only bites someone who turned it off and expected their max measure to surface the problem.
Consistency re-check
Nothing else moved. The additive join-target rule at :177-182 still agrees with "only a joined-to column can add to it" at :208; the join-type story from 0210d31 is untouched (:153 reverse one_to_many, forward always many_to_one from the FK owner); #primary-key-detection still resolves from the settings table and both troubleshooting exits; the composite-key snippet still carries config.contract.enforced with data_type on both columns plus the no-contracts fallback. No docs.json change needed — no new page.
Scope
The dbt pull implementation isn't in this OSS monorepo, so its behavioral claims (tier order, additive join-target keying, all-or-nothing constraints, FK deprioritization) rest on your source reads. Everything this commit touches that does land in this repo — the guard condition, the measure types, the error text — I checked in JoinGraph.ts. Security, performance, and test-coverage checks don't apply to a single .mdx content change. The Mintlify preview deployment is green.
The finding doesn't block merge; the sentence is correct as far as compile time goes.
· Branch docs/dbt-primary-key-detection ·
- name the measure types that trigger the compile error, and the runtime "Can't find join path" failure when none is present - drop the unqualified "Cube requires a key on both ends of every join" - Limitations no longer claims tests are unused; they feed key and join detection - note that warehouse enforcement is irrelevant (the pull only parses) - a dbt relationship describes one side, rather than dbt being unable to declare both Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @MikeNitsenko's task in 1m 42s —— View job Re-review of
|
| Claim | Check |
|---|---|
count, sum, avg, number are the qualifying types |
Confirmed — JoinGraph.ts:117-122, ['sum','avg','count','number'] plus a bare count(*) sql measure. min/max correctly excluded now |
| Compile error text | Verbatim match with joinRequired at :126-127 |
Runtime message Can't find join path to join '<cube>', '<cube>' |
Confirmed — :196 builds it as `Can't find join path to join ${cubesToJoin.map(v => `'${v}'`).join(', ')}`, so the quoting and comma in your rendering are exact |
| "depends on the cube's measures" scopes to the keyless cube | Confirmed — the guard pairs primaryKeys[X] with getMultipliedMeasures(X) for the same cube, and runs on both the from and to side |
Thread resolved. All ten threads on this PR are now resolved.
The other four sweeps hold up on the page's own terms:
- Section opener — "Cube relies on it to aggregate correctly across joins" replaces the flat "requires one on both ends", which the two-case passage below had just contradicted. Now the opener and the detail agree.
- Limitations / tests — the old bullet said tests aren't imported, which this PR falsified. "Read as evidence, not converted" is the accurate distinction, and it links back to the new section.
- One-directional relationships — "a dbt relationship describes the reference from the referencing side only" is the correct weaker claim; a project can write
relationshipson both models, and that would produce two forward joins rather than a reverse one. - Contract snippet — the
# …every other column of the model needs a data_type toocomment closes the one way the sample could mislead, since an enforced contract fails on any undocumented column.
Finding
Nit — dbt.mdx:206-207. "The pull only runs dbt parse" is stronger than the page's own framing 160 lines up: line 44–49 says "By default, no connection to your data warehouse is made… The one exception is [Infer column types from dbt catalog]", and line 237 confirms that option "additionally runs dbt docs generate". The load-bearing half of the new sentence is the first one (your warehouse doesn't need to enforce keys); the mechanism half both over-claims and repeats the intro. Suggested trim posted inline.
Consistency re-check
- Join-type story unchanged and still aligned:
:153reverseone_to_many,:434-440forward alwaysmany_to_onefrom the FK owner. - The additive join-target rule at
:177-182still agrees with "only a joined-to column can add to it" at:213, and with the keyless-cube case at:706. #primary-key-detectionresolves from the settings table (:147), "What gets generated" (:433), the new Limitations bullet (:618), and the troubleshooting accordion (:722). All other in-page anchors intact.- Both accordions keep the page's symptom-titled style;
<AccordionGroup>/<Accordion>/<Note>/<Warning>tags balance. Nodocs.jsonchange needed — no new page.
Scope
The dbt pull implementation isn't in this OSS monorepo, so its behavioral claims (tier order, additive join-target keying, all-or-nothing constraints, FK deprioritization, reverse-join type) rest on your source reads — I checked internal consistency and dbt semantics from the page. Everything touching this repo I verified in JoinGraph.ts, per the table above. Security, performance, and test-coverage checks don't apply to a single .mdx content change. The Mintlify preview deployment is green.
Looks good to merge; the nit is optional.
Updates the dbt integration page for the new primary key detection behavior in dbt pull.
unique+not_nulltests (on), the column-name fallback (on), and the configurable Primary key column suffixes (_id). Adds Also generate reverse joins (off).primary_keyconstraints, including model-level composite blocks → strictunique+not_nulltests → column-name suffixes), why only tier 1 yields composite keys, how a single column is picked (model-entity name preference, foreign keys avoided), and how to force a key from dbt.The old wording said keys come from "
id/*_idcolumns and columns with a dbtprimary_keyconstraint", which didn't mention model-level constraint blocks and implied every id-ish column gets marked — that ambiguity is what prompted the change.🤖 Generated with Claude Code