flowgrate 0.1.0__tar.gz
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.
- flowgrate-0.1.0/.github/workflows/ci.yml +33 -0
- flowgrate-0.1.0/.github/workflows/publish.yml +41 -0
- flowgrate-0.1.0/.gitignore +9 -0
- flowgrate-0.1.0/LICENSE +21 -0
- flowgrate-0.1.0/PKG-INFO +206 -0
- flowgrate-0.1.0/README.md +183 -0
- flowgrate-0.1.0/flowgrate/__init__.py +5 -0
- flowgrate-0.1.0/flowgrate/migration.py +9 -0
- flowgrate-0.1.0/flowgrate/runner.py +141 -0
- flowgrate-0.1.0/flowgrate/schema/__init__.py +11 -0
- flowgrate-0.1.0/flowgrate/schema/blueprint.py +141 -0
- flowgrate-0.1.0/flowgrate/schema/column_builder.py +135 -0
- flowgrate-0.1.0/flowgrate/schema/schema.py +55 -0
- flowgrate-0.1.0/flowgrate/schema/types.py +62 -0
- flowgrate-0.1.0/pyproject.toml +38 -0
- flowgrate-0.1.0/tests/__init__.py +0 -0
- flowgrate-0.1.0/tests/test_blueprint.py +175 -0
- flowgrate-0.1.0/tests/test_serializer.py +128 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
name: Test (Python ${{ matrix.python-version }})
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
22
|
+
uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python-version }}
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: |
|
|
28
|
+
python -m pip install --upgrade pip
|
|
29
|
+
pip install pytest
|
|
30
|
+
pip install -e .
|
|
31
|
+
|
|
32
|
+
- name: Run tests
|
|
33
|
+
run: pytest tests/ -v
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
name: Build and publish
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Set up Python
|
|
17
|
+
uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
|
|
21
|
+
- name: Install build tools
|
|
22
|
+
run: pip install hatchling build
|
|
23
|
+
|
|
24
|
+
- name: Set version from tag
|
|
25
|
+
run: |
|
|
26
|
+
VERSION=${GITHUB_REF_NAME#v}
|
|
27
|
+
sed -i "s/^version = .*/version = \"$VERSION\"/" pyproject.toml
|
|
28
|
+
|
|
29
|
+
- name: Run tests
|
|
30
|
+
run: |
|
|
31
|
+
pip install pytest
|
|
32
|
+
pip install -e .
|
|
33
|
+
pytest tests/ -v
|
|
34
|
+
|
|
35
|
+
- name: Build package
|
|
36
|
+
run: python -m build
|
|
37
|
+
|
|
38
|
+
- name: Publish to PyPI
|
|
39
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
40
|
+
with:
|
|
41
|
+
password: ${{ secrets.PYPI_API_KEY }}
|
flowgrate-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 flowgrate
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
flowgrate-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flowgrate
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Flowgrate — Laravel-style database migrations with a fluent API
|
|
5
|
+
Project-URL: Homepage, https://github.com/flowgrate/python
|
|
6
|
+
Project-URL: Repository, https://github.com/flowgrate/python
|
|
7
|
+
Project-URL: Issues, https://github.com/flowgrate/python/issues
|
|
8
|
+
Author-email: flowgrate <hi@flowgrate.dev>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: database,fluent,migrations,postgresql,schema
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Database
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# flowgrate/python
|
|
25
|
+
|
|
26
|
+
Python SDK for [Flowgrate](https://github.com/flowgrate/core) — Laravel-style database migrations with a fluent API.
|
|
27
|
+
|
|
28
|
+
## How it works
|
|
29
|
+
|
|
30
|
+
Define migrations in Python using the fluent Blueprint API. The SDK serializes them to JSON and pipes to the Flowgrate CLI, which compiles and executes the SQL.
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- Python 3.10+
|
|
35
|
+
- [Flowgrate CLI](https://github.com/flowgrate/core/releases) — download the binary for your platform and put it on your `PATH`
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Linux (amd64)
|
|
39
|
+
curl -L https://github.com/flowgrate/core/releases/latest/download/flowgrate-linux-amd64 -o flowgrate
|
|
40
|
+
chmod +x flowgrate
|
|
41
|
+
sudo mv flowgrate /usr/local/bin/
|
|
42
|
+
|
|
43
|
+
# macOS (Apple Silicon)
|
|
44
|
+
curl -L https://github.com/flowgrate/core/releases/latest/download/flowgrate-darwin-arm64 -o flowgrate
|
|
45
|
+
chmod +x flowgrate
|
|
46
|
+
sudo mv flowgrate /usr/local/bin/
|
|
47
|
+
|
|
48
|
+
# macOS (Intel)
|
|
49
|
+
curl -L https://github.com/flowgrate/core/releases/latest/download/flowgrate-darwin-amd64 -o flowgrate
|
|
50
|
+
chmod +x flowgrate
|
|
51
|
+
sudo mv flowgrate /usr/local/bin/
|
|
52
|
+
|
|
53
|
+
# Or build from source
|
|
54
|
+
go install github.com/flowgrate/core@latest
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Setup
|
|
58
|
+
|
|
59
|
+
**1. Install the SDK:**
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install flowgrate
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**2. Create `Program.py` (entry point for the CLI to invoke):**
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from flowgrate import FlowgrateRunner
|
|
69
|
+
import os
|
|
70
|
+
|
|
71
|
+
FlowgrateRunner.run(os.path.dirname(__file__))
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**3. Create `flowgrate.yml` next to your migrations:**
|
|
75
|
+
|
|
76
|
+
Generate it with the CLI (recommended):
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
flowgrate init --db=postgres://user:pass@localhost/mydb
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Or create manually:
|
|
83
|
+
|
|
84
|
+
```yaml
|
|
85
|
+
database:
|
|
86
|
+
url: postgres://user:pass@localhost/mydb
|
|
87
|
+
|
|
88
|
+
migrations:
|
|
89
|
+
project: ./migrations
|
|
90
|
+
sdk: python
|
|
91
|
+
run: python Program.py
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**4. Generate and run migrations:**
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
flowgrate make CreateUsersTable
|
|
98
|
+
flowgrate up
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Migration anatomy
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from flowgrate import Migration, Schema
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class CreateUsersTable(Migration):
|
|
108
|
+
def up(self):
|
|
109
|
+
with Schema.create("users") as t:
|
|
110
|
+
t.id()
|
|
111
|
+
t.string("name")
|
|
112
|
+
t.string("email", 100)
|
|
113
|
+
t.timestamps()
|
|
114
|
+
|
|
115
|
+
def down(self):
|
|
116
|
+
Schema.drop_if_exists("users")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Migration files must follow the naming convention: `YYYYMMDD_HHMMSS_MigrationName.py`
|
|
120
|
+
|
|
121
|
+
## Blueprint API reference
|
|
122
|
+
|
|
123
|
+
### Create / drop table
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
Schema.create("users") # CREATE TABLE
|
|
127
|
+
Schema.table("users") # ALTER TABLE
|
|
128
|
+
Schema.drop("users") # DROP TABLE
|
|
129
|
+
Schema.drop_if_exists("users") # DROP TABLE IF EXISTS
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Column types
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
t.id() # BIGSERIAL PRIMARY KEY
|
|
136
|
+
t.small_integer("level") # SMALLINT
|
|
137
|
+
t.integer("views") # INTEGER
|
|
138
|
+
t.big_integer("score") # BIGINT
|
|
139
|
+
t.decimal("price", 10, 2) # NUMERIC(10, 2)
|
|
140
|
+
t.float("rating") # REAL
|
|
141
|
+
t.double("latitude") # DOUBLE PRECISION
|
|
142
|
+
t.boolean("active") # BOOLEAN
|
|
143
|
+
t.string("name") # VARCHAR(255)
|
|
144
|
+
t.string("code", 10) # VARCHAR(10)
|
|
145
|
+
t.text("bio") # TEXT
|
|
146
|
+
t.uuid("public_id") # UUID
|
|
147
|
+
t.json("settings") # JSON
|
|
148
|
+
t.jsonb("metadata") # JSONB
|
|
149
|
+
t.binary("avatar") # BYTEA
|
|
150
|
+
t.date("birthday") # DATE
|
|
151
|
+
t.time("opens_at") # TIME
|
|
152
|
+
t.timestamp("verified_at") # TIMESTAMP
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Column modifiers (chainable)
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
.nullable() # NULL
|
|
159
|
+
.default(value) # DEFAULT value
|
|
160
|
+
.default_expression("NOW()") # DEFAULT NOW() — raw SQL
|
|
161
|
+
.generated_uuid() # DEFAULT gen_random_uuid()
|
|
162
|
+
.comment("description")
|
|
163
|
+
.unique() # single-column unique index
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Helpers
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
t.timestamps() # created_at + updated_at TIMESTAMP DEFAULT NOW()
|
|
170
|
+
t.soft_deletes() # deleted_at TIMESTAMP NULL
|
|
171
|
+
t.remember_token() # remember_token VARCHAR(100) NULL
|
|
172
|
+
t.polymorphic("commentable") # commentable_id BIGINT + commentable_type VARCHAR(255) + index
|
|
173
|
+
t.nullable_polymorphic("taggable")
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Foreign keys
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
t.foreign_id("role_id") \
|
|
180
|
+
.constrained("roles") \
|
|
181
|
+
.on_delete("cascade") \
|
|
182
|
+
.on_update("cascade")
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Indexes
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
t.unique("email", "tenant_id", name="uq_users_email_tenant")
|
|
189
|
+
t.index("created_at")
|
|
190
|
+
t.index("email", "name", name="idx_users_search")
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### ALTER TABLE
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
with Schema.table("users") as t:
|
|
197
|
+
t.add_column("phone").string(20).nullable()
|
|
198
|
+
t.change_column("name").string(500)
|
|
199
|
+
t.drop_column("avatar")
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Running in Docker
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
docker compose exec sdk python Program.py | flowgrate up
|
|
206
|
+
```
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# flowgrate/python
|
|
2
|
+
|
|
3
|
+
Python SDK for [Flowgrate](https://github.com/flowgrate/core) — Laravel-style database migrations with a fluent API.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
Define migrations in Python using the fluent Blueprint API. The SDK serializes them to JSON and pipes to the Flowgrate CLI, which compiles and executes the SQL.
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Python 3.10+
|
|
12
|
+
- [Flowgrate CLI](https://github.com/flowgrate/core/releases) — download the binary for your platform and put it on your `PATH`
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# Linux (amd64)
|
|
16
|
+
curl -L https://github.com/flowgrate/core/releases/latest/download/flowgrate-linux-amd64 -o flowgrate
|
|
17
|
+
chmod +x flowgrate
|
|
18
|
+
sudo mv flowgrate /usr/local/bin/
|
|
19
|
+
|
|
20
|
+
# macOS (Apple Silicon)
|
|
21
|
+
curl -L https://github.com/flowgrate/core/releases/latest/download/flowgrate-darwin-arm64 -o flowgrate
|
|
22
|
+
chmod +x flowgrate
|
|
23
|
+
sudo mv flowgrate /usr/local/bin/
|
|
24
|
+
|
|
25
|
+
# macOS (Intel)
|
|
26
|
+
curl -L https://github.com/flowgrate/core/releases/latest/download/flowgrate-darwin-amd64 -o flowgrate
|
|
27
|
+
chmod +x flowgrate
|
|
28
|
+
sudo mv flowgrate /usr/local/bin/
|
|
29
|
+
|
|
30
|
+
# Or build from source
|
|
31
|
+
go install github.com/flowgrate/core@latest
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Setup
|
|
35
|
+
|
|
36
|
+
**1. Install the SDK:**
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install flowgrate
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**2. Create `Program.py` (entry point for the CLI to invoke):**
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from flowgrate import FlowgrateRunner
|
|
46
|
+
import os
|
|
47
|
+
|
|
48
|
+
FlowgrateRunner.run(os.path.dirname(__file__))
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**3. Create `flowgrate.yml` next to your migrations:**
|
|
52
|
+
|
|
53
|
+
Generate it with the CLI (recommended):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
flowgrate init --db=postgres://user:pass@localhost/mydb
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Or create manually:
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
database:
|
|
63
|
+
url: postgres://user:pass@localhost/mydb
|
|
64
|
+
|
|
65
|
+
migrations:
|
|
66
|
+
project: ./migrations
|
|
67
|
+
sdk: python
|
|
68
|
+
run: python Program.py
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**4. Generate and run migrations:**
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
flowgrate make CreateUsersTable
|
|
75
|
+
flowgrate up
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Migration anatomy
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from flowgrate import Migration, Schema
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class CreateUsersTable(Migration):
|
|
85
|
+
def up(self):
|
|
86
|
+
with Schema.create("users") as t:
|
|
87
|
+
t.id()
|
|
88
|
+
t.string("name")
|
|
89
|
+
t.string("email", 100)
|
|
90
|
+
t.timestamps()
|
|
91
|
+
|
|
92
|
+
def down(self):
|
|
93
|
+
Schema.drop_if_exists("users")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Migration files must follow the naming convention: `YYYYMMDD_HHMMSS_MigrationName.py`
|
|
97
|
+
|
|
98
|
+
## Blueprint API reference
|
|
99
|
+
|
|
100
|
+
### Create / drop table
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
Schema.create("users") # CREATE TABLE
|
|
104
|
+
Schema.table("users") # ALTER TABLE
|
|
105
|
+
Schema.drop("users") # DROP TABLE
|
|
106
|
+
Schema.drop_if_exists("users") # DROP TABLE IF EXISTS
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Column types
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
t.id() # BIGSERIAL PRIMARY KEY
|
|
113
|
+
t.small_integer("level") # SMALLINT
|
|
114
|
+
t.integer("views") # INTEGER
|
|
115
|
+
t.big_integer("score") # BIGINT
|
|
116
|
+
t.decimal("price", 10, 2) # NUMERIC(10, 2)
|
|
117
|
+
t.float("rating") # REAL
|
|
118
|
+
t.double("latitude") # DOUBLE PRECISION
|
|
119
|
+
t.boolean("active") # BOOLEAN
|
|
120
|
+
t.string("name") # VARCHAR(255)
|
|
121
|
+
t.string("code", 10) # VARCHAR(10)
|
|
122
|
+
t.text("bio") # TEXT
|
|
123
|
+
t.uuid("public_id") # UUID
|
|
124
|
+
t.json("settings") # JSON
|
|
125
|
+
t.jsonb("metadata") # JSONB
|
|
126
|
+
t.binary("avatar") # BYTEA
|
|
127
|
+
t.date("birthday") # DATE
|
|
128
|
+
t.time("opens_at") # TIME
|
|
129
|
+
t.timestamp("verified_at") # TIMESTAMP
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Column modifiers (chainable)
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
.nullable() # NULL
|
|
136
|
+
.default(value) # DEFAULT value
|
|
137
|
+
.default_expression("NOW()") # DEFAULT NOW() — raw SQL
|
|
138
|
+
.generated_uuid() # DEFAULT gen_random_uuid()
|
|
139
|
+
.comment("description")
|
|
140
|
+
.unique() # single-column unique index
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Helpers
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
t.timestamps() # created_at + updated_at TIMESTAMP DEFAULT NOW()
|
|
147
|
+
t.soft_deletes() # deleted_at TIMESTAMP NULL
|
|
148
|
+
t.remember_token() # remember_token VARCHAR(100) NULL
|
|
149
|
+
t.polymorphic("commentable") # commentable_id BIGINT + commentable_type VARCHAR(255) + index
|
|
150
|
+
t.nullable_polymorphic("taggable")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Foreign keys
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
t.foreign_id("role_id") \
|
|
157
|
+
.constrained("roles") \
|
|
158
|
+
.on_delete("cascade") \
|
|
159
|
+
.on_update("cascade")
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Indexes
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
t.unique("email", "tenant_id", name="uq_users_email_tenant")
|
|
166
|
+
t.index("created_at")
|
|
167
|
+
t.index("email", "name", name="idx_users_search")
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### ALTER TABLE
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
with Schema.table("users") as t:
|
|
174
|
+
t.add_column("phone").string(20).nullable()
|
|
175
|
+
t.change_column("name").string(500)
|
|
176
|
+
t.drop_column("avatar")
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Running in Docker
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
docker compose exec sdk python Program.py | flowgrate up
|
|
183
|
+
```
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import importlib.util
|
|
3
|
+
import inspect
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from .migration import Migration
|
|
10
|
+
from .schema.schema import Schema, SchemaOperation
|
|
11
|
+
from .schema.types import ColumnDefinition, ColumnAction, IndexDefinition, ForeignKeyDefinition
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_MIGRATION_PATTERN = re.compile(r"^\d{8}_\d{6}_\w+\.py$")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> None:
|
|
18
|
+
"""Entry point: run migrations from the current directory."""
|
|
19
|
+
directory = sys.argv[1] if len(sys.argv) > 1 else os.path.dirname(os.path.abspath(sys.argv[0]))
|
|
20
|
+
FlowgrateRunner.run(directory)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FlowgrateRunner:
|
|
24
|
+
@classmethod
|
|
25
|
+
def run(cls, directory: str) -> None:
|
|
26
|
+
"""Discover all migrations in directory, serialize to JSON and write to stdout."""
|
|
27
|
+
migrations = cls._discover(directory)
|
|
28
|
+
for name, migration_cls in migrations:
|
|
29
|
+
manifest = cls._build_manifest(name, migration_cls())
|
|
30
|
+
print(json.dumps(manifest, ensure_ascii=False))
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def _discover(cls, directory: str) -> list[tuple[str, type[Migration]]]:
|
|
34
|
+
directory = os.path.abspath(directory)
|
|
35
|
+
files = sorted(
|
|
36
|
+
f for f in os.listdir(directory)
|
|
37
|
+
if _MIGRATION_PATTERN.match(f)
|
|
38
|
+
)
|
|
39
|
+
result = []
|
|
40
|
+
for filename in files:
|
|
41
|
+
path = os.path.join(directory, filename)
|
|
42
|
+
migration_cls = cls._load(path)
|
|
43
|
+
if migration_cls:
|
|
44
|
+
name = os.path.splitext(filename)[0]
|
|
45
|
+
result.append((name, migration_cls))
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def _load(cls, path: str) -> type[Migration] | None:
|
|
50
|
+
spec = importlib.util.spec_from_file_location("_migration", path)
|
|
51
|
+
if spec is None or spec.loader is None:
|
|
52
|
+
return None
|
|
53
|
+
module = importlib.util.module_from_spec(spec)
|
|
54
|
+
spec.loader.exec_module(module) # type: ignore[union-attr]
|
|
55
|
+
|
|
56
|
+
for _, obj in inspect.getmembers(module, inspect.isclass):
|
|
57
|
+
if issubclass(obj, Migration) and obj is not Migration:
|
|
58
|
+
return obj
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def _build_manifest(cls, name: str, migration: Migration) -> dict:
|
|
63
|
+
Schema.reset()
|
|
64
|
+
migration.up()
|
|
65
|
+
up = [cls._serialize_op(op) for op in Schema._operations]
|
|
66
|
+
|
|
67
|
+
Schema.reset()
|
|
68
|
+
migration.down()
|
|
69
|
+
down = [cls._serialize_op(op) for op in Schema._operations]
|
|
70
|
+
|
|
71
|
+
Schema.reset()
|
|
72
|
+
return {"migration": name, "up": up, "down": down}
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def _serialize_op(cls, op: SchemaOperation) -> dict:
|
|
76
|
+
result: dict = {"action": op.action, "table": op.table}
|
|
77
|
+
|
|
78
|
+
if op.if_exists:
|
|
79
|
+
result["if_exists"] = True
|
|
80
|
+
|
|
81
|
+
if op.blueprint:
|
|
82
|
+
bp = op.blueprint
|
|
83
|
+
is_alter = op.action == "alter_table"
|
|
84
|
+
if bp.columns:
|
|
85
|
+
result["columns"] = [cls._serialize_col(c, is_alter) for c in bp.columns]
|
|
86
|
+
if bp.indexes:
|
|
87
|
+
result["indexes"] = [cls._serialize_idx(i) for i in bp.indexes]
|
|
88
|
+
if bp.foreign_keys:
|
|
89
|
+
result["foreign_keys"] = [cls._serialize_fk(f) for f in bp.foreign_keys]
|
|
90
|
+
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def _serialize_col(cls, col: ColumnDefinition, in_alter: bool = False) -> dict:
|
|
95
|
+
d: dict = {"name": col.name, "type": col.type.value}
|
|
96
|
+
|
|
97
|
+
if in_alter or col.action != ColumnAction.ADD:
|
|
98
|
+
d["column_action"] = col.action.value
|
|
99
|
+
if col.length is not None:
|
|
100
|
+
d["length"] = col.length
|
|
101
|
+
if col.precision is not None:
|
|
102
|
+
d["precision"] = col.precision
|
|
103
|
+
if col.scale is not None:
|
|
104
|
+
d["scale"] = col.scale
|
|
105
|
+
if col.nullable:
|
|
106
|
+
d["nullable"] = True
|
|
107
|
+
if col.primary:
|
|
108
|
+
d["primary"] = True
|
|
109
|
+
if col.auto_increment:
|
|
110
|
+
d["auto_increment"] = True
|
|
111
|
+
if col.has_default:
|
|
112
|
+
if col.is_default_expression:
|
|
113
|
+
d["default_expression"] = col.default_value
|
|
114
|
+
else:
|
|
115
|
+
d["default"] = col.default_value
|
|
116
|
+
if col.comment:
|
|
117
|
+
d["comment"] = col.comment
|
|
118
|
+
|
|
119
|
+
return d
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def _serialize_idx(cls, idx: IndexDefinition) -> dict:
|
|
123
|
+
d: dict = {"columns": idx.columns}
|
|
124
|
+
if idx.unique:
|
|
125
|
+
d["unique"] = True
|
|
126
|
+
if idx.name:
|
|
127
|
+
d["name"] = idx.name
|
|
128
|
+
return d
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def _serialize_fk(cls, fk: ForeignKeyDefinition) -> dict:
|
|
132
|
+
d: dict = {
|
|
133
|
+
"column": fk.column,
|
|
134
|
+
"references_table": fk.references_table,
|
|
135
|
+
"references_column": fk.references_column,
|
|
136
|
+
}
|
|
137
|
+
if fk.on_update:
|
|
138
|
+
d["on_update"] = fk.on_update
|
|
139
|
+
if fk.on_delete:
|
|
140
|
+
d["on_delete"] = fk.on_delete
|
|
141
|
+
return d
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .types import ColumnType, ColumnAction, ColumnDefinition, IndexDefinition, ForeignKeyDefinition
|
|
2
|
+
from .blueprint import Blueprint
|
|
3
|
+
from .column_builder import ColumnBuilder
|
|
4
|
+
from .schema import Schema, SchemaOperation
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"ColumnType", "ColumnAction", "ColumnDefinition",
|
|
8
|
+
"IndexDefinition", "ForeignKeyDefinition",
|
|
9
|
+
"Blueprint", "ColumnBuilder",
|
|
10
|
+
"Schema", "SchemaOperation",
|
|
11
|
+
]
|