code_moniker_core/lang/sql/
mod.rs1mod body;
2mod canonicalize;
3mod kinds;
4mod strategy;
5
6use canonicalize::compute_module_moniker;
7
8use crate::core::code_graph::CodeGraph;
9use crate::core::moniker::Moniker;
10
11use crate::lang::canonical_walker::CanonicalWalker;
12
13#[derive(Clone, Debug, Default)]
14pub struct Presets {
15 pub external_schemas: Vec<String>,
16}
17
18pub fn extract(
19 uri: &str,
20 source: &str,
21 anchor: &Moniker,
22 _deep: bool,
23 _presets: &Presets,
24) -> CodeGraph {
25 let module = compute_module_moniker(anchor, uri);
26 let (def_cap, ref_cap) = CodeGraph::capacity_for_source(source.len());
27 let mut graph = CodeGraph::with_capacity(module.clone(), kinds::MODULE, def_cap, ref_cap);
28 let tree = strategy::parse(source);
29 let callable_table =
30 strategy::collect_callable_table(tree.root_node(), source.as_bytes(), &module);
31 let strat = strategy::Strategy {
32 module: module.clone(),
33 source_str: source,
34 emit_comments: true,
35 callable_table: &callable_table,
36 };
37 let walker = CanonicalWalker::new(&strat, source.as_bytes());
38 walker.walk(tree.root_node(), &module, &mut graph);
39 graph
40}
41
42pub struct Lang;
43
44impl crate::lang::LangExtractor for Lang {
45 type Presets = Presets;
46 const LANG_TAG: &'static str = "sql";
47 const ALLOWED_KINDS: &'static [&'static str] =
48 &["function", "procedure", "view", "table", "schema"];
49 const ALLOWED_VISIBILITIES: &'static [&'static str] = &[];
50
51 fn extract(
52 uri: &str,
53 source: &str,
54 anchor: &Moniker,
55 deep: bool,
56 presets: &Self::Presets,
57 ) -> CodeGraph {
58 extract(uri, source, anchor, deep, presets)
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65 use crate::core::moniker::MonikerBuilder;
66
67 fn anchor() -> Moniker {
68 MonikerBuilder::new().project(b"app").build()
69 }
70
71 fn run(uri: &str, src: &str) -> CodeGraph {
72 extract(uri, src, &anchor(), false, &Presets::default())
73 }
74
75 fn def_monikers(g: &CodeGraph) -> Vec<String> {
76 g.defs()
77 .map(|d| crate::core::uri::to_uri(&d.moniker, &Default::default()).unwrap())
78 .collect()
79 }
80
81 fn ref_targets(g: &CodeGraph) -> Vec<String> {
82 g.refs()
83 .map(|r| crate::core::uri::to_uri(&r.target, &Default::default()).unwrap())
84 .collect()
85 }
86
87 #[test]
88 fn qualified_function_emits_full_signature() {
89 let g = run(
90 "foo.sql",
91 "CREATE FUNCTION public.bar(a int, b text) RETURNS int LANGUAGE sql AS $$ SELECT 1 $$;",
92 );
93 assert!(
94 def_monikers(&g).iter().any(|m| m
95 == "code+moniker://app/lang:sql/module:foo/schema:public/function:bar(a:int4,b:text)"),
96 "got defs: {:?}",
97 def_monikers(&g)
98 );
99 let func = g
100 .defs()
101 .find(|d| d.kind == b"function")
102 .expect("function def");
103 assert_eq!(func.signature, b"a:int4,b:text");
104 }
105
106 #[test]
107 fn overloads_with_different_types_both_land() {
108 let g = run(
109 "foo.sql",
110 "CREATE FUNCTION m(x int) RETURNS int LANGUAGE sql AS $$ SELECT x $$;\
111 CREATE FUNCTION m(x text) RETURNS text LANGUAGE sql AS $$ SELECT x $$;",
112 );
113 assert_eq!(g.defs().filter(|d| d.kind == b"function").count(), 2);
114 }
115
116 #[test]
117 fn top_level_select_emits_qualified_call() {
118 let g = run("foo.sql", "SELECT public.bar(1, 2);");
119 assert!(
120 ref_targets(&g)
121 .iter()
122 .any(|t| t == "code+moniker://app/lang:sql/module:foo/schema:public/function:bar"),
123 "got refs: {:?}",
124 ref_targets(&g)
125 );
126 }
127
128 #[test]
129 fn empty_source_yields_only_module_root() {
130 let g = run("db/functions/plan/create_plan.sql", "");
131 let defs: Vec<_> = g.defs().collect();
132 assert_eq!(defs.len(), 1);
133 assert_eq!(
134 crate::core::uri::to_uri(&defs[0].moniker, &Default::default()).unwrap(),
135 "code+moniker://app/lang:sql/dir:db/dir:functions/dir:plan/module:create_plan"
136 );
137 }
138
139 #[test]
140 fn nested_calls_both_emit_name_only_targets() {
141 let g = run("foo.sql", "SELECT f(g(a, b));");
142 assert!(
143 ref_targets(&g)
144 .iter()
145 .any(|t| t == "code+moniker://app/lang:sql/module:foo/function:f"),
146 "outer call f should emit name-only target, got refs: {:?}",
147 ref_targets(&g)
148 );
149 assert!(
150 ref_targets(&g)
151 .iter()
152 .any(|t| t == "code+moniker://app/lang:sql/module:foo/function:g"),
153 "inner call g should emit name-only target, got refs: {:?}",
154 ref_targets(&g)
155 );
156 }
157
158 #[test]
159 fn comment_def_bytes_are_a_real_comment_in_outer_source() {
160 let src = r#"CREATE OR REPLACE FUNCTION foo.bar(
161 p_a uuid,
162 p_b text
163)
164RETURNS void
165LANGUAGE plpgsql
166SECURITY DEFINER
167SET search_path = foo, pg_temp
168AS $$
169DECLARE
170 v_x text;
171BEGIN
172 -- real comment, do not lose
173 v_x := 'hello';
174END;
175$$;
176"#;
177 let g = run("fixture.sql", src);
178 for d in g.defs().filter(|d| d.kind == b"comment") {
179 let (s, e) = d.position.expect("comment def must have a position");
180 let slice = &src.as_bytes()[s as usize..e as usize];
181 assert!(
182 slice.starts_with(b"--") || slice.starts_with(b"/*"),
183 "comment def bytes {s}..{e} are not a real comment: {:?}",
184 std::str::from_utf8(slice).unwrap_or("?")
185 );
186 }
187 }
188
189 #[test]
190 fn function_param_emits_uses_type_with_pg_catalog_target() {
191 let g = run(
192 "pkg.sql",
193 "CREATE FUNCTION f(x int, y text) RETURNS bigint LANGUAGE sql AS $$ SELECT 1 $$;",
194 );
195 let int_target = "code+moniker://app/external_pkg:pg_catalog/path:int4";
196 let text_target = "code+moniker://app/external_pkg:pg_catalog/path:text";
197 let bigint_target = "code+moniker://app/external_pkg:pg_catalog/path:int8";
198 let targets = ref_targets(&g);
199 assert!(
200 targets.iter().any(|t| t == int_target),
201 "int param must emit uses_type → pg_catalog/path:int4, got: {targets:?}"
202 );
203 assert!(
204 targets.iter().any(|t| t == text_target),
205 "text param must emit uses_type → pg_catalog/path:text"
206 );
207 assert!(
208 targets.iter().any(|t| t == bigint_target),
209 "bigint return must emit uses_type → pg_catalog/path:int8"
210 );
211 let uses_type_count = g.refs().filter(|r| r.kind == b"uses_type").count();
212 assert!(
213 uses_type_count >= 3,
214 "expected at least 3 uses_type refs (2 params + 1 return), got {uses_type_count}"
215 );
216 }
217
218 #[test]
219 fn builtin_function_call_carries_external_confidence() {
220 let g = run("pkg.sql", "SELECT now();");
221 let r = g
222 .refs()
223 .find(|r| r.kind == b"calls")
224 .expect("calls ref for now()");
225 assert_eq!(
226 r.confidence,
227 b"external".to_vec(),
228 "builtin functions like now() must be marked external, got {:?}",
229 std::str::from_utf8(&r.confidence).unwrap_or("?")
230 );
231 }
232}