Repository navigation
Oracle: descending text_steps_doc_id_id_index blocks every later schema migration #9439
Description
On Oracle, text_steps_doc_id_id_index created with "id" DESC makes every later migration that changes the schema fail with There is no column with name ""SYS_NC00007$"" on table "oc_text_steps", in any app, core included. Since #9393 (backported to stable32–35, not yet in a release), Version080000Date20260331132113 creates exactly that index when an Oracle instance upgrades. Fix: create the index ascending on Oracle. Instances that already have the descending index need it replaced by hand before their next major upgrade.
Details
Observed: upgrade 32 → 33 on Oracle 23 stopped in core Version33000Date20250819110529 with the error above. The index had been created with the workaround statement from #9393's description.
Cause
- Oracle stores a DESC key as a hidden virtual column,
SYS_NCnnnnn$.user_ind_columnsreturns that name for the index, whileuser_tab_columnsdoesn't list it. - It looks like
OracleMigrator::getDiffrebuilds every table throughaddIndex(), and Doctrine'sTable::_createIndex()requires each index column to exist, so any step whosechangeSchemareturns a schema throws. Steps that return null pass, so the failure surfaces one or more migrations after the index is created. This part is from reading the code; corrections welcome. - Fresh installs run app migrations schema-only (
Installer.php#L548), sopostSchemaChange, where the rawCREATE INDEXlives, never runs on them. If that's right, phpunit-oci never executes that statement.
Oracle 23.26.3, oc_text_steps with the index created ("document_id", "id" DESC)
| view | what it shows |
|---|---|
user_ind_columns |
document_id ASC, SYS_NC00007$ DESC |
user_ind_expressions |
position 2 = "id" |
user_tab_cols |
SYS_NC00007$, hidden, virtual |
user_tab_columns |
no SYS_NC column |
Recreated ("document_id", "id"): the index columns read back as document_id, id, and the table has no hidden column. Plans on 200k rows with statistics, for the StepMapper::getLatestVersion and getBeforeVersion query shapes:
| index | operation | cost |
|---|---|---|
("document_id", "id") |
INDEX RANGE SCAN DESCENDING |
3 |
("document_id", "id" DESC) |
INDEX RANGE SCAN |
3 |
Repro (Oracle, as the Nextcloud schema user; creates the index in its #9393 form; fresh installs have no index to drop):
CREATE INDEX text_steps_doc_id_id_index ON "oc_text_steps" ("document_id", "id" DESC);
SELECT index_name, column_name, descend FROM user_ind_columns WHERE column_name LIKE 'SYS_NC%';Then run any migration that returns a schema.
Workaround for affected instances:
DROP INDEX text_steps_doc_id_id_index;
CREATE INDEX text_steps_doc_id_id_index ON "oc_text_steps" ("document_id", "id");A repair step in text can't fully replace this: core migrations run before app migrations, so on a major upgrade core's first schema-changing step fails before text's repair runs.
Server side
Server is the only place that runs early enough to cover instances that already have the index, and to stop one app's index blocking core's and every other app's migrations. Two options:
- A pre-upgrade repair step (
Repair::getBeforeUpgradeRepairSteps(), which runs beforedoCoreUpgrade()) that recreates DESC-only indexes ascending on Oracle. OracleMigratortolerating expression index columns. Skipping such an index only in the rebuilt target would probably make the comparison emitDROP INDEXfor it, so it would likely also have to be ignored on the introspected source side.
Related, not addressed here: fresh installs on PostgreSQL and SQLite never get this index, since it is only created in postSchemaChange.
Activity