mizzle 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.
- mizzle-0.1.0/.gitignore +19 -0
- mizzle-0.1.0/LICENSE +21 -0
- mizzle-0.1.0/PKG-INFO +175 -0
- mizzle-0.1.0/README.md +146 -0
- mizzle-0.1.0/assets/icon-1024.png +0 -0
- mizzle-0.1.0/assets/icon-512.png +0 -0
- mizzle-0.1.0/assets/icon.svg +1 -0
- mizzle-0.1.0/assets/logo-dark.png +0 -0
- mizzle-0.1.0/assets/logo-dark.svg +1 -0
- mizzle-0.1.0/assets/logo.png +0 -0
- mizzle-0.1.0/assets/logo.svg +1 -0
- mizzle-0.1.0/assets/mark-1024.png +0 -0
- mizzle-0.1.0/assets/mark-512.png +0 -0
- mizzle-0.1.0/assets/mark-dark-1024.png +0 -0
- mizzle-0.1.0/assets/mark-dark-512.png +0 -0
- mizzle-0.1.0/assets/mark-dark.svg +1 -0
- mizzle-0.1.0/assets/mark.svg +1 -0
- mizzle-0.1.0/assets/social-preview.png +0 -0
- mizzle-0.1.0/examples/social/.env.example +4 -0
- mizzle-0.1.0/examples/social/README.md +49 -0
- mizzle-0.1.0/examples/social/migrations/0001_social_schema/migration.sql +88 -0
- mizzle-0.1.0/examples/social/migrations/0001_social_schema/snapshot.json +400 -0
- mizzle-0.1.0/examples/social/migrations/0002_create_table_bookmarks/migration.sql +17 -0
- mizzle-0.1.0/examples/social/migrations/0002_create_table_bookmarks/snapshot.json +477 -0
- mizzle-0.1.0/examples/social/migrations/meta/journal.json +18 -0
- mizzle-0.1.0/examples/social/mizzle.toml +6 -0
- mizzle-0.1.0/examples/social/pyproject.toml +52 -0
- mizzle-0.1.0/examples/social/social/__init__.py +0 -0
- mizzle-0.1.0/examples/social/social/app.py +311 -0
- mizzle-0.1.0/examples/social/social/models.py +69 -0
- mizzle-0.1.0/examples/social/social/schema.py +120 -0
- mizzle-0.1.0/examples/social/social/seed.py +154 -0
- mizzle-0.1.0/examples/social/tests/__init__.py +0 -0
- mizzle-0.1.0/examples/social/tests/test_api.py +133 -0
- mizzle-0.1.0/examples/social/uv.lock +531 -0
- mizzle-0.1.0/pyproject.toml +143 -0
- mizzle-0.1.0/release/mizzle-studio-0.1.0.zip +0 -0
- mizzle-0.1.0/src/mizzle/__init__.py +124 -0
- mizzle-0.1.0/src/mizzle/__main__.py +5 -0
- mizzle-0.1.0/src/mizzle/core/__init__.py +0 -0
- mizzle-0.1.0/src/mizzle/core/compiler.py +365 -0
- mizzle-0.1.0/src/mizzle/core/errors.py +78 -0
- mizzle-0.1.0/src/mizzle/core/naming.py +13 -0
- mizzle-0.1.0/src/mizzle/core/query/__init__.py +0 -0
- mizzle-0.1.0/src/mizzle/core/query/base.py +82 -0
- mizzle-0.1.0/src/mizzle/core/query/builder.py +205 -0
- mizzle-0.1.0/src/mizzle/core/query/insert.py +131 -0
- mizzle-0.1.0/src/mizzle/core/query/relational.py +271 -0
- mizzle-0.1.0/src/mizzle/core/query/returning.py +22 -0
- mizzle-0.1.0/src/mizzle/core/query/select.py +214 -0
- mizzle-0.1.0/src/mizzle/core/query/update.py +78 -0
- mizzle-0.1.0/src/mizzle/core/schema/__init__.py +0 -0
- mizzle-0.1.0/src/mizzle/core/schema/column.py +231 -0
- mizzle-0.1.0/src/mizzle/core/schema/constraints.py +85 -0
- mizzle-0.1.0/src/mizzle/core/schema/relations.py +191 -0
- mizzle-0.1.0/src/mizzle/core/schema/table.py +144 -0
- mizzle-0.1.0/src/mizzle/core/schema/types.py +61 -0
- mizzle-0.1.0/src/mizzle/core/snapshot.py +98 -0
- mizzle-0.1.0/src/mizzle/core/sql/__init__.py +0 -0
- mizzle-0.1.0/src/mizzle/core/sql/nodes.py +158 -0
- mizzle-0.1.0/src/mizzle/core/sql/operators.py +213 -0
- mizzle-0.1.0/src/mizzle/core/sql/raw.py +83 -0
- mizzle-0.1.0/src/mizzle/core/steps.py +283 -0
- mizzle-0.1.0/src/mizzle/db.py +237 -0
- mizzle-0.1.0/src/mizzle/dialects/__init__.py +0 -0
- mizzle-0.1.0/src/mizzle/dialects/base.py +62 -0
- mizzle-0.1.0/src/mizzle/dialects/postgres/__init__.py +87 -0
- mizzle-0.1.0/src/mizzle/dialects/postgres/compiler.py +128 -0
- mizzle-0.1.0/src/mizzle/dialects/postgres/ddl.py +280 -0
- mizzle-0.1.0/src/mizzle/dialects/postgres/driver.py +166 -0
- mizzle-0.1.0/src/mizzle/dialects/postgres/introspect.py +209 -0
- mizzle-0.1.0/src/mizzle/dialects/postgres/types.py +117 -0
- mizzle-0.1.0/src/mizzle/kit/__init__.py +27 -0
- mizzle-0.1.0/src/mizzle/kit/cli.py +424 -0
- mizzle-0.1.0/src/mizzle/kit/codegen.py +309 -0
- mizzle-0.1.0/src/mizzle/kit/config.py +182 -0
- mizzle-0.1.0/src/mizzle/kit/diff.py +385 -0
- mizzle-0.1.0/src/mizzle/kit/init.py +105 -0
- mizzle-0.1.0/src/mizzle/kit/journal.py +183 -0
- mizzle-0.1.0/src/mizzle/kit/migrate.py +96 -0
- mizzle-0.1.0/src/mizzle/kit/snapshot.py +156 -0
- mizzle-0.1.0/src/mizzle/pg.py +57 -0
- mizzle-0.1.0/src/mizzle/py.typed +0 -0
- mizzle-0.1.0/src/mizzle/pydantic.py +158 -0
- mizzle-0.1.0/src/mizzle/studio/__init__.py +0 -0
- mizzle-0.1.0/src/mizzle/studio/assets.py +276 -0
- mizzle-0.1.0/src/mizzle/studio/server.py +458 -0
- mizzle-0.1.0/src/mizzle/studio/ui.json +5 -0
- mizzle-0.1.0/tests/__init__.py +0 -0
- mizzle-0.1.0/tests/conftest.py +4 -0
- mizzle-0.1.0/tests/integration/__init__.py +0 -0
- mizzle-0.1.0/tests/integration/conftest.py +51 -0
- mizzle-0.1.0/tests/integration/test_database.py +240 -0
- mizzle-0.1.0/tests/integration/test_kit_cli.py +244 -0
- mizzle-0.1.0/tests/integration/test_relational.py +196 -0
- mizzle-0.1.0/tests/integration/test_roundtrip.py +202 -0
- mizzle-0.1.0/tests/integration/test_studio.py +128 -0
- mizzle-0.1.0/tests/models.py +34 -0
- mizzle-0.1.0/tests/typing/__init__.py +0 -0
- mizzle-0.1.0/tests/typing/check_types.py +66 -0
- mizzle-0.1.0/tests/unit/__init__.py +0 -0
- mizzle-0.1.0/tests/unit/conftest.py +39 -0
- mizzle-0.1.0/tests/unit/test_columns.py +102 -0
- mizzle-0.1.0/tests/unit/test_compiler.py +151 -0
- mizzle-0.1.0/tests/unit/test_errors.py +24 -0
- mizzle-0.1.0/tests/unit/test_kit.py +252 -0
- mizzle-0.1.0/tests/unit/test_mutations.py +192 -0
- mizzle-0.1.0/tests/unit/test_pydantic.py +110 -0
- mizzle-0.1.0/tests/unit/test_raw_py314.py +22 -0
- mizzle-0.1.0/tests/unit/test_select.py +177 -0
- mizzle-0.1.0/tests/unit/test_studio_assets.py +168 -0
- mizzle-0.1.0/tests/unit/test_tables.py +111 -0
- mizzle-0.1.0/uv.lock +919 -0
mizzle-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.pyc
|
|
3
|
+
.venv/
|
|
4
|
+
.coverage
|
|
5
|
+
htmlcov/
|
|
6
|
+
dist/
|
|
7
|
+
.pytest_cache/
|
|
8
|
+
.mypy_cache/
|
|
9
|
+
.ruff_cache/
|
|
10
|
+
|
|
11
|
+
# Studio UI: local dev build, and the release manifest (written by release.yml)
|
|
12
|
+
src/mizzle/studio/static/
|
|
13
|
+
src/mizzle/studio/ui.json
|
|
14
|
+
|
|
15
|
+
# Local secrets (database URLs, passwords)
|
|
16
|
+
.env
|
|
17
|
+
|
|
18
|
+
# Local AI-assistant working notes (design specs, plans)
|
|
19
|
+
docs/superpowers/
|
mizzle-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mizzle contributors
|
|
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.
|
mizzle-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mizzle
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fully typed async ORM for PostgreSQL: SQL-like queries, schema-driven migrations and a data studio
|
|
5
|
+
Project-URL: Homepage, https://github.com/mizzlehq/mizzle
|
|
6
|
+
Project-URL: Repository, https://github.com/mizzlehq/mizzle
|
|
7
|
+
Project-URL: Issues, https://github.com/mizzlehq/mizzle/issues
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: async,asyncpg,migrations,orm,postgresql,query-builder,sql,typed
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Database
|
|
19
|
+
Classifier: Topic :: Database :: Front-Ends
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Requires-Dist: asyncpg>=0.30
|
|
23
|
+
Provides-Extra: pydantic
|
|
24
|
+
Requires-Dist: pydantic>=2.7; extra == 'pydantic'
|
|
25
|
+
Provides-Extra: studio
|
|
26
|
+
Requires-Dist: starlette>=0.40; extra == 'studio'
|
|
27
|
+
Requires-Dist: uvicorn>=0.30; extra == 'studio'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
<picture>
|
|
32
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/logo-dark.svg">
|
|
33
|
+
<img alt="mizzle" src="assets/logo.svg" width="320">
|
|
34
|
+
</picture>
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
# mizzle
|
|
38
|
+
|
|
39
|
+
A fully typed, async ORM for Python and PostgreSQL: write queries that read like SQL,
|
|
40
|
+
get typed rows back, and evolve your schema with generated migrations. If you know
|
|
41
|
+
[Drizzle](https://orm.drizzle.team) from TypeScript, mizzle will feel familiar.
|
|
42
|
+
|
|
43
|
+
- **Schema in Python classes.** Result rows are typed for pyright and mypy, with no code
|
|
44
|
+
generation.
|
|
45
|
+
- **Two query styles:** a SQL-like builder (`select`, `insert`, `update`, `delete`, joins,
|
|
46
|
+
CTEs, upserts) and relational queries that load nested data in one SQL statement.
|
|
47
|
+
- **Migration kit:** `generate`, `migrate`, `push`, `pull`, `check`, `drop`, `typegen`.
|
|
48
|
+
- **Studio:** browse and edit data in your browser (`mizzle studio`).
|
|
49
|
+
- **Safe by default:** values are always bind parameters, `update`/`delete` refuse to run
|
|
50
|
+
without `.where()`, migrations are locked, transactional and tamper-checked, and errors
|
|
51
|
+
never include row values.
|
|
52
|
+
|
|
53
|
+
Built for PostgreSQL first; other SQL databases plug in through the dialect layer
|
|
54
|
+
(`src/mizzle/dialects/`).
|
|
55
|
+
|
|
56
|
+
A complete example app (FastAPI, migrations, seed data, tests) lives in
|
|
57
|
+
[`examples/social`](examples/social).
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install mizzle # ORM + migration CLI
|
|
63
|
+
pip install "mizzle[studio]" # + Studio (its UI downloads into your venv on first run, ~250 KB)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Schema
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
# app/db/schema.py
|
|
70
|
+
import enum
|
|
71
|
+
from typing import final, override
|
|
72
|
+
|
|
73
|
+
from mizzle import Table, index, many, one, sql
|
|
74
|
+
from mizzle.pg import integer, pg_enum, serial, text, timestamp, varchar
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Role(enum.StrEnum):
|
|
78
|
+
ADMIN = "admin"
|
|
79
|
+
USER = "user"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@final
|
|
83
|
+
class Users(Table, name="users"):
|
|
84
|
+
id = serial().primary_key() # Column[int]
|
|
85
|
+
email = varchar(255).not_null().unique() # Column[str]
|
|
86
|
+
bio = text() # Column[str | None]
|
|
87
|
+
role = pg_enum(Role).not_null().default(Role.USER) # Column[Role]
|
|
88
|
+
created_at = timestamp(tz=True).not_null().default(sql("now()"))
|
|
89
|
+
|
|
90
|
+
posts = many(lambda: Posts)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@final
|
|
94
|
+
class Posts(Table, name="posts"):
|
|
95
|
+
id = serial().primary_key()
|
|
96
|
+
author_id = integer().not_null().references(lambda: Users.id, on_delete="cascade")
|
|
97
|
+
title = text().not_null()
|
|
98
|
+
|
|
99
|
+
author = one(lambda: Users, fields=lambda: [Posts.author_id], references=lambda: [Users.id])
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
@override
|
|
103
|
+
def config(cls):
|
|
104
|
+
return [index("posts_author_idx").on(cls.author_id)]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Queries
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from mizzle import Database, desc, eq
|
|
111
|
+
|
|
112
|
+
async with await Database.connect("postgresql://...", schema=[Users, Posts]) as db:
|
|
113
|
+
[ann] = await db.insert(Users).values(email="ann@x.io").returning() # Users
|
|
114
|
+
admins = await db.select().from_(Users).where(eq(Users.role, Role.ADMIN)) # list[Users]
|
|
115
|
+
pairs = await (
|
|
116
|
+
db.select(Users, Posts).from_(Users).left_join(Posts, eq(Posts.author_id, Users.id))
|
|
117
|
+
) # list[tuple[Users, Posts | None]]
|
|
118
|
+
|
|
119
|
+
# Relational: one SQL statement, typed nested results.
|
|
120
|
+
users = await db.query(Users).find_many(
|
|
121
|
+
with_={"posts": {"order_by": desc(Posts.id), "limit": 5}},
|
|
122
|
+
)
|
|
123
|
+
users[0].posts[0].title # str
|
|
124
|
+
|
|
125
|
+
async with db.transaction() as tx:
|
|
126
|
+
await tx.update(Users).set(bio="hi").where(eq(Users.id, ann.id))
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Raw SQL: `sql(t"select * from users where id = {user_id}")` (Python 3.14+) or
|
|
130
|
+
`sql("... where id = {}", user_id)`. Values become bind parameters. **Never build the text
|
|
131
|
+
passed to `sql()` from user input** (for example with an f-string).
|
|
132
|
+
|
|
133
|
+
## Migrations
|
|
134
|
+
|
|
135
|
+
`mizzle init` creates `mizzle.toml`, a starter schema module and the migrations folder
|
|
136
|
+
(`--pyproject` puts the settings in `[tool.mizzle]` of `pyproject.toml` instead):
|
|
137
|
+
|
|
138
|
+
```toml
|
|
139
|
+
schema = "app.db.schema" # module(s) with your tables
|
|
140
|
+
out = "migrations" # commit this folder
|
|
141
|
+
url_env = "DATABASE_URL" # the URL is read from the environment
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
mizzle init # set up mizzle.toml, db/schema.py and migrations/
|
|
146
|
+
mizzle generate # write migrations/0001_.../migration.sql + snapshot.json
|
|
147
|
+
mizzle migrate # apply pending migrations (safe with many app servers)
|
|
148
|
+
mizzle push # dev: sync schema straight to the database
|
|
149
|
+
mizzle pull # existing database -> your schema module (or pulled_schema.py next to it)
|
|
150
|
+
mizzle check # find broken or clashing migrations
|
|
151
|
+
mizzle drop # delete the latest, unapplied migration
|
|
152
|
+
mizzle typegen # TypedDicts for insert/update values
|
|
153
|
+
mizzle studio # open Studio
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Renames: on a terminal, `generate` asks whether a new column is a rename. In CI pass
|
|
157
|
+
`--rename users.name=full_name`. Changes that can lose data need confirmation or
|
|
158
|
+
`--allow-data-loss`. From code: `await mizzle.kit.migrate(db, "migrations")`.
|
|
159
|
+
|
|
160
|
+
## Development
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
docker compose up -d
|
|
164
|
+
export MIZZLE_TEST_URL=postgresql://mizzle:mizzle@localhost:54329/mizzle
|
|
165
|
+
uv run pytest # integration tests skip when MIZZLE_TEST_URL is unset
|
|
166
|
+
uv run pyright && uv run mypy
|
|
167
|
+
uv run ruff check && uv run ruff format --check
|
|
168
|
+
|
|
169
|
+
cd studio-ui && pnpm install && pnpm build # builds the UI into src/mizzle/studio/static
|
|
170
|
+
pnpm dev # UI dev server; proxies /api to `mizzle studio`
|
|
171
|
+
pnpm check # Biome: format, lint, sort imports (--write)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Releases: push a `v*` tag. `.github/workflows/release.yml` builds the UI, packages it into
|
|
175
|
+
the wheel, and publishes to PyPI with trusted publishing.
|
mizzle-0.1.0/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/logo-dark.svg">
|
|
4
|
+
<img alt="mizzle" src="assets/logo.svg" width="320">
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
# mizzle
|
|
9
|
+
|
|
10
|
+
A fully typed, async ORM for Python and PostgreSQL: write queries that read like SQL,
|
|
11
|
+
get typed rows back, and evolve your schema with generated migrations. If you know
|
|
12
|
+
[Drizzle](https://orm.drizzle.team) from TypeScript, mizzle will feel familiar.
|
|
13
|
+
|
|
14
|
+
- **Schema in Python classes.** Result rows are typed for pyright and mypy, with no code
|
|
15
|
+
generation.
|
|
16
|
+
- **Two query styles:** a SQL-like builder (`select`, `insert`, `update`, `delete`, joins,
|
|
17
|
+
CTEs, upserts) and relational queries that load nested data in one SQL statement.
|
|
18
|
+
- **Migration kit:** `generate`, `migrate`, `push`, `pull`, `check`, `drop`, `typegen`.
|
|
19
|
+
- **Studio:** browse and edit data in your browser (`mizzle studio`).
|
|
20
|
+
- **Safe by default:** values are always bind parameters, `update`/`delete` refuse to run
|
|
21
|
+
without `.where()`, migrations are locked, transactional and tamper-checked, and errors
|
|
22
|
+
never include row values.
|
|
23
|
+
|
|
24
|
+
Built for PostgreSQL first; other SQL databases plug in through the dialect layer
|
|
25
|
+
(`src/mizzle/dialects/`).
|
|
26
|
+
|
|
27
|
+
A complete example app (FastAPI, migrations, seed data, tests) lives in
|
|
28
|
+
[`examples/social`](examples/social).
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install mizzle # ORM + migration CLI
|
|
34
|
+
pip install "mizzle[studio]" # + Studio (its UI downloads into your venv on first run, ~250 KB)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Schema
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
# app/db/schema.py
|
|
41
|
+
import enum
|
|
42
|
+
from typing import final, override
|
|
43
|
+
|
|
44
|
+
from mizzle import Table, index, many, one, sql
|
|
45
|
+
from mizzle.pg import integer, pg_enum, serial, text, timestamp, varchar
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Role(enum.StrEnum):
|
|
49
|
+
ADMIN = "admin"
|
|
50
|
+
USER = "user"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@final
|
|
54
|
+
class Users(Table, name="users"):
|
|
55
|
+
id = serial().primary_key() # Column[int]
|
|
56
|
+
email = varchar(255).not_null().unique() # Column[str]
|
|
57
|
+
bio = text() # Column[str | None]
|
|
58
|
+
role = pg_enum(Role).not_null().default(Role.USER) # Column[Role]
|
|
59
|
+
created_at = timestamp(tz=True).not_null().default(sql("now()"))
|
|
60
|
+
|
|
61
|
+
posts = many(lambda: Posts)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@final
|
|
65
|
+
class Posts(Table, name="posts"):
|
|
66
|
+
id = serial().primary_key()
|
|
67
|
+
author_id = integer().not_null().references(lambda: Users.id, on_delete="cascade")
|
|
68
|
+
title = text().not_null()
|
|
69
|
+
|
|
70
|
+
author = one(lambda: Users, fields=lambda: [Posts.author_id], references=lambda: [Users.id])
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
@override
|
|
74
|
+
def config(cls):
|
|
75
|
+
return [index("posts_author_idx").on(cls.author_id)]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Queries
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from mizzle import Database, desc, eq
|
|
82
|
+
|
|
83
|
+
async with await Database.connect("postgresql://...", schema=[Users, Posts]) as db:
|
|
84
|
+
[ann] = await db.insert(Users).values(email="ann@x.io").returning() # Users
|
|
85
|
+
admins = await db.select().from_(Users).where(eq(Users.role, Role.ADMIN)) # list[Users]
|
|
86
|
+
pairs = await (
|
|
87
|
+
db.select(Users, Posts).from_(Users).left_join(Posts, eq(Posts.author_id, Users.id))
|
|
88
|
+
) # list[tuple[Users, Posts | None]]
|
|
89
|
+
|
|
90
|
+
# Relational: one SQL statement, typed nested results.
|
|
91
|
+
users = await db.query(Users).find_many(
|
|
92
|
+
with_={"posts": {"order_by": desc(Posts.id), "limit": 5}},
|
|
93
|
+
)
|
|
94
|
+
users[0].posts[0].title # str
|
|
95
|
+
|
|
96
|
+
async with db.transaction() as tx:
|
|
97
|
+
await tx.update(Users).set(bio="hi").where(eq(Users.id, ann.id))
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Raw SQL: `sql(t"select * from users where id = {user_id}")` (Python 3.14+) or
|
|
101
|
+
`sql("... where id = {}", user_id)`. Values become bind parameters. **Never build the text
|
|
102
|
+
passed to `sql()` from user input** (for example with an f-string).
|
|
103
|
+
|
|
104
|
+
## Migrations
|
|
105
|
+
|
|
106
|
+
`mizzle init` creates `mizzle.toml`, a starter schema module and the migrations folder
|
|
107
|
+
(`--pyproject` puts the settings in `[tool.mizzle]` of `pyproject.toml` instead):
|
|
108
|
+
|
|
109
|
+
```toml
|
|
110
|
+
schema = "app.db.schema" # module(s) with your tables
|
|
111
|
+
out = "migrations" # commit this folder
|
|
112
|
+
url_env = "DATABASE_URL" # the URL is read from the environment
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
mizzle init # set up mizzle.toml, db/schema.py and migrations/
|
|
117
|
+
mizzle generate # write migrations/0001_.../migration.sql + snapshot.json
|
|
118
|
+
mizzle migrate # apply pending migrations (safe with many app servers)
|
|
119
|
+
mizzle push # dev: sync schema straight to the database
|
|
120
|
+
mizzle pull # existing database -> your schema module (or pulled_schema.py next to it)
|
|
121
|
+
mizzle check # find broken or clashing migrations
|
|
122
|
+
mizzle drop # delete the latest, unapplied migration
|
|
123
|
+
mizzle typegen # TypedDicts for insert/update values
|
|
124
|
+
mizzle studio # open Studio
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Renames: on a terminal, `generate` asks whether a new column is a rename. In CI pass
|
|
128
|
+
`--rename users.name=full_name`. Changes that can lose data need confirmation or
|
|
129
|
+
`--allow-data-loss`. From code: `await mizzle.kit.migrate(db, "migrations")`.
|
|
130
|
+
|
|
131
|
+
## Development
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
docker compose up -d
|
|
135
|
+
export MIZZLE_TEST_URL=postgresql://mizzle:mizzle@localhost:54329/mizzle
|
|
136
|
+
uv run pytest # integration tests skip when MIZZLE_TEST_URL is unset
|
|
137
|
+
uv run pyright && uv run mypy
|
|
138
|
+
uv run ruff check && uv run ruff format --check
|
|
139
|
+
|
|
140
|
+
cd studio-ui && pnpm install && pnpm build # builds the UI into src/mizzle/studio/static
|
|
141
|
+
pnpm dev # UI dev server; proxies /api to `mizzle studio`
|
|
142
|
+
pnpm check # Biome: format, lint, sort imports (--write)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Releases: push a `v*` tag. `.github/workflows/release.yml` builds the UI, packages it into
|
|
146
|
+
the wheel, and publishes to PyPI with trusted publishing.
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#0B1020"/><g stroke-width="5" stroke-linecap="round" transform="translate(6.4 6.4) scale(0.8)"><path d="M14.0 20.0H50.0" stroke="#93B4FF"/><path d="M22.0 32.0H54.0" stroke="#5F86EE"/><path d="M10.0 44.0H38.0" stroke="#3A5BB8"/></g></svg>
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 186 64" role="img" aria-label="mizzle"><g stroke-width="6" stroke-linecap="round"><path d="M12 20.0H48" stroke="#93B4FF"/><path d="M20 32.0H52" stroke="#5F86EE"/><path d="M8 44.0H36" stroke="#3A5BB8"/></g><!-- "mizzle" set in Geist (SIL Open Font License), converted to outlines --><path transform="translate(64 43)" fill="#F8FAFC" d="M2.584 0.0V-20.33H7.353L7.524 -15.389999999999999L7.03 -15.618Q7.41 -17.252 8.265 -18.4015Q9.12 -19.551 10.3455 -20.168499999999998Q11.571 -20.785999999999998 13.033999999999999 -20.785999999999998Q15.599 -20.785999999999998 17.185499999999998 -19.313499999999998Q18.772 -17.841 19.209 -15.314L18.525 -15.295Q18.867 -17.081 19.7125 -18.296999999999997Q20.558 -19.512999999999998 21.8215 -20.149499999999996Q23.085 -20.785999999999998 24.662 -20.785999999999998Q26.771 -20.785999999999998 28.291 -19.902499999999996Q29.811 -19.019 30.6375 -17.2995Q31.464 -15.58 31.464 -13.072V0.0H26.182V-11.647Q26.182 -14.097999999999999 25.3935 -15.3235Q24.605 -16.549 22.913999999999998 -16.549Q21.812 -16.549 21.033 -15.96Q20.253999999999998 -15.371 19.836 -14.25Q19.418 -13.129 19.418 -11.533V0.0H14.591999999999999V-11.533Q14.591999999999999 -13.965 13.87 -15.257Q13.148 -16.549 11.381 -16.549Q10.279 -16.549 9.5 -15.96Q8.721 -15.371 8.2935 -14.2405Q7.866 -13.11 7.866 -11.533V0.0ZM35.872 0.0V-20.33H41.153999999999996V0.0ZM35.777 -22.933V-27.378999999999998H41.248999999999995V-22.933ZM44.878 0.0V-4.256L56.354 -16.320999999999998H45.334V-20.33H62.643V-16.073999999999998L51.015 -4.009H62.928V0.0ZM65.968 0.0V-4.256L77.444 -16.320999999999998H66.42399999999999V-20.33H83.733V-16.073999999999998L72.10499999999999 -4.009H84.018V0.0ZM92.834 0.0Q90.47800000000001 0.0 89.11000000000001 -1.197Q87.742 -2.394 87.742 -5.054V-26.98H93.024V-5.6049999999999995Q93.024 -4.769 93.4325 -4.389Q93.84100000000001 -4.009 94.601 -4.009H96.026V0.0ZM107.53999999999999 0.45599999999999996Q104.462 0.45599999999999996 102.19149999999999 -0.855Q99.92099999999999 -2.166 98.6765 -4.56Q97.432 -6.954 97.432 -10.165Q97.432 -13.376 98.6765 -15.751000000000001Q99.92099999999999 -18.126 102.1725 -19.456Q104.424 -20.785999999999998 107.464 -20.785999999999998Q110.428 -20.785999999999998 112.64150000000001 -19.474999999999998Q114.855 -18.163999999999998 116.0615 -15.713Q117.268 -13.262 117.268 -9.842V-8.721H102.904Q103.037 -6.175 104.291 -4.9115Q105.545 -3.6479999999999997 107.616 -3.6479999999999997Q109.155 -3.6479999999999997 110.181 -4.3225Q111.207 -4.997 111.625 -6.289L116.983 -5.966Q116.166 -2.964 113.6485 -1.254Q111.131 0.45599999999999996 107.53999999999999 0.45599999999999996ZM102.904 -12.103H111.834Q111.701 -14.459 110.5135 -15.6085Q109.326 -16.758 107.464 -16.758Q105.602 -16.758 104.386 -15.551499999999999Q103.17 -14.344999999999999 102.904 -12.103Z"/></svg>
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 186 64" role="img" aria-label="mizzle"><g stroke-width="6" stroke-linecap="round"><path d="M12 20.0H48" stroke="#1D4ED8"/><path d="M20 32.0H52" stroke="#5B7FE3"/><path d="M8 44.0H36" stroke="#A3B8F1"/></g><!-- "mizzle" set in Geist (SIL Open Font License), converted to outlines --><path transform="translate(64 43)" fill="#0F172A" d="M2.584 0.0V-20.33H7.353L7.524 -15.389999999999999L7.03 -15.618Q7.41 -17.252 8.265 -18.4015Q9.12 -19.551 10.3455 -20.168499999999998Q11.571 -20.785999999999998 13.033999999999999 -20.785999999999998Q15.599 -20.785999999999998 17.185499999999998 -19.313499999999998Q18.772 -17.841 19.209 -15.314L18.525 -15.295Q18.867 -17.081 19.7125 -18.296999999999997Q20.558 -19.512999999999998 21.8215 -20.149499999999996Q23.085 -20.785999999999998 24.662 -20.785999999999998Q26.771 -20.785999999999998 28.291 -19.902499999999996Q29.811 -19.019 30.6375 -17.2995Q31.464 -15.58 31.464 -13.072V0.0H26.182V-11.647Q26.182 -14.097999999999999 25.3935 -15.3235Q24.605 -16.549 22.913999999999998 -16.549Q21.812 -16.549 21.033 -15.96Q20.253999999999998 -15.371 19.836 -14.25Q19.418 -13.129 19.418 -11.533V0.0H14.591999999999999V-11.533Q14.591999999999999 -13.965 13.87 -15.257Q13.148 -16.549 11.381 -16.549Q10.279 -16.549 9.5 -15.96Q8.721 -15.371 8.2935 -14.2405Q7.866 -13.11 7.866 -11.533V0.0ZM35.872 0.0V-20.33H41.153999999999996V0.0ZM35.777 -22.933V-27.378999999999998H41.248999999999995V-22.933ZM44.878 0.0V-4.256L56.354 -16.320999999999998H45.334V-20.33H62.643V-16.073999999999998L51.015 -4.009H62.928V0.0ZM65.968 0.0V-4.256L77.444 -16.320999999999998H66.42399999999999V-20.33H83.733V-16.073999999999998L72.10499999999999 -4.009H84.018V0.0ZM92.834 0.0Q90.47800000000001 0.0 89.11000000000001 -1.197Q87.742 -2.394 87.742 -5.054V-26.98H93.024V-5.6049999999999995Q93.024 -4.769 93.4325 -4.389Q93.84100000000001 -4.009 94.601 -4.009H96.026V0.0ZM107.53999999999999 0.45599999999999996Q104.462 0.45599999999999996 102.19149999999999 -0.855Q99.92099999999999 -2.166 98.6765 -4.56Q97.432 -6.954 97.432 -10.165Q97.432 -13.376 98.6765 -15.751000000000001Q99.92099999999999 -18.126 102.1725 -19.456Q104.424 -20.785999999999998 107.464 -20.785999999999998Q110.428 -20.785999999999998 112.64150000000001 -19.474999999999998Q114.855 -18.163999999999998 116.0615 -15.713Q117.268 -13.262 117.268 -9.842V-8.721H102.904Q103.037 -6.175 104.291 -4.9115Q105.545 -3.6479999999999997 107.616 -3.6479999999999997Q109.155 -3.6479999999999997 110.181 -4.3225Q111.207 -4.997 111.625 -6.289L116.983 -5.966Q116.166 -2.964 113.6485 -1.254Q111.131 0.45599999999999996 107.53999999999999 0.45599999999999996ZM102.904 -12.103H111.834Q111.701 -14.459 110.5135 -15.6085Q109.326 -16.758 107.464 -16.758Q105.602 -16.758 104.386 -15.551499999999999Q103.17 -14.344999999999999 102.904 -12.103Z"/></svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="mizzle"><g stroke-width="6" stroke-linecap="round"><path d="M14.0 20.0H50.0" stroke="#93B4FF"/><path d="M22.0 32.0H54.0" stroke="#5F86EE"/><path d="M10.0 44.0H38.0" stroke="#3A5BB8"/></g></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="mizzle"><g stroke-width="6" stroke-linecap="round"><path d="M14.0 20.0H50.0" stroke="#1D4ED8"/><path d="M22.0 32.0H54.0" stroke="#5B7FE3"/><path d="M10.0 44.0H38.0" stroke="#A3B8F1"/></g></svg>
|
|
Binary file
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# mizzle example: a small social network API
|
|
2
|
+
|
|
3
|
+
FastAPI + mizzle: profiles, follows, posts with visibility (public / followers / private),
|
|
4
|
+
comments, likes and bookmarks. It shows the patterns a real app needs:
|
|
5
|
+
|
|
6
|
+
- **Schema** in `social/schema.py` (`@final` tables, enums, composite keys, checks, indexes,
|
|
7
|
+
relations) and its **migration history** in `migrations/`.
|
|
8
|
+
- **Request models with allowlists** (`social/models.py`): clients can only send the listed
|
|
9
|
+
fields, so they cannot set ids, authors or timestamps.
|
|
10
|
+
- **Relational queries** for nested data (a post with its comments and their authors) and
|
|
11
|
+
typed `select` queries for counts.
|
|
12
|
+
- **Upserts** for idempotent likes, follows and bookmarks.
|
|
13
|
+
- **Migrations at startup** (`MIGRATE_ON_START=1`), safe with many workers (advisory lock).
|
|
14
|
+
- **Database errors as HTTP**: unique → 409, check constraint → 422, missing reference → 404.
|
|
15
|
+
- **End-to-end tests** against a fresh database (`tests/test_api.py`).
|
|
16
|
+
|
|
17
|
+
## Run it
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
docker compose -f ../../docker-compose.yml up -d # Postgres on localhost:54329
|
|
21
|
+
cp .env.example .env
|
|
22
|
+
uv sync
|
|
23
|
+
uv run mizzle migrate # or rely on MIGRATE_ON_START=1
|
|
24
|
+
uv run python -m social.seed # optional demo data
|
|
25
|
+
uv run uvicorn social.app:create_app --factory --reload
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Open http://127.0.0.1:8000/docs. Authentication is out of scope: send `X-User-Id: <id>`
|
|
29
|
+
to act as a user (the seed creates ids 1-24).
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
curl -s localhost:8000/feed -H 'X-User-Id: 1' | python -m json.tool | head -40
|
|
33
|
+
uv run mizzle studio # browse the data
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Change the schema
|
|
37
|
+
|
|
38
|
+
Edit `social/schema.py`, then:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv run mizzle generate --name what_changed
|
|
42
|
+
uv run mizzle migrate
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Test
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
MIZZLE_TEST_URL=postgresql://mizzle:mizzle@localhost:54329/mizzle uv run pytest
|
|
49
|
+
```
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
-- create enum visibility
|
|
2
|
+
-- create table users
|
|
3
|
+
-- create table follows
|
|
4
|
+
-- create table posts
|
|
5
|
+
-- create table comments
|
|
6
|
+
-- create table likes
|
|
7
|
+
-- create index follows_followee_idx
|
|
8
|
+
-- create index posts_author_created_idx
|
|
9
|
+
-- create index comments_post_idx
|
|
10
|
+
-- create index likes_post_idx
|
|
11
|
+
-- add foreign key follows_follower_id_fkey
|
|
12
|
+
-- add foreign key follows_followee_id_fkey
|
|
13
|
+
-- add foreign key posts_author_id_fkey
|
|
14
|
+
-- add foreign key comments_post_id_fkey
|
|
15
|
+
-- add foreign key comments_author_id_fkey
|
|
16
|
+
-- add foreign key likes_user_id_fkey
|
|
17
|
+
-- add foreign key likes_post_id_fkey
|
|
18
|
+
CREATE TYPE "visibility" AS ENUM ('public', 'followers', 'private');
|
|
19
|
+
-- mizzle:breakpoint
|
|
20
|
+
CREATE TABLE "users" (
|
|
21
|
+
"id" serial NOT NULL,
|
|
22
|
+
"username" character varying(32) NOT NULL,
|
|
23
|
+
"email" character varying(255) NOT NULL,
|
|
24
|
+
"display_name" text NOT NULL,
|
|
25
|
+
"bio" text,
|
|
26
|
+
"created_at" timestamp with time zone NOT NULL DEFAULT now(),
|
|
27
|
+
CONSTRAINT "users_pkey" PRIMARY KEY ("id"),
|
|
28
|
+
CONSTRAINT "users_username_key" UNIQUE ("username"),
|
|
29
|
+
CONSTRAINT "users_email_key" UNIQUE ("email"),
|
|
30
|
+
CONSTRAINT "users_username_format" CHECK (username ~ '^[a-z0-9_]{3,32}$')
|
|
31
|
+
);
|
|
32
|
+
-- mizzle:breakpoint
|
|
33
|
+
CREATE TABLE "follows" (
|
|
34
|
+
"follower_id" integer NOT NULL,
|
|
35
|
+
"followee_id" integer NOT NULL,
|
|
36
|
+
"created_at" timestamp with time zone NOT NULL DEFAULT now(),
|
|
37
|
+
CONSTRAINT "follows_pkey" PRIMARY KEY ("follower_id", "followee_id"),
|
|
38
|
+
CONSTRAINT "follows_not_self" CHECK (follower_id <> followee_id)
|
|
39
|
+
);
|
|
40
|
+
-- mizzle:breakpoint
|
|
41
|
+
CREATE TABLE "posts" (
|
|
42
|
+
"id" serial NOT NULL,
|
|
43
|
+
"author_id" integer NOT NULL,
|
|
44
|
+
"body" text NOT NULL,
|
|
45
|
+
"visibility" visibility NOT NULL DEFAULT 'public',
|
|
46
|
+
"meta" jsonb,
|
|
47
|
+
"created_at" timestamp with time zone NOT NULL DEFAULT now(),
|
|
48
|
+
CONSTRAINT "posts_pkey" PRIMARY KEY ("id"),
|
|
49
|
+
CONSTRAINT "posts_body_not_empty" CHECK (length(body) > 0)
|
|
50
|
+
);
|
|
51
|
+
-- mizzle:breakpoint
|
|
52
|
+
CREATE TABLE "comments" (
|
|
53
|
+
"id" serial NOT NULL,
|
|
54
|
+
"post_id" integer NOT NULL,
|
|
55
|
+
"author_id" integer NOT NULL,
|
|
56
|
+
"body" text NOT NULL,
|
|
57
|
+
"created_at" timestamp with time zone NOT NULL DEFAULT now(),
|
|
58
|
+
CONSTRAINT "comments_pkey" PRIMARY KEY ("id")
|
|
59
|
+
);
|
|
60
|
+
-- mizzle:breakpoint
|
|
61
|
+
CREATE TABLE "likes" (
|
|
62
|
+
"user_id" integer NOT NULL,
|
|
63
|
+
"post_id" integer NOT NULL,
|
|
64
|
+
"created_at" timestamp with time zone NOT NULL DEFAULT now(),
|
|
65
|
+
CONSTRAINT "likes_pkey" PRIMARY KEY ("user_id", "post_id")
|
|
66
|
+
);
|
|
67
|
+
-- mizzle:breakpoint
|
|
68
|
+
CREATE INDEX "follows_followee_idx" ON "follows" ("followee_id");
|
|
69
|
+
-- mizzle:breakpoint
|
|
70
|
+
CREATE INDEX "posts_author_created_idx" ON "posts" ("author_id", "created_at");
|
|
71
|
+
-- mizzle:breakpoint
|
|
72
|
+
CREATE INDEX "comments_post_idx" ON "comments" ("post_id");
|
|
73
|
+
-- mizzle:breakpoint
|
|
74
|
+
CREATE INDEX "likes_post_idx" ON "likes" ("post_id");
|
|
75
|
+
-- mizzle:breakpoint
|
|
76
|
+
ALTER TABLE "follows" ADD CONSTRAINT "follows_follower_id_fkey" FOREIGN KEY ("follower_id") REFERENCES "users" ("id") ON DELETE CASCADE;
|
|
77
|
+
-- mizzle:breakpoint
|
|
78
|
+
ALTER TABLE "follows" ADD CONSTRAINT "follows_followee_id_fkey" FOREIGN KEY ("followee_id") REFERENCES "users" ("id") ON DELETE CASCADE;
|
|
79
|
+
-- mizzle:breakpoint
|
|
80
|
+
ALTER TABLE "posts" ADD CONSTRAINT "posts_author_id_fkey" FOREIGN KEY ("author_id") REFERENCES "users" ("id") ON DELETE CASCADE;
|
|
81
|
+
-- mizzle:breakpoint
|
|
82
|
+
ALTER TABLE "comments" ADD CONSTRAINT "comments_post_id_fkey" FOREIGN KEY ("post_id") REFERENCES "posts" ("id") ON DELETE CASCADE;
|
|
83
|
+
-- mizzle:breakpoint
|
|
84
|
+
ALTER TABLE "comments" ADD CONSTRAINT "comments_author_id_fkey" FOREIGN KEY ("author_id") REFERENCES "users" ("id") ON DELETE CASCADE;
|
|
85
|
+
-- mizzle:breakpoint
|
|
86
|
+
ALTER TABLE "likes" ADD CONSTRAINT "likes_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE;
|
|
87
|
+
-- mizzle:breakpoint
|
|
88
|
+
ALTER TABLE "likes" ADD CONSTRAINT "likes_post_id_fkey" FOREIGN KEY ("post_id") REFERENCES "posts" ("id") ON DELETE CASCADE;
|