Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SQL generation and execution

orchiddb::compiler is the database-free entry point for clients that own their SQL engine. It parses Cypher, Gremlin or mapped SPARQL, lowers through Graph IR and relational IR, and returns SQL plus output field names. It never opens a connection, registers functions, mutates a schema, or executes a SQL statement.

[dependencies]
orchiddb = { path = "../orchiddb" }

All language clients use the same compiler. The Java binding exposes it through JNI. Its JNI library uses this configuration: DuckDB and libduckdb-sys are absent from the native dependency graph. Existing managed engine/CLI builds retain their optional duckdb feature; it must now be enabled explicitly.

Versioned request

compile(CompileRequest) is the typed Rust API. compile_json(&str) provides the same API for language bindings. Both return errors before client execution. The JSON protocol is version 1; unknown fields and unsupported versions fail.

{
  "version": 1,
  "dialect": "duckdb",
  "language": "cypher",
  "query": "MATCH (p:Person) WHERE p.name=$name RETURN p.name AS name",
  "parameters": {"name": "Ada"},
  "tables": [{
    "name": "\"people\"",
    "columns": [
      {"name": "id", "data_type": "int64", "nullable": false},
      {"name": "name", "data_type": "string", "nullable": true}
    ]
  }],
  "nodes": [{
    "label": "Person", "table": "\"people\"", "id": "id",
    "properties": {"name": "name"}
  }]
}
let response = orchiddb::compiler::compile_json(request_json).await?;
// Response contains version, dialect, sql, fields, logical_plan,
// constraint_proofs, and layout_selections.
// The client decides whether, where and how to execute that SQL.

Table names use SQL identifier syntax, including quoted qualified identifiers. Bindings must quote individual identifier parts. Column/property names are literal strings. Schemas contain metadata only; no source data is passed into the compiler. Mapped identities may use a non-null scalar or an ordered tuple of non-null scalars: booleans, integers, floats, strings, binary, decimals, dates, times, timestamps, durations and intervals. Floating-point keys follow the target engine’s equality semantics. Each label retains its native component types. Heterogeneous scans and joins use typed struct variants to preserve types and component boundaries; label equality accompanies identity comparisons. Node id and edge id, source, and target accept either a column-name string or an ordered array of column names, for example "id": ["tenant", "id"]. Endpoint columns correspond positionally to the referenced node key. Arrays must be nonempty with distinct column names; a one-column array is a scalar key. Bindings must ensure ID uniqueness/non-nullability and referential integrity in their actual data. Mapped identities do not require a physical primary-key index; supported index types depend on the target database.

Optional edges describe label, table, id, source/target columns, source_label/target_label, and property-to-column mappings. An FK-backed edge also sets foreign_key to "src" or "dst" (the endpoint whose row owns the FK), uses the child table, and supplies the child key as its id. Partially NULL FKs do not produce graph edges. These declarations describe reads in this API; compile does not execute mutations. Gremlin tuple-ID projection may require the unified runtime’s native operator rather than standalone SQL compilation.

Optional computed_relationships define expression-based edges with predicates, score properties, per-source ranking, and candidate stages. source_metadata describes physical formats, options, and indexes for the owning engine to lower. Same-engine PostgreSQL search stays in a correlated SQL island; dependent search transfers include a template that clients bind with op: "bind_operation" before the ordinary bind operation. See the search reference for the protocol and generated SQL, or Search for an overview.

With the optional quickwit and elasticsearch features, engine descriptors may select these non-SQL adapters. Their transfers carry typed structured request operations. The execution_engine remains a SQL engine for graph joins and expressions around those operations. See Quickwit and Elasticsearch for configuration and execution; endpoints and credentials remain in caller-owned sessions, outside the compiler request.

Optional functions describe language-facing name, SQL target, parameter type list, return type (returns), and aggregate flag. The caller installs real implementations in its engine. SPARQL’s optional ontology maps classes, properties and directed relationships; see the public request structs for the complete contract.

Supported schema types are boolean, int8/int16/int32/int64, uint8/uint16/uint32/uint64, float32/float64, string, binary, json, date, time and duration (microseconds), interval (month/day/nanosecond), timestamp (microseconds, no timezone), and decimal:precision:scale with precision up to 38. Native list and struct schema declarations use list:<element-type> and struct:<JSON field-to-type object>, for example list:string or list:struct:{"sku":"string","quantity":"int64"}. These type strings recurse. The json type describes a document column, stored as PostgreSQL JSONB or DuckDB JSON; see JSON documents and relationships for functions and collection mappings. Collection sources can compose to expand nested lists or documents. Unsupported source types require a caller-owned cast/view. Unsupported SQL dialects fail explicitly; DuckDB and PostgreSQL rendering are currently implemented.

Permission filtering

The JSON protocol can filter mapped nodes against an application-owned flat grants relation. A top-level authorization identifies the current subject. Each node permission_scopes entry names the node source column to compare and a grants relation with its resource type and permission. The compiler emits the membership filter at the node scan, before graph traversal. Multiple scopes combine with OR, which supports direct object grants alongside project or tenant grants.

{
  "authorization": {"subject_type":"user", "subject_id":"alice"},
  "nodes": [{
    "label":"Document", "table":"documents", "id":"id",
    "properties":{"title":"title", "project_id":"project_id"},
    "permission_scopes":[
      {"resource_column":"id", "relation":{"table":"effective_grants", "resource_type":"document", "permission":"view"}},
      {"resource_column":"project_id", "relation":{"table":"effective_grants", "resource_type":"project", "permission":"view"}}
    ]
  }]
}

Include the grants source and all its columns in tables. Flat relation column names default to resource_type, resource_rel, resource_id, subject_type, subject_rel, and subject_id; callers can override every column name. Scope columns must exist on their mapped node. Requests with protected nodes and no principal fail closed. The relation must already contain effective grants for the principal, including any group or nested-set resolution. The compiler and clients do not connect to or synchronize a permission system.

Collection-backed logical tables

The optional collection_sources array defines named relations by expanding a native list column. Each definition supplies name, parent table, list column, optional parent_columns (output alias to parent column), and either element (a scalar element’s output name) or fields (output alias to struct field). Node, edge, and RDF mappings reference the resulting relation by name.

{
  "collection_sources": [{
    "name": "person_tags",
    "table": "people",
    "column": "tags",
    "parent_columns": {"person_id": "id"},
    "element": "tag"
  }]
}

This is a request fragment; declare people.id and people.tags in tables, using list:string for the tags column. No database view is created or required. For a full struct-list mapping, identity rules, and limitations, see collection columns as logical tables.

The compiler registers physical schemas, then optional logical_sources (physical-layout alternatives), then collection and representation sources in dependency order. Parent filters can guide layout selection below UNNEST; element filters apply after expansion. logical_plan exposes the expansion and layout_selections reports physical choices and parent-scan byte/file estimates. Those estimates do not describe expanded row counts. Include collection and layout definitions and their metadata in plan cache keys. These definitions do not enable writes.

Equivalent derived and materialized inputs

The optional representation_sources array registers multiple complete, equivalent inputs for one mapped relation. Inputs can be named tables/collections or SQL queries, including joins and grouped summaries. The optimizer chooses per occurrence after pushing filters, and exposes its decisions in representation_selections, alongside layout_selections and logical_plan. The shared engine uses the same selection and exposes it in execution statistics.

Boundaries

The API specializes SQL to typed Cypher parameter values. It does not return JDBC placeholders. Bindings must treat generated SQL as potentially sensitive and include parameters, schema, ontology, function declarations, mappings, dialect, engine routing, authorization principal, and permission scopes in their plan cache keys. Gremlin/SPARQL parameter bindings are not yet implemented. Conformance evidence applies to its recorded API and revision; SQL emission must also fit the target dialect.

Writes, opaque extensions, and unlowerable operations fail. SPARQL SERVICE, including SERVICE SILENT, is unsupported in both compiler and managed runtime; OrchidDB does not make remote SPARQL HTTP requests. Internal table materializations are rejected by inspecting the logical plan before unparsing; they are never collected or executed. Constant seed rows in mapped plans are emitted as SQL expressions instead of private tables.

Regression tests: cargo test --test sql_compiler --test execution --test federation.

Cross-engine reads

Current source builds accept an engines registry, an execution_engine, and an engine on each table. Engine IDs name caller-owned connections; supported dialects are duckdb and postgres. Tables without an engine use the execution engine, whose dialect must match the request.

The compiler pushes eligible source subtrees into SQL islands, including filters, projections, same-engine joins, and aggregates. Cross-engine joins execute on the selected execution engine. CompiledSql.transfers describes each source query and its typed result columns.

Use a federation helper, such as Rust’s federation::execute or Java’s FederatedQuery.query, to execute these plans. It buffers source results and binds them as typed common table expressions in the final query. It creates no tables or views. Large transfers increase memory use and SQL statement size. Connections keep their own snapshots and transaction policy; this read interface provides no distributed writes or snapshot.

See the mixed-engine request and client APIs for a complete configuration.

Caller-owned data flow

For a plan without transfers, you can execute CompiledSql.sql directly. For a common adapter boundary, implement execution::SqlSession: declare a dialect, a driver error type, and a result type implementing Arrow RecordBatchReader that can borrow the session. Its async query method executes the SQL; execution::execute first rejects protocol/dialect mismatches. It performs no schema discovery, setup statements, data copying, buffering or commits. Futures may stay on the calling thread; Send and background scheduling are not required. Your adapter controls streaming errors, cancellation and drop cleanup.

The DuckDB application example declares its own driver dependency and implements the interface with a borrowed connection and native Arrow reader. Retained Rust batches own their buffers after advancing or dropping the reader. It registers a SQL function, queries uncommitted caller data, and verifies caller rollback afterward:

cargo run --manifest-path examples/duckdb-client/Cargo.toml

For SQL output alone, with no DuckDB build:

cargo run --example compile_sql

Resolve metadata and compile against the same session/schema version used for execution. The dialect check cannot distinguish two databases using the same SQL dialect; engine identity and routing belong to your application.

RDF rules in compiler requests

SPARQL requests accept rdf rules and an optional dataset (default default). The same tables, nodes, and edges catalog can serve all three languages. For example, a rule over registered customer columns is:

{"table":"customers",
 "subject":{"kind":"template","prefix":"urn:customer:","columns":["tenant","id"]},
 "predicate":{"kind":"constant","value":"https://example.com/name"},
 "object":{"kind":"literal","column":"name"}}

Term kinds are iri (column), template (prefix and ordered columns), blank (scope and ordered columns), literal (column with optional datatype, language, or language_column), and constant (value with optional datatype or language). Rules may specify graph and dataset. Runtime writes additionally require writable: true and a complete key array. The compiler remains read-only. Existing ontology declarations translate into the same relational rules.

Requests using rdf retain typed RDF output: each variable’s lexical column is accompanied by __rdf:term:kind:?variable, __rdf:term:datatype:?variable, and __rdf:term:language:?variable. A null kind means unbound. fields lists the visible variable names. Legacy ontology requests retain their scalar result columns and native numeric types.