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 strat = strategy::Strategy {
30 module: module.clone(),
31 source_str: source,
32 };
33 let walker = CanonicalWalker::new(&strat, source.as_bytes());
34 walker.walk(tree.root_node(), &module, &mut graph);
35 graph
36}
37
38pub struct Lang;
39
40impl crate::lang::LangExtractor for Lang {
41 type Presets = Presets;
42 const LANG_TAG: &'static str = "sql";
43 const ALLOWED_KINDS: &'static [&'static str] =
44 &["function", "procedure", "view", "table", "schema"];
45 const ALLOWED_VISIBILITIES: &'static [&'static str] = &[];
46
47 fn extract(
48 uri: &str,
49 source: &str,
50 anchor: &Moniker,
51 deep: bool,
52 presets: &Self::Presets,
53 ) -> CodeGraph {
54 extract(uri, source, anchor, deep, presets)
55 }
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61 use crate::core::moniker::MonikerBuilder;
62
63 fn anchor() -> Moniker {
64 MonikerBuilder::new().project(b"app").build()
65 }
66
67 fn run(uri: &str, src: &str) -> CodeGraph {
68 extract(uri, src, &anchor(), false, &Presets::default())
69 }
70
71 fn def_monikers(g: &CodeGraph) -> Vec<String> {
72 g.defs()
73 .map(|d| crate::core::uri::to_uri(&d.moniker, &Default::default()).unwrap())
74 .collect()
75 }
76
77 fn ref_targets(g: &CodeGraph) -> Vec<String> {
78 g.refs()
79 .map(|r| crate::core::uri::to_uri(&r.target, &Default::default()).unwrap())
80 .collect()
81 }
82
83 #[test]
84 fn qualified_function_emits_full_signature() {
85 let g = run(
86 "foo.sql",
87 "CREATE FUNCTION public.bar(a int, b text) RETURNS int LANGUAGE sql AS $$ SELECT 1 $$;",
88 );
89 assert!(
90 def_monikers(&g).iter().any(|m| m
91 == "code+moniker://app/lang:sql/module:foo/schema:public/function:bar(a:int4,b:text)"),
92 "got defs: {:?}",
93 def_monikers(&g)
94 );
95 let func = g
96 .defs()
97 .find(|d| d.kind == b"function")
98 .expect("function def");
99 assert_eq!(func.signature, b"a:int4,b:text");
100 }
101
102 #[test]
103 fn unqualified_function_omits_schema() {
104 let g = run(
105 "foo.sql",
106 "CREATE FUNCTION bar() RETURNS void LANGUAGE sql AS $$ $$;",
107 );
108 assert!(
109 def_monikers(&g)
110 .iter()
111 .any(|m| m == "code+moniker://app/lang:sql/module:foo/function:bar()")
112 );
113 assert_eq!(g.defs().filter(|d| d.kind == b"function").count(), 1);
114 }
115
116 #[test]
117 fn overloads_with_different_types_both_land() {
118 let g = run(
119 "foo.sql",
120 "CREATE FUNCTION m(x int) RETURNS int LANGUAGE sql AS $$ SELECT x $$;\
121 CREATE FUNCTION m(x text) RETURNS text LANGUAGE sql AS $$ SELECT x $$;",
122 );
123 assert_eq!(g.defs().filter(|d| d.kind == b"function").count(), 2);
124 }
125
126 #[test]
127 fn create_table_emits_table_under_schema() {
128 let g = run(
129 "schema.sql",
130 "CREATE TABLE esac.module_t (id uuid PRIMARY KEY);",
131 );
132 assert!(
133 def_monikers(&g).iter().any(
134 |m| m == "code+moniker://app/lang:sql/module:schema/schema:esac/table:module_t"
135 )
136 );
137 }
138
139 #[test]
140 fn create_view_emits_view_and_call_ref() {
141 let g = run("schema.sql", "CREATE VIEW v AS SELECT esac.foo() FROM t;");
142 assert!(
143 def_monikers(&g)
144 .iter()
145 .any(|m| m == "code+moniker://app/lang:sql/module:schema/view:v")
146 );
147 assert!(
148 ref_targets(&g)
149 .iter()
150 .any(|t| t == "code+moniker://app/lang:sql/module:schema/schema:esac/function:foo"),
151 "got refs: {:?}",
152 ref_targets(&g)
153 );
154 }
155
156 #[test]
157 fn top_level_select_emits_qualified_call() {
158 let g = run("foo.sql", "SELECT public.bar(1, 2);");
159 assert!(
160 ref_targets(&g)
161 .iter()
162 .any(|t| t == "code+moniker://app/lang:sql/module:foo/schema:public/function:bar"),
163 "got refs: {:?}",
164 ref_targets(&g)
165 );
166 }
167
168 #[test]
169 fn unqualified_top_level_call_omits_schema() {
170 let g = run("foo.sql", "SELECT bar();");
171 assert!(
172 ref_targets(&g)
173 .iter()
174 .any(|t| t == "code+moniker://app/lang:sql/module:foo/function:bar"),
175 "got refs: {:?}",
176 ref_targets(&g)
177 );
178 }
179
180 #[test]
181 fn empty_source_yields_only_module_root() {
182 let g = run("db/functions/plan/create_plan.sql", "");
183 let defs: Vec<_> = g.defs().collect();
184 assert_eq!(defs.len(), 1);
185 assert_eq!(
186 crate::core::uri::to_uri(&defs[0].moniker, &Default::default()).unwrap(),
187 "code+moniker://app/lang:sql/dir:db/dir:functions/dir:plan/module:create_plan"
188 );
189 }
190
191 #[test]
192 fn nested_calls_both_emit_name_only_targets() {
193 let g = run("foo.sql", "SELECT f(g(a, b));");
194 assert!(
195 ref_targets(&g)
196 .iter()
197 .any(|t| t == "code+moniker://app/lang:sql/module:foo/function:f"),
198 "outer call f should emit name-only target, got refs: {:?}",
199 ref_targets(&g)
200 );
201 assert!(
202 ref_targets(&g)
203 .iter()
204 .any(|t| t == "code+moniker://app/lang:sql/module:foo/function:g"),
205 "inner call g should emit name-only target, got refs: {:?}",
206 ref_targets(&g)
207 );
208 }
209
210 #[test]
211 fn line_comment_emits_comment_def() {
212 let g = run(
213 "pkg.sql",
214 "-- this is a header\nCREATE FUNCTION f() RETURNS int LANGUAGE sql AS $$ SELECT 1 $$;",
215 );
216 assert_eq!(
217 g.defs().filter(|d| d.kind == b"comment").count(),
218 1,
219 "a single -- line comment must produce one comment def, defs: {:?}",
220 g.def_monikers()
221 );
222 }
223
224 #[test]
225 fn function_param_emits_uses_type_with_pg_catalog_target() {
226 let g = run(
227 "pkg.sql",
228 "CREATE FUNCTION f(x int, y text) RETURNS bigint LANGUAGE sql AS $$ SELECT 1 $$;",
229 );
230 let int_target = "code+moniker://app/external_pkg:pg_catalog/path:int4";
231 let text_target = "code+moniker://app/external_pkg:pg_catalog/path:text";
232 let bigint_target = "code+moniker://app/external_pkg:pg_catalog/path:int8";
233 let targets = ref_targets(&g);
234 assert!(
235 targets.iter().any(|t| t == int_target),
236 "int param must emit uses_type → pg_catalog/path:int4, got: {targets:?}"
237 );
238 assert!(
239 targets.iter().any(|t| t == text_target),
240 "text param must emit uses_type → pg_catalog/path:text"
241 );
242 assert!(
243 targets.iter().any(|t| t == bigint_target),
244 "bigint return must emit uses_type → pg_catalog/path:int8"
245 );
246 let uses_type_count = g.refs().filter(|r| r.kind == b"uses_type").count();
247 assert!(
248 uses_type_count >= 3,
249 "expected at least 3 uses_type refs (2 params + 1 return), got {uses_type_count}"
250 );
251 }
252
253 #[test]
254 fn table_column_types_emit_uses_type() {
255 let g = run("pkg.sql", "CREATE TABLE t (id int, name text);");
256 let int_target = "code+moniker://app/external_pkg:pg_catalog/path:int4";
257 let text_target = "code+moniker://app/external_pkg:pg_catalog/path:text";
258 let targets = ref_targets(&g);
259 assert!(
260 targets.iter().any(|t| t == int_target),
261 "column type int must emit uses_type → int4"
262 );
263 assert!(
264 targets.iter().any(|t| t == text_target),
265 "column type text must emit uses_type → text"
266 );
267 }
268
269 #[test]
270 fn builtin_function_call_carries_external_confidence() {
271 let g = run("pkg.sql", "SELECT now();");
272 let r = g
273 .refs()
274 .find(|r| r.kind == b"calls")
275 .expect("calls ref for now()");
276 assert_eq!(
277 r.confidence,
278 b"external".to_vec(),
279 "builtin functions like now() must be marked external, got {:?}",
280 std::str::from_utf8(&r.confidence).unwrap_or("?")
281 );
282 }
283
284 #[test]
285 fn function_def_has_byte_range() {
286 let g = run(
287 "pkg.sql",
288 "CREATE FUNCTION f() RETURNS int LANGUAGE sql AS $$ SELECT 1 $$;",
289 );
290 let func = g.defs().find(|d| d.kind == b"function").expect("function");
291 let (s, e) = func.position.expect("position");
292 assert!(s <= e, "start={s} end={e}");
293 }
294}