fix: resolve HTTP 500 on object delete#1331
Conversation
Development to beta
Development to beta
- updateObjectEntity never called ensureTableForRegisterSchema, so columns added to getMetadataColumns() after a table was created (e.g. _tmlo) were missing when a soft-delete UPDATE ran against older tables. Added the same ensure call that insertObjectEntity already has. - array_diff_key in MagicTableHandler used array_flip() on the result of getExistingTableColumns(), which returns a column-name-keyed map, not a flat list. The flip silently dropped every entry, making every column appear missing and triggering a spurious full sync on every request. Removed the erroneous array_flip(). - Schema properties with format date-time/date were written to MySQL DATETIME columns as raw ISO 8601 strings (e.g. 2024-03-21T10:15:30+00:00) which MySQL rejects. Applied DateTimeNormalizer::formatForDatabase() in prepareObjectDataForTable for these property types, matching the normalisation already in place for system metadata fields. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
All three fixes are sound. The only actionable follow-up is caching getExistingTableColumns results to avoid the information_schema hit on every update. How big of a problem is it to ignore that right now? Not a problem right now. information_schema.columns is a fast metadata query — no row data involved, and MySQL/MariaDB keeps it in memory. On a dev instance with a handful of schemas it'll be sub-millisecond. It only becomes noticeable at scale: many concurrent users, high update frequency, or dozens of dynamic tables. Worth a ticket for later but not blocking this PR. |
…ema queries Added $tableColumnsVerifiedCache static property to MagicMapper that records whether a given register+schema table's columns have been verified in the current PHP process. Once verified, ensureTableForRegisterSchema returns immediately without hitting information_schema on subsequent writes. - Fast path in ensureTableForRegisterSchema: if verified flag is set, skip buildTableColumnsFromSchema + getExistingTableColumns entirely - Flag is set to true after a passing sanity check (no missing columns) - Flag is set to true after a successful syncTableForRegisterSchema, covering both the automatic path and the manual frontend sync button (TablesController) - Flag is cleared in invalidateTableCache() alongside existing cache entries - Flag is cleared in clearAllStaticCaches() alongside existing caches - Added setTableColumnsVerified() and isTableColumnsVerified() static accessors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 147/147 | |||
| npm | ✅ | ✅ 599/599 | |||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-04-23 11:15 UTC
Download the full PDF report from the workflow artifacts.
…tion DateTime misses DateTimeImmutable — if upstream passes a DateTimeImmutable the value falls through both branches and reaches MySQL unformatted. Using DateTimeInterface covers both concrete types. Fixed in two locations: - System metadata fields (created/updated/expires) around the existing normalisation block - Schema property date-time/date fields added in the previous commit Also adds MagicTableHandlerTest covering the array_flip regression and the $tableColumnsVerifiedCache fast path, as suggested in code review. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 147/147 | |||
| npm | ✅ | ✅ 599/599 | |||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-04-23 11:21 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 147/147 | |||
| npm | ✅ | ✅ 599/599 | |||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-04-23 11:28 UTC
Download the full PDF report from the workflow artifacts.
|
Nog kleine nitpick opmerking: je Test plan checklist is niet afgevinkt in de beschrijving |
Summary
updateObjectEntitymissingensureTableForRegisterSchemacall — the soft-delete path (which is the default for all API DELETE requests) updates the dynamic table without first checking for missing columns. Columns added togetMetadataColumns()after a table was created (e.g._tmlo) causedSQLSTATE[42S22]: Unknown column '_tmlo' in 'SET'. Added the sameensureTableForRegisterSchemacall thatinsertObjectEntityalready has.array_flip()on column map inMagicTableHandler—getExistingTableColumns()returns a map keyed by column name;array_flip()silently dropped all entries because the values are arrays, not strings. This madearray_diff_keyreport every column as missing and triggered a spurious full table sync on every single request. Removed the erroneousarray_flip().format: date-timeordatewere passed raw (e.g.2024-03-21T10:15:30+00:00) to MySQLDATETIMEcolumns, which only acceptY-m-d H:i:s. AppliedDateTimeNormalizer::formatForDatabase()inprepareObjectDataForTablefor these property types, matching the normalisation already in place for system metadata fields (created,updated,expires).Test plan
_tmlowas added — should return 200, not 500array_flipPHP warnings in nextcloud.log during any object operationformat: date-timecontaining an ISO 8601 value — should soft-delete cleanlyensureTableForRegisterSchemais a no-op (cache hit) on subsequent updates once the table is already up to dateImplementation plan: column-verified cache
Problem
ensureTableForRegisterSchemacurrently hitsinformation_schemaon every objectwrite (insert, update, soft-delete) to verify the dynamic table has all required
columns. This query is a no-op in the happy path but runs unconditionally.
Solution
Add a static boolean cache (
$tableColumnsVerifiedCache) toMagicMapperthatrecords whether a given register+schema table has been verified in the current
PHP process. Once verified, subsequent requests skip the
information_schemaquery entirely.
Design decisions
information_schemaquery, which is acceptable since misses are rare (firstrequest after deploy or schema change). Caching the full column list would add
complexity to keep it in sync after ALTERs without meaningful gain.
ALTER TABLE. The sync happens on the next object write via
ensureTableForRegisterSchema.hasRegisterSchemaChanged()already detectsthe version mismatch and triggers sync correctly, so no hook into schema save
is needed.
Changes
MagicMapper.phpstatic $tableColumnsVerifiedCache = []alongside existing static cachessetTableColumnsVerified(string $cacheKey)static accessorisTableColumnsVerified(string $cacheKey): boolstatic accessorinvalidateTableCache($cacheKey)to also unset the verified flagclearAllStaticCaches()to also clear$tableColumnsVerifiedCacheMagicMapper/MagicTableHandler.phpensureTableForRegisterSchema: returns immediately if verified flag is set,skipping
buildTableColumnsFromSchema+getExistingTableColumnsentirely.Sets flag to
trueafter a passing sanity check.syncTableForRegisterSchema: sets flag totrueafter a successful sync.Covers both the automatic path (via
ensureTableForRegisterSchema) and themanual path (via
TablesControllerfrontend sync button).Invalidation triggers
syncTableForRegisterSchemaruns, sets flag totruewith new statesyncTableForRegisterSchemaruns, sets flag totrueinvalidateTableCacheclears flag (already happened)clearAllStaticCachesclears all flags (already happened)Status
✅ Implemented and tested — delete returns 204 with zero MagicTableHandler
warnings in logs.
🤖 Generated with Claude Code