Metadata-Version: 2.4
Name: ormguard
Version: 0.1.0
Summary: Fail-fast schema validation for SQLAlchemy — Hibernate's ddl-auto=validate for Python.
Project-URL: Homepage, https://github.com/gogo1414/ormguard
Project-URL: Repository, https://github.com/gogo1414/ormguard
Project-URL: Issues, https://github.com/gogo1414/ormguard/issues
Project-URL: Changelog, https://github.com/gogo1414/ormguard/blob/main/CHANGELOG.md
Author: JuneSoo
License: MIT
License-File: LICENSE
Keywords: alembic,drift,fastapi,orm,schema,sqlalchemy,validation
Classifier: Framework :: FastAPI
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Database
Requires-Python: >=3.9
Requires-Dist: sqlalchemy>=1.4
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Description-Content-Type: text/markdown

# ormguard

**Fail-fast schema validation for SQLAlchemy.** Bring Hibernate's
`hibernate.ddl-auto=validate` to Python: at startup, reflect the connected
database and verify it matches your ORM entities. Catch entity↔DB drift at
**boot time** instead of as a runtime `column does not exist` error.

## Quickstart (60 seconds)

```bash
pip install ormguard
python -m ormguard --selfcheck   # see it catch drift against in-memory SQLite — no DB, no setup
```

Guard your app at boot so it refuses to start on drift (FastAPI):

```python
from ormguard.integrations.fastapi import schema_guard_lifespan

app = FastAPI(lifespan=schema_guard_lifespan(engine, Base, strict=True))
```

...or fail CI before you ship (exit code 1 on ERROR findings):

```bash
python -m ormguard --url "$DATABASE_URL" --metadata myapp.db:Base
```

That's it. Details and every mode are below.

## The problem

In JPA/Hibernate, `ddl-auto=validate` checks every entity against the live
schema when the app starts and **refuses to boot** on a mismatch. SQLAlchemy
has no equivalent — `create_all()` only creates missing tables, nothing
validates existing ones. So an entity can map a column the database doesn't
have (or ignore a column it does have) and the server starts perfectly fine…
until the request that touches that column blows up in production.

`alembic check` and Atlas help, but both compare against migration metadata /
a live DB through autogenerate and run in CI. ormguard checks the **actual
database the app is about to use, at the moment it boots.**

## Install

```bash
pip install ormguard   # (POC: install from source — see below)
```

## Try it in one line (no DB, no host project)

```bash
python -m ormguard --selfcheck
```

Spins up an in-memory SQLite database with deliberate drift and prints exactly
what ormguard catches — a zero-setup way to see it work or sanity-check an
install.

## Docs

- [docs/DESIGN.md](docs/DESIGN.md) — problem, the Hibernate `validate` analogy, why existing tools don't fit, architecture, roadmap.
- [docs/USAGE.md](docs/USAGE.md) — every usage mode with examples.
- [docs/V2_OFFLINE_REPLAY.md](docs/V2_OFFLINE_REPLAY.md) — spec for the offline multi-tenant Alembic replay mode (the differentiator).

## Use it

### As a startup guard (FastAPI)

```python
from ormguard.integrations.fastapi import schema_guard_lifespan

app = FastAPI(lifespan=schema_guard_lifespan(engine, Base, strict=True))
# strict=True -> app refuses to start on ERROR-level drift
```

### As a function (any framework)

```python
from ormguard import assert_schema, validate

assert_schema(engine, Base, strict=True)      # raise on drift

report = validate(engine, Base)               # or inspect findings yourself
if not report.ok:
    print(report.format_text())
```

### In CI

```bash
python -m ormguard --url "$DATABASE_URL" --metadata myapp.db:Base --schema aivelabs_sv
# exit code 1 on ERROR findings
```

Or drop in the GitHub Action — no install step needed:

```yaml
- uses: gogo1414/ormguard@v1
  with:
    database-url: ${{ secrets.DATABASE_URL }}
    metadata: myapp.db:Base
    args: --schema public --check-indexes   # optional
```

(Until ormguard is on PyPI, set `version:` to a VCS URL, e.g.
`version: git+https://github.com/gogo1414/ormguard@main`.)

Get a team-channel ping when drift is found — one webhook works for Slack or
Discord (add `--notify-on any` to include warnings):

```bash
python -m ormguard --url "$DATABASE_URL" --metadata myapp.db:Base \
  --notify-webhook "$SLACK_OR_DISCORD_WEBHOOK"
```

```yaml
- uses: gogo1414/ormguard@v1
  with:
    database-url: ${{ secrets.DATABASE_URL }}
    metadata: myapp.db:Base
    notify-webhook: ${{ secrets.SLACK_WEBHOOK }}
```

### Multi-tenant (one ORM, many databases)

```python
from ormguard import validate_many, format_matrix

reports = validate_many({"larosee": e1, "hmall": e2, "cafe24": e3}, Base)
print(format_matrix(reports))
```

## What it checks (v1)

| Finding | Default severity | Meaning |
|---|---|---|
| `table_missing` | ERROR | entity declares a table the DB lacks |
| `column_missing` | ERROR | entity maps a column the DB lacks (the crash case) |
| `column_extra` | WARN | DB column not mapped by any entity (silently unused) |
| `nullable_mismatch` | WARN | NOT NULL / NULL disagreement |
| `type_mismatch` | WARN (opt-in) | column type differs — off by default (dialect-dependent) |
| `index_missing` | WARN (opt-in) | ORM declares an index the DB lacks — off by default |
| `index_extra` | WARN (opt-in) | DB has an index not declared in the ORM — off by default |
| `fk_missing` | WARN (opt-in) | ORM declares a foreign key the DB lacks — off by default |
| `fk_extra` | WARN (opt-in) | DB has a foreign key not declared in the ORM — off by default |
| `default_missing` | WARN (opt-in) | ORM sets a server_default the DB column lacks — off by default |
| `default_extra` | WARN (opt-in) | DB column has a default the ORM doesn't declare — off by default |

Configurable via `Config`: restrict schemas, ignore tables/columns, flip
severities, toggle nullable/type/index/foreign-key/default/extra checks.

Index checks (`--check-indexes` / `Config(check_indexes=True)`) compare by column
set and uniqueness — not by name — and skip indexes that merely back a primary
key or unique constraint. Foreign-key checks (`--check-foreign-keys`) compare by
local columns, referred table, and referred columns. Server-default checks
(`--check-defaults`) compare only *whether* a DB default exists — not its value,
which is too dialect-dependent — and skip primary keys. All keep false positives low.

Out of scope for v1 (planned): check
constraints, enums, and an **offline multi-tenant Alembic replay** mode that
diffs ORM against the schema migrations *would* produce per tenant — without a
database.

## Develop

```bash
pip install -e ".[dev]"
pytest        # runs against in-memory SQLite, no external DB needed
```

## License

MIT
