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.
Files changed (49) hide show
  1. jetbase/cli/main.py +146 -4
  2. jetbase/commands/current.py +20 -0
  3. jetbase/commands/fix_checksums.py +172 -0
  4. jetbase/commands/fix_files.py +133 -0
  5. jetbase/commands/history.py +53 -0
  6. jetbase/commands/init.py +29 -0
  7. jetbase/commands/lock_status.py +25 -0
  8. jetbase/commands/new.py +65 -0
  9. jetbase/commands/rollback.py +172 -0
  10. jetbase/commands/status.py +212 -0
  11. jetbase/commands/unlock.py +29 -0
  12. jetbase/commands/upgrade.py +248 -0
  13. jetbase/commands/validators.py +37 -0
  14. jetbase/config.py +304 -25
  15. jetbase/constants.py +10 -2
  16. jetbase/database/connection.py +40 -0
  17. jetbase/database/queries/base.py +353 -0
  18. jetbase/database/queries/default_queries.py +215 -0
  19. jetbase/database/queries/postgres.py +14 -0
  20. jetbase/database/queries/query_loader.py +87 -0
  21. jetbase/database/queries/sqlite.py +197 -0
  22. jetbase/engine/checksum.py +25 -0
  23. jetbase/engine/dry_run.py +105 -0
  24. jetbase/engine/file_parser.py +324 -0
  25. jetbase/engine/formatters.py +61 -0
  26. jetbase/engine/lock.py +65 -0
  27. jetbase/engine/repeatable.py +125 -0
  28. jetbase/engine/validation.py +238 -0
  29. jetbase/engine/version.py +144 -0
  30. jetbase/enums.py +37 -1
  31. jetbase/exceptions.py +87 -0
  32. jetbase/models.py +45 -0
  33. jetbase/repositories/lock_repo.py +129 -0
  34. jetbase/repositories/migrations_repo.py +451 -0
  35. jetbase-0.12.1.dist-info/METADATA +135 -0
  36. jetbase-0.12.1.dist-info/RECORD +39 -0
  37. {jetbase-0.7.0.dist-info → jetbase-0.12.1.dist-info}/WHEEL +1 -1
  38. jetbase/core/dry_run.py +0 -38
  39. jetbase/core/file_parser.py +0 -199
  40. jetbase/core/initialize.py +0 -33
  41. jetbase/core/repository.py +0 -169
  42. jetbase/core/rollback.py +0 -67
  43. jetbase/core/upgrade.py +0 -75
  44. jetbase/core/version.py +0 -163
  45. jetbase/queries.py +0 -72
  46. jetbase-0.7.0.dist-info/METADATA +0 -12
  47. jetbase-0.7.0.dist-info/RECORD +0 -17
  48. {jetbase-0.7.0.dist-info → jetbase-0.12.1.dist-info}/entry_points.txt +0 -0
  49. {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
+ ]