schema_versions.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. #!/usr/bin/env python
  2. # Copyright 2023 The Matrix.org Foundation C.I.C.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. """A script to calculate which versions of Synapse have backwards-compatible
  16. database schemas. It creates a Markdown table of Synapse versions and the earliest
  17. compatible version.
  18. It is compatible with the mdbook protocol for preprocessors (see
  19. https://rust-lang.github.io/mdBook/for_developers/preprocessors.html#implementing-a-preprocessor-with-a-different-language):
  20. Exit 0 to denote support for all renderers:
  21. ./scripts-dev/schema_versions.py supports <mdbook renderer>
  22. Parse a JSON list from stdin and add the table to the proper documetnation page:
  23. ./scripts-dev/schema_versions.py
  24. Additionally, the script supports dumping the table to stdout for debugging:
  25. ./scripts-dev/schema_versions.py dump
  26. """
  27. import io
  28. import json
  29. import sys
  30. from collections import defaultdict
  31. from typing import Any, Dict, Iterator, Optional, Tuple
  32. import git
  33. from packaging import version
  34. # The schema version has moved around over the years.
  35. SCHEMA_VERSION_FILES = (
  36. "synapse/storage/schema/__init__.py",
  37. "synapse/storage/prepare_database.py",
  38. "synapse/storage/__init__.py",
  39. "synapse/app/homeserver.py",
  40. )
  41. # Skip versions of Synapse < v1.0, they're old and essentially not
  42. # compatible with today's federation.
  43. OLDEST_SHOWN_VERSION = version.parse("v1.0")
  44. def get_schema_versions(tag: git.Tag) -> Tuple[Optional[int], Optional[int]]:
  45. """Get the schema and schema compat versions for a tag."""
  46. schema_version = None
  47. schema_compat_version = None
  48. for file in SCHEMA_VERSION_FILES:
  49. try:
  50. schema_file = tag.commit.tree / file
  51. except KeyError:
  52. continue
  53. # We (usually) can't execute the code since it might have unknown imports.
  54. if file != "synapse/storage/schema/__init__.py":
  55. with io.BytesIO(schema_file.data_stream.read()) as f:
  56. for line in f.readlines():
  57. if line.startswith(b"SCHEMA_VERSION"):
  58. schema_version = int(line.split()[2])
  59. # Bail early.
  60. break
  61. else:
  62. # SCHEMA_COMPAT_VERSION is sometimes across multiple lines, the easist
  63. # thing to do is exec the code. Luckily it has only ever existed in
  64. # a file which imports nothing else from Synapse.
  65. locals: Dict[str, Any] = {}
  66. exec(schema_file.data_stream.read().decode("utf-8"), {}, locals)
  67. schema_version = locals["SCHEMA_VERSION"]
  68. schema_compat_version = locals.get("SCHEMA_COMPAT_VERSION")
  69. return schema_version, schema_compat_version
  70. def get_tags(repo: git.Repo) -> Iterator[git.Tag]:
  71. """Return an iterator of tags sorted by version."""
  72. tags = []
  73. for tag in repo.tags:
  74. # All "real" Synapse tags are of the form vX.Y.Z.
  75. if not tag.name.startswith("v"):
  76. continue
  77. # There's a weird tag from the initial react UI.
  78. if tag.name == "v0.1":
  79. continue
  80. try:
  81. tag_version = version.parse(tag.name)
  82. except version.InvalidVersion:
  83. # Skip invalid versions.
  84. continue
  85. # Skip pre- and post-release versions.
  86. if tag_version.is_prerelease or tag_version.is_postrelease or tag_version.local:
  87. continue
  88. # Skip old versions.
  89. if tag_version < OLDEST_SHOWN_VERSION:
  90. continue
  91. tags.append((tag_version, tag))
  92. # Sort based on the version number (not lexically).
  93. return (tag for _, tag in sorted(tags, key=lambda t: t[0]))
  94. def calculate_version_chart() -> str:
  95. repo = git.Repo(path=".")
  96. # Map of schema version -> Synapse versions which are at that schema version.
  97. schema_versions = defaultdict(list)
  98. # Map of schema version -> Synapse versions which are compatible with that
  99. # schema version.
  100. schema_compat_versions = defaultdict(list)
  101. # Find ranges of versions which are compatible with a schema version.
  102. #
  103. # There are two modes of operation:
  104. #
  105. # 1. Pre-schema_compat_version (i.e. schema_compat_version of None), then
  106. # Synapse is compatible up/downgrading to a version with
  107. # schema_version >= its current version.
  108. #
  109. # 2. Post-schema_compat_version (i.e. schema_compat_version is *not* None),
  110. # then Synapse is compatible up/downgrading to a version with
  111. # schema version >= schema_compat_version.
  112. #
  113. # This is more generous and avoids versions that cannot be rolled back.
  114. #
  115. # See https://github.com/matrix-org/synapse/pull/9933 which was included in v1.37.0.
  116. for tag in get_tags(repo):
  117. schema_version, schema_compat_version = get_schema_versions(tag)
  118. # If a schema compat version is given, prefer that over the schema version.
  119. schema_versions[schema_version].append(tag.name)
  120. schema_compat_versions[schema_compat_version or schema_version].append(tag.name)
  121. # Generate a table which maps the latest Synapse version compatible with each
  122. # schema version.
  123. result = f"| {'Versions': ^19} | Compatible version |\n"
  124. result += f"|{'-' * (19 + 2)}|{'-' * (18 + 2)}|\n"
  125. for schema_version, synapse_versions in schema_compat_versions.items():
  126. result += f"| {synapse_versions[0] + ' – ' + synapse_versions[-1]: ^19} | {schema_versions[schema_version][0]: ^18} |\n"
  127. return result
  128. if __name__ == "__main__":
  129. if len(sys.argv) == 3 and sys.argv[1] == "supports":
  130. # We don't care about the renderer which is being used, which is the second argument.
  131. sys.exit(0)
  132. elif len(sys.argv) == 2 and sys.argv[1] == "dump":
  133. print(calculate_version_chart())
  134. else:
  135. # Expect JSON data on stdin.
  136. context, book = json.load(sys.stdin)
  137. for section in book["sections"]:
  138. if "Chapter" in section and section["Chapter"]["path"] == "upgrade.md":
  139. section["Chapter"]["content"] = section["Chapter"]["content"].replace(
  140. "<!-- REPLACE_WITH_SCHEMA_VERSIONS -->", calculate_version_chart()
  141. )
  142. # Print the result back out to stdout.
  143. print(json.dumps(book))