{"record":{"id":"c1c703b3935e0204","repo":"doctrine/orm","slug":"table-tablename-has-no-primary-key-doctrine-doe","errorCode":null,"errorMessage":"Table {tableName} has no primary key. Doctrine does not support reverse engineering from tables that don't have a primary key.","messagePattern":"Table (.+?) has no primary key\\. Doctrine does not support reverse engineering from tables that don't have a primary key\\.","errorType":"exception","errorClass":"MappingException","httpStatus":null,"severity":"error","filePath":"src/Mapping/Driver/DatabaseDriver.php","lineNumber":298,"sourceCode":"\n        foreach ($this->sm->listTables() as $table) {\n            $tableName   = self::getAssetName($table);\n            $foreignKeys = $table->getForeignKeys();\n\n            $allForeignKeyColumns = [];\n\n            foreach ($foreignKeys as $foreignKey) {\n                $allForeignKeyColumns = array_merge($allForeignKeyColumns, self::getReferencingColumnNames($foreignKey));\n            }\n\n            if (method_exists($table, 'getPrimaryKeyConstraint')) {\n                $primaryKey = $table->getPrimaryKeyConstraint();\n            } else {\n                $primaryKey = $table->getPrimaryKey();\n            }\n\n            if ($primaryKey === null) {\n                throw new MappingException(\n                    'Table ' . $tableName . ' has no primary key. Doctrine does not ' .\n                    \"support reverse engineering from tables that don't have a primary key.\",\n                );\n            }\n\n            if ($primaryKey instanceof PrimaryKeyConstraint) {\n                $pkColumns = array_map(static fn (UnqualifiedName $name) => $name->toString(), $primaryKey->getColumnNames());\n            } else {\n                $pkColumns = self::getIndexedColumns($primaryKey);\n            }\n\n            sort($pkColumns);\n            sort($allForeignKeyColumns);\n\n            if ($pkColumns === $allForeignKeyColumns && count($foreignKeys) === 2) {\n                $this->manyToManyTables[$tableName] = $table;\n            } else {\n                // lower-casing is necessary because of Oracle Uppercase Tablenames,","sourceCodeStart":280,"sourceCodeEnd":316,"githubUrl":"https://github.com/doctrine/orm/blob/d9b9ff73016bf598ae07515f97289ce8074e97a5/src/Mapping/Driver/DatabaseDriver.php#L280-L316","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"-- before\nCREATE TABLE report_temp (fetched_at DATETIME, sku VARCHAR(64), qty INT);\n\n-- after\nCREATE TABLE report_temp (fetched_at DATETIME, sku VARCHAR(64), qty INT,\n  PRIMARY KEY (fetched_at, sku));","handlingStrategy":"validation","validationCode":"// Exclude PK-less utility tables before reverse engineering\n$schemaManager->getDatabasePlatform()->...;\n// DBAL 3: $conn->getConfiguration()->setSchemaAssetsFilter('#^(?!report_temp)#');\n// DBAL 4: $conn->getConfiguration()->setSchemaAssetsFilter(static fn ($name) => $name !== 'report_temp');","typeGuard":null,"tryCatchPattern":"try { $driver->loadMetadataForClass($class, $metadata); } catch (MappingException $e) { /* log and skip tables that cannot be mapped */ }","preventionTips":["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."],"tags":["doctrine-orm","reverse-engineering","database-schema","primary-key"],"backgroundTag":"missing-primary-key","analyzedSha":"d9b9ff73016bf598ae07515f97289ce8074e97a5","analyzedAt":"2026-08-21T06:13:15.863Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}