summaryrefslogtreecommitdiff
path: root/source/storage/Migration.mc
blob: e41d3fbcb9fe26024a32f1c0c0690e5be4844ff6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// Author:  cirakg & claude ai
// Summary: Storage schema versions: create the metadata on first start, migrate old schemas, refuse newer ones.
//
// The metadata "m" is a dictionary:
//     "schema"  data version (CURRENT)
//     "ids"     entry IDs in display order
//     "pending" IDs of pending locked entries (README 7.7)
//
// Rules (README 6.3):
//   * Each migration is one small, idempotent function from version k to
//     k + 1, run in order at start-up.
//   * A migration that rewrites records writes each new record under a
//     temporary key and switches the index to it before deleting the old
//     one, so a crash or power loss mid-migration loses nothing. The only
//     migration so far (0 to 1) changes just the metadata, in one write.
//   * Data from a newer schema (after a downgrade) is never modified.
//
// Version 0 never shipped: it is a fixture layout ("m" = {"order" => ids})
// that exists to test the migration machinery before a real migration is
// needed.

import Toybox.Application;
import Toybox.Lang;

module Migration {

    const CURRENT = 1;
    const META_KEY = "m";

    // The metadata at the CURRENT schema, migrating or creating it as needed.
    // Throws VaultError NEWER_SCHEMA or DAMAGED.
    function run(store as Persist.Backend) as Dictionary<Application.Storage.KeyType, Application.Storage.ValueType> {
        var stored = store.getValue(META_KEY);
        if (stored == null) {
            var fresh = {"schema" => CURRENT, "ids" => [], "pending" => []} as Dictionary<Application.Storage.KeyType, Application.Storage.ValueType>;
            store.setValue(META_KEY, fresh);
            return fresh;
        }
        if (!(stored instanceof Dictionary)) {
            throw new VaultError(VaultError.DAMAGED, "metadata damaged");
        }
        var meta = stored as Dictionary<Application.Storage.KeyType, Application.Storage.ValueType>;
        var schema = meta.hasKey("schema") ? meta["schema"] : 0;
        if (!(schema instanceof Number) || schema < 0) {
            throw new VaultError(VaultError.DAMAGED, "metadata damaged");
        }
        if (schema > CURRENT) {
            throw new VaultError(VaultError.NEWER_SCHEMA, "data from a newer version");
        }
        if (schema == 0) {
            meta = v0ToV1(meta);
            store.setValue(META_KEY, meta);
        }
        if (!isValidV1(meta)) {
            throw new VaultError(VaultError.DAMAGED, "metadata damaged");
        }
        return meta;
    }

    // Version 0 to 1: "order" becomes "ids", and "pending" is added.
    function v0ToV1(meta as Dictionary<Application.Storage.KeyType, Application.Storage.ValueType>) as Dictionary<Application.Storage.KeyType, Application.Storage.ValueType> {
        var order = meta["order"];
        return {
            "schema" => 1,
            "ids" => order instanceof Array ? order : [],
            "pending" => []
        } as Dictionary<Application.Storage.KeyType, Application.Storage.ValueType>;
    }

    // True when "ids" and "pending" are arrays of distinct valid IDs.
    function isValidV1(meta as Dictionary<Application.Storage.KeyType, Application.Storage.ValueType>) as Boolean {
        var seen = {} as Dictionary<String, Boolean>;
        var lists = [meta["ids"], meta["pending"]];
        for (var l = 0; l < lists.size(); l++) {
            var list = lists[l];
            if (!(list instanceof Array)) {
                return false;
            }
            for (var i = 0; i < list.size(); i++) {
                var id = list[i];
                if (!(id instanceof String) || !Entry.isValidId(id) || seen.hasKey(id)) {
                    return false;
                }
                seen[id] = true;
            }
        }
        return true;
    }
}