Skip to content

Oracle diagrams are scoped to the connecting user

Five bulk dictionary reads build the tree regardless of table count, and every one of them is filtered by owner, so other schemas are simply not drawn.

A table you can query is missing from the picture. You select from REPORTING.INVOICE in the editor, it answers, you open the diagram, and the box is not there. Nothing failed and no error was raised, so the natural conclusion is that the diagram is broken.

It is not. The Oracle diagram is built from five reads of the data dictionary, and every one of those five carries OWNER = :1, bound to the user the connection authenticated as. REPORTING.INVOICE is visible to your session and owned by somebody else, so it was never in the result set the tree and the diagram are drawn from.

Five reads, whatever the table count

getSchema() on the Oracle provider issues five bulk queries against the ALL_* data-dictionary views and groups the rows in memory, by table:

What is read View
Tables and the row estimate ALL_TABLES (NUM_ROWS)
Columns ALL_TAB_COLUMNS
Primary keys ALL_CONSTRAINTS + ALL_CONS_COLUMNS, CONSTRAINT_TYPE = 'P'
Foreign keys ALL_CONSTRAINTS type 'R', joined to the referenced constraint
Indexes ALL_INDEXES + ALL_IND_COLUMNS

Five round trips for four tables. Five round trips for four hundred. The count is a property of the code, not of the schema, because the grouping happens in the process rather than in a query per table.

The two other shapes in this codebase put the cost somewhere else. A per-table loop is N+1: the round-trip count follows the table count. A single stitched query is one round trip, which is what the PostgreSQL provider does, but it needs a materialized CTE that only holds together in one dialect. Five bulk statements sit between the two: a fixed count, no generated SQL, and five flat result sets grouped in memory by table name.

Where the declared keys come from

The boxes come from ALL_TABLES and ALL_TAB_COLUMNS. The edges - the part that makes it a diagram rather than a list - come from ALL_CONSTRAINTS. A foreign key in Oracle is a constraint of type R whose R_CONSTRAINT_NAME names the primary or unique constraint it points at, so the read joins the constraint back to the one it references:

SELECT ac.TABLE_NAME,
       acc.COLUMN_NAME,
       rc.TABLE_NAME AS REF_TABLE,
       rcc.COLUMN_NAME AS REF_COLUMN
FROM ALL_CONSTRAINTS ac
JOIN ALL_CONS_COLUMNS acc ON ac.CONSTRAINT_NAME = acc.CONSTRAINT_NAME AND ac.OWNER = acc.OWNER
JOIN ALL_CONSTRAINTS rc ON ac.R_CONSTRAINT_NAME = rc.CONSTRAINT_NAME AND ac.R_OWNER = rc.OWNER
JOIN ALL_CONS_COLUMNS rcc ON rc.CONSTRAINT_NAME = rcc.CONSTRAINT_NAME AND rc.OWNER = rcc.OWNER
WHERE ac.OWNER = :1 AND ac.CONSTRAINT_TYPE = 'R'

Two consequences follow from that being the only source of an edge.

The first is the rule the feature page publishes for every engine: edges are discovered from declared foreign keys. A relationship your application enforces in code, or that lives only in a naming convention, declares nothing for this query to find and draws no line. Oracle is not special there; it is the same sentence as ClickHouse, arrived at from the opposite direction, since ClickHouse declares no foreign keys at all.

The second is specific to this dictionary read. The join above matches ac.R_OWNER to the referenced constraint’s owner, but the outer filter is ac.OWNER = :1. A foreign key declared in your schema and pointing at a table in another one has a resolvable target row and no box to attach it to, because the target table was excluded by the table read. The relationship exists in Oracle. It is not in the picture.

Why the schema read is owner-scoped

OWNER = :1 is not the same predicate as “everything you can read”. The ALL_* views show every object the session holds any privilege on: application schemas the user has a single SELECT grant on, integration staging areas, whatever a vendor package installed. Drawing all of them produces a picture of the instance, not of the schema you are working in.

Scoping to the connecting user makes the drawn scope match the credential. What you see is what that user owns, which is a boundary a DBA can reason about and grant against.

It also settles the edge set with no further work. Nodes and edges are filtered by the same predicate, so the diagram can never contain an edge pointing into empty space.

What is not listed, and how to see it

There is no cross-schema browsing on the Oracle provider. Objects in other schemas that your user can see are not listed in the tree, are not drawn in the diagram, and there is no setting that changes it. There is also no two-phase loading - no separate list-then-relations call - so /api/db/schema/list falls back to the same full getSchema(). The tree arrives whole or not at all.

Two things do work, and neither pretends to be the diagram.

Add a connection for that user. Oracle connects to a service rather than a database name, so a second connection differs from the first only in the credential: same host, same port, same service, a different user. That user’s schema becomes a tree and a diagram of its own.

Ask the dictionary yourself. The editor sends the SQL you type, and the ALL_* views are ordinary tables to it:

SELECT OWNER, TABLE_NAME, CONSTRAINT_NAME, R_OWNER
FROM ALL_CONSTRAINTS
WHERE CONSTRAINT_TYPE = 'R' AND R_OWNER <> OWNER
ORDER BY OWNER, TABLE_NAME

That returns the cross-schema references your session can see. It is a result grid, not a graph, and it is the honest answer: the product will not draw something it did not read.

What the tables do not carry

The TableSchema this read returns has no size field. Not zero, not unknown - the field does not exist, because none of the five queries reads a segment. Storage figures on Oracle come from the monitoring path, which reads USER_SEGMENTS and DBA_DATA_FILES under its own privilege guards, and that path is separate from the schema read on purpose: a schema tree that needed a DBA grant to render would not render for the application user it is usually opened as.

The one number the tables do carry is rowCount, taken from ALL_TABLES.NUM_ROWS. That is the optimizer’s estimate. It is as fresh as the last DBMS_STATS run, and it is NULL on a table nobody has gathered statistics for, which is every table you created this morning. Read it as an order of magnitude for choosing which box to open first. Do not read it as a count; the count is SELECT COUNT(*), and it costs what it costs.

The rest of what the Oracle provider does and declines to do - Thin-mode transport, the maintenance vocabulary, and an agent auto run that ends engine-unsupported on Oracle while plan mode still opens and executes nothing - is on the engine page.