Skip to main content

gobby_code/
schema.rs

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