Skip to main content

gobby_code/
schema.rs

1use anyhow::{Context as _, bail};
2use postgres::Client;
3
4use crate::setup::DEFAULT_SCHEMA;
5
6const REQUIRED_TABLES: &[&str] = &[
7    "code_indexed_projects",
8    "code_indexed_files",
9    "code_symbols",
10    "code_content_chunks",
11    "code_imports",
12    "code_calls",
13];
14
15const REQUIRED_BM25_INDEXES: &[&str] = &["code_symbols_search_bm25", "code_content_search_bm25"];
16
17const MIGRATION_HINT: &str = "Configure the Gobby PostgreSQL hub with the required code-index schema, `pg_search` extension, and BM25 indexes. For standalone databases, run `gcode setup --standalone --database-url <dsn>`.";
18
19/// Validate that the Gobby-owned PostgreSQL hub schema exists.
20///
21/// gcode does not create, alter, or drop hub tables. Schema ownership stays in
22/// the Gobby baseline and migration chain.
23pub fn validate_runtime_schema(client: &mut Client) -> anyhow::Result<()> {
24    if !extension_exists(client, "pg_search")? {
25        bail!("PostgreSQL hub is missing required extension `pg_search`. {MIGRATION_HINT}");
26    }
27
28    let missing_tables = missing_relations(client, REQUIRED_TABLES)?;
29    if !missing_tables.is_empty() {
30        bail!(
31            "PostgreSQL hub is missing required code-index tables: {}. {MIGRATION_HINT}",
32            missing_tables.join(", ")
33        );
34    }
35
36    let missing_indexes = missing_relations(client, REQUIRED_BM25_INDEXES)?;
37    if !missing_indexes.is_empty() {
38        bail!(
39            "PostgreSQL hub is missing required pg_search BM25 indexes: {}. {MIGRATION_HINT}",
40            missing_indexes.join(", ")
41        );
42    }
43
44    Ok(())
45}
46
47fn extension_exists(client: &mut Client, extension: &str) -> anyhow::Result<bool> {
48    client
49        .query_one(
50            "SELECT EXISTS(SELECT 1 FROM pg_extension WHERE extname = $1)",
51            &[&extension],
52        )
53        .with_context(|| format!("failed to check PostgreSQL extension `{extension}`"))?
54        .try_get(0)
55        .context("failed to decode PostgreSQL extension check")
56}
57
58fn missing_relations(client: &mut Client, relations: &[&str]) -> anyhow::Result<Vec<String>> {
59    let mut missing = Vec::new();
60    for relation in relations {
61        let qualified = format!("{DEFAULT_SCHEMA}.{relation}");
62        let row = client
63            .query_one("SELECT to_regclass($1) IS NOT NULL", &[&qualified])
64            .with_context(|| format!("failed to check PostgreSQL relation `{qualified}`"))?;
65        let exists: bool = row
66            .try_get(0)
67            .context("failed to decode PostgreSQL relation check")?;
68        if !exists {
69            missing.push((*relation).to_string());
70        }
71    }
72    Ok(missing)
73}
74
75#[cfg(test)]
76fn required_relation_regclass_name(relation: &str) -> String {
77    format!("{DEFAULT_SCHEMA}.{relation}")
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    #[test]
84    fn required_schema_contract_names_code_index_tables_and_bm25_indexes() {
85        assert!(REQUIRED_TABLES.contains(&"code_symbols"));
86        assert!(REQUIRED_TABLES.contains(&"code_content_chunks"));
87        assert!(REQUIRED_BM25_INDEXES.contains(&"code_symbols_search_bm25"));
88        assert!(REQUIRED_BM25_INDEXES.contains(&"code_content_search_bm25"));
89    }
90
91    #[test]
92    fn validates_runtime_schema_when_postgres_test_dsn_is_set() {
93        let Ok(database_url) = std::env::var("GCODE_POSTGRES_TEST_DATABASE_URL") else {
94            return;
95        };
96
97        let mut client = gobby_core::postgres::connect_readwrite(&database_url)
98            .expect("connect test PostgreSQL hub");
99        assert!(validate_runtime_schema(&mut client).is_ok());
100    }
101
102    #[test]
103    fn missing_schema_requires_setup() {
104        assert!(
105            MIGRATION_HINT.contains("gcode setup --standalone"),
106            "missing runtime schema guidance must point standalone users at explicit setup"
107        );
108    }
109
110    #[test]
111    fn relation_validation_qualifies_public_schema() {
112        assert_eq!(
113            required_relation_regclass_name("code_symbols"),
114            "public.code_symbols"
115        );
116    }
117}