Skip to content

feat: qualified names, scope, and visibility for node model (3.12)#437

Open
carlos-alm wants to merge 6 commits intomainfrom
feat/qualified-names-scoping
Open

feat: qualified names, scope, and visibility for node model (3.12)#437
carlos-alm wants to merge 6 commits intomainfrom
feat/qualified-names-scoping

Conversation

@carlos-alm
Copy link
Contributor

Summary

  • Migration v15: Adds qualified_name, scope, visibility columns to nodes table with indexes
  • Visibility extraction for all 8 language extractors: JS/TS (accessibility_modifier + # private fields), Java/C#/PHP (modifier AST nodes), Python (naming convention), Go (capitalization), Rust (pub keyword)
  • New repository functions: findNodesByScope(db, scopeName, opts) for "all methods of class X" queries, findNodeByQualifiedName(db, qualifiedName) for direct lookup without edge traversal
  • childrenData() now returns qualifiedName, scope, visibility for parent nodes and children
  • ROADMAP items 3.8 (Domain Errors), 3.9 (Builder Pipeline), 3.11 (Unified Graph Model), 3.12 (Qualified Names) updated to reflect completed status

Test plan

  • New tests/integration/qualified-names.test.js — 8 tests covering qualified_name lookup, scope queries, visibility extraction (WASM engine), and childrenData output
  • All 1712 existing tests pass (0 failures)
  • Lint clean (biome)
  • Validated with codegraph build + codegraph children NodeQuery --json on the codegraph codebase itself

…ADMAP 3.12)

Enrich the node model with hierarchical scoping information so queries
like "all methods of class X" work without edge traversal.

Schema: migration v15 adds qualified_name, scope, visibility columns
with indexes. All 8 language extractors now emit visibility metadata
(JS/TS modifiers, Java/C#/PHP AST nodes, Python/Go/Rust conventions).

New repository functions: findNodesByScope(), findNodeByQualifiedName().
childrenData() now returns qualifiedName, scope, visibility for all nodes.

Also updates ROADMAP items 3.8, 3.9, 3.11, 3.12 to reflect completed status.

Impact: 33 functions changed, 35 affected
@claude
Copy link

claude bot commented Mar 13, 2026

Claude finished @carlos-alm's task —— View job


PR Review: Qualified Names, Scope, and Visibility (3.12) ⭐⭐⭐⭐☆

Review Checklist:

  • Phase 1: Root Cause Analysis
  • Phase 2: Configuration Change Detection
  • Phase 3: Test Integrity
  • Phase 4: Code Quality & Security
  • Phase 5: Backlog Compliance
  • Final Assessment & Recommendation

Phase 1: Root Cause Analysis ✅

Problem Being Solved: This PR implements ROADMAP item 3.12, adding hierarchical scoping and qualified name resolution to the node model. The current system requires edge traversal to query "all methods of class X" or resolve qualified names like "DateHelper.format".

Why This Approach: The PR adds three new columns (qualified_name, scope, visibility) to the nodes table, enabling direct lookups without expensive edge traversals. This is a foundational enhancement that supports future features requiring scoped queries.

Root Cause Justification: This is a planned architectural enhancement, not a bug fix. The approach is sound - adding indexed columns for direct lookup is more efficient than repeated graph traversal.

Phase 2: Configuration Change Detection ✅

No HIGH-RISK changes detected:

  • ✅ No Biome linting rules disabled or relaxed
  • ✅ No test timeout increases or coverage threshold lowering
  • ✅ No TypeScript strict mode changes
  • ✅ No module resolution modifications
  • ✅ No eslint-disable pragmas or workarounds

The only configuration file changes are additions to the ROADMAP.md marking items as completed.

Phase 3: Test Integrity ✅

No test weakening detected:

  • ✅ New comprehensive test file: tests/integration/qualified-names.test.js with 8 focused tests
  • ✅ All tests use proper assertions (expect().toBeDefined(), expect().toBe(), expect().toContain())
  • ✅ Tests cover all three new columns: qualified_name, scope, visibility
  • ✅ Tests validate both WASM engine (visibility) and default engine paths
  • ✅ Tests verify childrenData() API returns new columns correctly
  • ✅ No existing tests modified or weakened - pure addition
  • ✅ Test cleanup properly implemented (tmpDir cleanup, db.close() in finally blocks)

Test Quality: Tests are well-structured with realistic fixtures and comprehensive coverage of the new functionality.

Phase 4: Code Quality & Security ✅

Database Security:

  • ✅ All SQL operations use prepared statements - no injection vulnerabilities
  • batchInsertNodes uses parameterized placeholders: (?,?,?,?,?,?,?,?,?)
  • ✅ Dynamic SQL in findNodesByScope properly parameterized with boundary checks

Code Quality:

  • Schema Changes: Migration v15 properly adds columns with indexes for performance
  • Visibility Extraction: Language-specific visibility detection implemented for all 8 extractors with shared utilities in helpers.js
  • Error Handling: Proper try/catch with fallback defaults (lines src/builder/stages/insert-nodes.js:201-205)
  • Performance: Indexed columns with batch operations, no N+1 queries
  • API Design: New functions follow existing patterns - findNodeByQualifiedName(), findNodesByScope() match naming conventions

Implementation Quality at src/builder/stages/insert-nodes.js:58-72:

  • Qualified name computation: Methods already prefixed as 'Class.method', used directly
  • Scope derivation: Extracts class portion from qualified name (dotIdx logic)
  • Proper null handling for top-level definitions

Phase 5: Backlog Compliance ✅

  • Zero-dep: ✓ — Builds entirely on existing tree-sitter AST and SQLite infrastructure, no new runtime dependencies
  • Foundation-aligned: ✓ — Maintains always-current graph (P1), zero-cost core (P4), embeddable API (P5), optimizes for agent navigation (P8)
  • Problem-fit: 4/5 — Directly reduces token waste by enabling efficient scoped queries without edge traversal. Supports future qualified name resolution for preventing hallucinated dependencies
  • Breaking: No — Purely additive: new DB columns (with indexes), new API functions, extended childrenData() output (backward compatible)
  • Tier: Tier 1 — Zero-dep + foundation-aligned with high problem-fit

Critical Concerns

None identified. This is a well-executed implementation of a planned architectural enhancement.

Final Recommendation

  • Rating: ⭐⭐⭐⭐☆ (4/5 stars)
  • Action: APPROVE
  • Reasoning: High-quality implementation of ROADMAP item 3.12. Comprehensive test coverage, proper security practices, performance-conscious design with indexed lookups, and clean API design. The PR adds significant architectural value by enabling direct scoped queries without breaking existing functionality. Only minor deduction for being foundational (not immediately user-visible) rather than exceptional.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 13, 2026

Greptile Summary

This PR completes milestone 3.12 by enriching the node model with qualified_name, scope, and visibility columns across migration v15, all eight language extractors, the batch-insert pipeline, and two new repository query functions. All critical issues from previous review rounds (dunder classification, dead WeakMap, LIMIT 1 ambiguity, LIKE wildcard escaping, C# private protected ordering, duplicate escapeLike declaration, indexOflastIndexOf for multi-segment scope, and missing migration backfill) have been addressed in the referenced follow-up commits.

Changes of note:

  • Migration v15 adds the three columns and backfills qualified_name = name for existing rows; scope and visibility are left NULL for pre-existing data (requiring a rebuild, as they cannot be derived without re-parsing).
  • batchInsertNodes expanded to 9 columns; insert-nodes.js Phase 1 derives scope via lastIndexOf('.') and Phase 3 computes child qualified_name as parent.child.
  • extractModifierVisibility (Java/C#/PHP), pythonVisibility, goVisibility, and rustVisibility are added to helpers.js and wired into each extractor. The compound-modifier scan over VISIBILITY_KEYWORDS uses Set insertion order (public → private → protected), which is safe because Java/C#/PHP do not permit a single member to hold multiple conflicting visibility modifiers simultaneously.
  • Minor gap (Rust): extractStructFields in rust.js does not call rustVisibility(field) for individual field nodes. Rust fields can each be explicitly pub, so fields will always have null visibility even when the source declares them public. The fix is a one-liner per field (see inline comment).
  • Minor gap (Go): The type_declaration → struct branch in go.js does not call goVisibility(nameNode.text), meaning Go struct types — unlike Go functions and methods — will have null visibility. The same one-liner fix applies (see inline comment).

Confidence Score: 4/5

  • Safe to merge; two minor omissions in Rust field and Go struct type visibility coverage do not affect correctness of the broader feature.
  • All previously flagged critical issues have been resolved. The implementation is logically sound across all eight extractors and the repository layer. The two remaining gaps (Rust struct field visibility and Go struct type visibility) produce null rather than incorrect values, so no existing data is corrupted — they are incremental improvement candidates. Score of 4 rather than 5 reflects these open gaps in the Rust and Go extractors.
  • src/extractors/rust.js (extractStructFields missing rustVisibility call) and src/extractors/go.js (type_declaration struct branch missing goVisibility call).

Important Files Changed

Filename Overview
src/extractors/helpers.js Adds four visibility helpers (pythonVisibility, goVisibility, rustVisibility, extractModifierVisibility) with correct dunder guard, compound modifier handling, and C# private protected fix. All previously reported issues addressed.
src/db/repository/nodes.js Adds findNodesByScope and findNodeByQualifiedName; fixes all prior issues (dead WeakMap removed, returns array, escapeLike with ESCAPE clause, duplicate declaration removed). findNodeChildren expanded to include new columns.
src/db/migrations.js Migration v15 adds qualified_name/scope/visibility columns and correctly backfills qualified_name=name for existing rows. scope and visibility are intentionally left NULL (require re-parse), consistent with the documented requirement to rebuild.
src/builder/stages/insert-nodes.js Phase 1 uses lastIndexOf('.') to compute scope for method definitions; Phase 3 constructs qualified_name as parent.child and sets scope=def.name for children. Logic is correct for all tested scenarios.
src/extractors/javascript.js Adds local extractVisibility function checking accessibility_modifier (TS) and private_property_identifier (#) prefix. Applied consistently to methods and class fields. No issues found.
src/extractors/rust.js rustVisibility added to function_item and struct_item, but extractStructFields and extractEnumVariants do not forward visibility to individual Rust struct fields or trait methods — minor gap in the visibility coverage for Rust.
src/extractors/go.js goVisibility applied to function_declaration and method_declaration. type_declaration (struct types) does not receive visibility — Go exported struct types (e.g. type Foo struct{}) won't have visibility set.
tests/integration/qualified-names.test.js 8 integration tests covering qualified_name lookup, scope queries, kind filters, null scope for top-level functions, Python dunder/private visibility, WASM private # field extraction, and childrenData shape. Comprehensive coverage for the new functionality.

Sequence Diagram

sequenceDiagram
    participant Extractor as Language Extractor<br/>(js/ts/java/cs/php/py/go/rs)
    participant InsertNodes as insert-nodes.js
    participant DB as SQLite nodes table
    participant Repo as Repository<br/>(nodes.js)
    participant API as childrenData /<br/>findNodesByScope /<br/>findNodeByQualifiedName

    Extractor->>InsertNodes: def { name, kind, line, visibility }
    Note over InsertNodes: Phase 1: qualified_name = def.name<br/>scope = def.name.slice(0, lastIndexOf('.'))
    InsertNodes->>DB: INSERT OR IGNORE (name, kind, file, line,<br/>end_line, parent_id, qualified_name, scope, visibility)

    Extractor->>InsertNodes: child { name, kind, line, visibility }
    Note over InsertNodes: Phase 3: qualified_name = def.name + '.' + child.name<br/>scope = def.name
    InsertNodes->>DB: INSERT OR IGNORE (child row with parent_id)

    Note over DB: Migration v15 backfills<br/>qualified_name = name for pre-existing rows

    API->>Repo: findNodesByScope(db, 'DateHelper', { kind: 'method' })
    Repo->>DB: SELECT * WHERE scope = ? AND kind = ?
    DB-->>Repo: [DateHelper.format, DateHelper.constructor, ...]
    Repo-->>API: node[]

    API->>Repo: findNodeByQualifiedName(db, 'DateHelper.format')
    Repo->>DB: SELECT * WHERE qualified_name = ?
    DB-->>Repo: [{ name, scope, visibility, ... }]
    Repo-->>API: node[]
Loading

Comments Outside Diff (2)

  1. src/extractors/rust.js, line 214-227 (link)

    Rust struct field visibility not extracted

    extractStructFields builds field objects without a visibility key. In Rust, struct fields can be individually marked pub, making the field public regardless of the struct's own visibility. Because rustVisibility checks for a visibility_modifier child — the same mechanism used for struct_item and function_item — it would work identically here.

    Without this, a query like findNodesByScope on a struct will return fields where visibility is always null, even when the source code explicitly marks them pub.

  2. src/extractors/go.js, line 71-80 (link)

    Go struct type visibility not extracted

    The type_declaration branch that emits struct nodes does not call goVisibility. Since Go uses the same capitalization convention for all exported identifiers — types included — type Foo struct{} should be 'public' and type foo struct{} should be 'private'.

    Functions and methods in the same file already call goVisibility(nameNode.text), so this is an inconsistency in the otherwise uniform convention-based approach.

Last reviewed commit: 63afc1a

Comment on lines +31 to +35
export function pythonVisibility(name) {
if (name.startsWith('__') && !name.endsWith('__')) return 'private';
if (name.startsWith('_')) return 'protected';
return undefined;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dunder methods incorrectly classified as 'protected'

pythonVisibility falls through to the startsWith('_') guard for all dunder methods (e.g. __init__, __str__, __len__, __repr__, __eq__). Because '__init__'.startsWith('__') && !'__init__'.endsWith('__') evaluates to false, the first branch is skipped, and the second branch '__init__'.startsWith('_') returns true'protected'.

Dunder methods are publicly callable by the Python runtime and by external code (e.g. str(obj) calls __str__, len(obj) calls __len__). Marking them 'protected' misrepresents their actual accessibility and will produce wrong visibility data for every Python class in the graph.

The missing guard is an explicit dunder (double-underscore on both sides) early-exit returning undefined:

Suggested change
export function pythonVisibility(name) {
if (name.startsWith('__') && !name.endsWith('__')) return 'private';
if (name.startsWith('_')) return 'protected';
return undefined;
}
export function pythonVisibility(name) {
if (name.startsWith('__') && name.endsWith('__')) return undefined;
if (name.startsWith('__')) return 'private';
if (name.startsWith('_')) return 'protected';
return undefined;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9e86b57. Added early return for dunder names (__init__, __str__, etc.) before the single-underscore check. Test added to verify dunders return undefined.

Comment on lines +262 to +276
const _findNodesByScopeStmt = new WeakMap();
export function findNodesByScope(db, scopeName, opts = {}) {
let sql = 'SELECT * FROM nodes WHERE scope = ?';
const params = [scopeName];
if (opts.kind) {
sql += ' AND kind = ?';
params.push(opts.kind);
}
if (opts.file) {
sql += ' AND file LIKE ?';
params.push(`%${opts.file}%`);
}
sql += ' ORDER BY file, line';
return db.prepare(sql).all(...params);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_findNodesByScopeStmt WeakMap is declared but never used

_findNodesByScopeStmt is declared on line 262 following the same caching pattern as every other function in this file, but findNodesByScope calls db.prepare(sql).all(...params) directly on every invocation — the WeakMap is never passed to cachedStmt.

Because the SQL shape varies with opts.kind and opts.file, a single cached statement doesn't work here. The dead declaration implies the caching was intended but not wired up, which is misleading. Either remove the WeakMap or consider caching the common (no-opts) variant:

Suggested change
const _findNodesByScopeStmt = new WeakMap();
export function findNodesByScope(db, scopeName, opts = {}) {
let sql = 'SELECT * FROM nodes WHERE scope = ?';
const params = [scopeName];
if (opts.kind) {
sql += ' AND kind = ?';
params.push(opts.kind);
}
if (opts.file) {
sql += ' AND file LIKE ?';
params.push(`%${opts.file}%`);
}
sql += ' ORDER BY file, line';
return db.prepare(sql).all(...params);
}
export function findNodesByScope(db, scopeName, opts = {}) {
let sql = 'SELECT * FROM nodes WHERE scope = ?';
const params = [scopeName];
if (opts.kind) {
sql += ' AND kind = ?';
params.push(opts.kind);
}
if (opts.file) {
sql += ' AND file LIKE ?';
params.push(`%${opts.file}%`);
}
sql += ' ORDER BY file, line';
return db.prepare(sql).all(...params);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9e86b57. Removed the dead _findNodesByScopeStmt WeakMap. findNodesByScope uses dynamic SQL (kind/file filters are optional) so it correctly uses db.prepare() directly.

Comment on lines +284 to +291
const _findNodeByQualifiedNameStmt = new WeakMap();
export function findNodeByQualifiedName(db, qualifiedName) {
return cachedStmt(
_findNodeByQualifiedNameStmt,
db,
'SELECT * FROM nodes WHERE qualified_name = ? LIMIT 1',
).get(qualifiedName);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LIMIT 1 silently hides ambiguous qualified names across files

qualified_name has an index but no UNIQUE constraint, so the same value (e.g. DateHelper.format) can legitimately exist for identically-named classes in different files. LIMIT 1 will silently return whichever row SQLite encounters first with no indication that a second match was discarded.

This matters because the PR description presents findNodeByQualifiedName as a "direct lookup without edge traversal". Callers have no signal that their result may be ambiguous. Consider either:

  • Adding an optional file param (to narrow the result when the caller knows the file), or
  • Returning all matches (an array) and letting callers decide, or
  • Documenting the LIMIT 1 behaviour explicitly in the JSDoc so callers are aware.

At minimum the JSDoc should warn that results are unspecified if qualified_name is not unique across the database.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9e86b57. findNodeByQualifiedName now returns an array of all matches instead of LIMIT 1. Added optional opts.file parameter to narrow results when needed. Tests updated accordingly.

- Fix pythonVisibility dunder check ordering (__init__ no longer misclassified)
- Change findNodeByQualifiedName to return array with optional file filter
- Remove dead _findNodesByScopeStmt WeakMap, restore _findNodeByQualifiedNameStmt
- Update test to match new findNodeByQualifiedName return type

Impact: 2 functions changed, 4 affected
@carlos-alm
Copy link
Contributor Author

@greptileai

Comment on lines +263 to +276
export function findNodesByScope(db, scopeName, opts = {}) {
let sql = 'SELECT * FROM nodes WHERE scope = ?';
const params = [scopeName];
if (opts.kind) {
sql += ' AND kind = ?';
params.push(opts.kind);
}
if (opts.file) {
sql += ' AND file LIKE ?';
params.push(`%${opts.file}%`);
}
sql += ' ORDER BY file, line';
return db.prepare(sql).all(...params);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

opts.file LIKE pattern does not escape SQL wildcards

Both findNodesByScope and findNodeByQualifiedName construct the file filter as `%${opts.file}%`. Any _ or % character that legitimately appears in the file path is treated as a LIKE wildcard, which can produce false matches.

This is particularly likely in practice — underscores are common in file names (e.g. src/date_helper.js would also match src/dateXhelper.js). % is rarer but possible in URL-encoded paths.

The same pattern appears in findNodeByQualifiedName (line 292) and should be addressed in both places. A minimal fix:

// Escape LIKE wildcards in a file path segment
function escapeLike(s) {
  return s.replace(/[%_\\]/g, '\\$&');
}

Then use %${escapeLike(opts.file)}% with ESCAPE '\\' appended to each LIKE clause.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d34915e. Added escapeLike() helper that escapes %, _, and backslash characters. Both findNodesByScope and findNodeByQualifiedName now use escapeLike(opts.file) with ESCAPE clause in their LIKE patterns.

@carlos-alm
Copy link
Contributor Author

@greptileai

Comment on lines +70 to +76
if (modifierTypes.has(child.type)) {
const text = child.text;
if (VISIBILITY_KEYWORDS.has(text)) return text;
// Compound modifiers node (Java: "public static") — scan its text for a keyword
for (const kw of VISIBILITY_KEYWORDS) {
if (text.includes(kw)) return kw;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C# private protected compound modifier returns wrong visibility

extractModifierVisibility checks modifiers with text.includes(kw), iterating keywords in Set insertion order: 'public', 'private', 'protected'. For the valid C# 7.2 compound access modifier private protected, the modifiers/visibility_modifier node text will be "private protected":

  • VISIBILITY_KEYWORDS.has("private protected")false (not an exact match)
  • "private protected".includes('public')false
  • "private protected".includes('private')true → returns 'private'

This is incorrect. private protected grants access to derived classes within the same assembly, making it significantly more permissive than private. Returning 'private' here would cause the graph to mark those members as strictly private, hiding the protected-derived-class relationship.

A simple fix is to guard against the compound case before the generic substring scan:

if (modifierTypes.has(child.type)) {
  const text = child.text;
  if (VISIBILITY_KEYWORDS.has(text)) return text;
  // Handle C# 'private protected' — treat as protected (accessible to derived types)
  if (text === 'private protected') return 'protected';
  // Compound modifiers node (Java: "public static") — scan its text for a keyword
  for (const kw of VISIBILITY_KEYWORDS) {
    if (text.includes(kw)) return kw;
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d34915e. Added explicit guard for 'private protected' before the generic substring scan — returns 'protected' since it grants access to derived types within the same assembly.

…e JSDoc

- Add escapeLike() helper to escape SQL LIKE wildcards (%, _, \) in
  opts.file parameters for findNodesByScope and findNodeByQualifiedName
- Guard C# 'private protected' compound modifier before substring scan
  so it returns 'protected' instead of incorrectly matching 'private'
- Update batchInsertNodes JSDoc to reflect the 9-column row format

Impact: 4 functions changed, 11 affected
@carlos-alm
Copy link
Contributor Author

@greptileai

Comment on lines +268 to +270
function escapeLike(s) {
return s.replace(/[%_\\]/g, '\\$&');
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate escapeLike function — SyntaxError in ESM

escapeLike is declared twice in the same module scope: once at line 254 (directly after findNodeChildren) and again here at line 268 (inside the JSDoc block for findNodesByScope). JavaScript ES modules always run in strict mode, and duplicate function declarations in strict mode are a static SyntaxError — the module will fail to parse and any import of nodes.js (including src/db.js and all callers) will throw at startup, breaking the entire application.

The second declaration is a copy-paste artefact from the earlier escapeLike fix: the first copy was added to address the escaping concern, and then the JSDoc + function body for findNodesByScope was re-inserted alongside it.

Remove the duplicate at line 268–270:

Suggested change
function escapeLike(s) {
return s.replace(/[%_\\]/g, '\\$&');
}
export function findNodesByScope(db, scopeName, opts = {}) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 65a15e9. Removed the duplicate escapeLike() declaration — copy-paste artefact from the previous fix.

Comment on lines +60 to +61
const dotIdx = def.name.indexOf('.');
const scope = dotIdx !== -1 ? def.name.slice(0, dotIdx) : null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indexOf truncates scope for multi-segment names

def.name.indexOf('.') finds the first dot, so for any hypothetical name with more than one segment (e.g. a future extractor emitting "Outer.Inner.method") the scope would be set to "Outer" instead of "Outer.Inner". findNodesByScope(db, 'Inner') would then find nothing for such methods.

The scope should be derived from everything except the last segment, matching the caller expectation that scope is the direct parent name. Use lastIndexOf to be consistent:

Suggested change
const dotIdx = def.name.indexOf('.');
const scope = dotIdx !== -1 ? def.name.slice(0, dotIdx) : null;
const dotIdx = def.name.lastIndexOf('.');
const scope = dotIdx !== -1 ? def.name.slice(0, dotIdx) : null;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 65a15e9. Changed indexOf to lastIndexOf so multi-segment names like Outer.Inner.method correctly derive scope as Outer.Inner instead of truncating at the first dot.

- Remove duplicate escapeLike() declaration that would cause a
  SyntaxError in strict mode ESM
- Use lastIndexOf('.') instead of indexOf('.') for scope derivation
  so multi-segment names like "Outer.Inner.method" correctly produce
  scope "Outer.Inner" instead of truncating to "Outer"

Impact: 1 functions changed, 0 affected
@carlos-alm
Copy link
Contributor Author

@greptileai

Comment on lines +232 to +241
{
version: 15,
up: `
ALTER TABLE nodes ADD COLUMN qualified_name TEXT;
ALTER TABLE nodes ADD COLUMN scope TEXT;
ALTER TABLE nodes ADD COLUMN visibility TEXT;
CREATE INDEX IF NOT EXISTS idx_nodes_qualified_name ON nodes(qualified_name);
CREATE INDEX IF NOT EXISTS idx_nodes_scope ON nodes(scope);
`,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Migration doesn't backfill qualified_name for existing databases

Migration v15 adds qualified_name, scope, and visibility columns as nullable, but existing rows get NULL for all three. Because findNodeByQualifiedName queries the qualified_name column directly (not name), any existing database upgraded from ≤ v14 will silently return zero results from the new lookup functions until the user runs a full rebuild — even though every node already has a valid name that is identical to what qualified_name would hold.

qualified_name is always equal to name at insert time (def.name is used verbatim), so this can be backfilled cheaply and safely in the migration itself:

ALTER TABLE nodes ADD COLUMN qualified_name TEXT;
ALTER TABLE nodes ADD COLUMN scope TEXT;
ALTER TABLE nodes ADD COLUMN visibility TEXT;
-- Backfill qualified_name from name (always identical at insert time)
UPDATE nodes SET qualified_name = name WHERE qualified_name IS NULL;
-- Backfill scope from name for dotted entries (extracts 'A.B' → 'A' for single-level names)
-- Full scope accuracy requires a rebuild, but the UPDATE covers the common case.
CREATE INDEX IF NOT EXISTS idx_nodes_qualified_name ON nodes(qualified_name);
CREATE INDEX IF NOT EXISTS idx_nodes_scope ON nodes(scope);

The same UPDATE nodes SET qualified_name = name should also be added to the initSchema fallback block (around line 323) so databases that are patched via the ALTER TABLE path also get backfilled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 63afc1a. Migration v15 now runs UPDATE nodes SET qualified_name = name WHERE qualified_name IS NULL after adding the columns. The initSchema fallback path also backfills so databases patched via ALTER TABLE get the same treatment.

Existing databases upgraded from ≤v14 would have NULL qualified_name
for all rows, causing findNodeByQualifiedName to return zero results
until a full rebuild. Backfill in both migration v15 and initSchema
fallback path.

Impact: 1 functions changed, 0 affected
@carlos-alm
Copy link
Contributor Author

@greptileai

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant