Browse documentationOpen

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 asDriver types collapsed into it
bigintbigint, bigserial, int8
integerint, int4, integer, mediumint, serial
smallintint2, smallint, smallserial, tinyint
varcharvarchar, char, character, character varying, bpchar, uuid
timestamptimestamp, timestamptz, timestamp with time zone, timestamp without time zone, datetime, datetime2, smalldatetime
texttext, tinytext, mediumtext, longtext, json, jsonb
decimaldecimal, numeric, float, real, double, double precision
booleanbool, boolean, and any tinyint(1)

Three rules shape the table:

  • Length and precision are dropped. varchar(255) and varchar(64) both render as varchar. The diagram is about shape, not storage.
  • unsigned is dropped. bigint unsigned renders as bigint.
  • tinyint(1) is boolean. MySQL's boolean is a tinyint(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:

MarkerCondition
PKThe column appears in the table's primary key
FKThe column appears in a foreign key constraint
UKThe 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.