diff --git a/changelog.d/11823.misc b/changelog.d/11823.misc
new file mode 100644
index 0000000000..2d153eae4a
--- /dev/null
+++ b/changelog.d/11823.misc
@@ -0,0 +1 @@
+Minor updates and documentation for database schema delta files.
diff --git a/docs/development/database_schema.md b/docs/development/database_schema.md
index 256a629210..a767d3af9f 100644
--- a/docs/development/database_schema.md
+++ b/docs/development/database_schema.md
@@ -96,6 +96,60 @@ Ensure postgres is installed, then run:
NB at the time of writing, this script predates the split into separate `state`/`main`
databases so will require updates to handle that correctly.
+## Delta files
+
+Delta files define the steps required to upgrade the database from an earlier version.
+They can be written as either a file containing a series of SQL statements, or a Python
+module.
+
+Synapse remembers which delta files it has applied to a database (they are stored in the
+`applied_schema_deltas` table) and will not re-apply them (even if a given file is
+subsequently updated).
+
+Delta files should be placed in a directory named `synapse/storage/schema/<database>/delta/<version>/`.
+They are applied in alphanumeric order, so by convention the first two characters
+of the filename should be an integer such as `01`, to put the file in the right order.
+
+### SQL delta files
+
+These should be named `*.sql`, or — for changes which should only be applied for a
+given database engine — `*.sql.posgres` or `*.sql.sqlite`. For example, a delta which
+adds a new column to the `foo` table might be called `01add_bar_to_foo.sql`.
+
+Note that our SQL parser is a bit simple - it understands comments (`--` and `/*...*/`),
+but complex statements which require a `;` in the middle of them (such as `CREATE
+TRIGGER`) are beyond it and you'll have to use a Python delta file.
+
+### Python delta files
+
+For more flexibility, a delta file can take the form of a python module. These should
+be named `*.py`. Note that database-engine-specific modules are not supported here –
+instead you can write `if isinstance(database_engine, PostgresEngine)` or similar.
+
+A Python delta module should define either or both of the following functions:
+
+```python
+import synapse.config.homeserver
+import synapse.storage.engines
+import synapse.storage.types
+
+
+def run_create(
+ cur: synapse.storage.types.Cursor,
+ database_engine: synapse.storage.engines.BaseDatabaseEngine,
+) -> None:
+ """Called whenever an existing or new database is to be upgraded"""
+ ...
+
+def run_upgrade(
+ cur: synapse.storage.types.Cursor,
+ database_engine: synapse.storage.engines.BaseDatabaseEngine,
+ config: synapse.config.homeserver.HomeServerConfig,
+) -> None:
+ """Called whenever an existing database is to be upgraded."""
+ ...
+```
+
## Boolean columns
Boolean columns require special treatment, since SQLite treats booleans the
diff --git a/synapse/storage/prepare_database.py b/synapse/storage/prepare_database.py
index 1823e18720..e3153d1a4a 100644
--- a/synapse/storage/prepare_database.py
+++ b/synapse/storage/prepare_database.py
@@ -499,9 +499,12 @@ def _upgrade_existing_database(
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) # type: ignore
- logger.info("Running script %s", relative_path)
- module.run_create(cur, database_engine) # type: ignore
- if not is_empty:
+ if hasattr(module, "run_create"):
+ logger.info("Running %s:run_create", relative_path)
+ module.run_create(cur, database_engine) # type: ignore
+
+ if not is_empty and hasattr(module, "run_upgrade"):
+ logger.info("Running %s:run_upgrade", relative_path)
module.run_upgrade(cur, database_engine, config=config) # type: ignore
elif ext == ".pyc" or file_name == "__pycache__":
# Sometimes .pyc files turn up anyway even though we've
|