doctrine/orm · error · MappingException
Table {tableName} has no primary key. Doctrine does not supp
Error message
Table {tableName} has no primary key. Doctrine does not support reverse engineering from tables that don't have a primary key. What it means
While reverse engineering a database schema, DatabaseDriver requires every entity table to have a primary key constraint, because Doctrine entities fundamentally need an identifier to manage identity and unit-of-work tracking. When a table's primary key cannot be determined (it literally has none, considering the DBAL version's API), a MappingException is thrown naming the table.
Source
Thrown at src/Mapping/Driver/DatabaseDriver.php:298
foreach ($this->sm->listTables() as $table) {
$tableName = self::getAssetName($table);
$foreignKeys = $table->getForeignKeys();
$allForeignKeyColumns = [];
foreach ($foreignKeys as $foreignKey) {
$allForeignKeyColumns = array_merge($allForeignKeyColumns, self::getReferencingColumnNames($foreignKey));
}
if (method_exists($table, 'getPrimaryKeyConstraint')) {
$primaryKey = $table->getPrimaryKeyConstraint();
} else {
$primaryKey = $table->getPrimaryKey();
}
if ($primaryKey === null) {
throw new MappingException(
'Table ' . $tableName . ' has no primary key. Doctrine does not ' .
"support reverse engineering from tables that don't have a primary key.",
);
}
if ($primaryKey instanceof PrimaryKeyConstraint) {
$pkColumns = array_map(static fn (UnqualifiedName $name) => $name->toString(), $primaryKey->getColumnNames());
} else {
$pkColumns = self::getIndexedColumns($primaryKey);
}
sort($pkColumns);
sort($allForeignKeyColumns);
if ($pkColumns === $allForeignKeyColumns && count($foreignKeys) === 2) {
$this->manyToManyTables[$tableName] = $table;
} else {
// lower-casing is necessary because of Oracle Uppercase Tablenames,View on GitHub (pinned to d9b9ff7301)
Solutions
- Add a primary key to the offending table: ALTER TABLE my_table ADD PRIMARY KEY (id); (or add an AUTOINCREMENT id column first).
- Exclude the table from reverse engineering via the schema asset filter (FilterSchemaAssetsExpression or schema_assets_filter config) so it never reaches the driver.
- If the table is a many-to-many join table, pass it through DatabaseDriver::setTables($entityTables, [$joinTable]) as a manyToManyTable instead of an entity table.
Example fix
-- before CREATE TABLE report_temp (fetched_at DATETIME, sku VARCHAR(64), qty INT); -- after CREATE TABLE report_temp (fetched_at DATETIME, sku VARCHAR(64), qty INT, PRIMARY KEY (fetched_at, sku));
Defensive patterns
Strategy: validation
Validate before calling
// Exclude PK-less utility tables before reverse engineering
$schemaManager->getDatabasePlatform()->...;
// DBAL 3: $conn->getConfiguration()->setSchemaAssetsFilter('#^(?!report_temp)#');
// DBAL 4: $conn->getConfiguration()->setSchemaAssetsFilter(static fn ($name) => $name !== 'report_temp'); Try / catch
try { $driver->loadMetadataForClass($class, $metadata); } catch (MappingException $e) { /* log and skip tables that cannot be mapped */ } Prevention
- Design every table with a primary key from the start.
- Maintain a schema-asset filter for non-entity tables.
- When converting legacy schemas, run convert-mapping on a filtered allow-list of tables.
When it happens
Trigger: Running orm:convert-mapping --from-database, the SchemaTool, or DatabaseDriver::loadMetadataForClass() against a schema where at least one table has no primary key constraint — e.g. a hand-made many-to-many join table, staging/import tables, or legacy reporting tables.
Common situations: Reverse engineering legacy databases (older WordPress/Magento-style schemas) known for PK-less tables; generated tables where the PK was dropped; CI converting a shared/staging schema that contains utility tables.
Related errors
- Argument #2 passed to %s() must be an instance of %s, %s giv
- Unknown class {className}
- Uninitialized result set mapping.
- Unable to use access strategy type of [%s] without a Concurr
- Unrecognized access strategy type [%s]
AI-assisted analysis of doctrine/orm@d9b9ff7301 (2026-08-21).
Data as JSON: /api/errors/c1c703b3935e0204.
Report an issue: GitHub.