1use arrow::datatypes::{DataType, Field, Schema, TimeUnit};
17use std::sync::Arc;
18
19pub const CODE_EMBEDDING_DIM: i32 = 768;
21
22pub mod node_col {
26 pub const ID: usize = 0;
27 pub const KIND: usize = 1;
28 pub const PARENT_ID: usize = 2;
29 pub const NAME: usize = 3;
30 pub const SIGNATURE: usize = 4;
31 pub const DOCSTRING: usize = 5;
32 pub const BODY_HASH: usize = 6;
33 pub const BODY: usize = 7;
34 pub const EMBEDDING: usize = 8;
35 pub const LOC: usize = 9;
36 pub const CYCLOMATIC_COMPLEXITY: usize = 10;
37 pub const COVERAGE_PCT: usize = 11;
38 pub const LAST_MODIFIED: usize = 12;
39 pub const START_LINE: usize = 13;
41 pub const END_LINE: usize = 14;
42 pub const START_COL: usize = 15;
43 pub const END_COL: usize = 16;
44 pub const FILE_PATH: usize = 17;
45 pub const BYTE_OFFSET: usize = 18;
46}
47
48pub mod edge_col {
52 pub const SOURCE_ID: usize = 0;
53 pub const TARGET_ID: usize = 1;
54 pub const PREDICATE: usize = 2;
55 pub const WEIGHT: usize = 3;
56 pub const COMMIT_ID: usize = 4;
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
82pub enum CodeNodeKind {
83 File,
85 Module,
86 Class,
87 Function,
88 Method,
89 Parameter,
90 Variable,
91 Test,
92 Type,
95 Constant,
97 Import,
99 RustFn,
102 RustMethod,
104 RustImpl,
106 RustTrait,
108 RustStruct,
110 RustEnum,
112 RustMod,
114 RustMacro,
116 RustUse,
118 RustConst,
120 RustStatic,
122 RustTypeAlias,
124 RustAttribute,
126 RustLifetime,
128 RustTest,
130 PythonFunction,
133 PythonMethod,
135 PythonClass,
137 PythonDecorator,
139 PythonImport,
141 PythonModule,
143 PythonLambda,
145 PythonAsync,
147 PythonProperty,
149}
150
151impl CodeNodeKind {
152 pub const ALL: [CodeNodeKind; 35] = [
155 Self::File,
157 Self::Module,
158 Self::Class,
159 Self::Function,
160 Self::Method,
161 Self::Parameter,
162 Self::Variable,
163 Self::Test,
164 Self::Type,
166 Self::Constant,
167 Self::Import,
168 Self::RustFn,
170 Self::RustMethod,
171 Self::RustImpl,
172 Self::RustTrait,
173 Self::RustStruct,
174 Self::RustEnum,
175 Self::RustMod,
176 Self::RustMacro,
177 Self::RustUse,
178 Self::RustConst,
179 Self::RustStatic,
180 Self::RustTypeAlias,
181 Self::RustAttribute,
182 Self::RustLifetime,
183 Self::RustTest,
184 Self::PythonFunction,
186 Self::PythonMethod,
187 Self::PythonClass,
188 Self::PythonDecorator,
189 Self::PythonImport,
190 Self::PythonModule,
191 Self::PythonLambda,
192 Self::PythonAsync,
193 Self::PythonProperty,
194 ];
195
196 pub fn as_str(self) -> &'static str {
197 match self {
198 Self::File => "file",
199 Self::Module => "module",
200 Self::Class => "class",
201 Self::Function => "function",
202 Self::Method => "method",
203 Self::Parameter => "parameter",
204 Self::Variable => "variable",
205 Self::Test => "test",
206 Self::Type => "type",
207 Self::Constant => "constant",
208 Self::Import => "import",
209 Self::RustFn => "rust_fn",
210 Self::RustMethod => "rust_method",
211 Self::RustImpl => "rust_impl",
212 Self::RustTrait => "rust_trait",
213 Self::RustStruct => "rust_struct",
214 Self::RustEnum => "rust_enum",
215 Self::RustMod => "rust_mod",
216 Self::RustMacro => "rust_macro",
217 Self::RustUse => "rust_use",
218 Self::RustConst => "rust_const",
219 Self::RustStatic => "rust_static",
220 Self::RustTypeAlias => "rust_type_alias",
221 Self::RustAttribute => "rust_attribute",
222 Self::RustLifetime => "rust_lifetime",
223 Self::RustTest => "rust_test",
224 Self::PythonFunction => "python_function",
225 Self::PythonMethod => "python_method",
226 Self::PythonClass => "python_class",
227 Self::PythonDecorator => "python_decorator",
228 Self::PythonImport => "python_import",
229 Self::PythonModule => "python_module",
230 Self::PythonLambda => "python_lambda",
231 Self::PythonAsync => "python_async",
232 Self::PythonProperty => "python_property",
233 }
234 }
235
236 pub fn parse(s: &str) -> Option<Self> {
237 match s {
238 "file" => Some(Self::File),
239 "module" => Some(Self::Module),
240 "class" => Some(Self::Class),
241 "function" => Some(Self::Function),
242 "method" => Some(Self::Method),
243 "parameter" => Some(Self::Parameter),
244 "variable" => Some(Self::Variable),
245 "test" => Some(Self::Test),
246 "type" => Some(Self::Type),
247 "constant" => Some(Self::Constant),
248 "import" => Some(Self::Import),
249 "rust_fn" => Some(Self::RustFn),
250 "rust_method" => Some(Self::RustMethod),
251 "rust_impl" => Some(Self::RustImpl),
252 "rust_trait" => Some(Self::RustTrait),
253 "rust_struct" => Some(Self::RustStruct),
254 "rust_enum" => Some(Self::RustEnum),
255 "rust_mod" => Some(Self::RustMod),
256 "rust_macro" => Some(Self::RustMacro),
257 "rust_use" => Some(Self::RustUse),
258 "rust_const" => Some(Self::RustConst),
259 "rust_static" => Some(Self::RustStatic),
260 "rust_type_alias" => Some(Self::RustTypeAlias),
261 "rust_attribute" => Some(Self::RustAttribute),
262 "rust_lifetime" => Some(Self::RustLifetime),
263 "rust_test" => Some(Self::RustTest),
264 "python_function" => Some(Self::PythonFunction),
265 "python_method" => Some(Self::PythonMethod),
266 "python_class" => Some(Self::PythonClass),
267 "python_decorator" => Some(Self::PythonDecorator),
268 "python_import" => Some(Self::PythonImport),
269 "python_module" => Some(Self::PythonModule),
270 "python_lambda" => Some(Self::PythonLambda),
271 "python_async" => Some(Self::PythonAsync),
272 "python_property" => Some(Self::PythonProperty),
273 _ => None,
274 }
275 }
276
277 pub fn is_rust_specific(self) -> bool {
279 matches!(
280 self,
281 Self::RustFn
282 | Self::RustMethod
283 | Self::RustImpl
284 | Self::RustTrait
285 | Self::RustStruct
286 | Self::RustEnum
287 | Self::RustMod
288 | Self::RustMacro
289 | Self::RustUse
290 | Self::RustConst
291 | Self::RustStatic
292 | Self::RustTypeAlias
293 | Self::RustAttribute
294 | Self::RustLifetime
295 | Self::RustTest
296 )
297 }
298
299 pub fn is_python_specific(self) -> bool {
301 matches!(
302 self,
303 Self::PythonFunction
304 | Self::PythonMethod
305 | Self::PythonClass
306 | Self::PythonDecorator
307 | Self::PythonImport
308 | Self::PythonModule
309 | Self::PythonLambda
310 | Self::PythonAsync
311 | Self::PythonProperty
312 )
313 }
314}
315
316impl std::fmt::Display for CodeNodeKind {
317 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318 f.write_str(self.as_str())
319 }
320}
321
322#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
324pub enum CodeEdgePredicate {
325 Calls,
326 InheritsFrom,
327 Imports,
328 Defines,
329 Uses,
330 Tests,
331 Contains,
332 References,
333 ImplementsTrait,
335 ReExports,
337 MacroExpands,
339 TestTargets,
341}
342
343impl CodeEdgePredicate {
344 pub const ALL: [CodeEdgePredicate; 12] = [
345 Self::Calls,
346 Self::InheritsFrom,
347 Self::Imports,
348 Self::Defines,
349 Self::Uses,
350 Self::Tests,
351 Self::Contains,
352 Self::References,
353 Self::ImplementsTrait,
354 Self::ReExports,
355 Self::MacroExpands,
356 Self::TestTargets,
357 ];
358
359 pub fn as_str(self) -> &'static str {
360 match self {
361 Self::Calls => "calls",
362 Self::InheritsFrom => "inherits_from",
363 Self::Imports => "imports",
364 Self::Defines => "defines",
365 Self::Uses => "uses",
366 Self::Tests => "tests",
367 Self::Contains => "contains",
368 Self::References => "references",
369 Self::ImplementsTrait => "implements_trait",
370 Self::ReExports => "re_exports",
371 Self::MacroExpands => "macro_expands",
372 Self::TestTargets => "test_targets",
373 }
374 }
375
376 pub fn parse(s: &str) -> Option<Self> {
377 match s {
378 "calls" => Some(Self::Calls),
379 "inherits_from" => Some(Self::InheritsFrom),
380 "imports" => Some(Self::Imports),
381 "defines" => Some(Self::Defines),
382 "uses" => Some(Self::Uses),
383 "tests" => Some(Self::Tests),
384 "contains" => Some(Self::Contains),
385 "references" => Some(Self::References),
386 "implements_trait" => Some(Self::ImplementsTrait),
387 "re_exports" => Some(Self::ReExports),
388 "macro_expands" => Some(Self::MacroExpands),
389 "test_targets" => Some(Self::TestTargets),
390 _ => None,
391 }
392 }
393}
394
395impl std::fmt::Display for CodeEdgePredicate {
396 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
397 f.write_str(self.as_str())
398 }
399}
400
401pub fn extract_file_path(node_id: &str) -> Option<String> {
413 let rest = node_id.split_once(':')?.1;
414 let file_part = rest.split("::").next()?;
415 if file_part.is_empty() {
416 None
417 } else {
418 Some(file_part.to_string())
419 }
420}
421
422pub fn code_nodes_schema() -> Schema {
452 Schema::new(vec![
453 Field::new("id", DataType::Utf8, false),
455 Field::new(
456 "kind",
457 DataType::Dictionary(Box::new(DataType::Int8), Box::new(DataType::Utf8)),
458 false,
459 ),
460 Field::new("parent_id", DataType::Utf8, true),
461 Field::new("name", DataType::Utf8, false),
462 Field::new("signature", DataType::Utf8, true),
463 Field::new("docstring", DataType::Utf8, true),
464 Field::new("body_hash", DataType::Utf8, true),
465 Field::new("body", DataType::LargeUtf8, true),
466 Field::new(
468 "embedding",
469 DataType::FixedSizeList(
470 Arc::new(Field::new("item", DataType::Float32, false)),
471 CODE_EMBEDDING_DIM,
472 ),
473 true,
474 ),
475 Field::new("loc", DataType::Int32, true),
477 Field::new("cyclomatic_complexity", DataType::Int32, true),
478 Field::new("coverage_pct", DataType::Float64, true),
479 Field::new(
480 "last_modified",
481 DataType::Timestamp(TimeUnit::Millisecond, Some("UTC".into())),
482 true,
483 ),
484 Field::new("start_line", DataType::UInt32, true),
486 Field::new("end_line", DataType::UInt32, true),
487 Field::new("start_col", DataType::UInt32, true),
488 Field::new("end_col", DataType::UInt32, true),
489 Field::new("file_path", DataType::Utf8, true),
490 Field::new("byte_offset", DataType::UInt64, true),
491 ])
492}
493
494pub fn code_edges_schema() -> Schema {
503 Schema::new(vec![
504 Field::new("source_id", DataType::Utf8, false),
505 Field::new("target_id", DataType::Utf8, false),
506 Field::new(
507 "predicate",
508 DataType::Dictionary(Box::new(DataType::Int8), Box::new(DataType::Utf8)),
509 false,
510 ),
511 Field::new("weight", DataType::Float32, true),
512 Field::new("commit_id", DataType::Utf8, true),
513 ])
514}
515
516use arrow::array::{
519 Float32Array, Float64Array, Int8Array, Int32Array, RecordBatch, StringArray, UInt32Array,
520 UInt64Array,
521};
522
523#[derive(Debug, Clone)]
530pub struct CodeNode {
531 pub id: String,
532 pub kind: CodeNodeKind,
533 pub parent_id: Option<String>,
534 pub name: String,
535 pub signature: Option<String>,
536 pub docstring: Option<String>,
537 pub body_hash: Option<String>,
538 pub body: Option<String>,
539 pub loc: Option<i32>,
540 pub cyclomatic_complexity: Option<i32>,
541 pub coverage_pct: Option<f64>,
542 pub last_modified: Option<i64>,
543 pub start_line: Option<u32>,
545 pub end_line: Option<u32>,
546 pub start_col: Option<u32>,
547 pub end_col: Option<u32>,
548 pub file_path: Option<String>,
549 pub byte_offset: Option<u64>,
550}
551
552impl Default for CodeNode {
553 fn default() -> Self {
554 Self {
555 id: String::new(),
556 kind: CodeNodeKind::File,
557 parent_id: None,
558 name: String::new(),
559 signature: None,
560 docstring: None,
561 body_hash: None,
562 body: None,
563 loc: None,
564 cyclomatic_complexity: None,
565 coverage_pct: None,
566 last_modified: None,
567 start_line: None,
568 end_line: None,
569 start_col: None,
570 end_col: None,
571 file_path: None,
572 byte_offset: None,
573 }
574 }
575}
576
577#[derive(Debug, Clone)]
579pub struct CodeEdge {
580 pub source_id: String,
581 pub target_id: String,
582 pub predicate: CodeEdgePredicate,
583 pub weight: Option<f32>,
584 pub commit_id: Option<String>,
585}
586
587pub fn build_code_nodes_batch(nodes: &[CodeNode]) -> Result<RecordBatch, arrow::error::ArrowError> {
589 use arrow::array::{Int8DictionaryArray, PrimitiveBuilder};
590
591 let schema = Arc::new(code_nodes_schema());
592 let n = nodes.len();
593
594 if n == 0 {
595 return Ok(RecordBatch::new_empty(schema));
596 }
597
598 let ids: Vec<&str> = nodes.iter().map(|n| n.id.as_str()).collect();
599 let names: Vec<&str> = nodes.iter().map(|n| n.name.as_str()).collect();
600 let parent_ids: Vec<Option<&str>> = nodes.iter().map(|n| n.parent_id.as_deref()).collect();
601 let signatures: Vec<Option<&str>> = nodes.iter().map(|n| n.signature.as_deref()).collect();
602 let docstrings: Vec<Option<&str>> = nodes.iter().map(|n| n.docstring.as_deref()).collect();
603 let body_hashes: Vec<Option<&str>> = nodes.iter().map(|n| n.body_hash.as_deref()).collect();
604 let bodies: Vec<Option<&str>> = nodes.iter().map(|n| n.body.as_deref()).collect();
605
606 let kind_keys = Int8Array::from(
608 nodes
609 .iter()
610 .map(|n| {
611 CodeNodeKind::ALL
612 .iter()
613 .position(|k| *k == n.kind)
614 .expect("valid kind") as i8
615 })
616 .collect::<Vec<i8>>(),
617 );
618 let kind_values = StringArray::from(
619 CodeNodeKind::ALL
620 .iter()
621 .map(|k| k.as_str())
622 .collect::<Vec<_>>(),
623 );
624 let kind_dict = Int8DictionaryArray::try_new(kind_keys, Arc::new(kind_values))?;
625
626 let embedding_field = Arc::new(Field::new("item", DataType::Float32, false));
628 let null_embedding = arrow::array::FixedSizeListArray::try_new(
629 embedding_field,
630 CODE_EMBEDDING_DIM,
631 Arc::new(Float32Array::from(vec![
632 0.0f32;
633 n * CODE_EMBEDDING_DIM as usize
634 ])),
635 Some(arrow::buffer::NullBuffer::new(
636 arrow::buffer::BooleanBuffer::from(vec![false; n]),
637 )),
638 )?;
639
640 let locs: Vec<Option<i32>> = nodes.iter().map(|n| n.loc).collect();
642 let complexities: Vec<Option<i32>> = nodes.iter().map(|n| n.cyclomatic_complexity).collect();
643 let coverages: Vec<Option<f64>> = nodes.iter().map(|n| n.coverage_pct).collect();
644
645 let mut last_mod_builder =
646 PrimitiveBuilder::<arrow::datatypes::TimestampMillisecondType>::new().with_timezone("UTC");
647 for node in nodes {
648 match node.last_modified {
649 Some(ts) => last_mod_builder.append_value(ts),
650 None => last_mod_builder.append_null(),
651 }
652 }
653
654 let start_lines: Vec<Option<u32>> = nodes.iter().map(|n| n.start_line).collect();
656 let end_lines: Vec<Option<u32>> = nodes.iter().map(|n| n.end_line).collect();
657 let start_cols: Vec<Option<u32>> = nodes.iter().map(|n| n.start_col).collect();
658 let end_cols: Vec<Option<u32>> = nodes.iter().map(|n| n.end_col).collect();
659 let file_paths: Vec<Option<&str>> = nodes.iter().map(|n| n.file_path.as_deref()).collect();
660 let byte_offsets: Vec<Option<u64>> = nodes.iter().map(|n| n.byte_offset).collect();
661
662 RecordBatch::try_new(
663 schema,
664 vec![
665 Arc::new(StringArray::from(ids)),
666 Arc::new(kind_dict),
667 Arc::new(StringArray::from(parent_ids)),
668 Arc::new(StringArray::from(names)),
669 Arc::new(StringArray::from(signatures)),
670 Arc::new(StringArray::from(docstrings)),
671 Arc::new(StringArray::from(body_hashes)),
672 Arc::new(arrow::array::LargeStringArray::from(bodies)),
673 Arc::new(null_embedding),
674 Arc::new(Int32Array::from(locs)),
675 Arc::new(Int32Array::from(complexities)),
676 Arc::new(Float64Array::from(coverages)),
677 Arc::new(last_mod_builder.finish()),
678 Arc::new(UInt32Array::from(start_lines)),
679 Arc::new(UInt32Array::from(end_lines)),
680 Arc::new(UInt32Array::from(start_cols)),
681 Arc::new(UInt32Array::from(end_cols)),
682 Arc::new(StringArray::from(file_paths)),
683 Arc::new(UInt64Array::from(byte_offsets)),
684 ],
685 )
686}
687
688pub fn build_code_edges_batch(edges: &[CodeEdge]) -> Result<RecordBatch, arrow::error::ArrowError> {
690 use arrow::array::Int8DictionaryArray;
691
692 let schema = Arc::new(code_edges_schema());
693 let n = edges.len();
694
695 if n == 0 {
696 return Ok(RecordBatch::new_empty(schema));
697 }
698
699 let source_ids: Vec<&str> = edges.iter().map(|e| e.source_id.as_str()).collect();
700 let target_ids: Vec<&str> = edges.iter().map(|e| e.target_id.as_str()).collect();
701 let weights: Vec<Option<f32>> = edges.iter().map(|e| e.weight).collect();
702 let commit_ids: Vec<Option<&str>> = edges.iter().map(|e| e.commit_id.as_deref()).collect();
703
704 let pred_keys = Int8Array::from(
706 edges
707 .iter()
708 .map(|e| {
709 CodeEdgePredicate::ALL
710 .iter()
711 .position(|p| *p == e.predicate)
712 .expect("valid predicate") as i8
713 })
714 .collect::<Vec<i8>>(),
715 );
716 let pred_values = StringArray::from(
717 CodeEdgePredicate::ALL
718 .iter()
719 .map(|p| p.as_str())
720 .collect::<Vec<_>>(),
721 );
722 let pred_dict = Int8DictionaryArray::try_new(pred_keys, Arc::new(pred_values))?;
723
724 RecordBatch::try_new(
725 schema,
726 vec![
727 Arc::new(StringArray::from(source_ids)),
728 Arc::new(StringArray::from(target_ids)),
729 Arc::new(pred_dict),
730 Arc::new(Float32Array::from(weights)),
731 Arc::new(StringArray::from(commit_ids)),
732 ],
733 )
734}
735
736#[cfg(test)]
737mod tests {
738 use super::*;
739 use arrow::array::Array;
740
741 #[test]
742 fn test_code_nodes_schema_field_count() {
743 let schema = code_nodes_schema();
744 assert_eq!(schema.fields().len(), 19);
745 }
746
747 #[test]
748 fn test_code_edges_schema_field_count() {
749 let schema = code_edges_schema();
750 assert_eq!(schema.fields().len(), 5);
751 }
752
753 #[test]
754 fn test_code_node_kind_roundtrip() {
755 for kind in CodeNodeKind::ALL {
756 let s = kind.as_str();
757 let parsed = CodeNodeKind::parse(s).expect("should parse");
758 assert_eq!(parsed, kind);
759 }
760 }
761
762 #[test]
763 fn test_code_edge_predicate_roundtrip() {
764 for pred in CodeEdgePredicate::ALL {
765 let s = pred.as_str();
766 let parsed = CodeEdgePredicate::parse(s).expect("should parse");
767 assert_eq!(parsed, pred);
768 }
769 }
770
771 #[test]
772 fn test_build_code_nodes_batch() {
773 let nodes = vec![
774 CodeNode {
775 id: "func:brain/main.py::main".to_string(),
776 kind: CodeNodeKind::Function,
777 parent_id: Some("mod:brain/main.py".to_string()),
778 name: "main".to_string(),
779 signature: Some("def main() -> None".to_string()),
780 docstring: Some("Entry point.".to_string()),
781 body_hash: Some("abc123".to_string()),
782 body: Some("def main():\n pass".to_string()),
783 loc: Some(42),
784 cyclomatic_complexity: Some(5),
785 coverage_pct: Some(0.85),
786 last_modified: None,
787 ..Default::default()
788 },
789 CodeNode {
790 id: "class:brain/store.py::Store".to_string(),
791 kind: CodeNodeKind::Class,
792 parent_id: Some("mod:brain/store.py".to_string()),
793 name: "Store".to_string(),
794 signature: None,
795 docstring: Some("Main store class.".to_string()),
796 body_hash: Some("def456".to_string()),
797 body: Some("class Store:\n pass".to_string()),
798 loc: Some(120),
799 cyclomatic_complexity: Some(12),
800 coverage_pct: None,
801 last_modified: Some(chrono::Utc::now().timestamp_millis()),
802 ..Default::default()
803 },
804 ];
805
806 let batch = build_code_nodes_batch(&nodes).expect("should build batch");
807 assert_eq!(batch.num_rows(), 2);
808 assert_eq!(batch.num_columns(), 19);
809
810 let ids = batch
812 .column(node_col::ID)
813 .as_any()
814 .downcast_ref::<StringArray>()
815 .expect("id column");
816 assert_eq!(ids.value(0), "func:brain/main.py::main");
817 assert_eq!(ids.value(1), "class:brain/store.py::Store");
818
819 assert!(matches!(
821 batch.column(node_col::KIND).data_type(),
822 DataType::Dictionary(_, _)
823 ));
824
825 let emb = batch.column(node_col::EMBEDDING);
827 assert!(emb.is_null(0));
828 assert!(emb.is_null(1));
829
830 let locs = batch
832 .column(node_col::LOC)
833 .as_any()
834 .downcast_ref::<Int32Array>()
835 .expect("loc column");
836 assert_eq!(locs.value(0), 42);
837 assert_eq!(locs.value(1), 120);
838 }
839
840 #[test]
841 fn test_build_code_edges_batch() {
842 let edges = vec![
843 CodeEdge {
844 source_id: "func:a.py::foo".to_string(),
845 target_id: "func:b.py::bar".to_string(),
846 predicate: CodeEdgePredicate::Calls,
847 weight: Some(1.0),
848 commit_id: Some("abc123".to_string()),
849 },
850 CodeEdge {
851 source_id: "class:c.py::Child".to_string(),
852 target_id: "class:c.py::Parent".to_string(),
853 predicate: CodeEdgePredicate::InheritsFrom,
854 weight: None,
855 commit_id: None,
856 },
857 CodeEdge {
858 source_id: "mod:a.py".to_string(),
859 target_id: "mod:b.py".to_string(),
860 predicate: CodeEdgePredicate::Imports,
861 weight: None,
862 commit_id: None,
863 },
864 ];
865
866 let batch = build_code_edges_batch(&edges).expect("should build batch");
867 assert_eq!(batch.num_rows(), 3);
868 assert_eq!(batch.num_columns(), 5);
869
870 assert!(matches!(
872 batch.column(edge_col::PREDICATE).data_type(),
873 DataType::Dictionary(_, _)
874 ));
875
876 let sources = batch
878 .column(edge_col::SOURCE_ID)
879 .as_any()
880 .downcast_ref::<StringArray>()
881 .expect("source_id column");
882 assert_eq!(sources.value(0), "func:a.py::foo");
883 assert_eq!(sources.value(2), "mod:a.py");
884 }
885
886 #[test]
887 fn test_empty_batches() {
888 let nodes_batch = build_code_nodes_batch(&[]).expect("empty nodes batch");
889 assert_eq!(nodes_batch.num_rows(), 0);
890 assert_eq!(nodes_batch.num_columns(), 19);
891
892 let edges_batch = build_code_edges_batch(&[]).expect("empty edges batch");
893 assert_eq!(edges_batch.num_rows(), 0);
894 assert_eq!(edges_batch.num_columns(), 5);
895 }
896
897 #[test]
900 fn test_all_35_code_node_kind_variants_roundtrip() {
901 assert_eq!(CodeNodeKind::ALL.len(), 35, "expected 35 variants in ALL");
903 for kind in CodeNodeKind::ALL {
904 let s = kind.as_str();
905 let parsed = CodeNodeKind::parse(s)
906 .unwrap_or_else(|| panic!("parse({s:?}) returned None for {kind:?}"));
907 assert_eq!(parsed, kind, "roundtrip failed for {kind:?}");
908 }
909 }
910
911 #[test]
912 fn test_rust_specific_variants_are_flagged() {
913 let rust_kinds = [
914 CodeNodeKind::RustFn,
915 CodeNodeKind::RustMethod,
916 CodeNodeKind::RustImpl,
917 CodeNodeKind::RustTrait,
918 CodeNodeKind::RustStruct,
919 CodeNodeKind::RustEnum,
920 CodeNodeKind::RustMod,
921 CodeNodeKind::RustMacro,
922 CodeNodeKind::RustUse,
923 CodeNodeKind::RustConst,
924 CodeNodeKind::RustStatic,
925 CodeNodeKind::RustTypeAlias,
926 CodeNodeKind::RustAttribute,
927 CodeNodeKind::RustLifetime,
928 CodeNodeKind::RustTest,
929 ];
930 assert_eq!(rust_kinds.len(), 15, "expected 15 Rust-specific variants");
931 for k in rust_kinds {
932 assert!(k.is_rust_specific(), "{k:?} should be rust-specific");
933 }
934
935 let generic_kinds = [
937 CodeNodeKind::File,
938 CodeNodeKind::Module,
939 CodeNodeKind::Class,
940 CodeNodeKind::Function,
941 CodeNodeKind::Method,
942 CodeNodeKind::Parameter,
943 CodeNodeKind::Variable,
944 CodeNodeKind::Test,
945 CodeNodeKind::Type,
946 CodeNodeKind::Constant,
947 CodeNodeKind::Import,
948 ];
949 for k in generic_kinds {
950 assert!(!k.is_rust_specific(), "{k:?} should NOT be rust-specific");
951 }
952 }
953
954 #[test]
955 fn test_rust_kind_strings_have_rust_prefix() {
956 let rust_kinds = CodeNodeKind::ALL
957 .iter()
958 .filter(|k| k.is_rust_specific())
959 .collect::<Vec<_>>();
960 assert!(!rust_kinds.is_empty());
961 for k in rust_kinds {
962 assert!(
963 k.as_str().starts_with("rust_"),
964 "Rust-specific kind {k:?} string {:?} must start with 'rust_'",
965 k.as_str()
966 );
967 }
968 }
969
970 #[test]
971 fn test_position_columns_are_in_schema() {
972 let schema = code_nodes_schema();
973 let field_names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect();
974 assert!(field_names.contains(&"start_line"), "missing start_line");
975 assert!(field_names.contains(&"end_line"), "missing end_line");
976 assert!(field_names.contains(&"start_col"), "missing start_col");
977 assert!(field_names.contains(&"end_col"), "missing end_col");
978 assert!(field_names.contains(&"file_path"), "missing file_path");
979 assert!(field_names.contains(&"byte_offset"), "missing byte_offset");
980 }
981
982 #[test]
983 fn test_position_columns_are_nullable() {
984 let schema = code_nodes_schema();
985 for name in &[
986 "start_line",
987 "end_line",
988 "start_col",
989 "end_col",
990 "file_path",
991 "byte_offset",
992 ] {
993 let field = schema
994 .field_with_name(name)
995 .unwrap_or_else(|_| panic!("{name} not in schema"));
996 assert!(
997 field.is_nullable(),
998 "{name} must be nullable for backward compatibility"
999 );
1000 }
1001 }
1002
1003 #[test]
1004 fn test_position_columns_correct_types() {
1005 use arrow::datatypes::DataType;
1006 let schema = code_nodes_schema();
1007 let check = |name: &str, expected: DataType| {
1008 let field = schema.field_with_name(name).expect(name);
1009 assert_eq!(
1010 *field.data_type(),
1011 expected,
1012 "{name} has wrong type: expected {expected:?}"
1013 );
1014 };
1015 check("start_line", DataType::UInt32);
1016 check("end_line", DataType::UInt32);
1017 check("start_col", DataType::UInt32);
1018 check("end_col", DataType::UInt32);
1019 check("file_path", DataType::Utf8);
1020 check("byte_offset", DataType::UInt64);
1021 }
1022
1023 #[test]
1024 fn test_position_column_indices_match_schema() {
1025 let schema = code_nodes_schema();
1027 let check = |idx: usize, expected_name: &str| {
1028 let actual = schema.field(idx).name();
1029 assert_eq!(
1030 actual, expected_name,
1031 "node_col constant {idx} expected {expected_name}, got {actual}"
1032 );
1033 };
1034 check(node_col::START_LINE, "start_line");
1035 check(node_col::END_LINE, "end_line");
1036 check(node_col::START_COL, "start_col");
1037 check(node_col::END_COL, "end_col");
1038 check(node_col::FILE_PATH, "file_path");
1039 check(node_col::BYTE_OFFSET, "byte_offset");
1040 }
1041
1042 #[test]
1043 fn test_build_node_with_position_metadata() {
1044 let nodes = vec![
1045 CodeNode {
1046 id: "rust_fn:src/lib.rs::process".to_string(),
1047 kind: CodeNodeKind::RustFn,
1048 parent_id: Some("rust_mod:src/lib.rs".to_string()),
1049 name: "process".to_string(),
1050 signature: Some("fn process(input: &str) -> Result<()>".to_string()),
1051 start_line: Some(42),
1052 end_line: Some(58),
1053 start_col: Some(0),
1054 end_col: Some(1),
1055 file_path: Some("src/lib.rs".to_string()),
1056 byte_offset: Some(1024),
1057 loc: Some(17),
1058 ..Default::default()
1059 },
1060 CodeNode {
1062 id: "func:brain/old.py::legacy".to_string(),
1063 kind: CodeNodeKind::Function,
1064 name: "legacy".to_string(),
1065 ..Default::default()
1066 },
1067 ];
1068
1069 let batch = build_code_nodes_batch(&nodes).expect("should build batch");
1070 assert_eq!(batch.num_rows(), 2);
1071 assert_eq!(batch.num_columns(), 19);
1072
1073 let start_lines = batch
1075 .column(node_col::START_LINE)
1076 .as_any()
1077 .downcast_ref::<UInt32Array>()
1078 .expect("start_line column");
1079 assert!(!start_lines.is_null(0), "row 0 start_line should be set");
1080 assert_eq!(start_lines.value(0), 42);
1081
1082 let file_paths = batch
1083 .column(node_col::FILE_PATH)
1084 .as_any()
1085 .downcast_ref::<StringArray>()
1086 .expect("file_path column");
1087 assert_eq!(file_paths.value(0), "src/lib.rs");
1088
1089 assert!(start_lines.is_null(1), "row 1 start_line should be null");
1091 assert!(file_paths.is_null(1), "row 1 file_path should be null");
1092
1093 assert!(matches!(
1095 batch.column(node_col::KIND).data_type(),
1096 DataType::Dictionary(_, _)
1097 ));
1098 }
1099
1100 #[test]
1101 fn test_dictionary_encoding_roundtrip_all_kinds() {
1102 use arrow::array::Int8DictionaryArray;
1103
1104 let nodes: Vec<CodeNode> = CodeNodeKind::ALL
1106 .iter()
1107 .enumerate()
1108 .map(|(i, &kind)| CodeNode {
1109 id: format!("node_{i}"),
1110 kind,
1111 name: format!("item_{i}"),
1112 ..Default::default()
1113 })
1114 .collect();
1115
1116 let batch = build_code_nodes_batch(&nodes).expect("batch with all 35 kinds");
1117 assert_eq!(batch.num_rows(), 35);
1118
1119 let kind_col = batch
1121 .column(node_col::KIND)
1122 .as_any()
1123 .downcast_ref::<Int8DictionaryArray>()
1124 .expect("kind is Int8DictionaryArray");
1125 let kind_values = kind_col
1126 .values()
1127 .as_any()
1128 .downcast_ref::<StringArray>()
1129 .expect("kind values are strings");
1130
1131 for (i, &expected_kind) in CodeNodeKind::ALL.iter().enumerate() {
1132 let key = kind_col.keys().value(i) as usize;
1133 let decoded_str = kind_values.value(key);
1134 assert_eq!(
1135 decoded_str,
1136 expected_kind.as_str(),
1137 "row {i}: expected {:?}, got {decoded_str:?}",
1138 expected_kind.as_str()
1139 );
1140 }
1141 }
1142
1143 #[test]
1146 fn test_python_specific_variants_are_flagged() {
1147 let python_kinds = [
1148 CodeNodeKind::PythonFunction,
1149 CodeNodeKind::PythonMethod,
1150 CodeNodeKind::PythonClass,
1151 CodeNodeKind::PythonDecorator,
1152 CodeNodeKind::PythonImport,
1153 CodeNodeKind::PythonModule,
1154 CodeNodeKind::PythonLambda,
1155 CodeNodeKind::PythonAsync,
1156 CodeNodeKind::PythonProperty,
1157 ];
1158 assert_eq!(python_kinds.len(), 9, "expected 9 Python-specific variants");
1159 for k in python_kinds {
1160 assert!(k.is_python_specific(), "{k:?} should be python-specific");
1161 }
1162
1163 let non_python = [
1165 CodeNodeKind::File,
1166 CodeNodeKind::Module,
1167 CodeNodeKind::Class,
1168 CodeNodeKind::Function,
1169 CodeNodeKind::Method,
1170 CodeNodeKind::RustFn,
1171 CodeNodeKind::RustStruct,
1172 CodeNodeKind::Import,
1173 ];
1174 for k in non_python {
1175 assert!(
1176 !k.is_python_specific(),
1177 "{k:?} should NOT be python-specific"
1178 );
1179 }
1180 }
1181
1182 #[test]
1183 fn test_python_kind_strings_have_python_prefix() {
1184 let python_kinds = CodeNodeKind::ALL
1185 .iter()
1186 .filter(|k| k.is_python_specific())
1187 .collect::<Vec<_>>();
1188 assert_eq!(python_kinds.len(), 9);
1189 for k in python_kinds {
1190 assert!(
1191 k.as_str().starts_with("python_"),
1192 "Python-specific kind {k:?} string {:?} must start with 'python_'",
1193 k.as_str()
1194 );
1195 }
1196 }
1197
1198 #[test]
1199 fn test_python_variants_roundtrip() {
1200 let python_kinds = [
1201 CodeNodeKind::PythonFunction,
1202 CodeNodeKind::PythonMethod,
1203 CodeNodeKind::PythonClass,
1204 CodeNodeKind::PythonDecorator,
1205 CodeNodeKind::PythonImport,
1206 CodeNodeKind::PythonModule,
1207 CodeNodeKind::PythonLambda,
1208 CodeNodeKind::PythonAsync,
1209 CodeNodeKind::PythonProperty,
1210 ];
1211 for k in python_kinds {
1212 let s = k.as_str();
1213 let parsed = CodeNodeKind::parse(s)
1214 .unwrap_or_else(|| panic!("parse({s:?}) returned None for {k:?}"));
1215 assert_eq!(parsed, k, "roundtrip failed for {k:?}");
1216 }
1217 }
1218}