Metadata-Version: 2.4
Name: langgraph-checkpoint-dmdb
Version: 0.1.0
Summary: DM Database checkpoint and Store persistence for LangGraph.
Project-URL: Repository, https://gitee.com/maxbanana/langgraph-checkpoint-dmdb
Author-email: Theodore Ni <dev@ted.bio>
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.9
Requires-Dist: dmpython==2.5.32
Requires-Dist: langgraph-checkpoint<2.2,>=2.1.1
Requires-Dist: typing-extensions>=4.12.2
Description-Content-Type: text/markdown

# LangGraph Checkpoint DMDB

[LangGraph](https://github.com/langchain-ai/langgraph) checkpoint and Store persistence for DM Database, using `dmPython`.

## Compatibility

- `langgraph-checkpoint>=2.1.1,<2.2`
- `dmpython==2.5.32`
- Python 3.9+

The package provides:

- `DMDBSaver`: complete checkpoint history and time travel.
- `ShallowDMDBSaver`: latest-checkpoint-only persistence.
- `AsyncDMDBSaver` and `AsyncShallowDMDBSaver`: async graph APIs backed by isolated worker-thread connections.
- `DMDBStore` and `AsyncDMDBStore`: LangGraph Store CRUD, batch, JSON filtering, pagination, and namespace listing.

## Installation

```bash
pip install langgraph-checkpoint-dmdb
```

## Usage

For a long-lived compiled graph:

```python
from langgraph.checkpoint.dmdb import DMDBSaver

saver = DMDBSaver.from_conn_string(
    "dmdb://sysdba:password@localhost:5236/NLP_KNOWLEDGEBASE"
)
saver.setup()
graph = builder.compile(checkpointer=saver)

config = {"configurable": {"thread_id": "conversation-1"}}
for event in graph.stream(inputs, config=config):
    ...
```

Applications that already have structured database configuration should avoid constructing a URI:

```python
saver = DMDBSaver.from_conn_params(
    host=db.host,
    port=int(db.port),
    user=db.username,
    password=db.password,
    schema=db.database,
)
saver.setup()
graph = builder.compile(checkpointer=saver)
```

Short-lived scripts may use the Saver as a context manager:

```python
with DMDBSaver.from_conn_string(DMDB_URI) as saver:
    saver.setup()
    graph = builder.compile(checkpointer=saver)
    result = graph.invoke(inputs, config=config)
```

`from_conn_string()` and `from_conn_params()` create a connection factory. Each checkpoint operation owns a new dmPython connection and closes it afterward, so a Saver can be shared by concurrent synchronous graph executions without sharing a connection.

Do not return a graph from inside the `with` block and use it after the block exits; the Saver is closed on context exit.

## Async Checkpointing

dmPython has no native async API. The async savers run each database operation with `asyncio.to_thread()` and obtain a fresh connection from the factory, so connections are never shared between worker threads:

```python
from langgraph.checkpoint.dmdb import AsyncDMDBSaver

async with AsyncDMDBSaver.from_conn_params(
    host=db.host,
    port=int(db.port),
    user=db.username,
    password=db.password,
    schema=db.database,
) as saver:
    await saver.setup()
    graph = builder.compile(checkpointer=saver)
    result = await graph.ainvoke(inputs, config=config)
```

Async savers require a connection factory. Passing one shared dmPython connection is rejected because `dmPython.threadsafety == 1`.

## Shallow Saver

```python
from langgraph.checkpoint.dmdb import ShallowDMDBSaver

saver = ShallowDMDBSaver.from_conn_string(DMDB_URI)
saver.setup()
graph = builder.compile(checkpointer=saver)
```

The shallow saver retains only the latest checkpoint per thread and namespace. Do not use shallow and full savers for the same thread/namespace because shallow writes intentionally prune that history.

## LangGraph Store

```python
from langgraph.store.dmdb import DMDBStore

store = DMDBStore.from_conn_params(
    host=db.host,
    port=int(db.port),
    user=db.username,
    password=db.password,
    schema=db.database,
)
store.setup()
store.put(("users", user_id), "preferences", {"language": "zh-CN"})
items = store.search(("users", user_id), filter={"language": "zh-CN"})
graph = builder.compile(checkpointer=saver, store=store)
```

`AsyncDMDBStore` exposes the corresponding `aget`, `aput`, `adelete`, `asearch`, `alist_namespaces`, and `abatch` APIs.

## Connection URI

Supported schemes are `dmdb://` and `dm://`:

```text
dmdb://user:password@host:5236/schema
```

The path is treated as the DM schema. The Saver validates it and executes `SET SCHEMA` after connecting. Usernames and passwords containing URI-special characters must be percent encoded. `from_conn_params()` does not require URI encoding.

If dmPython raises `DatabaseError: [CODE:-70089] Encryption module failed to load` in Linux, add the wheel's bundled `dmssl` directory to `LD_LIBRARY_PATH` before starting Python. The exact site-packages path depends on the runtime image; see the dmPython 2.5.32 package metadata for its supported SSL setup.

## Transactions and concurrency

- dmPython connections use `autoCommit=False`.
- `put`, `put_writes`, and `delete_thread` commit atomically and roll back on error.
- Factory-backed Savers create an independent connection per operation.
- A directly supplied dmPython connection remains owned by the caller and is restricted to its creating thread.
- `setup()` is idempotent and must be called before first use.
- DM `MERGE` source parameters are explicitly cast to their target SQL types. BLOB/write batches execute row-by-row inside one transaction because dmPython/DM otherwise cache the first inferred bind length and can raise `-70005 String truncated` for later values.

## Driver Limitations

- Async APIs use worker threads rather than a native async DM protocol.
- Async classes cannot accept a single direct dmPython connection.
- Store JSON filtering and namespace matching run in Python to avoid DM-version-specific JSON functions.
- Store semantic `query` and vector indexing are not implemented; the MySQL 2.0.17 Store also did not implement semantic indexing.

## Tests

Unit and LangGraph integration tests do not require a DM instance:

```bash
uv run pytest -m "not integration"
```

Real DM integration requires `dmPython` and these environment variables:

```text
DM_HOST
DM_PORT
DM_USER
DM_PASSWORD
DM_SCHEMA
```

Run it with:

```bash
uv run pytest -m integration
```

## Design

The implementation plan and release gates are documented in [docs/dmdb-compatibility-plan.md](docs/dmdb-compatibility-plan.md). The complete MySQL-to-DMDB feature mapping is in [docs/mysql-feature-parity.md](docs/mysql-feature-parity.md).

This project derives serialization behavior and tests from `langgraph-checkpoint-mysql` 2.0.17. The original MIT copyright notice is retained in [LICENSE](LICENSE).
