Relationships
Traverse one-to-one, one-to-many, many-to-many, recursive, and polymorphic relationships.
GraphJin relationship traversal comes from schema metadata. Foreign keys, configured relationships, embedded JSON virtual tables, remote API joins, and database-join edges all become nested GraphQL selections, but they are not all executed the same way.
| Relationship kind | Execution model |
|---|---|
| Same-database SQL join | Compiled into the generated SQL using joins, lateral subqueries, and JSON aggregation. |
| MongoDB | Rendered as JSON DSL and translated to aggregation pipeline stages such as $lookup, $match, $project, $unwind, and $group. |
| Remote API joins | Parent rows are fetched first, then external operation results are merged into the response. |
| Cross database | Parent rows are fetched first, the join key is extracted, child queries run against the target database context, and placeholder fields are replaced in the JSON response. |
Parent and child traversal
query {
products(limit: 3, order_by: { id: asc }) {
id
owner {
id
email
}
}
}Relationships come from database schema discovery and explicit table configuration.
Example_queryParentsWithChildren
tests/query_test.go:623Example_queryChildrenWithParent
tests/query_test.go:650Related filters and ordering
Filters and ordering can target relationship paths when the path is known.
query {
products(
where: { owner: { id: { or: [{ eq: $user_id }, { eq: 3 }] } } }
order_by: { users: { email: desc }, id: desc }
limit: 5
) {
id
owner { id email }
}
}Example_queryWithWhereOnRelatedTable
tests/query_test.go:504Example_queryWithNestedOrderBy
tests/query_test.go:279Many-to-many
Join tables can expose many-to-many paths without user-written resolver code.
query {
products(limit: 2, order_by: { id: asc }) {
id
customers {
id
email
}
}
}When more than one join table or foreign key path is possible, use @through(table:) or @through(column:) to disambiguate. Agents should discover relationship rows in gj_catalog before guessing.
Example_queryManyToManyViaJoinTable1
tests/query_test.go:677TestCompositeFK_ThroughColumn_EmitsFullJoinCondition
core/internal/psql/psql_test.go:218Recursive relationships
Self-referential tables can model trees such as comments and categories.
query {
comments(id: $id) {
id
body
replies: comments(find: "children") {
id
body
}
}
}Example_queryWithRecursiveRelationship1
tests/query_test.go:1749Polymorphic relationships
Union-style selections let GraphJin expose polymorphic subjects.
query {
comments {
id
subject {
... on products {
id
name
}
... on users {
id
email
}
}
}
}Example_queryWithUnionForPolymorphicRelationships
tests/query_test.go:1111MongoDB relationships
MongoDB relationships are not SQL joins. GraphJin emits JSON DSL and the MongoDB driver translates related selections to aggregation pipeline $lookup stages.
{
"$lookup": {
"from": "products",
"let": { "userId": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$owner_id", "$$userId"] } } }
],
"as": "products"
}
}Array-column joins use $in; embedded JSON virtual tables use $unwind, $lookup, and $group rather than a simple lookup.
TestBuildCursorSeekFilterAsc
mongodriver/query_cursor_test.go:86Nested database joins
Cross-source joins preserve the same GraphQL shape but use different execution phases:
- Compile and execute the parent query against its source.
- Keep a placeholder field such as
__orders_db_joinin the parent JSON. - Extract the parent key from the result.
- Build a child query such as
orders(where: { user_id: { eq: 42 } }). - Execute that child query with the target source’s compiler and connection.
- Replace the placeholder with the child JSON.
That model is why same-database joins are usually cheaper than cross-database joins, and why table/source ownership should be explicit in multi-source deployments.
TestBuildChildGraphQLQueryNestedDatabaseJoin
core/multidb_test.go:809TestDatabaseJoinFieldIds
core/multidb_test.go:544