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}