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,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,,
|
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")
|
jetbase/core/file_parser.py
DELETED
|
@@ -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))
|
jetbase/core/initialize.py
DELETED
|
@@ -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)
|
jetbase/core/repository.py
DELETED
|
@@ -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
|
-
)
|