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