diff --git a/graphify/cache.py b/graphify/cache.py index 3f32b8931..7cd18f91e 100644 --- a/graphify/cache.py +++ b/graphify/cache.py @@ -37,7 +37,7 @@ except Exception: _EXTRACTOR_VERSION = "unknown" # Bump when AST cache-key semantics change independently of the package version. -_AST_CACHE_SCHEMA = 22 # SQL labels drop T-SQL brackets and routine bodies emit `reads_from`/`writes_to` (#4635), so cached `.sql` extractions re-run; prior (21): a CommonJS `const X = require('./m')` binding no longer mints a module-scope node that shadows the class the required file exports, so `new X()` resolves to the required declaration (#4621); prior (20): Obsidian `[[wikilink]]` targets that start with the scan root's own folder names now resolve to the real page (#4559), so cached markdown extractions re-run; prior (19): Dart `typedef` declarations are extracted as aliases (not variables of type `typedef`); C++ Metal/CUDA GPU qualifiers no longer mint sourceless god nodes (#4550); a `.h` header containing `extern "C"` routes to the C++ extractor; prior (18): Kotlin `typealias` (#4553), C# `delegate` (#4552), and Swift `macro` declarations (#4551) are now extracted so references stop minting sourceless stubs; Rust `use` rooted at a `mod` declared in the same scope carries `_rust_use_name` (#4564); prior (17): Python pytest-fixture markers (`_py_fixture`/`_python_fixture_param`, #4332) and module-level class-id collision salting (#2810); TS `this.field` typed from any method incl. static (#4473); Solidity callables carry `metadata.arity` + inherited-call/`super`/modifier raw calls (#4379); Go type-decl and Rust file-item case-distinct node ids (#4444); prior (16): Rust `use` edges carry the imported name as written (`_rust_use_name`, #4439); (15): Go generic receiver types normalized (`Box[T]` -> `Box`) so methods attach to the type node; prior (14): C# receiver resolution for nameless/`new X()`/`foreach` receivers (defer + receiver-chain markers) and `_`/case-distinct node ids (underscore-salted); prior (13): Kotlin/Scala/C++/Swift generic & template type params no longer mint sourceless stubs; Objective-C NS_ENUM/NS_OPTIONS extraction and `_callable`/`_callable_class` markers; Go and PHP receiver-typed member calls park cross-repo (`go_type_table`/`php_type_table`, receiver fields); prior (12): Java inherited-supertype (`receiver_unbound`) and chained/new/cast receiver typing (`java_receiver_chain`, `_java_ret`); C/C++ quoted cross-directory include provenance (`include_kind`); JS external imports anchored to the package, not the path leaf; prior (11): Python property-accessor decorators / `Self`, Swift `Self`, Go generic type params, Python forward type-hint file node, PowerShell dot-source, C# verbatim-interpolated string recovery; (10): C# partial types carry `type_path`; (9): Python property-read raw calls and `_py_property` nodes; (8): Rust `use`-group per-binding edges; (7): Python opaque-base/super(args) markers + nested-class enclosing ids. +_AST_CACHE_SCHEMA = 22 # SQL labels drop T-SQL brackets and routine bodies emit `reads_from`/`writes_to` (#4635), and a `GO` batch separator ends a statement (#4651), so cached `.sql` extractions re-run; prior (21): a CommonJS `const X = require('./m')` binding no longer mints a module-scope node that shadows the class the required file exports, so `new X()` resolves to the required declaration (#4621); prior (20): Obsidian `[[wikilink]]` targets that start with the scan root's own folder names now resolve to the real page (#4559), so cached markdown extractions re-run; prior (19): Dart `typedef` declarations are extracted as aliases (not variables of type `typedef`); C++ Metal/CUDA GPU qualifiers no longer mint sourceless god nodes (#4550); a `.h` header containing `extern "C"` routes to the C++ extractor; prior (18): Kotlin `typealias` (#4553), C# `delegate` (#4552), and Swift `macro` declarations (#4551) are now extracted so references stop minting sourceless stubs; Rust `use` rooted at a `mod` declared in the same scope carries `_rust_use_name` (#4564); prior (17): Python pytest-fixture markers (`_py_fixture`/`_python_fixture_param`, #4332) and module-level class-id collision salting (#2810); TS `this.field` typed from any method incl. static (#4473); Solidity callables carry `metadata.arity` + inherited-call/`super`/modifier raw calls (#4379); Go type-decl and Rust file-item case-distinct node ids (#4444); prior (16): Rust `use` edges carry the imported name as written (`_rust_use_name`, #4439); (15): Go generic receiver types normalized (`Box[T]` -> `Box`) so methods attach to the type node; prior (14): C# receiver resolution for nameless/`new X()`/`foreach` receivers (defer + receiver-chain markers) and `_`/case-distinct node ids (underscore-salted); prior (13): Kotlin/Scala/C++/Swift generic & template type params no longer mint sourceless stubs; Objective-C NS_ENUM/NS_OPTIONS extraction and `_callable`/`_callable_class` markers; Go and PHP receiver-typed member calls park cross-repo (`go_type_table`/`php_type_table`, receiver fields); prior (12): Java inherited-supertype (`receiver_unbound`) and chained/new/cast receiver typing (`java_receiver_chain`, `_java_ret`); C/C++ quoted cross-directory include provenance (`include_kind`); JS external imports anchored to the package, not the path leaf; prior (11): Python property-accessor decorators / `Self`, Swift `Self`, Go generic type params, Python forward type-hint file node, PowerShell dot-source, C# verbatim-interpolated string recovery; (10): C# partial types carry `type_path`; (9): Python property-read raw calls and `_py_property` nodes; (8): Rust `use`-group per-binding edges; (7): Python opaque-base/super(args) markers + nested-class enclosing ids. # Per-process memo: distributions() scans site-packages metadata, which is diff --git a/graphify/extractors/sql.py b/graphify/extractors/sql.py index efb32abb1..c9dac2eae 100644 --- a/graphify/extractors/sql.py +++ b/graphify/extractors/sql.py @@ -348,6 +348,44 @@ def _routine_body_refs(masked: str) -> list[tuple[str, str, int]]: return [(relation, name, pos) for pos, relation, name in sorted(refs)] +# A `GO` batch separator is a client-tool command, not SQL: tree-sitter-sql reads it as an +# identifier, so without a `;` before it the next statement is swallowed into an ERROR node. +# Found on the masked text (comments and strings blanked), alone on its line, optionally +# followed by a repeat count. +_GO_LINE_RX = re.compile(r"^[ \t]*(GO(?:[ \t]+\d+)?)[ \t\r]*;?[ \t\r]*$", re.IGNORECASE | re.MULTILINE) + + +def _go_separators(source: bytes) -> list[tuple[int, int]]: + """Byte spans of the `GO [n]` batch-separator lines in a T-SQL script. + + A bare `go` on its own line is a column or alias, not a separator, only when the last + non-blank character before it is a comma or the first one after it is a comma or `)`. + Any keyword may precede a real separator (`SET NOCOUNT ON` / `GO`), so none is consulted. + """ + try: + text = source.decode("utf-8") + except UnicodeDecodeError: + return [] + masked = _scan_sql(text)[0] + spans: list[tuple[int, int]] = [] + size = len(masked) + char_pos = byte_pos = 0 # a character offset and its byte offset, advanced as matches are met + for m in _GO_LINE_RX.finditer(masked): + before, after = m.start() - 1, m.end() + while before >= 0 and masked[before].isspace(): + before -= 1 + while after < size and masked[after].isspace(): + after += 1 + start, end = m.start(1), m.end(1) + byte_start = byte_pos + len(text[char_pos:start].encode("utf-8")) + byte_end = byte_start + len(text[start:end].encode("utf-8")) + char_pos, byte_pos = end, byte_end + if (before >= 0 and masked[before] == ",") or (after < size and masked[after] in ",)"): + continue + spans.append((byte_start, byte_end)) + return spans + + def _mask_sql_comments(text: str) -> str: """Masked text only — see _scan_sql for the span-reporting form.""" return _scan_sql(text)[0] @@ -422,7 +460,11 @@ def extract_sql(path: Path, content: str | bytes | None = None) -> dict: else content if content is not None else _read_source_bytes(path) ) - tree = parser.parse(source) + # `GO` is not SQL; as `;` (same length, so offsets and lines hold) it ends the statement. + patched = bytearray(source) + for start, end in _go_separators(source): + patched[start:end] = b";" + b" " * (end - start - 1) + tree = parser.parse(bytes(patched)) root = tree.root_node except Exception as e: return {"nodes": [], "edges": [], "error": str(e)}