byrcsc/laravel-cartographer · 1.x
Schema introspection.
Where structure comes from, how column types are normalized across MySQL, PostgreSQL, and SQLite, how PK, FK, and UK markers are decided, and why a live connection is required.
Structure comes from the database, through Laravel's native schema builder. For every table on the connection, Cartographer reads its columns, its indexes, and its foreign key constraints.
That choice has one cost and several benefits. The cost is that generating a diagram needs a reachable, migrated database. The benefits are that squashed migrations, raw SQL migrations, columns added by hand, and a decade of migration history all produce a correct diagram, because none of it is being replayed.
No data is read
Introspection queries the driver's metadata — information_schema on MySQL, the
system catalogs on PostgreSQL, PRAGMA on SQLite. No statement selects from an
application table. The test suite asserts this directly: a full read is
performed with the query log on, and every fixture table name is checked against
every query issued.
The connection therefore only needs permission to read schema metadata.
Choosing the connection
php artisan cartographer:erd --connection=reporting// config/cartographer.php
'connection' => 'reporting',The --connection option wins over the config key, and null in config means
the application's default connection. The value must be null or a non-empty
string; anything else fails the command.
Only one connection is read per run. An application whose models span several connections needs one diagram per connection — see diagrams per subsystem.
When the connection cannot be opened or introspected, the command fails with:
ERROR Unable to introspect database connection [reporting]. Check the connection name, credentials, and database availability.The same message covers an unknown connection name, wrong credentials, and a
database that is not running. The original driver exception is kept as the
previous exception on SchemaReadException.
Column types are normalized
The same application on three drivers reports three sets of type names. Cartographer maps them onto one small vocabulary, so the committed diagram does not change when a developer runs it against SQLite and CI runs it against MySQL.
| Rendered as | Driver types collapsed into it |
|---|---|
bigint | bigint, bigserial, int8 |
integer | int, int4, integer, mediumint, serial |
smallint | int2, smallint, smallserial, tinyint |
varchar | varchar, char, character, character varying, bpchar, uuid |
timestamp | timestamp, timestamptz, timestamp with time zone, timestamp without time zone, datetime, datetime2, smalldatetime |
text | text, tinytext, mediumtext, longtext, json, jsonb |
decimal | decimal, numeric, float, real, double, double precision |
boolean | bool, boolean, and any tinyint(1) |
Three rules shape the table:
- Length and precision are dropped.
varchar(255)andvarchar(64)both render asvarchar. The diagram is about shape, not storage. unsignedis dropped.bigint unsignedrenders asbigint.tinyint(1)is boolean. MySQL's boolean is atinyint(1), and it is matched on the native type before anything else.
A type the normalizer does not recognise passes through unchanged, with its
original spelling and whitespace. A PostGIS geometry column stays geometry;
a vector column stays whatever the driver called it. Nothing is lost, and future
column types need no package release.
The integer-to-bigint rule
int and integer render as bigint when the column takes part in a key, and
integer when it does not.
"Takes part in a key" means the column appears in any index, of any kind, or in
any foreign key constraint. SQLite reports id as INTEGER where MySQL and
PostgreSQL report bigint; without this rule, the same application would
produce a different diagram per driver on nearly every primary and foreign key.
The effect is confined to int-family types. A non-key integer column stays
integer:
attachments {
bigint id PK
integer size_bytes
}Key markers
Each column line carries the markers that apply to it, in this order:
| Marker | Condition |
|---|---|
PK | The column appears in the table's primary key |
FK | The column appears in a foreign key constraint |
UK | The column appears in a unique index and is not part of the primary key |
profiles {
bigint id PK
bigint user_id FK, UK
}Composite keys mark every column they contain. PK suppresses UK on the same
column, because a primary key is unique by definition and the second marker adds
nothing.
Two things follow, and both surprise people at least once:
FK requires a real constraint. Applications that model relationships in
Eloquent but never declare foreign keys in the database get correct edges and no
FK markers. The edges come from the models; the markers come from the schema.
Plain indexes are invisible. A non-unique index affects nothing you can see, though it does count towards the integer-to-bigint rule above. Mermaid entity blocks have no notation for it.
Which tables become entities
Introspection reads every table on the connection, but the graph only uses two kinds:
- Tables belonging to a discovered model.
- Pivot tables named by a many-to-many relation that is in scope.
A table with no model behind it — a legacy log table, a framework table like
jobs or cache, anything a package created — is read and then ignored. There
is no allow-list or deny-list of table names, and none is needed: the model list
is the filter.
Ordering
Tables, columns, indexes, and foreign keys are each sorted by name before use. Column order in the diagram is alphabetical, not the physical column order of the table. Drivers do not agree on the order they return metadata in, and a diagram that reshuffles itself between machines is not reviewable.
What to read next
- Diagram syntax for how columns and markers are rendered.
- Model discovery for how the model list, and therefore the table list, is decided.
- Troubleshooting for missing tables and connection failures.