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
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: jetbase
3
+ Version: 0.12.1
4
+ Summary: Jetbase is a Python database migration tool
5
+ Author-email: jaz <jaz.allibhai@gmail.com>
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: packaging>=25.0
9
+ Requires-Dist: rich>=12.2.0
10
+ Requires-Dist: sqlalchemy>=2.0.10
11
+ Requires-Dist: tomli>=2.0.2
12
+ Requires-Dist: typer>=0.12.3
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Welcome to Jetbase 🚀
16
+
17
+ **Jetbase** is a simple, lightweight database migration tool for Python projects.
18
+
19
+ Jetbase helps you manage database migrations in a simple, version-controlled way. Whether you're adding a new table, modifying columns, or need to undo a change, Jetbase makes it super easy!
20
+
21
+ ### Key Features ✨
22
+
23
+ - **📦 Simple Setup** — Get started with just one command
24
+ - **⬆️ Easy Upgrades** — Apply pending migrations with confidence
25
+ - **⬇️ Safe Rollbacks** — Made a mistake? No problem, roll it back!
26
+ - **📊 Clear Status** — Always know which migrations have been applied and which are pending
27
+ - **🔒 Migration Locking** — Prevents conflicts when multiple processes try to migrate
28
+ - **✅ Checksum Validation** — Detects if migration files have been modified
29
+ - **🔄 Repeatable Migrations** — Support for migrations that run on every upgrade
30
+
31
+ [📚 Full Documentation](https://jetbase-hq.github.io/jetbase/)
32
+
33
+ ## Quick Start 🏃‍♂️
34
+
35
+ ### Installation
36
+
37
+ **Using pip:**
38
+ ```shell
39
+ pip install jetbase
40
+ ```
41
+
42
+ **Using uv:**
43
+ ```shell
44
+ uv add jetbase
45
+ ```
46
+
47
+ ### Initialize Your Project
48
+
49
+ ```bash
50
+ jetbase init
51
+ cd jetbase
52
+ ```
53
+
54
+ This creates a `jetbase/` directory with:
55
+
56
+ - A `migrations/` folder for your SQL files
57
+ - An `env.py` configuration file
58
+
59
+ ### Configure Your Database
60
+
61
+ Edit `jetbase/env.py` with your database connection string (currently support for postgres and sqlite):
62
+
63
+ **PostgreSQL example:**
64
+ ```python
65
+ sqlalchemy_url = "postgresql+psycopg2://user:password@localhost:5432/mydb"
66
+ ```
67
+
68
+ **SQLite example:**
69
+ ```python
70
+ sqlalchemy_url = "sqlite:///mydb.db"
71
+ ```
72
+
73
+ ### Create Your First Migration
74
+
75
+ ```bash
76
+ jetbase new "create users table"
77
+ ```
78
+
79
+ This creates a new SQL file like `V20251225.120000__create_users_and_items_tables.sql`.
80
+
81
+ > **Tip:**
82
+ > You can also create migrations manually by adding SQL files in the `jetbase/migrations` directory, using the `V<version>__<description>.sql` naming convention (e.g., `V1__add_users_table.sql`, `V2.4__add_users_table.sql`).
83
+
84
+
85
+ ### Write Your Migration
86
+
87
+ Open the newly created file and add your SQL:
88
+
89
+ ```sql
90
+ -- upgrade
91
+ CREATE TABLE users (
92
+ id SERIAL PRIMARY KEY,
93
+ name VARCHAR(100) NOT NULL,
94
+ email VARCHAR(255) UNIQUE NOT NULL
95
+ );
96
+
97
+ CREATE TABLE items (
98
+ id SERIAL PRIMARY KEY,
99
+ name VARCHAR(100) NOT NULL
100
+ );
101
+
102
+ -- rollback
103
+ DROP TABLE items;
104
+ DROP TABLE users;
105
+ ```
106
+
107
+ ### Apply the Migration
108
+
109
+ ```bash
110
+ jetbase upgrade
111
+ ```
112
+
113
+ That's it! Your database is now up to date. 🎉
114
+
115
+ > **Note:**
116
+ > Jetbase uses SQLAlchemy under the hood to manage database connections.
117
+ > For any database other than SQLite, you must install the appropriate Python database driver.
118
+ > For example, to use Jetbase with PostgreSQL:
119
+
120
+ ```bash
121
+ pip install psycopg2
122
+ ```
123
+
124
+ You can also use another compatible driver if you prefer (such as `asyncpg`, `pg8000`, etc.).
125
+
126
+ ## Supported Databases
127
+
128
+ Jetbase currently supports:
129
+
130
+ - ✅ PostgreSQL
131
+ - ✅ SQLite
132
+
133
+ ## Need Help?
134
+
135
+ Open an issue on GitHub!
@@ -0,0 +1,39 @@
1
+ jetbase/config.py,sha256=81mex8hZcbos7KRB5ywndeSdmi-fV-J-WFcdIyIw2vE,10225
2
+ jetbase/constants.py,sha256=Erzv_eR4KaTW96qWvoYzqT5he69xHYjCe0zzx5vkZpE,535
3
+ jetbase/enums.py,sha256=_dVhrfl2WYqQjwf_ydDGvU-qR5LxQgdJVObph6aNWaM,998
4
+ jetbase/exceptions.py,sha256=N8_HloEfg4f28RjBO4ems2HRDSfxR9c10_6F25hvrt4,2227
5
+ jetbase/models.py,sha256=pIefNCinOuV2pPo2Mdtl_UbPuE7o7aXzu0ruP-bbIfI,1363
6
+ jetbase/cli/main.py,sha256=64dM71Mgofd1Mhv2gTdr54wnSWh8gfJIWbChwG8zKCE,5882
7
+ jetbase/commands/current.py,sha256=jXDxZ54hmEOgWpWLS474wCaBfPynPcyq6Qqm6efFBUs,735
8
+ jetbase/commands/fix_checksums.py,sha256=pi5nLSR1vHzYKZn8ftX2-lEinctHqrgHo_FzPjkVfts,6330
9
+ jetbase/commands/fix_files.py,sha256=aSorsu6bZrq_kLX14OrxDaee6KoEICjBMMoO832iQpQ,4785
10
+ jetbase/commands/history.py,sha256=P-BlODZxcixjsawMT0JHi_FVRAeyZwPLiN0UQy5oN9M,1922
11
+ jetbase/commands/init.py,sha256=LmFMkcF7zQbi3GWekgEy9Vb3_vTorrUkqOUgvAT1_qw,902
12
+ jetbase/commands/lock_status.py,sha256=yu3AxEkeaHua_5ydclOMU36FW0J9Kacs05lbmSrqQGA,838
13
+ jetbase/commands/new.py,sha256=6mxeP8yVDcXJSIBvnQgOqURRwANlNtsWT_77qE0z_NU,2280
14
+ jetbase/commands/rollback.py,sha256=PZclqfvHCyrXG4wyDmco9O3o9PCjHApYjWcEG73N73c,6176
15
+ jetbase/commands/status.py,sha256=5I_1gjmPShTHlTVUvvl2v2LJION5pGryfXYErybzFJ8,7307
16
+ jetbase/commands/unlock.py,sha256=sM0YJJ6BfYgAy-W4jhdhGBcwtM-JMzN5uTfc8FK3ApQ,835
17
+ jetbase/commands/upgrade.py,sha256=Vna0OviJHa6ge94cyJQbMkah3PvFPL0fraA2vProVqg,8998
18
+ jetbase/commands/validators.py,sha256=keWjDcuO5wHOSb76BwJxj_M8Dthz3ZMaP3Td5cg5bB8,1366
19
+ jetbase/database/connection.py,sha256=v3082NzzBQnd4vq-X6M4oXROPNtNI1r5iNwhuPZIEFg,1433
20
+ jetbase/database/queries/base.py,sha256=f00vJRNGWaACVOJ2283Oh1G1LLc_eHxI1A-SABWwYo8,11775
21
+ jetbase/database/queries/default_queries.py,sha256=7RzPMhgEK8_-In86s3HQN7Na-mY0zeydwLeyjX9v2tU,5257
22
+ jetbase/database/queries/postgres.py,sha256=D6yNlIfHT4s4GZIe7tAQfXvCeOVu2NxARctWsehrwfI,438
23
+ jetbase/database/queries/query_loader.py,sha256=VwvsAQVrpe44cJQFqVWqzVbXB9yWYTQ0i0elkiOapBo,2641
24
+ jetbase/database/queries/sqlite.py,sha256=3l1RBn9B363w-bslcutwQOAFpgSUJdeqSkHpYZZ0S4A,5641
25
+ jetbase/engine/checksum.py,sha256=Q59s_9dkk4uWHUyG8WZcoTvRr9wRdSCeaDKma5i2kw4,704
26
+ jetbase/engine/dry_run.py,sha256=Yz9qpnv1YfA0ZAjCP_HEtCaguuGTo-JqzmnyinjMJQI,3845
27
+ jetbase/engine/file_parser.py,sha256=cWPUYa9WolFXsKMdr4NT3Sdf70ePG6bt2sXs4PEzrUk,10299
28
+ jetbase/engine/formatters.py,sha256=fG91mN0ymYK90upOCwpg43g8LJ3o7inUec8mkBPMzAo,1895
29
+ jetbase/engine/lock.py,sha256=JCZxb9r26RN2OUJl0tCR6xsOQIex_nACcR0_Lgprp38,1901
30
+ jetbase/engine/repeatable.py,sha256=WyN46LyHiNxAdjLFlqye2w2I-mKXMGW7GuzdfpITSv8,4614
31
+ jetbase/engine/validation.py,sha256=tZg3X6LeNHl-cD2tmazjmQDS0mwdkzWVnko0rSn43-Y,9527
32
+ jetbase/engine/version.py,sha256=JpSnHiLiwiUsWIqsIEmeSVbhNqkibVpIYv7Pl6lioIM,5388
33
+ jetbase/repositories/lock_repo.py,sha256=foQ3zX38UTxXg4sH1Gh4NJ5xDqBygeueaG8DevElMeM,3940
34
+ jetbase/repositories/migrations_repo.py,sha256=23MO2KIFvvZ75xqxcllx9Gon6PDI5kvCDNL-k77bH7U,15459
35
+ jetbase-0.12.1.dist-info/METADATA,sha256=vYI37rlngvWZFe24sx8xK06vrDTnFlZvHcLkJDJa00s,3334
36
+ jetbase-0.12.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
37
+ jetbase-0.12.1.dist-info/entry_points.txt,sha256=-Pe7A0r8aGF-6C14hB0Ck8aEuszuH5pdadt5OWSiWwA,50
38
+ jetbase-0.12.1.dist-info/licenses/LICENSE,sha256=hyssQNKtnK32aFCk8mVR406ysMtGNP6_dZCig8zBk4Q,1065
39
+ jetbase-0.12.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
jetbase/core/dry_run.py DELETED
@@ -1,38 +0,0 @@
1
- import os
2
-
3
- from jetbase.core.file_parser import parse_rollback_statements, parse_upgrade_statements
4
- from jetbase.enums import MigrationOperationType
5
-
6
-
7
- def process_dry_run(
8
- version_to_filepath: dict[str, str], migration_operation: MigrationOperationType
9
- ) -> None:
10
- print("\nJETBASE - Dry Run Mode")
11
- print("No SQL will be executed. This is a preview of what would happen.")
12
- print("----------------------------------------\n\n")
13
-
14
- for version, file_path in version_to_filepath.items():
15
- if migration_operation == MigrationOperationType.UPGRADE:
16
- sql_statements: list[str] = parse_upgrade_statements(
17
- file_path=file_path, dry_run=True
18
- )
19
- elif migration_operation == MigrationOperationType.ROLLBACK:
20
- sql_statements: list[str] = parse_rollback_statements(
21
- file_path=file_path, dry_run=True
22
- )
23
- else:
24
- raise NotImplementedError(
25
- f"Dry run not implemented for migration operation: {migration_operation}"
26
- )
27
-
28
- filename: str = os.path.basename(file_path)
29
-
30
- num_sql_statements: int = len(sql_statements)
31
-
32
- print(
33
- f"\nSQL Preview for {filename} ({num_sql_statements} {'statements' if num_sql_statements != 1 else 'statement'})"
34
- )
35
- for statement in sql_statements:
36
- print("\n")
37
- print(statement)
38
- print("----------------------------------------\n")
@@ -1,199 +0,0 @@
1
- import re
2
-
3
- from jetbase.enums import MigrationOperationType
4
-
5
-
6
- def parse_upgrade_statements(file_path: str, dry_run: bool = False) -> list[str]:
7
- statements = []
8
- current_statement = []
9
-
10
- with open(file_path, "r") as file:
11
- for line in file:
12
- if not dry_run:
13
- line = line.strip()
14
- else:
15
- line = line.rstrip()
16
-
17
- if (
18
- line.strip().startswith("--")
19
- and line[2:].strip().lower() == MigrationOperationType.ROLLBACK.value
20
- ):
21
- break
22
-
23
- if not line or line.strip().startswith("--"):
24
- continue
25
- current_statement.append(line)
26
-
27
- if line.strip().endswith(";"):
28
- if not dry_run:
29
- statement = " ".join(current_statement)
30
- else:
31
- statement = "\n".join(current_statement)
32
- statement = statement.rstrip(";").strip()
33
- if statement:
34
- statements.append(statement)
35
- current_statement = []
36
-
37
- return statements
38
-
39
-
40
- def parse_rollback_statements(file_path: str, dry_run: bool = False) -> list[str]:
41
- statements = []
42
- current_statement = []
43
- in_rollback_section = False
44
-
45
- with open(file_path, "r") as file:
46
- for line in file:
47
- if not dry_run:
48
- line = line.strip()
49
- else:
50
- line = line.rstrip()
51
-
52
- if not in_rollback_section:
53
- if (
54
- line.strip().startswith("--")
55
- and line[2:].strip().lower()
56
- == MigrationOperationType.ROLLBACK.value
57
- ):
58
- in_rollback_section = True
59
- else:
60
- continue
61
-
62
- if in_rollback_section:
63
- if not line or line.strip().startswith("--"):
64
- continue
65
- current_statement.append(line)
66
-
67
- if line.strip().endswith(";"):
68
- if not dry_run:
69
- statement = " ".join(current_statement)
70
- else:
71
- statement = "\n".join(current_statement)
72
- statement = statement.rstrip(";").strip()
73
- if statement:
74
- statements.append(statement)
75
- current_statement = []
76
-
77
- return statements
78
-
79
-
80
- def is_filename_format_valid(filename: str) -> bool:
81
- """
82
- Validates if a filename follows the expected migration file naming convention.
83
- A valid filename must:
84
- - Start with "V"
85
- - Have a valid version number following "V"
86
- - Contain "__" (double underscore)
87
- - End with ".sql"
88
- - Have a valid version number extractable from the filename
89
- Args:
90
- filename (str): The filename to validate.
91
- Returns:
92
- bool: True if the filename meets all validation criteria, False otherwise.
93
- Example:
94
- >>> is_valid_filename_format("V1__initial_migration.sql")
95
- True
96
- >>> is_valid_filename_format("migration.sql")
97
- False
98
- """
99
- if not filename.endswith(".sql"):
100
- return False
101
- if not filename.startswith("V"):
102
- return False
103
- if "__" not in filename:
104
- return False
105
- description: str = _get_raw_description_from_filename(filename=filename)
106
- if len(description.strip()) == 0:
107
- return False
108
- raw_version: str = _get_version_from_filename(filename=filename)
109
- if not _is_valid_version(version=raw_version):
110
- return False
111
- return True
112
-
113
-
114
- def get_description_from_filename(filename: str) -> str:
115
- """
116
- Extract and format the description from a migration filename.
117
-
118
- Args:
119
- filename: The migration filename (e.g., "V1_2_0__add_feature.sql")
120
-
121
- Returns:
122
- str: The formatted description string (e.g., "add feature")
123
- """
124
-
125
- raw_description: str = _get_raw_description_from_filename(filename=filename)
126
- formatted_description: str = raw_description.replace("_", " ")
127
- return formatted_description
128
-
129
-
130
- def is_filename_length_valid(filename: str, max_length: int = 512) -> bool:
131
- """
132
- Check if the filename length is within the specified maximum length.
133
-
134
- Args:
135
- filename: The migration filename to check.
136
- max_length: The maximum allowed length for the filename.
137
-
138
- Returns:
139
- bool: True if the filename length is within the maximum length, False otherwise.
140
- """
141
- return len(filename) <= max_length
142
-
143
-
144
- def _get_version_from_filename(filename: str) -> str:
145
- """
146
- Extract the version string from a migration filename.
147
-
148
- Args:
149
- filename: The migration filename (e.g., "V1_2_0__add_feature.sql")
150
-
151
- Returns:
152
- str: The extracted version string (e.g., "1_2_0")
153
- """
154
-
155
- version: str = filename[1 : filename.index("__")]
156
- return version
157
-
158
-
159
- def _get_raw_description_from_filename(filename: str) -> str:
160
- """
161
- Extract the description string from a migration filename.
162
-
163
- Args:
164
- filename: The migration filename (e.g., "V1_2_0__add_feature.sql")
165
-
166
- Returns:
167
- str: The extracted description string (e.g., "add_feature")
168
- """
169
-
170
- description: str = filename[
171
- filename.index("__") + 2 : filename.index(".sql")
172
- ].strip()
173
- return description
174
-
175
-
176
- def _is_valid_version(version: str) -> bool:
177
- """
178
- Validate that a version string follows the correct format.
179
-
180
- Rules:
181
- - Must start and end with a number
182
- - Can be any length (1 or greater)
183
- - Can contain periods (.) or underscores (_) between numbers
184
-
185
- Valid examples: "1", "1.2", "1_2", "1.2.3", "1_2_3", "10.20.30"
186
- Invalid examples: ".1", "1.", "_1", "1_", "1..2", "1__2", "abc", ""
187
-
188
- Args:
189
- version: The version string to validate
190
-
191
- Returns:
192
- bool: True if version is valid, False otherwise
193
- """
194
- if not version:
195
- return False
196
-
197
- # Pattern: starts with digit, ends with digit, can have periods/underscores between digits
198
- pattern = r"^\d+([._]\d+)*$"
199
- return bool(re.match(pattern, version))
@@ -1,33 +0,0 @@
1
- from pathlib import Path
2
-
3
- from jetbase.constants import BASE_DIR, CONFIG_FILE, CONFIG_FILE_CONTENT, MIGRATIONS_DIR
4
-
5
-
6
- def create_directory_structure(base_path: str) -> None:
7
- """
8
- Create the basic directory structure for a new Jetbase project.
9
-
10
- This function creates:
11
- - A migrations directory
12
- - A config file with default content
13
-
14
- After creating the structure, it prints a confirmation message.
15
-
16
- Args:
17
- base_path (str): The base path where the Jetbase project structure will be created
18
-
19
- Returns:
20
- None
21
- """
22
- migrations_dir: Path = Path(base_path) / MIGRATIONS_DIR
23
- migrations_dir.mkdir(parents=True, exist_ok=True)
24
-
25
- config_path: Path = Path(base_path) / CONFIG_FILE
26
- with open(config_path, "w") as f:
27
- f.write(CONFIG_FILE_CONTENT)
28
-
29
- print(f"Initialized Jetbase project in {Path(base_path).absolute()}")
30
-
31
-
32
- def initialize_cmd() -> None:
33
- create_directory_structure(base_path=BASE_DIR)
@@ -1,169 +0,0 @@
1
- from sqlalchemy import Engine, Result, create_engine, text
2
-
3
- from jetbase.config import get_sqlalchemy_url
4
- from jetbase.core.file_parser import get_description_from_filename
5
- from jetbase.enums import MigrationOperationType
6
- from jetbase.queries import (
7
- CHECK_IF_MIGRATIONS_TABLE_EXISTS_QUERY,
8
- CHECK_IF_VERSION_EXISTS_QUERY,
9
- CREATE_MIGRATIONS_TABLE_STMT,
10
- DELETE_VERSION_STMT,
11
- INSERT_VERSION_STMT,
12
- LATEST_VERSION_QUERY,
13
- LATEST_VERSIONS_BY_STARTING_VERSION_QUERY,
14
- LATEST_VERSIONS_QUERY,
15
- )
16
-
17
-
18
- def get_last_updated_version() -> str | None:
19
- """
20
- Retrieves the latest version from the database.
21
- This function connects to the database, executes a query to get the most recent version,
22
- and returns that version as a string.
23
- Returns:
24
- str | None: The latest version string if available, None if no version was found.
25
- """
26
-
27
- engine: Engine = create_engine(url=get_sqlalchemy_url())
28
-
29
- with engine.begin() as connection:
30
- result: Result[tuple[str]] = connection.execute(LATEST_VERSION_QUERY)
31
- latest_version: str | None = result.scalar()
32
- if not latest_version:
33
- return None
34
- return latest_version
35
-
36
-
37
- def create_migrations_table_if_not_exists() -> None:
38
- """
39
- Creates the migrations table in the database
40
- if it does not already exist.
41
- Returns:
42
- None
43
- """
44
-
45
- engine: Engine = create_engine(url=get_sqlalchemy_url())
46
- with engine.begin() as connection:
47
- connection.execute(statement=CREATE_MIGRATIONS_TABLE_STMT)
48
-
49
-
50
- def run_migration(
51
- sql_statements: list[str],
52
- version: str,
53
- migration_operation: MigrationOperationType,
54
- filename: str | None = None,
55
- ) -> None:
56
- """
57
- Execute a database migration by running SQL statements and recording the migration version.
58
- Args:
59
- sql_statements (list[str]): List of SQL statements to execute as part of the migration
60
- version (str): Version identifier to record after successful migration
61
- Returns:
62
- None
63
- """
64
-
65
- if migration_operation == MigrationOperationType.UPGRADE and filename is None:
66
- raise ValueError("Filename must be provided for upgrade migrations.")
67
-
68
- engine: Engine = create_engine(url=get_sqlalchemy_url())
69
-
70
- with engine.begin() as connection:
71
- for statement in sql_statements:
72
- connection.execute(text(statement))
73
-
74
- if migration_operation == MigrationOperationType.UPGRADE:
75
- assert filename is not None
76
- description: str = get_description_from_filename(filename=filename)
77
- connection.execute(
78
- statement=INSERT_VERSION_STMT,
79
- parameters={
80
- "version": version,
81
- "description": description,
82
- "filename": filename,
83
- },
84
- )
85
-
86
- elif migration_operation == MigrationOperationType.ROLLBACK:
87
- connection.execute(
88
- statement=DELETE_VERSION_STMT, parameters={"version": version}
89
- )
90
-
91
-
92
- def get_latest_versions(limit: int) -> list[str]:
93
- """
94
- Retrieve the latest N migration versions from the database.
95
- Args:
96
- limit (int): The number of latest versions to retrieve
97
- Returns:
98
- list[str]: A list of the latest migration version strings
99
- """
100
-
101
- engine: Engine = create_engine(url=get_sqlalchemy_url())
102
- latest_versions: list[str] = []
103
-
104
- with engine.begin() as connection:
105
- result: Result[tuple[str]] = connection.execute(
106
- statement=LATEST_VERSIONS_QUERY,
107
- parameters={"limit": limit},
108
- )
109
- latest_versions: list[str] = [row[0] for row in result.fetchall()]
110
-
111
- return latest_versions
112
-
113
-
114
- def get_latest_versions_by_starting_version(
115
- starting_version: str,
116
- ) -> list[str]:
117
- """
118
- Retrieve the latest N migration versions from the database,
119
- starting from a specific version.
120
- Args:
121
- starting_version (str): The version to start retrieving from
122
- limit (int): The number of latest versions to retrieve
123
- Returns:
124
- list[str]: A list of the latest migration version strings
125
- """
126
-
127
- engine: Engine = create_engine(url=get_sqlalchemy_url())
128
- latest_versions: list[str] = []
129
- starting_version = starting_version
130
-
131
- with engine.begin() as connection:
132
- version_exists_result: Result[tuple[int]] = connection.execute(
133
- statement=CHECK_IF_VERSION_EXISTS_QUERY,
134
- parameters={"version": starting_version},
135
- )
136
- version_exists: int = version_exists_result.scalar_one()
137
-
138
- if version_exists == 0:
139
- raise ValueError(
140
- f"'{starting_version}' has not been applied yet or does not exist."
141
- )
142
-
143
- latest_versions_result: Result[tuple[str]] = connection.execute(
144
- statement=LATEST_VERSIONS_BY_STARTING_VERSION_QUERY,
145
- parameters={"starting_version": starting_version},
146
- )
147
- latest_versions: list[str] = [
148
- row[0] for row in latest_versions_result.fetchall()
149
- ]
150
-
151
- return latest_versions
152
-
153
-
154
- def migrations_table_exists() -> bool:
155
- """
156
- Check if the jetbase_migrations table exists in the database.
157
- Returns:
158
- bool: True if the jetbase_migrations table exists, False otherwise.
159
- """
160
-
161
- engine: Engine = create_engine(url=get_sqlalchemy_url())
162
-
163
- with engine.begin() as connection:
164
- result: Result[tuple[bool]] = connection.execute(
165
- statement=CHECK_IF_MIGRATIONS_TABLE_EXISTS_QUERY
166
- )
167
- table_exists: bool = result.scalar_one()
168
-
169
- return table_exists
jetbase/core/rollback.py DELETED
@@ -1,67 +0,0 @@
1
- import os
2
-
3
- from jetbase.core.dry_run import process_dry_run
4
- from jetbase.core.file_parser import parse_rollback_statements
5
- from jetbase.core.repository import (
6
- get_latest_versions,
7
- get_latest_versions_by_starting_version,
8
- migrations_table_exists,
9
- run_migration,
10
- )
11
- from jetbase.core.version import get_migration_filepaths_by_version
12
- from jetbase.enums import MigrationOperationType
13
-
14
-
15
- def rollback_cmd(
16
- count: int | None = None, to_version: str | None = None, dry_run: bool = False
17
- ) -> None:
18
- table_exists: bool = migrations_table_exists()
19
- if not table_exists:
20
- print("No migrations have been applied; nothing to rollback.")
21
- return
22
-
23
- if count is not None and to_version is not None:
24
- raise ValueError(
25
- "Cannot specify both 'count' and 'to_version' for rollback. "
26
- "Select only one, or do not specify either to rollback the last migration."
27
- )
28
- if count is None and to_version is None:
29
- count = 1
30
-
31
- latest_migration_versions: list[str] = []
32
- if count:
33
- latest_migration_versions = get_latest_versions(limit=count)
34
- elif to_version:
35
- latest_migration_versions = get_latest_versions_by_starting_version(
36
- starting_version=to_version
37
- )
38
-
39
- if not latest_migration_versions:
40
- print("No migrations have been applied; nothing to rollback.")
41
- return
42
-
43
- versions_to_rollback: dict[str, str] = get_migration_filepaths_by_version(
44
- directory=os.path.join(os.getcwd(), "migrations"),
45
- version_to_start_from=latest_migration_versions[-1],
46
- end_version=latest_migration_versions[0],
47
- )
48
-
49
- versions_to_rollback: dict[str, str] = dict(reversed(versions_to_rollback.items()))
50
-
51
- if not dry_run:
52
- for version, file_path in versions_to_rollback.items():
53
- sql_statements: list[str] = parse_rollback_statements(file_path=file_path)
54
- run_migration(
55
- sql_statements=sql_statements,
56
- version=version,
57
- migration_operation=MigrationOperationType.ROLLBACK,
58
- )
59
- filename: str = os.path.basename(file_path)
60
-
61
- print(f"Rollback applied successfully: {filename}")
62
-
63
- else:
64
- process_dry_run(
65
- version_to_filepath=versions_to_rollback,
66
- migration_operation=MigrationOperationType.ROLLBACK,
67
- )