From 134d01f480d5c44386c3afff72b2db61f1313735 Mon Sep 17 00:00:00 2001 From: James Manuel Date: Wed, 30 Sep 2026 12:57:54 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20=F0=9F=93=9D=20document=20changing=20a?= =?UTF-8?q?=20string=20column=20to=20text=20on=20Oracle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a rule to "Supporting more databases" that string columns can not be changed to text in place on Oracle (ORA-22858), and a subsection on the out-of-place conversion: the schema API copy steps, the raw SQL variant that keeps NOT NULL, and fixing an already released migration. Co-Authored-By: Claude Opus 5.5 Assisted-by: ClaudeCode:claude-opus-5-5 Signed-off-by: James Manuel --- developer_manual/basics/storage/database.rst | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/developer_manual/basics/storage/database.rst b/developer_manual/basics/storage/database.rst index d653c47f818..db9e0c41a84 100644 --- a/developer_manual/basics/storage/database.rst +++ b/developer_manual/basics/storage/database.rst @@ -536,6 +536,7 @@ When Oracle (``oci``) is supported (also when you don't list any databases), Nex * Sequence names can not be longer than 30 characters * String columns can not be NotNull and have an empty string as default value when being added in a later migration * String columns can not have a length longer than 4.000 characters, use text instead +* Existing string columns can not be changed to text in place. Oracle stores text as ``CLOB`` and rejects ``ALTER TABLE ... MODIFY`` from ``VARCHAR2`` to ``CLOB`` with ``ORA-22858``, even on an empty table. This is not checked automatically. See :ref:`oracle-string-to-text`. * Boolean columns can not be NotNull Additionally we assume that Oracle support means you are interested in scaling and therefore check additional restrictions of other databases in clustered setups: @@ -546,3 +547,25 @@ On top of that there are some configs which influence the queries you can run. K * MySQL deleting lot of entries - Use a ``LIMIT`` on the delete (not supported on other databases), see this `sample of the activity app `_ * MySQL ``ONLY_FULL_GROUP_BY`` - All values selected in a query with a ``GROUP BY`` need to be aggregated as per `MySQL manual `_ + +.. _oracle-string-to-text: + +Changing a string column to text +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Oracle can not convert a ``VARCHAR2`` column to ``CLOB`` in place, so a migration that calls ``setType()`` to change a string column to text fails on Oracle. Do the change out of place instead, with each step in its own migration class: + +1. Add a nullable text column ``_copy`` and copy the data into it in ``postSchemaChange()``. +2. Drop ````. +3. Add ```` again as a nullable text column and copy the data back in ``postSchemaChange()``. +4. Drop ``_copy``. + +The steps in ``apps/dav/lib/Migration/Version1008Date20181105*.php`` follow this pattern. + +Things to keep in mind: + +* **One migration class per step.** Within a single step only the end state is compared with the database. Dropping and re-adding a column under the same name in one step is therefore an in-place type change again, and fails with ``ORA-22858``. +* **The column ends up nullable.** Once a column is ``CLOB``, Oracle rejects any ``MODIFY`` that names the type, and Doctrine always names it, so ``NOT NULL`` can not be added back through the schema API. Adding a text column as ``NotNull`` with an empty-string default is also rejected by Nextcloud's schema check, as for string columns. +* **Guard the data copies.** ``postSchemaChange()`` runs even when ``changeSchema()`` returns ``null``. Check that the columns you copy between exist, so the step does not fail on instances where the change was already made. +* **Keeping NOT NULL.** If the column must stay ``NOT NULL``, do the conversion on Oracle only, with raw SQL in ``preSchemaChange()``: add a ``CLOB`` copy, copy and verify the data, drop the original, rename the copy. ``changeSchema()`` then sees a text column and changes nothing. Core's ``Version34000Date20260318095645`` does this for ``oc_jobs.argument``. +* **Fixing an already released migration.** Every migration that has not run yet is executed in version order, so a new migration dated after a released one that fails never gets to run. Change the failing migration itself. Instances where it already succeeded have it recorded and do not run it again.