duckdbinferalrelay

February 27, 2026 · View on GitHub

DuckDB extension for consuming LDES streams from Inferal Relay.

Build

make release      # Build extension
make duckdb       # Build + launch DuckDB CLI with extension loaded
make duckdb DB=dev.duckdb  # With persistent database
make test         # Run tests

Requires: cmake, C++ compiler. No Docker needed.

Quick start

-- Add a stream
SELECT inferal_relay_add_stream('my-stream', 'https://relay.inferal.com', 'acme', 'snapshots', 100);

-- Set up credentials via DuckDB Secrets Manager
CREATE PERSISTENT SECRET (
    TYPE inferal_relay,
    API_KEY 'sk-your-key',
    SCOPE 'my-stream'
);

-- Sync data (fetches pages until caught up, max 100 pages)
SELECT * FROM inferal_relay_sync('my-stream');

-- Or with a custom page limit
SELECT * FROM inferal_relay_sync('my-stream', 10);

-- Query members
SELECT member_id, generated_at, data
FROM inferal_relay.members
WHERE stream_name = 'my-stream'
ORDER BY generated_at DESC
LIMIT 10;

-- Or sync + query in one call (auto-refreshing table)
SELECT * FROM inferal_relay_live('my-stream');
COPY (SELECT * FROM inferal_relay_live('my-stream')) TO 'data.parquet';

External sync (no outbound HTTP)

For environments where the extension cannot make HTTP requests:

-- Get the URL to fetch externally
SELECT inferal_relay_build_next_url('my-stream');

-- Fetch with curl, a worker, etc., then push the body:
SELECT * FROM inferal_relay_ingest_page('my-stream', $body);
-- Returns: members_received, next_event_id, next_partition_hint, has_more

-- Repeat while has_more = true

Stateless table function

Read LDES pages directly as a table (no persistent state):

-- Read entire stream as rows (follows pagination automatically)
SELECT * FROM inferal_relay_stream('https://relay.inferal.com/streams/acme/snapshots?limit=100');

-- API key is resolved from secrets manager (matched by URL prefix)
-- or passed explicitly:
SELECT * FROM inferal_relay_stream(
    'https://relay.inferal.com/streams/acme/snapshots?limit=100',
    api_key := 'sk-your-key'
);

-- Cap to N pages
SELECT * FROM inferal_relay_stream(
    'https://relay.inferal.com/streams/acme/snapshots?limit=100',
    max_pages := 5
);

-- Resume from a specific event ID
SELECT * FROM inferal_relay_stream(
    'https://relay.inferal.com/streams/acme/snapshots?limit=100',
    since := '018f0001-0000-7000-8000-000000000000'
);

-- Materialize into a table
CREATE TABLE my_data AS
SELECT * FROM inferal_relay_stream('https://relay.inferal.com/streams/acme/snapshots?limit=100');

Live table function

Combines sync and query into a single call. Fetches new pages from the remote, then returns all stored members:

-- Sync + query in one call
SELECT * FROM inferal_relay_live('my-stream');

-- With explicit page limit
SELECT * FROM inferal_relay_live('my-stream', 50);

-- Export to Parquet
COPY (SELECT * FROM inferal_relay_live('my-stream')) TO 'data.parquet';

Returns columns: event_id VARCHAR, member_id VARCHAR, generated_at TIMESTAMP, member_path VARCHAR, data JSON.

If sync fails due to a transient HTTP error, the function still returns existing members from the database rather than throwing an error. Stream-not-found and disabled-stream errors throw immediately since they indicate configuration problems.

Credentials

Uses DuckDB's built-in Secrets Manager:

-- Per-stream secret (exact match on stream name)
CREATE PERSISTENT SECRET (
    TYPE inferal_relay,
    API_KEY 'sk-acme-xxx',
    SCOPE 'acme'
);

-- Per-host secret (longest prefix match on URL)
-- Also used by inferal_relay_stream() automatically
CREATE PERSISTENT SECRET (
    TYPE inferal_relay,
    API_KEY 'sk-global-yyy',
    SCOPE 'https://relay.inferal.com'
);

-- Global default (no scope, fallback)
CREATE PERSISTENT SECRET (
    TYPE inferal_relay,
    API_KEY 'sk-default-zzz'
);

-- Session-only secret
CREATE SECRET (
    TYPE inferal_relay,
    API_KEY 'sk-temp',
    SCOPE 'test-stream'
);

-- List secrets (values hidden)
SELECT * FROM duckdb_secrets();

Resolve compact IRIs

SELECT inferal_relay_resolve_term('my-stream', 'relay:eventId');
-- → 'https://relay.inferal.com/ontology#eventId'

SELECT inferal_relay_resolve_term('my-stream', 'name');
-- → 'https://schema.org/name' (falls back to @vocab)

Functions

Scalar functions

FunctionDescription
inferal_relay_add_stream(name, base_url, stream_id, facet, page_limit)Add a stream + cursor
inferal_relay_remove_stream(name)Remove stream and all data
inferal_relay_reset_cursor(name)Reset cursor for re-sync
inferal_relay_build_next_url(name)Get next fetch URL
inferal_relay_resolve_term(stream, term)Expand compact IRI
inferal_relay_version()Extension version

Table functions

FunctionDescription
inferal_relay_sync(stream [, max_pages])Full sync loop with HTTP
inferal_relay_ingest_page(stream, body)Ingest a pre-fetched page
inferal_relay_status(stream)Stream + cursor status
inferal_relay_live(stream [, max_pages])Sync + query: fetches new pages, returns all members
inferal_relay_stream(url [, api_key, max_pages, since])Stateless LDES reader (follows pagination, resolves secrets by URL)

Tables

All tables are in the inferal_relay schema (created on first use):

TablePurpose
streamsStream endpoint configuration
cursorsPagination state (1:1 with streams)
membersStored LDES members with dedup
contextsDeduplicated JSON-LD contexts
context_termsParsed term-to-IRI mappings
sync_logAudit trail of sync operations

License

Apache License 2.0