Skip to main content

Engine

Struct Engine 

Source
pub struct Engine { /* private fields */ }
Expand description
  • The connection is bound to the ephemeral primary at Self::ephemeral_path. Unqualified SQL routes here.
  • When Self::persistent_path is Some, the server attaches that file as "persistent" after engine construction. When None, no persistent storage is available this session (--ephemeral-only).

Implementations§

Source§

impl Engine

Source

pub const PERSISTENT_ALIAS: &'static str = "persistent"

Reserved alias under which the persistent database is attached when Self::persistent_path is set. Visible to the LLM via the database parameter and via list_attached_databases.

Source

pub fn new(persistent_db_path: Option<String>) -> Result<Self, McpError>

Create a new Engine. The connection is bound to a fresh ephemeral primary in a temp directory. If persistent_db_path is Some, the path is recorded so the server can ATTACH it post-construction; passing None means --ephemeral-only.

Connects to the shared daemon if available, falling back to a local hyperd.

§Errors
Source

pub fn new_no_daemon( persistent_db_path: Option<String>, ) -> Result<Self, McpError>

Create an engine that bypasses the shared daemon and spawns a private hyperd.

§Errors

Same as Self::new.

Source

pub fn is_running(&self) -> bool

Whether the backing hyperd is currently reachable.

In local mode, delegates to the owned HyperProcess. In daemon mode, probes the cached libpq daemon_endpoint directly with a short-timeout TCP connect — the same endpoint queries run against. This reflects current liveness of the resource the engine actually depends on, and is robust to two failure modes the health-port PING is not:

  • the health port being unreachable (stale daemon.json, port-scan-adopted daemon, firewall) while the libpq endpoint serves;
  • the daemon restarting hyperd on a new port, leaving the cached endpoint stale (the probe then correctly reports false).

Falls back to discovery (daemon.json + health-port PING) only when no endpoint has been cached yet (before the first connection attempt).

Source

pub fn hyperd_endpoint(&self) -> Result<String, McpError>

host:port endpoint of the hyperd process. Used by the watcher to build additional async connections via hyperdb_api::pool without touching the primary sync connection this engine holds.

§Errors

Returns ErrorCode::InternalError if the endpoint is unavailable.

Source

pub fn daemon_health_port(&self) -> Option<u16>

The daemon’s health port, if this engine is connected via daemon mode. Returns None in local mode (when this engine owns a private HyperProcess).

Source

pub fn ephemeral_path(&self) -> &Path

Absolute path to the ephemeral primary .hyper file on disk.

Source

pub fn persistent_path(&self) -> Option<&Path>

Absolute path to the persistent .hyper file, or None when the session is --ephemeral-only.

Source

pub fn primary_db_name(&self) -> String

Unqualified database name Hyper uses for the ephemeral primary — the stem of Self::ephemeral_path. Matches what hyperdb_api::Connection::new registers when it issues its implicit ATTACH DATABASE, so fully-qualified SQL built with this value resolves to the primary.

Also the correct value for SET schema_search_path = '…' while additional databases are attached: Hyper’s default search path ("$single") only covers the implicit primary when no other databases are attached, and starts resolving unqualified names to nothing the moment an ATTACH DATABASE runs.

Source

pub fn resolve_target_db( &self, requested: Option<&str>, ) -> Result<String, McpError>

Resolve a tool’s optional database parameter to a concrete alias suitable for fully-qualifying SQL. None and Some("") mean “the primary (ephemeral)”; Some("persistent") requires the persistent attachment exists; any other value is returned verbatim and assumed to be a user-attached alias.

Returns the database alias to qualify against, or None to mean “use the primary’s name”. This lets callers build qualified SQL uniformly: format!("\"{}\".\"public\".\"{}\"", alias_or_primary, table).

§Errors

Returns ErrorCode::InvalidArgument when Some("persistent") is passed but Self::persistent_path is None (--ephemeral-only mode).

Source

pub fn scoped_search_path( &self, alias: &str, ) -> Result<ScopedSearchPath<'_>, McpError>

Temporarily redirect the schema search path to alias for the duration of a tool call. Returns an RAII guard that restores the search path to the primary when dropped.

The engine Mutex is held by the caller (with_engine closure), so concurrent tool calls cannot observe the redirected path.

§Errors

Returns McpError if the SET statement fails (e.g. invalid alias or connection lost).

Source

pub fn log_dir(&self) -> &Path

Directory where hyperd writes its log files. The MCP binary should also drop its own client-side log here so debugging starts in one place.

Source

pub fn hyperd_log_path(&self) -> Option<PathBuf>

Best-guess path to the most recent hyperd log file, useful when something in the engine misbehaves and we want to surface the server log to the caller. Picks the newest hyperd*.log file in log_dir. Returns None if no matching file exists yet.

Source

pub fn has_persistent(&self) -> bool

true if a persistent database is attached to this session. Equivalent to Self::persistent_path being Some.

Source

pub fn persistent_was_just_created(&self) -> bool

true when this engine just created the persistent .hyper file during construction. The server consumes this signal once to decide whether to seed _table_catalog; subsequent reads stay true (the flag isn’t reset — it’s a fact about the engine’s startup, not a one-shot signal).

Source

pub fn catalog_present_in<F>( &self, alias: &str, prober: F, ) -> Result<bool, McpError>
where F: Fn(&Engine) -> Result<bool, McpError>,

Returns whether _table_catalog exists in alias, caching the per-DB result on first call so subsequent catalog read/ write paths skip the pg_catalog.pg_tables probe.

prober is the SQL-side existence check; the cache layer here is intentionally generic so the catalog module can keep its probe SQL in one place.

§Errors

Propagates whatever error prober returns on the first call. On subsequent calls, the cached value is returned without re-running the probe.

Source

pub fn mark_catalog_present_for(&self, alias: &str)

Synchronously set the catalog-presence cache to true for alias — used by table_catalog::ensure_exists_in after a successful CREATE TABLE IF NOT EXISTS so subsequent reads/ writes against that DB skip the existence probe.

Source

pub fn clear_catalog_cache_for(&self, alias: &str)

Drop the cached probe result for alias. Called by detach_database so that re-attaching the same alias to a different file (or with different writability) doesn’t reuse a stale entry.

Source

pub fn connection(&self) -> &Connection

Direct access to the underlying connection for operations not wrapped by Engine (e.g. export_csv, execute_query_to_arrow).

Source

pub fn execute_command(&self, sql: &str) -> Result<u64, McpError>

Execute a DDL/DML command. Returns affected row count.

§Errors

Converts any hyperdb_api::Error from the underlying connection into an McpError — typical causes are SQL syntax errors, constraint violations, permission failures, or ErrorCode::ConnectionLost when the link to hyperd has dropped.

Source

pub fn execute_in_transaction<F, T>(&self, f: F) -> Result<T, McpError>
where F: FnOnce(&Engine) -> Result<T, McpError>,

Run the given closure inside a database transaction.

Issues BEGIN TRANSACTION before calling f. If f returns Ok, commits the transaction; if it returns Err, rolls back and returns the original error. A failed rollback is logged via tracing::warn! and the original error is still surfaced (rollback failure usually means the transaction was already aborted by the server, which is functionally equivalent to a successful rollback).

This is the correctness primitive for ingest operations: it lets per-row INSERT loops (Parquet, Arrow, JSON) leave zero partial data on failure. The CSV COPY FROM path is already atomic at the statement level, but wrapping it in a transaction costs nothing and makes per-row INSERT loops atomic across the whole batch.

§DDL is auto-committed

Hyper treats DROP TABLE and CREATE TABLE as auto-committed even when issued inside a transaction. This means replace-mode ingest cannot roll back the original table once DDL has run. The guarantee is weaker than it looks: on failure, the new (empty) table stays in place rather than being replaced by partial data. Append-mode ingest is fully atomic because it doesn’t issue DDL on existing tables.

§Known wire protocol quirk

After a mid-transaction Hyper-level error (e.g. a NOT NULL violation on INSERT), the first SELECT after rollback may return an empty result set due to residual bytes on the connection. Retrying the query once restores normal behavior. The rollback itself is always correct — this is a read-side symptom only. See the query_resilient helper in tests/transaction_tests.rs for a robust pattern.

§Errors
  • Returns any McpError raised by BEGIN TRANSACTION or by COMMIT (typical causes: connection loss, serialization conflict, DDL auto-commit contention).
  • Returns whatever error f produces (rollback is performed first; a rollback failure is only logged, never surfaced).
§Panics

Does not introduce new panic sites. If f panics, the transaction is rolled back (best-effort) and the original panic is re-raised via std::panic::resume_unwind, preserving the panic payload.

Source

pub fn execute_query_to_json(&self, sql: &str) -> Result<Vec<Value>, McpError>

Execute a SELECT query and materialize all result rows as a JSON array of {column_name: value} objects.

Results are consumed chunk-by-chunk to avoid holding the entire result set in protocol buffers, though the final Vec<Value> does accumulate in memory. For truly huge results, prefer export to a file instead.

§Errors

Returns any McpError produced by Connection::execute_query or subsequent next_chunk calls — SQL errors, connection loss, and decoding failures all surface through this path.

Source

pub fn create_table( &self, table_name: &str, columns: &[ColumnSchema], replace: bool, ) -> Result<(), McpError>

Create a table from a schema definition.

  • replace = true: drops the existing table (if any) and recreates it. Old rows are lost. Schema is defined by columns.
  • replace = false (append mode): creates the table only if it doesn’t already exist. If it does exist, the schema defined here is ignored and subsequent inserts must match the existing schema.

Uses CREATE TABLE IF NOT EXISTS / DROP TABLE IF EXISTS so the operation is idempotent without needing a separate has_table probe. This is important for the watcher path, where a racy has_table check (false negative due to protocol desync) would otherwise attempt a bare CREATE TABLE that fails with “42P07 table already exists” and leaves the connection in an aborted state.

§Errors
Source

pub fn create_table_in( &self, table_name: &str, columns: &[ColumnSchema], replace: bool, target_db: Option<&str>, ) -> Result<(), McpError>

Create a table, optionally in a non-primary database. When target_db is Some, the table identifier is fully qualified as "db"."public"."table"; when None, it’s just "table".

§Errors

Same as Self::create_table.

Source

pub fn column_metadata( &self, table: &str, ) -> Result<Vec<ColumnSchema>, McpError>

Returns (name, hyper_type, nullable) for every column of table, in declaration order, by reading the catalog (the same path describe_table uses). Used by the merge ingest path to compare incoming-file schema against the existing table.

§Errors
  • Propagates Catalog::get_table_definition errors. Callers that need a “table missing” sentinel should pre-check via Catalog::get_table_names("public") (see describe_table for the precedent) — get_table_definition errors with a variable wording across Hyper versions.
Source

pub fn column_metadata_in( &self, target_db: Option<&str>, table: &str, ) -> Result<Vec<ColumnSchema>, McpError>

Like Self::column_metadata but for a table in target_db. None falls back to column_metadata (primary). Some(alias) reads via the qualified pg_catalog.pg_attribute join used by describe_columns_via_pg_catalog — the connection-bound Catalog API can’t see attached databases.

§Errors

Returns ErrorCode::TableNotFound when no rows come back from the qualified probe. Propagates connection errors.

Source

pub fn table_exists(&self, table: &str) -> Result<bool, McpError>

Returns true if table exists in the public schema. Avoids the per-version error-string ambiguity of Catalog::get_table_definition by listing names instead.

§Errors

Propagates errors from Catalog::get_table_names (typically connection loss).

Source

pub fn table_exists_in( &self, target_db: Option<&str>, table: &str, ) -> Result<bool, McpError>

Like Self::table_exists but for a table in target_db. None falls back to table_exists (primary). Some(alias) probes the qualified pg_catalog.pg_tables of the attached database — the connection-bound Catalog API can’t see attached databases.

§Errors

Propagates connection errors from the probe query.

Source

pub fn alter_table_add_columns( &self, table: &str, cols: &[ColumnSchema], ) -> Result<(), McpError>

Issue a single ALTER TABLE "<table>" ADD COLUMN "<n1>" <t1>, ADD COLUMN "<n2>" <t2>, … statement that adds all columns atomically. Hyper supports the multi-column form (verified 2026-05-07 against the pinned hyperd release), so partial-add failures don’t leave the schema half-widened.

New columns are always added nullable — existing rows have no value to satisfy NOT NULL. nullable on the input is ignored for that reason.

cols must be non-empty; an empty input is a no-op (returns Ok(()) without issuing SQL) so callers can pass the “columns missing from target” set directly without a length pre-check.

§Errors
  • Returns ErrorCode::SchemaMismatch if any element’s hyper_type is not a known Hyper type (same validation as create_table).
  • Propagates the underlying SQL error from the single ALTER statement. Because Hyper executes a multi-column ADD atomically, a failure leaves the table schema unchanged — no partial widening.
Source

pub fn alter_table_add_columns_in( &self, target_db: Option<&str>, table: &str, cols: &[ColumnSchema], ) -> Result<(), McpError>

Like Self::alter_table_add_columns but for a table in target_db. None keeps the unqualified identifier; Some(alias) emits "db"."public"."table" so the ALTER lands in the attached database.

§Errors

Same as Self::alter_table_add_columns.

Source

pub fn describe_tables(&self) -> Result<Vec<Value>, McpError>

List all tables in the public schema with their column definitions and row counts. Returned as a JSON-serializable Vec for direct use in MCP tool responses.

§Errors
  • Propagates any error from Catalog::get_table_names (typically connection loss or SQL errors from the underlying catalog probe).
  • Propagates any error from describe_table_with_catalog for individual tables — a single failing describe aborts the whole listing.
Source

pub fn describe_table(&self, table_name: &str) -> Result<Value, McpError>

Describe a single table by name. Returns the same JSON shape as an element of Self::describe_tables (name, columns, row_count).

Errors with ErrorCode::TableNotFound when the table doesn’t exist or is an internal _hyperdb_* bookkeeping table (callers should not be able to probe infrastructure via this path; it stays consistent with the full-list variant that hides them).

Uses get_table_names("public") as the authoritative existence check rather than pattern-matching the error string from get_table_definition, because the latter’s wording varies across Hyper versions and can slip past translate_table_missing.

§Errors
  • Returns ErrorCode::TableNotFound if table_name is an internal _hyperdb_* table or does not appear in public.
  • Propagates any error from Catalog::get_table_names or from describe_table_with_catalog (connection loss, catalog probe failures).
Source

pub fn sample_table(&self, table_name: &str, n: u64) -> Result<Value, McpError>

Sample rows from a table along with its schema and total row count.

Returns a single JSON object with table, row_count, sample_size, schema, and rows. n is clamped to the range 1..=100. Returns ErrorCode::TableNotFound if the table doesn’t exist.

Avoids the Catalog::has_table probe entirely — we just run the sample SELECT first and translate a Hyper “table does not exist” error into our own ErrorCode::TableNotFound. This sidesteps the old pattern where a racy has_table silently returning Err would be rewritten to false and surface as a spurious TableNotFound for tables that actually exist.

§Errors
  • Returns ErrorCode::TableNotFound (via translate_table_missing) if the sample SELECT surfaces a Hyper “table does not exist” error.
  • Propagates any other McpError from the sample query — SQL errors, permission failures, or connection loss.
  • The subsequent COUNT(*) and get_table_definition calls are best-effort: their errors are swallowed so the sample payload is still returned when available.
Source

pub fn sample_table_in( &self, target_db: Option<&str>, table_name: &str, n: u64, ) -> Result<Value, McpError>

Sample rows from a table in target_db (or the primary when None).

§Errors

Same as Self::sample_table.

Source

pub fn describe_tables_in( &self, target_db: Option<&str>, ) -> Result<Vec<Value>, McpError>

List public tables in target_db (or the primary when None).

§Errors

Returns McpError on catalog query failure.

Source

pub fn describe_table_in( &self, target_db: Option<&str>, table_name: &str, ) -> Result<Value, McpError>

Describe a single table in target_db (or the primary when None).

§Errors

Same as Self::describe_table.

Source

pub fn status(&self) -> Result<Value, McpError>

Collect workspace health and size metrics for the status MCP tool.

Includes logs with paths to the hyperd log file (if one exists yet) and the MCP client log. These are the first files to check when something misbehaves.

§Errors

Propagates any error from Catalog::get_table_names. Per-table row counts and disk usage fall back to 0 on read failure, so these do not bubble up.

Trait Implementations§

Source§

impl Debug for Engine

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Drop for Engine

Source§

fn drop(&mut self)

Executes the destructor for this type. Read more
Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more

Auto Trait Implementations§

§

impl !Freeze for Engine

§

impl !RefUnwindSafe for Engine

§

impl !UnwindSafe for Engine

§

impl Send for Engine

§

impl Sync for Engine

§

impl Unpin for Engine

§

impl UnsafeUnpin for Engine

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoRequest<T> for T

Source§

fn into_request(self) -> Request<T>

Wrap the input message T in a tonic::Request
Source§

impl<L> LayerExt<L> for L

Source§

fn named_layer<S>(&self, service: S) -> Layered<<L as Layer<S>>::Service, S>
where L: Layer<S>,

Applies the layer to a service and wraps it in Layered.
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more