@backbencher/brain 0.0.0-stage → 0.1.0
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.
- package/CHANGELOG.md +298 -0
- package/EMBEDDINGS.md +355 -0
- package/LICENSE +21 -0
- package/README.md +415 -2
- package/bin/bb +13 -0
- package/dist/analysis-exclusions-7ZJDXOVZ.js +35 -0
- package/dist/brain-index-health-KY7K552E.js +17 -0
- package/dist/brain-relationship-enrichment-QH7OOHUF.js +558 -0
- package/dist/brain.sql-EIM4WS4D.js +29 -0
- package/dist/chunk-2BSKD7O3.js +40 -0
- package/dist/chunk-2DE27DVC.js +1314 -0
- package/dist/chunk-2RRBMNBQ.js +322 -0
- package/dist/chunk-2RTN2DFU.js +711 -0
- package/dist/chunk-2WXYNLBA.js +29 -0
- package/dist/chunk-3CTFIJJB.js +17 -0
- package/dist/chunk-3LFTSMIV.js +174 -0
- package/dist/chunk-47HKX4AJ.js +930 -0
- package/dist/chunk-4H22TLOT.js +543 -0
- package/dist/chunk-5SD45FLC.js +818 -0
- package/dist/chunk-66A5ZFYA.js +1758 -0
- package/dist/chunk-6AIRWAO2.js +247 -0
- package/dist/chunk-6WNGD3NN.js +196 -0
- package/dist/chunk-7AN7U5IZ.js +371 -0
- package/dist/chunk-BHB7BYYI.js +626 -0
- package/dist/chunk-BQ3OVQ3P.js +125 -0
- package/dist/chunk-CW4XXSNN.js +1479 -0
- package/dist/chunk-D2Q4Y47A.js +53 -0
- package/dist/chunk-DIYAGQJL.js +34 -0
- package/dist/chunk-DYM4FQEE.js +90 -0
- package/dist/chunk-EWMWK7WN.js +75 -0
- package/dist/chunk-FVVQTSIM.js +3427 -0
- package/dist/chunk-FWSL4VG6.js +1290 -0
- package/dist/chunk-GBI7V7KY.js +69 -0
- package/dist/chunk-GEFFO6O2.js +152 -0
- package/dist/chunk-HOWAQXJO.js +236 -0
- package/dist/chunk-HQSPJDHI.js +613 -0
- package/dist/chunk-IFZNR5UJ.js +2863 -0
- package/dist/chunk-J4PMBXQQ.js +336 -0
- package/dist/chunk-KAET47ZL.js +125 -0
- package/dist/chunk-KCDECRJY.js +440 -0
- package/dist/chunk-KLK3PBTO.js +1119 -0
- package/dist/chunk-L5TDYKYK.js +76 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-OWISBAOF.js +61 -0
- package/dist/chunk-PJTNGD3Z.js +6924 -0
- package/dist/chunk-QLK6ED4Q.js +235 -0
- package/dist/chunk-R2GW4F6R.js +19 -0
- package/dist/chunk-RCMTFSEF.js +87 -0
- package/dist/chunk-RH7KC5HU.js +12050 -0
- package/dist/chunk-RM3PY6TG.js +376 -0
- package/dist/chunk-ROMM6KMC.js +433 -0
- package/dist/chunk-SK6YVHFZ.js +211 -0
- package/dist/chunk-SM7RFQRO.js +214 -0
- package/dist/chunk-UFE77L67.js +782 -0
- package/dist/chunk-UGR4QIGS.js +67 -0
- package/dist/chunk-UUNHHX4M.js +13 -0
- package/dist/chunk-V44PGPA6.js +2104 -0
- package/dist/chunk-VL52PL2T.js +71 -0
- package/dist/chunk-VURYFIHW.js +201 -0
- package/dist/chunk-XJKJYIPP.js +7436 -0
- package/dist/chunk-YI6FF2ZR.js +340 -0
- package/dist/cli/index.js +37 -0
- package/dist/config-HZAAYDP3.js +10 -0
- package/dist/content-search-642F7E5V.js +593 -0
- package/dist/database-TCTZDLE5.js +9 -0
- package/dist/doctor-GXCKSJOX.js +395 -0
- package/dist/edit-engine-KM7MPFEC.js +51 -0
- package/dist/engine-42TZFNIZ.js +11 -0
- package/dist/engine-LO6L4JLQ.js +18 -0
- package/dist/fingerprint-XDXFRWO6.js +33 -0
- package/dist/framework-intelligence-DWFCKV44.js +860 -0
- package/dist/freshness-3K6VU5OT.js +174 -0
- package/dist/graph-engine-PL5YA7BA.js +21 -0
- package/dist/graph_request-MM4REP6H.js +350 -0
- package/dist/impact_request-WDE47IZ5.js +68 -0
- package/dist/index-space-IUUN5WYV.js +15 -0
- package/dist/index-status-6BD7UBHR.js +34 -0
- package/dist/index.js +159 -0
- package/dist/instance-FO6QZRLH.js +19 -0
- package/dist/language-detection-DFCAU5DZ.js +11 -0
- package/dist/lsp/server.js +4301 -0
- package/dist/mcp/server.js +132 -0
- package/dist/onboard-DIQZV2NW.js +161 -0
- package/dist/override-companion-pass-XR7MHTP7.js +117 -0
- package/dist/parallel-indexer-4PW3AFXP.js +88 -0
- package/dist/pool-worker.js +1153 -0
- package/dist/project-health-ZS5TS2Z6.js +94 -0
- package/dist/project-index-5SDPHITE.js +57 -0
- package/dist/project-registry-DGZBIZS7.js +355 -0
- package/dist/real-bash-analyzer-SXSV67E2.js +2201 -0
- package/dist/real-c-analyzer-SY7FTZ6W.js +5942 -0
- package/dist/real-clean-analyzer-IGDHHLZK.js +3470 -0
- package/dist/real-clojure-analyzer-4OH7IWLB.js +2818 -0
- package/dist/real-cpp-analyzer-7LTDRJRJ.js +4101 -0
- package/dist/real-csharp-analyzer-KICU3TLH.js +3298 -0
- package/dist/real-css-analyzer-HPU277Q6.js +15 -0
- package/dist/real-dart-analyzer-7LZ5LCMW.js +3944 -0
- package/dist/real-elixir-analyzer-BE3MEVZO.js +2192 -0
- package/dist/real-erlang-analyzer-BZMAKYBK.js +1731 -0
- package/dist/real-fsharp-analyzer-5QLTOV3E.js +2390 -0
- package/dist/real-go-analyzer-POD7MZQR.js +4126 -0
- package/dist/real-graphql-analyzer-MAQRZ6QW.js +1442 -0
- package/dist/real-groovy-analyzer-MEWSUPMJ.js +2285 -0
- package/dist/real-haskell-analyzer-I5L2GR3Y.js +2533 -0
- package/dist/real-hcl-analyzer-DBOPXSWJ.js +725 -0
- package/dist/real-html-analyzer-O63XZLZD.js +1759 -0
- package/dist/real-java-analyzer-CHCW3EKA.js +4677 -0
- package/dist/real-javascript-analyzer-QAAJQYZE.js +14 -0
- package/dist/real-json-analyzer-UGBR7YQN.js +1640 -0
- package/dist/real-julia-analyzer-35Q4GHOZ.js +2085 -0
- package/dist/real-kotlin-analyzer-GRAYCWW2.js +3956 -0
- package/dist/real-lua-analyzer-5BAPDZDF.js +1957 -0
- package/dist/real-makefile-analyzer-YJKAHNAS.js +920 -0
- package/dist/real-markdown-analyzer-UKUQZO4F.js +1215 -0
- package/dist/real-matlab-analyzer-FVIKQZKG.js +480 -0
- package/dist/real-nim-analyzer-M7FAIABG.js +3603 -0
- package/dist/real-objective-c-analyzer-XMYQIGQI.js +1518 -0
- package/dist/real-perl-analyzer-ZLY6M2DZ.js +1865 -0
- package/dist/real-php-analyzer-KI2ZAM7X.js +9368 -0
- package/dist/real-powershell-analyzer-B3T5ZP6U.js +2915 -0
- package/dist/real-protobuf-analyzer-23OZ75XV.js +867 -0
- package/dist/real-python-analyzer-I4TIUB6D.js +4880 -0
- package/dist/real-r-analyzer-FJZJXXS6.js +2596 -0
- package/dist/real-ruby-analyzer-M3UFIVTQ.js +2429 -0
- package/dist/real-rust-analyzer-LVSONPAN.js +3359 -0
- package/dist/real-scala-analyzer-CWPI33Q2.js +3293 -0
- package/dist/real-scss-analyzer-TGRJQ7TH.js +1515 -0
- package/dist/real-solidity-analyzer-4P2VBVWA.js +2608 -0
- package/dist/real-sql-analyzer-J7DYZOZR.js +3840 -0
- package/dist/real-svelte-analyzer-NHEN2SX5.js +2532 -0
- package/dist/real-swift-analyzer-2NSYULWI.js +2311 -0
- package/dist/real-toml-analyzer-L3C66L7Q.js +929 -0
- package/dist/real-typescript-analyzer-FTC6ETRQ.js +14 -0
- package/dist/real-vb-analyzer-ORY7LAQS.js +3145 -0
- package/dist/real-vba-analyzer-JH2TTUFI.js +466 -0
- package/dist/real-vue-analyzer-OO4FUGBV.js +2680 -0
- package/dist/real-xml-analyzer-6CQVR477.js +2405 -0
- package/dist/real-yaml-analyzer-J6MQG3YX.js +3528 -0
- package/dist/real-zig-analyzer-EFKCPJNU.js +2430 -0
- package/dist/regex-search-OPTLKUN5.js +239 -0
- package/dist/rules-I453JTRC.js +152 -0
- package/dist/scoped-path-MJYWJZQX.js +11 -0
- package/dist/setup-show-Y5GB5KXA.js +15 -0
- package/dist/setup-wizard-G5YYR5T6.js +24 -0
- package/dist/symbol-search-KB4MU4SI.js +16 -0
- package/dist/taint-YOTN5X55.js +202 -0
- package/dist/taint-analyzer-N7YLZU3N.js +163 -0
- package/dist/tool-handlers-G4NAGJIH.js +72 -0
- package/dist/tool-registry-BPEJNSZG.js +17 -0
- package/dist/tool-validation-RF675A3C.js +32 -0
- package/dist/type-inference-4Y7WTMFE.js +13 -0
- package/dist/vector-binary-LMY4QGAN.js +19 -0
- package/dist/version-LXGNFO37.js +9 -0
- package/dist/wasm-NMWO3WXY.js +16 -0
- package/docs/EMBEDDINGS.md +132 -0
- package/docs/README.md +178 -0
- package/docs/data/m10-baseline.json +602 -0
- package/docs/data/m10-benchmark-baseline.json +57 -0
- package/docs/data/m10-benchmark-last-run.json +127 -0
- package/docs/data/m10-final-audit.json +215 -0
- package/docs/data/m12-final-audit.json +138 -0
- package/docs/data/m12-surface-matrix.json +50 -0
- package/docs/data/public-tools.json +1902 -0
- package/docs/indexing/DEAD-ASSETS.md +88 -0
- package/docs/indexing/INDEXER-REVIEW.md +634 -0
- package/docs/indexing/PERFORMANCE-FINDINGS.md +106 -0
- package/docs/indexing/README.md +427 -0
- package/docs/indexing/discovery-matrix.md +41 -0
- package/docs/indexing/embeddings.md +325 -0
- package/docs/indexing/environment-variables.md +77 -0
- package/docs/indexing/performance.md +323 -0
- package/docs/indexing/resume.md +345 -0
- package/docs/indexing/troubleshooting.md +391 -0
- package/docs/man/bb.1 +190 -0
- package/docs/tools/bb_analyze.md +147 -0
- package/docs/tools/bb_annotate.md +110 -0
- package/docs/tools/bb_annotations.md +106 -0
- package/docs/tools/bb_ask.md +101 -0
- package/docs/tools/bb_cross_language.md +103 -0
- package/docs/tools/bb_doc.md +169 -0
- package/docs/tools/bb_edit.md +185 -0
- package/docs/tools/bb_graph.md +169 -0
- package/docs/tools/bb_health.md +156 -0
- package/docs/tools/bb_impact.md +117 -0
- package/docs/tools/bb_index.md +108 -0
- package/docs/tools/bb_migrate.md +135 -0
- package/docs/tools/bb_patterns.md +140 -0
- package/docs/tools/bb_search.md +150 -0
- package/docs/tools/bb_security.md +100 -0
- package/docs/tools/bb_visualize.md +184 -0
- package/grammars/README.md +57 -0
- package/grammars/THIRD-PARTY-NOTICES.md +50 -0
- package/grammars/tree-sitter-c/LICENSE +21 -0
- package/grammars/tree-sitter-clojure/LICENSE.md +21 -0
- package/grammars/tree-sitter-cpp/LICENSE +21 -0
- package/grammars/tree-sitter-csharp/LICENSE +21 -0
- package/grammars/tree-sitter-css/LICENSE +21 -0
- package/grammars/tree-sitter-css/README.md +21 -0
- package/grammars/tree-sitter-dart/LICENSE +20 -0
- package/grammars/tree-sitter-dart-upstream/LICENSE +20 -0
- package/grammars/tree-sitter-dart-upstream/README.md +18 -0
- package/grammars/tree-sitter-elixir/LICENSE +176 -0
- package/grammars/tree-sitter-elixir/README.md +11 -0
- package/grammars/tree-sitter-fsharp/LICENSE +21 -0
- package/grammars/tree-sitter-fsharp/README.md +45 -0
- package/grammars/tree-sitter-graphql/LICENSE +21 -0
- package/grammars/tree-sitter-graphql/README.md +106 -0
- package/grammars/tree-sitter-groovy/LICENSE +21 -0
- package/grammars/tree-sitter-groovy/README.md +4 -0
- package/grammars/tree-sitter-haskell/LICENSE +21 -0
- package/grammars/tree-sitter-haskell/README.md +394 -0
- package/grammars/tree-sitter-hcl/CHANGELOG.md +102 -0
- package/grammars/tree-sitter-hcl/LICENSE +201 -0
- package/grammars/tree-sitter-hcl/README.md +39 -0
- package/grammars/tree-sitter-html/LICENSE +21 -0
- package/grammars/tree-sitter-html/README.md +21 -0
- package/grammars/tree-sitter-julia/LICENSE +21 -0
- package/grammars/tree-sitter-julia/README.md +24 -0
- package/grammars/tree-sitter-kotlin/LICENSE +9 -0
- package/grammars/tree-sitter-lua/LICENSE.md +21 -0
- package/grammars/tree-sitter-lua/README.md +29 -0
- package/grammars/tree-sitter-make/LICENSE +21 -0
- package/grammars/tree-sitter-matlab/LICENSE +19 -0
- package/grammars/tree-sitter-matlab/README.md +99 -0
- package/grammars/tree-sitter-nim/LICENSE +373 -0
- package/grammars/tree-sitter-nim/README.md +46 -0
- package/grammars/tree-sitter-objc/LICENSE +21 -0
- package/grammars/tree-sitter-objc/README.md +20 -0
- package/grammars/tree-sitter-perl/LICENSE +7 -0
- package/grammars/tree-sitter-perl/README.md +100 -0
- package/grammars/tree-sitter-powershell/LICENSE +21 -0
- package/grammars/tree-sitter-powershell/README.md +7 -0
- package/grammars/tree-sitter-proto/LICENSE +21 -0
- package/grammars/tree-sitter-proto/README.md +105 -0
- package/grammars/tree-sitter-r/LICENSE +9 -0
- package/grammars/tree-sitter-r/README.md +66 -0
- package/grammars/tree-sitter-ruby/LICENSE +22 -0
- package/grammars/tree-sitter-scss/LICENSE +21 -0
- package/grammars/tree-sitter-scss/README.md +17 -0
- package/grammars/tree-sitter-solidity/LICENSE +7 -0
- package/grammars/tree-sitter-sql/LICENSE +21 -0
- package/grammars/tree-sitter-typescript/LICENSE +21 -0
- package/grammars/tree-sitter-vba/LICENSE +21 -0
- package/grammars/tree-sitter-vba/README.md +738 -0
- package/grammars/tree-sitter-vbnet/LICENSE +21 -0
- package/grammars/tree-sitter-vbnet/README.md +322 -0
- package/grammars/tree-sitter-xml/LICENSE +19 -0
- package/grammars/tree-sitter-yaml/LICENSE +20 -0
- package/grammars/tree-sitter-yaml/README.md +21 -0
- package/package.json +184 -4
- package/wasm/dart.wasm +0 -0
- package/wasm/tree-sitter-bash.wasm +0 -0
- package/wasm/tree-sitter-c.wasm +0 -0
- package/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/wasm/tree-sitter-clean.wasm +0 -0
- package/wasm/tree-sitter-clojure.wasm +0 -0
- package/wasm/tree-sitter-core_schema.wasm +0 -0
- package/wasm/tree-sitter-cpp.wasm +0 -0
- package/wasm/tree-sitter-css.wasm +0 -0
- package/wasm/tree-sitter-dart.wasm +0 -0
- package/wasm/tree-sitter-elisp.wasm +0 -0
- package/wasm/tree-sitter-elixir.wasm +0 -0
- package/wasm/tree-sitter-elm.wasm +0 -0
- package/wasm/tree-sitter-embedded_template.wasm +0 -0
- package/wasm/tree-sitter-erlang.wasm +0 -0
- package/wasm/tree-sitter-fsharp.wasm +0 -0
- package/wasm/tree-sitter-fsharp_signature.wasm +0 -0
- package/wasm/tree-sitter-go.wasm +0 -0
- package/wasm/tree-sitter-graphql.wasm +0 -0
- package/wasm/tree-sitter-groovy.wasm +0 -0
- package/wasm/tree-sitter-haskell.wasm +0 -0
- package/wasm/tree-sitter-hcl.wasm +0 -0
- package/wasm/tree-sitter-html.wasm +0 -0
- package/wasm/tree-sitter-java.wasm +0 -0
- package/wasm/tree-sitter-javascript.wasm +0 -0
- package/wasm/tree-sitter-json.wasm +0 -0
- package/wasm/tree-sitter-json_schema.wasm +0 -0
- package/wasm/tree-sitter-julia.wasm +0 -0
- package/wasm/tree-sitter-kotlin.wasm +0 -0
- package/wasm/tree-sitter-lua.wasm +0 -0
- package/wasm/tree-sitter-make.wasm +0 -0
- package/wasm/tree-sitter-markdown.wasm +0 -0
- package/wasm/tree-sitter-markdown_inline.wasm +0 -0
- package/wasm/tree-sitter-matlab.wasm +0 -0
- package/wasm/tree-sitter-nim.wasm +0 -0
- package/wasm/tree-sitter-objc.wasm +0 -0
- package/wasm/tree-sitter-ocaml.wasm +0 -0
- package/wasm/tree-sitter-perl.wasm +0 -0
- package/wasm/tree-sitter-php.wasm +0 -0
- package/wasm/tree-sitter-php_only.wasm +0 -0
- package/wasm/tree-sitter-powershell.wasm +0 -0
- package/wasm/tree-sitter-protobuf.wasm +0 -0
- package/wasm/tree-sitter-python.wasm +0 -0
- package/wasm/tree-sitter-ql.wasm +0 -0
- package/wasm/tree-sitter-r.wasm +0 -0
- package/wasm/tree-sitter-rescript.wasm +0 -0
- package/wasm/tree-sitter-ruby.wasm +0 -0
- package/wasm/tree-sitter-rust.wasm +0 -0
- package/wasm/tree-sitter-scala.wasm +0 -0
- package/wasm/tree-sitter-scss.wasm +0 -0
- package/wasm/tree-sitter-solidity.wasm +0 -0
- package/wasm/tree-sitter-sql.wasm +0 -0
- package/wasm/tree-sitter-svelte.wasm +0 -0
- package/wasm/tree-sitter-swift.wasm +0 -0
- package/wasm/tree-sitter-systemrdl.wasm +0 -0
- package/wasm/tree-sitter-terraform.wasm +0 -0
- package/wasm/tree-sitter-tlaplus.wasm +0 -0
- package/wasm/tree-sitter-toml.wasm +0 -0
- package/wasm/tree-sitter-tsx.wasm +0 -0
- package/wasm/tree-sitter-typescript.wasm +0 -0
- package/wasm/tree-sitter-vba.wasm +0 -0
- package/wasm/tree-sitter-vbnet.wasm +0 -0
- package/wasm/tree-sitter-vue.wasm +0 -0
- package/wasm/tree-sitter-xml.wasm +0 -0
- package/wasm/tree-sitter-yaml.wasm +0 -0
- package/wasm/tree-sitter-zig.wasm +0 -0
- package/wasm/tree-sitter.wasm +0 -0
- package/wasm/tsx.wasm +0 -0
- package/wasm/typescript.wasm +0 -0
|
@@ -0,0 +1,3840 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ASTEngine
|
|
3
|
+
} from "./chunk-V44PGPA6.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
|
+
};
|