Skip to content

feat(client): glossary node create and hierarchy enumeration (#199) - #200

Merged
cjimti merged 1 commit into
mainfrom
feat/glossary-node-hierarchy-199
Aug 3, 2026
Merged

feat(client): glossary node create and hierarchy enumeration (#199)#200
cjimti merged 1 commit into
mainfrom
feat/glossary-node-hierarchy-199

Conversation

@cjimti

@cjimti cjimti commented Aug 3, 2026

Copy link
Copy Markdown
Member

Closes #199.

The client could operate on individual glossary terms but never answer "what is the shape of the glossary?" — no root listing, no children of a node, no parent chain, and no way to create a node at all. A glossary is a tree by design, so a consumer could only ever see a flat, unordered slice of it. This adds node creation and full hierarchy enumeration.

API

// Create
CreateGlossaryNode(ctx, name, definition, parentNode string) (string, error)

// Enumerate
GetRootGlossaryNodes(ctx, start, count int) ([]types.GlossaryNode, int, error)
GetRootGlossaryTerms(ctx, start, count int) ([]types.GlossaryTerm, int, error)
GetGlossaryNodeChildren(ctx, nodeURN string, start, count int) (*types.GlossaryChildren, error)
GetGlossaryParentChain(ctx, urn string) ([]types.GlossaryNode, error)

New domain types:

type GlossaryNode struct {
    URN, Name, Description, ParentNode string
    TermsCount, NodesCount             int   // DataHub's own childrenCount tally
}

// A page of what is directly under a node. DataHub returns nodes and terms as
// one mixed result set, so Start/Count/Total describe the combined page rather
// than either slice.
type GlossaryChildren struct {
    Nodes []GlossaryNode
    Terms []GlossaryTerm
    Start, Count, Total int
}

No new MCP tools — this is client/library surface only, per the issue's scope and the project's lean-tool-count rule. Existing term operations (get, create with parent, delete, description update) are untouched.

The open question in the issue, settled empirically

The issue flagged one detail as genuinely open: how children are enumerated. RelationshipsInput.types is [String!]! — a free-form string list — so the schema files name no relationship for glossary parentage. It could not be answered by reading entity.graphql, and it was not guessed.

A DataHub v1.6.0 quickstart was available locally, so I built a real glossary tree against it and probed the API directly. Children are the INCOMING side of the IsPartOf relationship on the parent node:

glossaryNode(urn: $urn) {
  children: relationships(input: {types: ["IsPartOf"], direction: INCOMING, start: 0, count: 100}) {
    total start count
    relationships { entity { urn type ... } }
  }
}

Against a tree of root → {child node → grandchild term, child term}, that edge returned:

"childrenCount": {"termsCount": 1, "nodesCount": 1},
"children": {"total": 2, "start": 0, "count": 2, "relationships": [
  {"entity": {"urn": "...:glossaryNode:...child-node", "type": "GLOSSARY_NODE"}},
  {"entity": {"urn": "...:glossaryTerm:...child-term", "type": "GLOSSARY_TERM"}}
]}

It returns both kinds of children, pages correctly (start: 1, count: 1 → the second child, total still 2), and its total matches the node's own childrenCount — the cheap correctness check the issue suggested. This also matches how DataHub's own UI fetches node children (datahub-web-react/src/graphql/glossaryNode.graphql), so the relationship name is not an artifact of one deployment.

The alternative the issue floated — searchAcrossEntities filtered on parentNode — was not needed.

Two behaviours only the live instance revealed

Children lag writes. They are served from the graph index, which DataHub populates asynchronously through MCL consumers. The first integration run failed with Total = 0 immediately after creating the children; the same query passed seconds later. This is documented on the method, and the integration test polls rather than asserting once. It matters to downstream consumers building a portal browser — a node created by a user will not immediately show its child.

An unknown node does not error. DataHub answers a lookup of a missing glossary node with an empty stub (exists: false), which is otherwise indistinguishable from a node that simply has no children:

{"missing": {"urn": "...:does-not-exist", "exists": false},
 "real":    {"urn": "...:mcpdh-test-root", "exists": true}}

So the children query selects exists and returns ErrNotFound. It is decoded through a *bool, so only a definitive false counts as absent — a DataHub version that omits the field leaves the pointer nil and the page still stands, rather than every node reading as missing.

Parent chain

GetGlossaryParentChain uses the polymorphic entity(urn:) lookup with inline fragments on both GlossaryTerm and GlossaryNode, so one query serves either. It reads the entity itself and is immediately consistent — verified on a term created moments earlier, while its parent's children query was still empty. Prefer it when confirming a just-written parent.

Ordering is DataHub's own: direct parent first, up to the root (verified — a grandchild term returned [child node, root]). Each returned node's ParentNode is filled from the next link in the chain, so a caller can rebuild the branch without another round trip.

Non-glossary URNs are rejected with ErrInvalidURN rather than silently returning an empty chain — entity(urn:) happily resolves a tag or dataset URN and just omits parentNodes, which would otherwise look like "this term is at the root".

Version compatibility

No change to the documented support floor. Every mechanism used here is present in entity.graphql at the v1.3.0 tag (the project's stated minimum), checked against the upstream repo rather than assumed:

Used Present at v1.3.0
createGlossaryNode entity.graphql:763
getRootGlossaryTerms / getRootGlossaryNodes :180 / :187
GlossaryNode.childrenCount :2693
ParentNodesResult :2718

All four new queries are registered in TestGraphQLQueriesMatchSchema, so they are validated against the pinned schema (v1.5.0.1) on every make schema-check.

Drive-by fix: the integration suite did not compile

pkg/client/write_integration_test.go was stale — getAspect, readGlobalTags, readGlossaryTerms, and readInstitutionalMemory had each gained an entityType parameter that the integration tests were never updated for, so make test-integration failed at build time and had presumably been failing for a while. Fixed (all affected call sites use dataset URNs, so "dataset"). The whole integration suite now builds and passes against v1.6.0.

Testing

  • Unitpkg/client/glossary_test.go, GraphQL httptest mocks in the style of write_entities_test.go: field mapping, the mixed node/term split, relationship input (IsPartOf / INCOMING / paging), paging clamps, exists handling both ways, parent-chain ordering and parent linkage, and URN rejection. Plus TestCreateGlossaryNode alongside the existing term test.

  • IntegrationTestIntegrationGlossaryHierarchy (new file, //go:build integration) builds the tree, enumerates it every way the issue asks for, cross-checks childrenCount, and deletes everything on cleanup. This is the documented live verification the acceptance criteria call for, and it is runnable rather than a note in a doc:

    export DATAHUB_URL=http://localhost:8080
    export DATAHUB_TOKEN=<token>
    make test-integration
    
  • Gatesmake verify clean: lint 0 issues, coverage 93.0% (client package 95.1%), schema-check, gosec, govulncheck, build-check. make patch-coverage: 182/182 changed lines = 100%.

  • Not runmake mutation. At --workers 1 gremlins re-runs the whole suite per mutant and was tracking to hours on this repo; it is a repo-wide gate outside make verify and was stopped, not skipped silently.

Acceptance criteria

Criterion Status
Create a node with name, definition, optional parent → URN CreateGlossaryNode, live-verified
Enumerate the tree end to end: root nodes, root terms, children, parent chain ✅ all four accessors
Root and children enumeration paged, surfacing total start/count + total, clamped to MaxLimit
Existing term operations unchanged ✅ no edits to the term read/create/delete paths
httptest mock tests + documented live verification of the children query ✅ unit suite + TestIntegrationGlossaryHierarchy

Downstream

Unblocks txn2/mcp-data-platform#1155 — its DataHub semantic adapter can currently only search glossary terms by name, so its portal cannot present the glossary as a browsable tree or create a node. Node create plus hierarchy enumeration is the missing piece for the portal glossary browser and editor (txn2/mcp-data-platform#1158).

Review notes

  • Human review required per project policy — every line, particularly the GraphQL selections in pkg/client/glossary.go.
  • Worth a second look: treating exists: false as ErrNotFound (a behaviour choice, not forced by the API) and the decision to fill ParentNode in the parent chain by inference from the next link rather than a second fetch.

The client could operate on individual glossary terms but never answer
"what is the shape of the glossary?" — no root listing, no children of a
node, no parent chain, and no way to create a node at all. A glossary is
a tree by design, so a consumer could only ever see a flat slice of it.

Adds:

  CreateGlossaryNode(ctx, name, definition, parentNode)
  GetRootGlossaryNodes(ctx, start, count)
  GetRootGlossaryTerms(ctx, start, count)
  GetGlossaryNodeChildren(ctx, nodeURN, start, count)
  GetGlossaryParentChain(ctx, urn)

plus types.GlossaryNode and types.GlossaryChildren. No new MCP tools;
existing term operations are unchanged.

How children are enumerated was the open question in the issue, since
RelationshipsInput.types is a free-form string list and the schema names
no relationship for glossary parentage. Settled against a live DataHub
v1.6.0 rather than guessed: children are the INCOMING side of the
IsPartOf relationship on the parent node. Verified on a real tree — the
edge returns both nodes and terms, pages on start/count, and its total
matches the node's own childrenCount. This matches how DataHub's own UI
fetches node children (datahub-web-react/src/graphql/glossaryNode.graphql).

Two behaviours only the live instance revealed:

- Children lag writes. They come from the graph index, which DataHub
  populates asynchronously, so a just-created child is not visible at
  once. Documented on the method; the integration test polls.
- An unknown node returns an empty stub rather than an error, which is
  indistinguishable from a childless node, so the client selects exists
  and returns ErrNotFound. It is read through a pointer, so a DataHub
  version that omits the field is not misread as absent.

GetGlossaryParentChain reads parentNodes on the entity itself and is
immediately consistent. It returns the chain direct-parent first, matching
DataHub's order, and fills each node's ParentNode from the next link so a
caller can rebuild the branch without another round trip. Non-glossary
URNs are rejected with ErrInvalidURN rather than silently returning an
empty chain.

No change to the support floor: createGlossaryNode, getRootGlossaryNodes,
getRootGlossaryTerms, parentNodes, and childrenCount are all present in
entity.graphql at the v1.3.0 tag, the documented minimum.

Also repairs write_integration_test.go, which no longer compiled: getAspect,
readGlobalTags, readGlossaryTerms, and readInstitutionalMemory had each
gained an entityType parameter that the integration tests were never updated
for. The full integration suite now builds and passes against v1.6.0.

Verified: make verify clean (lint 0 issues, coverage 93.0%, client 95.1%);
make test-integration green against a live DataHub v1.6.0, including the new
TestIntegrationGlossaryHierarchy, which builds a glossary tree, enumerates it
every way the issue asks for, and deletes it.
@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.39%. Comparing base (8b588e1) to head (f9f6192).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #200      +/-   ##
==========================================
+ Coverage   92.16%   92.39%   +0.22%     
==========================================
  Files          65       66       +1     
  Lines        4673     4812     +139     
==========================================
+ Hits         4307     4446     +139     
  Misses        221      221              
  Partials      145      145              
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cjimti
cjimti merged commit 2e39802 into main Aug 3, 2026
8 checks passed
@cjimti
cjimti deleted the feat/glossary-node-hierarchy-199 branch August 3, 2026 01:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

client: glossary node create and hierarchy enumeration (root nodes/terms, children, parent chain)

1 participant