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
jetbase/exceptions.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
class DuplicateMigrationVersionError(Exception):
|
|
2
|
+
"""
|
|
3
|
+
Raised when multiple migration files share the same version number.
|
|
4
|
+
|
|
5
|
+
This typically occurs when two files have the same version prefix
|
|
6
|
+
(e.g., V1.0 and V1_0) which normalize to the same version.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InvalidMigrationFilenameError(Exception):
|
|
13
|
+
"""
|
|
14
|
+
Raised when a migration filename doesn't match the required format.
|
|
15
|
+
|
|
16
|
+
Valid formats are:
|
|
17
|
+
- V{version}__{description}.sql for versioned migrations
|
|
18
|
+
- RA__{description}.sql for runs-always migrations
|
|
19
|
+
- ROC__{description}.sql for runs-on-change migrations
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MigrationFilenameTooLongError(Exception):
|
|
26
|
+
"""
|
|
27
|
+
Raised when a migration filename exceeds the maximum length of 512 characters.
|
|
28
|
+
|
|
29
|
+
Long filenames can cause issues with some filesystems and databases.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class OutOfOrderMigrationError(Exception):
|
|
36
|
+
"""
|
|
37
|
+
Raised when a new migration file has a version lower than the latest applied.
|
|
38
|
+
|
|
39
|
+
New migrations must have version numbers higher than all previously
|
|
40
|
+
applied migrations to maintain a consistent migration order.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ChecksumMismatchError(Exception):
|
|
47
|
+
"""
|
|
48
|
+
Raised when a migration file's current checksum differs from the stored checksum.
|
|
49
|
+
|
|
50
|
+
This indicates that the migration file was modified after being applied
|
|
51
|
+
to the database, which can cause inconsistencies between environments.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class MigrationVersionMismatchError(Exception):
|
|
58
|
+
"""
|
|
59
|
+
Raised when migration file versions don't match the expected sequence.
|
|
60
|
+
|
|
61
|
+
This occurs during checksum repair when the order of migration files
|
|
62
|
+
doesn't match the order of applied migrations.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class VersionNotFoundError(Exception):
|
|
69
|
+
"""
|
|
70
|
+
Raised when a specified version doesn't exist or hasn't been applied.
|
|
71
|
+
|
|
72
|
+
This can occur when attempting to rollback to a version that is not
|
|
73
|
+
in the migration history.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class DirectoryNotFoundError(Exception):
|
|
80
|
+
"""
|
|
81
|
+
Raised when the required Jetbase directories do not exist.
|
|
82
|
+
|
|
83
|
+
This occurs when commands are run outside of a Jetbase project
|
|
84
|
+
or when the migrations directory is missing.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
pass
|
jetbase/models.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class MigrationRecord:
|
|
7
|
+
"""
|
|
8
|
+
Represents a single migration record from the jetbase_migrations table.
|
|
9
|
+
|
|
10
|
+
Attributes:
|
|
11
|
+
order_executed (int): The sequential order in which this migration
|
|
12
|
+
was executed.
|
|
13
|
+
version (str): The version string for versioned migrations, or
|
|
14
|
+
None for repeatable migrations.
|
|
15
|
+
description (str): Human-readable description extracted from
|
|
16
|
+
the migration filename.
|
|
17
|
+
filename (str): The original filename of the migration file.
|
|
18
|
+
migration_type (str): The type of migration ('VERSIONED',
|
|
19
|
+
'RUNS_ALWAYS', or 'RUNS_ON_CHANGE').
|
|
20
|
+
applied_at (dt.datetime): Timestamp when the migration was applied.
|
|
21
|
+
checksum (str): SHA256 checksum of the migration's SQL statements.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
order_executed: int
|
|
25
|
+
version: str
|
|
26
|
+
description: str
|
|
27
|
+
filename: str
|
|
28
|
+
migration_type: str
|
|
29
|
+
applied_at: dt.datetime
|
|
30
|
+
checksum: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class LockStatus:
|
|
35
|
+
"""
|
|
36
|
+
Represents the current state of the migration lock.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
is_locked (bool): True if migrations are currently locked.
|
|
40
|
+
locked_at (dt.datetime | None): Timestamp when the lock was acquired,
|
|
41
|
+
or None if not locked.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
is_locked: bool
|
|
45
|
+
locked_at: dt.datetime | None
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import Result, Row
|
|
4
|
+
from sqlalchemy.engine import CursorResult
|
|
5
|
+
|
|
6
|
+
from jetbase.database.connection import get_db_connection
|
|
7
|
+
from jetbase.database.queries.base import QueryMethod
|
|
8
|
+
from jetbase.database.queries.query_loader import get_query
|
|
9
|
+
from jetbase.models import LockStatus
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def lock_table_exists() -> bool:
|
|
13
|
+
"""
|
|
14
|
+
Check if the jetbase_lock table exists in the database.
|
|
15
|
+
|
|
16
|
+
Queries the database to determine if the lock table has been created.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
bool: True if the jetbase_lock table exists, False otherwise.
|
|
20
|
+
"""
|
|
21
|
+
with get_db_connection() as connection:
|
|
22
|
+
result: Result[tuple[bool]] = connection.execute(
|
|
23
|
+
statement=get_query(QueryMethod.CHECK_IF_LOCK_TABLE_EXISTS_QUERY)
|
|
24
|
+
)
|
|
25
|
+
table_exists: bool = result.scalar_one()
|
|
26
|
+
|
|
27
|
+
return table_exists
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def create_lock_table_if_not_exists() -> None:
|
|
31
|
+
"""
|
|
32
|
+
Create the jetbase_lock table if it doesn't already exist.
|
|
33
|
+
|
|
34
|
+
Creates the lock table and initializes it with a single unlocked
|
|
35
|
+
record. This table is used to prevent concurrent migrations.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
None: Table is created as a side effect.
|
|
39
|
+
"""
|
|
40
|
+
with get_db_connection() as connection:
|
|
41
|
+
connection.execute(get_query(query_name=QueryMethod.CREATE_LOCK_TABLE_STMT))
|
|
42
|
+
|
|
43
|
+
# Initialize with single row if empty
|
|
44
|
+
connection.execute(
|
|
45
|
+
get_query(query_name=QueryMethod.INITIALIZE_LOCK_RECORD_STMT)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def fetch_lock_status() -> LockStatus:
|
|
50
|
+
"""
|
|
51
|
+
Get the current migration lock status from the database.
|
|
52
|
+
|
|
53
|
+
Queries the jetbase_lock table to determine if migrations are
|
|
54
|
+
currently locked and when the lock was acquired.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
LockStatus: A dataclass containing is_locked (bool) and
|
|
58
|
+
locked_at (datetime | None) fields.
|
|
59
|
+
"""
|
|
60
|
+
with get_db_connection() as connection:
|
|
61
|
+
result: Row[Any] | None = connection.execute(
|
|
62
|
+
get_query(query_name=QueryMethod.CHECK_LOCK_STATUS_STMT)
|
|
63
|
+
).first()
|
|
64
|
+
if result:
|
|
65
|
+
return LockStatus(is_locked=result.is_locked, locked_at=result.locked_at)
|
|
66
|
+
return LockStatus(is_locked=False, locked_at=None)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def unlock_database() -> None:
|
|
70
|
+
"""
|
|
71
|
+
Force unlock the migration lock unconditionally.
|
|
72
|
+
|
|
73
|
+
Clears the lock status regardless of which process holds it.
|
|
74
|
+
Use with caution as this can cause issues if a migration is
|
|
75
|
+
actually running.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
None: Lock is released as a side effect.
|
|
79
|
+
"""
|
|
80
|
+
with get_db_connection() as connection:
|
|
81
|
+
connection.execute(get_query(query_name=QueryMethod.FORCE_UNLOCK_STMT))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def lock_database(process_id: str) -> CursorResult:
|
|
85
|
+
"""
|
|
86
|
+
Attempt to acquire the migration lock for a specific process.
|
|
87
|
+
|
|
88
|
+
Uses an atomic update to acquire the lock only if it is not
|
|
89
|
+
already held. The rowcount of the result indicates success.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
process_id (str): Unique identifier for the process acquiring
|
|
93
|
+
the lock.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
CursorResult: Result object where rowcount=1 indicates success,
|
|
97
|
+
rowcount=0 indicates the lock is already held.
|
|
98
|
+
"""
|
|
99
|
+
with get_db_connection() as connection:
|
|
100
|
+
result = connection.execute(
|
|
101
|
+
get_query(query_name=QueryMethod.ACQUIRE_LOCK_STMT),
|
|
102
|
+
{
|
|
103
|
+
"process_id": process_id,
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def release_lock(process_id: str) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Release the migration lock held by a specific process.
|
|
113
|
+
|
|
114
|
+
Only releases the lock if it is currently held by the specified
|
|
115
|
+
process ID.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
process_id (str): The unique identifier of the process that
|
|
119
|
+
acquired the lock.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
None: Lock is released as a side effect.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
with get_db_connection() as connection:
|
|
126
|
+
connection.execute(
|
|
127
|
+
get_query(query_name=QueryMethod.RELEASE_LOCK_STMT),
|
|
128
|
+
{"process_id": process_id},
|
|
129
|
+
)
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
from sqlalchemy import Result, Row, text
|
|
2
|
+
|
|
3
|
+
from jetbase.database.connection import get_db_connection
|
|
4
|
+
from jetbase.database.queries.base import QueryMethod
|
|
5
|
+
from jetbase.database.queries.query_loader import get_query
|
|
6
|
+
from jetbase.engine.checksum import calculate_checksum
|
|
7
|
+
from jetbase.engine.file_parser import (
|
|
8
|
+
get_description_from_filename,
|
|
9
|
+
)
|
|
10
|
+
from jetbase.enums import MigrationDirectionType, MigrationType
|
|
11
|
+
from jetbase.exceptions import VersionNotFoundError
|
|
12
|
+
from jetbase.models import MigrationRecord
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run_migration(
|
|
16
|
+
sql_statements: list[str],
|
|
17
|
+
version: str | None,
|
|
18
|
+
migration_operation: MigrationDirectionType,
|
|
19
|
+
filename: str,
|
|
20
|
+
migration_type: MigrationType = MigrationType.VERSIONED,
|
|
21
|
+
) -> None:
|
|
22
|
+
"""
|
|
23
|
+
Execute SQL statements and record the migration in the database.
|
|
24
|
+
|
|
25
|
+
Runs all SQL statements within a transaction, then either inserts
|
|
26
|
+
(for upgrade) or deletes (for rollback) the migration record.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
sql_statements (list[str]): List of SQL statements to execute.
|
|
30
|
+
version (str | None): Version string for the migration. Required
|
|
31
|
+
for versioned migrations, can be None for repeatables.
|
|
32
|
+
migration_operation (MigrationDirectionType): Whether this is an
|
|
33
|
+
UPGRADE or ROLLBACK operation.
|
|
34
|
+
filename (str): The migration filename, used to extract description.
|
|
35
|
+
migration_type (MigrationType): Type of migration (VERSIONED,
|
|
36
|
+
RUNS_ALWAYS, or RUNS_ON_CHANGE). Defaults to VERSIONED.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
None: Migration is executed and recorded as a side effect.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
ValueError: If filename is None for an upgrade operation.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
if migration_operation == MigrationDirectionType.UPGRADE and filename is None:
|
|
46
|
+
raise ValueError("Filename must be provided for upgrade migrations.")
|
|
47
|
+
|
|
48
|
+
with get_db_connection() as connection:
|
|
49
|
+
for statement in sql_statements:
|
|
50
|
+
connection.execute(text(statement))
|
|
51
|
+
|
|
52
|
+
if migration_operation == MigrationDirectionType.UPGRADE:
|
|
53
|
+
assert filename is not None
|
|
54
|
+
|
|
55
|
+
description: str = get_description_from_filename(filename=filename)
|
|
56
|
+
checksum: str = calculate_checksum(sql_statements=sql_statements)
|
|
57
|
+
|
|
58
|
+
connection.execute(
|
|
59
|
+
statement=get_query(QueryMethod.INSERT_VERSION_STMT),
|
|
60
|
+
parameters={
|
|
61
|
+
"version": version,
|
|
62
|
+
"description": description,
|
|
63
|
+
"filename": filename,
|
|
64
|
+
"migration_type": migration_type.value,
|
|
65
|
+
"checksum": checksum,
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
elif migration_operation == MigrationDirectionType.ROLLBACK:
|
|
70
|
+
connection.execute(
|
|
71
|
+
statement=get_query(QueryMethod.DELETE_VERSION_STMT),
|
|
72
|
+
parameters={"version": version},
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def run_update_repeatable_migration(
|
|
77
|
+
sql_statements: list[str],
|
|
78
|
+
filename: str,
|
|
79
|
+
migration_type: MigrationType,
|
|
80
|
+
) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Execute and update an existing repeatable migration record.
|
|
83
|
+
|
|
84
|
+
Runs the SQL statements and updates the existing migration record
|
|
85
|
+
with a new checksum and applied_at timestamp.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
sql_statements (list[str]): List of SQL statements to execute.
|
|
89
|
+
filename (str): The migration filename to update.
|
|
90
|
+
migration_type (MigrationType): Type of repeatable migration
|
|
91
|
+
(RUNS_ALWAYS or RUNS_ON_CHANGE).
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
None: Migration is executed and record is updated as a side effect.
|
|
95
|
+
"""
|
|
96
|
+
checksum: str = calculate_checksum(sql_statements=sql_statements)
|
|
97
|
+
|
|
98
|
+
with get_db_connection() as connection:
|
|
99
|
+
for statement in sql_statements:
|
|
100
|
+
connection.execute(text(statement))
|
|
101
|
+
|
|
102
|
+
connection.execute(
|
|
103
|
+
statement=get_query(QueryMethod.UPDATE_REPEATABLE_MIGRATION_STMT),
|
|
104
|
+
parameters={
|
|
105
|
+
"checksum": checksum,
|
|
106
|
+
"filename": filename,
|
|
107
|
+
"migration_type": migration_type.value,
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def fetch_latest_versioned_migration() -> MigrationRecord | None:
|
|
113
|
+
"""
|
|
114
|
+
Get the most recently applied versioned migration from the database.
|
|
115
|
+
|
|
116
|
+
Queries the jetbase_migrations table for the versioned migration
|
|
117
|
+
with the most recent applied_at timestamp.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
MigrationRecord | None: The most recent migration record if any
|
|
121
|
+
migrations have been applied, otherwise None.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
table_exists: bool = migrations_table_exists()
|
|
125
|
+
if not table_exists:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
with get_db_connection() as connection:
|
|
129
|
+
result: Result[tuple[str]] = connection.execute(
|
|
130
|
+
get_query(
|
|
131
|
+
QueryMethod.MIGRATION_RECORDS_QUERY,
|
|
132
|
+
ascending=False,
|
|
133
|
+
migration_type=MigrationType.VERSIONED,
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
latest_migration: Row | None = result.first()
|
|
137
|
+
if not latest_migration:
|
|
138
|
+
return None
|
|
139
|
+
return MigrationRecord(*latest_migration)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def create_migrations_table_if_not_exists() -> None:
|
|
143
|
+
"""
|
|
144
|
+
Create the jetbase_migrations table if it doesn't already exist.
|
|
145
|
+
|
|
146
|
+
Creates the table used to track applied migrations, including
|
|
147
|
+
columns for version, description, filename, checksum, and timestamps.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
None: Table is created as a side effect.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
with get_db_connection() as connection:
|
|
154
|
+
connection.execute(
|
|
155
|
+
statement=get_query(QueryMethod.CREATE_MIGRATIONS_TABLE_STMT)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_latest_versions(
|
|
160
|
+
limit: int | None = None, starting_version: str | None = None
|
|
161
|
+
) -> list[str]:
|
|
162
|
+
"""
|
|
163
|
+
Get recent migration versions from the database.
|
|
164
|
+
|
|
165
|
+
Retrieves either the N most recent versions or all versions applied
|
|
166
|
+
after a specified starting version.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
limit (int | None): Maximum number of versions to return.
|
|
170
|
+
Cannot be used with starting_version. Defaults to None.
|
|
171
|
+
starting_version (str | None): Return all versions applied after
|
|
172
|
+
this version. Cannot be used with limit. Defaults to None.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
list[str]: List of version strings in descending order by
|
|
176
|
+
application time.
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
ValueError: If both limit and starting_version are specified,
|
|
180
|
+
or if neither is specified.
|
|
181
|
+
VersionNotFoundError: If starting_version has not been applied.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
if limit and starting_version:
|
|
185
|
+
raise ValueError(
|
|
186
|
+
"Cannot specify both 'limit' and 'starting_version'. Choose only one."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if not limit and not starting_version:
|
|
190
|
+
raise ValueError("Either 'limit' or 'starting_version' must be specified.")
|
|
191
|
+
|
|
192
|
+
latest_versions: list[str] = []
|
|
193
|
+
|
|
194
|
+
if limit:
|
|
195
|
+
with get_db_connection() as connection:
|
|
196
|
+
result: Result[tuple[str]] = connection.execute(
|
|
197
|
+
statement=get_query(QueryMethod.LATEST_VERSIONS_QUERY),
|
|
198
|
+
parameters={"limit": limit},
|
|
199
|
+
)
|
|
200
|
+
latest_versions: list[str] = [row[0] for row in result.fetchall()]
|
|
201
|
+
|
|
202
|
+
if starting_version:
|
|
203
|
+
with get_db_connection() as connection:
|
|
204
|
+
version_exists_result: Result[tuple[int]] = connection.execute(
|
|
205
|
+
statement=get_query(QueryMethod.CHECK_IF_VERSION_EXISTS_QUERY),
|
|
206
|
+
parameters={"version": starting_version},
|
|
207
|
+
)
|
|
208
|
+
version_exists: int = version_exists_result.scalar_one()
|
|
209
|
+
|
|
210
|
+
if version_exists == 0:
|
|
211
|
+
raise VersionNotFoundError(
|
|
212
|
+
f"Version '{starting_version}' has not been applied yet or does not exist."
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
latest_versions_result: Result[tuple[str]] = connection.execute(
|
|
216
|
+
statement=get_query(
|
|
217
|
+
QueryMethod.LATEST_VERSIONS_BY_STARTING_VERSION_QUERY
|
|
218
|
+
),
|
|
219
|
+
parameters={"starting_version": starting_version},
|
|
220
|
+
)
|
|
221
|
+
latest_versions: list[str] = [
|
|
222
|
+
row[0] for row in latest_versions_result.fetchall()
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
return latest_versions
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def migrations_table_exists() -> bool:
|
|
229
|
+
"""
|
|
230
|
+
Check if the jetbase_migrations table exists in the database.
|
|
231
|
+
|
|
232
|
+
Queries the database metadata to determine if the migrations
|
|
233
|
+
tracking table has been created.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
bool: True if the jetbase_migrations table exists, False otherwise.
|
|
237
|
+
"""
|
|
238
|
+
with get_db_connection() as connection:
|
|
239
|
+
result: Result[tuple[bool]] = connection.execute(
|
|
240
|
+
statement=get_query(QueryMethod.CHECK_IF_MIGRATIONS_TABLE_EXISTS_QUERY)
|
|
241
|
+
)
|
|
242
|
+
table_exists: bool = result.scalar_one()
|
|
243
|
+
|
|
244
|
+
return table_exists
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def get_migration_records() -> list[MigrationRecord]:
|
|
248
|
+
"""
|
|
249
|
+
Get all migration records from the database.
|
|
250
|
+
|
|
251
|
+
Retrieves the complete migration history including versioned
|
|
252
|
+
and repeatable migrations, ordered by application time.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
list[MigrationRecord]: List of all migration records in
|
|
256
|
+
chronological order.
|
|
257
|
+
"""
|
|
258
|
+
with get_db_connection() as connection:
|
|
259
|
+
results: Result[tuple[str, int, str]] = connection.execute(
|
|
260
|
+
statement=get_query(QueryMethod.MIGRATION_RECORDS_QUERY)
|
|
261
|
+
)
|
|
262
|
+
migration_records: list[MigrationRecord] = [
|
|
263
|
+
MigrationRecord(
|
|
264
|
+
order_executed=row.order_executed,
|
|
265
|
+
version=row.version,
|
|
266
|
+
description=row.description,
|
|
267
|
+
filename=row.filename,
|
|
268
|
+
migration_type=row.migration_type,
|
|
269
|
+
applied_at=row.applied_at,
|
|
270
|
+
checksum=row.checksum,
|
|
271
|
+
)
|
|
272
|
+
for row in results.fetchall()
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
return migration_records
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def get_checksums_by_version() -> list[tuple[str, str]]:
|
|
279
|
+
"""
|
|
280
|
+
Get version and checksum pairs for all versioned migrations.
|
|
281
|
+
|
|
282
|
+
Retrieves the checksum stored for each version when it was
|
|
283
|
+
originally applied, ordered by execution order.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
list[tuple[str, str]]: List of (version, checksum) tuples
|
|
287
|
+
in order of application.
|
|
288
|
+
"""
|
|
289
|
+
with get_db_connection() as connection:
|
|
290
|
+
results: Result[tuple[str, str]] = connection.execute(
|
|
291
|
+
statement=get_query(QueryMethod.GET_VERSION_CHECKSUMS_QUERY)
|
|
292
|
+
)
|
|
293
|
+
versions_and_checksums: list[tuple[str, str]] = [
|
|
294
|
+
(row.version, row.checksum) for row in results.fetchall()
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
return versions_and_checksums
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def get_migrated_versions() -> list[str]:
|
|
301
|
+
"""
|
|
302
|
+
Get all applied versioned migration versions from the database.
|
|
303
|
+
|
|
304
|
+
Returns the version string for each versioned migration that
|
|
305
|
+
has been applied, in order of application.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
list[str]: List of version strings in order of application.
|
|
309
|
+
"""
|
|
310
|
+
with get_db_connection() as connection:
|
|
311
|
+
results: Result[tuple[str]] = connection.execute(
|
|
312
|
+
statement=get_query(QueryMethod.GET_VERSION_CHECKSUMS_QUERY)
|
|
313
|
+
)
|
|
314
|
+
migrated_versions: list[str] = [row.version for row in results.fetchall()]
|
|
315
|
+
|
|
316
|
+
return migrated_versions
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def update_migration_checksums(versions_and_checksums: list[tuple[str, str]]) -> None:
|
|
320
|
+
"""
|
|
321
|
+
Update stored checksums for specified migration versions.
|
|
322
|
+
|
|
323
|
+
Updates the checksum values in the database for migrations
|
|
324
|
+
whose files have been modified since they were applied.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
versions_and_checksums (list[tuple[str, str]]): List of
|
|
328
|
+
(version, new_checksum) tuples to update.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
None: Checksums are updated as a side effect.
|
|
332
|
+
"""
|
|
333
|
+
with get_db_connection() as connection:
|
|
334
|
+
for version, checksum in versions_and_checksums:
|
|
335
|
+
connection.execute(
|
|
336
|
+
statement=get_query(QueryMethod.REPAIR_MIGRATION_CHECKSUM_STMT),
|
|
337
|
+
parameters={"version": version, "checksum": checksum},
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def get_existing_on_change_filenames_to_checksums() -> dict[str, str]:
|
|
342
|
+
"""
|
|
343
|
+
Get filename to checksum mapping for runs-on-change migrations.
|
|
344
|
+
|
|
345
|
+
Retrieves the checksums stored for each runs-on-change migration
|
|
346
|
+
when it was last applied.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
dict[str, str]: Dictionary mapping filenames to their stored
|
|
350
|
+
checksum values.
|
|
351
|
+
"""
|
|
352
|
+
with get_db_connection() as connection:
|
|
353
|
+
results: Result[tuple[str, str]] = connection.execute(
|
|
354
|
+
statement=get_query(QueryMethod.GET_RUNS_ON_CHANGE_MIGRATIONS_QUERY),
|
|
355
|
+
)
|
|
356
|
+
migration_filenames_to_checksums: dict[str, str] = {
|
|
357
|
+
row.filename: row.checksum for row in results.fetchall()
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return migration_filenames_to_checksums
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def get_existing_repeatable_always_migration_filenames() -> set[str]:
|
|
364
|
+
"""
|
|
365
|
+
Get filenames of all runs-always migrations in the database.
|
|
366
|
+
|
|
367
|
+
Retrieves the filenames of all runs-always migrations that have
|
|
368
|
+
been applied at least once.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
set[str]: Set of runs-always migration filenames.
|
|
372
|
+
"""
|
|
373
|
+
with get_db_connection() as connection:
|
|
374
|
+
results: Result[tuple[str]] = connection.execute(
|
|
375
|
+
statement=get_query(QueryMethod.GET_RUNS_ALWAYS_MIGRATIONS_QUERY),
|
|
376
|
+
)
|
|
377
|
+
migration_filenames: set[str] = {row.filename for row in results.fetchall()}
|
|
378
|
+
|
|
379
|
+
return migration_filenames
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def delete_missing_versions(versions: list[str]) -> None:
|
|
383
|
+
"""
|
|
384
|
+
Delete migration records for specified versions.
|
|
385
|
+
|
|
386
|
+
Removes records from the jetbase_migrations table for versioned
|
|
387
|
+
migrations whose files no longer exist.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
versions (list[str]): List of version strings to delete.
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
None: Records are deleted as a side effect.
|
|
394
|
+
"""
|
|
395
|
+
with get_db_connection() as connection:
|
|
396
|
+
for version in versions:
|
|
397
|
+
connection.execute(
|
|
398
|
+
statement=get_query(QueryMethod.DELETE_MISSING_VERSION_STMT),
|
|
399
|
+
parameters={"version": version},
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def delete_missing_repeatables(repeatable_filenames: list[str]) -> None:
|
|
404
|
+
"""
|
|
405
|
+
Delete migration records for specified repeatable filenames.
|
|
406
|
+
|
|
407
|
+
Removes records from the jetbase_migrations table for repeatable
|
|
408
|
+
migrations whose files no longer exist.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
repeatable_filenames (list[str]): List of filenames to delete.
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
None: Records are deleted as a side effect.
|
|
415
|
+
"""
|
|
416
|
+
with get_db_connection() as connection:
|
|
417
|
+
for r_file in repeatable_filenames:
|
|
418
|
+
connection.execute(
|
|
419
|
+
statement=get_query(QueryMethod.DELETE_MISSING_REPEATABLE_STMT),
|
|
420
|
+
parameters={"filename": r_file},
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def fetch_repeatable_migrations() -> list[MigrationRecord]:
|
|
425
|
+
"""
|
|
426
|
+
Get all repeatable migration records from the database.
|
|
427
|
+
|
|
428
|
+
Retrieves all runs-always and runs-on-change migrations that
|
|
429
|
+
have been applied.
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
list[MigrationRecord]: List of all repeatable migration records.
|
|
433
|
+
"""
|
|
434
|
+
with get_db_connection() as connection:
|
|
435
|
+
results: Result[tuple[str]] = connection.execute(
|
|
436
|
+
statement=get_query(
|
|
437
|
+
QueryMethod.MIGRATION_RECORDS_QUERY, all_repeatables=True
|
|
438
|
+
),
|
|
439
|
+
)
|
|
440
|
+
return [
|
|
441
|
+
MigrationRecord(
|
|
442
|
+
order_executed=row.order_executed,
|
|
443
|
+
version=row.version,
|
|
444
|
+
description=row.description,
|
|
445
|
+
filename=row.filename,
|
|
446
|
+
migration_type=row.migration_type,
|
|
447
|
+
applied_at=row.applied_at,
|
|
448
|
+
checksum=row.checksum,
|
|
449
|
+
)
|
|
450
|
+
for row in results.fetchall()
|
|
451
|
+
]
|