Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions crates/codegraph-core/src/db/repository/graph_read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1675,8 +1675,15 @@ impl NativeDatabase {
.map(|k| format!("'{k}'"))
.collect::<Vec<_>>()
.join(",");
// ORDER BY id: without an explicit order, SQLite's row order for a
// bare WHERE scan is unspecified — it happened to track physical/
// insertion order, which is only deterministic now that the build
// pipeline inserts nodes in a fixed (BTreeMap-sorted) order (#1734).
// Sorting explicitly here removes the dependency on that unspecified
// behavior so downstream consumers (e.g. community detection) build
// the same graph on every run regardless of how rows are stored.
let sql = format!(
"SELECT id, name, kind, file FROM nodes WHERE kind IN ({kinds_sql})"
"SELECT id, name, kind, file FROM nodes WHERE kind IN ({kinds_sql}) ORDER BY id"
);
let mut stmt = conn
.prepare_cached(&sql)
Expand All @@ -1701,7 +1708,8 @@ impl NativeDatabase {
let conn = self.conn()?;
let mut stmt = conn
.prepare_cached(
"SELECT source_id, target_id, confidence FROM edges WHERE kind = 'calls'",
"SELECT source_id, target_id, confidence FROM edges WHERE kind = 'calls' \
ORDER BY source_id, target_id",
)
.map_err(|e| napi::Error::from_reason(format!("get_call_edges prepare: {e}")))?;
let rows = stmt
Expand All @@ -1721,8 +1729,10 @@ impl NativeDatabase {
#[napi]
pub fn get_file_nodes_all(&self) -> napi::Result<Vec<NativeFileNodeRow>> {
let conn = self.conn()?;
// ORDER BY id — see the comment in get_callable_nodes for why an
// explicit order matters for build-to-build determinism (#1734).
let mut stmt = conn
.prepare_cached("SELECT id, name, file FROM nodes WHERE kind = 'file'")
.prepare_cached("SELECT id, name, file FROM nodes WHERE kind = 'file' ORDER BY id")
.map_err(|e| napi::Error::from_reason(format!("get_file_nodes_all prepare: {e}")))?;
let rows = stmt
.query_map([], |row| {
Expand All @@ -1743,7 +1753,8 @@ impl NativeDatabase {
let conn = self.conn()?;
let mut stmt = conn
.prepare_cached(
"SELECT source_id, target_id FROM edges WHERE kind IN ('imports','imports-type')",
"SELECT source_id, target_id FROM edges WHERE kind IN ('imports','imports-type') \
ORDER BY source_id, target_id",
)
.map_err(|e| napi::Error::from_reason(format!("get_import_edges prepare: {e}")))?;
let rows = stmt
Expand Down
48 changes: 24 additions & 24 deletions crates/codegraph-core/src/domain/graph/builder/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ use crate::features::structure;
use crate::types::{FileSymbols, ImportResolutionInput, TypeMapEntry};
use rusqlite::Connection;
use serde::Serialize;
use std::collections::{HashMap, HashSet};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::path::Path;
use std::time::Instant;

Expand Down Expand Up @@ -218,12 +218,12 @@ fn parse_and_index_files(
root_dir: &str,
include_dataflow: bool,
include_ast: bool,
) -> HashMap<String, FileSymbols> {
) -> BTreeMap<String, FileSymbols> {
let files_to_parse: Vec<String> =
parse_changes.iter().map(|c| c.abs_path.clone()).collect();
let parsed =
parallel::parse_files_parallel(&files_to_parse, root_dir, include_dataflow, include_ast);
let mut file_symbols: HashMap<String, FileSymbols> = HashMap::new();
let mut file_symbols: BTreeMap<String, FileSymbols> = BTreeMap::new();
for mut sym in parsed {
let rel = relative_path(root_dir, &sym.file);
sym.file = rel.clone();
Expand All @@ -235,7 +235,7 @@ fn parse_and_index_files(
/// Build the batched import-resolution input set and run resolution, returning
/// `(batch_resolved, known_files)`. Mirrors stage 6 of `run_pipeline`.
fn resolve_pipeline_imports(
file_symbols: &HashMap<String, FileSymbols>,
file_symbols: &BTreeMap<String, FileSymbols>,
collect_files: &[String],
root_dir: &str,
napi_aliases: &crate::types::PathAliases,
Expand Down Expand Up @@ -288,7 +288,7 @@ fn reconnect_saved_reverse_dep_edges(
/// are present (reverse-deps are reconnected, not re-parsed).
fn run_structure_phase(
conn: &Connection,
file_symbols: &HashMap<String, FileSymbols>,
file_symbols: &BTreeMap<String, FileSymbols>,
collect_directories: &HashSet<String>,
root_dir: &str,
line_count_map: &HashMap<String, i64>,
Expand Down Expand Up @@ -326,7 +326,7 @@ fn run_structure_phase(
/// nodes are gone (#1027).
fn run_role_classification(
conn: &Connection,
file_symbols: &HashMap<String, FileSymbols>,
file_symbols: &BTreeMap<String, FileSymbols>,
removal_reverse_deps: Vec<String>,
is_full_build: bool,
) {
Expand Down Expand Up @@ -367,7 +367,7 @@ struct AnalysisPersistenceResult {
/// analysis scope.
fn run_analysis_persistence(
conn: &Connection,
file_symbols: &HashMap<String, FileSymbols>,
file_symbols: &BTreeMap<String, FileSymbols>,
analysis_scope: Option<&Vec<String>>,
opts: &BuildOpts,
include_ast: bool,
Expand Down Expand Up @@ -765,7 +765,7 @@ fn reparse_barrel_candidates(
root_dir: &str,
napi_aliases: &crate::types::PathAliases,
known_files: &HashSet<String>,
file_symbols: &mut HashMap<String, FileSymbols>,
file_symbols: &mut BTreeMap<String, FileSymbols>,
batch_resolved: &mut HashMap<String, String>,
) {
// Find all barrel files from DB (files that have 'reexports' edges)
Expand Down Expand Up @@ -892,7 +892,7 @@ fn collect_imported_barrel_candidates(
from_files: &[String],
batch_resolved: &HashMap<String, String>,
barrel_files_in_db: &HashSet<String>,
file_symbols: &HashMap<String, FileSymbols>,
file_symbols: &BTreeMap<String, FileSymbols>,
) -> Vec<String> {
let mut out = Vec::new();
for rel_path in from_files {
Expand Down Expand Up @@ -926,7 +926,7 @@ fn collect_reexport_from_barrels(
conn: &Connection,
root_dir: &str,
changed_files: &[String],
file_symbols: &HashMap<String, FileSymbols>,
file_symbols: &BTreeMap<String, FileSymbols>,
) -> Vec<String> {
let mut out = Vec::new();
let mut stmt = match conn.prepare(
Expand Down Expand Up @@ -1018,7 +1018,7 @@ fn check_version_mismatch(conn: &Connection) -> bool {

/// Build InsertNodesBatch from parsed file symbols.
fn build_insert_batches(
file_symbols: &HashMap<String, FileSymbols>,
file_symbols: &BTreeMap<String, FileSymbols>,
) -> Vec<crate::domain::graph::builder::stages::insert_nodes::InsertNodesBatch> {
file_symbols
.iter()
Expand Down Expand Up @@ -1155,7 +1155,7 @@ const EDGE_NODE_KIND_FILTER: &str = "kind IN ('function','method','class','inter
/// ultimate definition files barrel chains resolve to. Mirrors the JS
/// `relevantFiles` accumulation in `loadNodes` (#976, greptile P1).
fn compute_edge_relevant_files(
file_symbols: &HashMap<String, FileSymbols>,
file_symbols: &BTreeMap<String, FileSymbols>,
import_ctx: &crate::domain::graph::builder::stages::import_edges::ImportEdgeContext,
) -> HashSet<String> {
let mut relevant_files: HashSet<String> = file_symbols.keys().cloned().collect();
Expand Down Expand Up @@ -1192,7 +1192,7 @@ fn compute_edge_relevant_files(
/// `Vec<NodeInfo>` suitable for the native edge builder.
fn load_edge_node_set(
conn: &Connection,
file_symbols: &HashMap<String, FileSymbols>,
file_symbols: &BTreeMap<String, FileSymbols>,
import_ctx: &crate::domain::graph::builder::stages::import_edges::ImportEdgeContext,
is_incremental: bool,
) -> Vec<crate::domain::graph::builder::stages::build_edges::NodeInfo> {
Expand Down Expand Up @@ -1354,7 +1354,7 @@ fn collect_imported_names_for_file(
/// so method calls and receiver edges on that variable resolve. Must run
/// before `build_and_insert_call_edges`.
fn propagate_return_types_across_files(
file_symbols: &mut HashMap<String, FileSymbols>,
file_symbols: &mut BTreeMap<String, FileSymbols>,
import_ctx: &ImportEdgeContext,
) {
use crate::domain::graph::builder::stages::build_edges::PROPAGATION_HOP_PENALTY;
Expand Down Expand Up @@ -1386,7 +1386,7 @@ fn propagate_return_types_across_files(
/// - `global_return_types`: flat map for qualified `Type.method` lookups; higher
/// confidence wins, tie-break is deterministic (paths visited in sorted order).
fn build_return_type_index(
file_symbols: &HashMap<String, FileSymbols>,
file_symbols: &BTreeMap<String, FileSymbols>,
) -> (
HashMap<String, HashMap<String, (String, f64)>>,
HashMap<String, (String, f64)>,
Expand Down Expand Up @@ -1511,7 +1511,7 @@ fn insert_call_edge_rows(conn: &Connection, edges: &[crate::domain::graph::build
/// Full builds always load every node — there is no smaller set anyway.
fn build_and_insert_call_edges(
conn: &Connection,
file_symbols: &HashMap<String, FileSymbols>,
file_symbols: &BTreeMap<String, FileSymbols>,
import_ctx: &ImportEdgeContext,
is_incremental: bool,
) {
Expand Down Expand Up @@ -1669,7 +1669,7 @@ fn build_analysis_node_map(

/// Convert FileSymbols AST nodes to FileAstBatch format for `ast::do_insert_ast_nodes`.
fn build_ast_batches(
file_symbols: &HashMap<String, FileSymbols>,
file_symbols: &BTreeMap<String, FileSymbols>,
analysis_files: &HashSet<&str>,
) -> Vec<FileAstBatch> {
let mut batches = Vec::new();
Expand Down Expand Up @@ -1698,7 +1698,7 @@ fn build_ast_batches(
/// Write complexity metrics from parsed definitions to the `function_complexity` table.
fn write_complexity(
conn: &Connection,
file_symbols: &HashMap<String, FileSymbols>,
file_symbols: &BTreeMap<String, FileSymbols>,
analysis_files: &HashSet<&str>,
node_id_map: &HashMap<(String, String, u32), i64>,
) -> bool {
Expand Down Expand Up @@ -1777,7 +1777,7 @@ fn write_complexity(
/// Write CFG blocks and edges from parsed definitions to DB tables.
fn write_cfg(
conn: &Connection,
file_symbols: &HashMap<String, FileSymbols>,
file_symbols: &BTreeMap<String, FileSymbols>,
analysis_files: &HashSet<&str>,
node_id_map: &HashMap<(String, String, u32), i64>,
) -> bool {
Expand Down Expand Up @@ -1882,7 +1882,7 @@ fn write_def_cfg(
/// `makeNodeResolver` logic (prefer same-file match, fall back to global).
fn write_dataflow(
conn: &Connection,
file_symbols: &HashMap<String, FileSymbols>,
file_symbols: &BTreeMap<String, FileSymbols>,
analysis_files: &HashSet<&str>,
) -> bool {
let tx = match conn.unchecked_transaction() {
Expand Down Expand Up @@ -2050,7 +2050,7 @@ mod tests {
use super::*;
use crate::types::{Import, PathAliases};

fn make_import_ctx(file_symbols: &HashMap<String, FileSymbols>) -> ImportEdgeContext {
fn make_import_ctx(file_symbols: &BTreeMap<String, FileSymbols>) -> ImportEdgeContext {
let mut batch_resolved = HashMap::new();
batch_resolved.insert("/repo/driver.js|./service.js".to_string(), "service.js".to_string());
ImportEdgeContext {
Expand Down Expand Up @@ -2089,7 +2089,7 @@ mod tests {
receiver_type_name: None,
});

let mut file_symbols = HashMap::new();
let mut file_symbols = BTreeMap::new();
file_symbols.insert("service.js".to_string(), service);
file_symbols.insert("driver.js".to_string(), driver);
let import_ctx = make_import_ctx(&file_symbols);
Expand Down Expand Up @@ -2120,7 +2120,7 @@ mod tests {
receiver_type_name: Some("Factory".to_string()),
});

let mut file_symbols = HashMap::new();
let mut file_symbols = BTreeMap::new();
file_symbols.insert("factory.js".to_string(), factory);
file_symbols.insert("driver.js".to_string(), driver);
let import_ctx = make_import_ctx(&file_symbols);
Expand Down Expand Up @@ -2151,7 +2151,7 @@ mod tests {
receiver_type_name: None,
});

let mut file_symbols = HashMap::new();
let mut file_symbols = BTreeMap::new();
file_symbols.insert("service.js".to_string(), service);
file_symbols.insert("driver.js".to_string(), driver);
let import_ctx = make_import_ctx(&file_symbols);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::domain::graph::builder::barrel_resolution::{self, BarrelContext, Reex
use crate::domain::graph::resolve;
use crate::types::{FileSymbols, PathAliases};
use rusqlite::Connection;
use std::collections::{HashMap, HashSet};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::path::Path;

/// A resolved reexport entry for a barrel file.
Expand All @@ -28,7 +28,7 @@ pub struct ImportEdgeContext {
/// Set of files that are barrel-only (reexport count >= definition count).
pub barrel_only_files: HashSet<String>,
/// Parsed symbols per relative path.
pub file_symbols: HashMap<String, FileSymbols>,
pub file_symbols: BTreeMap<String, FileSymbols>,
/// Root directory.
pub root_dir: String,
/// Path aliases.
Expand Down Expand Up @@ -601,7 +601,7 @@ mod tests {

#[test]
fn barrel_detection() {
let mut file_symbols = HashMap::new();
let mut file_symbols = BTreeMap::new();
// 1 def, 2 reexports → barrel
file_symbols.insert(
"src/index.ts".to_string(),
Expand Down
22 changes: 11 additions & 11 deletions crates/codegraph-core/src/features/structure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

use crate::types::FileSymbols;
use rusqlite::Connection;
use std::collections::{HashMap, HashSet};
use std::collections::{BTreeMap, HashMap, HashSet};

/// Per-file metrics to upsert into node_metrics.
#[derive(Debug, Clone)]
Expand All @@ -25,7 +25,7 @@ pub struct FileMetrics {

/// Build line count map from parsed file symbols.
pub fn build_line_count_map(
file_symbols: &HashMap<String, FileSymbols>,
file_symbols: &BTreeMap<String, FileSymbols>,
root_dir: &str,
) -> HashMap<String, i64> {
let mut map = HashMap::new();
Expand All @@ -50,7 +50,7 @@ pub fn update_changed_file_metrics(
conn: &Connection,
changed_files: &[String],
line_count_map: &HashMap<String, i64>,
file_symbols: &HashMap<String, FileSymbols>,
file_symbols: &BTreeMap<String, FileSymbols>,
) {
if changed_files.is_empty() {
return;
Expand Down Expand Up @@ -250,7 +250,7 @@ struct ImportEdge {
/// and contains-edge insertion to affected directories only.
pub fn build_full_structure(
conn: &Connection,
file_symbols: &HashMap<String, FileSymbols>,
file_symbols: &BTreeMap<String, FileSymbols>,
discovered_dirs: &HashSet<String>,
root_dir: &str,
line_count_map: &HashMap<String, i64>,
Expand Down Expand Up @@ -408,7 +408,7 @@ fn load_file_paths_in_dirs(conn: &Connection, dirs: &HashSet<String>) -> Vec<Str
fn insert_dir_to_file_contains_edges(
tx: &rusqlite::Transaction,
stmt: &mut rusqlite::Statement,
file_symbols: &HashMap<String, FileSymbols>,
file_symbols: &BTreeMap<String, FileSymbols>,
all_file_paths: &[String],
affected_dirs: Option<&HashSet<String>>,
) {
Expand Down Expand Up @@ -508,7 +508,7 @@ fn restore_unchanged_dir_edges(

fn insert_contains_edges(
conn: &Connection,
file_symbols: &HashMap<String, FileSymbols>,
file_symbols: &BTreeMap<String, FileSymbols>,
all_dirs: &HashSet<String>,
changed_files: Option<&[String]>,
) {
Expand Down Expand Up @@ -595,7 +595,7 @@ fn compute_import_edge_maps(

fn compute_file_metrics(
conn: &Connection,
file_symbols: &HashMap<String, FileSymbols>,
file_symbols: &BTreeMap<String, FileSymbols>,
line_count_map: &HashMap<String, i64>,
fan_in_map: &HashMap<String, i64>,
fan_out_map: &HashMap<String, i64>,
Expand Down Expand Up @@ -718,7 +718,7 @@ fn record_file_in_ancestor_dirs<'a>(
fn build_dir_files_map<'a>(
all_dirs: &'a HashSet<String>,
all_db_files: &'a [String],
file_symbols: &'a HashMap<String, FileSymbols>,
file_symbols: &'a BTreeMap<String, FileSymbols>,
) -> HashMap<&'a str, Vec<&'a str>> {
let mut dir_files: HashMap<&str, Vec<&str>> = HashMap::new();
for dir in all_dirs {
Expand Down Expand Up @@ -831,7 +831,7 @@ fn count_distinct_definitions(sym: &FileSymbols) -> i64 {
fn compute_dir_symbol_counts<'a>(
dir_files: &HashMap<&'a str, Vec<&'a str>>,
db_symbol_counts: &HashMap<String, i64>,
file_symbols: &HashMap<String, FileSymbols>,
file_symbols: &BTreeMap<String, FileSymbols>,
) -> HashMap<&'a str, i64> {
let mut dir_symbol_counts: HashMap<&str, i64> = HashMap::new();
for (dir, files) in dir_files {
Expand Down Expand Up @@ -897,7 +897,7 @@ fn write_directory_metric_rows(

fn compute_directory_metrics(
conn: &Connection,
file_symbols: &HashMap<String, FileSymbols>,
file_symbols: &BTreeMap<String, FileSymbols>,
all_dirs: &HashSet<String>,
import_edges: &[ImportEdge],
) {
Expand All @@ -920,7 +920,7 @@ mod tests {

#[test]
fn line_count_map_from_symbols() {
let mut file_symbols = HashMap::new();
let mut file_symbols = BTreeMap::new();
let mut sym = FileSymbols::new("src/a.ts".to_string());
sym.line_count = Some(42);
file_symbols.insert("src/a.ts".to_string(), sym.clone());
Expand Down
Loading
Loading