jetbase 0.7.0__py3-none-any.whl → 0.12.1__py3-none-any.whl
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.
- jetbase/cli/main.py +146 -4
- jetbase/commands/current.py +20 -0
- jetbase/commands/fix_checksums.py +172 -0
- jetbase/commands/fix_files.py +133 -0
- jetbase/commands/history.py +53 -0
- jetbase/commands/init.py +29 -0
- jetbase/commands/lock_status.py +25 -0
- jetbase/commands/new.py +65 -0
- jetbase/commands/rollback.py +172 -0
- jetbase/commands/status.py +212 -0
- jetbase/commands/unlock.py +29 -0
- jetbase/commands/upgrade.py +248 -0
- jetbase/commands/validators.py +37 -0
- jetbase/config.py +304 -25
- jetbase/constants.py +10 -2
- jetbase/database/connection.py +40 -0
- jetbase/database/queries/base.py +353 -0
- jetbase/database/queries/default_queries.py +215 -0
- jetbase/database/queries/postgres.py +14 -0
- jetbase/database/queries/query_loader.py +87 -0
- jetbase/database/queries/sqlite.py +197 -0
- jetbase/engine/checksum.py +25 -0
- jetbase/engine/dry_run.py +105 -0
- jetbase/engine/file_parser.py +324 -0
- jetbase/engine/formatters.py +61 -0
- jetbase/engine/lock.py +65 -0
- jetbase/engine/repeatable.py +125 -0
- jetbase/engine/validation.py +238 -0
- jetbase/engine/version.py +144 -0
- jetbase/enums.py +37 -1
- jetbase/exceptions.py +87 -0
- jetbase/models.py +45 -0
- jetbase/repositories/lock_repo.py +129 -0
- jetbase/repositories/migrations_repo.py +451 -0
- jetbase-0.12.1.dist-info/METADATA +135 -0
- jetbase-0.12.1.dist-info/RECORD +39 -0
- {jetbase-0.7.0.dist-info → jetbase-0.12.1.dist-info}/WHEEL +1 -1
- jetbase/core/dry_run.py +0 -38
- jetbase/core/file_parser.py +0 -199
- jetbase/core/initialize.py +0 -33
- jetbase/core/repository.py +0 -169
- jetbase/core/rollback.py +0 -67
- jetbase/core/upgrade.py +0 -75
- jetbase/core/version.py +0 -163
- jetbase/queries.py +0 -72
- jetbase-0.7.0.dist-info/METADATA +0 -12
- jetbase-0.7.0.dist-info/RECORD +0 -17
- {jetbase-0.7.0.dist-info → jetbase-0.12.1.dist-info}/entry_points.txt +0 -0
- {jetbase-0.7.0.dist-info → jetbase-0.12.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from jetbase.engine.dry_run import process_dry_run
|
|
4
|
+
from jetbase.engine.file_parser import parse_rollback_statements
|
|
5
|
+
from jetbase.engine.lock import (
|
|
6
|
+
migration_lock,
|
|
7
|
+
)
|
|
8
|
+
from jetbase.engine.version import get_migration_filepaths_by_version
|
|
9
|
+
from jetbase.enums import MigrationDirectionType
|
|
10
|
+
from jetbase.exceptions import VersionNotFoundError
|
|
11
|
+
from jetbase.repositories.lock_repo import create_lock_table_if_not_exists
|
|
12
|
+
from jetbase.repositories.migrations_repo import (
|
|
13
|
+
create_migrations_table_if_not_exists,
|
|
14
|
+
get_latest_versions,
|
|
15
|
+
run_migration,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def rollback_cmd(
|
|
20
|
+
count: int | None = None, to_version: str | None = None, dry_run: bool = False
|
|
21
|
+
) -> None:
|
|
22
|
+
"""
|
|
23
|
+
Rollback applied migrations.
|
|
24
|
+
|
|
25
|
+
Reverts previously applied migrations by executing their rollback SQL
|
|
26
|
+
statements in reverse order. Can rollback a specific number of migrations
|
|
27
|
+
or all migrations after a specified version.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
count (int | None): Number of migrations to rollback. If None and
|
|
31
|
+
to_version is also None, defaults to 1. Defaults to None.
|
|
32
|
+
to_version (str | None): Rollback all migrations applied after this
|
|
33
|
+
version. Cannot be used with count. Defaults to None.
|
|
34
|
+
dry_run (bool): If True, shows a preview of the rollback SQL without
|
|
35
|
+
executing it. Defaults to False.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
None: Prints rollback status for each migration to stdout.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
ValueError: If both count and to_version are specified.
|
|
42
|
+
VersionNotFoundError: If a required migration file is missing.
|
|
43
|
+
"""
|
|
44
|
+
create_migrations_table_if_not_exists()
|
|
45
|
+
create_lock_table_if_not_exists()
|
|
46
|
+
|
|
47
|
+
if count is not None and to_version is not None:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
"Cannot specify both 'count' and 'to_version' for rollback. "
|
|
50
|
+
"Select only one, or do not specify either to rollback the last migration."
|
|
51
|
+
)
|
|
52
|
+
if count is None and to_version is None:
|
|
53
|
+
count = 1
|
|
54
|
+
|
|
55
|
+
latest_migration_versions: list[str] = _get_latest_migration_versions(
|
|
56
|
+
count=count, to_version=to_version
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if not latest_migration_versions:
|
|
60
|
+
print("Nothing to rollback.")
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
versions_to_rollback: dict[str, str] = _get_versions_to_rollback(
|
|
64
|
+
latest_migration_versions=latest_migration_versions
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
_validate_rollback_files_exist(
|
|
68
|
+
latest_migration_versions=latest_migration_versions,
|
|
69
|
+
versions_to_rollback=versions_to_rollback,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if not dry_run:
|
|
73
|
+
with migration_lock():
|
|
74
|
+
print("Starting rollback...")
|
|
75
|
+
for version, file_path in versions_to_rollback.items():
|
|
76
|
+
sql_statements: list[str] = parse_rollback_statements(
|
|
77
|
+
file_path=file_path
|
|
78
|
+
)
|
|
79
|
+
filename: str = os.path.basename(file_path)
|
|
80
|
+
|
|
81
|
+
run_migration(
|
|
82
|
+
sql_statements=sql_statements,
|
|
83
|
+
version=version,
|
|
84
|
+
migration_operation=MigrationDirectionType.ROLLBACK,
|
|
85
|
+
filename=filename,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
print(f"Rollback applied successfully: {filename}")
|
|
89
|
+
print("Rollbacks completed successfully.")
|
|
90
|
+
|
|
91
|
+
else:
|
|
92
|
+
process_dry_run(
|
|
93
|
+
version_to_filepath=versions_to_rollback,
|
|
94
|
+
migration_operation=MigrationDirectionType.ROLLBACK,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _get_latest_migration_versions(
|
|
99
|
+
count: int | None = None, to_version: str | None = None
|
|
100
|
+
) -> list[str]:
|
|
101
|
+
"""
|
|
102
|
+
Get the latest migration versions from the database.
|
|
103
|
+
|
|
104
|
+
Retrieves migration versions either by count or by starting version.
|
|
105
|
+
If neither is specified, returns the single most recent migration.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
count (int | None): Number of migrations to retrieve.
|
|
109
|
+
Defaults to None.
|
|
110
|
+
to_version (str | None): Retrieve all migrations applied after
|
|
111
|
+
this version. Defaults to None.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
list[str]: List of version strings in order of application.
|
|
115
|
+
"""
|
|
116
|
+
if count:
|
|
117
|
+
return get_latest_versions(limit=count)
|
|
118
|
+
elif to_version:
|
|
119
|
+
return get_latest_versions(starting_version=to_version)
|
|
120
|
+
else:
|
|
121
|
+
return get_latest_versions(limit=1)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _get_versions_to_rollback(latest_migration_versions: list[str]) -> dict[str, str]:
|
|
125
|
+
"""
|
|
126
|
+
Get migration file paths for versions to rollback.
|
|
127
|
+
|
|
128
|
+
Maps version strings to their corresponding file paths, ordered
|
|
129
|
+
in reverse (newest first) for rollback execution.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
latest_migration_versions (list[str]): List of version strings
|
|
133
|
+
to rollback, ordered oldest to newest.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
dict[str, str]: Mapping of version to file path, reversed so
|
|
137
|
+
newest versions are rolled back first.
|
|
138
|
+
"""
|
|
139
|
+
versions_to_rollback: dict[str, str] = get_migration_filepaths_by_version(
|
|
140
|
+
directory=os.path.join(os.getcwd(), "migrations"),
|
|
141
|
+
version_to_start_from=latest_migration_versions[-1],
|
|
142
|
+
end_version=latest_migration_versions[0],
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return dict(reversed(versions_to_rollback.items()))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _validate_rollback_files_exist(
|
|
149
|
+
latest_migration_versions: list[str], versions_to_rollback: dict[str, str]
|
|
150
|
+
) -> None:
|
|
151
|
+
"""
|
|
152
|
+
Validate that all migration files exist for rollback.
|
|
153
|
+
|
|
154
|
+
Ensures every version that needs to be rolled back has a corresponding
|
|
155
|
+
migration file in the migrations directory.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
latest_migration_versions (list[str]): Version strings that need
|
|
159
|
+
to be rolled back.
|
|
160
|
+
versions_to_rollback (dict[str, str]): Mapping of version to
|
|
161
|
+
file path for available migration files.
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
VersionNotFoundError: If any migration file is missing.
|
|
165
|
+
"""
|
|
166
|
+
for version in latest_migration_versions:
|
|
167
|
+
if version not in list(versions_to_rollback.keys()):
|
|
168
|
+
raise VersionNotFoundError(
|
|
169
|
+
f"Migration file for version '{version}' not found. Cannot proceed with rollback.\n"
|
|
170
|
+
"Please restore the missing migration file and try again, or run 'jetbase fix' "
|
|
171
|
+
"to synchronize the migrations table with existing files before retrying the rollback."
|
|
172
|
+
)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
|
|
6
|
+
from jetbase.engine.file_parser import get_description_from_filename
|
|
7
|
+
from jetbase.engine.formatters import get_display_version
|
|
8
|
+
from jetbase.engine.repeatable import get_ra_filenames, get_runs_on_change_filepaths
|
|
9
|
+
from jetbase.engine.version import get_migration_filepaths_by_version
|
|
10
|
+
from jetbase.enums import MigrationType
|
|
11
|
+
from jetbase.models import MigrationRecord
|
|
12
|
+
from jetbase.repositories.migrations_repo import (
|
|
13
|
+
create_migrations_table_if_not_exists,
|
|
14
|
+
get_existing_on_change_filenames_to_checksums,
|
|
15
|
+
get_migration_records,
|
|
16
|
+
migrations_table_exists,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def status_cmd() -> None:
|
|
21
|
+
"""
|
|
22
|
+
Display applied and pending migrations.
|
|
23
|
+
|
|
24
|
+
Shows two tables: one listing all migrations that have been applied to
|
|
25
|
+
the database, and another listing pending migrations that are available
|
|
26
|
+
in the migrations directory but have not yet been applied.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
None: Prints formatted tables to stdout showing migration status.
|
|
30
|
+
"""
|
|
31
|
+
is_migrations_table: bool = migrations_table_exists()
|
|
32
|
+
if not is_migrations_table:
|
|
33
|
+
create_migrations_table_if_not_exists()
|
|
34
|
+
is_migrations_table = True
|
|
35
|
+
|
|
36
|
+
migration_records: list[MigrationRecord] = (
|
|
37
|
+
get_migration_records() if is_migrations_table else []
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
versioned_migration_records: list[MigrationRecord] = [
|
|
41
|
+
record
|
|
42
|
+
for record in migration_records
|
|
43
|
+
if record.migration_type == MigrationType.VERSIONED.value
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
latest_migrated_version: str | None = (
|
|
47
|
+
versioned_migration_records[-1].version if versioned_migration_records else None
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
pending_versioned_filepaths: dict[str, str] = get_migration_filepaths_by_version(
|
|
51
|
+
directory=os.path.join(os.getcwd(), "migrations"),
|
|
52
|
+
version_to_start_from=latest_migrated_version,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if latest_migrated_version:
|
|
56
|
+
pending_versioned_filepaths = dict(
|
|
57
|
+
list(pending_versioned_filepaths.items())[1:]
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
all_roc_filenames: list[str] = get_ra_filenames()
|
|
61
|
+
|
|
62
|
+
roc_filenames_changed_only: list[str] = [
|
|
63
|
+
os.path.basename(filepath)
|
|
64
|
+
for filepath in get_runs_on_change_filepaths(
|
|
65
|
+
directory=os.path.join(os.getcwd(), "migrations"), changed_only=True
|
|
66
|
+
)
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
roc_filenames_migrated: list[str] = list(
|
|
70
|
+
get_existing_on_change_filenames_to_checksums().keys()
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
all_roc_filenames: list[str] = [
|
|
74
|
+
os.path.basename(filepath)
|
|
75
|
+
for filepath in get_runs_on_change_filepaths(
|
|
76
|
+
directory=os.path.join(os.getcwd(), "migrations")
|
|
77
|
+
)
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
console = Console()
|
|
81
|
+
|
|
82
|
+
applied_table: Table = _create_migrations_display_table(title="Migrations Applied")
|
|
83
|
+
|
|
84
|
+
_add_applied_rows(table=applied_table, migration_records=migration_records)
|
|
85
|
+
|
|
86
|
+
console.print(applied_table)
|
|
87
|
+
console.print()
|
|
88
|
+
|
|
89
|
+
pending_table: Table = _create_migrations_display_table(title="Migrations Pending")
|
|
90
|
+
|
|
91
|
+
_add_pending_rows(
|
|
92
|
+
table=pending_table,
|
|
93
|
+
pending_versioned_filepaths=pending_versioned_filepaths,
|
|
94
|
+
migration_records=migration_records,
|
|
95
|
+
roc_filenames_changed_only=roc_filenames_changed_only,
|
|
96
|
+
all_roc_filenames=all_roc_filenames,
|
|
97
|
+
roc_filenames_migrated=roc_filenames_migrated,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
console.print(pending_table)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _create_migrations_display_table(title: str) -> Table:
|
|
104
|
+
"""
|
|
105
|
+
Create a rich Table for displaying migrations.
|
|
106
|
+
|
|
107
|
+
Configures a table with styled columns for displaying migration
|
|
108
|
+
version numbers and descriptions.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
title (str): The title to display above the table.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Table: A configured rich Table with Version and Description columns.
|
|
115
|
+
"""
|
|
116
|
+
display_table: Table = Table(
|
|
117
|
+
title=title, show_header=True, header_style="bold magenta"
|
|
118
|
+
)
|
|
119
|
+
display_table.add_column("Version", style="cyan")
|
|
120
|
+
display_table.add_column("Description", style="green")
|
|
121
|
+
|
|
122
|
+
return display_table
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _add_applied_rows(table: Table, migration_records: list[MigrationRecord]) -> None:
|
|
126
|
+
"""
|
|
127
|
+
Add applied migration rows to the table.
|
|
128
|
+
|
|
129
|
+
Populates the table with rows for each applied migration, displaying
|
|
130
|
+
the version number and description for both versioned and repeatable
|
|
131
|
+
migrations.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
table (Table): The rich Table to add rows to.
|
|
135
|
+
migration_records (list[MigrationRecord]): List of migration records
|
|
136
|
+
that have been applied to the database.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
None: Modifies the table in place.
|
|
140
|
+
"""
|
|
141
|
+
for record in migration_records:
|
|
142
|
+
if record.migration_type == MigrationType.VERSIONED.value:
|
|
143
|
+
table.add_row(record.version, record.description)
|
|
144
|
+
else:
|
|
145
|
+
table.add_row(
|
|
146
|
+
get_display_version(migration_type=record.migration_type),
|
|
147
|
+
record.description,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _add_pending_rows(
|
|
152
|
+
table: Table,
|
|
153
|
+
pending_versioned_filepaths: dict[str, str],
|
|
154
|
+
migration_records: list[MigrationRecord],
|
|
155
|
+
roc_filenames_changed_only: list[str],
|
|
156
|
+
all_roc_filenames: list[str],
|
|
157
|
+
roc_filenames_migrated: list[str],
|
|
158
|
+
) -> None:
|
|
159
|
+
"""
|
|
160
|
+
Add pending migration rows to the table.
|
|
161
|
+
|
|
162
|
+
Populates the table with rows for each pending migration including
|
|
163
|
+
versioned migrations, runs-always migrations, and runs-on-change
|
|
164
|
+
migrations that have been modified or not yet applied.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
table (Table): The rich Table to add rows to.
|
|
168
|
+
pending_versioned_filepaths (dict[str, str]): Mapping of version
|
|
169
|
+
strings to file paths for pending versioned migrations.
|
|
170
|
+
migration_records (list[MigrationRecord]): All migration records
|
|
171
|
+
from the database.
|
|
172
|
+
roc_filenames_changed_only (list[str]): Filenames of runs-on-change
|
|
173
|
+
migrations that have been modified since last applied.
|
|
174
|
+
all_roc_filenames (list[str]): All runs-on-change migration filenames.
|
|
175
|
+
roc_filenames_migrated (list[str]): Runs-on-change migrations that
|
|
176
|
+
have been previously applied.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
None: Modifies the table in place.
|
|
180
|
+
"""
|
|
181
|
+
for version, filepath in pending_versioned_filepaths.items():
|
|
182
|
+
table.add_row(
|
|
183
|
+
version, get_description_from_filename(filename=os.path.basename(filepath))
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Runs always
|
|
187
|
+
for ra_filename in get_ra_filenames():
|
|
188
|
+
description: str = get_description_from_filename(filename=ra_filename)
|
|
189
|
+
table.add_row(
|
|
190
|
+
get_display_version(migration_type=MigrationType.RUNS_ALWAYS.value),
|
|
191
|
+
description,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Runs on change - changed only
|
|
195
|
+
for record in migration_records:
|
|
196
|
+
if (
|
|
197
|
+
record.migration_type == MigrationType.RUNS_ON_CHANGE.value
|
|
198
|
+
and record.filename in roc_filenames_changed_only
|
|
199
|
+
):
|
|
200
|
+
table.add_row(
|
|
201
|
+
get_display_version(migration_type=MigrationType.RUNS_ON_CHANGE.value),
|
|
202
|
+
record.description,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Runs on change - new
|
|
206
|
+
for filename in all_roc_filenames:
|
|
207
|
+
if filename not in roc_filenames_migrated:
|
|
208
|
+
description: str = get_description_from_filename(filename=filename)
|
|
209
|
+
table.add_row(
|
|
210
|
+
get_display_version(migration_type=MigrationType.RUNS_ON_CHANGE.value),
|
|
211
|
+
description,
|
|
212
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from jetbase.config import get_config
|
|
2
|
+
from jetbase.repositories.lock_repo import (
|
|
3
|
+
lock_table_exists,
|
|
4
|
+
unlock_database,
|
|
5
|
+
)
|
|
6
|
+
from jetbase.repositories.migrations_repo import migrations_table_exists
|
|
7
|
+
|
|
8
|
+
sqlalchemy_url: str = get_config(required={"sqlalchemy_url"}).sqlalchemy_url
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def unlock_cmd() -> None:
|
|
12
|
+
"""
|
|
13
|
+
Force release the migration lock.
|
|
14
|
+
|
|
15
|
+
Unconditionally releases the migration lock in the jetbase_lock table.
|
|
16
|
+
Use this only if you are certain that no migration is currently running,
|
|
17
|
+
as unlocking during an active migration can cause database corruption.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
None: Prints "Unlock successful." to stdout.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
if not lock_table_exists() or not migrations_table_exists():
|
|
24
|
+
print("Unlock successful.")
|
|
25
|
+
return
|
|
26
|
+
#
|
|
27
|
+
unlock_database()
|
|
28
|
+
|
|
29
|
+
print("Unlock successful.")
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from jetbase.constants import MIGRATIONS_DIR
|
|
4
|
+
from jetbase.engine.dry_run import process_dry_run
|
|
5
|
+
from jetbase.engine.file_parser import parse_upgrade_statements
|
|
6
|
+
from jetbase.engine.lock import migration_lock
|
|
7
|
+
from jetbase.engine.repeatable import (
|
|
8
|
+
get_repeatable_always_filepaths,
|
|
9
|
+
get_runs_on_change_filepaths,
|
|
10
|
+
)
|
|
11
|
+
from jetbase.engine.validation import run_migration_validations
|
|
12
|
+
from jetbase.engine.version import (
|
|
13
|
+
get_migration_filepaths_by_version,
|
|
14
|
+
)
|
|
15
|
+
from jetbase.enums import MigrationDirectionType, MigrationType
|
|
16
|
+
from jetbase.models import MigrationRecord
|
|
17
|
+
from jetbase.repositories.lock_repo import create_lock_table_if_not_exists
|
|
18
|
+
from jetbase.repositories.migrations_repo import (
|
|
19
|
+
create_migrations_table_if_not_exists,
|
|
20
|
+
fetch_latest_versioned_migration,
|
|
21
|
+
get_existing_on_change_filenames_to_checksums,
|
|
22
|
+
get_existing_repeatable_always_migration_filenames,
|
|
23
|
+
run_migration,
|
|
24
|
+
run_update_repeatable_migration,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def upgrade_cmd(
|
|
29
|
+
count: int | None = None,
|
|
30
|
+
to_version: str | None = None,
|
|
31
|
+
dry_run: bool = False,
|
|
32
|
+
skip_validation: bool = False,
|
|
33
|
+
skip_checksum_validation: bool = False,
|
|
34
|
+
skip_file_validation: bool = False,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Apply pending migrations to the database in order.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
count (int | None): Maximum number of migrations to apply.
|
|
41
|
+
to_version (str | None): Apply migrations up to this version.
|
|
42
|
+
dry_run (bool): Preview SQL without executing.
|
|
43
|
+
skip_validation (bool): Skip all validations.
|
|
44
|
+
skip_checksum_validation (bool): Skip checksum validation only.
|
|
45
|
+
skip_file_validation (bool): Skip file validation only.
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
ValueError: If both count and to_version are specified.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
if count is not None and to_version is not None:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
"Cannot specify both 'count' and 'to_version' for upgrade. "
|
|
54
|
+
"Select only one, or do not specify either to run all pending migrations."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if count:
|
|
58
|
+
if count < 1 or not isinstance(count, int):
|
|
59
|
+
raise ValueError("'count' must be a positive integer.")
|
|
60
|
+
|
|
61
|
+
create_migrations_table_if_not_exists()
|
|
62
|
+
create_lock_table_if_not_exists()
|
|
63
|
+
|
|
64
|
+
latest_migration: MigrationRecord | None = fetch_latest_versioned_migration()
|
|
65
|
+
|
|
66
|
+
if latest_migration:
|
|
67
|
+
run_migration_validations(
|
|
68
|
+
latest_migrated_version=latest_migration.version,
|
|
69
|
+
skip_validation=skip_validation,
|
|
70
|
+
skip_checksum_validation=skip_checksum_validation,
|
|
71
|
+
skip_file_validation=skip_file_validation,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
filepaths_by_version: dict[str, str] = _get_filepaths_by_version(
|
|
75
|
+
latest_migration=latest_migration,
|
|
76
|
+
count=count,
|
|
77
|
+
to_version=to_version,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
repeatable_always_filepaths: list[str] = get_repeatable_always_filepaths(
|
|
81
|
+
directory=os.path.join(os.getcwd(), MIGRATIONS_DIR)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
runs_on_change_filepaths: list[str] = get_runs_on_change_filepaths(
|
|
85
|
+
directory=os.path.join(os.getcwd(), MIGRATIONS_DIR),
|
|
86
|
+
changed_only=True,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if not dry_run:
|
|
90
|
+
if (
|
|
91
|
+
not filepaths_by_version
|
|
92
|
+
and not repeatable_always_filepaths
|
|
93
|
+
and not runs_on_change_filepaths
|
|
94
|
+
):
|
|
95
|
+
print("Migrations are up to date.")
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
with migration_lock():
|
|
99
|
+
print("Starting migrations...")
|
|
100
|
+
|
|
101
|
+
_run_versioned_migrations(filepaths_by_version=filepaths_by_version)
|
|
102
|
+
|
|
103
|
+
_run_repeatable_always_migrations(
|
|
104
|
+
repeatable_always_filepaths=repeatable_always_filepaths
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
_run_repeatable_on_change_migrations(
|
|
108
|
+
runs_on_change_filepaths=runs_on_change_filepaths
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
print("Migrations completed successfully.")
|
|
112
|
+
else:
|
|
113
|
+
process_dry_run(
|
|
114
|
+
version_to_filepath=filepaths_by_version,
|
|
115
|
+
migration_operation=MigrationDirectionType.UPGRADE,
|
|
116
|
+
repeatable_always_filepaths=repeatable_always_filepaths,
|
|
117
|
+
runs_on_change_filepaths=runs_on_change_filepaths,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _get_filepaths_by_version(
|
|
122
|
+
latest_migration: MigrationRecord | None,
|
|
123
|
+
count: int | None = None,
|
|
124
|
+
to_version: str | None = None,
|
|
125
|
+
) -> dict[str, str]:
|
|
126
|
+
"""
|
|
127
|
+
Get pending migration file paths filtered by count or target version.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
latest_migration (MigrationRecord | None): The most recently
|
|
131
|
+
applied migration, or None if no migrations applied.
|
|
132
|
+
count (int | None): Limit to this many migrations. Defaults to None.
|
|
133
|
+
to_version (str | None): Include migrations up to this version.
|
|
134
|
+
Defaults to None.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
dict[str, str]: Mapping of version to file path for pending migrations.
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
FileNotFoundError: If to_version is not found in pending migrations.
|
|
141
|
+
"""
|
|
142
|
+
filepaths_by_version: dict[str, str] = get_migration_filepaths_by_version(
|
|
143
|
+
directory=os.path.join(os.getcwd(), MIGRATIONS_DIR),
|
|
144
|
+
version_to_start_from=latest_migration.version if latest_migration else None,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if latest_migration:
|
|
148
|
+
filepaths_by_version = dict(list(filepaths_by_version.items())[1:])
|
|
149
|
+
|
|
150
|
+
if count:
|
|
151
|
+
filepaths_by_version = dict(list(filepaths_by_version.items())[:count])
|
|
152
|
+
elif to_version:
|
|
153
|
+
if filepaths_by_version.get(to_version) is None:
|
|
154
|
+
raise FileNotFoundError(
|
|
155
|
+
f"The specified to_version '{to_version}' does not exist among pending migrations."
|
|
156
|
+
)
|
|
157
|
+
seen_versions: dict[str, str] = {}
|
|
158
|
+
for file_version, file_path in filepaths_by_version.items():
|
|
159
|
+
seen_versions[file_version] = file_path
|
|
160
|
+
if file_version == to_version:
|
|
161
|
+
break
|
|
162
|
+
filepaths_by_version = seen_versions
|
|
163
|
+
|
|
164
|
+
return filepaths_by_version
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _run_versioned_migrations(filepaths_by_version: dict[str, str]) -> None:
|
|
168
|
+
"""
|
|
169
|
+
Execute versioned (V__) migrations.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
filepaths_by_version (dict[str, str]): Mapping of version to file path.
|
|
173
|
+
"""
|
|
174
|
+
for version, file_path in filepaths_by_version.items():
|
|
175
|
+
sql_statements: list[str] = parse_upgrade_statements(file_path=file_path)
|
|
176
|
+
filename: str = os.path.basename(file_path)
|
|
177
|
+
|
|
178
|
+
run_migration(
|
|
179
|
+
sql_statements=sql_statements,
|
|
180
|
+
version=version,
|
|
181
|
+
migration_operation=MigrationDirectionType.UPGRADE,
|
|
182
|
+
filename=filename,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
print(f"Migration applied successfully: {filename}")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _run_repeatable_always_migrations(
|
|
189
|
+
repeatable_always_filepaths: list[str],
|
|
190
|
+
) -> None:
|
|
191
|
+
"""
|
|
192
|
+
Execute runs-always (RA__) migrations.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
repeatable_always_filepaths (list[str]): List of RA__ file paths.
|
|
196
|
+
"""
|
|
197
|
+
if repeatable_always_filepaths:
|
|
198
|
+
for filepath in repeatable_always_filepaths:
|
|
199
|
+
sql_statements: list[str] = parse_upgrade_statements(file_path=filepath)
|
|
200
|
+
filename: str = os.path.basename(filepath)
|
|
201
|
+
|
|
202
|
+
if filename in get_existing_repeatable_always_migration_filenames():
|
|
203
|
+
run_update_repeatable_migration(
|
|
204
|
+
sql_statements=sql_statements,
|
|
205
|
+
filename=filename,
|
|
206
|
+
migration_type=MigrationType.RUNS_ALWAYS,
|
|
207
|
+
)
|
|
208
|
+
print(f"Migration applied successfully: {filename}")
|
|
209
|
+
else:
|
|
210
|
+
run_migration(
|
|
211
|
+
sql_statements=sql_statements,
|
|
212
|
+
version=None,
|
|
213
|
+
migration_operation=MigrationDirectionType.UPGRADE,
|
|
214
|
+
filename=filename,
|
|
215
|
+
migration_type=MigrationType.RUNS_ALWAYS,
|
|
216
|
+
)
|
|
217
|
+
print(f"Migration applied successfully: {filename}")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _run_repeatable_on_change_migrations(runs_on_change_filepaths: list[str]) -> None:
|
|
221
|
+
"""
|
|
222
|
+
Execute runs-on-change (ROC__) migrations.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
runs_on_change_filepaths (list[str]): List of ROC__ file paths.
|
|
226
|
+
"""
|
|
227
|
+
if runs_on_change_filepaths:
|
|
228
|
+
for filepath in runs_on_change_filepaths:
|
|
229
|
+
sql_statements: list[str] = parse_upgrade_statements(file_path=filepath)
|
|
230
|
+
filename: str = os.path.basename(filepath)
|
|
231
|
+
|
|
232
|
+
if filename in list(get_existing_on_change_filenames_to_checksums().keys()):
|
|
233
|
+
# update migration
|
|
234
|
+
run_update_repeatable_migration(
|
|
235
|
+
sql_statements=sql_statements,
|
|
236
|
+
filename=filename,
|
|
237
|
+
migration_type=MigrationType.RUNS_ON_CHANGE,
|
|
238
|
+
)
|
|
239
|
+
print(f"Migration applied successfully: {filename}")
|
|
240
|
+
else:
|
|
241
|
+
run_migration(
|
|
242
|
+
sql_statements=sql_statements,
|
|
243
|
+
version=None,
|
|
244
|
+
migration_operation=MigrationDirectionType.UPGRADE,
|
|
245
|
+
filename=filename,
|
|
246
|
+
migration_type=MigrationType.RUNS_ON_CHANGE,
|
|
247
|
+
)
|
|
248
|
+
print(f"Migration applied successfully: {filename}")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from jetbase.exceptions import DirectoryNotFoundError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def validate_jetbase_directory() -> None:
|
|
7
|
+
"""
|
|
8
|
+
Ensure command is run from jetbase directory with migrations folder.
|
|
9
|
+
|
|
10
|
+
Validates that the current working directory is named 'jetbase' and
|
|
11
|
+
contains a 'migrations' subdirectory. This validation is required
|
|
12
|
+
before running most Jetbase CLI commands.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
None: Returns silently if validation passes.
|
|
16
|
+
|
|
17
|
+
Raises:
|
|
18
|
+
DirectoryNotFoundError: If the current directory is not named
|
|
19
|
+
'jetbase' or if the 'migrations' subdirectory does not exist.
|
|
20
|
+
"""
|
|
21
|
+
current_dir = Path.cwd()
|
|
22
|
+
|
|
23
|
+
# Check if current directory is named 'jetbase'
|
|
24
|
+
if current_dir.name != "jetbase":
|
|
25
|
+
raise DirectoryNotFoundError(
|
|
26
|
+
"Command must be run from the 'jetbase' directory.\n"
|
|
27
|
+
"You can run 'jetbase init' to create a Jetbase project."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Check if migrations directory exists
|
|
31
|
+
migrations_dir = current_dir / "migrations"
|
|
32
|
+
if not migrations_dir.exists() or not migrations_dir.is_dir():
|
|
33
|
+
raise DirectoryNotFoundError(
|
|
34
|
+
f"'migrations' directory not found in {current_dir}.\n"
|
|
35
|
+
"Add a migrations directory inside the 'jetbase' directory to proceed.\n"
|
|
36
|
+
"You can also run 'jetbase init' to create a Jetbase project."
|
|
37
|
+
)
|