Skip to content

7.2.6 - JsonPatch, Queue Sink, CDC Sink & Smuggler πŸπŸ”€

Choose a tag to compare

@poissoncorp poissoncorp released this 16 Sep 13:35
· 4 commits to v7.2 since this release
90f7b56

7.2.6 syncs the Python client with the RavenDB 7.2.5 and 7.2.6 C# clients in one go, and it is a big one. The session patch methods now build RFC 6902 JsonPatch commands instead of JavaScript patch scripts. Three subsystems the Python client simply never had arrive complete: Queue Sink, CDC Sink and the database smuggler. Server-wide connection strings, SSO on client certificates and per-turn output schemas for AI agents land alongside them. And 269 types that existed but were only reachable through their module paths are now importable straight from ravendb.

Highlights

Session patching with JsonPatch

session.advanced.patch, patch_array and patch_object now emit an RFC 6902 JsonPatch command wherever the path and the value allow it, so the server applies the change structurally instead of running a patch script. Anything JsonPatch cannot express keeps the old JavaScript path automatically: composite values, remove_all predicates, increment, and paths that are not addressable as a JSON pointer. Patches for the same document merge into a single command, as they always did.

from ravendb import DocumentStore

store = DocumentStore(urls=["http://localhost:8080"], database="NorthWind")
store.initialize()

with store.open_session() as session:
    session.advanced.patch("users/1-A", "Name", "patched")      # add   /Name
    session.advanced.patch("users/1-A", "Address.City", "Krakow")  # add   /Address/City
    session.advanced.patch("users/1-A", "Tags[0]", "primary")   # replace /Tags/0
    session.advanced.patch_array("users/1-A", "Tags", lambda tags: tags.add("new"))
    session.save_changes()  # one JsonPatch command, four operations

The standalone operation is available too, for building a patch by hand:

from ravendb import JsonPatchDocument, JsonPatchOperation

patch = JsonPatchDocument().add("/Name", "patched").add("/Tags/-", "c").replace("/Tags/0", "z")
result = store.operations.send(JsonPatchOperation("users/1-A", patch))

Set store.conventions.session_patch_behavior = SessionPatchBehavior.JAVA_SCRIPT before initialize() to stay on the previous code path entirely.

New operations: JsonPatchOperation
New classes: JsonPatchDocument, JsonPatchResult, JsonPatchCommandData
New enums: SessionPatchBehavior (JSON_PATCH default, JAVA_SCRIPT), CommandType.JSON_PATCH
New convention: DocumentConventions.session_patch_behavior

Queue Sink

RavenDB consuming messages from Kafka, RabbitMQ, Azure Queue Storage, Amazon SQS or Azure Service Bus. The client had the QueueSink ongoing-task enum member and nothing behind it: no configuration, no operations, no task result. It now has all of it, including DatabaseRecord.queue_sinks.

from ravendb import (
    AddQueueSinkOperation, AzureServiceBusSinkSource,
    QueueBrokerType, QueueSinkConfiguration, QueueSinkScript,
)

store.maintenance.send(AddQueueSinkOperation(QueueSinkConfiguration(
    name="orders-sink",
    broker_type=QueueBrokerType.AZURE_SERVICE_BUS,
    connection_string_name="asb",
    scripts=[QueueSinkScript("orders", [
        AzureServiceBusSinkSource.queue("orders"),
        AzureServiceBusSinkSource.subscription("events", "audit"),
    ], "put('orders/', this);")],
)))

New operations: AddQueueSinkOperation, UpdateQueueSinkOperation
New classes: QueueSinkConfiguration, QueueSinkScript, AzureServiceBusSinkSource, QueueSinkProcessState, OngoingTaskQueueSink, AddQueueSinkOperationResult, UpdateQueueSinkOperationResult

CDC Sink

The whole public configuration tree (13 model types, from CdcColumnMapping up to CdcSinkConfiguration, with embedded and linked tables nesting to any depth), plus the two task operations, the ongoing-task result with its lag and health fields, and DatabaseRecord.cdc_sinks.

from ravendb import (
    AddCdcSinkOperation, CdcColumnMapping, CdcColumnType, CdcSinkConfiguration,
    CdcSinkOnDeleteConfig, CdcSinkPostgresSettings, CdcSinkTableConfig,
)

store.maintenance.send(AddCdcSinkOperation(CdcSinkConfiguration(
    name="orders-cdc", connection_string_name="pg",
    postgres=CdcSinkPostgresSettings(publication_name="pub", slot_name="slot"),
    tables=[CdcSinkTableConfig(
        collection_name="Orders", source_table_name="orders",
        columns=[CdcColumnMapping("payload", "Payload", CdcColumnType.JSON)],
        primary_key_columns=["id"],
        on_delete=CdcSinkOnDeleteConfig(ignore_deletes=True))],
)))

Schema discovery and mapping preview come with it: GetCdcSinkSchemaOperation browses the source database a CDC task would read: tables, columns, primary and foreign keys, each annotated with what CDC can actually capture. TestCdcSinkMappingOperation runs real rows through the configured mapping without saving anything.

New operations: AddCdcSinkOperation, UpdateCdcSinkOperation, GetCdcSinkSchemaOperation, TestCdcSinkMappingOperation
New classes: CdcSinkConfiguration, CdcSinkTableConfig, CdcSinkEmbeddedTableConfig, CdcSinkLinkedTableConfig, CdcSinkOnDeleteConfig, CdcSinkPostgresSettings, CdcColumnMapping, OngoingTaskCdcSink
New enums: CdcColumnType, CdcSinkRelationType, CdcSinkProcessState, CdcSinkTaskState, CdcSinkTableLoadState, OngoingTaskType.CDC_SINK

Database smuggler

store.smuggler exports a database to a file or a stream and imports one back. Item selection is a set of DatabaseItemType members, rendered to the comma-separated string the server parses.

from ravendb import DatabaseItemType, DatabaseSmugglerExportOptions, DatabaseSmugglerImportOptions

store.smuggler.export(DatabaseSmugglerExportOptions(
    operate_on_types={DatabaseItemType.DOCUMENTS},
    collections=["Orders"]), "dump.ravendbdump").wait_for_completion()

other.smuggler.import_data(DatabaseSmugglerImportOptions(), "dump.ravendbdump")
store.smuggler.for_database("reporting").import_incremental(options, backup_dir)

Results are typed. A finished smuggler operation hands back a SmugglerResult with counts per item type, rather than a raw dictionary:

result = store.smuggler.import_data(options, "dump.ravendbdump").wait_for_completion()

result.documents.read_count
result.documents.attachments.read_count
result.database_record.updated

New property: store.smuggler
New classes: DatabaseSmuggler, DatabaseSmugglerExportOptions, DatabaseSmugglerImportOptions, DatabaseSmugglerOptions, SmugglerResult, SmugglerProgressBase, DatabaseRecordProgress, SmugglerOperation
New enums: DatabaseItemType, DatabaseRecordItemType, ExportCompressionAlgorithm

Server-wide connection strings

A connection string the cluster propagates to every database except the ones you exclude. ServerWideConnectionString flattens the underlying connection string into the request body, the shape the endpoint expects, and dispatches back to the right concrete type on read.

from ravendb import RavenConnectionString
from ravendb.serverwide.operations.connection_strings import (
    PutServerWideConnectionStringOperation, ServerWideConnectionString,
)

store.maintenance.server.send(PutServerWideConnectionStringOperation(
    ServerWideConnectionString(
        connection_string=RavenConnectionString("shared", "orders", ["http://localhost:8080"]),
        excluded_databases=["scratch"])))

# the name a propagated connection string carries inside a database record
ServerWideConnectionString.get_database_record_connection_string_name("shared")
# -> "Server Wide Connection String, shared"

New operations: PutServerWideConnectionStringOperation, GetServerWideConnectionStringsOperation, RemoveServerWideConnectionStringOperation
New classes: ServerWideConnectionString, ConnectionStringUsage
New enum: ConnectionStringUsageKind

AI agents: per-turn output schema

An agent's output schema is fixed when the agent is created. A single conversation turn can now override it without disturbing the turns around it.

from ravendb import AiOutputOptions

chat = store.ai.conversation("agents/support", "conversations/")
chat.set_user_prompt("How does the customer feel about the delay?")

# a schema derived from a sample object, for this turn only
chat.run_with_schema(AiOutputOptions(sample_object={"Sentiment": "positive"}))

# an explicit JSON schema, or no structure at all so the model answers in prose
chat.run_with_schema(AiOutputOptions(output_schema=schema))
chat.run_with_schema(AiOutputOptions(no_schema=True))

C# splits these across a dozen generic overloads keyed on TAnswer; Python has one options object and two entry points. Asking for no schema while also supplying one is refused.

New classes: AiOutputOptions
New methods: AiConversation.run_with_schema, AiConversation.stream_with_schema

AI agents: reading a conversation back

store.ai.get_conversation_messages(...) takes either a conversation id or a GetConversationMessagesOptions for timestamp paging and a detail level. Messages come back with roles, inlined tool calls and per-message usage.

from ravendb import AiConversationDetailLevel, GetConversationMessagesOptions

messages = store.ai.get_conversation_messages(GetConversationMessagesOptions(
    "chats/1-A", page_size=25, detail_level=AiConversationDetailLevel.DETAILED))

for message in messages.messages:
    print(message.role, message.content)

New operations: GetConversationMessagesOperation
New classes: GetConversationMessagesOptions, AiConversationMessage, AiConversationMessagesResult, AiToolCallResult
New enums: AiConversationDetailLevel, AiMessageRole

SSO on client certificates

CertificateMetadata gains usage, sso_identifiers, sso_server_public_key_pinning_hashes and allow_any_sso_server. The interesting part is the write side: EditClientCertificateOperation now builds its body explicitly, so the three SSO fields are only sent when you set them. Leave them unset and the server keeps the stored SSO configuration, which is what a plain rename or a disabled-only toggle on an SSO user wants. Pass an empty list and it clears the stored value.

from ravendb import EditClientCertificateOperation, SecurityClearance, SsoIdentifier, SsoProvider

store.maintenance.server.send(EditClientCertificateOperation(
    EditClientCertificateOperation.Parameters(
        thumbprint=thumbprint, name="reporting", clearance=SecurityClearance.OPERATOR,
        permissions={}, sso_identifiers=[SsoIdentifier(SsoProvider.MICROSOFT, "user@example.com")])))

New classes: SsoIdentifier
New enums: SsoProvider, CertificateUsage
New fields on CertificateMetadata, new opt-in parameters on EditClientCertificateOperation.Parameters

Typed license limits

A refusal on licensing grounds used to arrive as a bare RavenException you had to string-match. It now arrives as LicenseLimitException, registered in the exception dispatcher, alongside the full LimitType enum (59 members, including ServerWideConnectionStrings, CdcSink and Sso). It derives from RavenException, so existing handlers keep working.

from ravendb import AddCdcSinkOperation, LicenseLimitException

try:
    store.maintenance.send(AddCdcSinkOperation(configuration))
except LicenseLimitException as error:
    print("not available on this license:", error)

New exceptions: LicenseLimitException
New enums: LimitType

The whole public surface is importable from ravendb

ravendb/__init__.py carried a long backlog of types that existed but were never exported, so whole subsystems could only be reached through their module paths. 269 names are now importable straight from the package root: subscriptions, time series, counters, the changes API, index and spatial enums, the exception hierarchy, ETL connection strings for every queue broker, and the server-wide operations.

# all of these used to need their full module path
from ravendb import (
    SubscriptionCreationOptions, SubscriptionWorkerOptions,   # subscriptions
    TimeSeriesRange, ConfigureTimeSeriesOperation,            # time series
    CounterBatchOperation, DocumentChange, DatabaseChanges,   # counters, changes
    FieldIndexing, FieldStorage, SpatialOptions,              # index definitions
    ConcurrencyException, RavenException,                     # exceptions
    DeleteDatabaseOperation, GetLogsConfigurationOperation,   # server-wide
    KafkaConnectionSettings, RabbitMqConnectionSettings,      # ETL connection strings
)

test_imports.py now asserts every name the package root binds, so an export cannot be dropped without failing the suite.

Other Changes

  • Azure Service Bus as a queue ETL broker - QueueBrokerType.AZURE_SERVICE_BUS and AzureServiceBusConnectionSettings, with all three authentication shapes: a connection string, Entra ID service-principal credentials (AzureServiceBusEntraId), or passwordless managed identity (AzureServiceBusPasswordless).
  • Cloud settings conversions - S3Settings.to_remote_attachments_s3_settings() and RemoteAttachmentsS3Settings.to_s3_settings(), and the same pair for Azure. Backup S3Settings gains storage_class, which it was missing entirely.
  • S3 checksum opt-out - disable_checksum_validation on both S3Settings and RemoteAttachmentsS3Settings, for S3-compatible storage without modern object integrity checks.
  • Chunk text on embeddings generation - EmbeddingsGenerationConfiguration.store_chunk_text keeps each chunk's text next to its embedding.
  • A typed query-tool failure - QueryToolFailedException joins the AiException family and the dispatcher, so an agent's failed query tool is catchable by type.
  • Cancel pending action tools - AiConversation.cancel_pending_action_tools drops the tool calls a conversation is still waiting on instead of answering them; the flag clears once a run succeeds.
  • Pull replication cursors - hub_cursor and sink_cursor on OngoingTaskPullReplicationAsSink.
  • Connection string usages - the GET endpoint returns a UsedBy array on every connection string; it is parsed into connection_string.used_by instead of being dropped.
  • A vector search survives a query alias - VectorSearchToken inherited add_alias from WhereToken, so qualifying a field with a query alias threw away the similarity threshold, the candidate count and exactness. Chasing that turned up an older problem: the caller ran token.add_alias(from_alias) and discarded the result, so no alias was ever applied.
  • DatabaseRecord deserialization - from_json called .items() on a possibly-absent AutoIndexes and passed a possibly-absent LockMode straight into an enum, so any payload missing either raised. to_json had the mirror bug and could never serialize an auto-index. AutoIndexDefinition.from_json had the same strictness one level down.
  • Constants - the X-Forwarded-For header and three new SupportedFeatures keys.
  • RequestExecutor.CLIENT_VERSION is now 7.2.6.

Compatibility

Every signature change in this release is additive. Four constructors that would have taken a new parameter mid-list (RunConversationOperation, EmbeddingsGenerationConfiguration and RemoteAttachmentsS3Settings) take it at the end instead, so positional callers are unaffected, and Operation.wait_for_completion() still returns None.

The one behaviour change worth knowing about is the session patch default. Measured against a live server, a JsonPatch and the equivalent script are identical for setting an existing member, creating a new one, overwriting an array element, and writing through a missing intermediate (both error). They differ in exactly one case: writing past the end of an array, which the script appends and JsonPatch rejects. SessionPatchBehavior.JAVA_SCRIPT restores the previous path wholesale.