fix(sql): end the statement at GO batch separators

GO is a client-tool command, not SQL, and tree-sitter-sql has no rule
for it. Before parsing, each GO [n] line found on the masked text
(comments and strings blanked) becomes `;` plus spaces, so byte
offsets and line numbers don't move. A go line after a comma, or
before a comma or `)`, is a column and stays.

AST cache schema 20 -> 21 so cached .sql extractions re-run.

(cherry picked from commit 2293c31d79)
This commit is contained in:
Deepjyoti-Sarmah authored and safishamsi committed 2026-10-22 13:49:33 +01:00
1 parent 01715accd5
commit 9ce1404719
2 files changed
+44 -2

No files matched your search

+1 -1
View File
@@ -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
+43 -1
View File
@@ -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)}