Add a schema migration step#
When the .jGIS file format changes (field renames, restructured data, new
required fields), a migration step must be added so that older files are
automatically upgraded on load. Migration runs in both load paths:
JupyterLab (with server):
YJGIS.set()inpython/jupytergis_core/jupytergis_core/jgis_ydoc.pyJupyterLite (browser-only):
fromString()inpackages/schema/src/model.ts
Both runners apply the same linear chain of version-to-version steps. A file at any older version is brought up to the current schema version automatically.
Steps to add a migration#
1. Bump the schema version#
Edit packages/schema/src/schema/project/jgis.json and increment the
schemaVersion default (e.g. "0.6.0" → "0.7.0"). The build script
regenerates src/_interface/version.d.ts and version.js automatically.
2. Write the Python step#
Create python/jupytergis_core/jupytergis_core/migrations/v0_6_to_v0_7.py
with a migrate(doc: dict) -> dict function that takes the parsed document
and returns a transformed copy:
def migrate(doc: dict) -> dict:
layers = dict(doc.get("layers", {}))
for layer_id, layer in layers.items():
# ... transform layer ...
layers[layer_id] = layer
return {**doc, "layers": layers}
3. Register the Python step#
Add the step to STEPS in
python/jupytergis_core/jupytergis_core/migrations/__init__.py:
from . import v0_6_to_v0_7
STEPS = [
("0.5.0", "0.6.0", v0_5_to_v0_6.migrate),
("0.6.0", "0.7.0", v0_6_to_v0_7.migrate), # new
]
4. Write the JS step#
Create packages/schema/src/migrations/v0_6_to_v0_7.ts with a migrate
function that operates on a plain JSON object:
export function migrate(doc: Record<string, any>): Record<string, any> {
const layers = { ...doc.layers };
for (const [id, layer] of Object.entries(layers) as [string, any][]) {
// ... transform layer ...
layers[id] = { ...layer };
}
return { ...doc, layers };
}
5. Register the JS step#
Add the step to STEPS in packages/schema/src/migrations/index.ts:
import { migrate as migrateV0_6ToV0_7 } from './v0_6_to_v0_7';
const STEPS: IMigrationStep[] = [
{ from: '0.5.0', to: '0.6.0', migrate: migrateV0_5ToV0_6 },
{ from: '0.6.0', to: '0.7.0', migrate: migrateV0_6ToV0_7 }, // new
];
6. Add fixtures and tests#
Fixtures live in packages/schema/test-fixtures/migrations/. Each version
directory holds .jGIS files; a migration step is tested for every file
that exists in both the from and to directories.
Create
packages/schema/test-fixtures/migrations/v0.6.0/<fixture>.jGISwith a document in the old format (the input).Run the Python migration to generate the expected output:
python3 -c " import json from jupytergis_core.migrations import migrate doc = json.load(open('packages/schema/test-fixtures/migrations/v0.6.0/<fixture>.jGIS')) print(json.dumps(migrate(doc), indent=2, sort_keys=True)) "
Save the output as
packages/schema/test-fixtures/migrations/v0.7.0/<fixture>.jGIS.Verify both test suites pass:
pytest python/jupytergis_core/jupytergis_core/tests/test_migrations.py -v jlpm lerna run test --scope @jupytergis/base
Rules#
Steps must form a contiguous chain. Each step’s
toversion must equal the next step’sfromversion.Step functions must be pure. Return a new dict/object; do not mutate the input.
Both Python and JS steps must produce identical output for the same input. The committed fixture files are the shared source of truth — if the two implementations diverge, one of the test suites will fail.
A fixture file has an independent lifecycle. Add it to a version directory when a new case appears; omit it from a later directory when it is no longer relevant. A step is only tested for files present in both the
fromandtodirectories.