Skip to main content

gobby_code/setup/
ddl.rs

1use super::contracts::NAMESPACE;
2use super::identifiers::qualified_relation;
3use gobby_core::setup::{
4    OwnedObject, SetupContext, SetupError, SetupReport, StandaloneSetup, StoreKind,
5};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct GcodeStandaloneSetup {
9    schema: String,
10}
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub(crate) struct PostgresObjectDefinition {
14    pub(crate) name: String,
15    pub(crate) sql: String,
16}
17
18impl GcodeStandaloneSetup {
19    pub fn new(schema: impl Into<String>) -> Self {
20        Self {
21            schema: schema.into(),
22        }
23    }
24
25    pub fn schema(&self) -> &str {
26        &self.schema
27    }
28
29    fn object_definition(&self, name: &str, sql: String) -> PostgresObjectDefinition {
30        PostgresObjectDefinition {
31            name: name.to_string(),
32            sql,
33        }
34    }
35
36    fn qualified(&self, relation: &str) -> Result<String, SetupError> {
37        qualified_relation(&self.schema, relation, "relation")
38    }
39
40    pub(crate) fn postgres_object_definitions(
41        &self,
42    ) -> Result<Vec<PostgresObjectDefinition>, SetupError> {
43        let code_indexed_projects = self.qualified("code_indexed_projects")?;
44        let code_indexed_files = self.qualified("code_indexed_files")?;
45        let code_symbols = self.qualified("code_symbols")?;
46        let code_content_chunks = self.qualified("code_content_chunks")?;
47        let code_imports = self.qualified("code_imports")?;
48        let code_calls = self.qualified("code_calls")?;
49
50        Ok(vec![
51            self.object_definition(
52                "pg_search extension",
53                "CREATE EXTENSION IF NOT EXISTS pg_search;".to_string(),
54            ),
55            self.object_definition(
56                "code_indexed_projects table",
57                format!(
58                    "CREATE TABLE IF NOT EXISTS {code_indexed_projects} (
59                        id TEXT PRIMARY KEY,
60                        root_path TEXT NOT NULL,
61                        total_files INTEGER NOT NULL DEFAULT 0,
62                        total_symbols INTEGER NOT NULL DEFAULT 0,
63                        last_indexed_at TIMESTAMPTZ,
64                        index_duration_ms INTEGER,
65                        created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
66                        updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
67                    );"
68                ),
69            ),
70            self.object_definition(
71                "code_indexed_files table",
72                format!(
73                    "CREATE TABLE IF NOT EXISTS {code_indexed_files} (
74                        id TEXT PRIMARY KEY,
75                        project_id TEXT NOT NULL,
76                        file_path TEXT NOT NULL,
77                        language TEXT NOT NULL,
78                        content_hash TEXT NOT NULL,
79                        symbol_count INTEGER NOT NULL DEFAULT 0,
80                        byte_size INTEGER NOT NULL DEFAULT 0,
81                        graph_synced BOOLEAN NOT NULL DEFAULT FALSE,
82                        vectors_synced BOOLEAN NOT NULL DEFAULT FALSE,
83                        graph_sync_attempted_at TIMESTAMPTZ,
84                        indexed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
85                        UNIQUE (project_id, file_path)
86                    );"
87                ),
88            ),
89            self.object_definition(
90                "idx_cif_project index",
91                format!(
92                    "CREATE INDEX IF NOT EXISTS idx_cif_project
93                     ON {code_indexed_files}(project_id);"
94                ),
95            ),
96            self.object_definition(
97                "idx_cif_graph_synced index",
98                format!(
99                    "CREATE INDEX IF NOT EXISTS idx_cif_graph_synced
100                     ON {code_indexed_files}(project_id, graph_synced);"
101                ),
102            ),
103            self.object_definition(
104                "idx_cif_vectors_synced index",
105                format!(
106                    "CREATE INDEX IF NOT EXISTS idx_cif_vectors_synced
107                     ON {code_indexed_files}(project_id, vectors_synced);"
108                ),
109            ),
110            self.object_definition(
111                "code_symbols table",
112                format!(
113                    "CREATE TABLE IF NOT EXISTS {code_symbols} (
114                        id TEXT PRIMARY KEY,
115                        project_id TEXT NOT NULL,
116                        file_path TEXT NOT NULL,
117                        name TEXT NOT NULL,
118                        qualified_name TEXT NOT NULL,
119                        kind TEXT NOT NULL,
120                        language TEXT NOT NULL,
121                        byte_start INTEGER NOT NULL,
122                        byte_end INTEGER NOT NULL,
123                        line_start INTEGER NOT NULL,
124                        line_end INTEGER NOT NULL,
125                        signature TEXT,
126                        docstring TEXT,
127                        parent_symbol_id TEXT,
128                        content_hash TEXT NOT NULL,
129                        summary TEXT,
130                        created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
131                        updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
132                    );"
133                ),
134            ),
135            self.object_definition(
136                "idx_cs_project index",
137                format!("CREATE INDEX IF NOT EXISTS idx_cs_project ON {code_symbols}(project_id);"),
138            ),
139            self.object_definition(
140                "idx_cs_file index",
141                format!(
142                    "CREATE INDEX IF NOT EXISTS idx_cs_file
143                     ON {code_symbols}(project_id, file_path);"
144                ),
145            ),
146            self.object_definition(
147                "idx_cs_name index",
148                format!("CREATE INDEX IF NOT EXISTS idx_cs_name ON {code_symbols}(name);"),
149            ),
150            self.object_definition(
151                "idx_cs_qualified index",
152                format!(
153                    "CREATE INDEX IF NOT EXISTS idx_cs_qualified
154                     ON {code_symbols}(qualified_name);"
155                ),
156            ),
157            self.object_definition(
158                "idx_cs_kind index",
159                format!("CREATE INDEX IF NOT EXISTS idx_cs_kind ON {code_symbols}(kind);"),
160            ),
161            self.object_definition(
162                "idx_cs_parent index",
163                format!(
164                    "CREATE INDEX IF NOT EXISTS idx_cs_parent
165                     ON {code_symbols}(parent_symbol_id);"
166                ),
167            ),
168            self.object_definition(
169                "code_content_chunks table",
170                format!(
171                    "CREATE TABLE IF NOT EXISTS {code_content_chunks} (
172                        id TEXT PRIMARY KEY,
173                        project_id TEXT NOT NULL,
174                        file_path TEXT NOT NULL,
175                        chunk_index INTEGER NOT NULL,
176                        line_start INTEGER NOT NULL,
177                        line_end INTEGER NOT NULL,
178                        content TEXT NOT NULL,
179                        language TEXT,
180                        created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
181                        UNIQUE (project_id, file_path, chunk_index)
182                    );"
183                ),
184            ),
185            self.object_definition(
186                "idx_ccc_project index",
187                format!(
188                    "CREATE INDEX IF NOT EXISTS idx_ccc_project
189                     ON {code_content_chunks}(project_id);"
190                ),
191            ),
192            self.object_definition(
193                "idx_ccc_file index",
194                format!(
195                    "CREATE INDEX IF NOT EXISTS idx_ccc_file
196                     ON {code_content_chunks}(project_id, file_path);"
197                ),
198            ),
199            self.object_definition(
200                "code_imports table",
201                format!(
202                    "CREATE TABLE IF NOT EXISTS {code_imports} (
203                        id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
204                        project_id TEXT NOT NULL,
205                        source_file TEXT NOT NULL,
206                        target_module TEXT NOT NULL,
207                        UNIQUE (project_id, source_file, target_module)
208                    );"
209                ),
210            ),
211            self.object_definition(
212                "idx_ci_file index",
213                format!(
214                    "CREATE INDEX IF NOT EXISTS idx_ci_file
215                     ON {code_imports}(project_id, source_file);"
216                ),
217            ),
218            self.object_definition(
219                "code_calls table",
220                format!(
221                    "CREATE TABLE IF NOT EXISTS {code_calls} (
222                        id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
223                        project_id TEXT NOT NULL,
224                        caller_symbol_id TEXT NOT NULL,
225                        callee_symbol_id TEXT NOT NULL DEFAULT '',
226                        callee_name TEXT NOT NULL,
227                        callee_target_kind TEXT NOT NULL DEFAULT 'unresolved',
228                        callee_external_module TEXT NOT NULL DEFAULT '',
229                        file_path TEXT NOT NULL,
230                        line INTEGER NOT NULL DEFAULT 0,
231                        UNIQUE (
232                            project_id, caller_symbol_id, callee_symbol_id, callee_name,
233                            callee_target_kind, callee_external_module, file_path, line
234                        )
235                    );"
236                ),
237            ),
238            self.object_definition(
239                "idx_cc_file index",
240                format!(
241                    "CREATE INDEX IF NOT EXISTS idx_cc_file
242                     ON {code_calls}(project_id, file_path);"
243                ),
244            ),
245            self.object_definition(
246                "idx_cc_caller index",
247                format!(
248                    "CREATE INDEX IF NOT EXISTS idx_cc_caller
249                     ON {code_calls}(project_id, caller_symbol_id);"
250                ),
251            ),
252            self.object_definition(
253                "idx_cc_target index",
254                format!(
255                    "CREATE INDEX IF NOT EXISTS idx_cc_target
256                     ON {code_calls}(project_id, callee_target_kind, callee_symbol_id, callee_name);"
257                ),
258            ),
259            self.object_definition(
260                "code_symbols_search_bm25 index",
261                format!(
262                    "CREATE INDEX IF NOT EXISTS code_symbols_search_bm25
263                     ON {code_symbols}
264                     USING bm25 (id, name, qualified_name, signature, docstring, summary)
265                     WITH (key_field = 'id');"
266                ),
267            ),
268            self.object_definition(
269                "code_content_search_bm25 index",
270                format!(
271                    "CREATE INDEX IF NOT EXISTS code_content_search_bm25
272                     ON {code_content_chunks}
273                     USING bm25 (id, content)
274                     WITH (key_field = 'id');"
275                ),
276            ),
277        ])
278    }
279}
280
281impl StandaloneSetup for GcodeStandaloneSetup {
282    fn namespace(&self) -> &str {
283        NAMESPACE
284    }
285
286    fn owned_objects(&self) -> Result<Vec<OwnedObject>, SetupError> {
287        Ok(self
288            .postgres_object_definitions()?
289            .into_iter()
290            .map(owned_object)
291            .collect())
292    }
293
294    fn create(&self, ctx: &mut SetupContext<'_>) -> Result<SetupReport, SetupError> {
295        let mut report = SetupReport::default();
296        let mut objects = self.owned_objects()?.into_iter();
297        while let Some(mut object) = objects.next() {
298            match (object.creator)(ctx) {
299                Ok(()) => report.created.push(object.name),
300                Err(err) => {
301                    report.failed.push((object.name, err.to_string()));
302                    report.skipped.extend(objects.map(|object| object.name));
303                    break;
304                }
305            }
306        }
307        Ok(report)
308    }
309}
310
311fn owned_object(definition: PostgresObjectDefinition) -> OwnedObject {
312    let object_name = definition.name;
313    let sql = definition.sql;
314    OwnedObject {
315        name: object_name.clone(),
316        store: StoreKind::Postgres,
317        creator: Box::new(move |ctx| execute_postgres_ddl(ctx, &object_name, &sql)),
318    }
319}
320
321fn execute_postgres_ddl(
322    ctx: &mut SetupContext<'_>,
323    object: &str,
324    sql: &str,
325) -> Result<(), SetupError> {
326    let Some(pg) = ctx.pg.as_deref_mut() else {
327        return Err(SetupError::ConnectionFailed {
328            store: "postgres".to_string(),
329            message: "PostgreSQL connection was not supplied to setup context".to_string(),
330        });
331    };
332
333    pg.batch_execute(sql)
334        .map_err(|err| SetupError::CreationFailed {
335            object: object.to_string(),
336            message: err.to_string(),
337        })
338}