Skip to content

SurrealDB sink

Writes each record as a SurrealDB record over the HTTP /sql endpoint. The table can be fixed or derived from the topic, and the sink can define it along with the namespace and database. Record ids are built from topic, partition and offset and every write is an INSERT IGNORE, so a replayed batch inserts nothing.

TypeSink
Librarylibpicomq_connector_surrealdb_sink
Ships inThe pico-connectors image
DestinationTable, templated per topic, inside one namespace and database
Creates destinationYes, with auto_define_table
On replayNo duplicates in any configuration
PayloadAny schema. JSON stays an object, text a string, everything else base64
orders.eubatch of 1000resolve tableorders_{segment[-1]}INSERT IGNOREid from offsettableorders_euup to batch_size records per statement, retried on transient errors

Quick start

toml
type = "sink"
key = "orders_surreal"
enabled = true
version = 0
name = "Orders to SurrealDB"
path = "libpicomq_connector_surrealdb_sink"

[[topics]]
pattern = 'orders\..*'
schema = "json"
batch_length = 1000
poll_interval = "100ms"

[plugin_config]
endpoint = "surrealdb:8000"
namespace = "picomq"
database = "app"
table = "orders_{topic_segment[-1]}"
username = "root"
password = "root"
auto_define_table = true
define_indexes = true

Keep the password out of the file with an environment override.

bash
PICOMQ_CONNECTORS_SINK_ORDERS_SURREAL_PLUGIN_CONFIG_PASSWORD=secret

How it works

On open() the sink validates the configuration. auth_scope and payload_format must be known values, endpoint must have a host and no path, query or embedded credentials, and namespace, database and a static table must match [A-Za-z_][A-Za-z0-9_]*. auto_define_table requires auth_scope = "root" and define_indexes requires include_metadata = true, both are rejected otherwise.

It then builds an HTTP client with query_timeout, posts the credentials to /signin unless auth_scope is none, and calls /health. With auto_define_table on it runs DEFINE NAMESPACE IF NOT EXISTS, USE NS and DEFINE DATABASE IF NOT EXISTS, and for a static table DEFINE TABLE IF NOT EXISTS <table> SCHEMALESS. With define_indexes on it also runs DEFINE INDEX IF NOT EXISTS <table>_pico_offset_idx ON TABLE <table> FIELDS pico_topic, pico_partition, pico_offset.

For each batch the runtime hands over, the sink does the following.

  1. Resolves table against the topic name and rewrites it into a valid identifier.
  2. Defines the table if auto_define_table is on and this name has not been seen before. The result is cached per name.
  3. Builds one record per message with id, the metadata fields, the key, the headers, payload and payload_encoding. A record whose payload cannot be converted is left out, counted as an error, and remembered.
  4. Splits the records into chunks of batch_size and sends each as INSERT IGNORE INTO <table> [ ...records as JSON... ] RETURN NONE to /sql with Surreal-NS and Surreal-DB headers and basic auth.
  5. Checks every statement in the response has status OK, otherwise the chunk fails with the statement's detail.
  6. Attempts every chunk, then returns the last error if any chunk failed or any record was dropped. The runtime holds the offset and redelivers the whole batch.

Transient errors are retried inside the sink up to max_retries attempts in total, with an exponential backoff starting at retry_delay, doubling per attempt, capped at max_retry_delay, with 20 percent jitter. Transient means a response mentioning transaction conflict or transaction can be retried, a connection or timeout error from the HTTP client, or HTTP 408, 429, 500, 502, 503 or 504. A connection error also triggers a reconnect, which repeats the sign in, health check and namespace setup before the next attempt. Query errors such as a syntax or permission failure fail the chunk immediately.

Configuration

All keys go under [plugin_config].

KeyTypeDefaultMeaning
endpointstringrequiredhost:port or http(s)://host:port. No path, query or credentials
namespacestringrequiredSurrealDB namespace. Letters, digits and _ only, not starting with a digit
databasestringrequiredSurrealDB database, same rules
tabletemplaterequiredTable name. Supports {topic} and {topic_segment[n]}, see templating
usernamestringnoneRequired unless auth_scope = "none"
passwordstringnoneRequired unless auth_scope = "none". Redacted in the API
auth_scopestringrootLevel the credentials sign in at. root, namespace, database or none
use_tlsboolfalseUse https:// when endpoint has no scheme. Ignored when a scheme is given
auto_define_tableboolfalseDefine namespace, database and each table on first use. Needs auth_scope = "root"
define_indexesboolfalseAlso define an index on pico_topic, pico_partition, pico_offset per table. Needs auto_define_table and include_metadata
payload_formatstringautoauto picks by decoded schema. json, text or base64 force one. binary is an alias for base64
include_metadatabooltrueAdd pico_topic, pico_partition, pico_offset, pico_timestamp and pico_schema
include_headersbooltrueAdd pico_headers when the record has headers
include_keybooltrueAdd pico_key when the record has a key
batch_sizeint1000Records per INSERT statement. Values below 1 become 1
query_timeoutduration30sHTTP timeout for every request
max_retriesint3Attempts per chunk on transient errors. 0 is treated as 1
retry_delayduration100msFirst backoff delay, doubled per attempt
max_retry_delayduration5sBackoff cap. Raised to retry_delay if set lower
verbose_loggingboolfalseLog every batch at info instead of debug

payload_format = "json" requires the payload to parse as JSON, and text requires valid UTF-8. A record that fails is dropped from its chunk and the batch returns an error, so the runtime redelivers it until the sink stops. Use auto unless the topic schema is guaranteed.

What lands in the table

With defaults, a JSON record becomes the following.

json
{
  "id": "t6f72646572732d6575_p0_o4711",
  "pico_topic": "orders.eu",
  "pico_partition": "0",
  "pico_offset": "4711",
  "pico_timestamp": "1756934104118",
  "pico_schema": "json",
  "pico_key": "azE=",
  "pico_headers": { "trace-id": "abc" },
  "payload": { "order_id": 42, "total": 42.5 },
  "payload_encoding": "json"
}
FieldPresent whenContent
idalwayst<hex of topic>_p<partition>_o<offset>. Hex keeps the id a plain identifier for any topic name
pico_topicinclude_metadataTopic the record came from
pico_partitioninclude_metadataAlways "0" on PicoMQ, as a string
pico_offsetinclude_metadataRecord offset as a string, so 64-bit values survive
pico_timestampinclude_metadataRecord timestamp, epoch milliseconds as a string
pico_schemainclude_metadataTopic schema the batch was decoded with
pico_keyinclude_key and the record has a keyRecord key, base64
pico_headersinclude_headers and the record has headersObject of header name to value. UTF-8 values are strings, others are { "data": base64, "pico_header_encoding": "base64" }
payloadalwaysObject for JSON, string for text, base64 string otherwise
payload_encodingalwaysjson, text or base64

The metadata numbers are strings, so a range query on pico_offset has to cast, type::int(pico_offset) > 4000. The index created by define_indexes is not unique, deduplication comes from the record id.

Table names

The resolved name is rewritten. Every character outside letters, digits and _ becomes _, and a leading digit gets a _ prefix. A topic of orders.us-east under events_{topic} produces the table events_orders_us_east, and a topic of 42_events under {topic} produces _42_events.

Replay

The runtime redelivers a batch after a crash between the write and the offset commit. See Delivery guarantees.

ConfigurationResult of a replayed batch
AnyEvery record id already exists, INSERT IGNORE skips it. No visible change

The id does not depend on include_metadata, include_key or include_headers, so the guarantee holds with all of them off. An existing record is never updated by a replay.

Requirements

  • SurrealDB with the HTTP API reachable from the runtime, /sql, /signin and /health.
  • A user matching auth_scope. auto_define_table needs the root user because it issues namespace and database DDL. Without it the namespace, database and tables must exist, or the database must allow implicit table creation.
  • use_tls = true or an https:// endpoint when credentials cross a network.

Troubleshooting

SymptomCause
SurrealDB auth_scope must be one of root, namespace, database, or noneA typo in auth_scope. Unlike other sinks this one refuses to start
SurrealDB endpoint must not include embedded credentialsuser:pass@ in endpoint. Move them to username and password
SurrealDB namespace must contain only ASCII letters, digits, and underscoresHyphens or dots in namespace or database
SurrealDB auto_define_table requires auth_scope=rootTable creation with a namespace or database user. Define the tables ahead of time or use root
Failed to authenticate with SurrealDB at startWrong credentials, or credentials that exist at a different level than auth_scope
Invalid JSON payload or Invalid UTF-8 payloadA forced payload_format that does not match the records. Use auto
SurrealDB batch insert failed after N attemptsA query error, or a transient error that outlasted max_retries. The message carries the statement detail