@backbencher/brain 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/CHANGELOG.md +298 -0
  2. package/EMBEDDINGS.md +355 -0
  3. package/LICENSE +21 -0
  4. package/README.md +415 -2
  5. package/bin/bb +13 -0
  6. package/dist/analysis-exclusions-7ZJDXOVZ.js +35 -0
  7. package/dist/brain-index-health-Q5APTJQL.js +17 -0
  8. package/dist/brain-relationship-enrichment-MYR2AM4F.js +558 -0
  9. package/dist/brain.sql-EIM4WS4D.js +29 -0
  10. package/dist/chunk-22V37YTK.js +1758 -0
  11. package/dist/chunk-2BSKD7O3.js +40 -0
  12. package/dist/chunk-2DE27DVC.js +1314 -0
  13. package/dist/chunk-2RRBMNBQ.js +322 -0
  14. package/dist/chunk-2WXYNLBA.js +29 -0
  15. package/dist/chunk-3CTFIJJB.js +17 -0
  16. package/dist/chunk-3ZTOHHSW.js +2104 -0
  17. package/dist/chunk-4H22TLOT.js +543 -0
  18. package/dist/chunk-5BVAH26D.js +340 -0
  19. package/dist/chunk-62DHLYW6.js +3427 -0
  20. package/dist/chunk-7AN7U5IZ.js +371 -0
  21. package/dist/chunk-7DRPNVIN.js +782 -0
  22. package/dist/chunk-7MBYT777.js +174 -0
  23. package/dist/chunk-AHDWGM7Q.js +236 -0
  24. package/dist/chunk-B5GEAAYF.js +818 -0
  25. package/dist/chunk-BEIZZ73B.js +1479 -0
  26. package/dist/chunk-BQ3OVQ3P.js +125 -0
  27. package/dist/chunk-D2Q4Y47A.js +53 -0
  28. package/dist/chunk-DIYAGQJL.js +34 -0
  29. package/dist/chunk-DWKXZ6IF.js +336 -0
  30. package/dist/chunk-DYM4FQEE.js +90 -0
  31. package/dist/chunk-ENW4BTBA.js +930 -0
  32. package/dist/chunk-EWMWK7WN.js +75 -0
  33. package/dist/chunk-EY5ZZUUA.js +626 -0
  34. package/dist/chunk-GBI7V7KY.js +69 -0
  35. package/dist/chunk-GEFFO6O2.js +152 -0
  36. package/dist/chunk-HJEWO5EV.js +247 -0
  37. package/dist/chunk-HQSPJDHI.js +613 -0
  38. package/dist/chunk-KAET47ZL.js +125 -0
  39. package/dist/chunk-KCDECRJY.js +440 -0
  40. package/dist/chunk-L5TDYKYK.js +76 -0
  41. package/dist/chunk-MLKGABMK.js +9 -0
  42. package/dist/chunk-NO3U3FJQ.js +201 -0
  43. package/dist/chunk-OWISBAOF.js +61 -0
  44. package/dist/chunk-QKTRECYS.js +1119 -0
  45. package/dist/chunk-QLK6ED4Q.js +235 -0
  46. package/dist/chunk-R2GW4F6R.js +19 -0
  47. package/dist/chunk-RCMTFSEF.js +87 -0
  48. package/dist/chunk-RM3PY6TG.js +376 -0
  49. package/dist/chunk-ROMM6KMC.js +433 -0
  50. package/dist/chunk-SGGCSHAX.js +12050 -0
  51. package/dist/chunk-SK6YVHFZ.js +211 -0
  52. package/dist/chunk-SM7RFQRO.js +214 -0
  53. package/dist/chunk-UGR4QIGS.js +67 -0
  54. package/dist/chunk-UQNQZXHY.js +711 -0
  55. package/dist/chunk-UUNHHX4M.js +13 -0
  56. package/dist/chunk-VL52PL2T.js +71 -0
  57. package/dist/chunk-WIG36E67.js +196 -0
  58. package/dist/chunk-XHK3WWGB.js +7436 -0
  59. package/dist/chunk-XTQM2IP3.js +2863 -0
  60. package/dist/chunk-ZE2LG76D.js +1296 -0
  61. package/dist/chunk-ZETTGPAW.js +6950 -0
  62. package/dist/cli/index.js +37 -0
  63. package/dist/config-HZAAYDP3.js +10 -0
  64. package/dist/content-search-7XIP555Q.js +593 -0
  65. package/dist/database-76RP44JG.js +9 -0
  66. package/dist/doctor-ANGETYMN.js +395 -0
  67. package/dist/edit-engine-CG72LDZD.js +51 -0
  68. package/dist/engine-BKEOGN7N.js +11 -0
  69. package/dist/engine-VIW6NSKO.js +18 -0
  70. package/dist/fingerprint-OBLIMZLP.js +33 -0
  71. package/dist/framework-intelligence-3LORTPOX.js +860 -0
  72. package/dist/freshness-3K6VU5OT.js +174 -0
  73. package/dist/graph-engine-HBSQNPTB.js +21 -0
  74. package/dist/graph_request-Q6NKB6N2.js +350 -0
  75. package/dist/impact_request-LYP672WE.js +68 -0
  76. package/dist/index-space-IUUN5WYV.js +15 -0
  77. package/dist/index-status-7OVGYSGW.js +34 -0
  78. package/dist/index.js +159 -0
  79. package/dist/instance-FO6QZRLH.js +19 -0
  80. package/dist/language-detection-DFCAU5DZ.js +11 -0
  81. package/dist/lsp/server.js +4301 -0
  82. package/dist/mcp/server.js +132 -0
  83. package/dist/onboard-DIQZV2NW.js +161 -0
  84. package/dist/override-companion-pass-XR7MHTP7.js +117 -0
  85. package/dist/parallel-indexer-FK2XKZXY.js +88 -0
  86. package/dist/pool-worker.js +1153 -0
  87. package/dist/project-health-6XPENVAA.js +94 -0
  88. package/dist/project-index-5SDPHITE.js +57 -0
  89. package/dist/project-registry-ZJ3DVUGO.js +355 -0
  90. package/dist/real-bash-analyzer-EJAU5LW5.js +2201 -0
  91. package/dist/real-c-analyzer-HBD3FNNH.js +5942 -0
  92. package/dist/real-clean-analyzer-4EEZLPV5.js +3470 -0
  93. package/dist/real-clojure-analyzer-Q55EBOO2.js +2818 -0
  94. package/dist/real-cpp-analyzer-IVVOD25R.js +4101 -0
  95. package/dist/real-csharp-analyzer-ZBNLNJZJ.js +3298 -0
  96. package/dist/real-css-analyzer-6XRQLUN7.js +15 -0
  97. package/dist/real-dart-analyzer-OLR72EFW.js +3944 -0
  98. package/dist/real-elixir-analyzer-MQWGAH2I.js +2192 -0
  99. package/dist/real-erlang-analyzer-7BVPQEHO.js +1731 -0
  100. package/dist/real-fsharp-analyzer-C2UPFNOA.js +2390 -0
  101. package/dist/real-go-analyzer-UKGI43YS.js +4126 -0
  102. package/dist/real-graphql-analyzer-VE2EEFII.js +1442 -0
  103. package/dist/real-groovy-analyzer-W26VACFW.js +2285 -0
  104. package/dist/real-haskell-analyzer-5CVVAJRX.js +2533 -0
  105. package/dist/real-hcl-analyzer-5ZT4U43N.js +725 -0
  106. package/dist/real-html-analyzer-JWNX3C6I.js +1759 -0
  107. package/dist/real-java-analyzer-GU42VBGQ.js +4677 -0
  108. package/dist/real-javascript-analyzer-QA4QOJ6W.js +14 -0
  109. package/dist/real-json-analyzer-MPA6PRXL.js +1640 -0
  110. package/dist/real-julia-analyzer-22CWYQMN.js +2085 -0
  111. package/dist/real-kotlin-analyzer-QDZJLXXU.js +3956 -0
  112. package/dist/real-lua-analyzer-G7NHLYKS.js +1957 -0
  113. package/dist/real-makefile-analyzer-OCY3GT5N.js +920 -0
  114. package/dist/real-markdown-analyzer-TEJK2JXJ.js +1215 -0
  115. package/dist/real-matlab-analyzer-Q5SWLN7Z.js +480 -0
  116. package/dist/real-nim-analyzer-YLXSK563.js +3603 -0
  117. package/dist/real-objective-c-analyzer-D23ACC2R.js +1518 -0
  118. package/dist/real-perl-analyzer-WWTRHYDT.js +1865 -0
  119. package/dist/real-php-analyzer-JNKJHTCL.js +9368 -0
  120. package/dist/real-powershell-analyzer-XRSNQERV.js +2915 -0
  121. package/dist/real-protobuf-analyzer-3OKUCZ7T.js +867 -0
  122. package/dist/real-python-analyzer-VXAFWI72.js +4880 -0
  123. package/dist/real-r-analyzer-ZGAIDVI4.js +2596 -0
  124. package/dist/real-ruby-analyzer-X2KTLVBH.js +2429 -0
  125. package/dist/real-rust-analyzer-OFFHPL2A.js +3359 -0
  126. package/dist/real-scala-analyzer-TSTW27J6.js +3293 -0
  127. package/dist/real-scss-analyzer-AVWYPSOQ.js +1515 -0
  128. package/dist/real-solidity-analyzer-UCI2KWUB.js +2608 -0
  129. package/dist/real-sql-analyzer-F77ZMQXR.js +3840 -0
  130. package/dist/real-svelte-analyzer-SF7NRUO5.js +2532 -0
  131. package/dist/real-swift-analyzer-MN5QT3QN.js +2311 -0
  132. package/dist/real-toml-analyzer-66L7BDCQ.js +929 -0
  133. package/dist/real-typescript-analyzer-6KEMVDWQ.js +14 -0
  134. package/dist/real-vb-analyzer-VICVPOP7.js +3145 -0
  135. package/dist/real-vba-analyzer-HXLTTHKM.js +466 -0
  136. package/dist/real-vue-analyzer-QXR56CFV.js +2680 -0
  137. package/dist/real-xml-analyzer-YL465ZJW.js +2405 -0
  138. package/dist/real-yaml-analyzer-SUBCBNSI.js +3528 -0
  139. package/dist/real-zig-analyzer-HIQLZMGY.js +2430 -0
  140. package/dist/regex-search-NJKHMZNO.js +239 -0
  141. package/dist/rules-ACHPB7QU.js +152 -0
  142. package/dist/scoped-path-MJYWJZQX.js +11 -0
  143. package/dist/setup-show-NT5ZHYNE.js +15 -0
  144. package/dist/setup-wizard-G5YYR5T6.js +24 -0
  145. package/dist/symbol-search-GZJIM2J2.js +16 -0
  146. package/dist/taint-KXUHHPIE.js +202 -0
  147. package/dist/taint-analyzer-N7YLZU3N.js +163 -0
  148. package/dist/tool-handlers-OJDC35DI.js +72 -0
  149. package/dist/tool-registry-BPEJNSZG.js +17 -0
  150. package/dist/tool-validation-Q57YMMBX.js +32 -0
  151. package/dist/type-inference-QMT2RMLV.js +13 -0
  152. package/dist/vector-binary-LMY4QGAN.js +19 -0
  153. package/dist/version-ABP2TUL2.js +9 -0
  154. package/dist/wasm-NMWO3WXY.js +16 -0
  155. package/docs/EMBEDDINGS.md +132 -0
  156. package/docs/README.md +178 -0
  157. package/docs/data/m10-baseline.json +602 -0
  158. package/docs/data/m10-benchmark-baseline.json +57 -0
  159. package/docs/data/m10-benchmark-last-run.json +127 -0
  160. package/docs/data/m10-final-audit.json +215 -0
  161. package/docs/data/m12-final-audit.json +138 -0
  162. package/docs/data/m12-surface-matrix.json +50 -0
  163. package/docs/data/public-tools.json +1902 -0
  164. package/docs/indexing/DEAD-ASSETS.md +88 -0
  165. package/docs/indexing/INDEXER-REVIEW.md +634 -0
  166. package/docs/indexing/PERFORMANCE-FINDINGS.md +106 -0
  167. package/docs/indexing/README.md +427 -0
  168. package/docs/indexing/discovery-matrix.md +41 -0
  169. package/docs/indexing/embeddings.md +325 -0
  170. package/docs/indexing/environment-variables.md +77 -0
  171. package/docs/indexing/performance.md +323 -0
  172. package/docs/indexing/resume.md +345 -0
  173. package/docs/indexing/troubleshooting.md +391 -0
  174. package/docs/man/bb.1 +190 -0
  175. package/docs/tools/bb_analyze.md +147 -0
  176. package/docs/tools/bb_annotate.md +110 -0
  177. package/docs/tools/bb_annotations.md +106 -0
  178. package/docs/tools/bb_ask.md +101 -0
  179. package/docs/tools/bb_cross_language.md +103 -0
  180. package/docs/tools/bb_doc.md +169 -0
  181. package/docs/tools/bb_edit.md +185 -0
  182. package/docs/tools/bb_graph.md +169 -0
  183. package/docs/tools/bb_health.md +156 -0
  184. package/docs/tools/bb_impact.md +117 -0
  185. package/docs/tools/bb_index.md +108 -0
  186. package/docs/tools/bb_migrate.md +135 -0
  187. package/docs/tools/bb_patterns.md +140 -0
  188. package/docs/tools/bb_search.md +150 -0
  189. package/docs/tools/bb_security.md +100 -0
  190. package/docs/tools/bb_visualize.md +184 -0
  191. package/grammars/README.md +57 -0
  192. package/grammars/THIRD-PARTY-NOTICES.md +50 -0
  193. package/grammars/tree-sitter-c/LICENSE +21 -0
  194. package/grammars/tree-sitter-clojure/LICENSE.md +21 -0
  195. package/grammars/tree-sitter-cpp/LICENSE +21 -0
  196. package/grammars/tree-sitter-csharp/LICENSE +21 -0
  197. package/grammars/tree-sitter-css/LICENSE +21 -0
  198. package/grammars/tree-sitter-css/README.md +21 -0
  199. package/grammars/tree-sitter-dart/LICENSE +20 -0
  200. package/grammars/tree-sitter-dart-upstream/LICENSE +20 -0
  201. package/grammars/tree-sitter-dart-upstream/README.md +18 -0
  202. package/grammars/tree-sitter-elixir/LICENSE +176 -0
  203. package/grammars/tree-sitter-elixir/README.md +11 -0
  204. package/grammars/tree-sitter-fsharp/LICENSE +21 -0
  205. package/grammars/tree-sitter-fsharp/README.md +45 -0
  206. package/grammars/tree-sitter-graphql/LICENSE +21 -0
  207. package/grammars/tree-sitter-graphql/README.md +106 -0
  208. package/grammars/tree-sitter-groovy/LICENSE +21 -0
  209. package/grammars/tree-sitter-groovy/README.md +4 -0
  210. package/grammars/tree-sitter-haskell/LICENSE +21 -0
  211. package/grammars/tree-sitter-haskell/README.md +394 -0
  212. package/grammars/tree-sitter-hcl/CHANGELOG.md +102 -0
  213. package/grammars/tree-sitter-hcl/LICENSE +201 -0
  214. package/grammars/tree-sitter-hcl/README.md +39 -0
  215. package/grammars/tree-sitter-html/LICENSE +21 -0
  216. package/grammars/tree-sitter-html/README.md +21 -0
  217. package/grammars/tree-sitter-julia/LICENSE +21 -0
  218. package/grammars/tree-sitter-julia/README.md +24 -0
  219. package/grammars/tree-sitter-kotlin/LICENSE +9 -0
  220. package/grammars/tree-sitter-lua/LICENSE.md +21 -0
  221. package/grammars/tree-sitter-lua/README.md +29 -0
  222. package/grammars/tree-sitter-make/LICENSE +21 -0
  223. package/grammars/tree-sitter-matlab/LICENSE +19 -0
  224. package/grammars/tree-sitter-matlab/README.md +99 -0
  225. package/grammars/tree-sitter-nim/LICENSE +373 -0
  226. package/grammars/tree-sitter-nim/README.md +46 -0
  227. package/grammars/tree-sitter-objc/LICENSE +21 -0
  228. package/grammars/tree-sitter-objc/README.md +20 -0
  229. package/grammars/tree-sitter-perl/LICENSE +7 -0
  230. package/grammars/tree-sitter-perl/README.md +100 -0
  231. package/grammars/tree-sitter-powershell/LICENSE +21 -0
  232. package/grammars/tree-sitter-powershell/README.md +7 -0
  233. package/grammars/tree-sitter-proto/LICENSE +21 -0
  234. package/grammars/tree-sitter-proto/README.md +105 -0
  235. package/grammars/tree-sitter-r/LICENSE +9 -0
  236. package/grammars/tree-sitter-r/README.md +66 -0
  237. package/grammars/tree-sitter-ruby/LICENSE +22 -0
  238. package/grammars/tree-sitter-scss/LICENSE +21 -0
  239. package/grammars/tree-sitter-scss/README.md +17 -0
  240. package/grammars/tree-sitter-solidity/LICENSE +7 -0
  241. package/grammars/tree-sitter-sql/LICENSE +21 -0
  242. package/grammars/tree-sitter-typescript/LICENSE +21 -0
  243. package/grammars/tree-sitter-vba/LICENSE +21 -0
  244. package/grammars/tree-sitter-vba/README.md +738 -0
  245. package/grammars/tree-sitter-vbnet/LICENSE +21 -0
  246. package/grammars/tree-sitter-vbnet/README.md +322 -0
  247. package/grammars/tree-sitter-xml/LICENSE +19 -0
  248. package/grammars/tree-sitter-yaml/LICENSE +20 -0
  249. package/grammars/tree-sitter-yaml/README.md +21 -0
  250. package/package.json +184 -4
  251. package/wasm/dart.wasm +0 -0
  252. package/wasm/tree-sitter-bash.wasm +0 -0
  253. package/wasm/tree-sitter-c.wasm +0 -0
  254. package/wasm/tree-sitter-c_sharp.wasm +0 -0
  255. package/wasm/tree-sitter-clean.wasm +0 -0
  256. package/wasm/tree-sitter-clojure.wasm +0 -0
  257. package/wasm/tree-sitter-core_schema.wasm +0 -0
  258. package/wasm/tree-sitter-cpp.wasm +0 -0
  259. package/wasm/tree-sitter-css.wasm +0 -0
  260. package/wasm/tree-sitter-dart.wasm +0 -0
  261. package/wasm/tree-sitter-elisp.wasm +0 -0
  262. package/wasm/tree-sitter-elixir.wasm +0 -0
  263. package/wasm/tree-sitter-elm.wasm +0 -0
  264. package/wasm/tree-sitter-embedded_template.wasm +0 -0
  265. package/wasm/tree-sitter-erlang.wasm +0 -0
  266. package/wasm/tree-sitter-fsharp.wasm +0 -0
  267. package/wasm/tree-sitter-fsharp_signature.wasm +0 -0
  268. package/wasm/tree-sitter-go.wasm +0 -0
  269. package/wasm/tree-sitter-graphql.wasm +0 -0
  270. package/wasm/tree-sitter-groovy.wasm +0 -0
  271. package/wasm/tree-sitter-haskell.wasm +0 -0
  272. package/wasm/tree-sitter-hcl.wasm +0 -0
  273. package/wasm/tree-sitter-html.wasm +0 -0
  274. package/wasm/tree-sitter-java.wasm +0 -0
  275. package/wasm/tree-sitter-javascript.wasm +0 -0
  276. package/wasm/tree-sitter-json.wasm +0 -0
  277. package/wasm/tree-sitter-json_schema.wasm +0 -0
  278. package/wasm/tree-sitter-julia.wasm +0 -0
  279. package/wasm/tree-sitter-kotlin.wasm +0 -0
  280. package/wasm/tree-sitter-lua.wasm +0 -0
  281. package/wasm/tree-sitter-make.wasm +0 -0
  282. package/wasm/tree-sitter-markdown.wasm +0 -0
  283. package/wasm/tree-sitter-markdown_inline.wasm +0 -0
  284. package/wasm/tree-sitter-matlab.wasm +0 -0
  285. package/wasm/tree-sitter-nim.wasm +0 -0
  286. package/wasm/tree-sitter-objc.wasm +0 -0
  287. package/wasm/tree-sitter-ocaml.wasm +0 -0
  288. package/wasm/tree-sitter-perl.wasm +0 -0
  289. package/wasm/tree-sitter-php.wasm +0 -0
  290. package/wasm/tree-sitter-php_only.wasm +0 -0
  291. package/wasm/tree-sitter-powershell.wasm +0 -0
  292. package/wasm/tree-sitter-protobuf.wasm +0 -0
  293. package/wasm/tree-sitter-python.wasm +0 -0
  294. package/wasm/tree-sitter-ql.wasm +0 -0
  295. package/wasm/tree-sitter-r.wasm +0 -0
  296. package/wasm/tree-sitter-rescript.wasm +0 -0
  297. package/wasm/tree-sitter-ruby.wasm +0 -0
  298. package/wasm/tree-sitter-rust.wasm +0 -0
  299. package/wasm/tree-sitter-scala.wasm +0 -0
  300. package/wasm/tree-sitter-scss.wasm +0 -0
  301. package/wasm/tree-sitter-solidity.wasm +0 -0
  302. package/wasm/tree-sitter-sql.wasm +0 -0
  303. package/wasm/tree-sitter-svelte.wasm +0 -0
  304. package/wasm/tree-sitter-swift.wasm +0 -0
  305. package/wasm/tree-sitter-systemrdl.wasm +0 -0
  306. package/wasm/tree-sitter-terraform.wasm +0 -0
  307. package/wasm/tree-sitter-tlaplus.wasm +0 -0
  308. package/wasm/tree-sitter-toml.wasm +0 -0
  309. package/wasm/tree-sitter-tsx.wasm +0 -0
  310. package/wasm/tree-sitter-typescript.wasm +0 -0
  311. package/wasm/tree-sitter-vba.wasm +0 -0
  312. package/wasm/tree-sitter-vbnet.wasm +0 -0
  313. package/wasm/tree-sitter-vue.wasm +0 -0
  314. package/wasm/tree-sitter-xml.wasm +0 -0
  315. package/wasm/tree-sitter-yaml.wasm +0 -0
  316. package/wasm/tree-sitter-zig.wasm +0 -0
  317. package/wasm/tree-sitter.wasm +0 -0
  318. package/wasm/tsx.wasm +0 -0
  319. package/wasm/typescript.wasm +0 -0
@@ -0,0 +1,3840 @@
1
+ import {
2
+ ASTEngine
3
+ } from "./chunk-3ZTOHHSW.js";
4
+ import "./chunk-BQ3OVQ3P.js";
5
+ import "./chunk-SK6YVHFZ.js";
6
+ import "./chunk-2BSKD7O3.js";
7
+ import "./chunk-KAET47ZL.js";
8
+ import "./chunk-KCDECRJY.js";
9
+ import "./chunk-MLKGABMK.js";
10
+
11
+ // src/ast/real-sql-analyzer.ts
12
+ var IDENTITY = { byteOffset: 0, rowOffset: 0, columnOffset: 0 };
13
+ var SQL_KEYWORDS = /* @__PURE__ */ new Set([
14
+ "all",
15
+ "alter",
16
+ "analyze",
17
+ "and",
18
+ "any",
19
+ "array",
20
+ "as",
21
+ "asc",
22
+ "at",
23
+ "authorization",
24
+ "begin",
25
+ "between",
26
+ "both",
27
+ "by",
28
+ "cascade",
29
+ "case",
30
+ "cast",
31
+ "check",
32
+ "collate",
33
+ "column",
34
+ "comment",
35
+ "commit",
36
+ "concurrently",
37
+ "conflict",
38
+ "constraint",
39
+ "copy",
40
+ "create",
41
+ "cross",
42
+ "cube",
43
+ "current",
44
+ "current_date",
45
+ "current_role",
46
+ "current_time",
47
+ "current_timestamp",
48
+ "current_user",
49
+ "database",
50
+ "default",
51
+ "deferrable",
52
+ "deferred",
53
+ "delete",
54
+ "desc",
55
+ "distinct",
56
+ "do",
57
+ "drop",
58
+ "else",
59
+ "end",
60
+ "except",
61
+ "execute",
62
+ "exists",
63
+ "explain",
64
+ "false",
65
+ "fetch",
66
+ "filter",
67
+ "first",
68
+ "for",
69
+ "foreign",
70
+ "from",
71
+ "full",
72
+ "function",
73
+ "generated",
74
+ "global",
75
+ "grant",
76
+ "group",
77
+ "grouping",
78
+ "having",
79
+ "if",
80
+ "ilike",
81
+ "in",
82
+ "index",
83
+ "initially",
84
+ "inner",
85
+ "inout",
86
+ "insert",
87
+ "intersect",
88
+ "into",
89
+ "is",
90
+ "join",
91
+ "key",
92
+ "language",
93
+ "last",
94
+ "lateral",
95
+ "leading",
96
+ "left",
97
+ "like",
98
+ "limit",
99
+ "local",
100
+ "localtime",
101
+ "localtimestamp",
102
+ "natural",
103
+ "not",
104
+ "nothing",
105
+ "null",
106
+ "nulls",
107
+ "offset",
108
+ "on",
109
+ "only",
110
+ "or",
111
+ "order",
112
+ "out",
113
+ "outer",
114
+ "over",
115
+ "overlaps",
116
+ "partition",
117
+ "preceding",
118
+ "precision",
119
+ "primary",
120
+ "procedure",
121
+ "range",
122
+ "recursive",
123
+ "references",
124
+ "rename",
125
+ "replace",
126
+ "reset",
127
+ "restrict",
128
+ "returning",
129
+ "revoke",
130
+ "right",
131
+ "rollback",
132
+ "row",
133
+ "rows",
134
+ "savepoint",
135
+ "schema",
136
+ "select",
137
+ "session_user",
138
+ "set",
139
+ "show",
140
+ "similar",
141
+ "some",
142
+ "start",
143
+ "table",
144
+ "tablesample",
145
+ "then",
146
+ "to",
147
+ "trailing",
148
+ "transaction",
149
+ "trigger",
150
+ "true",
151
+ "truncate",
152
+ "union",
153
+ "unique",
154
+ "unknown",
155
+ "unlogged",
156
+ "update",
157
+ "using",
158
+ "vacuum",
159
+ "values",
160
+ "view",
161
+ "when",
162
+ "where",
163
+ "window",
164
+ "with"
165
+ ]);
166
+ function isPostgresCatalogName(t) {
167
+ if (t.length <= 3) return false;
168
+ if (!t.startsWith("pg_")) return false;
169
+ for (let i = 3; i < t.length; i++) {
170
+ const c = t.charCodeAt(i);
171
+ if (c !== 95 && (c < 97 || c > 122)) return false;
172
+ }
173
+ return true;
174
+ }
175
+ var INFORMATION_SCHEMA_VIEWS = /* @__PURE__ */ new Set([
176
+ "columns",
177
+ "tables",
178
+ "views",
179
+ "routines",
180
+ "schemata",
181
+ "sequences",
182
+ "key_column_usage",
183
+ "table_constraints",
184
+ "constraint_column_usage",
185
+ "referential_constraints",
186
+ "check_constraints",
187
+ "routine_parameters",
188
+ "table_privileges",
189
+ "column_privileges",
190
+ "role_table_grants",
191
+ "role_routine_grants",
192
+ "trigger",
193
+ "triggers",
194
+ "views"
195
+ ]);
196
+ var PLATFORM_SCHEMAS = /* @__PURE__ */ new Set(["vault", "auth", "storage", "extensions", "realtime", "graphql", "pgsodium", "pgtle"]);
197
+ var MODE_KEYWORD_TYPES = /* @__PURE__ */ new Set(["keyword_in", "keyword_out", "keyword_inout", "keyword_variadic"]);
198
+ function classifyExternalTarget(target, qualified, wholeQuotedName = false) {
199
+ if (qualified) {
200
+ const q = qualified.toLowerCase();
201
+ if (q.startsWith("information_schema.")) return "informationSchema";
202
+ const qdot = q.indexOf(".");
203
+ if (qdot > 0 && PLATFORM_SCHEMAS.has(q.slice(0, qdot))) return "platform";
204
+ }
205
+ const t = target.toLowerCase();
206
+ if (!wholeQuotedName && t.startsWith("information_schema.")) return "informationSchema";
207
+ if (INFORMATION_SCHEMA_VIEWS.has(t)) return "informationSchema";
208
+ if (isPostgresCatalogName(t)) return "postgresCatalog";
209
+ if (t === "public") return "implicitSchema";
210
+ const dot = t.indexOf(".");
211
+ if (!wholeQuotedName && dot > 0) {
212
+ const schema = t.slice(0, dot);
213
+ if (PLATFORM_SCHEMAS.has(schema)) return "platform";
214
+ }
215
+ return null;
216
+ }
217
+ var SQL_PROVIDED_FUNCTIONS = /* @__PURE__ */ new Set([
218
+ // 102 names. `isempty` is PostgreSQL's range function and sits in the PostgreSQL group, not
219
+ // pgTAP's `is_empty` — the two are one character apart and a different provider.
220
+ // pgTAP — the PostgreSQL unit-testing extension. `boundary/internal/db/sqltest/` is a suite of
221
+ // it: `select plan(8);` … `select * from finish();`.
222
+ "plan",
223
+ "finish",
224
+ "collect_tap",
225
+ "ok",
226
+ "skip",
227
+ "cmp_ok",
228
+ "set_ne",
229
+ "results_ne",
230
+ "row_eq",
231
+ "is_empty",
232
+ "isnt_empty",
233
+ "is_strict",
234
+ "isnt_strict",
235
+ "has_index",
236
+ "hasnt_index",
237
+ "has_function",
238
+ "has_trigger",
239
+ "has_view",
240
+ "hasnt_view",
241
+ "has_table",
242
+ "hasnt_table",
243
+ "has_column",
244
+ "hasnt_column",
245
+ "has_domain",
246
+ "has_type",
247
+ "col_not_null",
248
+ "col_is_null",
249
+ "col_is_pk",
250
+ "col_has_default",
251
+ "col_hasnt_default",
252
+ "col_default_is",
253
+ "col_type_is",
254
+ "domain_type_is",
255
+ "fk_ok",
256
+ "trigger_is",
257
+ "volatility_is",
258
+ "throws_like",
259
+ // PostgreSQL's own built-ins — the `SQL_BUILTINS` remit's first clause. `isempty` is HERE and not
260
+ // with pgTAP's `is_empty`: it is PostgreSQL's range function (`isempty(anyrange)`), a different
261
+ // spelling with a different provider, and the two are one character apart.
262
+ "isempty",
263
+ "family",
264
+ "json_typeof",
265
+ "json_to_recordset",
266
+ "jsonb_to_recordset",
267
+ "jsonb_array_length",
268
+ "jsonb_object_keys",
269
+ "jsonb_array_elements_text",
270
+ "to_regprocedure",
271
+ "convert_to",
272
+ "convert_from",
273
+ "regexp_split_to_array",
274
+ "regexp_split_to_table",
275
+ "current_schema",
276
+ "xmlconcat2",
277
+ "pow",
278
+ "enum_first",
279
+ "get_byte",
280
+ "cosh",
281
+ // uuid-ossp, beside the `uuid_generate_v4`/`uuid_generate_v1`/`uuid_nil` `SQL_BUILTINS` carries.
282
+ "uuid_generate_v1mc",
283
+ // DuckDB — the engine the corpus's `duckdb/` tree runs on.
284
+ "read_parquet",
285
+ "read_csv",
286
+ "read_csv_auto",
287
+ "len",
288
+ "strlen",
289
+ "strftime",
290
+ "quantile_cont",
291
+ "quantile_disc",
292
+ "approx_quantile",
293
+ "reservoir_quantile",
294
+ "date_diff",
295
+ "array_contains",
296
+ "array_push_back",
297
+ "array_pop_front",
298
+ "list",
299
+ "list_resize",
300
+ "list_distinct",
301
+ "list_transform",
302
+ "list_position",
303
+ "list_aggregate",
304
+ "list_sum",
305
+ "countif",
306
+ "arg_min",
307
+ "hash",
308
+ "bitstring_agg",
309
+ "lttb",
310
+ "median",
311
+ "min_by",
312
+ "max_by",
313
+ "product",
314
+ "instr",
315
+ "group_concat",
316
+ // SQLite — its core functions plus the extensions its own source tree ships.
317
+ "hex",
318
+ "strdup",
319
+ "randomblob",
320
+ "sha1",
321
+ "sha1b",
322
+ "sha1_query",
323
+ "sha3_query",
324
+ "ieee754_from_int",
325
+ "parse_create_index",
326
+ "shell_prompt_test",
327
+ // MSSQL's metadata function, and pg_cron's scheduling pair.
328
+ "columnproperty",
329
+ "schedule",
330
+ "unschedule"
331
+ ]);
332
+ var SQL_BUILTINS = /* @__PURE__ */ new Set([
333
+ // Provided by PostgreSQL or a stock EXTENSION rather than by this project.
334
+ //
335
+ // WHY THIS BLOCK EXISTS, measured 2026-09-27. Every name the analyzer
336
+ // suppresses here is one a consumer of the graph could otherwise mistake for
337
+ // a project function — and an AI agent reading `calls X->encode` has no way to
338
+ // know `encode` is pgcrypto's. Classifying the whole unknown population on
339
+ // ProxiCall put 48 edges / 3 names in this category (`encode` 30, `digest` 17,
340
+ // `daterange` 1) and a further 122 edges / 42 names that were ALSO SQL-provided
341
+ // but live in files the grammar cannot parse (`uuid_generate_v4` 16,
342
+ // `gen_random_bytes` 12, `array_append`, `regexp_replace`, `regexp_matches`,
343
+ // `has_function_privilege`). Only 11 edges are genuinely dangling project
344
+ // references, so the raw "unknown" count was overwhelmingly this gap and not a
345
+ // real dangling-reference problem.
346
+ //
347
+ // Extensions are included deliberately: uuid-ossp, pgcrypto and PostGIS ship
348
+ // these functions as part of a normal PostgreSQL install, and a project that
349
+ // calls them has no declaration for them anywhere.
350
+ //
351
+ // pgcrypto
352
+ "encode",
353
+ "decode",
354
+ "digest",
355
+ "hmac",
356
+ "crypt",
357
+ "gen_salt",
358
+ "gen_random_bytes",
359
+ "gen_random_uuid",
360
+ // uuid-ossp and friends
361
+ "uuid_generate_v4",
362
+ "uuid_generate_v1",
363
+ "uuid_nil",
364
+ "gen_random_uuid",
365
+ // pgTAP / assertion helpers
366
+ "has_function_privilege",
367
+ "has_table_privilege",
368
+ "has_schema_privilege",
369
+ "has_column_privilege",
370
+ "has_database_privilege",
371
+ "has_type_privilege",
372
+ "throws_ok",
373
+ "lives_ok",
374
+ "is",
375
+ "isnt",
376
+ "matches",
377
+ "results_eq",
378
+ "set_eq",
379
+ // information_schema / catalogue accessors
380
+ "pg_get_functiondef",
381
+ "pg_get_viewdef",
382
+ "pg_get_indexdef",
383
+ "pg_get_constraintdef",
384
+ "pg_get_triggerdef",
385
+ "pg_get_expr",
386
+ "pg_get_userbyid",
387
+ "pg_get_viewdef",
388
+ "pg_get_partkeydef",
389
+ "pg_get_statisticsobjdef_columns",
390
+ "pg_tablespace_location",
391
+ "pg_relation_filenode",
392
+ "pg_get_function_arguments",
393
+ "pg_get_function_result",
394
+ "obj_description",
395
+ "col_description",
396
+ "shobj_description",
397
+ "col_description_schema",
398
+ "pg_describe_object",
399
+ "pg_identify_object",
400
+ "pg_get_object_address",
401
+ "pg_get_object_keywords",
402
+ "has_any_column_privilege",
403
+ "has_sequence_privilege",
404
+ "pg_get_ruledef",
405
+ "pg_get_triggerdef",
406
+ "pg_stat_get_numscans",
407
+ "aclexplode",
408
+ "acldefault",
409
+ "makeaclitem",
410
+ "pg_backend_memory_contexts",
411
+ "pg_get_function_identity_arguments",
412
+ "format_type",
413
+ "pg_typeof",
414
+ "pg_table_is_visible",
415
+ "pg_function_is_visible",
416
+ "pg_opclass_is_visible",
417
+ "pg_conversion_is_visible",
418
+ "pg_collation_is_visible",
419
+ "pg_ts_config_is_visible",
420
+ "pg_statistics_obj_is_visible",
421
+ "pg_total_relation_size",
422
+ "pg_relation_size",
423
+ "pg_table_size",
424
+ "pg_indexes_size",
425
+ "pg_database_size",
426
+ "pg_size_pretty",
427
+ "pg_column_size",
428
+ "pg_size_bytes",
429
+ "pg_encoding_to_char",
430
+ "pg_char_to_encoding",
431
+ "pg_advisory_lock",
432
+ "pg_advisory_unlock",
433
+ "pg_advisory_xact_lock",
434
+ "pg_try_advisory_lock",
435
+ "pg_notify",
436
+ "pg_sleep",
437
+ "pg_backend_pid",
438
+ "pg_current_xact_id",
439
+ "txid_current",
440
+ "pg_export_snapshot",
441
+ "pg_import_snapshot",
442
+ "pg_cancel_backend",
443
+ "pg_terminate_backend",
444
+ "pg_reload_conf",
445
+ "pg_stat_reset",
446
+ "pg_is_in_recovery",
447
+ "pg_last_wal_receive_lsn",
448
+ "pg_current_wal_lsn",
449
+ "pg_wal_lsn_diff",
450
+ "pg_switch_wal",
451
+ "pg_create_restore_point",
452
+ "pg_replication_origin_advance",
453
+ "pg_logical_emit_message",
454
+ "pg_column_is_updatable",
455
+ "pg_index_column_has_property",
456
+ "pg_indexam_has_property",
457
+ "pg_index_has_property",
458
+ "pg_index_column_has_property",
459
+ "pg_options_to_table",
460
+ "pg_tablespace_databases",
461
+ "pg_ls_dir",
462
+ "pg_read_file",
463
+ "pg_read_binary_file",
464
+ "pg_stat_file",
465
+ "pg_ls_logdir",
466
+ "pg_ls_waldir",
467
+ "pg_ls_tmpdir",
468
+ "pg_ls_archive_statusdir",
469
+ // string / pattern builtins the corpus leans on
470
+ "regexp_replace",
471
+ "regexp_match",
472
+ "regexp_matches",
473
+ "regexp_count",
474
+ "regexp_instr",
475
+ "regexp_like",
476
+ "regexp_substr",
477
+ "split_part",
478
+ "string_to_array",
479
+ "array_to_string",
480
+ "array_position",
481
+ "array_positions",
482
+ "array_remove",
483
+ "array_replace",
484
+ "array_prepend",
485
+ "array_append",
486
+ "array_cat",
487
+ "array_fill",
488
+ "cardinality",
489
+ "array_dims",
490
+ "array_upper",
491
+ "array_lower",
492
+ "array_ndims",
493
+ "array_length",
494
+ "trim_array",
495
+ "width_bucket",
496
+ // range / multirange, which the corpus uses for exclusion constraints
497
+ "daterange",
498
+ "tsrange",
499
+ "tstzrange",
500
+ "int4range",
501
+ "int8range",
502
+ "numrange",
503
+ "range_merge",
504
+ "range_lower",
505
+ "range_upper",
506
+ "range_lower_inc",
507
+ "range_upper_inc",
508
+ "range_lower_inf",
509
+ "range_upper_inf",
510
+ "range_empty",
511
+ "int4multirange",
512
+ "datemultirange",
513
+ "int8multirange",
514
+ "nummultirange",
515
+ "tsmultirange",
516
+ "tstzmultirange",
517
+ "range_contains",
518
+ "range_contained_by",
519
+ "range_overlaps",
520
+ "range_adjacent",
521
+ // PostGIS, which the extension corpus is full of
522
+ "st_x",
523
+ "st_y",
524
+ "st_z",
525
+ "st_m",
526
+ "st_2d",
527
+ "st_3d",
528
+ "st_srid",
529
+ "st_set_srid",
530
+ "st_astext",
531
+ "st_asbinary",
532
+ "st_asgeojson",
533
+ "st_geometrytype",
534
+ "st_type",
535
+ "st_geometryn",
536
+ "st_numgeometries",
537
+ "st_npoints",
538
+ "st_nrings",
539
+ "st_isempty",
540
+ "st_isvalid",
541
+ "st_isvalidreason",
542
+ "st_issimple",
543
+ "st_isclosed",
544
+ "st_isring",
545
+ "st_transform",
546
+ "st_intersects",
547
+ "st_contains",
548
+ "st_within",
549
+ "st_containsproperly",
550
+ "st_covers",
551
+ "st_coveredby",
552
+ "st_overlaps",
553
+ "st_crosses",
554
+ "st_touches",
555
+ "st_distance",
556
+ "st_dwithin",
557
+ "st_area",
558
+ "st_perimeter",
559
+ "st_length",
560
+ "st_length2d",
561
+ "st_buffer",
562
+ "st_convexhull",
563
+ "st_envelope",
564
+ "st_centroid",
565
+ "st_makeenvelope",
566
+ "st_makepoint",
567
+ "st_makeline",
568
+ "st_geographyfromtext",
569
+ "st_geometryfromtext",
570
+ "st_geogfromtext",
571
+ "st_geomfromtext",
572
+ "st_point",
573
+ "postgis_version",
574
+ "postgis_full_version",
575
+ "postgis_geos_version",
576
+ "box2d",
577
+ "box3d",
578
+ "geometry",
579
+ "geography",
580
+ "box",
581
+ "path",
582
+ "polygon",
583
+ // aggregate / window extras the corpus uses
584
+ "percentile_cont",
585
+ "percentile_disc",
586
+ "mode",
587
+ "rank",
588
+ "dense_rank",
589
+ "row_number",
590
+ "ntile",
591
+ "first_value",
592
+ "last_value",
593
+ "nth_value",
594
+ "grouping",
595
+ "corr",
596
+ "covar_pop",
597
+ "covar_samp",
598
+ "regr_slope",
599
+ "regr_intercept",
600
+ "regr_r2",
601
+ "regr_count",
602
+ "regr_avgx",
603
+ "regr_avgy",
604
+ "bool_and",
605
+ "bool_or",
606
+ "every",
607
+ "bit_and",
608
+ "bit_or",
609
+ "bit_xor",
610
+ "json_agg",
611
+ "json_object_agg",
612
+ // statistical aggregates, which the corpus uses and this set was missing
613
+ "stddev",
614
+ "stddev_pop",
615
+ "stddev_samp",
616
+ "variance",
617
+ "var_pop",
618
+ "var_samp",
619
+ "stddev_pop_zscore",
620
+ // hosted-platform functions a project calls with no local declaration
621
+ "jwt",
622
+ "auth_uid",
623
+ "current_user_id",
624
+ "aggregate",
625
+ // aggregate
626
+ "any_value",
627
+ "array_agg",
628
+ "avg",
629
+ "bit_and",
630
+ "bit_or",
631
+ "bool_and",
632
+ "bool_or",
633
+ "count",
634
+ "every",
635
+ "json_agg",
636
+ "json_object_agg",
637
+ "jsonb_agg",
638
+ "jsonb_object_agg",
639
+ "max",
640
+ "min",
641
+ "string_agg",
642
+ "sum",
643
+ "xmlagg",
644
+ // window
645
+ "cume_dist",
646
+ "dense_rank",
647
+ "first_value",
648
+ "lag",
649
+ "last_value",
650
+ "lead",
651
+ "nth_value",
652
+ "ntile",
653
+ "percent_rank",
654
+ "percentile_cont",
655
+ "percentile_disc",
656
+ "rank",
657
+ "row_number",
658
+ // numeric / math
659
+ "abs",
660
+ "acos",
661
+ "asin",
662
+ "atan",
663
+ "atan2",
664
+ "cbrt",
665
+ "ceil",
666
+ "ceiling",
667
+ "cos",
668
+ "cot",
669
+ "degrees",
670
+ "div",
671
+ "exp",
672
+ "floor",
673
+ "gcd",
674
+ "lcm",
675
+ "ln",
676
+ "log",
677
+ "log10",
678
+ "mod",
679
+ "pi",
680
+ "power",
681
+ "radians",
682
+ "random",
683
+ "round",
684
+ "scale",
685
+ "sign",
686
+ "sin",
687
+ "sqrt",
688
+ "tan",
689
+ "trunc",
690
+ "width_bucket",
691
+ // string
692
+ "ascii",
693
+ "btrim",
694
+ "char_length",
695
+ "character_length",
696
+ "chr",
697
+ "concat",
698
+ "concat_ws",
699
+ "format",
700
+ "initcap",
701
+ "left",
702
+ "length",
703
+ "lower",
704
+ "lpad",
705
+ "ltrim",
706
+ "md5",
707
+ "overlay",
708
+ "position",
709
+ "quote_ident",
710
+ "quote_literal",
711
+ "repeat",
712
+ "replace",
713
+ "reverse",
714
+ "right",
715
+ "rpad",
716
+ "rtrim",
717
+ "split_part",
718
+ "starts_with",
719
+ "strpos",
720
+ "substr",
721
+ "substring",
722
+ "translate",
723
+ "trim",
724
+ "upper",
725
+ // date / time
726
+ "age",
727
+ "clock_timestamp",
728
+ "current_date",
729
+ "current_time",
730
+ "current_timestamp",
731
+ "date",
732
+ "date_bin",
733
+ "date_part",
734
+ "date_trunc",
735
+ "datediff",
736
+ "dateadd",
737
+ "datetime",
738
+ "extract",
739
+ "getdate",
740
+ "getutcdate",
741
+ "interval",
742
+ "isfinite",
743
+ "justify_days",
744
+ "justify_hours",
745
+ "justify_interval",
746
+ "localtime",
747
+ "localtimestamp",
748
+ "make_date",
749
+ "make_interval",
750
+ "make_time",
751
+ "make_timestamp",
752
+ "make_timestamptz",
753
+ "now",
754
+ "statement_timestamp",
755
+ "sysdate",
756
+ "timeofday",
757
+ "to_char",
758
+ "to_date",
759
+ "to_number",
760
+ "to_timestamp",
761
+ "transaction_timestamp",
762
+ // conditional / null
763
+ "coalesce",
764
+ "greatest",
765
+ "ifnull",
766
+ "iif",
767
+ "least",
768
+ "nullif",
769
+ "nvl",
770
+ "nvl2",
771
+ // json
772
+ "json_array_length",
773
+ "json_build_array",
774
+ "json_build_object",
775
+ "json_each",
776
+ "json_each_text",
777
+ "json_extract",
778
+ "json_object",
779
+ "json_object_field",
780
+ "json_object_keys",
781
+ "json_populate_record",
782
+ "json_query",
783
+ "json_set",
784
+ "json_strip_nulls",
785
+ "json_type",
786
+ "jsonb_array_elements",
787
+ "jsonb_build_object",
788
+ "jsonb_each",
789
+ "jsonb_each_text",
790
+ "jsonb_pretty",
791
+ "jsonb_set",
792
+ "jsonb_typeof",
793
+ "row_to_json",
794
+ "to_json",
795
+ "to_jsonb",
796
+ // array / set
797
+ "array_fill",
798
+ "array_length",
799
+ "array_lower",
800
+ "array_position",
801
+ "array_to_string",
802
+ "array_upper",
803
+ "cardinality",
804
+ "generate_series",
805
+ "generate_subscripts",
806
+ "unnest",
807
+ "range_agg",
808
+ "width_bucket",
809
+ // system / session / type
810
+ "coalesce",
811
+ "current_setting",
812
+ "format_type",
813
+ "nextval",
814
+ "pg_advisory_lock",
815
+ "pg_backend_pid",
816
+ "pg_cancel_backend",
817
+ "pg_client_encoding",
818
+ "pg_column_size",
819
+ "pg_current_xact_id",
820
+ "pg_terminate_backend",
821
+ "pg_typeof",
822
+ "pg_total_relation_size",
823
+ "set_config",
824
+ "setseed",
825
+ "setval",
826
+ "to_regclass",
827
+ "txid_current",
828
+ "version",
829
+ "newid",
830
+ "scope_identity",
831
+ "dense_rank",
832
+ // cast / conversion
833
+ "cast",
834
+ "convert",
835
+ "convert_tz",
836
+ "parse_ident",
837
+ "try_cast",
838
+ "try_convert",
839
+ "try_parse_ident"
840
+ ]);
841
+ var identifierTypes = /* @__PURE__ */ new Set(["identifier", "literal"]);
842
+ var queryTypes = /* @__PURE__ */ new Set(["select", "set_operation", "create_query", "subquery", "cte"]);
843
+ var PSQL_INCLUDE_COMMANDS = /* @__PURE__ */ new Set(["i", "ir"]);
844
+ var PSQL_INCLUDE_SUFFIXES = [".sql", ".psql", ".dump"];
845
+ var REPLACEABLE_DECLARATIONS = /* @__PURE__ */ new Set([
846
+ "create_function",
847
+ "create_procedure",
848
+ "create_view",
849
+ "create_materialized_view"
850
+ ]);
851
+ var CONSTRUCTOR_VALUE_NODES = /* @__PURE__ */ new Set(["list", "array"]);
852
+ var RESERVED_AS_COLUMN_NAME = /* @__PURE__ */ new Set(["like"]);
853
+ function isTagIdentStart(code) {
854
+ return code >= 65 && code <= 90 || // A-Z
855
+ code >= 97 && code <= 122 || // a-z
856
+ code === 95;
857
+ }
858
+ function isTagIdentPart(code) {
859
+ return isTagIdentStart(code) || code >= 48 && code <= 57;
860
+ }
861
+ function dollarTagAt(content, at) {
862
+ if (content.charCodeAt(at) !== 36) return null;
863
+ let i = at + 1;
864
+ if (i < content.length && isTagIdentStart(content.charCodeAt(i))) {
865
+ i++;
866
+ while (i < content.length && isTagIdentPart(content.charCodeAt(i))) i++;
867
+ }
868
+ if (i < content.length && content.charCodeAt(i) === 36) return content.slice(at, i + 1);
869
+ return null;
870
+ }
871
+ function opensWithConstraintKeyword(text) {
872
+ const KEYWORD = "CONSTRAINT";
873
+ let i = 0;
874
+ const isSpace = (c) => c === 32 || c === 9 || c === 10 || c === 13 || c === 12 || c === 11;
875
+ while (i < text.length && isSpace(text.charCodeAt(i))) i++;
876
+ if (text.charCodeAt(i) === 44) {
877
+ i++;
878
+ while (i < text.length && isSpace(text.charCodeAt(i))) i++;
879
+ }
880
+ if (i + KEYWORD.length > text.length) return false;
881
+ for (let k = 0; k < KEYWORD.length; k++) {
882
+ let c = text.charCodeAt(i + k);
883
+ if (c >= 97 && c <= 122) c -= 32;
884
+ if (c !== KEYWORD.charCodeAt(k)) return false;
885
+ }
886
+ return isSpace(text.charCodeAt(i + KEYWORD.length));
887
+ }
888
+ var RESYNC_ERROR_SPAN_RATIO = 0.6;
889
+ var RESYNC_MIN_ERROR_NODES = 1;
890
+ function fileModuleNameFor(filePath) {
891
+ const slash = filePath.lastIndexOf("/");
892
+ const base = slash >= 0 ? filePath.slice(slash + 1) : filePath;
893
+ return base.length > 0 ? base : "migration.sql";
894
+ }
895
+ function qualifiedNameFor(file, name, ownerName, kind) {
896
+ return name === file && kind === ASTEngine.SymbolKind.Module ? file : `${file}::${ownerName ? `${ownerName}.${name}` : name}`;
897
+ }
898
+ var RealSqlAnalyzer = class _RealSqlAnalyzer {
899
+ analyze(rootNode, content, filePath) {
900
+ this.treeIndexes = /* @__PURE__ */ new WeakMap();
901
+ const fileModuleName = fileModuleNameFor(filePath);
902
+ const state = {
903
+ fileModuleName,
904
+ nestedStatements: /* @__PURE__ */ new Set(),
905
+ localCtes: /* @__PURE__ */ new Set(),
906
+ symbols: [],
907
+ relationships: [],
908
+ imports: [],
909
+ exports: [],
910
+ relationshipKeys: /* @__PURE__ */ new Set(),
911
+ relationshipsByKey: /* @__PURE__ */ new Map(),
912
+ redeclared: /* @__PURE__ */ new Map(),
913
+ routineNames: /* @__PURE__ */ new Set(),
914
+ includeKeys: /* @__PURE__ */ new Set(),
915
+ symbolKeys: /* @__PURE__ */ new Set(),
916
+ declarationKeys: /* @__PURE__ */ new Set(),
917
+ symbolNames: /* @__PURE__ */ new Set(),
918
+ idCounts: /* @__PURE__ */ new Map(),
919
+ patterns: /* @__PURE__ */ new Set(),
920
+ slice: IDENTITY,
921
+ metadata: {
922
+ linesOfCode: this.countLines(content),
923
+ performanceScore: 100,
924
+ architecturalInsights: [],
925
+ detectedFrameworks: ["SQL"],
926
+ patterns: [],
927
+ complexity: 1,
928
+ cyclomaticComplexity: 1
929
+ },
930
+ ambiguity: {
931
+ errorNodes: 0,
932
+ unparseableRegions: 0,
933
+ resyncStatements: 0,
934
+ resyncDeclarations: 0,
935
+ resyncSkipped: 0,
936
+ keywordTargetsSuppressed: 0,
937
+ recoveryCallsSuppressed: 0,
938
+ sweepDeclarationsRecovered: 0,
939
+ sweepStatementsDispatched: 0,
940
+ builtinCallsSuppressed: 0,
941
+ quotedIdentifiers: 0,
942
+ constraintsRecoveredFromError: 0,
943
+ executeLiterals: 0,
944
+ executeRecovered: 0,
945
+ executeSkipped: 0,
946
+ psqlCommandsSkipped: 0,
947
+ includeNonLiteral: 0,
948
+ extensionImportsEmitted: 0,
949
+ overridesEmitted: 0,
950
+ overloadNotReplacementSkipped: 0,
951
+ pgCatalogMarkedExternal: 0,
952
+ providedFunctionCallsMarked: 0,
953
+ instantiationsEmitted: 0,
954
+ reservedNameSymbolsSuppressed: 0,
955
+ /**
956
+ * A `contains` edge DROPPED because its source and target bare NAMES were equal.
957
+ *
958
+ * The counter exists because the drop was SILENT — a legal column that shares its table's
959
+ * name simply did not appear in the graph, and nothing said so. Measured over the whole
960
+ * admitted population on 2026-10-21: **9 edges in 7 files**, every one of them a real
961
+ * declaration the table did not own in the graph. The guard now tests the DECLARATION
962
+ * IDENTITY where the call site has it, so these edges are published; the counter is what
963
+ * would catch a case where the identity is not available and the name test still fires.
964
+ */
965
+ sameNameContainmentSuppressed: 0,
966
+ indexOwnersAdopted: 0,
967
+ indexOwnersExternalToFile: 0,
968
+ unnamedIndexSkipped: 0,
969
+ containmentRetargeted: 0,
970
+ containmentRetargetSkipped: 0
971
+ }
972
+ };
973
+ this.collectParseErrors(rootNode, state);
974
+ state.symbols.unshift({
975
+ name: fileModuleName,
976
+ kind: ASTEngine.SymbolKind.Module,
977
+ range: this.range(rootNode, state),
978
+ visibility: "public",
979
+ metadata: { sqlSpecific: true, isMigration: true, scriptPath: filePath }
980
+ });
981
+ if (rootNode) this.processRoot(rootNode, state);
982
+ const resync = this.resyncNeeded(rootNode, state);
983
+ if (resync) this.runResync(content, state);
984
+ this.runExecuteScan(content, state);
985
+ this.adoptLateDeclaredIndexOwners(state);
986
+ this.publishQualifiedNames(state);
987
+ this.retargetContainmentEdges(state);
988
+ const tables = state.symbols.filter((s) => s.metadata?.type === "table");
989
+ if (tables.length > 2) state.metadata.architecturalInsights.push("Complex schema detected");
990
+ if (state.patterns.has("Foreign Key Constraints")) {
991
+ state.metadata.architecturalInsights.push("Relational integrity enforced via Foreign Keys");
992
+ }
993
+ if (state.externalTargets) state.metadata.externalTargets = state.externalTargets;
994
+ state.metadata.patterns = [...state.patterns].sort();
995
+ state.metadata.complexity = Math.max(1, state.metadata.cyclomaticComplexity ?? 1);
996
+ state.metadata.ambiguity = state.ambiguity;
997
+ return {
998
+ symbols: state.symbols,
999
+ relationships: state.relationships,
1000
+ imports: state.imports,
1001
+ exports: state.exports,
1002
+ metadata: state.metadata,
1003
+ errors: state.metadata.errors ?? []
1004
+ };
1005
+ }
1006
+ /**
1007
+ * Give an index symbol the owner its `contains` edge already names, when the
1008
+ * table was declared AFTER the index.
1009
+ *
1010
+ * `processIndex` attaches the owner when the table symbol already exists, which
1011
+ * covers `CREATE TABLE` then `CREATE INDEX`. The reverse order — a migration that
1012
+ * adds the index first, or a file where the table is recovered by the
1013
+ * declaration sweep or the EXECUTE-literal pass after the index was walked —
1014
+ * leaves the index with `ownerName: undefined` while `contains table -> index`
1015
+ * says otherwise. 70 containment edges over a 200-file corpus slice were in that
1016
+ * state before this pass, and `bb_graph`'s owner-cascade and `bb_impact`'s reverse
1017
+ * walk both read `ownerName`, so the member read as an orphan that happened to be
1018
+ * named in an edge.
1019
+ *
1020
+ * WHY IT IS A SEPARATE PASS AND NOT A LOOKAHEAD. Declaration order in a SQL file
1021
+ * is not the order the analyzer visits in: the root walk dispatches top-level
1022
+ * statements, the declaration sweep then re-walks the whole tree, and the resync
1023
+ * and EXECUTE-literal passes append afterwards. Any "is the table there yet"
1024
+ * question asked at `processIndex` time is answered about a partial file. A pass
1025
+ * at the end is a question about the finished one.
1026
+ *
1027
+ * IT ADDS NO EDGE AND NO SYMBOL. It writes four fields the symbol should have had
1028
+ * from the start, and counts what it did in `ambiguity.indexOwnersAdopted`, so a
1029
+ * file where the order was already right reports 0.
1030
+ */
1031
+ /**
1032
+ * CAUSE A — publish a `qualifiedName` that is an IDENTIFIER rather than a repeat of `name`.
1033
+ *
1034
+ * MEASURED before this existed. Over the whole admitted SQL population (1,962 files, 199 corpus
1035
+ * repos, production `parseFile`; record
1036
+ * `validation/m14/fixtures/sql/measurements/pass13-ambiguity-split-BEFORE.json`): 18,652 symbols
1037
+ * collapsed to 7,875 distinct `name`s, and the count of symbols whose `qualifiedName` DIFFERED
1038
+ * from their `name` was **0**. Every ambiguous target in that run therefore also had no
1039
+ * `qualifiedName` to disambiguate it — the brief's A symptom, 100% of the time.
1040
+ *
1041
+ * WHY THE FIELD WAS INERT, and it was not this lane's choice of names.
1042
+ * `src/ast/pool-worker.ts` derives the stored value as
1043
+ * `s.qualifiedName || s.metadata?.fqn || (s.scope ? \`${s.scope}.${s.name}\` : s.name)`.
1044
+ * This analyzer sets `ownerName` and never `scope`, so the third arm never fired and the derived
1045
+ * value was `name`. `metadata.fqn` is the arm that formula reads first after the analyzer's own
1046
+ * field, it is the field the C# family already writes (`real-csharp-analyzer.ts` `fqn: fullName`),
1047
+ * and fsharp took it for the same reason (`real-fsharp-analyzer.ts:138`). So this writes the one
1048
+ * field that is designed for the job rather than renaming anything.
1049
+ *
1050
+ * WHY THE FILE IS PART OF IT. `ownerName` alone is not an identifier for this language: SQL
1051
+ * collides overwhelmingly WITHIN a kind, and the corpus measurement names the offenders — 187
1052
+ * files declare a symbol called `id`, 104 declare `name`, 36 declare `changed_by`, every one of
1053
+ * them a `property` (a column). An `owner.name` qualified name would read `items.id` in
1054
+ * `001_init.sql` and `items.id` again in `002_orders.sql`, so the name that exists to make a
1055
+ * symbol addressable would still not address it. `search_path` is what resolves those in a live
1056
+ * database and a migration file is loaded as a unit, so the file is the outer scope SQL actually
1057
+ * has, and it goes in.
1058
+ *
1059
+ * `name` is untouched. The published name surface every consumer reads is exactly what it was,
1060
+ * which is why this needs no fixture change and no coordination with any other lane; the
1061
+ * relationship targets are untouched too, so no edge changes which row it names. What changes is
1062
+ * that `brain_symbol.qualified_name` now carries an identifier, and the store's pass-1 resolution
1063
+ * (`target_external = qualified_name`, the only EXACT match it owns) has something exact to match
1064
+ * for any edge that chooses to target it.
1065
+ *
1066
+ * NOT CLAIMED: this does not make an edge resolve that did not resolve. Resolution is the store's
1067
+ * name-keyed passes, measured in `store-resolution.mts`; a distinct qualified name is a
1068
+ * precondition for a name-keyed choice to be checkable, not a resolution itself.
1069
+ */
1070
+ publishQualifiedNames(state) {
1071
+ const file = state.fileModuleName;
1072
+ for (const s of state.symbols) {
1073
+ if (!s.name) continue;
1074
+ const fqn = qualifiedNameFor(file, s.name, s.ownerName, s.kind);
1075
+ s.qualifiedName = fqn;
1076
+ s.metadata = { ...s.metadata, fqn };
1077
+ }
1078
+ }
1079
+ /**
1080
+ * Rewrites every `contains` edge's TARGET to the qualified name of its member.
1081
+ *
1082
+ * ## WHY THIS IS THE CHANGE AND NOT THE OTHER ONE
1083
+ *
1084
+ * `pass13-ambiguity-split-AFTER.json` measured `K3_qname` EQUAL to `K0_bare` on all six firing
1085
+ * types — 651/651, 2,512/2,512, 4,364/4,364 — for one reason: every symbol carried a qualified
1086
+ * name and no edge targeted one, so the identifier existed and nothing used it. The store's
1087
+ * resolution pass 1 (`parallel-indexer.ts:3773`) matches `target_external = qualified_name`, which
1088
+ * is the only EXACT match it owns; pass 2 needs the bare name to be unique in the project and pass 3
1089
+ * needs a dot. A bare member name meets none of the three in a migration corpus.
1090
+ *
1091
+ * `contains` is the ONE type where the qualified name is a fact rather than a guess, and that is
1092
+ * what makes this safe to do here and not on the other five:
1093
+ *
1094
+ * - The member was declared INSIDE the owner, in THIS parse result. `createSymbol` pushes the
1095
+ * member and emits `contains owner -> member` in the same walk. So the target's identity is
1096
+ * derivable from this file alone — no guessing which file declares it.
1097
+ * - `calls`, `uses` and `references` name something in ANOTHER file and the analyzer does not
1098
+ * know which. Emitting a qualified name there would be inventing the file, which is the
1099
+ * fabrication the M14 contract forbids and which the oracle counts as `unexplained`. Those
1100
+ * targets stay bare, and the report says so.
1101
+ *
1102
+ * ## WHY IT IS SELF-VERIFYING
1103
+ *
1104
+ * The candidate is admitted only if it is in the set of qualified names THIS parse result
1105
+ * published. So the pass cannot introduce a target string no symbol publishes — the exact defect
1106
+ * the graphql lane shipped and had to revert. A candidate that fails the check is left bare and
1107
+ * counted in `containmentRetargetSkipped`, which is the falsifier: a non-zero value means the
1108
+ * edge's (owner, member) pair does not name a symbol this file publishes, and the number of
1109
+ * those is the cost.
1110
+ *
1111
+ * ## WHY THE SOURCE STAYS BARE
1112
+ *
1113
+ * Only the target is rewritten. The store resolves a relationship's SOURCE by exact name match
1114
+ * over the symbols of the file being stored (`nameToSymbol.get(sourceName)`), so a qualified
1115
+ * source would stop resolving — `store-resolution.mts` currently reads `src_unres: 200` on
1116
+ * `contains` and this pass must not make it worse. Containment survives on the target alone,
1117
+ * because the member's qualified name already contains the owner.
1118
+ *
1119
+ * ## WHAT IT DOES NOT CLAIM
1120
+ *
1121
+ * This does not make an edge resolve that did not resolve. Measured over the admitted corpus at
1122
+ * the 600-file population of `validation/m14/fixtures/sql/contains-fqn-probe.mts`:
1123
+ *
1124
+ * contains edges 5,552
1125
+ * target qualified name is a symbol this file published 5,552 (100.0%)
1126
+ * bare name today: unique 2,067 (37.2%)
1127
+ * bare name today: ambiguous 3,485 (62.8%)
1128
+ * qualified names published by >1 symbol 195 <- the falsifier
1129
+ * contains edges landing on such a name 85 (1.5%)
1130
+ *
1131
+ * So the projected move is 62.8% of `contains` targets from "the reverse walk picks a winner by a
1132
+ * rule nobody chose" to an exact bind, with 85 edges still landing on a name that is not unique —
1133
+ * which is the residue `memberKind` already separates on the edge, and the honest limit of this
1134
+ * change.
1135
+ */
1136
+ retargetContainmentEdges(state) {
1137
+ const published = /* @__PURE__ */ new Set();
1138
+ for (const s of state.symbols) {
1139
+ const fqn = typeof s.qualifiedName === "string" ? s.qualifiedName : s.metadata?.fqn;
1140
+ if (typeof fqn === "string" && fqn.length > 0) published.add(fqn);
1141
+ }
1142
+ const file = state.fileModuleName;
1143
+ for (const rel of state.relationships) {
1144
+ if (rel.type !== ASTEngine.RelationshipType.Contains) continue;
1145
+ const owner = rel.source;
1146
+ const member = rel.target;
1147
+ if (!owner || !member) continue;
1148
+ const candidate = qualifiedNameFor(file, member, owner, ASTEngine.SymbolKind.Property);
1149
+ if (!published.has(candidate)) {
1150
+ state.ambiguity.containmentRetargetSkipped++;
1151
+ continue;
1152
+ }
1153
+ rel.target = candidate;
1154
+ rel.metadata = { ...rel.metadata, qualifiedTarget: true };
1155
+ state.ambiguity.containmentRetargeted++;
1156
+ }
1157
+ }
1158
+ adoptLateDeclaredIndexOwners(state) {
1159
+ for (const symbol of state.symbols) {
1160
+ if (symbol.metadata?.type !== "index") continue;
1161
+ if (symbol.ownerName) continue;
1162
+ const table = symbol.metadata.table;
1163
+ if (typeof table !== "string" || table.length === 0) continue;
1164
+ const owner = this.existingSymbol(table, ASTEngine.SymbolKind.Class, state);
1165
+ if (!owner) {
1166
+ symbol.ownerName = table;
1167
+ symbol.ownerKind = ASTEngine.SymbolKind.Class;
1168
+ symbol.parent = table;
1169
+ symbol.metadata = { ...symbol.metadata, ownerExternalToFile: true };
1170
+ state.ambiguity.indexOwnersExternalToFile++;
1171
+ continue;
1172
+ }
1173
+ symbol.ownerName = owner.name;
1174
+ symbol.ownerKind = owner.kind;
1175
+ symbol.parent = owner.name;
1176
+ symbol.parentId = `${owner.id}.${symbol.name}`;
1177
+ symbol.metadata = { ...symbol.metadata, ownerDeclaredLater: true };
1178
+ state.ambiguity.indexOwnersAdopted++;
1179
+ }
1180
+ }
1181
+ // ---------------------------------------------------------------- errors
1182
+ /**
1183
+ * ERROR and MISSING nodes, reported on the analyzer's own channel.
1184
+ *
1185
+ * The engine also reports tree errors, but every gate that calls
1186
+ * `analyzer.analyze` directly (the M14 fixture lane, the mutation harness, the
1187
+ * contract battery) sees only what the analyzer returns, and the previous
1188
+ * version returned `errors: []` unconditionally — a file it could not parse at
1189
+ * all read as a clean 0 symbols / 0 relationships / 0 errors. A `severity`
1190
+ * field is part of the ParseError contract and the indexer keys its
1191
+ * `hasSyntaxErrors` report off it, so these are warnings, not errors.
1192
+ */
1193
+ collectParseErrors(rootNode, state) {
1194
+ if (!rootNode || !rootNode.hasError) return;
1195
+ const errors = [];
1196
+ const stack = [rootNode];
1197
+ while (stack.length > 0) {
1198
+ const current = stack.pop();
1199
+ if (current.type === "ERROR" || current.isMissing) {
1200
+ state.ambiguity.errorNodes++;
1201
+ if (current.type === "ERROR") state.ambiguity.unparseableRegions++;
1202
+ if (errors.length < 50) {
1203
+ errors.push({
1204
+ message: current.type === "ERROR" ? "Unrecoverable syntax region: tree-sitter-sql could not parse this subtree. Extraction continued on a best-effort basis, so symbols and relationships inside this range may be missing." : `Missing ${current.type} token: the source is syntactically incomplete at this point. Extraction continued on a best-effort basis.`,
1205
+ range: this.range(current, state),
1206
+ severity: "warning"
1207
+ });
1208
+ }
1209
+ }
1210
+ for (let i = current.namedChildCount - 1; i >= 0; i--) {
1211
+ const child = current.namedChild(i);
1212
+ if (child) stack.push(child);
1213
+ }
1214
+ }
1215
+ ;
1216
+ state.metadata.errors = errors;
1217
+ }
1218
+ // ---------------------------------------------------------------- resync
1219
+ /** Is this parse damaged badly enough that a per-statement re-parse pays? */
1220
+ resyncNeeded(rootNode, state) {
1221
+ if (!rootNode || !rootNode.hasError) return false;
1222
+ if (state.ambiguity.errorNodes < RESYNC_MIN_ERROR_NODES) return false;
1223
+ if (rootNode.type === "ERROR") return true;
1224
+ for (let i = 0; i < rootNode.namedChildCount; i++) {
1225
+ const child = rootNode.namedChild(i);
1226
+ if (child?.type !== "ERROR") continue;
1227
+ const span = child.endIndex - child.startIndex;
1228
+ if (span > 0 && span / Math.max(1, rootNode.endIndex - rootNode.startIndex) >= RESYNC_ERROR_SPAN_RATIO) {
1229
+ return true;
1230
+ }
1231
+ }
1232
+ return false;
1233
+ }
1234
+ /**
1235
+ * Re-parse the file one top-level statement at a time and extract from the
1236
+ * statements the whole-file parse could not reach.
1237
+ *
1238
+ * Why this is worth a second parse: the grammar requires a dollar-quoted body
1239
+ * to be `BEGIN … END` of parseable `statement`s, so ONE statement keyword it
1240
+ * does not know (RAISE, GET DIAGNOSTICS, …) leaves the closing tag
1241
+ * unconsumed and the parser absorbs the rest of the file into a single ERROR.
1242
+ * Measured over 441 files / 2,336 textual declarations: the whole-file parse
1243
+ * reaches 1,557 (66.65%); re-parsing per statement reaches 2,263 (96.88%).
1244
+ *
1245
+ * Offsets are translated back to FILE coordinates by `Slice`, so a symbol
1246
+ * recovered here carries the same 1-based lines and byte bounds as one the
1247
+ * main pass produced. Any failure degrades to the main pass alone, which is
1248
+ * exactly today's behaviour.
1249
+ */
1250
+ runResync(content, state) {
1251
+ const slices = this.splitTopLevelStatements(content);
1252
+ if (slices.length === 0) return;
1253
+ state.ambiguity.resyncStatements = slices.length;
1254
+ let parser = null;
1255
+ try {
1256
+ parser = this.acquireParser();
1257
+ } catch {
1258
+ state.ambiguity.resyncSkipped = slices.length;
1259
+ return;
1260
+ }
1261
+ if (!parser) {
1262
+ state.ambiguity.resyncSkipped = slices.length;
1263
+ return;
1264
+ }
1265
+ const savedSlice = state.slice;
1266
+ try {
1267
+ for (const slice of slices) {
1268
+ let tree;
1269
+ try {
1270
+ tree = parser.parse(slice.text);
1271
+ } catch {
1272
+ state.ambiguity.resyncSkipped++;
1273
+ continue;
1274
+ }
1275
+ if (!tree?.rootNode) {
1276
+ state.ambiguity.resyncSkipped++;
1277
+ continue;
1278
+ }
1279
+ const before = state.symbols.length;
1280
+ state.slice = {
1281
+ byteOffset: slice.start,
1282
+ rowOffset: slice.row,
1283
+ columnOffset: slice.column
1284
+ };
1285
+ this.processRoot(tree.rootNode, state);
1286
+ const recovered = state.symbols.length - before;
1287
+ if (recovered > 0) state.ambiguity.resyncDeclarations += recovered;
1288
+ state.slice = savedSlice;
1289
+ tree.delete?.();
1290
+ }
1291
+ } catch {
1292
+ state.ambiguity.resyncSkipped++;
1293
+ } finally {
1294
+ state.slice = savedSlice;
1295
+ }
1296
+ }
1297
+ /**
1298
+ * `EXECUTE [IMMEDIATE] '<sql>'` literals, as re-parseable slices.
1299
+ *
1300
+ * WHY THIS PASS EXISTS, and why it is TEXT rather than tree.
1301
+ *
1302
+ * PostgreSQL migrations reach the database two ways: by writing DDL, or by
1303
+ * handing it over as a string inside a procedural body. Measured on the 387
1304
+ * PostgreSQL extension files this lane can parse within 400 KB
1305
+ * (/usr/share/postgresql/14/extension), the analyzer emits 2,172 symbols while
1306
+ * **1,326 further CREATE declarations sit inside `EXECUTE '...'` literals**,
1307
+ * across 78 files — INDEX 546, TABLE 312, SEQUENCE 234, SCHEMA 78, VIEW 78,
1308
+ * TRIGGER 78. That is a potential +61 % on the corpus, and it is invisible to
1309
+ * every tree-based pass, because the DDL is a string: a tree-sitter grammar
1310
+ * parses one language and cannot produce `statement` nodes for SQL that is
1311
+ * text inside a literal, at any amount of grammar work.
1312
+ *
1313
+ * The plpgsql grammar candidate (validation/m60/sql/v4-plpgsql-candidate.json)
1314
+ * was the attempt to reach them through the tree, and it is instructive: it
1315
+ * cut ProxiCall error nodes 36 % and added 9 % structure while leaving the
1316
+ * product output byte-identical, because parsing the body still leaves the DDL
1317
+ * inside a string. This pass reads the text instead, which is why it needs no
1318
+ * grammar change and works on the grammar that is actually wired.
1319
+ *
1320
+ * WHAT IS SKIPPED, and why each skip is necessary:
1321
+ * - `--` and `/* *\/` comments, so `-- EXECUTE 'CREATE TABLE x'` in a
1322
+ * comment is not a declaration. Measured on the corpus, comments carry
1323
+ * real DDL text and this is the main false-positive risk.
1324
+ * - `'...'` strings, so an `EXECUTE` inside another string is not matched,
1325
+ * and so the matched literal's own text is not rescanned.
1326
+ * - `"..."` and `` `...` `` quoted identifiers, for the same reason.
1327
+ * Dollar-quoted regions are deliberately NOT skipped: that is precisely where
1328
+ * these literals live.
1329
+ *
1330
+ * The returned text is the RAW between-the-quotes slice, with `''` left
1331
+ * intact. That is not laziness, it is what makes the offsets exact: `''` is a
1332
+ * valid escaped quote inside a SQL string, so the parser reads it correctly
1333
+ * as written, and the slice is a contiguous substring of the file, so
1334
+ * `slice.byteOffset` maps every recovered range back to its true absolute
1335
+ * position. Unescaping first would shorten the text and silently shift every
1336
+ * range after the first escaped quote.
1337
+ */
1338
+ scanExecuteLiterals(content) {
1339
+ const out = [];
1340
+ let i = 0;
1341
+ let lineComment = false;
1342
+ let blockComment = false;
1343
+ let quote = null;
1344
+ let row = 0;
1345
+ let lineStart = 0;
1346
+ const isSpace = (c) => c === 32 || c === 9 || c === 10 || c === 13 || c === 12 || c === 11;
1347
+ const isWord = (c) => isTagIdentPart(c);
1348
+ const wordAt = (at, word) => {
1349
+ if (at + word.length > content.length) return 0;
1350
+ for (let k = 0; k < word.length; k++) {
1351
+ let c = content.charCodeAt(at + k);
1352
+ if (c >= 97 && c <= 122) c -= 32;
1353
+ if (c !== word.charCodeAt(k)) return 0;
1354
+ }
1355
+ if (at > 0 && isWord(content.charCodeAt(at - 1))) return 0;
1356
+ if (isWord(content.charCodeAt(at + word.length))) return 0;
1357
+ return word.length;
1358
+ };
1359
+ while (i < content.length) {
1360
+ const c = content[i];
1361
+ if (lineComment) {
1362
+ if (c === "\n") {
1363
+ lineComment = false;
1364
+ row++;
1365
+ lineStart = i + 1;
1366
+ }
1367
+ i++;
1368
+ continue;
1369
+ }
1370
+ if (blockComment) {
1371
+ if (c === "*" && content[i + 1] === "/") {
1372
+ blockComment = false;
1373
+ i += 2;
1374
+ continue;
1375
+ }
1376
+ if (c === "\n") {
1377
+ row++;
1378
+ lineStart = i + 1;
1379
+ }
1380
+ i++;
1381
+ continue;
1382
+ }
1383
+ if (quote) {
1384
+ if (c === quote) {
1385
+ if (content[i + 1] === quote) {
1386
+ i += 2;
1387
+ continue;
1388
+ }
1389
+ quote = null;
1390
+ }
1391
+ if (c === "\n") {
1392
+ row++;
1393
+ lineStart = i + 1;
1394
+ }
1395
+ i++;
1396
+ continue;
1397
+ }
1398
+ if (c === "-" && content[i + 1] === "-") {
1399
+ lineComment = true;
1400
+ i += 2;
1401
+ continue;
1402
+ }
1403
+ if (c === "/" && content[i + 1] === "*") {
1404
+ blockComment = true;
1405
+ i += 2;
1406
+ continue;
1407
+ }
1408
+ if (c === "'" || c === '"' || c === "`") {
1409
+ quote = c;
1410
+ i++;
1411
+ continue;
1412
+ }
1413
+ if (isWord(content.charCodeAt(i))) {
1414
+ const len = wordAt(i, "EXECUTE");
1415
+ if (len) {
1416
+ let j = i + len;
1417
+ while (j < content.length && isSpace(content.charCodeAt(j))) j++;
1418
+ const imm = wordAt(j, "IMMEDIATE");
1419
+ if (imm) {
1420
+ j += imm;
1421
+ while (j < content.length && isSpace(content.charCodeAt(j))) j++;
1422
+ }
1423
+ if (content[j] === "'") {
1424
+ let k = j + 1;
1425
+ let closed = false;
1426
+ while (k < content.length) {
1427
+ if (content[k] === "'") {
1428
+ if (content[k + 1] === "'") {
1429
+ k += 2;
1430
+ continue;
1431
+ }
1432
+ closed = true;
1433
+ break;
1434
+ }
1435
+ if (content[k] === "\n") {
1436
+ row++;
1437
+ lineStart = k + 1;
1438
+ }
1439
+ k++;
1440
+ }
1441
+ if (closed) {
1442
+ const text = content.slice(j + 1, k);
1443
+ if (text.length > 0) out.push({ text, start: j + 1 });
1444
+ i = k + 1;
1445
+ continue;
1446
+ }
1447
+ }
1448
+ }
1449
+ }
1450
+ if (c === "\n") {
1451
+ row++;
1452
+ lineStart = i + 1;
1453
+ }
1454
+ i++;
1455
+ }
1456
+ void lineStart;
1457
+ return out;
1458
+ }
1459
+ /** 0-based row of an absolute offset. */
1460
+ rowOf(content, offset) {
1461
+ let row = 0;
1462
+ for (let i = 0; i < offset && i < content.length; i++) {
1463
+ if (content[i] === "\n") row++;
1464
+ }
1465
+ return row;
1466
+ }
1467
+ /**
1468
+ * The EXECUTE-literal pass: re-parse each literal's contents as SQL and let
1469
+ * the ordinary extraction path run over it.
1470
+ *
1471
+ * Structurally identical to `runResync`, and for the same reasons: the same
1472
+ * parser, `state.slice` for absolute offsets, and `declarationKeys` making it
1473
+ * strictly additive. A declaration that the main pass or the resync already
1474
+ * emitted cannot be emitted again, so this pass can only add.
1475
+ */
1476
+ runExecuteScan(content, state) {
1477
+ const literals = this.scanExecuteLiterals(content);
1478
+ if (literals.length === 0) return;
1479
+ state.ambiguity.executeLiterals = literals.length;
1480
+ let parser = null;
1481
+ try {
1482
+ parser = this.acquireParser();
1483
+ } catch {
1484
+ state.ambiguity.executeSkipped = literals.length;
1485
+ return;
1486
+ }
1487
+ if (!parser) {
1488
+ state.ambiguity.executeSkipped = literals.length;
1489
+ return;
1490
+ }
1491
+ const savedSlice = state.slice;
1492
+ try {
1493
+ for (const lit of literals) {
1494
+ let tree;
1495
+ try {
1496
+ tree = parser.parse(lit.text);
1497
+ } catch {
1498
+ state.ambiguity.executeSkipped++;
1499
+ continue;
1500
+ }
1501
+ if (!tree?.rootNode) {
1502
+ state.ambiguity.executeSkipped++;
1503
+ continue;
1504
+ }
1505
+ const before = state.symbols.length;
1506
+ const at = this.lineColumnAt(content, lit.start);
1507
+ state.slice = { byteOffset: lit.start, rowOffset: at.row, columnOffset: at.column };
1508
+ this.processRoot(tree.rootNode, state);
1509
+ state.ambiguity.executeRecovered += state.symbols.length - before;
1510
+ state.slice = savedSlice;
1511
+ tree.delete?.();
1512
+ }
1513
+ } catch {
1514
+ state.ambiguity.executeSkipped++;
1515
+ } finally {
1516
+ state.slice = savedSlice;
1517
+ }
1518
+ }
1519
+ /**
1520
+ * Hand the analyzer the grammar it should re-parse with.
1521
+ *
1522
+ * `analyze` is synchronous and `Language.load` is not, so the resync pass
1523
+ * cannot build its own parser on the first file it sees. The engine calls this
1524
+ * in `parseRaw` — it has already loaded the SQL grammar for the file it is
1525
+ * parsing — and any harness that drives the analyzer directly can do the same,
1526
+ * which is what keeps the resync visible to the fixture lane instead of
1527
+ * silently inert. Without a grammar the resync is skipped and counted in
1528
+ * `ambiguity.resyncSkipped`; the main pass is unaffected either way.
1529
+ */
1530
+ static setGrammar(language, ParserCtor) {
1531
+ if (!language) return;
1532
+ _RealSqlAnalyzer._language = language;
1533
+ if (ParserCtor) globalThis.__bbTreeSitterParser = ParserCtor;
1534
+ _RealSqlAnalyzer._parser = null;
1535
+ }
1536
+ static _language = null;
1537
+ static _parser = null;
1538
+ static _loading = false;
1539
+ /** The parser is built lazily and reused: a file that parsed cleanly must not
1540
+ * pay for a second parse, and a clean file never asks for one at all. */
1541
+ acquireParser() {
1542
+ if (_RealSqlAnalyzer._parser) return _RealSqlAnalyzer._parser;
1543
+ const language = _RealSqlAnalyzer._language;
1544
+ if (language) {
1545
+ const ParserCtor = globalThis.__bbTreeSitterParser;
1546
+ if (ParserCtor) {
1547
+ try {
1548
+ const parser = new ParserCtor();
1549
+ parser.setLanguage(language);
1550
+ _RealSqlAnalyzer._parser = parser;
1551
+ return parser;
1552
+ } catch {
1553
+ return null;
1554
+ }
1555
+ }
1556
+ }
1557
+ this.kickoffGrammarLoad();
1558
+ return null;
1559
+ }
1560
+ /** One-shot async load, so a harness that never called `setGrammar` still gets
1561
+ * the resync from the second SQL file onwards. */
1562
+ kickoffGrammarLoad() {
1563
+ if (_RealSqlAnalyzer._loading || _RealSqlAnalyzer._language) return;
1564
+ _RealSqlAnalyzer._loading = true;
1565
+ void (async () => {
1566
+ try {
1567
+ const wasm = await import("./wasm-NMWO3WXY.js");
1568
+ const mod = await wasm.loadWebTreeSitter();
1569
+ const ParserCtor = mod?.Parser ?? mod?.default?.Parser ?? mod?.default;
1570
+ const LanguageCtor = mod?.Language ?? mod?.default?.Language;
1571
+ if (ParserCtor) globalThis.__bbTreeSitterParser = ParserCtor;
1572
+ if (!LanguageCtor) return;
1573
+ const { readFileSync } = await import("fs");
1574
+ const bytes = new Uint8Array(readFileSync(wasm.resolveWasmAsset("tree-sitter-sql.wasm")));
1575
+ const grammar = await LanguageCtor.load(bytes);
1576
+ _RealSqlAnalyzer.setGrammar(grammar);
1577
+ } catch {
1578
+ } finally {
1579
+ _RealSqlAnalyzer._loading = false;
1580
+ }
1581
+ })();
1582
+ }
1583
+ /**
1584
+ * Split a SQL file into top-level statements on `;`, honouring the four
1585
+ * contexts a `;` can appear in without ending a statement: a single-quoted
1586
+ * string, a double-quoted identifier, a `--`/`/* *\/` comment, and a
1587
+ * dollar-quoted string. The dollar-quote case is the one that matters — a
1588
+ * PL/pgSQL body is full of `;` and splitting inside it would hand the resync
1589
+ * a fragment that cannot parse.
1590
+ */
1591
+ splitTopLevelStatements(content) {
1592
+ const out = [];
1593
+ let start = 0;
1594
+ let i = 0;
1595
+ let quote = null;
1596
+ let lineComment = false;
1597
+ let blockComment = false;
1598
+ let dollar = null;
1599
+ const origin = this.lineColumnAt(content, 0);
1600
+ const push = (endExclusive) => {
1601
+ const text = content.slice(start, endExclusive);
1602
+ if (text.trim().length === 0) return;
1603
+ const at = this.lineColumnAt(content, start);
1604
+ out.push({ text, start, row: at.row, column: start === 0 ? origin.column + at.column : at.column });
1605
+ };
1606
+ while (i < content.length) {
1607
+ const c = content[i];
1608
+ if (lineComment) {
1609
+ if (c === "\n") lineComment = false;
1610
+ i++;
1611
+ continue;
1612
+ }
1613
+ if (blockComment) {
1614
+ if (c === "*" && content[i + 1] === "/") {
1615
+ blockComment = false;
1616
+ i += 2;
1617
+ continue;
1618
+ }
1619
+ i++;
1620
+ continue;
1621
+ }
1622
+ if (dollar) {
1623
+ if (content.startsWith(dollar, i)) {
1624
+ i += dollar.length;
1625
+ dollar = null;
1626
+ continue;
1627
+ }
1628
+ i++;
1629
+ continue;
1630
+ }
1631
+ if (quote) {
1632
+ if (c === "\\") {
1633
+ i += 2;
1634
+ continue;
1635
+ }
1636
+ if (c === quote) {
1637
+ if (content[i + 1] === quote) {
1638
+ i += 2;
1639
+ continue;
1640
+ }
1641
+ quote = null;
1642
+ }
1643
+ i++;
1644
+ continue;
1645
+ }
1646
+ if (c === "-" && content[i + 1] === "-") {
1647
+ lineComment = true;
1648
+ i += 2;
1649
+ continue;
1650
+ }
1651
+ if (c === "/" && content[i + 1] === "*") {
1652
+ blockComment = true;
1653
+ i += 2;
1654
+ continue;
1655
+ }
1656
+ if (c === "'" || c === '"' || c === "`") {
1657
+ quote = c;
1658
+ i++;
1659
+ continue;
1660
+ }
1661
+ if (c === "$") {
1662
+ const tag = dollarTagAt(content, i);
1663
+ if (tag) {
1664
+ dollar = tag;
1665
+ i += tag.length;
1666
+ continue;
1667
+ }
1668
+ }
1669
+ if (c === ";") {
1670
+ i++;
1671
+ push(i);
1672
+ start = i;
1673
+ continue;
1674
+ }
1675
+ i++;
1676
+ }
1677
+ if (start < content.length) push(content.length);
1678
+ return out;
1679
+ }
1680
+ /** 0-based row and 0-based column of a byte offset. */
1681
+ lineColumnAt(content, offset) {
1682
+ let row = 0;
1683
+ let lineStart = 0;
1684
+ for (let i = 0; i < offset && i < content.length; i++) {
1685
+ if (content[i] === "\n") {
1686
+ row++;
1687
+ lineStart = i + 1;
1688
+ }
1689
+ }
1690
+ return { row, column: offset - lineStart };
1691
+ }
1692
+ // ---------------------------------------------------------------- walk
1693
+ processRoot(node, state) {
1694
+ const stack = [];
1695
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
1696
+ const child = node.namedChild(i);
1697
+ if (child) stack.push(child);
1698
+ }
1699
+ while (stack.length > 0) {
1700
+ const current = stack.pop();
1701
+ if (current.type === "transaction" || current.type === "block") {
1702
+ this.addPattern(current.type === "transaction" ? "Transaction Management" : "Procedural Block", state);
1703
+ this.bumpComplexity(current, state);
1704
+ for (let i = current.namedChildCount - 1; i >= 0; i--) {
1705
+ const child = current.namedChild(i);
1706
+ if (child && (child.type === "statement" || child.type === "block" || child.type === "transaction")) stack.push(child);
1707
+ }
1708
+ continue;
1709
+ }
1710
+ state.localCtes.clear();
1711
+ if (current.type === "statement") {
1712
+ for (const cte of this.descendants(current, "cte")) {
1713
+ const nm = this.firstIdentifier(cte, state);
1714
+ if (nm) state.localCtes.add(this.identifierName(nm, state));
1715
+ }
1716
+ }
1717
+ if (current.type === "statement") state.nestedStatements.add(current.startIndex + state.slice.byteOffset);
1718
+ this.processStatement(current, state, null);
1719
+ }
1720
+ this.sweepDeclarations(node, state);
1721
+ }
1722
+ /**
1723
+ * Every declaration node in the tree, at any depth, each emitted once.
1724
+ *
1725
+ * `program` requires every top-level item to be a `statement`/`block`/
1726
+ * `transaction`, and error recovery routinely buries a declaration inside one
1727
+ * of those: on the corpus's `database/schema.sql` a function whose body the
1728
+ * grammar could not fully parse swallows the rest of the file, and the 158
1729
+ * `CREATE TABLE` nodes after it end up NESTED INSIDE that function's
1730
+ * `function_body` — a declaration inside a declaration. Walking the whole tree
1731
+ * for declaration nodes finds them: 1,644 of 2,336 corpus declarations against
1732
+ * 1,557 for the root-children walk alone.
1733
+ *
1734
+ * The sweep DESCENDS THROUGH declarations rather than stopping at them, which
1735
+ * is the whole point: a `create_table` inside a `create_function` is reachable
1736
+ * only by passing through the function. The earlier version stopped at the first
1737
+ * declaration it met, so every such nested declaration stayed invisible — and
1738
+ * `business_profiles`, `business_rules`, `business_content` and
1739
+ * `business_locations` (the corpus's most-referenced tables) were among the
1740
+ * casualties, 117 edges in the unknown-symbol population of `probe/
1741
+ * unknown-symbols.mts`. `declarationKeys` keeps the walk from dispatching the
1742
+ * same node twice, and `createSymbol`'s identity check makes a re-dispatch
1743
+ * idempotent, so descending is safe and costs one redundant walk of a body.
1744
+ */
1745
+ sweepDeclarations(node, state) {
1746
+ const stack = [node];
1747
+ while (stack.length > 0) {
1748
+ const current = stack.pop();
1749
+ if (current.type === "statement" && !state.nestedStatements.has(current.startIndex + state.slice.byteOffset)) {
1750
+ state.nestedStatements.add(current.startIndex + state.slice.byteOffset);
1751
+ const saved = new Set(state.localCtes);
1752
+ state.localCtes = this.cteScope(current, state);
1753
+ this.processStatement(current, state, null);
1754
+ state.localCtes.clear();
1755
+ state.ambiguity.sweepStatementsDispatched++;
1756
+ for (const name of saved) state.localCtes.add(name);
1757
+ }
1758
+ if (DECLARATION_NODES.has(current.type)) {
1759
+ if (this.markDeclaration(current, state)) {
1760
+ this.dispatchDeclaration(current, state, null);
1761
+ state.ambiguity.sweepDeclarationsRecovered++;
1762
+ }
1763
+ }
1764
+ for (let i = current.namedChildCount - 1; i >= 0; i--) {
1765
+ const child = current.namedChild(i);
1766
+ if (child) stack.push(child);
1767
+ }
1768
+ }
1769
+ }
1770
+ processStatement(node, state, owner) {
1771
+ if (node.type === "statement") {
1772
+ const children = this.namedChildren(node);
1773
+ const isQueryPart = (c) => queryTypes.has(c.type) || c.type === "from" || c.type === "cte";
1774
+ if (children.some(isQueryPart)) this.processQuery(node, state, owner);
1775
+ for (const child of children) {
1776
+ if (isQueryPart(child)) continue;
1777
+ this.processStatement(child, state, owner);
1778
+ }
1779
+ return;
1780
+ }
1781
+ if (DECLARATION_NODES.has(node.type)) {
1782
+ const declaredName = this.declarationOwner(node, state);
1783
+ this.processCalls(node, declaredName, state);
1784
+ if (this.markDeclaration(node, state)) this.dispatchDeclaration(node, state, owner);
1785
+ return;
1786
+ }
1787
+ switch (node.type) {
1788
+ case "alter_table":
1789
+ this.addPattern("Schema Evolution (ALTER)", state);
1790
+ this.recordTableTarget(node, "uses", "alter", state);
1791
+ this.processCalls(node, this.referenceLeaf(this.directChild(node, "object_reference"), state) ?? state.fileModuleName, state);
1792
+ this.processAlterTable(node, state);
1793
+ return;
1794
+ case "drop_table":
1795
+ case "drop_view":
1796
+ case "drop_index":
1797
+ case "drop_type":
1798
+ case "drop_schema":
1799
+ case "drop_sequence":
1800
+ case "drop_column":
1801
+ case "drop_constraint":
1802
+ case "drop_function":
1803
+ case "drop_policy":
1804
+ this.addPattern("Schema Evolution (DROP)", state);
1805
+ this.recordTableTarget(node, "uses", "drop", state);
1806
+ return;
1807
+ case "do_statement":
1808
+ this.processDo(node, state);
1809
+ return;
1810
+ case "insert":
1811
+ this.recordDml(node, "insert", state);
1812
+ return;
1813
+ case "update":
1814
+ this.recordDml(node, "update", state);
1815
+ return;
1816
+ case "delete":
1817
+ this.recordDml(node, "delete", state);
1818
+ return;
1819
+ case "select":
1820
+ case "set_operation":
1821
+ case "cte":
1822
+ case "create_query":
1823
+ case "subquery":
1824
+ this.processQuery(node, state, owner);
1825
+ return;
1826
+ case "set_statement":
1827
+ case "reset_statement":
1828
+ this.addPattern("Session Configuration", state);
1829
+ return;
1830
+ case "comment_statement":
1831
+ this.processComment(node, state);
1832
+ return;
1833
+ case "grant_statement":
1834
+ case "revoke_statement":
1835
+ this.processGrant(node, state, node.type === "grant_statement" ? "grant" : "revoke");
1836
+ return;
1837
+ case "transaction":
1838
+ case "block":
1839
+ this.addPattern("Transaction Management", state);
1840
+ this.bumpComplexity(node, state);
1841
+ return;
1842
+ // No `merge` / `truncate` / `copy` arms: those grammar rules are HIDDEN
1843
+ // (`_merge_statement`, `_truncate_statement`, `_copy_statement`), so the
1844
+ // keywords and references inline into `statement` and a node of those
1845
+ // types can never exist. Measured over the corpus and the dialect battery.
1846
+ // The `keyword_*` arms below are the only real routes.
1847
+ case "keyword_merge":
1848
+ this.processInlineMerge(node, state);
1849
+ return;
1850
+ case "keyword_truncate":
1851
+ this.processInlineTruncate(node, state);
1852
+ return;
1853
+ case "keyword_copy":
1854
+ this.processInlineCopy(node, state);
1855
+ return;
1856
+ case "returning":
1857
+ this.processReturning(node, state);
1858
+ return;
1859
+ case "psql_meta_command":
1860
+ this.processPsqlMetaCommand(node, state);
1861
+ return;
1862
+ default:
1863
+ return;
1864
+ }
1865
+ }
1866
+ /** Record that this declaration node has been dispatched, so neither the main
1867
+ * walk nor the sweep can emit it twice, and so the sweep can report how much
1868
+ * it contributed that the main walk could not reach.
1869
+ *
1870
+ * THE KEY IS IN FILE COORDINATES, and that is load-bearing rather than
1871
+ * cosmetic. `node.startIndex` is relative to the PARSE that produced the node,
1872
+ * and three of this analyzer's passes produce nodes from a re-parsed SLICE: the
1873
+ * main walk parses the whole file (so its offsets are already file offsets),
1874
+ * the resync pass parses one statement at a time, and the EXECUTE-literal pass
1875
+ * parses the text between a literal's quotes. Keyed on the raw `startIndex`,
1876
+ * a declaration inside an EXECUTE literal collided with whatever the main pass
1877
+ * had already seen at the same SLICE-LOCAL offset, and was silently dropped.
1878
+ * `state.slice.byteOffset` maps a slice node back to its true position — the
1879
+ * raw literal is handed to the parser with its `''` escapes intact precisely so
1880
+ * it stays a contiguous substring of the file and that mapping holds — so the
1881
+ * file coordinate is both correct and unique.
1882
+ *
1883
+ * This was invisible while `processStatement` called `markDeclaration` and then
1884
+ * dispatched regardless of the answer: the redundant dispatch is what was
1885
+ * masking the collision. Making the guard load-bearing (`if (markDeclaration(…))`)
1886
+ * and correct are the same piece of work, and `validation/m14/fixtures/sql/
1887
+ * sqld11` is what caught it — its `quoted_name` table lives inside an EXECUTE
1888
+ * literal and vanished the moment the guard started meaning what it says. */
1889
+ /**
1890
+ * The name a declaration's own invocations belong to.
1891
+ *
1892
+ * A function call inside a `CREATE FUNCTION` body is a call BY that function; one
1893
+ * inside a table's column `DEFAULT` is a call BY that table. `global` is the
1894
+ * analyzer's synthetic owner and is only correct where there is no declaration to
1895
+ * attribute to — which is why the anonymous-statement path uses it and this does
1896
+ * not.
1897
+ */
1898
+ declarationOwner(node, state) {
1899
+ const object = this.directChild(node, "object_reference");
1900
+ const nameNode = object ? this.referenceNameNode(object) : this.firstIdentifier(node);
1901
+ if (!nameNode) return state.fileModuleName;
1902
+ return this.identifierName(nameNode, state) || state.fileModuleName;
1903
+ }
1904
+ markDeclaration(node, state) {
1905
+ const key = `${node.startIndex + state.slice.byteOffset}:${node.type}`;
1906
+ if (state.declarationKeys.has(key)) return false;
1907
+ state.declarationKeys.add(key);
1908
+ return true;
1909
+ }
1910
+ dispatchDeclaration(node, state, owner) {
1911
+ if (REPLACEABLE_DECLARATIONS.has(node.type)) this.recordReplacement(node, state);
1912
+ switch (node.type) {
1913
+ case "create_table":
1914
+ this.processTable(node, state);
1915
+ return;
1916
+ case "create_view":
1917
+ case "create_materialized_view":
1918
+ this.processView(node, state);
1919
+ return;
1920
+ case "create_index":
1921
+ this.processIndex(node, state);
1922
+ return;
1923
+ case "create_function":
1924
+ this.processFunction(node, state);
1925
+ return;
1926
+ case "create_procedure":
1927
+ this.processProcedure(node, state);
1928
+ return;
1929
+ case "create_trigger":
1930
+ this.processTrigger(node, state);
1931
+ return;
1932
+ case "create_type":
1933
+ this.processType(node, state);
1934
+ return;
1935
+ case "create_schema":
1936
+ this.processNamedContainer(node, ASTEngine.SymbolKind.Namespace, "schema", state);
1937
+ return;
1938
+ case "create_sequence":
1939
+ this.processNamedContainer(node, ASTEngine.SymbolKind.Constant, "sequence", state);
1940
+ return;
1941
+ case "create_role":
1942
+ this.processNamedContainer(node, ASTEngine.SymbolKind.Namespace, "role", state);
1943
+ return;
1944
+ case "create_extension":
1945
+ this.processExtension(node, state);
1946
+ return;
1947
+ case "create_database":
1948
+ this.processDatabase(node, state);
1949
+ return;
1950
+ case "create_policy":
1951
+ this.processPolicy(node, state);
1952
+ return;
1953
+ case "do_statement":
1954
+ this.processDo(node, state);
1955
+ return;
1956
+ default:
1957
+ return;
1958
+ }
1959
+ }
1960
+ /**
1961
+ * `CREATE OR REPLACE FUNCTION f` when this file ALREADY declared `f` of the
1962
+ * same kind: the second declaration replaces the first.
1963
+ *
1964
+ * The scope is exactly that, and each clause of it is a measured boundary:
1965
+ *
1966
+ * - SAME FILE only. 666 OR REPLACE declarations exist over the 1,460-file
1967
+ * census and all but a handful replace a declaration made in a DIFFERENT
1968
+ * file — a migration chain re-declaring what the previous migration
1969
+ * declared. A per-file analyzer cannot see those, so this emits for the 15
1970
+ * events it can and declares the rest rather than pretending to them.
1971
+ * - SAME KIND only. Keyed on the name alone, `CREATE FUNCTION
1972
+ * hdb_catalog.f()` at line 7 and `CREATE TRIGGER f` at line 25 of Hasura's
1973
+ * `10_to_11.sql` counted as a redefinition; they are two different objects
1974
+ * that happen to share a name, and the probe that produced that number is
1975
+ * the same shape-versus-role error this lane has now hit three times.
1976
+ * - `OR REPLACE` REQUIRED on the later declaration. Measured 20 same-kind
1977
+ * re-declarations carry no `OR REPLACE` and PostgreSQL rejects those
1978
+ * outright ("relation already exists"), so calling them overrides would
1979
+ * assert a replacement the language does not perform.
1980
+ *
1981
+ * The SOURCE is the file's own `module` symbol, this lane's spelling for a file-level fact
1982
+ * about a named object (`uses <file>->t` for a DML target, `references <file>->t` for
1983
+ * `GRANT … ON t`). It was the literal `global` until the file became a symbol; see
1984
+ * `fileModuleNameFor` for the measurement. The source and the target cannot be the same
1985
+ * object: SQL replacement is same-name, so any source/target pair would collapse to a
1986
+ * self-loop `overrides f->f` that tells a graph consumer nothing about which
1987
+ * declaration replaced which. The lines that do distinguish them ride in
1988
+ * metadata instead, which is where a consumer can read them.
1989
+ */
1990
+ recordReplacement(node, state) {
1991
+ const object = this.firstDescendant(node, "object_reference");
1992
+ const nameNode = object ? this.referenceNameNode(object) : this.firstIdentifier(node);
1993
+ if (!nameNode) return;
1994
+ const name = this.identifierName(nameNode, state);
1995
+ const nameKey = `${node.type}:${name}`;
1996
+ const key = `${nameKey}${this.routineSignature(node, state)}`;
1997
+ const line = node.startPosition.row + state.slice.rowOffset + 1;
1998
+ const replaces = this.directChild(node, "keyword_replace") !== null;
1999
+ if (!replaces) {
2000
+ if (!state.redeclared.has(key)) state.redeclared.set(key, line);
2001
+ state.routineNames.add(nameKey);
2002
+ return;
2003
+ }
2004
+ const prior = state.redeclared.get(key);
2005
+ if (prior === void 0) {
2006
+ if (state.routineNames.has(nameKey)) state.ambiguity.overloadNotReplacementSkipped++;
2007
+ state.redeclared.set(key, line);
2008
+ state.routineNames.add(nameKey);
2009
+ return;
2010
+ }
2011
+ this.addRelationship(ASTEngine.RelationshipType.Overrides, state.fileModuleName, name, state, {
2012
+ orReplace: true,
2013
+ redefinition: true,
2014
+ scope: "file",
2015
+ priorLine: prior,
2016
+ replacementLine: line
2017
+ });
2018
+ state.redeclared.set(key, line);
2019
+ state.routineNames.add(nameKey);
2020
+ state.ambiguity.overridesEmitted++;
2021
+ }
2022
+ /**
2023
+ * The argument-type list that makes a ROUTINE's identity, as a key suffix.
2024
+ *
2025
+ * PostgreSQL does not identify a function by its name. `CREATE OR REPLACE
2026
+ * FUNCTION f(a int)` and `CREATE OR REPLACE FUNCTION f(a int, b int)` are two
2027
+ * functions that coexist, which is why `\df f` prints two rows — so an
2028
+ * `overrides` edge between them asserts a replacement the language never
2029
+ * performs. Only the INPUT types participate: `RETURNS TABLE (…)` declares OUT
2030
+ * parameters and is not part of the call signature.
2031
+ *
2032
+ * Returns `""` for every non-routine kind. A VIEW has no arguments and no
2033
+ * overloading, so `CREATE OR REPLACE VIEW v` twice really is one object
2034
+ * replacing itself, and the key stays name-only exactly as before.
2035
+ *
2036
+ * The types are lowercased and comma-joined so a spelling difference that
2037
+ * PostgreSQL does not distinguish (`INTEGER` / `integer`) cannot split one
2038
+ * identity into two keys.
2039
+ */
2040
+ routineSignature(node, state) {
2041
+ if (node.type !== "create_function" && node.type !== "create_procedure") return "";
2042
+ const args = this.directChild(node, "function_arguments");
2043
+ if (!args) return "()";
2044
+ const types = [];
2045
+ for (const argument of this.namedChildren(args)) {
2046
+ if (argument.type !== "function_argument") continue;
2047
+ const param = this.functionArgument(argument, state);
2048
+ if (param?.direction === "out") continue;
2049
+ types.push(this.argumentTypeText(argument, state).toLowerCase());
2050
+ }
2051
+ return `(${types.join(",")})`;
2052
+ }
2053
+ /**
2054
+ * One parameter's declared type, read even when the parameter has NO NAME.
2055
+ *
2056
+ * WHY THIS IS NOT `functionArgument(...).type`. A PostgreSQL parameter may be
2057
+ * written without a name — `CREATE FUNCTION f(integer, text)` is legal and is
2058
+ * the form the corpus's own overloads use. `functionArgument` looks for the
2059
+ * parameter's name first and returns `null` when there is none, so reading the
2060
+ * type through it collapsed every unnamed parameter to the empty string: the
2061
+ * 60-file incoming-gate population's remaining `overrides` edges after the
2062
+ * first signature fix were `overloaded_unnamed_param` (5 declarations),
2063
+ * `overloaded_unnamed_json_jsonb_param` (3), `color` (2) and `isodate` (2) —
2064
+ * every one of them a set of unnamed-parameter overloads whose types the key
2065
+ * could not see.
2066
+ *
2067
+ * The discriminator is CHILD ORDER, not a search, and that is the pinned
2068
+ * grammar's own layout: the mode keyword (if any) comes first, then the
2069
+ * parameter NAME if it has one, then the type. `firstIdentifier` cannot be used
2070
+ * to find the name, because an unnamed `object_reference` type such as
2071
+ * `my_type` contains an `identifier` descendant of its own and would be
2072
+ * mistaken for the parameter's name — the same shape-versus-role error that
2073
+ * produced the fabricated FK on every custom-typed column (SQL-D1).
2074
+ *
2075
+ * The `array_size_definition` suffix is KEPT: PostgreSQL treats `integer[]` and
2076
+ * `integer` as different input argument types, so dropping the `[]` would merge
2077
+ * two distinct routines.
2078
+ */
2079
+ argumentTypeText(argument, state) {
2080
+ const custom = argument.childForFieldName?.("custom_type");
2081
+ if (custom) {
2082
+ const sized = this.namedChildren(argument).some((c) => c.type === "array_size_definition");
2083
+ return `${this.referenceText(custom, state)}${sized ? "[]" : ""}`;
2084
+ }
2085
+ const children = this.namedChildren(argument);
2086
+ let i = 0;
2087
+ while (i < children.length && MODE_KEYWORD_TYPES.has(children[i].type)) i++;
2088
+ if (children[i]?.type === "identifier") {
2089
+ return this.spanAfter(argument, children[i], (c) => c.type === "keyword_default" || c.type === "literal");
2090
+ }
2091
+ const parts = [];
2092
+ for (; i < children.length; i++) {
2093
+ const t = children[i].text.trim();
2094
+ if (t.length > 0) parts.push(t);
2095
+ }
2096
+ return parts.join(" ");
2097
+ }
2098
+ // ---------------------------------------------------------------- tables
2099
+ processTable(node, state) {
2100
+ const object = this.directChild(node, "object_reference");
2101
+ const nameNode = object ? this.referenceNameNode(object) : this.firstIdentifier(node);
2102
+ if (!nameNode) return;
2103
+ const table = this.createSymbol(
2104
+ this.identifierName(nameNode, state),
2105
+ ASTEngine.SymbolKind.Class,
2106
+ nameNode,
2107
+ null,
2108
+ state,
2109
+ {
2110
+ type: "table",
2111
+ isSchemaEntity: true,
2112
+ qualifiedName: object ? this.referenceText(object, state) : this.identifierName(nameNode, state)
2113
+ }
2114
+ );
2115
+ const childOwner = state.symbols.find((s) => s.name === table.name && s.metadata?.type === "table");
2116
+ for (const clause of this.namedChildren(node).filter((c) => c.type === "inherit_clause")) {
2117
+ const parents = this.namedChildren(clause).filter((c) => c.type === "object_reference").map((c) => this.identifierName(this.referenceNameNode(c) ?? c, state)).filter((n) => n.length > 0);
2118
+ if (!childOwner) continue;
2119
+ for (const parent of parents) {
2120
+ this.addRelationship(ASTEngine.RelationshipType.Extends, table.name, parent, state, {
2121
+ clause: "inherits",
2122
+ evidenceClass: "structural"
2123
+ }, this.range(clause, state).startLine);
2124
+ }
2125
+ }
2126
+ const definitions = this.directChild(node, "column_definitions");
2127
+ if (definitions) {
2128
+ for (const column of this.namedChildren(definitions)) {
2129
+ if (column.type === "column_definition") this.processColumn(column, table, state);
2130
+ }
2131
+ const tableConstraints = this.directChild(definitions, "constraints");
2132
+ if (tableConstraints) {
2133
+ for (const child of this.namedChildren(tableConstraints)) {
2134
+ if (child.type === "constraint") {
2135
+ this.processConstraint(child, table, state);
2136
+ } else {
2137
+ const recovered = this.recoverNamedConstraint(child);
2138
+ if (recovered) this.processConstraint(recovered, table, state);
2139
+ }
2140
+ }
2141
+ }
2142
+ }
2143
+ this.addPattern("Tables", state);
2144
+ }
2145
+ /**
2146
+ * Is this column name a reserved word that error recovery re-emitted?
2147
+ *
2148
+ * Only an UNQUOTED name can be one: a quoted `"like"` arrives as a `literal` and
2149
+ * is a legal column name in every dialect, so the guard keys on the node type as
2150
+ * well as the text. See `RESERVED_AS_COLUMN_NAME` for the measured counts and for
2151
+ * why this list is two words and not the whole keyword set.
2152
+ */
2153
+ reservedColumnName(node) {
2154
+ if (node.type !== "identifier") return false;
2155
+ if (!RESERVED_AS_COLUMN_NAME.has(node.text.toLowerCase())) return false;
2156
+ return true;
2157
+ }
2158
+ /**
2159
+ * A `CONSTRAINT <name> FOREIGN KEY` the grammar could not build a `constraint`
2160
+ * node for arrives as an ERROR whose own children are still
2161
+ * `keyword_constraint` + `identifier`. Rebuild a constraint-shaped view of it
2162
+ * so `processConstraint` has a node to read, and say so in the symbol's
2163
+ * metadata — the recovery is deterministic, but it is a recovery.
2164
+ */
2165
+ recoverNamedConstraint(node) {
2166
+ if (node.type !== "ERROR" && !node.hasError) return null;
2167
+ const text = node.text;
2168
+ if (!opensWithConstraintKeyword(text)) return null;
2169
+ const children = this.namedChildren(node);
2170
+ const keywordAt = children.findIndex((c) => c.type === "keyword_constraint");
2171
+ if (keywordAt < 0) return null;
2172
+ const nameAt = children.findIndex((c, i) => i > keywordAt && identifierTypes.has(c.type));
2173
+ if (nameAt < 0) return null;
2174
+ const rest = this.descendants(node, "constraint")[0];
2175
+ void rest;
2176
+ return node;
2177
+ }
2178
+ processColumn(node, table, state) {
2179
+ const rawName = node.childForFieldName?.("name") ?? this.firstIdentifier(node);
2180
+ if (!rawName || !identifierTypes.has(rawName.type)) return;
2181
+ if (this.reservedColumnName(rawName)) {
2182
+ state.ambiguity.reservedNameSymbolsSuppressed++;
2183
+ return;
2184
+ }
2185
+ let isPrimaryKey = false;
2186
+ let isUnique = false;
2187
+ let isNullable = true;
2188
+ let references;
2189
+ let referenceNode = null;
2190
+ const children = this.namedChildren(node);
2191
+ for (let i = 0; i < children.length; i++) {
2192
+ const child = children[i];
2193
+ if (child.type === "keyword_primary") isPrimaryKey = true;
2194
+ if (child.type === "keyword_unique") isUnique = true;
2195
+ if (child.type === "keyword_not" && children[i + 1]?.type === "keyword_null") isNullable = false;
2196
+ if (child.type === "object_reference" && children[i - 1]?.type === "keyword_references") {
2197
+ referenceNode = child;
2198
+ references = this.referenceLeaf(child, state);
2199
+ }
2200
+ }
2201
+ const column = this.createSymbol(
2202
+ this.identifierName(rawName, state),
2203
+ ASTEngine.SymbolKind.Property,
2204
+ rawName,
2205
+ table,
2206
+ state,
2207
+ {
2208
+ type: "column",
2209
+ sqlType: this.columnType(node, state),
2210
+ isPrimaryKey,
2211
+ isUnique,
2212
+ isNullable,
2213
+ references
2214
+ }
2215
+ );
2216
+ this.addRelationship(ASTEngine.RelationshipType.Contains, table.name, column.name, state, { memberKind: ASTEngine.SymbolKind.Property }, this.range(rawName, state).startLine, table.id, column.id);
2217
+ if (isPrimaryKey) this.addPattern("Primary Key Constraint", state);
2218
+ if (isUnique) this.addPattern("Unique Constraint", state);
2219
+ if (references && referenceNode) {
2220
+ this.addPattern("Foreign Key Constraints", state);
2221
+ this.addRelationship(ASTEngine.RelationshipType.References, table.name, references, state, {
2222
+ constraint: "foreign_key",
2223
+ column: column.name
2224
+ });
2225
+ }
2226
+ }
2227
+ /**
2228
+ * A column's declared type.
2229
+ *
2230
+ * The grammar puts `type` on the type node and, for an array, on
2231
+ * `array_size_definition` as well — `childForFieldName("type")` returns only
2232
+ * the first, so `c int[]` reported `"int"`. Both fields are read and the
2233
+ * array suffix appended. A user-defined type arrives as `custom_type` (which
2234
+ * `childForFieldName("type")` also resolves) and is returned whole, so
2235
+ * `my_schema.my_type` and `character varying(20)` survive intact.
2236
+ */
2237
+ columnType(node, state) {
2238
+ const parts = [];
2239
+ for (let i = 0; i < node.childCount; i++) {
2240
+ const field = node.fieldNameForChild(i);
2241
+ if (field !== "type" && field !== "custom_type") continue;
2242
+ const child = node.child(i);
2243
+ if (!child) continue;
2244
+ if (child.type === "identifier" || child.type === "literal") continue;
2245
+ if (parts.some((p) => p.startIndex === child.startIndex)) continue;
2246
+ parts.push({ text: child.text, startIndex: child.startIndex, endIndex: child.endIndex });
2247
+ }
2248
+ if (parts.length === 0) {
2249
+ const fallback = node.childForFieldName?.("type") ?? node.childForFieldName?.("custom_type");
2250
+ if (fallback) parts.push({ text: fallback.text, startIndex: fallback.startIndex, endIndex: fallback.endIndex });
2251
+ }
2252
+ void state;
2253
+ if (parts.length === 0) return void 0;
2254
+ parts.sort((a, b) => a.startIndex - b.startIndex);
2255
+ let out = "";
2256
+ for (const part of parts) {
2257
+ if (out && part.startIndex > parts[parts.indexOf(part) - 1].endIndex) out += " ";
2258
+ out += part.text;
2259
+ }
2260
+ return out;
2261
+ }
2262
+ processConstraint(node, table, state) {
2263
+ const children = this.namedChildren(node);
2264
+ const keywordAt = children.findIndex((c) => c.type === "keyword_constraint");
2265
+ let nameNode = null;
2266
+ if (keywordAt >= 0) {
2267
+ nameNode = children.slice(keywordAt + 1).find((c) => identifierTypes.has(c.type)) ?? null;
2268
+ } else {
2269
+ nameNode = node.childForFieldName?.("name") ?? null;
2270
+ }
2271
+ if (!nameNode && keywordAt < 0) {
2272
+ const cand = this.directChild(node, "identifier");
2273
+ const cols = children.find((c) => c.type === "ordered_columns" || c.type === "column");
2274
+ if (cand && (!cols || cand.startIndex < cols.startIndex)) nameNode = cand;
2275
+ }
2276
+ const hasPrimary = this.containsChildType(node, "keyword_primary");
2277
+ const hasUnique = this.containsChildType(node, "keyword_unique");
2278
+ const referenceNode = this.referencesTarget(node);
2279
+ const reference = referenceNode ? this.referenceLeaf(referenceNode, state) : void 0;
2280
+ const recovered = node.type === "ERROR";
2281
+ if (nameNode) {
2282
+ const constraint = this.createSymbol(
2283
+ this.identifierName(nameNode, state),
2284
+ ASTEngine.SymbolKind.Constant,
2285
+ nameNode,
2286
+ table,
2287
+ state,
2288
+ {
2289
+ type: "constraint",
2290
+ constraintKind: reference ? "foreign_key" : hasPrimary ? "primary_key" : hasUnique ? "unique" : "check",
2291
+ ...recovered ? { recoveredFromErrorRegion: true } : {}
2292
+ }
2293
+ );
2294
+ this.addRelationship(ASTEngine.RelationshipType.Contains, table.name, constraint.name, state, { memberKind: ASTEngine.SymbolKind.Constant }, this.range(nameNode, state).startLine, table.id, constraint.id);
2295
+ }
2296
+ if (hasPrimary) this.addPattern("Primary Key Constraint", state);
2297
+ if (hasUnique) this.addPattern("Unique Constraint", state);
2298
+ if (reference) {
2299
+ this.addPattern("Foreign Key Constraints", state);
2300
+ this.addRelationship(ASTEngine.RelationshipType.References, table.name, reference, state, { constraint: "foreign_key" });
2301
+ }
2302
+ }
2303
+ /** The `object_reference` a `REFERENCES` clause points at, if there is one. */
2304
+ referencesTarget(node) {
2305
+ const children = this.namedChildren(node);
2306
+ for (let i = 0; i < children.length; i++) {
2307
+ if (children[i].type === "keyword_references" && children[i + 1]?.type === "object_reference") {
2308
+ return children[i + 1];
2309
+ }
2310
+ }
2311
+ return null;
2312
+ }
2313
+ // ---------------------------------------------------------------- views / indexes
2314
+ processView(node, state) {
2315
+ const object = this.firstDescendant(node, "object_reference");
2316
+ const nameNode = object ? this.referenceNameNode(object) : this.firstIdentifier(node);
2317
+ if (!nameNode) return;
2318
+ const kind = node.type === "create_materialized_view" ? "materialized_view" : "view";
2319
+ const view = this.createSymbol(
2320
+ this.identifierName(nameNode, state),
2321
+ ASTEngine.SymbolKind.Class,
2322
+ nameNode,
2323
+ null,
2324
+ state,
2325
+ { type: kind, isSchemaEntity: true, qualifiedName: object ? this.referenceText(object, state) : this.identifierName(nameNode, state) }
2326
+ );
2327
+ this.addPattern(kind === "view" ? "Views" : "Materialized Views", state);
2328
+ const query = this.firstDescendant(node, "create_query");
2329
+ if (query) this.processQuery(query, state, view);
2330
+ }
2331
+ processIndex(node, state) {
2332
+ const identifiers = this.namedChildren(node).filter((c) => c.type === "identifier" || c.type === "literal");
2333
+ const nameNode = identifiers[0] ?? this.firstIdentifier(node);
2334
+ if (!nameNode) return;
2335
+ const object = this.firstDescendant(node, "object_reference");
2336
+ const tableName = object ? this.referenceLeaf(object, state) : void 0;
2337
+ let hasOwnName = false;
2338
+ for (let i = 0; i < node.childCount; i++) {
2339
+ if (node.fieldNameForChild(i) === "column") {
2340
+ hasOwnName = true;
2341
+ break;
2342
+ }
2343
+ }
2344
+ if (!hasOwnName) {
2345
+ state.ambiguity.unnamedIndexSkipped++;
2346
+ return;
2347
+ }
2348
+ const name = this.identifierName(nameNode, state);
2349
+ const owner = tableName ? this.existingSymbol(tableName, ASTEngine.SymbolKind.Class, state) : void 0;
2350
+ this.createSymbol(name, ASTEngine.SymbolKind.Constant, nameNode, owner ?? null, state, {
2351
+ type: "index",
2352
+ indexName: name,
2353
+ table: tableName,
2354
+ columns: this.identifierTexts(this.firstDescendant(node, "index_fields"), state),
2355
+ ...tableName && !owner ? { ownerDeclaredLater: true } : {}
2356
+ });
2357
+ this.addPattern("Custom Indexes", state);
2358
+ if (tableName) {
2359
+ this.addRelationship(ASTEngine.RelationshipType.References, name, tableName, state, { dependency: true, indexOn: tableName });
2360
+ this.addRelationship(ASTEngine.RelationshipType.Contains, tableName, name, state, { memberKind: ASTEngine.SymbolKind.Constant }, this.range(nameNode, state).startLine, owner?.id ?? tableName, owner ? `${owner.id}.${name}` : name);
2361
+ if (state.metadata.performanceScore !== void 0) state.metadata.performanceScore += 10;
2362
+ }
2363
+ }
2364
+ // ---------------------------------------------------------------- functions
2365
+ processFunction(node, state) {
2366
+ this.processCallable(node, state, { kind: ASTEngine.SymbolKind.Function, pattern: "Stored Functions" });
2367
+ }
2368
+ processProcedure(node, state) {
2369
+ this.processCallable(node, state, { kind: ASTEngine.SymbolKind.Function, pattern: "Procedures", type: "procedure" });
2370
+ }
2371
+ processCallable(node, state, opts) {
2372
+ const object = this.firstDescendant(node, "object_reference");
2373
+ const nameNode = object ? this.referenceNameNode(object) : this.firstIdentifier(node);
2374
+ if (!nameNode) return;
2375
+ const name = this.identifierName(nameNode, state);
2376
+ const params = this.callableParameters(node, state);
2377
+ const returns = this.returnTypeText(node, state);
2378
+ const returnsTable = this.returnsTableNode(node);
2379
+ this.createSymbol(name, opts.kind, nameNode, null, state, {
2380
+ ...opts.type ? { type: opts.type } : {},
2381
+ functionName: name,
2382
+ qualifiedName: object ? this.referenceText(object, state) : name,
2383
+ sqlFunction: true,
2384
+ ...returnsTable ? { returnsTable: true } : {}
2385
+ }, { parameters: params, returnType: returns });
2386
+ this.addPattern(opts.pattern, state);
2387
+ this.processQueryDependencies(node, null, state, name);
2388
+ this.processCalls(node, name, state);
2389
+ }
2390
+ /**
2391
+ * A parameter's declared type, read from the grammar's own layout.
2392
+ *
2393
+ * `function_argument` has NO `type` field at all — only `custom_type`, and only
2394
+ * when the type is a user-defined name. The builtin type is simply the child
2395
+ * after the parameter's name, which is why the previous accessor (matching
2396
+ * `type` or `keyword_*`) lost `integer`, `bigint`, `numeric`, `varchar`,
2397
+ * `timestamp` and `uuid` — 69 parameters on the corpus, and inconsistently:
2398
+ * `UUID` and `TEXT` came through, `bigint` did not.
2399
+ *
2400
+ * It also matched the mode keyword first, so `IN a integer` produced
2401
+ * `{ name: "a", type: "IN" }` — a fabricated type. Everything from the name to
2402
+ * the DEFAULT is the type; the mode keyword sits BEFORE the name and so falls
2403
+ * outside the span on its own.
2404
+ */
2405
+ callableParameters(node, state) {
2406
+ const params = [];
2407
+ const args = this.directChild(node, "function_arguments");
2408
+ if (args) {
2409
+ for (const argument of this.namedChildren(args)) {
2410
+ if (argument.type !== "function_argument") continue;
2411
+ const param = this.functionArgument(argument, state);
2412
+ if (param) params.push(param);
2413
+ }
2414
+ }
2415
+ const table = this.returnsTableNode(node);
2416
+ if (table) {
2417
+ for (const column of this.namedChildren(table)) {
2418
+ if (column.type !== "column_definition") continue;
2419
+ const nameNode = column.childForFieldName?.("name") ?? this.firstIdentifier(column);
2420
+ if (!nameNode) continue;
2421
+ params.push({
2422
+ name: this.identifierName(nameNode, state),
2423
+ type: this.columnType(column, state),
2424
+ direction: "out"
2425
+ });
2426
+ }
2427
+ }
2428
+ return params;
2429
+ }
2430
+ functionArgument(argument, state) {
2431
+ const nameNode = argument.childForFieldName?.("name") ?? this.firstIdentifier(argument);
2432
+ if (!nameNode) return null;
2433
+ const children = this.namedChildren(argument);
2434
+ const nameIndex = children.findIndex((c) => c.startIndex === nameNode.startIndex);
2435
+ const hasDefault = children.some((c) => c.type === "keyword_default");
2436
+ const type = this.spanAfter(argument, nameNode, (c) => c.type === "keyword_default" || c.type === "literal");
2437
+ const before = nameIndex >= 0 ? children.slice(0, nameIndex) : [];
2438
+ const modeNode = before.find((c) => c.type === "keyword_inout") ?? before.find((c) => c.type === "keyword_in") ?? before.find((c) => c.type === "keyword_out");
2439
+ const param = { name: this.identifierName(nameNode, state) };
2440
+ if (type) param.type = type;
2441
+ if (hasDefault) param.isOptional = true;
2442
+ const direction = modeNode?.type === "keyword_inout" ? "inout" : modeNode?.type === "keyword_in" ? "in" : modeNode?.type === "keyword_out" ? "out" : void 0;
2443
+ if (direction) param.direction = direction;
2444
+ return param;
2445
+ }
2446
+ /**
2447
+ * The source text between `fromExclusive` and the first child that `stopAt`
2448
+ * accepts, read out of the container's own text so the spacing is the
2449
+ * source's own (`int[]`, not `int []`; `numeric(10,2)`, not `numeric (10,2)`).
2450
+ */
2451
+ spanAfter(container, fromExclusive, stopAt) {
2452
+ const from = fromExclusive.endIndex - container.startIndex;
2453
+ let to = container.endIndex - container.startIndex;
2454
+ for (const child of this.namedChildren(container)) {
2455
+ if (child.startIndex < fromExclusive.endIndex) continue;
2456
+ if (stopAt(child)) {
2457
+ to = child.startIndex - container.startIndex;
2458
+ break;
2459
+ }
2460
+ }
2461
+ if (to <= from) return "";
2462
+ return container.text.substring(from, to).trim();
2463
+ }
2464
+ /** The `column_definitions` of a `RETURNS TABLE (...)`, if that is the return. */
2465
+ returnsTableNode(node) {
2466
+ const children = this.namedChildren(node);
2467
+ const returnsAt = children.findIndex((c) => c.type === "keyword_returns");
2468
+ if (returnsAt < 0) return null;
2469
+ if (children[returnsAt + 1]?.type !== "keyword_table") return null;
2470
+ return children[returnsAt + 2]?.type === "column_definitions" ? children[returnsAt + 2] : null;
2471
+ }
2472
+ /**
2473
+ * Everything after `RETURNS`, up to the body or the language clause.
2474
+ *
2475
+ * The previous version took the single next sibling, which read `int` for
2476
+ * `RETURNS int[]` (the `[]` is a following `array_size_definition`), `SETOF`
2477
+ * for `RETURNS SETOF record`, and `TABLE` for `RETURNS TABLE (a int, b text)`.
2478
+ */
2479
+ returnTypeText(node, state) {
2480
+ const children = this.namedChildren(node);
2481
+ const returnsAt = children.findIndex((c) => c.type === "keyword_returns");
2482
+ if (returnsAt < 0) return void 0;
2483
+ void state;
2484
+ return this.spanAfter(
2485
+ node,
2486
+ children[returnsAt],
2487
+ (c) => c.type === "function_body" || c.type === "function_language" || c.type === "function_volatility" || c.type === "function_strictness"
2488
+ ) || void 0;
2489
+ }
2490
+ // ---------------------------------------------------------------- triggers
2491
+ /**
2492
+ * A trigger's name, its table, and the function it executes.
2493
+ *
2494
+ * These were read as the FIRST THREE `object_reference`s anywhere in the
2495
+ * subtree. A `WHEN (old.a <> new.a)` clause contributes two more
2496
+ * (`old` and `new` are `object_reference`s in this grammar), so the action
2497
+ * function was read as `old` and the analyzer emitted
2498
+ * `calls tg -> old` instead of `calls tg -> audit_it`. The three references are
2499
+ * read from the trigger's own top level instead: the first is the name, the one
2500
+ * after `keyword_on` is the table, and the last is the executed routine.
2501
+ */
2502
+ processTrigger(node, state) {
2503
+ const top = this.namedChildren(node).filter((c) => c.type === "object_reference");
2504
+ if (top.length === 0) return;
2505
+ const nameRef = top[0];
2506
+ const onAt = this.namedChildren(node).findIndex((c) => c.type === "keyword_on");
2507
+ const tableRef = onAt >= 0 ? this.namedChildren(node).slice(onAt + 1).find((c) => c.type === "object_reference") ?? null : top[1] ?? null;
2508
+ const actionRef = top.length > 1 ? top[top.length - 1] : null;
2509
+ const nameNode = this.referenceNameNode(nameRef);
2510
+ if (!nameNode) return;
2511
+ const name = this.identifierName(nameNode, state);
2512
+ const target = tableRef && tableRef !== nameRef ? this.referenceLeaf(tableRef, state) : void 0;
2513
+ const action = actionRef && actionRef !== nameRef && actionRef !== tableRef ? this.referenceLeaf(actionRef, state) : void 0;
2514
+ this.createSymbol(name, ASTEngine.SymbolKind.Function, nameNode, null, state, {
2515
+ type: "trigger",
2516
+ isTrigger: true,
2517
+ targetTable: target,
2518
+ actionFunction: action
2519
+ });
2520
+ this.addPattern("Triggers", state);
2521
+ if (target) this.addRelationship(ASTEngine.RelationshipType.References, name, target, state, { triggerTarget: true });
2522
+ if (action) {
2523
+ const parts = actionRef ? this.referenceParts(actionRef, state) : [];
2524
+ this.addRelationship(
2525
+ ASTEngine.RelationshipType.Calls,
2526
+ name,
2527
+ action,
2528
+ state,
2529
+ { triggerAction: true, ...parts.length > 1 ? { qualified: parts.join(".") } : {} }
2530
+ );
2531
+ }
2532
+ }
2533
+ // ---------------------------------------------------------------- types / containers
2534
+ processType(node, state) {
2535
+ const object = this.firstDescendant(node, "object_reference");
2536
+ const nameNode = object ? this.referenceNameNode(object) : this.firstIdentifier(node);
2537
+ if (!nameNode) return;
2538
+ const enumElements = this.firstDescendant(node, "enum_elements");
2539
+ const values = this.literalTexts(enumElements);
2540
+ const attributes = this.directChild(node, "column_definitions");
2541
+ const fields = attributes ? this.namedChildren(attributes).filter((c) => c.type === "column_definition").map((c) => {
2542
+ const fieldName = c.childForFieldName?.("name") ?? this.firstIdentifier(c);
2543
+ return fieldName ? this.identifierName(fieldName, state) : void 0;
2544
+ }).filter((v) => !!v) : [];
2545
+ this.createSymbol(this.identifierName(nameNode, state), ASTEngine.SymbolKind.Type, nameNode, null, state, {
2546
+ type: values.length > 0 ? "enum" : "custom_type",
2547
+ enumValues: values,
2548
+ ...fields.length ? { fields } : {},
2549
+ customType: true
2550
+ });
2551
+ this.addPattern(values.length > 0 ? "Enum Types" : "Custom Types", state);
2552
+ }
2553
+ processNamedContainer(node, kind, type, state) {
2554
+ const object = this.firstDescendant(node, "object_reference");
2555
+ const nameNode = object ? this.referenceNameNode(object) : this.firstIdentifier(node);
2556
+ if (!nameNode) return;
2557
+ this.createSymbol(this.identifierName(nameNode, state), kind, nameNode, null, state, { type, isSchemaEntity: true });
2558
+ this.addPattern(type === "schema" ? "Schemas" : type === "sequence" ? "Sequences" : type === "extension" ? "Extensions" : "Roles", state);
2559
+ }
2560
+ /**
2561
+ * `CREATE EXTENSION name` — a declaration AND a dependency, and only the
2562
+ * declaration was being read.
2563
+ *
2564
+ * The extension is the closest thing SQL has to a separately published unit: it
2565
+ * ships its own types, functions and operators, it lives outside this unit, and
2566
+ * the database cannot run this file without it. `processNamedContainer` recorded
2567
+ * the symbol and stopped there, so 27 declarations over the 1,460-file census
2568
+ * produced 27 `module` symbols and **no edge at all** — a real cross-unit
2569
+ * dependency, invisible in the graph, in the one construct where SQL genuinely
2570
+ * has one.
2571
+ *
2572
+ * The edge is `imports`, and it is the same type the psql `\i`/`\ir` pass emits
2573
+ * for a different reason: a dependency on a named artifact resolved from outside
2574
+ * the file before the file's own SQL can mean anything. The two mechanisms are
2575
+ * kept distinguishable in metadata (`extensionInstall` vs `psqlInclude`) because
2576
+ * they answer different questions — one is "this file loads another FILE", the
2577
+ * other is "this file requires another DISTRIBUTION to be installed" — and a
2578
+ * consumer that cannot tell them apart will read a `\i schema.sql` edge as
2579
+ * evidence about the environment.
2580
+ *
2581
+ * WHY the FILE MODULE as the SOURCE and why the target is an external name. The
2582
+ * extension is not declared by this file; it is installed into the DATABASE.
2583
+ * Labelling the edge `external` keeps it distinguishable from a psql include,
2584
+ * whose target IS a file in this repository and is in principle resolvable. That
2585
+ * makes the extension edges the only `imports` edges in this lane with a target
2586
+ * that can never bind, which is a property of the construct and is stated here
2587
+ * rather than left for a reader to discover from the resolution rate.
2588
+ *
2589
+ * `evidenceClass` is `structural`: `create_extension` is a visible rule and the
2590
+ * name is in the `object_reference` the rule puts there. Nothing here is a name
2591
+ * match. `CREATE EXTENSION IF NOT EXISTS` is the same shape with a guard
2592
+ * keyword, so it is the same edge — the extension is still required.
2593
+ */
2594
+ processExtension(node, state) {
2595
+ this.processNamedContainer(node, ASTEngine.SymbolKind.Module, "extension", state);
2596
+ const object = this.firstDescendant(node, "object_reference");
2597
+ const nameNode = object ? this.referenceNameNode(object) : this.firstIdentifier(node);
2598
+ if (!nameNode) {
2599
+ state.ambiguity.includeNonLiteral++;
2600
+ return;
2601
+ }
2602
+ const name = this.identifierName(nameNode, state);
2603
+ if (SQL_KEYWORDS.has(name.toLowerCase())) {
2604
+ state.ambiguity.keywordTargetsSuppressed++;
2605
+ return;
2606
+ }
2607
+ const line = node.startPosition.row + state.slice.rowOffset + 1;
2608
+ this.addRelationship(ASTEngine.RelationshipType.Imports, state.fileModuleName, name, state, {
2609
+ extensionInstall: true,
2610
+ external: true,
2611
+ fileDependency: false,
2612
+ scope: "database",
2613
+ line
2614
+ });
2615
+ state.ambiguity.extensionImportsEmitted++;
2616
+ state.imports.push({
2617
+ module: name,
2618
+ items: [],
2619
+ importType: "sql_extension",
2620
+ lineNumber: line,
2621
+ line,
2622
+ range: this.range(node, state),
2623
+ isExternal: true,
2624
+ metadata: { extensionInstall: true, scope: "database" }
2625
+ });
2626
+ }
2627
+ processDatabase(node, state) {
2628
+ const object = this.directChild(node, "object_reference");
2629
+ const nameNode = object ? this.referenceNameNode(object) : this.firstIdentifier(node);
2630
+ if (!nameNode) return;
2631
+ this.createSymbol(this.identifierName(nameNode, state), ASTEngine.SymbolKind.Namespace, nameNode, null, state, {
2632
+ type: "database",
2633
+ isSchemaEntity: true
2634
+ });
2635
+ this.addPattern("Databases", state);
2636
+ }
2637
+ /**
2638
+ * `CREATE POLICY <name> ON <table> …` — a row-level-security policy.
2639
+ *
2640
+ * The name is the first `object_reference` and the table is the one after
2641
+ * `keyword_on`; both are real schema entities, so the policy is a symbol and
2642
+ * the table is an edge. `AS PERMISSIVE|RESTRICTIVE` and `TO <role>` are
2643
+ * deliberately not edged: a role is not a table, which is the same rule the
2644
+ * GRANT rule is written to keep.
2645
+ *
2646
+ * The policy's USING/WITH CHECK expressions are read through the same
2647
+ * declaration sweep as any other query node, so tables named inside them are
2648
+ * still reached.
2649
+ */
2650
+ processPolicy(node, state) {
2651
+ const children = this.namedChildren(node);
2652
+ const nameRef = children.find((c) => c.type === "object_reference");
2653
+ const onAt = children.findIndex((c) => c.type === "keyword_on");
2654
+ const targetRef = onAt >= 0 ? children.slice(onAt + 1).find((c) => c.type === "object_reference") ?? null : null;
2655
+ const nameNode = nameRef ? this.referenceNameNode(nameRef) : null;
2656
+ if (!nameRef || !nameNode) return;
2657
+ const name = this.identifierName(nameNode, state);
2658
+ this.createSymbol(name, ASTEngine.SymbolKind.Constant, nameNode, null, state, {
2659
+ type: "policy",
2660
+ isSchemaEntity: true,
2661
+ ...targetRef ? { policyOn: this.referenceLeaf(targetRef, state) } : {}
2662
+ });
2663
+ this.addPattern("Row Level Security", state);
2664
+ const target = targetRef ? this.referenceLeaf(targetRef, state) : void 0;
2665
+ if (target) this.addRelationship(ASTEngine.RelationshipType.References, name, target, state, { policyOn: true });
2666
+ }
2667
+ /**
2668
+ * `DO $$ … $$` — an anonymous procedural block.
2669
+ *
2670
+ * It declares nothing, so it mints no symbol, but the body is a real query
2671
+ * when the grammar could parse it (`EXECUTE format('DROP POLICY …')` and
2672
+ * `SELECT … FROM pg_policies` are the shapes a migrations corpus is full of),
2673
+ * so its table references are read and the procedural pattern is reported.
2674
+ */
2675
+ processDo(node, state) {
2676
+ this.addPattern("Anonymous Procedural Block", state);
2677
+ this.bumpComplexity(node, state);
2678
+ this.processQueryDependencies(node, null, state);
2679
+ this.processCalls(node, state.fileModuleName, state);
2680
+ }
2681
+ // ---------------------------------------------------------------- dml
2682
+ recordDml(node, operation, state) {
2683
+ const scope = node.parent?.type === "statement" ? node.parent : node;
2684
+ const object = this.dmlTargetNode(node);
2685
+ const target = object ? this.referenceLeaf(object, state) : this.referenceLeaf(this.firstDescendant(scope, "object_reference"), state);
2686
+ if (target) {
2687
+ const parts = object ? this.referenceParts(object, state) : [];
2688
+ this.addRelationship(ASTEngine.RelationshipType.Uses, state.fileModuleName, target, state, {
2689
+ operation,
2690
+ tableReference: true,
2691
+ ...parts.length > 1 ? { qualified: parts.join(".") } : {}
2692
+ });
2693
+ }
2694
+ this.addPattern(`${operation.toUpperCase()} Statements`, state);
2695
+ this.processQueryDependencies(scope, null, state);
2696
+ }
2697
+ /**
2698
+ * `MERGE INTO t USING s` — the rule is hidden (`_merge_statement`), so there is
2699
+ * no `merge` node: the keywords and both references are inlined into
2700
+ * `statement`. Both tables are real dependencies and neither was emitted.
2701
+ */
2702
+ processMerge(node, state) {
2703
+ for (const ref of this.namedChildren(node).filter((c) => c.type === "object_reference")) {
2704
+ const target = this.referenceLeaf(ref, state);
2705
+ if (target) this.addRelationship(ASTEngine.RelationshipType.Uses, state.fileModuleName, target, state, { operation: "merge", tableReference: true });
2706
+ }
2707
+ this.addPattern("MERGE Statements", state);
2708
+ }
2709
+ processTruncate(node, state) {
2710
+ for (const ref of this.namedChildren(node).filter((c) => c.type === "object_reference")) {
2711
+ const target = this.referenceLeaf(ref, state);
2712
+ if (target) this.addRelationship(ASTEngine.RelationshipType.Uses, state.fileModuleName, target, state, { operation: "truncate", tableReference: true });
2713
+ }
2714
+ this.addPattern("TRUNCATE Statements", state);
2715
+ }
2716
+ processCopy(node, state) {
2717
+ const target = this.referenceLeaf(this.firstDescendant(node, "object_reference"), state);
2718
+ if (target) this.addRelationship(ASTEngine.RelationshipType.Uses, state.fileModuleName, target, state, { operation: "copy", tableReference: true });
2719
+ this.addPattern("COPY Statements", state);
2720
+ }
2721
+ /**
2722
+ * A hidden-rule statement (`MERGE`, `TRUNCATE`, `COPY`) arrives as a KEYWORD
2723
+ * child of `statement`, because `_merge_statement` / `_truncate_statement` /
2724
+ * `_copy_statement` inline into their parent and emit no node of their own — so
2725
+ * no node of type `merge`/`truncate`/`copy` can exist to dispatch on. The
2726
+ * handler needs the enclosing `statement` (it holds the object_references), so
2727
+ * walk up SEEKING for it rather than by a fixed hop count: the first version
2728
+ * climbed four parents, sailed past `statement`, landed on `program`, and
2729
+ * emitted nothing.
2730
+ */
2731
+ /**
2732
+ * The nearest ANCESTOR `statement` — the scope a nested statement inherits.
2733
+ *
2734
+ * SQL attaches its `WITH` list to the STATEMENT, so a statement inside a `cte`
2735
+ * body, inside a plpgsql block or inside a dollar-quoted fragment inherits the
2736
+ * CTEs of the statement it is nested in, and nothing else. Distinct from
2737
+ * `enclosingStatement` above, which SEEKING includes the node itself and falls
2738
+ * back to it: this one starts at the parent and admits that there may be none,
2739
+ * because a top-level statement has no enclosing scope to inherit.
2740
+ *
2741
+ * `descendants` is transitive, so one level of lookup suffices however deeply
2742
+ * the statement is buried.
2743
+ */
2744
+ ancestorStatement(node) {
2745
+ let parent = node.parent;
2746
+ while (parent) {
2747
+ if (parent.type === "statement") return parent;
2748
+ parent = parent.parent;
2749
+ }
2750
+ return null;
2751
+ }
2752
+ enclosingStatement(node) {
2753
+ let current = node;
2754
+ while (current) {
2755
+ if (current.type === "statement") return current;
2756
+ current = current.parent;
2757
+ }
2758
+ return node;
2759
+ }
2760
+ processInlineMerge(keyword, state) {
2761
+ this.processMerge(this.enclosingStatement(keyword), state);
2762
+ }
2763
+ processInlineTruncate(keyword, state) {
2764
+ this.processTruncate(this.enclosingStatement(keyword), state);
2765
+ }
2766
+ processInlineCopy(keyword, state) {
2767
+ this.processCopy(this.enclosingStatement(keyword), state);
2768
+ }
2769
+ /** `RETURNING col` names columns of the statement's own target table. */
2770
+ processReturning(node, state) {
2771
+ const columns = this.identifierTexts(node, state);
2772
+ if (columns.length === 0) return;
2773
+ this.addPattern("RETURNING Clause", state);
2774
+ }
2775
+ /**
2776
+ * psql meta-commands. `\i` / `\ir` name another FILE, which is an import; the
2777
+ * other 121 shapes over the census corpus do not, and are counted rather than
2778
+ * silently ignored so a command the grammar starts producing later appears in
2779
+ * a number instead of in nothing.
2780
+ *
2781
+ * `psql_meta_command` is a LEAF token — the whole `\ir schema.sql` is one node
2782
+ * with no children — so both the command word and its argument have to be read
2783
+ * out of that one string. A character scan rather than a regex, per this
2784
+ * lane's zero-regex policy (`reviews/regex-policy.md`, and
2785
+ * `validation/m12/results/regex-census.json` which once recorded SQL at all as
2786
+ * unregistered).
2787
+ *
2788
+ * Three guards, each of which has already cost something if dropped:
2789
+ * - the command must be `i` or `ir` exactly, so `\echo`, `\set`, `\gexec`,
2790
+ * `\gset`, `\pset`, `\if`, `\endif` stay out (`\gset` writes a variable
2791
+ * FROM a query, which is the opposite direction, and `\o`/`\g` write a
2792
+ * query result TO a file, which is not a source dependency).
2793
+ * - the argument must not begin with `:`. `\i :script` includes a file whose
2794
+ * NAME is computed by an earlier `\set`, and a target invented from a
2795
+ * variable name is a fabricated edge. Measured 0 on the corpus, so the
2796
+ * guard costs nothing there and is kept for the corpus that uses it.
2797
+ * - the argument must carry a SQL/dump suffix, so `\i` onto an extensionless
2798
+ * name is skipped rather than resolved against nothing.
2799
+ *
2800
+ * The path is kept exactly as written — `../../orioledb.sql` is what the file
2801
+ * says and what a reader needs — with `relativeToScript` recording that `\ir`
2802
+ * resolves against the including script's own directory rather than psql's
2803
+ * working directory.
2804
+ */
2805
+ processPsqlMetaCommand(node, state) {
2806
+ const text = node.text;
2807
+ if (text.length < 2 || text.charCodeAt(0) !== 92) return;
2808
+ const isSpace = (c) => c === 32 || c === 9 || c === 10 || c === 13 || c === 12 || c === 11;
2809
+ let end = 1;
2810
+ while (end < text.length && !isSpace(text.charCodeAt(end))) end++;
2811
+ const command = text.slice(1, end).toLowerCase();
2812
+ if (!PSQL_INCLUDE_COMMANDS.has(command)) {
2813
+ state.ambiguity.psqlCommandsSkipped++;
2814
+ return;
2815
+ }
2816
+ let start = end;
2817
+ while (start < text.length && isSpace(text.charCodeAt(start))) start++;
2818
+ let stop = text.length;
2819
+ while (stop > start && isSpace(text.charCodeAt(stop - 1))) stop--;
2820
+ const argument = text.slice(start, stop);
2821
+ if (argument.length === 0) {
2822
+ state.ambiguity.includeNonLiteral++;
2823
+ return;
2824
+ }
2825
+ if (argument.charCodeAt(0) === 58) {
2826
+ state.ambiguity.includeNonLiteral++;
2827
+ return;
2828
+ }
2829
+ for (let i = 0; i < argument.length; i++) {
2830
+ if (isSpace(argument.charCodeAt(i))) {
2831
+ state.ambiguity.includeNonLiteral++;
2832
+ return;
2833
+ }
2834
+ }
2835
+ const lowered = argument.toLowerCase();
2836
+ let suffixOk = false;
2837
+ for (const suffix of PSQL_INCLUDE_SUFFIXES) {
2838
+ if (lowered.length > suffix.length && lowered.endsWith(suffix)) {
2839
+ suffixOk = true;
2840
+ break;
2841
+ }
2842
+ }
2843
+ if (!suffixOk) {
2844
+ state.ambiguity.includeNonLiteral++;
2845
+ return;
2846
+ }
2847
+ const line = node.startPosition.row + state.slice.rowOffset + 1;
2848
+ const kind = `${command} ${argument} ${line}`;
2849
+ if (state.includeKeys.has(kind)) return;
2850
+ state.includeKeys.add(kind);
2851
+ this.addRelationship(ASTEngine.RelationshipType.Imports, state.fileModuleName, argument, state, {
2852
+ psqlInclude: true,
2853
+ includeKind: command,
2854
+ relativeToScript: command === "ir",
2855
+ fileDependency: true,
2856
+ line
2857
+ });
2858
+ this.addPattern("psql Script Include", state);
2859
+ state.imports.push({
2860
+ module: argument,
2861
+ items: [],
2862
+ importType: "sql_import",
2863
+ lineNumber: line,
2864
+ line,
2865
+ range: this.range(node, state),
2866
+ isExternal: false,
2867
+ metadata: { psqlInclude: true, includeKind: command, relativeToScript: command === "ir" }
2868
+ });
2869
+ }
2870
+ /** `COMMENT ON <object> …` — the object named is a real dependency. */
2871
+ processComment(node, state) {
2872
+ this.addPattern("SQL Documentation", state);
2873
+ const children = this.namedChildren(node);
2874
+ const onAt = children.findIndex((c) => c.type === "keyword_on");
2875
+ const object = onAt >= 0 ? children.slice(onAt + 1).find((c) => c.type === "object_reference") ?? null : null;
2876
+ if (!object) return;
2877
+ const parts = this.referenceParts(object, state);
2878
+ const isColumnComment = children.some((c) => c.type === "keyword_column");
2879
+ const target = isColumnComment && parts.length >= 2 ? parts[parts.length - 2] : parts[parts.length - 1];
2880
+ if (!target) return;
2881
+ const column = isColumnComment && parts.length >= 1 ? parts[parts.length - 1] : void 0;
2882
+ this.addRelationship(ASTEngine.RelationshipType.References, state.fileModuleName, target, state, {
2883
+ operation: "comment",
2884
+ documented: true,
2885
+ ...column && column !== target ? { column } : {}
2886
+ });
2887
+ }
2888
+ /**
2889
+ * `GRANT` / `REVOKE`.
2890
+ *
2891
+ * The grammar rule keeps the grantee an `identifier`/`literal` and never an
2892
+ * `object_reference`, which is the structural reason `REVOKE SELECT ON t FROM
2893
+ * joe` can no longer publish `uses global->joe` — before the rule existed the
2894
+ * statement was an ERROR and the role name was reachable through a `from`
2895
+ * clause. The object the privilege applies to IS an `object_reference`, and it
2896
+ * is a real schema entity, so it is the one thing here that gets an edge.
2897
+ */
2898
+ processGrant(node, state, operation) {
2899
+ this.addPattern("Permissions (GRANT/REVOKE)", state);
2900
+ const object = this.firstDescendant(node, "object_reference");
2901
+ const target = object ? this.referenceLeaf(object, state) : void 0;
2902
+ if (target) {
2903
+ this.addRelationship(ASTEngine.RelationshipType.References, state.fileModuleName, target, state, { operation, privilege: true });
2904
+ }
2905
+ }
2906
+ /**
2907
+ * Columns added by `ALTER TABLE … ADD COLUMN`.
2908
+ *
2909
+ * The grammar gives each added column its OWN `add_column` SIBLING rather than
2910
+ * one comma list, so `ALTER TABLE t ADD COLUMN a int, ADD COLUMN b text,
2911
+ * ADD COLUMN c numeric(6,2)` has three. Reading the first descendant published
2912
+ * only `a` and dropped the rest — measured on the corpus, a five-column
2913
+ * `ADD COLUMN IF NOT EXISTS …` migration published one of its five columns.
2914
+ */
2915
+ processAlterTable(node, state) {
2916
+ const table = this.referenceLeaf(this.directChild(node, "object_reference"), state);
2917
+ if (!table) return;
2918
+ const owner = state.symbols.find((s) => s.name === table && s.metadata?.type === "table");
2919
+ for (const spec of this.namedChildren(node).filter((c) => c.type === "inherit_specification")) {
2920
+ const isRemoval = this.namedChildren(spec).some((c) => c.type === "keyword_no");
2921
+ if (isRemoval) continue;
2922
+ const parentNode = this.namedChildren(spec).find((c) => c.type === "object_reference");
2923
+ const parent = parentNode ? this.referenceLeaf(parentNode, state) : "";
2924
+ if (!parent || !owner) continue;
2925
+ this.addRelationship(ASTEngine.RelationshipType.Extends, table, parent, state, {
2926
+ clause: "inherit",
2927
+ evidenceClass: "structural"
2928
+ }, this.range(spec, state).startLine);
2929
+ }
2930
+ const added = this.namedChildren(node).filter((c) => c.type === "add_column");
2931
+ if (added.length === 0) return;
2932
+ if (!owner) return;
2933
+ for (const add of added) {
2934
+ const definition = this.firstDescendant(add, "column_definition");
2935
+ if (!definition) continue;
2936
+ const nameNode = definition.childForFieldName?.("name") ?? this.firstIdentifier(definition);
2937
+ if (!nameNode || !identifierTypes.has(nameNode.type)) continue;
2938
+ const inheritKind = this.inheritClauseKind(nameNode, add, node);
2939
+ if (inheritKind === "inherit" || inheritKind === "noInherit" || this.reservedColumnName(nameNode)) {
2940
+ state.ambiguity.reservedNameSymbolsSuppressed++;
2941
+ if (inheritKind === "inherit") {
2942
+ const parentNode = definition.childForFieldName?.("type") ?? definition.childForFieldName?.("custom_type");
2943
+ const parentName = parentNode ? this.identifierName(parentNode, state) : "";
2944
+ if (parentName) {
2945
+ this.addRelationship(ASTEngine.RelationshipType.Extends, table, parentName, state, {
2946
+ clause: "inherit"
2947
+ }, this.range(nameNode, state).startLine);
2948
+ }
2949
+ }
2950
+ continue;
2951
+ }
2952
+ const column = this.createSymbol(
2953
+ this.identifierName(nameNode, state),
2954
+ ASTEngine.SymbolKind.Property,
2955
+ nameNode,
2956
+ owner,
2957
+ state,
2958
+ { type: "column", sqlType: this.columnType(definition, state), addedByAlter: true }
2959
+ );
2960
+ this.addRelationship(ASTEngine.RelationshipType.Contains, table, column.name, state, { memberKind: ASTEngine.SymbolKind.Property }, this.range(nameNode, state).startLine, this.existingSymbol(table, ASTEngine.SymbolKind.Class, state)?.id ?? table, column.id);
2961
+ }
2962
+ }
2963
+ /**
2964
+ * WHAT IS THIS `ALTER TABLE` COLUMN, really? The wired grammar has no `INHERIT` clause, so
2965
+ * `ALTER TABLE c INHERIT p` arrives as
2966
+ * (add_column (column_definition name:(identifier "INHERIT")
2967
+ * custom_type:(object_reference name:(identifier "p"))))
2968
+ * — a column named INHERIT whose type is the parent table. THREE legal SQL statements arrive in
2969
+ * that shape, and the answer is STRUCTURAL, never a name list:
2970
+ *
2971
+ * "column" `ALTER TABLE c ADD COLUMN inherit int;` — a REAL column. `inherit` is a
2972
+ * NON-RESERVED PostgreSQL keyword (PostgreSQL 18 keyword appendix, Table C.1:
2973
+ * `INHERIT | non-reserved`), so this is legal SQL and the column is PUBLISHED.
2974
+ * The grammar SAW the column: the `add_column` carries `keyword_add` and/or
2975
+ * `keyword_column`.
2976
+ * "inherit" `ALTER TABLE c INHERIT p;` — the mis-parse of a real inheritance clause. The
2977
+ * grammar saw NEITHER `ADD` nor `COLUMN`, because it had nothing else to call it,
2978
+ * and there is no `NO`. The parent's name survives in `custom_type`, so the
2979
+ * RELATION is recoverable and `extends` is published from it.
2980
+ * "noInherit" `ALTER TABLE c NO INHERIT p;` — the clause that REMOVES inheritance. It parses
2981
+ * to (alter_table … (ERROR (keyword_no)) (add_column (column_definition
2982
+ * name:(identifier "INHERIT") custom_type:(object_reference "p")))), and it is
2983
+ * NEITHER a column NOR a relation: publishing either asserts the opposite of what
2984
+ * the statement says.
2985
+ *
2986
+ * `null` means the column is not one of these three — an ordinary column, and the caller's
2987
+ * business.
2988
+ */
2989
+ inheritClauseKind(nameNode, addColumn, alterTable) {
2990
+ if (nameNode.type !== "identifier") return null;
2991
+ if (nameNode.text.toLowerCase() !== "inherit") return null;
2992
+ if (this.directChild(addColumn, "keyword_add") !== null) return "column";
2993
+ if (this.directChild(addColumn, "keyword_column") !== null) return "column";
2994
+ if (this.descendants(alterTable, "keyword_no").length > 0) return "noInherit";
2995
+ return "inherit";
2996
+ }
2997
+ // ---------------------------------------------------------------- queries
2998
+ processQuery(node, state, owner) {
2999
+ if (this.containsType(node, "join")) this.addPattern("Join Query Mapping", state);
3000
+ if (this.containsType(node, "cte")) this.addPattern("Common Table Expressions (CTE)", state);
3001
+ if (this.containsType(node, "set_operation")) this.addPattern("Set Operations (UNION/INTERSECT/EXCEPT)", state);
3002
+ if (this.containsType(node, "subquery")) this.addPattern("Subqueries", state);
3003
+ if (this.containsType(node, "case")) this.addPattern("Conditional Logic (CASE/WHEN)", state);
3004
+ if (this.containsType(node, "all_fields")) {
3005
+ this.addPattern("SELECT *", state);
3006
+ if (state.metadata.performanceScore !== void 0) state.metadata.performanceScore = Math.max(0, state.metadata.performanceScore - 30);
3007
+ this.addInsight("SELECT * detected \u2014 consider explicit column selection", state);
3008
+ }
3009
+ this.bumpComplexity(node, state);
3010
+ this.processQueryDependencies(node, owner, state);
3011
+ this.processCalls(node, owner?.name ?? state.fileModuleName, state);
3012
+ }
3013
+ processQueryDependencies(node, owner, state, sourceOverride) {
3014
+ const source = sourceOverride ?? owner?.name ?? state.fileModuleName;
3015
+ const localCtes = this.cteScope(node, state);
3016
+ for (const relation of this.descendants(node, "relation")) {
3017
+ const object = this.namedChildren(relation).find((c) => c.type === "object_reference");
3018
+ if (!object) continue;
3019
+ this.addTableEdge(source, object, localCtes, state);
3020
+ }
3021
+ for (const from of this.descendants(node, "from")) {
3022
+ for (const child of this.namedChildren(from)) {
3023
+ if (child.type !== "object_reference") continue;
3024
+ this.addTableEdge(source, child, localCtes, state);
3025
+ }
3026
+ }
3027
+ for (const merge of this.descendants(node, "keyword_merge")) {
3028
+ void merge;
3029
+ }
3030
+ this.processInstantiations(node, source, state);
3031
+ }
3032
+ /**
3033
+ * `ROW(...)::my_composite` — a value BUILT for a named type.
3034
+ *
3035
+ * PostgreSQL implements a composite cast as a call to that type's constructor,
3036
+ * so this is the one place SQL constructs an instance of a declared type, and it
3037
+ * is what the ontology's `instantiates` names. Measured over the 1,460-file
3038
+ * census: 4,406 `cast` nodes, of which only those carrying a `custom_type`
3039
+ * FIELD name a user type at all (a builtin target such as `::integer` is a bare
3040
+ * anonymous token), and of those the ones whose VALUE side is a constructor
3041
+ * rather than a pre-existing value are the construction events.
3042
+ *
3043
+ * Two guards, and both were arrived at by discarding a wrong answer:
3044
+ *
3045
+ * - the value side must CONSTRUCT. `x::my_enum` and `col::my_type` convert a
3046
+ * value that already exists; publishing those would assert an object was
3047
+ * created where none was. A `cast` whose left side is an ERROR is also
3048
+ * skipped: DuckDB's `[a+b, …]::vec4` parses as `ERROR :: type` because the
3049
+ * grammar cannot read a bracketed array literal, so the shape is a damaged
3050
+ * parse and not a construction the analyzer can vouch for.
3051
+ * - the type name must not be a keyword or a builtin. DuckDB's
3052
+ * `CAST(ROW(x) AS ROW(a INT, b INT))` puts the literal word `ROW` in the
3053
+ * same `custom_type` field, and 67 casts corpus-wide target a name no file
3054
+ * declares. Those stay external names, which is what they are.
3055
+ *
3056
+ * The target may be declared in another file — a composite type is usually
3057
+ * declared once and constructed from many migrations — so an unresolved target
3058
+ * is the normal case for this type and is reported as such rather than
3059
+ * suppressed. `evidenceClass` is `structural` because the constructor and the
3060
+ * type name are both in grammar-mandated positions; nothing here is a name
3061
+ * match.
3062
+ */
3063
+ /**
3064
+ * The names of the `cte` nodes the given node is INSIDE, nearest first.
3065
+ *
3066
+ * A CTE's own name is bound inside its own body — that is what makes
3067
+ * `WITH RECURSIVE t AS (… FROM t …)` work — so every reference inside that body
3068
+ * has to see it. The nodes naming it are ANCESTORS of the reference and
3069
+ * DESCENDANTS of nothing the reference owns, which is why a scope computed from
3070
+ * `descendants(reference, "cte")` or `descendants(enclosingStatement, "cte")`
3071
+ * cannot contain it. Only the chain from the node to the root does.
3072
+ *
3073
+ * Bounded by construction: it is one pass up the parent chain and it collects a
3074
+ * node's own name, so it cannot pick up a name a later sibling CTE declared.
3075
+ */
3076
+ ancestorCteNames(node, state) {
3077
+ const names = [];
3078
+ for (let a = node.parent; a; a = a.parent) {
3079
+ if (a.type !== "cte") continue;
3080
+ const nm = this.firstIdentifier(a, state);
3081
+ if (nm) names.push(this.identifierName(nm, state));
3082
+ }
3083
+ return names;
3084
+ }
3085
+ /**
3086
+ * THE CTE SCOPE of a node, which is two sets and neither of them is
3087
+ * `descendants(node, "cte")`.
3088
+ *
3089
+ * SQL's rule is that a WITH list's names are visible to every LATER item in the
3090
+ * list and to the main query — not only inside the item that declares them. So:
3091
+ *
3092
+ * WITH aoc5_input(i) AS (…), lines(y,line) AS (… FROM aoc5_input …),
3093
+ * rules(b,a) AS (… FROM lines …), updates(y,x,v) AS (… FROM lines …)
3094
+ * SELECT … FROM updates;
3095
+ *
3096
+ * `updates`' body reads `lines`, a SIBLING CTE. Both the per-node set and the
3097
+ * sweep's `ancestorStatement` set miss it: the body is its own `statement`, its
3098
+ * subtree holds no `cte`, and the node that names `lines` is neither an ancestor
3099
+ * nor a descendant of the reference. Measured over the grown corpus, this leaked
3100
+ * 32 `uses` edges across 16 DuckDB benchmark files — the same wrong output as
3101
+ * pass 1's CTE leak, reached through a third entry point.
3102
+ *
3103
+ * The two sets that do settle it:
3104
+ *
3105
+ * 1. every `cte` under the OUTERMOST enclosing `statement`, which is the WITH
3106
+ * list the body belongs to — siblings included;
3107
+ * 2. the `cte` names on the ANCESTOR CHAIN, which is what a CTE's own body
3108
+ * needs and is the only set that can see the node naming it.
3109
+ *
3110
+ * WHY THE OUTERMOST statement and not the nearest. Two `WITH` lists in one file
3111
+ * are two scopes, and a name from the first must not shadow a table in the
3112
+ * second. The outermost enclosing `statement` is exactly one WITH list.
3113
+ *
3114
+ * WHY A WIDER SCOPE WAS TRIED AND REVERTED, since this is the third attempt.
3115
+ * `ParseState.localCtes`'s field comment records an ancestor walk that collected
3116
+ * everything under the with-clause and shadowed real tables outside its actual
3117
+ * scope. That version walked ancestors and kept DESCENDANTS of each — which
3118
+ * re-reaches the same set this one does, but from the wrong root when the
3119
+ * reference is in the OUTER query: it pulled in a WITH list belonging to a
3120
+ * function body further up the file. Anchoring on the outermost statement of the
3121
+ * reference itself removes that, because a reference in the outer query has that
3122
+ * outer query as its own outermost statement. `sqld5` and `sqld14` pin both
3123
+ * directions.
3124
+ */
3125
+ cteScope(node, state) {
3126
+ let outermost = node;
3127
+ for (let a = node.parent; a; a = a.parent) if (a.type === "statement") outermost = a;
3128
+ const scope = /* @__PURE__ */ new Set();
3129
+ const collect = (from) => {
3130
+ for (const cte of this.descendants(from, "cte")) {
3131
+ const nm = this.firstIdentifier(cte, state);
3132
+ if (nm) scope.add(this.identifierName(nm, state));
3133
+ }
3134
+ };
3135
+ collect(outermost);
3136
+ if (outermost !== node) collect(node);
3137
+ if (node.type === "cte" || node.parent?.type === "cte") {
3138
+ let listRoot = node.parent;
3139
+ if (listRoot?.type === "cte") listRoot = listRoot.parent;
3140
+ if (listRoot) collect(listRoot);
3141
+ }
3142
+ for (const name of this.ancestorCteNames(node, state)) scope.add(name);
3143
+ return scope;
3144
+ }
3145
+ processInstantiations(node, source, state) {
3146
+ for (const cast of this.descendants(node, "cast")) {
3147
+ const target = cast.childForFieldName?.("custom_type");
3148
+ if (!target) continue;
3149
+ const name = this.referenceLeaf(target, state);
3150
+ if (!name) continue;
3151
+ const lowered = name.toLowerCase();
3152
+ if (SQL_KEYWORDS.has(lowered) || SQL_BUILTINS.has(lowered)) {
3153
+ state.ambiguity.keywordTargetsSuppressed++;
3154
+ continue;
3155
+ }
3156
+ let value = null;
3157
+ for (let i = 0; i < cast.childCount; i++) {
3158
+ const child = cast.child(i);
3159
+ if (!child) continue;
3160
+ if (child.type === "::") break;
3161
+ if (child.isNamed) value = child;
3162
+ }
3163
+ if (!value || value.hasError) continue;
3164
+ if (!this.constructsAValue(value)) continue;
3165
+ this.addRelationship(ASTEngine.RelationshipType.Instantiates, source, name, state, {
3166
+ operation: "cast",
3167
+ construction: true,
3168
+ constructor: value.type
3169
+ });
3170
+ state.ambiguity.instantiationsEmitted++;
3171
+ }
3172
+ }
3173
+ /**
3174
+ * Is this expression BUILDING a value, rather than naming one?
3175
+ *
3176
+ * `ROW(1,2)` is the grammar's `invocation` whose callee is the bare word `ROW`,
3177
+ * and that callee name is the discriminator: `nextval('s')::bigint` is also an
3178
+ * `invocation` and is a conversion. `('a', 2)` and `ARRAY[...]` are `list` /
3179
+ * `array`, which cannot be anything but a constructed collection. Anything else
3180
+ * — a `field`, a `parameter`, a bare `literal`, a `parenthesized_expression`
3181
+ * wrapping a column — is a value that already existed.
3182
+ */
3183
+ constructsAValue(node) {
3184
+ if (CONSTRUCTOR_VALUE_NODES.has(node.type)) return true;
3185
+ if (node.type === "invocation") {
3186
+ const callee = this.directChild(node, "object_reference");
3187
+ const nameNode = callee ? this.referenceNameNode(callee) : this.firstIdentifier(node);
3188
+ return nameNode?.text.toLowerCase() === "row";
3189
+ }
3190
+ return false;
3191
+ }
3192
+ addTableEdge(source, object, localCtes, state) {
3193
+ const parts = this.referenceParts(object, state);
3194
+ if (parts.length === 0) return;
3195
+ const leaf = parts[parts.length - 1];
3196
+ if (parts.length === 1 && (localCtes.has(leaf) || state.localCtes.has(leaf))) return;
3197
+ const wholeQuoted = this.wholeQuotedReferenceName(object, parts.length);
3198
+ this.addRelationship(ASTEngine.RelationshipType.Uses, source, leaf, state, {
3199
+ tableReference: true,
3200
+ ...parts.length > 1 ? { qualified: parts.join(".") } : {},
3201
+ ...wholeQuoted ? { quotedName: true } : {}
3202
+ });
3203
+ }
3204
+ /**
3205
+ * Call edges, one per `invocation` the grammar actually recognised.
3206
+ *
3207
+ * An `invocation` node whose OWN subtree carries an ERROR is skipped, and that
3208
+ * test is the whole fix. Error recovery will synthesise an `invocation` whose
3209
+ * callee slot holds whatever token it happened to reach, and the measured case
3210
+ * is a PL/pgSQL cursor loop:
3211
+ *
3212
+ * CREATE OR REPLACE FUNCTION _drop_all_policies(p_table TEXT) … AS $$
3213
+ * DECLARE r RECORD;
3214
+ * BEGIN
3215
+ * FOR r IN SELECT policyname FROM pg_policies WHERE … LOOP -- unparseable
3216
+ * EXECUTE format('DROP POLICY … %I', r.policyname, p_table);
3217
+ * END LOOP;
3218
+ * END; $$ LANGUAGE plpgsql;
3219
+ *
3220
+ * parses the `FOR … LOOP` as `invocation( object_reference("p_table"),
3221
+ * ERROR("LOOP EXECUTE format"), term, term, term )` — so the first
3222
+ * object_reference child, which the callee is read from, is the function's own
3223
+ * PARAMETER, and the analyzer published `calls _drop_all_policies -> p_table`:
3224
+ * a call into a variable. Five such edges measured on the corpus, one per
3225
+ * migration that writes this shape.
3226
+ *
3227
+ * Per-node `hasError` is the right granularity: a clean invocation beside a
3228
+ * damaged sibling is unaffected, so no real call is lost, and it is a property
3229
+ * of the tree rather than a guess about the text. The suppression is counted in
3230
+ * `ambiguity.recoveryCallsSuppressed` so it is visible rather than silent.
3231
+ */
3232
+ processCalls(node, source, state) {
3233
+ for (const invocation of this.descendants(node, "invocation")) {
3234
+ if (invocation.hasError) {
3235
+ state.ambiguity.recoveryCallsSuppressed++;
3236
+ continue;
3237
+ }
3238
+ const object = this.directChild(invocation, "object_reference");
3239
+ const target = object ? this.referenceLeaf(object, state) : "";
3240
+ if (!target) continue;
3241
+ if (SQL_KEYWORDS.has(target.toLowerCase())) {
3242
+ state.ambiguity.keywordTargetsSuppressed++;
3243
+ continue;
3244
+ }
3245
+ if (SQL_BUILTINS.has(target.toLowerCase())) {
3246
+ state.ambiguity.builtinCallsSuppressed++;
3247
+ continue;
3248
+ }
3249
+ const parts = object ? this.referenceParts(object, state) : [];
3250
+ const wholeQuoted = object ? this.wholeQuotedReferenceName(object, parts.length) : false;
3251
+ this.addRelationship(ASTEngine.RelationshipType.Calls, source, target, state, {
3252
+ ...parts.length > 1 ? { qualified: parts.join(".") } : {},
3253
+ ...wholeQuoted ? { quotedName: true } : {}
3254
+ });
3255
+ }
3256
+ }
3257
+ recordTableTarget(node, type, operation, state) {
3258
+ const object = this.firstDescendant(node, "object_reference");
3259
+ if (!object) return;
3260
+ const target = this.referenceLeaf(object, state);
3261
+ if (!target) return;
3262
+ const parts = this.referenceParts(object, state);
3263
+ this.addRelationship(type === "uses" ? ASTEngine.RelationshipType.Uses : ASTEngine.RelationshipType.References, state.fileModuleName, target, state, {
3264
+ operation,
3265
+ tableReference: true,
3266
+ ...parts.length > 1 ? { qualified: parts.join(".") } : {}
3267
+ });
3268
+ }
3269
+ /**
3270
+ * The `object_reference` node whose leaf is the DML statement's own target, or
3271
+ * `null` when the shape does not have one. Split out of `dmlTarget` so the CALLER
3272
+ * can read the reference's parts and not only its leaf: a bare leaf throws away
3273
+ * the qualification, which is the whole content of `INSERT INTO app.t`.
3274
+ */
3275
+ dmlTargetNode(node) {
3276
+ if (node.type === "insert") return this.directChild(node, "object_reference");
3277
+ if (node.type === "update") {
3278
+ const relation = this.directChild(node, "relation");
3279
+ return relation ? this.directChild(relation, "object_reference") : this.firstDescendant(node, "object_reference");
3280
+ }
3281
+ if (node.type === "delete") {
3282
+ const scope = node.parent?.type === "statement" ? node.parent : node;
3283
+ return this.directChild(scope, "from") ? this.firstDescendant(this.directChild(scope, "from"), "object_reference") : this.firstDescendant(scope, "object_reference");
3284
+ }
3285
+ return this.firstDescendant(node, "object_reference");
3286
+ }
3287
+ dmlTarget(node, state) {
3288
+ const object = this.dmlTargetNode(node);
3289
+ return object ? this.referenceLeaf(object, state) : void 0;
3290
+ }
3291
+ // ---------------------------------------------------------------- symbols / edges
3292
+ createSymbol(name, kind, node, parent, state, metadata = {}, extra = {}) {
3293
+ const identity = `${node.startIndex + state.slice.byteOffset}:${kind}:${name}:${parent?.name ?? ""}`;
3294
+ if (state.symbolKeys.has(identity)) {
3295
+ return state.symbols.find((s) => s.name === name && s.kind === kind && s.ownerName === parent?.name) ?? this.placeholder(name, kind, parent);
3296
+ }
3297
+ state.symbolKeys.add(identity);
3298
+ const symbol = {
3299
+ name,
3300
+ kind,
3301
+ range: this.range(node, state),
3302
+ parentId: parent ? `${parent.id}.${name}` : void 0,
3303
+ ownerName: parent?.name,
3304
+ ownerKind: parent?.kind,
3305
+ parent: parent?.name,
3306
+ metadata,
3307
+ ...extra
3308
+ };
3309
+ let id = symbol.parentId ?? name;
3310
+ const seen = state.idCounts.get(id) ?? 0;
3311
+ state.idCounts.set(id, seen + 1);
3312
+ if (seen > 0) id = `${id}#${seen + 1}`;
3313
+ symbol.id = id;
3314
+ state.symbols.push(symbol);
3315
+ state.symbolNames.add(name);
3316
+ if (parent) this.addRelationship(ASTEngine.RelationshipType.Contains, parent.name, name, state, { memberKind: symbol.kind }, symbol.range.startLine, parent.id, symbol.id);
3317
+ return symbol;
3318
+ }
3319
+ /**
3320
+ * A symbol of this name and kind already emitted in this file, if any.
3321
+ *
3322
+ * Used to attach a member to an owner that is declared as a SIBLING statement.
3323
+ * `createSymbol` returns a `placeholder` when its identity key is taken, so an
3324
+ * index declared after its table already resolves through the ordinary path; this
3325
+ * covers the other order, where the index arrives first.
3326
+ */
3327
+ existingSymbol(name, kind, state) {
3328
+ return state.symbols.find((s) => s.name === name && s.kind === kind);
3329
+ }
3330
+ placeholder(name, kind, parent) {
3331
+ return {
3332
+ name,
3333
+ kind,
3334
+ id: parent ? `${parent.id}.${name}` : name,
3335
+ range: { startLine: 1, startColumn: 1, endLine: 1, endColumn: 1, startIndex: 0, endIndex: 0 },
3336
+ ownerName: parent?.name,
3337
+ metadata: {}
3338
+ };
3339
+ }
3340
+ addRelationship(type, source, target, state, metadata = {}, line, ownerId, memberId) {
3341
+ if (!source || !target) return;
3342
+ if (type === ASTEngine.RelationshipType.Contains) {
3343
+ if (ownerId !== void 0 && memberId !== void 0) {
3344
+ if (ownerId === memberId) return;
3345
+ } else if (source === target) {
3346
+ state.ambiguity.sameNameContainmentSuppressed++;
3347
+ return;
3348
+ }
3349
+ }
3350
+ if (SQL_KEYWORDS.has(target.toLowerCase()) && !state.symbolNames.has(target)) {
3351
+ state.ambiguity.keywordTargetsSuppressed++;
3352
+ return;
3353
+ }
3354
+ let externalKind = classifyExternalTarget(
3355
+ target,
3356
+ metadata.qualified,
3357
+ metadata.quotedName === true
3358
+ );
3359
+ if (!externalKind && type === ASTEngine.RelationshipType.Calls && !state.symbolNames.has(target) && SQL_PROVIDED_FUNCTIONS.has(target.toLowerCase())) {
3360
+ externalKind = "providedFunction";
3361
+ state.ambiguity.providedFunctionCallsMarked++;
3362
+ }
3363
+ if (externalKind && !state.externalTargets) {
3364
+ state.externalTargets = { postgresCatalog: 0, informationSchema: 0, implicitSchema: 0, platform: 0, providedFunction: 0 };
3365
+ }
3366
+ if (externalKind) {
3367
+ state.externalTargets[externalKind]++;
3368
+ metadata = { ...metadata, external: externalKind };
3369
+ if (externalKind === "postgresCatalog") {
3370
+ metadata = { ...metadata, external: externalKind, isExternal: true };
3371
+ state.ambiguity.pgCatalogMarkedExternal++;
3372
+ }
3373
+ if (externalKind === "providedFunction") {
3374
+ metadata = { ...metadata, external: externalKind, isExternal: true };
3375
+ }
3376
+ }
3377
+ const key = `${type}\0${source}\0${target}\0${metadata.memberKind ?? ""}`;
3378
+ const existing = state.relationshipsByKey.get(key);
3379
+ if (existing) {
3380
+ if (metadata.operation) {
3381
+ const operations = Array.isArray(existing.metadata?.operations) ? existing.metadata.operations : existing.metadata?.operation ? [existing.metadata.operation] : [];
3382
+ if (!operations.includes(metadata.operation)) {
3383
+ existing.metadata = { ...existing.metadata, operations: [...operations, metadata.operation] };
3384
+ }
3385
+ }
3386
+ return;
3387
+ }
3388
+ state.relationshipKeys.add(key);
3389
+ const relationship = {
3390
+ type,
3391
+ source,
3392
+ target,
3393
+ confidence: 1,
3394
+ weight: 1,
3395
+ ...line !== void 0 ? { line, line_number: line } : {},
3396
+ metadata: { ...metadata, evidenceClass: metadata.evidenceClass ?? this.evidenceClassFor(type, metadata) }
3397
+ };
3398
+ state.relationshipsByKey.set(key, relationship);
3399
+ state.relationships.push(relationship);
3400
+ }
3401
+ /**
3402
+ * The tier an emitted edge was actually derived at.
3403
+ *
3404
+ * WHY THIS EXISTS, and what it cost to be missing. Until this mapping existed
3405
+ * every edge this analyzer emitted carried `confidence: 1` and NO
3406
+ * `evidenceClass` at all — so the honesty contract's §4 requirement, that the
3407
+ * heuristic/deterministic ratio be a queryable NUMBER, was not answerable for
3408
+ * this lane: the census reported `evidenceClass` absent on 22,073 of 22,073
3409
+ * edges, which reads as "no heuristics" and in fact means "nothing recorded".
3410
+ * A field that is absent is not the same claim as a field that is uniformly
3411
+ * `structural`, and only one of them is true.
3412
+ *
3413
+ * The tiers, and why each edge lands where it does:
3414
+ *
3415
+ * structural the fact is in a grammar-mandated position. Every
3416
+ * `contains` (an owner and a member the grammar nested
3417
+ * it under), every `imports` (a `psql_meta_command`
3418
+ * token naming a file), every `overrides` (an
3419
+ * `OR REPLACE` on a name this file declared), every
3420
+ * `instantiates` (a `cast` whose `custom_type` FIELD is
3421
+ * the type), and every `references` — the FK target sits
3422
+ * after `keyword_references`, the trigger's table after
3423
+ * `keyword_on`, the policy's table after `keyword_on`,
3424
+ * GRANT/REVOKE's object is the statement's own
3425
+ * `object_reference`. Each of those is a fixed slot, not a
3426
+ * name match.
3427
+ * usage_contextual the name sits in a bare name slot with nothing around
3428
+ * it to corroborate: a `uses` edge from a `from`/`relation`
3429
+ * clause (the relation may be a CTE, which is why the CTE
3430
+ * guard exists), and a `calls` edge whose callee is an
3431
+ * UNQUALIFIED `invocation` — `my_func(1)`, which the
3432
+ * grammar has told us is a call and not which `my_func`
3433
+ * among the schemas resolves it.
3434
+ * reference_qualified the name was read through a QUALIFIED reference, which
3435
+ * is the one case that carries its own disambiguation:
3436
+ * `schema.func(...)` names one object by construction.
3437
+ *
3438
+ * `confidence` stays 1 for all of them, and that is now a claim rather than an
3439
+ * omission: no edge this analyzer emits is a guess about a NAME. Where the
3440
+ * analyzer genuinely cannot tell — a target it labels `external`, a
3441
+ * concatenation fragment whose identifier is computed at run time — the edge is
3442
+ * not emitted at all, and the suppression is counted in `ambiguity` instead.
3443
+ */
3444
+ evidenceClassFor(type, metadata) {
3445
+ switch (type) {
3446
+ case ASTEngine.RelationshipType.Imports:
3447
+ return "import_based";
3448
+ case ASTEngine.RelationshipType.Calls:
3449
+ case ASTEngine.RelationshipType.Uses:
3450
+ return metadata.qualified ? "reference_qualified" : "usage_contextual";
3451
+ case ASTEngine.RelationshipType.Contains:
3452
+ case ASTEngine.RelationshipType.References:
3453
+ case ASTEngine.RelationshipType.Extends:
3454
+ case ASTEngine.RelationshipType.Implements:
3455
+ case ASTEngine.RelationshipType.Exports:
3456
+ case ASTEngine.RelationshipType.Overrides:
3457
+ case ASTEngine.RelationshipType.Instantiates:
3458
+ return "structural";
3459
+ default:
3460
+ return "structural";
3461
+ }
3462
+ }
3463
+ // ---------------------------------------------------------------- identifiers
3464
+ /**
3465
+ * An identifier's NAME, with the delimiter removed.
3466
+ *
3467
+ * Three quoting styles, three behaviours before this: `"MyTable"` kept its
3468
+ * double quotes in `name` and `id`, `` `users` `` kept its backticks, and
3469
+ * `[dbo].[Users]` had them stripped. The quotes are SQL syntax, not part of
3470
+ * the name, so they are removed here and the raw text kept in metadata.
3471
+ */
3472
+ identifierName(node, state) {
3473
+ const raw = node.text;
3474
+ const stripped = this.stripIdentifierQuotes(raw);
3475
+ if (stripped !== raw) {
3476
+ state.ambiguity.quotedIdentifiers++;
3477
+ return stripped;
3478
+ }
3479
+ return raw;
3480
+ }
3481
+ stripIdentifierQuotes(value) {
3482
+ if (value.length < 2) return value;
3483
+ const first = value[0];
3484
+ const last = value[value.length - 1];
3485
+ if (first === '"' && last === '"' || first === "`" && last === "`" || first === "[" && last === "]") {
3486
+ const inner = value.slice(1, -1);
3487
+ return first === '"' ? inner.replaceAll('""', '"') : inner;
3488
+ }
3489
+ return value;
3490
+ }
3491
+ /**
3492
+ * Is this reference ONE quoted identifier, so a dot inside its text belongs to the NAME?
3493
+ *
3494
+ * NOT `node.type === "literal"`, and the grammar is why. Measured:
3495
+ * `SELECT * FROM "vault.secret"` parses to `object_reference( identifier("\"vault.secret\"") )` —
3496
+ * a single `identifier` whose TEXT carries the quotes — while `SELECT * FROM vault.secret`
3497
+ * parses to `object_reference( identifier("vault"), identifier("secret") )`. So quotedness is a
3498
+ * property of the leaf's text, not of its node type, and a check on the node type reads every
3499
+ * quoted name as unqualified: it passed the fixture's positive case for the wrong reason and
3500
+ * would have left `"vault.secret"` labelled as Supabase's `vault` schema.
3501
+ *
3502
+ * `parts.length === 1` is the other half and it is what makes this safe: `"vault"."secret"` is
3503
+ * two leaves and IS qualified, so a two-part reference is never treated as a whole name.
3504
+ */
3505
+ wholeQuotedReferenceName(node, parts) {
3506
+ if (parts !== 1) return false;
3507
+ const nameNode = this.referenceNameNode(node);
3508
+ if (!nameNode) return false;
3509
+ if (nameNode.type === "literal") return true;
3510
+ return this.stripIdentifierQuotes(nameNode.text) !== nameNode.text;
3511
+ }
3512
+ /**
3513
+ * The parts of an `object_reference`, each unquoted.
3514
+ *
3515
+ * A QUALIFIED COLUMN reference nests its table as an `object_reference` CHILD
3516
+ * rather than as a sibling identifier — this is the shape
3517
+ * `COMMENT ON COLUMN business_inventory_items.calories` produces:
3518
+ *
3519
+ * object_reference "business_inventory_items.calories"
3520
+ * object_reference "business_inventory_items"
3521
+ * identifier "calories"
3522
+ *
3523
+ * so a flat identifier walk sees only `calories` and the reference resolves to
3524
+ * the COLUMN, which is how a column was published as a table dependency. When
3525
+ * the first named child is itself a reference, its parts are the prefix.
3526
+ */
3527
+ referenceParts(node, state) {
3528
+ const parts = [];
3529
+ for (const child of this.namedChildren(node)) {
3530
+ if (child.type === "object_reference") {
3531
+ parts.push(...this.referenceParts(child, state));
3532
+ continue;
3533
+ }
3534
+ if (child.type === "identifier" || child.type === "literal") parts.push(this.identifierName(child, state));
3535
+ }
3536
+ if (parts.length === 0) {
3537
+ for (const id of this.descendants(node, "identifier")) parts.push(this.identifierName(id, state));
3538
+ }
3539
+ return parts;
3540
+ }
3541
+ referenceNameNode(node) {
3542
+ if (node.type === "identifier" || node.type === "literal") return node;
3543
+ return node.childForFieldName?.("name") ?? this.lastIdentifier(node);
3544
+ }
3545
+ lastIdentifier(node) {
3546
+ const ids = this.descendants(node, "identifier");
3547
+ if (ids.length) return ids[ids.length - 1];
3548
+ const literals = this.descendants(node, "literal");
3549
+ return literals.length ? literals[literals.length - 1] : null;
3550
+ }
3551
+ referenceLeaf(node, state) {
3552
+ if (!node) return void 0;
3553
+ const nameNode = this.referenceNameNode(node);
3554
+ return nameNode ? this.identifierName(nameNode, state) : void 0;
3555
+ }
3556
+ referenceText(node, state) {
3557
+ const parts = this.referenceParts(node, state);
3558
+ return parts.length ? parts.join(".") : node.text;
3559
+ }
3560
+ // ---------------------------------------------------------------- metadata
3561
+ addPattern(pattern, state) {
3562
+ state.patterns.add(pattern);
3563
+ }
3564
+ addInsight(insight, state) {
3565
+ const list = state.metadata.architecturalInsights;
3566
+ if (Array.isArray(list) && !list.includes(insight)) list.push(insight);
3567
+ }
3568
+ bumpComplexity(node, state) {
3569
+ const stack = [node];
3570
+ let increment = 0;
3571
+ while (stack.length) {
3572
+ const current = stack.pop();
3573
+ if (current.type === "case" || current.type === "join" || current.type === "when_clause" || current.type === "set_operation") increment++;
3574
+ for (let i = 0; i < current.namedChildCount; i++) {
3575
+ const child = current.namedChild(i);
3576
+ if (child) stack.push(child);
3577
+ }
3578
+ }
3579
+ state.metadata.cyclomaticComplexity = (state.metadata.cyclomaticComplexity ?? 1) + increment;
3580
+ }
3581
+ // ---------------------------------------------------------------- traversal helpers
3582
+ directChild(node, type) {
3583
+ for (const child of this.namedChildren(node)) if (child.type === type) return child;
3584
+ return null;
3585
+ }
3586
+ /**
3587
+ * Per-parse memo for `namedChildren`, and a per-TREE document-order index for `descendants`.
3588
+ *
3589
+ * WHY THIS EXISTS, measured rather than guessed. `node --cpu-prof` on
3590
+ * `postgrest/test/io/fixtures/big_schema.sql` (392 KB, 67,508 named nodes, depth 43) put
3591
+ * **134 of 154 seconds** inside `walkDescendants` — 87 % of the whole run — against
3592
+ * `ts_parser_parse_wasm` at **0.33 s**. The grammar was never the problem; the walk was.
3593
+ * The file then crossed `engine.ts`'s 120 s analyzer timeout, returned
3594
+ * `analysisVersion: "2.0-basic-fallback"` with 0 symbols and 0 relationships, and in 2 of 3
3595
+ * solo runs. That is what made this lane's corpus total BIMODAL (see the docs page).
3596
+ *
3597
+ * The blowup is arithmetic, and it is not subtle once counted: 29,662 `descendants` calls
3598
+ * collapsed to 4,199 distinct (node, type) queries, and those 4,199 walks visited
3599
+ * **99,159,647 nodes** — 1,469 visits per node in the file. The queries sit near the root, so
3600
+ * each one rescanned a near-root subtree: the memo alone removed 85 % of the CALLS and none of
3601
+ * the COST.
3602
+ *
3603
+ * WHY AN INDEX RATHER THAN A SMALLER WALK. "Is node X of type T inside subtree S" is an
3604
+ * interval question, and tree-sitter already gives every node a byte range. So one preorder
3605
+ * walk per tree groups nodes by type in document order, and a query becomes a binary search for
3606
+ * the first node of type T at or after S's start, then a forward scan while candidates begin
3607
+ * before S's end, keeping those that also END inside it. Cost becomes proportional to the
3608
+ * matches inside the subtree rather than to the subtree — the difference between 23,617 visits
3609
+ * per query and a handful.
3610
+ *
3611
+ * WHY THE KEY IS `(node.tree, node.id)` AND NOT THE NODE OBJECT. This is the load-bearing
3612
+ * detail, and it was measured rather than assumed, because the obvious key is wrong:
3613
+ * web-tree-sitter's `unmarshalNode` ends in `new Node(...)`, so **every** property access
3614
+ * returns a FRESH wrapper. Verified directly on the wired wasm (`_ident.mts` in this lane's
3615
+ * fixtures): `rootNode === rootNode` is false, `namedChild(0) === namedChild(0)` is false, while
3616
+ * `rootNode.tree === rootNode.tree` is true and `namedChild(0).id === namedChild(0).id` is true.
3617
+ * A `WeakMap<Node, …>` therefore misses on essentially every lookup — which is exactly what a
3618
+ * first attempt did: it removed 85 % of the `descendants` CALLS and none of the COST, and an
3619
+ * index built the same way rebuilt itself **200+ times for one tree** because the root handle
3620
+ * it keyed on was a new object each time. `Node.id` is documented as unique within a tree and
3621
+ * `Node.tree` is the tree it belongs to, and both are plain fields, so together they are a
3622
+ * stable identity.
3623
+ *
3624
+ * WHY PER TREE, LAZILY. `runResync` re-parses each top-level statement into its OWN tree and
3625
+ * feeds those nodes back through `processRoot`, so there is more than one tree per parse and
3626
+ * "the" tree is not a field anyone can set — `node.tree` is what says which. The build is lazy
3627
+ * because a file that asks no subtree question should not pay for it.
3628
+ */
3629
+ treeIndexes = /* @__PURE__ */ new WeakMap();
3630
+ /** This tree's index, built on first use. One preorder walk, then O(1) thereafter. */
3631
+ indexFor(node) {
3632
+ const tree = node.tree;
3633
+ const existing = this.treeIndexes.get(tree);
3634
+ if (existing) return existing;
3635
+ const byType = /* @__PURE__ */ new Map();
3636
+ const nodes = /* @__PURE__ */ new Map();
3637
+ const stack = [{ node: node.tree.rootNode, depth: 0 }];
3638
+ while (stack.length) {
3639
+ const { node: current, depth } = stack.pop();
3640
+ const count = current.namedChildCount;
3641
+ const children = [];
3642
+ for (let i = 0; i < count; i++) {
3643
+ const child = current.namedChild(i);
3644
+ if (child) children.push(child);
3645
+ }
3646
+ const entry = nodes.get(current.id);
3647
+ if (entry) {
3648
+ entry.children = children;
3649
+ entry.depth = depth;
3650
+ } else nodes.set(current.id, { children, depth });
3651
+ const bucket = byType.get(current.type);
3652
+ if (bucket) bucket.push(current);
3653
+ else byType.set(current.type, [current]);
3654
+ for (let i = children.length - 1; i >= 0; i--) stack.push({ node: children[i], depth: depth + 1 });
3655
+ }
3656
+ const index = { byType, nodes };
3657
+ this.treeIndexes.set(tree, index);
3658
+ return index;
3659
+ }
3660
+ firstIdentifier(node, state) {
3661
+ if (!node) return null;
3662
+ void state;
3663
+ const ids = this.descendants(node, "identifier");
3664
+ if (ids.length) return ids[0];
3665
+ const literals = this.descendants(node, "literal");
3666
+ return literals.length ? literals[0] : null;
3667
+ }
3668
+ containsChildType(node, type) {
3669
+ if (node.type === type) return true;
3670
+ for (const child of this.namedChildren(node)) if (this.containsChildType(child, type)) return true;
3671
+ return false;
3672
+ }
3673
+ /**
3674
+ * An index's key columns, in source order.
3675
+ *
3676
+ * `index_fields` holds a list of `field` nodes, each of which names its column
3677
+ * in a FIELD (`column` for a plain name, `function` for an expression) and
3678
+ * carries the name as a child — an `identifier` when unquoted, a `literal` when
3679
+ * quoted. Collecting `identifier` descendants alone therefore dropped every
3680
+ * quoted index column (`ON t ("b")` reported `[]`) and, on an expression
3681
+ * column, reported the expression's ARGUMENT rather than the expression.
3682
+ * Read the `field`'s own `column` field first, and its `function` text second,
3683
+ * because both are what the writer of the index meant.
3684
+ */
3685
+ identifierTexts(node, state) {
3686
+ if (!node) return [];
3687
+ const out = [];
3688
+ for (const field of this.namedChildren(node)) {
3689
+ if (field.type !== "field") continue;
3690
+ const column = field.childForFieldName?.("column");
3691
+ if (column) {
3692
+ out.push(this.identifierName(column, state));
3693
+ continue;
3694
+ }
3695
+ const fn = field.childForFieldName?.("function");
3696
+ if (fn) {
3697
+ out.push(fn.text);
3698
+ continue;
3699
+ }
3700
+ const leaf = this.firstIdentifier(field, state);
3701
+ if (leaf) out.push(this.identifierName(leaf, state));
3702
+ }
3703
+ return out;
3704
+ }
3705
+ literalTexts(node) {
3706
+ return node ? this.descendants(node, "literal").map((n) => n.text) : [];
3707
+ }
3708
+ containsType(node, type) {
3709
+ return this.findDescendant(node, type) !== null;
3710
+ }
3711
+ /**
3712
+ * The first descendant of `type` in DOCUMENT order, or null.
3713
+ *
3714
+ * Same order `descendants` produces — children pushed in reverse so the stack pops left to
3715
+ * right — but it STOPS at the first match. `descendants(node, type)[0]` walked the entire
3716
+ * subtree and then threw all but one node away, and `containsType` above did the same to ask
3717
+ * a yes/no question; both were hot enough to show in the profile.
3718
+ */
3719
+ /**
3720
+ * The first DESCENDANT of `type` in document order, or null.
3721
+ *
3722
+ * `findDescendant` stops at the first match; `descendants` cannot, because it returns a list.
3723
+ * Both read the same index, so a `null` here and an empty array there mean the same thing.
3724
+ */
3725
+ findDescendant(node, type) {
3726
+ if (node.type === type) return node;
3727
+ const index = this.indexFor(node);
3728
+ const depth = index.nodes.get(node.id).depth;
3729
+ const candidates = index.byType.get(type);
3730
+ if (!candidates) return null;
3731
+ for (let i = this.lowerBound(candidates, node.startIndex); i < candidates.length; i++) {
3732
+ const candidate = candidates[i];
3733
+ if (candidate.startIndex > node.endIndex) break;
3734
+ if (candidate.endIndex <= node.endIndex && index.nodes.get(candidate.id).depth > depth) return candidate;
3735
+ }
3736
+ return null;
3737
+ }
3738
+ /** The first entry of a document-order array whose `startIndex` is >= the given one. */
3739
+ lowerBound(sorted, startIndex) {
3740
+ let lo = 0;
3741
+ let hi = sorted.length;
3742
+ while (lo < hi) {
3743
+ const mid = lo + hi >> 1;
3744
+ if (sorted[mid].startIndex < startIndex) lo = mid + 1;
3745
+ else hi = mid;
3746
+ }
3747
+ return lo;
3748
+ }
3749
+ firstDescendant(node, type) {
3750
+ return this.findDescendant(node, type);
3751
+ }
3752
+ /**
3753
+ * Every DESCENDANT of `type` in `node`'s subtree, in document order.
3754
+ *
3755
+ * `type` is REQUIRED, not optional. The walk this replaced had an optional type and an untyped
3756
+ * branch, and all 14 call sites passed one — so the untyped branch was unreachable code that a
3757
+ * source count would have called a feature. Requiring the argument turns that from a claim into
3758
+ * a compile error if a caller ever wants the untyped walk.
3759
+ */
3760
+ descendants(node, type) {
3761
+ const index = this.indexFor(node);
3762
+ const entry = index.nodes.get(node.id);
3763
+ let memo = entry.descendants;
3764
+ if (!memo) {
3765
+ memo = /* @__PURE__ */ new Map();
3766
+ entry.descendants = memo;
3767
+ }
3768
+ const hit = memo.get(type);
3769
+ if (hit) return hit;
3770
+ const found = this.withinType(index, node, type);
3771
+ memo.set(type, found);
3772
+ return found;
3773
+ }
3774
+ /** Every node of `type` strictly inside `node`'s subtree, in document order. */
3775
+ withinType(index, node, type) {
3776
+ const candidates = index.byType.get(type);
3777
+ if (!candidates) return [];
3778
+ const depth = index.nodes.get(node.id).depth;
3779
+ const found = [];
3780
+ for (let i = this.lowerBound(candidates, node.startIndex); i < candidates.length; i++) {
3781
+ const candidate = candidates[i];
3782
+ if (candidate.startIndex > node.endIndex) break;
3783
+ if (candidate.endIndex <= node.endIndex && index.nodes.get(candidate.id).depth > depth) found.push(candidate);
3784
+ }
3785
+ return found;
3786
+ }
3787
+ namedChildren(node) {
3788
+ return this.indexFor(node).nodes.get(node.id).children;
3789
+ }
3790
+ /**
3791
+ * 1-based rows AND columns.
3792
+ *
3793
+ * The engine's own ParseError range uses `startPosition.column + 1`
3794
+ * (engine.ts:1180) and every other analyzer that builds a range helper does
3795
+ * the same — this one was the only 0-based range in the tree, so all four
3796
+ * column fields of every SQL symbol were off by one across the whole corpus.
3797
+ * `startIndex`/`endIndex` stay 0-based byte offsets, as everywhere else.
3798
+ */
3799
+ range(node, state) {
3800
+ const slice = state.slice;
3801
+ const startRow = node.startPosition.row + slice.rowOffset;
3802
+ const endRow = node.endPosition.row + slice.rowOffset;
3803
+ return {
3804
+ startLine: startRow + 1,
3805
+ startColumn: node.startPosition.row === 0 ? node.startPosition.column + slice.columnOffset + 1 : node.startPosition.column + 1,
3806
+ endLine: endRow + 1,
3807
+ endColumn: node.endPosition.row === 0 ? node.endPosition.column + slice.columnOffset + 1 : node.endPosition.column + 1,
3808
+ startIndex: node.startIndex + slice.byteOffset,
3809
+ endIndex: node.endIndex + slice.byteOffset
3810
+ };
3811
+ }
3812
+ countLines(content) {
3813
+ let lines = 1;
3814
+ for (let i = 0; i < content.length; i++) if (content[i] === "\n") lines++;
3815
+ return lines;
3816
+ }
3817
+ };
3818
+ var DECLARATION_NODES = /* @__PURE__ */ new Set([
3819
+ "create_table",
3820
+ "create_view",
3821
+ "create_materialized_view",
3822
+ "create_index",
3823
+ "create_function",
3824
+ "create_procedure",
3825
+ "create_trigger",
3826
+ "create_type",
3827
+ "create_schema",
3828
+ "create_sequence",
3829
+ "create_role",
3830
+ "create_extension",
3831
+ "create_database",
3832
+ "create_policy"
3833
+ ]);
3834
+ export {
3835
+ DECLARATION_NODES,
3836
+ RealSqlAnalyzer,
3837
+ SQL_BUILTINS,
3838
+ SQL_KEYWORDS,
3839
+ SQL_PROVIDED_FUNCTIONS
3840
+ };