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;
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}