Skip to main content

nusy_codegraph/
crate_schema.rs

1//! Arrow schemas for the crate-level dependency graph.
2//!
3//! Two tables:
4//! - **CrateNodes**: one row per crate (workspace members + notable external crates)
5//! - **CrateEdges**: one row per dependency relationship (source depends on target)
6//!
7//! These tables are produced by [`crate::crate_graph::build_crate_graph`] and can be
8//! stored as Parquet snapshots via the Arrow persistence layer.
9//!
10//! # Schema rationale
11//!
12//! CrateNode `id` is the crate name (e.g. `"nusy-arrow-core"`), matching the `name`
13//! field in `[package]`.  CrateEdge `source` → `target` represents "source depends on
14//! target".  `topo_sort_crates` returns targets before sources (dependency-first order).
15
16use arrow::datatypes::{DataType, Field, Schema};
17use std::sync::Arc;
18
19// ─── CrateNode column indices ────────────────────────────────────────────────
20
21/// Named column indices for the CrateNodes schema.
22pub mod crate_node_col {
23    pub const ID: usize = 0;
24    pub const VERSION: usize = 1;
25    pub const WORKSPACE_MEMBER: usize = 2;
26    pub const DESCRIPTION: usize = 3;
27    pub const EDITION: usize = 4;
28}
29
30// ─── CrateEdge column indices ────────────────────────────────────────────────
31
32/// Named column indices for the CrateEdges schema.
33pub mod crate_edge_col {
34    pub const SOURCE: usize = 0;
35    pub const TARGET: usize = 1;
36    pub const VERSION_REQ: usize = 2;
37    pub const OPTIONAL: usize = 3;
38    pub const DEV_DEP: usize = 4;
39    pub const BUILD_DEP: usize = 5;
40    pub const SOURCE_KIND: usize = 6;
41}
42
43// ─── Schemas ─────────────────────────────────────────────────────────────────
44
45/// Arrow schema for crate nodes.
46///
47/// Columns:
48/// - `id`: crate name, e.g. `"nusy-arrow-core"` — primary key
49/// - `version`: resolved semver string
50/// - `workspace_member`: true when listed under `[workspace].members`
51/// - `description`: optional description from `[package].description`
52/// - `edition`: Rust edition, e.g. `"2024"`
53pub fn crate_node_schema() -> Arc<Schema> {
54    Arc::new(Schema::new(vec![
55        Field::new("id", DataType::Utf8, false),
56        Field::new("version", DataType::Utf8, false),
57        Field::new("workspace_member", DataType::Boolean, false),
58        Field::new("description", DataType::Utf8, true),
59        Field::new("edition", DataType::Utf8, false),
60    ]))
61}
62
63/// Arrow schema for crate dependency edges.
64///
65/// Columns:
66/// - `source`: the crate that declares the dependency
67/// - `target`: the crate being depended upon
68/// - `version_req`: semver requirement, e.g. `"55"`, `">=1, <2"`, `"*"` for path deps
69/// - `optional`: `true` if the dep is `optional = true`
70/// - `dev_dep`: `true` if the dep came from `[dev-dependencies]`
71/// - `build_dep`: `true` if the dep came from `[build-dependencies]`
72/// - `source_kind`: provenance string — one of `"workspace"`, `"crates_io"`, `"git"`, `"path"`
73pub fn crate_edge_schema() -> Arc<Schema> {
74    Arc::new(Schema::new(vec![
75        Field::new("source", DataType::Utf8, false),
76        Field::new("target", DataType::Utf8, false),
77        Field::new("version_req", DataType::Utf8, false),
78        Field::new("optional", DataType::Boolean, false),
79        Field::new("dev_dep", DataType::Boolean, false),
80        Field::new("build_dep", DataType::Boolean, false),
81        Field::new("source_kind", DataType::Utf8, false),
82    ]))
83}
84
85// ─── Tests ───────────────────────────────────────────────────────────────────
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn test_crate_node_schema_field_count() {
93        let schema = crate_node_schema();
94        assert_eq!(
95            schema.fields().len(),
96            5,
97            "CrateNode schema should have 5 fields"
98        );
99    }
100
101    #[test]
102    fn test_crate_edge_schema_field_count() {
103        let schema = crate_edge_schema();
104        assert_eq!(
105            schema.fields().len(),
106            7,
107            "CrateEdge schema should have 7 fields"
108        );
109    }
110
111    #[test]
112    fn test_crate_node_schema_field_names() {
113        let schema = crate_node_schema();
114        let names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect();
115        assert_eq!(
116            names,
117            vec![
118                "id",
119                "version",
120                "workspace_member",
121                "description",
122                "edition"
123            ]
124        );
125    }
126
127    #[test]
128    fn test_crate_edge_schema_field_names() {
129        let schema = crate_edge_schema();
130        let names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect();
131        assert_eq!(
132            names,
133            vec![
134                "source",
135                "target",
136                "version_req",
137                "optional",
138                "dev_dep",
139                "build_dep",
140                "source_kind"
141            ]
142        );
143    }
144
145    #[test]
146    fn test_crate_node_schema_nullability() {
147        let schema = crate_node_schema();
148        // id, version, workspace_member, edition must be non-null
149        for name in &["id", "version", "edition"] {
150            let field = schema.field_with_name(name).expect(name);
151            assert!(!field.is_nullable(), "{name} should be non-nullable");
152        }
153        // workspace_member
154        let wm = schema.field_with_name("workspace_member").unwrap();
155        assert!(!wm.is_nullable(), "workspace_member should be non-nullable");
156        // description is nullable
157        let desc = schema.field_with_name("description").unwrap();
158        assert!(desc.is_nullable(), "description should be nullable");
159    }
160
161    #[test]
162    fn test_crate_edge_schema_nullability() {
163        let schema = crate_edge_schema();
164        let non_null = ["source", "target", "version_req", "source_kind"];
165        for name in &non_null {
166            let field = schema.field_with_name(name).expect(name);
167            assert!(!field.is_nullable(), "{name} should be non-nullable");
168        }
169        let bool_cols = ["optional", "dev_dep", "build_dep"];
170        for name in &bool_cols {
171            let field = schema.field_with_name(name).expect(name);
172            assert!(
173                !field.is_nullable(),
174                "{name} bool col should be non-nullable"
175            );
176        }
177    }
178
179    #[test]
180    fn test_crate_node_col_constants_match_schema() {
181        let schema = crate_node_schema();
182        let check = |idx: usize, expected: &str| {
183            let actual = schema.field(idx).name();
184            assert_eq!(
185                actual, expected,
186                "crate_node_col[{idx}] expected {expected}, got {actual}"
187            );
188        };
189        check(crate_node_col::ID, "id");
190        check(crate_node_col::VERSION, "version");
191        check(crate_node_col::WORKSPACE_MEMBER, "workspace_member");
192        check(crate_node_col::DESCRIPTION, "description");
193        check(crate_node_col::EDITION, "edition");
194    }
195
196    #[test]
197    fn test_crate_edge_col_constants_match_schema() {
198        let schema = crate_edge_schema();
199        let check = |idx: usize, expected: &str| {
200            let actual = schema.field(idx).name();
201            assert_eq!(
202                actual, expected,
203                "crate_edge_col[{idx}] expected {expected}, got {actual}"
204            );
205        };
206        check(crate_edge_col::SOURCE, "source");
207        check(crate_edge_col::TARGET, "target");
208        check(crate_edge_col::VERSION_REQ, "version_req");
209        check(crate_edge_col::OPTIONAL, "optional");
210        check(crate_edge_col::DEV_DEP, "dev_dep");
211        check(crate_edge_col::BUILD_DEP, "build_dep");
212        check(crate_edge_col::SOURCE_KIND, "source_kind");
213    }
214
215    #[test]
216    fn test_crate_node_schema_types() {
217        let schema = crate_node_schema();
218        assert_eq!(
219            *schema.field(crate_node_col::ID).data_type(),
220            DataType::Utf8
221        );
222        assert_eq!(
223            *schema.field(crate_node_col::VERSION).data_type(),
224            DataType::Utf8
225        );
226        assert_eq!(
227            *schema.field(crate_node_col::WORKSPACE_MEMBER).data_type(),
228            DataType::Boolean
229        );
230        assert_eq!(
231            *schema.field(crate_node_col::DESCRIPTION).data_type(),
232            DataType::Utf8
233        );
234        assert_eq!(
235            *schema.field(crate_node_col::EDITION).data_type(),
236            DataType::Utf8
237        );
238    }
239
240    #[test]
241    fn test_crate_edge_schema_types() {
242        let schema = crate_edge_schema();
243        for col in &[
244            crate_edge_col::SOURCE,
245            crate_edge_col::TARGET,
246            crate_edge_col::VERSION_REQ,
247            crate_edge_col::SOURCE_KIND,
248        ] {
249            assert_eq!(*schema.field(*col).data_type(), DataType::Utf8);
250        }
251        for col in &[
252            crate_edge_col::OPTIONAL,
253            crate_edge_col::DEV_DEP,
254            crate_edge_col::BUILD_DEP,
255        ] {
256            assert_eq!(*schema.field(*col).data_type(), DataType::Boolean);
257        }
258    }
259}