Skip to main content

code_moniker_core/lang/sql/
mod.rs

1mod 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}