mirror of
https://github.com/hoshikawa2/first_contas.git
synced 2026-07-09 18:24:20 +00:00
first commit
This commit is contained in:
763
legacy_reference/workflows/oracle_checkpoint.py
Normal file
763
legacy_reference/workflows/oracle_checkpoint.py
Normal file
@@ -0,0 +1,763 @@
|
||||
"""LangGraph checkpoint saver backed by Oracle ADB."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator, Mapping, Sequence
|
||||
from copy import deepcopy
|
||||
import logging
|
||||
from random import random
|
||||
from typing import Any
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
BaseCheckpointSaver,
|
||||
ChannelVersions,
|
||||
Checkpoint,
|
||||
CheckpointMetadata,
|
||||
CheckpointTuple,
|
||||
WRITES_IDX_MAP,
|
||||
get_checkpoint_id,
|
||||
get_serializable_checkpoint_metadata,
|
||||
)
|
||||
from langgraph.checkpoint.serde.types import _DeltaSnapshot
|
||||
|
||||
from agente_contas_tim.repositories.oracle_state_connection import (
|
||||
OracleStateConnectionFactory,
|
||||
is_unique_constraint_error,
|
||||
normalize_json,
|
||||
set_blob_inputsizes,
|
||||
read_json_value,
|
||||
set_json_inputsizes,
|
||||
)
|
||||
|
||||
|
||||
_DEFAULT_NS = "__default__"
|
||||
_PRIMITIVE_TYPES = (str, int, float, bool)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OracleCheckpointSaver(BaseCheckpointSaver):
|
||||
"""Synchronous LangGraph checkpointer persisted in Oracle native JSON/BLOB."""
|
||||
|
||||
_REQUIRED_TABLES = {
|
||||
"TB_CONT_WORKFLOW_CHECKPOINT",
|
||||
"TB_CONT_WORKFLOW_CHECKPOINT_WRITE",
|
||||
"TB_CONT_WORKFLOW_CHECKPOINT_BLOB",
|
||||
}
|
||||
|
||||
def __init__(self, connection_factory: OracleStateConnectionFactory) -> None:
|
||||
super().__init__()
|
||||
self._connection_factory = connection_factory
|
||||
self._connection_factory.validate_required_tables(self._REQUIRED_TABLES)
|
||||
|
||||
def setup(self) -> None:
|
||||
self._connection_factory.validate_required_tables(self._REQUIRED_TABLES)
|
||||
|
||||
def get_tuple(self, config: Mapping[str, Any]) -> CheckpointTuple | None:
|
||||
configurable = dict(config.get("configurable", {}) or {})
|
||||
thread_id = str(configurable["thread_id"])
|
||||
checkpoint_ns = _to_db_ns(str(configurable.get("checkpoint_ns", "")))
|
||||
checkpoint_id = get_checkpoint_id(config) # type: ignore[arg-type]
|
||||
|
||||
with self._connection_factory.connect() as conn, conn.cursor() as cur:
|
||||
if checkpoint_id:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT
|
||||
thread_id,
|
||||
checkpoint_ns,
|
||||
checkpoint_id,
|
||||
parent_checkpoint_id,
|
||||
checkpoint_json,
|
||||
metadata_json
|
||||
FROM tb_cont_workflow_checkpoint
|
||||
WHERE thread_id = :thread_id
|
||||
AND (
|
||||
checkpoint_ns = :checkpoint_ns
|
||||
OR (
|
||||
:checkpoint_ns = :default_checkpoint_ns
|
||||
AND checkpoint_ns IS NULL
|
||||
)
|
||||
)
|
||||
AND checkpoint_id = :checkpoint_id
|
||||
""",
|
||||
{
|
||||
"thread_id": thread_id,
|
||||
"checkpoint_ns": checkpoint_ns,
|
||||
"default_checkpoint_ns": _DEFAULT_NS,
|
||||
"checkpoint_id": checkpoint_id,
|
||||
},
|
||||
)
|
||||
else:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT
|
||||
thread_id,
|
||||
checkpoint_ns,
|
||||
checkpoint_id,
|
||||
parent_checkpoint_id,
|
||||
checkpoint_json,
|
||||
metadata_json
|
||||
FROM tb_cont_workflow_checkpoint
|
||||
WHERE thread_id = :thread_id
|
||||
AND (
|
||||
checkpoint_ns = :checkpoint_ns
|
||||
OR (
|
||||
:checkpoint_ns = :default_checkpoint_ns
|
||||
AND checkpoint_ns IS NULL
|
||||
)
|
||||
)
|
||||
ORDER BY checkpoint_id DESC
|
||||
FETCH FIRST 1 ROWS ONLY
|
||||
""",
|
||||
{
|
||||
"thread_id": thread_id,
|
||||
"checkpoint_ns": checkpoint_ns,
|
||||
"default_checkpoint_ns": _DEFAULT_NS,
|
||||
},
|
||||
)
|
||||
row = _fetchone_dict(cur)
|
||||
if row is None:
|
||||
return None
|
||||
return self._load_checkpoint_tuple(cur, row)
|
||||
|
||||
def list(
|
||||
self,
|
||||
config: Mapping[str, Any] | None,
|
||||
*,
|
||||
filter: dict[str, Any] | None = None,
|
||||
before: Mapping[str, Any] | None = None,
|
||||
limit: int | None = None,
|
||||
) -> Iterator[CheckpointTuple]:
|
||||
clauses: list[str] = []
|
||||
params: dict[str, Any] = {}
|
||||
configurable = dict((config or {}).get("configurable", {}) or {})
|
||||
if configurable.get("thread_id"):
|
||||
clauses.append("thread_id = :thread_id")
|
||||
params["thread_id"] = str(configurable["thread_id"])
|
||||
if "checkpoint_ns" in configurable:
|
||||
clauses.append(
|
||||
"(checkpoint_ns = :checkpoint_ns OR "
|
||||
"(:checkpoint_ns = :default_checkpoint_ns "
|
||||
"AND checkpoint_ns IS NULL))"
|
||||
)
|
||||
params["checkpoint_ns"] = _to_db_ns(
|
||||
str(configurable.get("checkpoint_ns", ""))
|
||||
)
|
||||
params["default_checkpoint_ns"] = _DEFAULT_NS
|
||||
if before:
|
||||
before_id = get_checkpoint_id(before) # type: ignore[arg-type]
|
||||
if before_id:
|
||||
clauses.append("checkpoint_id < :before_checkpoint_id")
|
||||
params["before_checkpoint_id"] = before_id
|
||||
|
||||
where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
|
||||
query = f"""
|
||||
SELECT
|
||||
thread_id,
|
||||
checkpoint_ns,
|
||||
checkpoint_id,
|
||||
parent_checkpoint_id,
|
||||
checkpoint_json,
|
||||
metadata_json
|
||||
FROM tb_cont_workflow_checkpoint
|
||||
{where}
|
||||
ORDER BY checkpoint_id DESC
|
||||
"""
|
||||
|
||||
emitted = 0
|
||||
with self._connection_factory.connect() as conn, conn.cursor() as cur:
|
||||
cur.execute(query, params)
|
||||
rows = [_row_to_dict(cur, row) for row in cur.fetchall()]
|
||||
for row in rows:
|
||||
metadata = read_json_value(row.get("metadata_json")) or {}
|
||||
if filter and not _metadata_matches(metadata, filter):
|
||||
continue
|
||||
yield self._load_checkpoint_tuple(cur, row)
|
||||
emitted += 1
|
||||
if limit is not None and emitted >= limit:
|
||||
return
|
||||
|
||||
def put(
|
||||
self,
|
||||
config: Mapping[str, Any],
|
||||
checkpoint: Checkpoint,
|
||||
metadata: CheckpointMetadata,
|
||||
new_versions: ChannelVersions,
|
||||
) -> dict[str, Any]:
|
||||
configurable = dict(config["configurable"])
|
||||
thread_id = str(configurable.pop("thread_id"))
|
||||
checkpoint_ns = str(configurable.pop("checkpoint_ns", ""))
|
||||
parent_checkpoint_id = configurable.pop("checkpoint_id", None)
|
||||
checkpoint_id = str(checkpoint["id"])
|
||||
db_checkpoint_ns = _to_db_ns(checkpoint_ns)
|
||||
|
||||
checkpoint_copy = deepcopy(checkpoint)
|
||||
channel_values = dict(checkpoint_copy.get("channel_values") or {})
|
||||
checkpoint_copy["channel_values"] = channel_values
|
||||
blob_values: dict[str, Any] = {}
|
||||
for channel, value in list(channel_values.items()):
|
||||
if isinstance(value, _DeltaSnapshot):
|
||||
blob_values[channel] = value
|
||||
channel_values[channel] = True
|
||||
continue
|
||||
if value is None or isinstance(value, _PRIMITIVE_TYPES):
|
||||
continue
|
||||
blob_values[channel] = channel_values.pop(channel)
|
||||
|
||||
try:
|
||||
with self._connection_factory.connect() as conn, conn.cursor() as cur:
|
||||
blob_versions = {
|
||||
channel: version
|
||||
for channel, version in dict(new_versions).items()
|
||||
if channel in blob_values
|
||||
}
|
||||
for channel, version in blob_versions.items():
|
||||
self._upsert_blob(
|
||||
cur,
|
||||
thread_id=thread_id,
|
||||
checkpoint_ns=db_checkpoint_ns,
|
||||
channel=str(channel),
|
||||
version=str(version),
|
||||
value=blob_values[channel],
|
||||
)
|
||||
|
||||
set_json_inputsizes(cur, "checkpoint_json", "metadata_json")
|
||||
cur.execute(
|
||||
"""
|
||||
MERGE INTO tb_cont_workflow_checkpoint dst
|
||||
USING (
|
||||
SELECT
|
||||
:thread_id thread_id,
|
||||
:checkpoint_ns checkpoint_ns,
|
||||
:checkpoint_id checkpoint_id
|
||||
FROM dual
|
||||
) src
|
||||
ON (
|
||||
dst.thread_id = src.thread_id
|
||||
AND dst.checkpoint_ns = src.checkpoint_ns
|
||||
AND dst.checkpoint_id = src.checkpoint_id
|
||||
)
|
||||
WHEN MATCHED THEN UPDATE SET
|
||||
parent_checkpoint_id = :parent_checkpoint_id,
|
||||
checkpoint_json = :checkpoint_json,
|
||||
metadata_json = :metadata_json
|
||||
WHEN NOT MATCHED THEN INSERT (
|
||||
thread_id,
|
||||
checkpoint_ns,
|
||||
checkpoint_id,
|
||||
parent_checkpoint_id,
|
||||
checkpoint_json,
|
||||
metadata_json
|
||||
) VALUES (
|
||||
:thread_id,
|
||||
:checkpoint_ns,
|
||||
:checkpoint_id,
|
||||
:parent_checkpoint_id,
|
||||
:checkpoint_json,
|
||||
:metadata_json
|
||||
)
|
||||
""",
|
||||
{
|
||||
"thread_id": thread_id,
|
||||
"checkpoint_ns": db_checkpoint_ns,
|
||||
"checkpoint_id": checkpoint_id,
|
||||
"parent_checkpoint_id": parent_checkpoint_id,
|
||||
"checkpoint_json": normalize_json(checkpoint_copy),
|
||||
"metadata_json": normalize_json(
|
||||
get_serializable_checkpoint_metadata(
|
||||
config, # type: ignore[arg-type]
|
||||
metadata,
|
||||
)
|
||||
),
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
logger.error(
|
||||
"oracle_checkpoint.put.failed thread_id=%s checkpoint_ns=%s "
|
||||
"checkpoint_id=%s parent_checkpoint_id=%s blob_channels=%s "
|
||||
"channel_keys=%s",
|
||||
thread_id,
|
||||
db_checkpoint_ns,
|
||||
checkpoint_id,
|
||||
parent_checkpoint_id,
|
||||
sorted(str(channel) for channel in blob_values.keys()),
|
||||
sorted(str(channel) for channel in channel_values.keys()),
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
|
||||
return {
|
||||
"configurable": {
|
||||
"thread_id": thread_id,
|
||||
"checkpoint_ns": checkpoint_ns,
|
||||
"checkpoint_id": checkpoint_id,
|
||||
}
|
||||
}
|
||||
|
||||
def put_writes(
|
||||
self,
|
||||
config: Mapping[str, Any],
|
||||
writes: Sequence[tuple[str, Any]],
|
||||
task_id: str,
|
||||
task_path: str = "",
|
||||
) -> None:
|
||||
configurable = dict(config["configurable"])
|
||||
thread_id = str(configurable["thread_id"])
|
||||
checkpoint_ns = _to_db_ns(str(configurable.get("checkpoint_ns", "")))
|
||||
checkpoint_id = str(configurable["checkpoint_id"])
|
||||
use_upsert = all(channel in WRITES_IDX_MAP for channel, _ in writes)
|
||||
|
||||
try:
|
||||
with self._connection_factory.connect() as conn, conn.cursor() as cur:
|
||||
set_blob_inputsizes(cur, "blob_payload")
|
||||
for idx, (channel, value) in enumerate(writes):
|
||||
write_idx = WRITES_IDX_MAP.get(channel, idx)
|
||||
type_name, payload = self.serde.dumps_typed(value)
|
||||
params = {
|
||||
"thread_id": thread_id,
|
||||
"checkpoint_ns": checkpoint_ns,
|
||||
"checkpoint_id": checkpoint_id,
|
||||
"task_id": task_id,
|
||||
"task_path": task_path,
|
||||
"idx": write_idx,
|
||||
"channel": channel,
|
||||
"type_name": type_name,
|
||||
"blob_payload": _blob_payload_for_storage(
|
||||
type_name,
|
||||
payload,
|
||||
),
|
||||
}
|
||||
if use_upsert:
|
||||
cur.execute(
|
||||
"""
|
||||
MERGE INTO tb_cont_workflow_checkpoint_write dst
|
||||
USING (
|
||||
SELECT
|
||||
:thread_id thread_id,
|
||||
:checkpoint_ns checkpoint_ns,
|
||||
:checkpoint_id checkpoint_id,
|
||||
:task_id task_id,
|
||||
:idx idx
|
||||
FROM dual
|
||||
) src
|
||||
ON (
|
||||
dst.thread_id = src.thread_id
|
||||
AND dst.checkpoint_ns = src.checkpoint_ns
|
||||
AND dst.checkpoint_id = src.checkpoint_id
|
||||
AND dst.task_id = src.task_id
|
||||
AND dst.idx = src.idx
|
||||
)
|
||||
WHEN MATCHED THEN UPDATE SET
|
||||
task_path = :task_path,
|
||||
channel = :channel,
|
||||
type_name = :type_name,
|
||||
blob_payload = :blob_payload,
|
||||
value_json = NULL
|
||||
WHEN NOT MATCHED THEN INSERT (
|
||||
thread_id,
|
||||
checkpoint_ns,
|
||||
checkpoint_id,
|
||||
task_id,
|
||||
task_path,
|
||||
idx,
|
||||
channel,
|
||||
type_name,
|
||||
blob_payload,
|
||||
value_json
|
||||
) VALUES (
|
||||
:thread_id,
|
||||
:checkpoint_ns,
|
||||
:checkpoint_id,
|
||||
:task_id,
|
||||
:task_path,
|
||||
:idx,
|
||||
:channel,
|
||||
:type_name,
|
||||
:blob_payload,
|
||||
NULL
|
||||
)
|
||||
""",
|
||||
params,
|
||||
)
|
||||
else:
|
||||
try:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO tb_cont_workflow_checkpoint_write (
|
||||
thread_id,
|
||||
checkpoint_ns,
|
||||
checkpoint_id,
|
||||
task_id,
|
||||
task_path,
|
||||
idx,
|
||||
channel,
|
||||
type_name,
|
||||
blob_payload,
|
||||
value_json
|
||||
) VALUES (
|
||||
:thread_id,
|
||||
:checkpoint_ns,
|
||||
:checkpoint_id,
|
||||
:task_id,
|
||||
:task_path,
|
||||
:idx,
|
||||
:channel,
|
||||
:type_name,
|
||||
:blob_payload,
|
||||
NULL
|
||||
)
|
||||
""",
|
||||
params,
|
||||
)
|
||||
except Exception as exc:
|
||||
if is_unique_constraint_error(exc):
|
||||
continue
|
||||
raise
|
||||
except Exception:
|
||||
logger.error(
|
||||
"oracle_checkpoint.put_writes.failed thread_id=%s "
|
||||
"checkpoint_ns=%s checkpoint_id=%s task_id=%s "
|
||||
"task_path=%s channels=%s use_upsert=%s",
|
||||
thread_id,
|
||||
checkpoint_ns,
|
||||
checkpoint_id,
|
||||
task_id,
|
||||
task_path,
|
||||
[str(channel) for channel, _ in writes],
|
||||
use_upsert,
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
|
||||
def delete_thread(self, thread_id: str) -> None:
|
||||
with self._connection_factory.connect() as conn, conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"DELETE FROM tb_cont_workflow_checkpoint_write WHERE thread_id = :thread_id",
|
||||
{"thread_id": thread_id},
|
||||
)
|
||||
cur.execute(
|
||||
"DELETE FROM tb_cont_workflow_checkpoint_blob WHERE thread_id = :thread_id",
|
||||
{"thread_id": thread_id},
|
||||
)
|
||||
cur.execute(
|
||||
"DELETE FROM tb_cont_workflow_checkpoint WHERE thread_id = :thread_id",
|
||||
{"thread_id": thread_id},
|
||||
)
|
||||
|
||||
def close(self) -> None:
|
||||
return None
|
||||
|
||||
def _load_checkpoint_tuple(
|
||||
self,
|
||||
cur: Any,
|
||||
row: dict[str, Any],
|
||||
) -> CheckpointTuple:
|
||||
checkpoint = read_json_value(row.get("checkpoint_json")) or {}
|
||||
metadata = read_json_value(row.get("metadata_json")) or {}
|
||||
thread_id = str(row["thread_id"])
|
||||
checkpoint_ns = _db_ns_from_row(row.get("checkpoint_ns"))
|
||||
checkpoint_id = str(row["checkpoint_id"])
|
||||
parent_checkpoint_id = row.get("parent_checkpoint_id")
|
||||
channel_values = dict(checkpoint.get("channel_values") or {})
|
||||
checkpoint["channel_values"] = {
|
||||
**channel_values,
|
||||
**self._load_blobs(
|
||||
cur,
|
||||
checkpoint=checkpoint,
|
||||
thread_id=thread_id,
|
||||
checkpoint_ns=checkpoint_ns,
|
||||
),
|
||||
}
|
||||
pending_writes = self._load_writes(
|
||||
cur,
|
||||
thread_id=thread_id,
|
||||
checkpoint_ns=checkpoint_ns,
|
||||
checkpoint_id=checkpoint_id,
|
||||
)
|
||||
return CheckpointTuple(
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": thread_id,
|
||||
"checkpoint_ns": _from_db_ns(checkpoint_ns),
|
||||
"checkpoint_id": checkpoint_id,
|
||||
}
|
||||
},
|
||||
checkpoint,
|
||||
metadata,
|
||||
(
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": thread_id,
|
||||
"checkpoint_ns": _from_db_ns(checkpoint_ns),
|
||||
"checkpoint_id": parent_checkpoint_id,
|
||||
}
|
||||
}
|
||||
if parent_checkpoint_id
|
||||
else None
|
||||
),
|
||||
pending_writes,
|
||||
)
|
||||
|
||||
def _load_blobs(
|
||||
self,
|
||||
cur: Any,
|
||||
*,
|
||||
checkpoint: dict[str, Any],
|
||||
thread_id: str,
|
||||
checkpoint_ns: str,
|
||||
) -> dict[str, Any]:
|
||||
channel_versions = checkpoint.get("channel_versions") or {}
|
||||
if not isinstance(channel_versions, dict):
|
||||
return {}
|
||||
values: dict[str, Any] = {}
|
||||
for channel, version in channel_versions.items():
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT channel, type_name, blob_payload
|
||||
FROM tb_cont_workflow_checkpoint_blob
|
||||
WHERE thread_id = :thread_id
|
||||
AND (
|
||||
checkpoint_ns = :checkpoint_ns
|
||||
OR (
|
||||
:checkpoint_ns = :default_checkpoint_ns
|
||||
AND checkpoint_ns IS NULL
|
||||
)
|
||||
)
|
||||
AND channel = :channel
|
||||
AND version = :version
|
||||
""",
|
||||
{
|
||||
"thread_id": thread_id,
|
||||
"checkpoint_ns": checkpoint_ns,
|
||||
"default_checkpoint_ns": _DEFAULT_NS,
|
||||
"channel": str(channel),
|
||||
"version": str(version),
|
||||
},
|
||||
)
|
||||
blob_row = _fetchone_dict(cur)
|
||||
if blob_row is None:
|
||||
continue
|
||||
type_name = str(blob_row.get("type_name") or "")
|
||||
if type_name == "empty":
|
||||
continue
|
||||
payload = _read_lob_bytes(blob_row.get("blob_payload"))
|
||||
try:
|
||||
values[str(blob_row["channel"])] = self.serde.loads_typed(
|
||||
(type_name, payload)
|
||||
)
|
||||
except Exception:
|
||||
logger.error(
|
||||
"oracle_checkpoint.load_blob.failed thread_id=%s "
|
||||
"checkpoint_ns=%s channel=%s version=%s type_name=%s "
|
||||
"payload_len=%s",
|
||||
thread_id,
|
||||
checkpoint_ns,
|
||||
channel,
|
||||
version,
|
||||
type_name,
|
||||
len(payload),
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
return values
|
||||
|
||||
def _load_writes(
|
||||
self,
|
||||
cur: Any,
|
||||
*,
|
||||
thread_id: str,
|
||||
checkpoint_ns: str,
|
||||
checkpoint_id: str,
|
||||
) -> list[tuple[str, str, Any]]:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT task_id, channel, type_name, blob_payload
|
||||
FROM tb_cont_workflow_checkpoint_write
|
||||
WHERE thread_id = :thread_id
|
||||
AND (
|
||||
checkpoint_ns = :checkpoint_ns
|
||||
OR (
|
||||
:checkpoint_ns = :default_checkpoint_ns
|
||||
AND checkpoint_ns IS NULL
|
||||
)
|
||||
)
|
||||
AND checkpoint_id = :checkpoint_id
|
||||
ORDER BY task_id, idx
|
||||
""",
|
||||
{
|
||||
"thread_id": thread_id,
|
||||
"checkpoint_ns": checkpoint_ns,
|
||||
"default_checkpoint_ns": _DEFAULT_NS,
|
||||
"checkpoint_id": checkpoint_id,
|
||||
},
|
||||
)
|
||||
rows = [_row_to_dict(cur, row) for row in cur.fetchall()]
|
||||
writes: list[tuple[str, str, Any]] = []
|
||||
for row in rows:
|
||||
type_name = str(row["type_name"])
|
||||
payload = _read_lob_bytes(row.get("blob_payload"))
|
||||
try:
|
||||
value = self.serde.loads_typed((type_name, payload))
|
||||
except Exception:
|
||||
logger.error(
|
||||
"oracle_checkpoint.load_write.failed thread_id=%s "
|
||||
"checkpoint_ns=%s checkpoint_id=%s task_id=%s "
|
||||
"channel=%s type_name=%s payload_len=%s",
|
||||
thread_id,
|
||||
checkpoint_ns,
|
||||
checkpoint_id,
|
||||
row.get("task_id"),
|
||||
row.get("channel"),
|
||||
type_name,
|
||||
len(payload),
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
writes.append(
|
||||
(
|
||||
str(row["task_id"]),
|
||||
str(row["channel"]),
|
||||
value,
|
||||
)
|
||||
)
|
||||
return writes
|
||||
|
||||
def _upsert_blob(
|
||||
self,
|
||||
cur: Any,
|
||||
*,
|
||||
thread_id: str,
|
||||
checkpoint_ns: str,
|
||||
channel: str,
|
||||
version: str,
|
||||
value: Any,
|
||||
) -> None:
|
||||
type_name, payload = self.serde.dumps_typed(value)
|
||||
set_blob_inputsizes(cur, "blob_payload")
|
||||
cur.execute(
|
||||
"""
|
||||
MERGE INTO tb_cont_workflow_checkpoint_blob dst
|
||||
USING (
|
||||
SELECT
|
||||
:thread_id thread_id,
|
||||
:checkpoint_ns checkpoint_ns,
|
||||
:channel channel,
|
||||
:version version
|
||||
FROM dual
|
||||
) src
|
||||
ON (
|
||||
dst.thread_id = src.thread_id
|
||||
AND dst.checkpoint_ns = src.checkpoint_ns
|
||||
AND dst.channel = src.channel
|
||||
AND dst.version = src.version
|
||||
)
|
||||
WHEN MATCHED THEN UPDATE SET
|
||||
type_name = :type_name,
|
||||
blob_payload = :blob_payload,
|
||||
json_payload = NULL
|
||||
WHEN NOT MATCHED THEN INSERT (
|
||||
thread_id,
|
||||
checkpoint_ns,
|
||||
channel,
|
||||
version,
|
||||
type_name,
|
||||
blob_payload,
|
||||
json_payload
|
||||
) VALUES (
|
||||
:thread_id,
|
||||
:checkpoint_ns,
|
||||
:channel,
|
||||
:version,
|
||||
:type_name,
|
||||
:blob_payload,
|
||||
NULL
|
||||
)
|
||||
""",
|
||||
{
|
||||
"thread_id": thread_id,
|
||||
"checkpoint_ns": checkpoint_ns,
|
||||
"channel": channel,
|
||||
"version": version,
|
||||
"type_name": type_name,
|
||||
"blob_payload": _blob_payload_for_storage(type_name, payload),
|
||||
},
|
||||
)
|
||||
|
||||
def get_next_version(self, current: Any, channel: None) -> str:
|
||||
if current is None:
|
||||
current_v = 0
|
||||
elif isinstance(current, int):
|
||||
current_v = current
|
||||
else:
|
||||
current_v = int(str(current).split(".")[0])
|
||||
next_v = current_v + 1
|
||||
next_h = random()
|
||||
return f"{next_v:032}.{next_h:016}"
|
||||
|
||||
|
||||
def _to_db_ns(checkpoint_ns: str) -> str:
|
||||
return checkpoint_ns or _DEFAULT_NS
|
||||
|
||||
|
||||
def _from_db_ns(checkpoint_ns: str | None) -> str:
|
||||
return (
|
||||
""
|
||||
if not checkpoint_ns or checkpoint_ns == _DEFAULT_NS
|
||||
else checkpoint_ns
|
||||
)
|
||||
|
||||
|
||||
def _db_ns_from_row(checkpoint_ns: Any) -> str:
|
||||
if checkpoint_ns is None:
|
||||
return _DEFAULT_NS
|
||||
return str(checkpoint_ns) or _DEFAULT_NS
|
||||
|
||||
|
||||
def _fetchone_dict(cur: Any) -> dict[str, Any] | None:
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
return _row_to_dict(cur, row)
|
||||
|
||||
|
||||
def _row_to_dict(cur: Any, row: Any) -> dict[str, Any]:
|
||||
names = [str(col[0]).lower() for col in cur.description]
|
||||
return dict(zip(names, row, strict=False))
|
||||
|
||||
|
||||
def _metadata_matches(metadata: Any, expected: dict[str, Any]) -> bool:
|
||||
if not isinstance(metadata, dict):
|
||||
return False
|
||||
for key, value in expected.items():
|
||||
if metadata.get(key) != value:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _read_lob_bytes(value: Any) -> bytes:
|
||||
if value is None:
|
||||
return b""
|
||||
if isinstance(value, bytes):
|
||||
return value
|
||||
if isinstance(value, bytearray):
|
||||
return bytes(value)
|
||||
if isinstance(value, memoryview):
|
||||
return value.tobytes()
|
||||
if isinstance(value, str):
|
||||
return value.encode("utf-8")
|
||||
read = getattr(value, "read", None)
|
||||
if callable(read):
|
||||
return _read_lob_bytes(read())
|
||||
return bytes(value)
|
||||
|
||||
|
||||
def _blob_payload_for_storage(type_name: str, payload: bytes | None) -> bytes:
|
||||
if payload:
|
||||
return payload
|
||||
if type_name in {"empty", "null"}:
|
||||
# Oracle can normalize zero-length BLOB binds to NULL. LangGraph's
|
||||
# serde ignores the payload for these tags, so a one-byte sentinel keeps
|
||||
# legacy payload checks and Oracle's empty-BLOB behavior from breaking
|
||||
# resume writes that legitimately carry None.
|
||||
return b"\x00"
|
||||
return payload if payload is not None else b""
|
||||
Reference in New Issue
Block a user