fraiseql_db/introspector.rs
1//! Database introspection trait for querying table metadata.
2
3use fraiseql_error::Result;
4
5use crate::DatabaseType;
6
7/// Kind of database relation (table or view).
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum RelationKind {
10 /// A base table.
11 Table,
12 /// A view.
13 View,
14}
15
16/// Metadata about a database relation (table or view).
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct RelationInfo {
19 /// Schema name (e.g. "public", "main", "dbo").
20 pub schema: String,
21 /// Relation name.
22 pub name: String,
23 /// Whether the relation is a table or view.
24 pub kind: RelationKind,
25}
26
27/// Database introspection trait for querying table metadata.
28#[allow(async_fn_in_trait)] // Reason: trait is used with concrete types only, not dyn Trait
29pub trait DatabaseIntrospector: Send + Sync {
30 /// List all fact tables in the database (tables starting with "tf_").
31 ///
32 /// Returns: Vec of table names matching the tf_* pattern
33 async fn list_fact_tables(&self) -> Result<Vec<String>>;
34
35 /// Query column information for a table.
36 ///
37 /// Returns: Vec of (column_name, data_type, is_nullable)
38 async fn get_columns(&self, table_name: &str) -> Result<Vec<(String, String, bool)>>;
39
40 /// Query indexes for a table.
41 ///
42 /// Returns: Vec of column names that have indexes
43 async fn get_indexed_columns(&self, table_name: &str) -> Result<Vec<String>>;
44
45 /// Get database type (for SQL type parsing).
46 fn database_type(&self) -> DatabaseType;
47
48 /// Get sample JSONB data from a column to extract dimension paths.
49 ///
50 /// Returns: Sample JSON value from the column, or None if no data exists.
51 async fn get_sample_jsonb(
52 &self,
53 _table_name: &str,
54 _column_name: &str,
55 ) -> Result<Option<serde_json::Value>> {
56 Ok(None)
57 }
58
59 /// List all relations (tables and views) in the database.
60 ///
61 /// Returns: Vec of relation metadata. Default implementation returns an empty list.
62 async fn list_relations(&self) -> Result<Vec<RelationInfo>> {
63 Ok(Vec::new())
64 }
65
66 /// Get sample JSON rows from a column for schema inference and the L3
67 /// JSONB-key drift check (#384).
68 ///
69 /// Returns: Vec of parsed JSON values from the column.
70 ///
71 /// Deliberately has **no default implementation**: a default returning an
72 /// empty Vec made "cannot sample" indistinguishable from "table is empty",
73 /// which silently blinded the L3 drift check for every connector that
74 /// forgot to override it (the `PostgresIntrospector` did, so the check had
75 /// never sampled a row). Implementors that genuinely cannot sample must
76 /// return an error, not fabricate an empty result.
77 async fn get_sample_json_rows(
78 &self,
79 table_name: &str,
80 column_name: &str,
81 limit: usize,
82 ) -> Result<Vec<serde_json::Value>>;
83
84 /// Probe whether a callable SQL **function** named `name` exists — in `schema`
85 /// when given, else resolved against the connection `search_path`.
86 ///
87 /// Used to validate that a mutation's `sql_source` (a *function*, not a
88 /// relation) is backed. Resolved verbatim / case-sensitively, mirroring how the
89 /// runtime and `pg_catalog::resolve_functions` match function names.
90 ///
91 /// `None` ⇒ this connector cannot probe functions (e.g. a non-Postgres
92 /// dialect) and the caller should skip the function-existence check.
93 ///
94 /// # Errors
95 ///
96 /// Returns an error if the catalog query fails.
97 async fn function_exists(&self, _schema: Option<&str>, _name: &str) -> Result<Option<bool>> {
98 Ok(None)
99 }
100
101 /// Probe whether a **schema-qualified** relation exists, resolved
102 /// case-sensitively / verbatim — the same way the runtime resolves a qualified
103 /// `sql_source` (`quote_postgres_identifier` + `to_regclass`), so a mixed-case
104 /// relation in an off-`search_path` schema is not falsely reported missing.
105 ///
106 /// `None` ⇒ this connector cannot probe qualified relations directly (e.g. a
107 /// non-Postgres dialect); the caller falls back to the relation-map lookup.
108 ///
109 /// # Errors
110 ///
111 /// Returns an error if the catalog query fails.
112 async fn qualified_relation_exists(&self, _schema: &str, _name: &str) -> Result<Option<bool>> {
113 Ok(None)
114 }
115
116 /// The SQL body of `relation`, when it is a view (#1303).
117 ///
118 /// Read to answer one question the catalog cannot: does this view already
119 /// order itself? A view carrying its own `ORDER BY` is the compiler's *own*
120 /// documented remedy for `order_by = false`, and imposing a default page
121 /// ordering on top of it replaces the author's order with another one — the
122 /// single case in which #1303's fix is a regression. Seeing the body is what
123 /// turns that from a silent break into a compile-time diagnostic.
124 ///
125 /// `None` ⇒ this connector cannot read view bodies, or `relation` is a table
126 /// or does not exist. A caller must treat `None` as *unknown*, never as "does
127 /// not order itself".
128 ///
129 /// # Errors
130 ///
131 /// Returns an error if the catalog query fails.
132 async fn get_view_definition(&self, _relation: &str) -> Result<Option<String>> {
133 Ok(None)
134 }
135}