From 82fa5fd4d8c184e6d82cbc5b51b6dfceae936d5d Mon Sep 17 00:00:00 2001 From: Steve Pelech Date: Thu, 12 Nov 2026 13:54:06 -0600 Subject: [PATCH] fix(trace-path): use portable case-insensitive symbol lookup When trace_path found no exact symbol, its fallback queries used `COLLATE NOCASE`, which only SQLite has. On PostgreSQL the tool failed with "collation nocase for encoding UTF8 does not exist" for any symbol that was not an exact match (for example a wrong letter case or an unknown name). The fallback now compares LOWER(column) = LOWER(?), which works on both databases. Co-Authored-By: Claude Opus 5.5 --- REQUIREMENTS.md | 6 +++++- app/services/graph_traversal.py | 4 ++-- tests/test_trace_path_case_insensitive.py | 22 ++++++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 tests/test_trace_path_case_insensitive.py diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index bf09531..0bee9bd 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -2,7 +2,7 @@ > **Note:** This document is automatically generated and verified against the live test suite by `scripts/generate_requirements.py` and `tests/backend/test_requirements_sync.py`. -**Test Verification Baseline:** **1182 Automated Tests** (818 Pytest Backend + 314 Vitest Frontend + 50 Playwright E2E). +**Test Verification Baseline:** **1184 Automated Tests** (820 Pytest Backend + 314 Vitest Frontend + 50 Playwright E2E). --- @@ -1394,6 +1394,10 @@ over the PostgresConnectionAdapter using an initialized test engine._ - `test_upsert_reports_failure_when_every_record_is_rejected` - `test_rejected_record_warnings_do_not_log_the_chunk_text` +#### `tests/test_trace_path_case_insensitive.py` (2 tests) +- `test_lowercase_symbol_resolves_to_the_indexed_symbol` +- `test_graph_traversal_sql_has_no_sqlite_only_collation` + #### `tests/test_trace_path_compact.py` (3 tests) - `test_trace_nodes_have_no_permalinks_by_default_but_the_target_keeps_one` - `test_include_permalinks_returns_them_on_every_node` diff --git a/app/services/graph_traversal.py b/app/services/graph_traversal.py index 11ca18b..bbe9119 100644 --- a/app/services/graph_traversal.py +++ b/app/services/graph_traversal.py @@ -83,7 +83,7 @@ def trace_symbol_path( if not symbols_found: nc_query = ( "SELECT id, repo, filepath, name, full_symbol, kind, start_line, end_line, signature " - "FROM ast_symbols WHERE (name = ? COLLATE NOCASE OR full_symbol = ? COLLATE NOCASE OR full_symbol LIKE ? COLLATE NOCASE)" + "FROM ast_symbols WHERE (LOWER(name) = LOWER(?) OR LOWER(full_symbol) = LOWER(?) OR LOWER(full_symbol) LIKE LOWER(?))" + repo_sql ) symbols_found = conn.execute(nc_query, [target_sym, target_sym, f"%.{target_sym}"] + repo_arg).fetchall() @@ -107,7 +107,7 @@ def trace_symbol_path( if not rel_hit: rel_hit = conn.execute( "SELECT source_symbol, target_symbol FROM ast_relationships " - "WHERE (source_symbol = ? COLLATE NOCASE OR target_symbol = ? COLLATE NOCASE)" + "WHERE (LOWER(source_symbol) = LOWER(?) OR LOWER(target_symbol) = LOWER(?))" + repo_sql + " LIMIT 1", [target_sym, target_sym] + repo_arg, ).fetchone() diff --git a/tests/test_trace_path_case_insensitive.py b/tests/test_trace_path_case_insensitive.py new file mode 100644 index 0000000..c68cbe3 --- /dev/null +++ b/tests/test_trace_path_case_insensitive.py @@ -0,0 +1,22 @@ +"""trace_path: a symbol in another letter case resolves, with SQL that PostgreSQL accepts (no SQLite-only COLLATE NOCASE).""" + +import inspect +import json + +import app.services.graph_traversal as graph_traversal +from app.services.graph_traversal import trace_symbol_path + +# Same database setup as the DI caller tests: the method rows and CALLS rows of a small C# graph. +from tests.test_trace_path_di_callers import REPO, di_graph # noqa: F401 (pytest fixture) + + +def test_lowercase_symbol_resolves_to_the_indexed_symbol(di_graph): # noqa: F811 + result = json.loads(trace_symbol_path(symbol="assistantservice.publishinstance", repo=REPO, direction="callers", depth=1)) + assert "error" not in result + assert result["target"]["symbol"] == "AssistantService.PublishInstance" + assert [c["symbol"] for c in result["callers"]] == ["AssistantsController.PublishInstance"] + + +def test_graph_traversal_sql_has_no_sqlite_only_collation(): + # PostgreSQL rejects `COLLATE NOCASE` ("collation nocase for encoding UTF8 does not exist"). + assert "NOCASE" not in inspect.getsource(graph_traversal).upper()