Metadata-Version: 2.4
Name: terminusdb-migrations
Version: 0.1.1
Summary: Schema diff and migration planning for TerminusDB from Pydantic models
Project-URL: Source, https://gitlab.com/ed-tech6370840/terminusdb-migrations
Project-URL: Issues, https://gitlab.com/ed-tech6370840/terminusdb-migrations/-/issues
Project-URL: PyPI, https://pypi.org/project/terminusdb-migrations/
Keywords: terminusdb,migration,schema,pydantic,graph-database
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Database
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: terminusdb-async<1.0.0,>=0.1.0
Requires-Dist: terminusdb-pydantic<1.0.0,>=0.1.0

# terminusdb-migrations

[![PyPI](https://img.shields.io/pypi/v/terminusdb-migrations.svg)](https://pypi.org/project/terminusdb-migrations/)
[![Python](https://img.shields.io/pypi/pyversions/terminusdb-migrations.svg)](https://pypi.org/project/terminusdb-migrations/)

Schema diff and migration planning for TerminusDB, using Pydantic models as the desired schema.

`terminusdb-migrations` compares the live TerminusDB schema with schema documents generated from Python models and produces a migration plan that can be reviewed, dry-run, and applied through the native TerminusDB Migration API.

```text
Pydantic models
      ↓
desired TerminusDB schema
      ↓
       diff  ← current TerminusDB schema
      ↓
migration plan
      ↓
native TerminusDB Migration API
```

It is designed to work with [`terminusdb-pydantic`](https://pypi.org/project/terminusdb-pydantic/) and [`terminusdb-async`](https://pypi.org/project/terminusdb-async/).

> **Status:** early-stage / pre-1.0. Inspect generated migration plans before applying them to production data.
>
> **Project status:** this is a community package and is not an official TerminusDB migration tool.

## Installation

With uv:

```bash
uv add terminusdb-migrations
```

With pip:

```bash
python -m pip install terminusdb-migrations
```

Python 3.11+ is supported.

Internal package dependencies intentionally stay broad across patch releases:

```text
terminusdb-async    >=0.1.0,<1.0.0
terminusdb-pydantic >=0.1.0,<1.0.0
```

The minimum compatible version is only raised when a release actually requires newer API.

## Quick start

Define the desired schema as Pydantic models:

```python
from terminusdb_pydantic import LexicalKey, TerminusModel


class Discipline(TerminusModel):
    __terminus_key__ = LexicalKey("code")

    code: str
    name: str
    description: str | None = None


MODELS = [Discipline]
```

Build a migration plan against the live TerminusDB schema:

```python
from terminusdb_async import AsyncTerminusClient
from terminusdb_migrations import MigrationManager


async def inspect_schema() -> None:
    async with AsyncTerminusClient(
        "http://localhost:6363",
        organization="admin",
        database="edtech",
        username="admin",
        password="root",
    ) as db:
        manager = MigrationManager(db, MODELS)
        plan = await manager.plan()

        print(plan.render())
```

Apply the reviewed plan:

```python
await manager.apply(
    plan,
    author="schema-bot",
    message="Synchronize schema",
)
```

## Dry run

Use TerminusDB's migration dry-run before applying generated operations:

```python
await manager.apply(
    plan,
    author="schema-bot",
    message="Check schema synchronization",
    dry_run=True,
)
```

If a plan is marked destructive, `allow_destructive=True` is still required before it can be sent to TerminusDB. Destructive plans must be acknowledged explicitly.

## What the planner does

The current planner is intentionally conservative.

| Schema difference | Planner behavior |
| --- | --- |
| New class | `CreateClass` |
| New enum | schema document insert |
| New optional property | `CreateClassProperty` |
| New required property | warning; no default is invented |
| Existing property type changed | `CastClassProperty` |
| Enum value added | `ExpandEnum` |
| Enum value removed | warning + destructive |
| Key changed | `ChangeKey` + destructive |
| Property removed from model | warning + destructive |
| Class or enum absent from models | warning + destructive |

This table describes the current `0.1.x` behavior.

## MigrationPlan

A generated plan exposes:

```python
plan.operations
plan.schema_documents
plan.warnings
plan.destructive
plan.empty
```

For human review:

```python
print(plan.render())
```

### `operations`

Native TerminusDB migration operations ready for the Migration API.

### `schema_documents`

Schema documents that need to be inserted outside the migration operation list. In the current implementation, a newly introduced enum is handled this way.

### `warnings`

Changes that the planner refuses to guess automatically.

### `destructive`

Signals that applying the plan may remove information or materially change identity semantics.

## Safety model

Automatic schema diff cannot always infer developer intent.

For example, these two schemas are ambiguous:

```python
class Discipline(TerminusModel):
    title: str
```

and:

```python
class Discipline(TerminusModel):
    name: str
```

The difference may mean either:

- rename `title` to `name`; or
- delete `title` and add an unrelated `name`.

The planner therefore prefers warnings over silently destructive guesses.

The same principle applies to required properties without defaults, enum contractions, removals, and key changes.

## CLI

The package installs the `tdb-migrate` command.

Expose a model registry from a Python module:

```python
# app/models.py

from terminusdb_pydantic import LexicalKey, TerminusModel


class Discipline(TerminusModel):
    __terminus_key__ = LexicalKey("code")

    code: str
    name: str


MODELS = [Discipline]
```

Inspect changes:

```bash
tdb-migrate plan \
  --models app.models:MODELS \
  --db edtech
```

Dry-run the migration:

```bash
tdb-migrate apply \
  --models app.models:MODELS \
  --db edtech \
  --author schema-bot \
  --message "Synchronize schema" \
  --dry-run
```

Apply after review:

```bash
tdb-migrate apply \
  --models app.models:MODELS \
  --db edtech \
  --author schema-bot \
  --message "Synchronize schema"
```

For a plan explicitly marked destructive:

```bash
tdb-migrate apply \
  --models app.models:MODELS \
  --db edtech \
  --author schema-bot \
  --message "Apply reviewed destructive migration" \
  --allow-destructive
```

## Configuration

The CLI accepts command-line arguments and environment variables for the TerminusDB connection.

Useful environment variables:

```text
TERMINUSDB_URL
TERMINUSDB_ORG
TERMINUSDB_USER
TERMINUSDB_PASSWORD
TERMINUSDB_TOKEN
```

Example:

```bash
export TERMINUSDB_URL=http://localhost:6363
export TERMINUSDB_ORG=admin
export TERMINUSDB_USER=admin
export TERMINUSDB_PASSWORD=root

tdb-migrate plan --models app.models:MODELS --db edtech
```

## End-to-end example

```python
from terminusdb_async import AsyncTerminusClient
from terminusdb_migrations import MigrationManager
from terminusdb_pydantic import LexicalKey, TerminusModel


class Discipline(TerminusModel):
    __terminus_key__ = LexicalKey("code")

    code: str
    name: str
    description: str | None = None


async def migrate() -> None:
    async with AsyncTerminusClient(
        "http://localhost:6363",
        organization="admin",
        database="edtech",
        username="admin",
        password="root",
    ) as db:
        manager = MigrationManager(db, [Discipline])

        plan = await manager.plan()

        if plan.warnings:
            for warning in plan.warnings:
                print("WARNING:", warning)

        print(plan.render())

        if not plan.empty:
            await manager.apply(
                plan,
                author="schema-bot",
                message="Synchronize schema",
                dry_run=True,
                allow_destructive=plan.destructive,
            )
```

## Current boundaries

The package is not intended to replace TerminusDB's native migration/versioning mechanisms. It only generates and applies plans on top of them.

In `0.1.x`:

- rename intent is not automatically inferred from schema state alone;
- required-property data backfills are not invented;
- destructive removals are not silently executed;
- schema diff behavior is deliberately small and explicit.

## Package family

| Package | Purpose |
| --- | --- |
| [`terminusdb-async`](https://pypi.org/project/terminusdb-async/) | Async TerminusDB HTTP client |
| [`terminusdb-pydantic`](https://pypi.org/project/terminusdb-pydantic/) | Pydantic v2 ↔ JSON-LD and schema generation |
| [`terminusdb-migrations`](https://pypi.org/project/terminusdb-migrations/) | Schema diff and migration planning |

## Development

The project uses uv:

```bash
uv sync --group dev
uv run pytest -q
uv build --no-sources
```

CI tests Python 3.11, 3.12, and 3.13.

Development follows GitHub Flow on GitLab: short-lived branches are merged into `main` through Merge Requests. See `CONTRIBUTING.md`.

## Versioning

The package is pre-1.0. Internal package compatibility is expressed with ranges such as:

```toml
"terminusdb-async>=0.1.0,<1.0.0"
"terminusdb-pydantic>=0.1.0,<1.0.0"
```

Patch releases do not raise these lower bounds unless they actually depend on new API.
