csrd-migration 0.3.89__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,217 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ /env/
142
+ /venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # Ruff stuff:
192
+ .ruff_cache/
193
+
194
+ # PyPI configuration file
195
+ .pypirc
196
+
197
+ # Cursor
198
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
+ # refer to https://docs.cursor.com/context/ignore-files
201
+ .cursorignore
202
+ .cursorindexingignore
203
+
204
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
208
+
209
+ *.db
210
+
211
+
212
+ # Import linter cache
213
+ .import_linter_cache/
214
+
215
+ # IDE
216
+ .idea/
217
+ .idea/*
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: csrd-migration
3
+ Version: 0.3.89
4
+ Summary: Versioned schema migrations for csrd database adapters
5
+ Project-URL: Repository, https://github.com/csrd-api/fastapi-common
6
+ Project-URL: Documentation, https://github.com/csrd-api/fastapi-common/tree/main/packages/migration
7
+ Project-URL: Changelog, https://github.com/csrd-api/fastapi-common/blob/main/CHANGELOG.md
8
+ License: MIT
9
+ Requires-Python: >=3.12
10
+ Requires-Dist: csrd-repository
11
+ Description-Content-Type: text/markdown
12
+
13
+ # csrd-migration
14
+
15
+ Versioned, idempotent schema migrations for `csrd.repository` database adapters.
16
+
17
+ ## Quick start
18
+
19
+ ```python
20
+ from csrd.migration import Migration, MigrationRunner
21
+ from csrd.repository import SQLiteAdapter
22
+
23
+ MIGRATIONS = [
24
+ Migration(
25
+ version="001",
26
+ description="create users table",
27
+ up="CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT NOT NULL)",
28
+ down="DROP TABLE users",
29
+ ),
30
+ ]
31
+
32
+ async def setup_db():
33
+ adapter = SQLiteAdapter("app.db")
34
+ await adapter.connect()
35
+ runner = MigrationRunner()
36
+ await runner.apply_all(adapter, MIGRATIONS)
37
+ ```
38
+
39
+ ## API
40
+
41
+ ### `Migration`
42
+
43
+ Frozen dataclass representing a single schema change.
44
+
45
+ | Field | Type | Description |
46
+ |---|---|---|
47
+ | `version` | `str` | Unique version identifier (e.g. `"001"`, `"002"`) |
48
+ | `description` | `str` | Human-readable description |
49
+ | `up` | `str` | SQL to apply the migration |
50
+ | `down` | `str` | SQL to revert the migration |
51
+
52
+ ### `MigrationRunner`
53
+
54
+ Applies and tracks migrations against any `ABCDatabaseAdapter`.
55
+
56
+ | Method | Description |
57
+ |---|---|
58
+ | `apply_all(adapter, migrations)` | Apply all pending migrations in order |
59
+ | `current_version(adapter)` | Return the latest applied version, or `None` |
60
+ | `pending(adapter, migrations)` | Return migrations not yet applied |
61
+
62
+ The runner creates a `_csrd_migrations` table automatically to track
63
+ applied versions. Re-running is idempotent — already-applied migrations
64
+ are skipped.
65
+
66
+ ## Design
67
+
68
+ - **Adapter-agnostic** — works with `SQLiteAdapter`, `PGAdapter`, `MariaAdapter`
69
+ - **Idempotent** — safe to call on every startup
70
+ - **Ordered** — migrations are applied in list order
71
+ - **Future-ready** — the `Migration` dataclass is the stable interface for
72
+ a future model→SQL translation layer (see `docs/AUGMENT_PLAN.md`)
@@ -0,0 +1,60 @@
1
+ # csrd-migration
2
+
3
+ Versioned, idempotent schema migrations for `csrd.repository` database adapters.
4
+
5
+ ## Quick start
6
+
7
+ ```python
8
+ from csrd.migration import Migration, MigrationRunner
9
+ from csrd.repository import SQLiteAdapter
10
+
11
+ MIGRATIONS = [
12
+ Migration(
13
+ version="001",
14
+ description="create users table",
15
+ up="CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT NOT NULL)",
16
+ down="DROP TABLE users",
17
+ ),
18
+ ]
19
+
20
+ async def setup_db():
21
+ adapter = SQLiteAdapter("app.db")
22
+ await adapter.connect()
23
+ runner = MigrationRunner()
24
+ await runner.apply_all(adapter, MIGRATIONS)
25
+ ```
26
+
27
+ ## API
28
+
29
+ ### `Migration`
30
+
31
+ Frozen dataclass representing a single schema change.
32
+
33
+ | Field | Type | Description |
34
+ |---|---|---|
35
+ | `version` | `str` | Unique version identifier (e.g. `"001"`, `"002"`) |
36
+ | `description` | `str` | Human-readable description |
37
+ | `up` | `str` | SQL to apply the migration |
38
+ | `down` | `str` | SQL to revert the migration |
39
+
40
+ ### `MigrationRunner`
41
+
42
+ Applies and tracks migrations against any `ABCDatabaseAdapter`.
43
+
44
+ | Method | Description |
45
+ |---|---|
46
+ | `apply_all(adapter, migrations)` | Apply all pending migrations in order |
47
+ | `current_version(adapter)` | Return the latest applied version, or `None` |
48
+ | `pending(adapter, migrations)` | Return migrations not yet applied |
49
+
50
+ The runner creates a `_csrd_migrations` table automatically to track
51
+ applied versions. Re-running is idempotent — already-applied migrations
52
+ are skipped.
53
+
54
+ ## Design
55
+
56
+ - **Adapter-agnostic** — works with `SQLiteAdapter`, `PGAdapter`, `MariaAdapter`
57
+ - **Idempotent** — safe to call on every startup
58
+ - **Ordered** — migrations are applied in list order
59
+ - **Future-ready** — the `Migration` dataclass is the stable interface for
60
+ a future model→SQL translation layer (see `docs/AUGMENT_PLAN.md`)
@@ -0,0 +1,25 @@
1
+ [project]
2
+ name = "csrd-migration"
3
+ version = "0.3.89"
4
+ description = "Versioned schema migrations for csrd database adapters"
5
+ license = { text = "MIT" }
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "csrd-repository",
9
+ ]
10
+ readme = "README.md"
11
+
12
+ [tool.uv.sources]
13
+ csrd-repository = { workspace = true }
14
+
15
+ [project.urls]
16
+ Repository = "https://github.com/csrd-api/fastapi-common"
17
+ Documentation = "https://github.com/csrd-api/fastapi-common/tree/main/packages/migration"
18
+ Changelog = "https://github.com/csrd-api/fastapi-common/blob/main/CHANGELOG.md"
19
+
20
+ [build-system]
21
+ requires = ["hatchling"]
22
+ build-backend = "hatchling.build"
23
+
24
+ [tool.hatch.build.targets.wheel]
25
+ packages = ["src/csrd"]
@@ -0,0 +1,11 @@
1
+ """Versioned schema migrations for csrd database adapters.
2
+
3
+ Public API::
4
+
5
+ from csrd.migration import Migration, MigrationRunner
6
+ """
7
+
8
+ from ._migration import Migration
9
+ from ._runner import MigrationRunner
10
+
11
+ __all__ = ("Migration", "MigrationRunner")
@@ -0,0 +1,29 @@
1
+ """Frozen dataclass representing a single versioned schema change."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class Migration:
8
+ """A single versioned database schema change.
9
+
10
+ Migrations are applied in list order by :class:`MigrationRunner`.
11
+ Each migration must have a unique ``version`` string.
12
+
13
+ Parameters
14
+ ----------
15
+ version
16
+ Unique version identifier (e.g. ``"001"``, ``"002"``).
17
+ Compared lexicographically — use zero-padded numbers.
18
+ description
19
+ Human-readable description of the change.
20
+ up
21
+ SQL statement(s) to apply the migration.
22
+ down
23
+ SQL statement(s) to revert the migration.
24
+ """
25
+
26
+ version: str
27
+ description: str
28
+ up: str
29
+ down: str
@@ -0,0 +1,98 @@
1
+ """Migration runner — applies and tracks schema migrations."""
2
+
3
+ from csrd.repository import ABCDatabaseAdapter
4
+
5
+ from ._migration import Migration
6
+
7
+ _MIGRATIONS_TABLE = "_csrd_migrations"
8
+
9
+ _CREATE_TABLE_SQL = f"""
10
+ CREATE TABLE IF NOT EXISTS {_MIGRATIONS_TABLE} (
11
+ version TEXT PRIMARY KEY,
12
+ description TEXT NOT NULL,
13
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
14
+ )
15
+ """
16
+
17
+ # Postgres/MariaDB variant — uses CURRENT_TIMESTAMP instead of datetime('now')
18
+ _CREATE_TABLE_SQL_STANDARD = f"""
19
+ CREATE TABLE IF NOT EXISTS {_MIGRATIONS_TABLE} (
20
+ version VARCHAR(255) PRIMARY KEY,
21
+ description VARCHAR(1024) NOT NULL,
22
+ applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
23
+ )
24
+ """
25
+
26
+
27
+ class MigrationRunner:
28
+ """Apply and track versioned schema migrations.
29
+
30
+ The runner creates a ``_csrd_migrations`` tracking table automatically.
31
+ Already-applied migrations are skipped (idempotent).
32
+
33
+ Usage::
34
+
35
+ runner = MigrationRunner()
36
+ await runner.apply_all(adapter, migrations)
37
+ """
38
+
39
+ async def _ensure_table(self, adapter: ABCDatabaseAdapter) -> None:
40
+ """Create the migrations tracking table if it doesn't exist."""
41
+ # Try SQLite-style first; fall back to standard SQL on failure
42
+ try:
43
+ await adapter.execute(_CREATE_TABLE_SQL)
44
+ except Exception:
45
+ await adapter.execute(_CREATE_TABLE_SQL_STANDARD)
46
+
47
+ async def _applied_versions(self, adapter: ABCDatabaseAdapter) -> set[str]:
48
+ """Return the set of already-applied migration versions."""
49
+ rows = await adapter.fetch_all(f"SELECT version FROM {_MIGRATIONS_TABLE}")
50
+ return {row["version"] for row in rows}
51
+
52
+ async def apply_all(
53
+ self,
54
+ adapter: ABCDatabaseAdapter,
55
+ migrations: list[Migration],
56
+ ) -> list[Migration]:
57
+ """Apply all pending migrations in list order.
58
+
59
+ Returns the list of migrations that were actually applied
60
+ (empty if everything was already up-to-date).
61
+ """
62
+ await self._ensure_table(adapter)
63
+ applied = await self._applied_versions(adapter)
64
+
65
+ newly_applied: list[Migration] = []
66
+ for migration in migrations:
67
+ if migration.version in applied:
68
+ continue
69
+ await adapter.execute(migration.up)
70
+ await adapter.execute(
71
+ f"INSERT INTO {_MIGRATIONS_TABLE} (version, description) "
72
+ "VALUES (:version, :description)",
73
+ {"version": migration.version, "description": migration.description},
74
+ )
75
+ newly_applied.append(migration)
76
+
77
+ return newly_applied
78
+
79
+ async def current_version(
80
+ self,
81
+ adapter: ABCDatabaseAdapter,
82
+ ) -> str | None:
83
+ """Return the latest applied migration version, or ``None``."""
84
+ await self._ensure_table(adapter)
85
+ row = await adapter.fetch_one(
86
+ f"SELECT version FROM {_MIGRATIONS_TABLE} ORDER BY version DESC LIMIT 1"
87
+ )
88
+ return row["version"] if row else None
89
+
90
+ async def pending(
91
+ self,
92
+ adapter: ABCDatabaseAdapter,
93
+ migrations: list[Migration],
94
+ ) -> list[Migration]:
95
+ """Return migrations not yet applied, preserving list order."""
96
+ await self._ensure_table(adapter)
97
+ applied = await self._applied_versions(adapter)
98
+ return [m for m in migrations if m.version not in applied]
File without changes
@@ -0,0 +1,156 @@
1
+ """Tests for csrd.migration — Migration dataclass and MigrationRunner."""
2
+
3
+ import pytest
4
+
5
+ from csrd.migration import Migration, MigrationRunner
6
+ from csrd.repository import SQLiteAdapter
7
+
8
+ # ---------------------------------------------------------------------------
9
+ # Fixtures
10
+ # ---------------------------------------------------------------------------
11
+
12
+
13
+ @pytest.fixture
14
+ async def adapter(tmp_path):
15
+ """Provide a connected SQLiteAdapter backed by a temp file."""
16
+ db_path = str(tmp_path / "test.db")
17
+ adapter = SQLiteAdapter(db_path)
18
+ await adapter.connect()
19
+ yield adapter
20
+ await adapter.close()
21
+
22
+
23
+ MIGRATIONS = [
24
+ Migration(
25
+ version="001",
26
+ description="create users table",
27
+ up="CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT NOT NULL)",
28
+ down="DROP TABLE users",
29
+ ),
30
+ Migration(
31
+ version="002",
32
+ description="add email column",
33
+ up="ALTER TABLE users ADD COLUMN email TEXT",
34
+ down=(
35
+ "CREATE TABLE users_backup (id INTEGER PRIMARY KEY, username TEXT NOT NULL);"
36
+ "INSERT INTO users_backup SELECT id, username FROM users;"
37
+ "DROP TABLE users;"
38
+ "ALTER TABLE users_backup RENAME TO users;"
39
+ ),
40
+ ),
41
+ ]
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Migration dataclass
46
+ # ---------------------------------------------------------------------------
47
+
48
+
49
+ class TestMigration:
50
+ def test_frozen(self):
51
+ m = Migration(version="001", description="test", up="SELECT 1", down="SELECT 1")
52
+ with pytest.raises(AttributeError):
53
+ m.version = "002" # type: ignore[misc]
54
+
55
+ def test_fields(self):
56
+ m = Migration(version="001", description="test", up="UP", down="DOWN")
57
+ assert m.version == "001"
58
+ assert m.description == "test"
59
+ assert m.up == "UP"
60
+ assert m.down == "DOWN"
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # MigrationRunner
65
+ # ---------------------------------------------------------------------------
66
+
67
+
68
+ class TestMigrationRunner:
69
+ async def test_apply_all_creates_table_and_applies(self, adapter):
70
+ runner = MigrationRunner()
71
+ applied = await runner.apply_all(adapter, MIGRATIONS)
72
+
73
+ assert len(applied) == 2
74
+ assert applied[0].version == "001"
75
+ assert applied[1].version == "002"
76
+
77
+ # Verify the users table was actually created with both columns
78
+ row = await adapter.fetch_one(
79
+ "SELECT sql FROM sqlite_master WHERE type='table' AND name='users'"
80
+ )
81
+ assert row is not None
82
+ assert "email" in row["sql"]
83
+
84
+ async def test_apply_all_is_idempotent(self, adapter):
85
+ runner = MigrationRunner()
86
+ first = await runner.apply_all(adapter, MIGRATIONS)
87
+ second = await runner.apply_all(adapter, MIGRATIONS)
88
+
89
+ assert len(first) == 2
90
+ assert len(second) == 0 # nothing new to apply
91
+
92
+ async def test_apply_all_incremental(self, adapter):
93
+ runner = MigrationRunner()
94
+
95
+ # Apply only first migration
96
+ applied1 = await runner.apply_all(adapter, MIGRATIONS[:1])
97
+ assert len(applied1) == 1
98
+
99
+ # Now apply both — only second should be new
100
+ applied2 = await runner.apply_all(adapter, MIGRATIONS)
101
+ assert len(applied2) == 1
102
+ assert applied2[0].version == "002"
103
+
104
+ async def test_current_version_empty(self, adapter):
105
+ runner = MigrationRunner()
106
+ version = await runner.current_version(adapter)
107
+ assert version is None
108
+
109
+ async def test_current_version_after_apply(self, adapter):
110
+ runner = MigrationRunner()
111
+ await runner.apply_all(adapter, MIGRATIONS)
112
+ version = await runner.current_version(adapter)
113
+ assert version == "002"
114
+
115
+ async def test_current_version_partial(self, adapter):
116
+ runner = MigrationRunner()
117
+ await runner.apply_all(adapter, MIGRATIONS[:1])
118
+ version = await runner.current_version(adapter)
119
+ assert version == "001"
120
+
121
+ async def test_pending_all(self, adapter):
122
+ runner = MigrationRunner()
123
+ pending = await runner.pending(adapter, MIGRATIONS)
124
+ assert len(pending) == 2
125
+
126
+ async def test_pending_after_apply(self, adapter):
127
+ runner = MigrationRunner()
128
+ await runner.apply_all(adapter, MIGRATIONS[:1])
129
+ pending = await runner.pending(adapter, MIGRATIONS)
130
+ assert len(pending) == 1
131
+ assert pending[0].version == "002"
132
+
133
+ async def test_pending_none(self, adapter):
134
+ runner = MigrationRunner()
135
+ await runner.apply_all(adapter, MIGRATIONS)
136
+ pending = await runner.pending(adapter, MIGRATIONS)
137
+ assert len(pending) == 0
138
+
139
+ async def test_tracking_table_records(self, adapter):
140
+ runner = MigrationRunner()
141
+ await runner.apply_all(adapter, MIGRATIONS)
142
+
143
+ rows = await adapter.fetch_all(
144
+ "SELECT version, description FROM _csrd_migrations ORDER BY version"
145
+ )
146
+ assert len(rows) == 2
147
+ assert rows[0]["version"] == "001"
148
+ assert rows[0]["description"] == "create users table"
149
+ assert rows[1]["version"] == "002"
150
+ assert rows[1]["description"] == "add email column"
151
+
152
+ async def test_empty_migration_list(self, adapter):
153
+ runner = MigrationRunner()
154
+ applied = await runner.apply_all(adapter, [])
155
+ assert applied == []
156
+ assert await runner.current_version(adapter) is None