fraiseql-db 2.16.0

Database abstraction layer for FraiseQL v2
Documentation
//! Database introspection trait for querying table metadata.

use fraiseql_error::Result;

use crate::DatabaseType;

/// Kind of database relation (table or view).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RelationKind {
    /// A base table.
    Table,
    /// A view.
    View,
}

/// Metadata about a database relation (table or view).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RelationInfo {
    /// Schema name (e.g. "public", "main", "dbo").
    pub schema: String,
    /// Relation name.
    pub name:   String,
    /// Whether the relation is a table or view.
    pub kind:   RelationKind,
}

/// Database introspection trait for querying table metadata.
#[allow(async_fn_in_trait)] // Reason: trait is used with concrete types only, not dyn Trait
pub trait DatabaseIntrospector: Send + Sync {
    /// List all fact tables in the database (tables starting with "tf_").
    ///
    /// Returns: Vec of table names matching the tf_* pattern
    async fn list_fact_tables(&self) -> Result<Vec<String>>;

    /// Query column information for a table.
    ///
    /// Returns: Vec of (column_name, data_type, is_nullable)
    async fn get_columns(&self, table_name: &str) -> Result<Vec<(String, String, bool)>>;

    /// Query indexes for a table.
    ///
    /// Returns: Vec of column names that have indexes
    async fn get_indexed_columns(&self, table_name: &str) -> Result<Vec<String>>;

    /// Get database type (for SQL type parsing).
    fn database_type(&self) -> DatabaseType;

    /// Get sample JSONB data from a column to extract dimension paths.
    ///
    /// Returns: Sample JSON value from the column, or None if no data exists.
    async fn get_sample_jsonb(
        &self,
        _table_name: &str,
        _column_name: &str,
    ) -> Result<Option<serde_json::Value>> {
        Ok(None)
    }

    /// List all relations (tables and views) in the database.
    ///
    /// Returns: Vec of relation metadata. Default implementation returns an empty list.
    async fn list_relations(&self) -> Result<Vec<RelationInfo>> {
        Ok(Vec::new())
    }

    /// Get sample JSON rows from a column for schema inference and the L3
    /// JSONB-key drift check (#384).
    ///
    /// Returns: Vec of parsed JSON values from the column.
    ///
    /// Deliberately has **no default implementation**: a default returning an
    /// empty Vec made "cannot sample" indistinguishable from "table is empty",
    /// which silently blinded the L3 drift check for every connector that
    /// forgot to override it (the `PostgresIntrospector` did, so the check had
    /// never sampled a row). Implementors that genuinely cannot sample must
    /// return an error, not fabricate an empty result.
    async fn get_sample_json_rows(
        &self,
        table_name: &str,
        column_name: &str,
        limit: usize,
    ) -> Result<Vec<serde_json::Value>>;

    /// Probe whether a callable SQL **function** named `name` exists — in `schema`
    /// when given, else resolved against the connection `search_path`.
    ///
    /// Used to validate that a mutation's `sql_source` (a *function*, not a
    /// relation) is backed. Resolved verbatim / case-sensitively, mirroring how the
    /// runtime and `pg_catalog::resolve_functions` match function names.
    ///
    /// `None` ⇒ this connector cannot probe functions (e.g. a non-Postgres
    /// dialect) and the caller should skip the function-existence check.
    ///
    /// # Errors
    ///
    /// Returns an error if the catalog query fails.
    async fn function_exists(&self, _schema: Option<&str>, _name: &str) -> Result<Option<bool>> {
        Ok(None)
    }

    /// Probe whether a **schema-qualified** relation exists, resolved
    /// case-sensitively / verbatim — the same way the runtime resolves a qualified
    /// `sql_source` (`quote_postgres_identifier` + `to_regclass`), so a mixed-case
    /// relation in an off-`search_path` schema is not falsely reported missing.
    ///
    /// `None` ⇒ this connector cannot probe qualified relations directly (e.g. a
    /// non-Postgres dialect); the caller falls back to the relation-map lookup.
    ///
    /// # Errors
    ///
    /// Returns an error if the catalog query fails.
    async fn qualified_relation_exists(&self, _schema: &str, _name: &str) -> Result<Option<bool>> {
        Ok(None)
    }

    /// The SQL body of `relation`, when it is a view (#1303).
    ///
    /// Read to answer one question the catalog cannot: does this view already
    /// order itself? A view carrying its own `ORDER BY` is the compiler's *own*
    /// documented remedy for `order_by = false`, and imposing a default page
    /// ordering on top of it replaces the author's order with another one — the
    /// single case in which #1303's fix is a regression. Seeing the body is what
    /// turns that from a silent break into a compile-time diagnostic.
    ///
    /// `None` ⇒ this connector cannot read view bodies, or `relation` is a table
    /// or does not exist. A caller must treat `None` as *unknown*, never as "does
    /// not order itself".
    ///
    /// # Errors
    ///
    /// Returns an error if the catalog query fails.
    async fn get_view_definition(&self, _relation: &str) -> Result<Option<String>> {
        Ok(None)
    }
}