Skip to content

Paket-Migrationen #194

Open
Open

Description

@janbritz

QuestionPy-Pakete sollten* nach den Semantic Versioning Regeln versioniert werden. Daraus abgeleitet, müssen Migrationen entweder explizit angegeben werden oder können implizit geschehen.

Es sollen folgende Operationen ermöglicht werden:

upgrade

von (X) zu (Y) Art
1.5.0 1.5.1 implizit
1.5.* 1.6.* implizit
1.*.* 2.*.* explizit

Wenn der Question-State von Version X auf Version Y geupgraded werden soll, muss die Migration im Paket Y erfolgen.

Hierfür muss im "expliziten-Fall" und muss nicht* im "impliziten-Fall" eine upgrade_to-Funktion in Y implementiert sein, die die Migrations-Logik enthält.

downgrade

von (X) zu (Y) Art
1.5.1 1.5.0 implizit
1.6.* 1.5.* explizit
2.*.* 1.*.* explizit

Wenn der Question-State von Version X auf Version Y gedowngraded werden soll, muss die Migration im Paket X erfolgen.

Hierfür muss im "expliziten-Fall" und muss nicht* im "impliziten-Fall" eine downgrade_to-Funktion in X implementiert sein, die die Migrations-Logik enthält.

sidegrade

Wenn man sidegraden möchte, muss das immer explizit geschehen. Will man den State von Paket X nach Paket Y sidegraden, dann sollte im Paket Y eine sidegrade_from-Funktion implementiert sein. Hier könnte man noch überlegen, ob X auch eine sidegrade_to-Funktion implementieren darf. Eine sidegrade_from-Funktion sollte dabei nicht nur den Namespace und Shortname des Pakets entgegennehmen, sondern auch die Version, z.B.:
X möchte zu Y migrieren und Y hat einesidegrade_from("X", Version(1, 0, 0))-Funktion. - Y kann also alle States mit Paketversion 1.0.* direkt* migrieren. Ideal wäre es, wenn die upgrade_to- oder downgrade_to-Funktionen von X aufgerufen werden, um bei Inkompatibilität einen kompatiblen State zu erzielen.

* Wenn wir es nicht als Bedingung sehen, dass Pakete strikt nach SemVer versioniert werden, dann können wir zusätzliche upgrade_to-/downgrade_to-Funktionen erlauben (z.B.: upgrade_to(Version(1, 5, 1)). Allerdings wäre dann die beschriebene sidegrade_from-Logik so nicht umsetzbar.

Activity

  1. tumidi commented on Nov 21, 2025

    @tumidi
    Contributor

    QuestionPy-Pakete sollten* nach den Semantic Versioning Regeln versioniert werden. Daraus abgeleitet, müssen Migrationen entweder explizit angegeben werden oder können implizit geschehen.

    Inwieweit kann aus der (SemVer-)Paketversion abgeleitet werden ob eine Migration verpflichtend oder optional ist (bzw. explizit/implizit)?

    SemVer bezieht sich doch auf die Public API Surface, und trifft keinerlei Garantien über das interne Datenmodell.

    Ich würde da spontan eine Versionierung der Migrationen wie aus anderen Software-Paketen einfallen:

    • Eine von der Paketversion unabhängige Versionierung, bspw. alphanumerische Sortierung des Identifier oder aufsteigende Nummerierung.
    • Jede Paketversion kann null oder mehr Migrationen hinzufügen, die jeweils down/upgrade implementieren.
    • Unmögliche Downgrades werden mit raise NotImplementedError markiert o.ä.
    • Migrationsversion wird explizit als state getrackt.
    • Migrationen sind opt-in.

    downgrade

    von (X) zu (Y) Art
    1.5.1 1.5.0 implizit
    1.6.* 1.5.* explizit
    2.. 1.. explizit

    Warum ist diese Tabelle asymmetrisch zur upgrade-Tabelle?

    Hierfür [...] muss nicht* im "impliziten-Fall" eine upgrade_to-Funktion in Y implementiert sein, die die Migrations-Logik enthält.

    Für eine Spec/Doku halte ich die Bezeichnungen "implizit/explizit" für nicht so hilfreich. Was genau bedeutet eine "implizite Migration"? Eine No-op, also keine Migration? Eine automatische Migration? Nach welchen Regeln?

    Idee: ändern in "upgrade_to-Funktion verpflichtend (mandatory) oder optional".

    Fände ich persönlich etwas klarer und implizit/explizit müsste auch nicht extra definiert werden.

  2. janbritz commented on Nov 24, 2025

    @janbritz
    MemberAuthor

    Meinst du mit deinem Vorschlag konkret, dass z.B. im Manifest neben der Paketversion auch eine einfache State-Version (Migrationsversion?) angegeben werden muss, die getrennt voneinander betrachtet werden sollen?

    • Unmögliche Downgrades werden mit raise NotImplementedError markiert o.ä.
    • Migrationen sind opt-in.

    Ich würde vorschlagen, dass nur Migrationen angegeben werden müssen, die möglich sind. Andernfalls wird eine Migration als unmöglich interpretiert. Oder wie würdest du im Falle einer fehlenden Migration handeln?

    Eine zusätzliche State-Version halte ich auch im Hinblick auf die DX schöner, da sofort ersichtlich ist, wann eine Migration implementiert werden kann und man auch diese wirklich nur implementieren muss, wenn es nötig ist.

    @MHajoha hatte vorgeschlagen, dass die möglichen Migrationen in der qpy_manifest.json abgelegt werden sollten, um diese in der UI zu verwenden. Das könnte dann so aussehen:

    {
        // ... die restlichen Felder der 'qpy_manifest.json'
    
        // Die State-Version, die das aktuelle Paket verwendet.
        "state_version": 4,
        // Die State-Versionen, zu welcher ein State zum Aktuellen geupgraded werden kann.
        "possible_state_upgrades": [1, 2, 3],
        // Die State-Versionen, zu welcher der aktuelle State gedowngraded werden kann.
        "possible_state_downgrades": [1, 3],
    
        // Die Identifier und die State-Versionen, von welcher migriert werden können.
        "sidegradable_from": {
            "@other/package": [1, 2, 4]
        }
    }

    Im Code könnte das so aussehen:

    type MigrationFunction = Callable[[QuestionStateMigration], None]
    type Migrations = dict[int, MigrationFunction]
    
    type SideMigrations = dict[str, Migrations]
    
    class ExampleQuestion(Question):
        @staticmethod
        def _migrations_from() -> Migrations:  # analog auch _migrations_to()
            def from_1(_state: QuestionStateMigration) -> None:
                pass
        
            def from_2(_state: QuestionStateMigration) -> None:
                pass
        
            return {
                1: from_1,
                2: from_2,
            }
        
        @staticmethod
        def _migrations_side() -> SideMigrations:
            def llm_teacher() -> Migrations:
                def from_1(_state: QuestionStateMigration) -> None:
                    pass
        
                return {
                    1: from_1,
                }
        
            return {
                "@qpy/llm_teacher": llm_teacher()
            }

    Der Vorteil daran wäre, dass die Migrationsfunktionen außerhalb der Question definiert werden können, da diese mit der Zeit stark wachsen können und dass die IDE meckert, sobald für eine Version / einen Identifier mehrere Migrationsfunktionen angegeben wurden.

  3. MartinGauk commented on Nov 25, 2025

    @MartinGauk
    Contributor

    Nach unserer heutigen Diskussion geht mir jetzt ein Beispiel durch den Kopf:
    Bei einer Einstellung mit Dropdown-Menü wird eine neue Option hinzugefügt. Sagen wir es gibt ein Feld für "Währung" mit Auswahl USD und EUR, und ergänzt nun um CHF. Für diese Ergänzung würde man keinen Update-Code schreiben, weil die bestehenden Einstellungen ja erhalten bleiben sollen. Allerdings müsste man eigentlich einen Downgrade-Code schreiben, der States mit CHF auf eine andere Währung setzt oder den Downgrade ganz unterbindet.

    Ich fürchte aber, dass man als Paket-Entwickler an diesen Fall zu dem Zeitpunkt häufig gar nicht denkt und keine neue state_version einführt, einfach weil man eher Updates im Kopf hat und Downgrades zweitrangig sind.

    Wenn keine neue state_version existiert, wird davon ausgegangen, dass beliebig zwischen diesen Paket-Versionen mit derselben state_version gewechselt werden kann. Es kann aber gut sein, dass eine niedrigere Version nicht mit CHF klarkommt und die Ausführung in eine Exception endet.

  4. janbritz commented on Nov 25, 2025

    @janbritz
    MemberAuthor

    Im heutigen Meeting sind wir zum Entschluss gekommen, dass im Paket ein migrations-Package erstellt werden kann, in welchem alle verfügbaren Migrations in mehreren Dateien leben. Aus der Anzahl der Migrations kann dann geschlussfolgert werden, welche State-Version das aktuelle Paket verwendet.

    Durch die Bennenung der Dateien oder der Klassen soll mittels (alpha-)numerischer Sortierung die Reihenfolge der Migrations bestimmt werden.

    Sidegraden:
    TODO

  5. MartinGauk commented on Dec 1, 2025

    @MartinGauk
    Contributor

    Nach einigem Überlegen und Diskussion mit Claude 4.5 Sonnet komme ich zu dem Entschluss, dass wir Downgrades lieber gar nicht erst unterstützen sollten. Der Aufwand auf unserer Seite für die Grundlagen dazu und auf Seiten der Paketentwickler lohnt sich nicht. Die KI hat es gut auf den Punkt gebracht: "Die Realität: Die meisten Downgrades werden nie ausgeführt und sind beim ersten echten Bedarf kaputt.".

    Ich denke, es ist wichtiger, dass im LMS ein Konzept vorgesehen ist, dass vor einem Paket-Update ein Backup erstellt wird und man später dieses Backup wieder einspielen kann.
    Downgrades auf Paket-Versionen mit derselben State-Version könnten trotzdem mit Warnung ja zugelassen werden.

    Wie seht ihr das?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions