nautilus-orm-protocol 0.1.3

Wire protocol types and serialization for Nautilus ORM
Documentation

Nautilus Protocol

JSON-RPC 2.0 protocol for multi-language Nautilus clients.

Overview

This crate defines the stable wire format for communication between:

  • Language-specific clients (JavaScript, Python, etc.)
  • Nautilus engine (Rust binary running over stdin/stdout)

Public API

Module Key exports
wire RpcRequest, RpcResponse, RpcError, RpcId, ok(), err()
methods 11 method-name constants (ENGINE_HANDSHAKE, QUERY_*, SCHEMA_VALIDATE), request param structs, response structs
error ProtocolError enum (18 variants), stable error-code constants, Result<T> alias
version PROTOCOL_VERSION constant, ProtocolVersion wrapper

Design Notes

  • Single consumer — only nautilus-engine depends on this crate today, but the API is designed for any language client that speaks JSON-RPC 2.0 over stdin/stdout.
  • schema.validate types are defined but not yet implemented in the engine; they are reserved for a future release.
  • Value encoding (base64 for bytes, string for decimals, etc.) is specified in this README as the wire-format contract, but the Rust encoding/decoding logic lives in nautilus-engine, not here.

Protocol Stack

  • Transport: Line-delimited JSON over stdin/stdout
  • Format: JSON-RPC 2.0
  • Versioning: Protocol version included in every request

Protocol Version

Current version: 1

All client requests must include protocolVersion: 1 in their params. The engine will reject requests with unsupported versions.

JSON Encoding

The protocol uses a stable JSON encoding for Nautilus Value types to ensure cross-language compatibility.

Value Type Mappings

Note: These encoding rules define the stable wire format between clients and the engine. The actual encoding/decoding logic lives in the engine layer, not in this crate — nautilus-protocol only defines the request/response structures and their Serialize / Deserialize implementations.

Nautilus Type JSON Type Example Notes
Null null null
Bool boolean true, false
I32 number 42 32-bit signed integer
I64 number 9007199254740991 64-bit signed integer*
F64 number 3.14159 IEEE 754 double
String string "hello" UTF-8 text
Bytes string "SGVsbG8=" Base64 encoded
Decimal string "123.45" String to avoid precision loss
DateTime string "2026-02-18T10:30:00Z" RFC3339 / ISO 8601
Uuid string "550e8400-e29b-41d4-a716-446655440000" Hyphenated lowercase
Json any {"key": "value"} Pass-through JSON value
Array array ["a", "b", "c"] Homogeneous array
Array2D array [["a", "b"], ["c", "d"]] 2D array

*Note on BigInt: JavaScript clients should be aware of Number.MAX_SAFE_INTEGER (2^53 - 1). For values outside this range, the engine may return strings in future protocol versions.

Example Value Encodings

{
  "userId": "550e8400-e29b-41d4-a716-446655440000",
  "email": "user@example.com",
  "age": 25,
  "balance": "1234.56",
  "createdAt": "2026-02-18T10:30:00Z",
  "isActive": true,
  "avatar": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
  "tags": ["rust", "database", "orm"],
  "metadata": {
    "custom": "json",
    "nested": true
  }
}

Methods

engine.handshake

Initial handshake to validate protocol compatibility.

Request:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "engine.handshake",
  "params": {
    "protocolVersion": 1,
    "clientName": "nautilus-js",
    "clientVersion": "0.1.0"
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "engineVersion": "0.1.0",
    "protocolVersion": 1
  }
}

query.findMany

Execute a findMany query on a model.

Request:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "query.findMany",
  "params": {
    "protocolVersion": 1,
    "model": "User",
    "args": {
      "where": {
        "email": {
          "contains": "test"
        }
      },
      "orderBy": {
        "createdAt": "desc"
      },
      "take": 10
    }
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "data": [
      {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "email": "test@example.com",
        "createdAt": "2026-02-18T10:30:00Z"
      }
    ]
  }
}

query.create

Create a new record.

Request:

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "query.create",
  "params": {
    "protocolVersion": 1,
    "model": "User",
    "data": {
      "email": "new@example.com",
      "name": "New User"
    }
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "count": 1,
    "data": [
      {
        "id": "660f9511-f3ac-52e5-b827-557766551111",
        "email": "new@example.com",
        "name": "New User"
      }
    ]
  }
}

query.findFirst

Return the first record matching the given arguments (or null).

Request:

{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "query.findFirst",
  "params": {
    "protocolVersion": 1,
    "model": "User",
    "args": {
      "where": { "isActive": true },
      "orderBy": { "createdAt": "desc" }
    }
  }
}

Response: Same shape as query.findMany (QueryResult).

query.findFirstOrThrow

Same as query.findFirst, but returns error code 3004 (Record not found) if no matching record exists.

query.findUnique

Find a single record by a unique filter.

Request:

{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "query.findUnique",
  "params": {
    "protocolVersion": 1,
    "model": "User",
    "filter": { "id": 42 }
  }
}

Response: Same shape as query.findMany (QueryResult).

query.findUniqueOrThrow

Same as query.findUnique, but returns error code 3004 (Record not found) if no matching record exists.

query.createMany

Create multiple records in a single operation.

Request:

{
  "jsonrpc": "2.0",
  "id": 6,
  "method": "query.createMany",
  "params": {
    "protocolVersion": 1,
    "model": "User",
    "data": [
      { "email": "alice@example.com", "name": "Alice" },
      { "email": "bob@example.com", "name": "Bob" }
    ]
  }
}

Response: MutationResult with count reflecting the number of inserted rows.

query.update

Update a record matching a filter.

Request:

{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "query.update",
  "params": {
    "protocolVersion": 1,
    "model": "User",
    "filter": { "id": 1 },
    "data": { "name": "Updated Name" }
  }
}

Response: MutationResult with count and optional data (when the dialect supports RETURNING).

query.delete

Delete a record matching a filter.

Request:

{
  "jsonrpc": "2.0",
  "id": 8,
  "method": "query.delete",
  "params": {
    "protocolVersion": 1,
    "model": "Post",
    "filter": { "id": 99 }
  }
}

Response: MutationResult with count.

schema.validate (reserved — not yet implemented)

Validate a schema string without applying it.

Request:

{
  "jsonrpc": "2.0",
  "id": 9,
  "method": "schema.validate",
  "params": {
    "protocolVersion": 1,
    "schema": "model User { id Int @id\n  name String }"
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 9,
  "result": {
    "valid": true,
    "errors": null
  }
}

Error Codes

Standard JSON-RPC Errors

Code Message Description
-32700 Parse error Invalid JSON received
-32600 Invalid Request Invalid JSON-RPC request object
-32601 Method not found Unknown method name
-32602 Invalid params Invalid method parameters
-32603 Internal error Internal JSON-RPC error

Nautilus Error Codes

Range Category Example Codes
1000-1999 Schema/Validation 1000 Schema validation1001 Invalid model1002 Invalid field1003 Type mismatch
2000-2999 Query Planning 2000 Query planning2001 Invalid filter2002 Invalid orderBy2003 Unsupported operation
3000-3999 Database Execution 3000 Database execution3001 Connection failed3002 Constraint violation3003 Query timeout
9000-9999 Internal Engine 9000 Internal error9001 Unsupported protocol version9002 Invalid method9003 Invalid request params

Error Response Example:

{
  "jsonrpc": "2.0",
  "id": 4,
  "error": {
    "code": 1001,
    "message": "Invalid model: UnknownModel"
  }
}

Versioning Policy

Backward Compatibility

  • Minor changes (new optional fields, new methods): No version bump, clients ignore unknown fields
  • Breaking changes (renamed/removed fields, changed semantics): Protocol version increment

Client Compatibility

Clients should:

  1. Send handshake with their protocol version
  2. Check engine's protocol version in response
  3. Abort if versions are incompatible

Migration Path

When protocol version 2 is released:

  • Engine will support both v1 and v2 simultaneously (for a transition period)
  • Clients send their version in every request
  • Engine responds according to requested version

Transport

Line-Delimited JSON

Each request and response is a single JSON object on one line, terminated by \n:

{"jsonrpc":"2.0","id":1,"method":"engine.handshake","params":{"protocolVersion":1}}\n
{"jsonrpc":"2.0","id":1,"result":{"engineVersion":"0.1.0","protocolVersion":1}}\n
{"jsonrpc":"2.0","id":2,"method":"query.findMany","params":{"protocolVersion":1,"model":"User"}}\n

Multiplexing

The protocol supports concurrent in-flight requests:

  • Each request has a unique id
  • Responses may arrive out-of-order
  • Clients match responses to requests by id

Logging

The engine writes:

  • stdout: JSON-RPC responses only (one per line)
  • stderr: Debug logs, warnings, errors (not JSON)

Clients should:

  • Read stdout for responses
  • Optionally capture stderr for debugging

Usage Example

use nautilus_protocol::*;
use serde_json::json;

// Create a findMany request
let request = RpcRequest {
    jsonrpc: "2.0".to_string(),
    id: Some(RpcId::Number(1)),
    method: QUERY_FIND_MANY.to_string(),
    params: serde_json::to_value(FindManyParams {
        protocol_version: PROTOCOL_VERSION,
        model: "User".to_string(),
        args: Some(json!({
            "where": { "email": { "contains": "test" } }
        })),
    }).unwrap(),
};

// Serialize to JSON line
let line = format!("{}\n", serde_json::to_string(&request).unwrap());

// Write to engine stdin...
// Read response from engine stdout...

License

MIT or Apache-2.0