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