linkrs_query/
extensions.rs1use std::sync::Arc;
21
22use linkrs_core::error::DBResult;
23use parking_lot::RwLock;
24
25use crate::binder::BoundStatement;
26use crate::parser::ast::stmt::Ast;
27use crate::planning::plan::ExecutionPlan;
28use crate::planning::plan::SubPlan;
29
30#[derive(Debug, Clone, Default)]
32pub struct BinderExtensionContext {
33 pub space_name: Option<String>,
34 pub space_id: u64,
35}
36
37pub trait ParserExtension: Send + Sync + std::fmt::Debug {
41 fn name(&self) -> &'static str {
42 std::any::type_name::<Self>()
43 }
44
45 fn transform(&self, _query_text: &str) -> Option<DBResult<String>> {
46 None
47 }
48}
49
50pub trait BinderExtension: Send + Sync + std::fmt::Debug {
54 fn name(&self) -> &'static str {
55 std::any::type_name::<Self>()
56 }
57
58 fn bind(
59 &self,
60 _ast: &Arc<Ast>,
61 _ctx: &BinderExtensionContext,
62 ) -> Option<DBResult<BoundStatement>> {
63 None
64 }
65}
66
67pub trait PlannerExtension: Send + Sync + std::fmt::Debug {
71 fn name(&self) -> &'static str {
72 std::any::type_name::<Self>()
73 }
74
75 fn plan(&self, _bound: &BoundStatement) -> Option<DBResult<SubPlan>> {
76 None
77 }
78}
79
80pub trait MapperExtension: Send + Sync + std::fmt::Debug {
85 fn name(&self) -> &'static str {
86 std::any::type_name::<Self>()
87 }
88
89 fn map(&self, _plan: &ExecutionPlan) -> Option<DBResult<ExecutionPlan>> {
90 None
91 }
92}
93
94#[derive(Debug, Default)]
98pub struct ExtensionRegistry {
99 parsers: RwLock<Vec<Arc<dyn ParserExtension>>>,
100 binders: RwLock<Vec<Arc<dyn BinderExtension>>>,
101 planners: RwLock<Vec<Arc<dyn PlannerExtension>>>,
102 mappers: RwLock<Vec<Arc<dyn MapperExtension>>>,
103}
104
105impl ExtensionRegistry {
106 pub fn new() -> Self {
107 Self::default()
108 }
109
110 pub fn add_parser_extension(&self, extension: Arc<dyn ParserExtension>) {
111 self.parsers.write().push(extension);
112 }
113
114 pub fn add_binder_extension(&self, extension: Arc<dyn BinderExtension>) {
115 self.binders.write().push(extension);
116 }
117
118 pub fn add_planner_extension(&self, extension: Arc<dyn PlannerExtension>) {
119 self.planners.write().push(extension);
120 }
121
122 pub fn add_mapper_extension(&self, extension: Arc<dyn MapperExtension>) {
123 self.mappers.write().push(extension);
124 }
125
126 pub fn parser_extension_count(&self) -> usize {
127 self.parsers.read().len()
128 }
129
130 pub fn binder_extension_count(&self) -> usize {
131 self.binders.read().len()
132 }
133
134 pub fn planner_extension_count(&self) -> usize {
135 self.planners.read().len()
136 }
137
138 pub fn mapper_extension_count(&self) -> usize {
139 self.mappers.read().len()
140 }
141
142 fn snapshot_parsers(&self) -> Vec<Arc<dyn ParserExtension>> {
143 let guard = self.parsers.read();
144 if guard.is_empty() {
145 return Vec::new();
146 }
147 guard.clone()
148 }
149
150 fn snapshot_binders(&self) -> Vec<Arc<dyn BinderExtension>> {
151 let guard = self.binders.read();
152 if guard.is_empty() {
153 return Vec::new();
154 }
155 guard.clone()
156 }
157
158 fn snapshot_planners(&self) -> Vec<Arc<dyn PlannerExtension>> {
159 let guard = self.planners.read();
160 if guard.is_empty() {
161 return Vec::new();
162 }
163 guard.clone()
164 }
165
166 fn snapshot_mappers(&self) -> Vec<Arc<dyn MapperExtension>> {
167 let guard = self.mappers.read();
168 if guard.is_empty() {
169 return Vec::new();
170 }
171 guard.clone()
172 }
173
174 pub fn try_parse_transform(&self, query_text: &str) -> Option<DBResult<String>> {
176 for extension in self.snapshot_parsers() {
177 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
178 extension.transform(query_text)
179 }));
180 match outcome {
181 Ok(Some(result)) => return Some(result),
182 Ok(None) => continue,
183 Err(_) => {
184 log::error!(
185 "parser extension {} panicked; falling back to builtin",
186 extension.name()
187 );
188 }
189 }
190 }
191 None
192 }
193
194 pub fn try_bind(
196 &self,
197 ast: &Arc<Ast>,
198 ctx: &BinderExtensionContext,
199 ) -> Option<DBResult<BoundStatement>> {
200 for extension in self.snapshot_binders() {
201 let outcome =
202 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| extension.bind(ast, ctx)));
203 match outcome {
204 Ok(Some(result)) => return Some(result),
205 Ok(None) => continue,
206 Err(_) => {
207 log::error!(
208 "binder extension {} panicked; falling back to builtin",
209 extension.name()
210 );
211 }
212 }
213 }
214 None
215 }
216
217 pub fn try_plan(&self, bound: &BoundStatement) -> Option<DBResult<SubPlan>> {
219 for extension in self.snapshot_planners() {
220 let outcome =
221 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| extension.plan(bound)));
222 match outcome {
223 Ok(Some(result)) => return Some(result),
224 Ok(None) => continue,
225 Err(_) => {
226 log::error!(
227 "planner extension {} panicked; falling back to builtin",
228 extension.name()
229 );
230 }
231 }
232 }
233 None
234 }
235
236 pub fn try_map(&self, plan: &ExecutionPlan) -> Option<DBResult<ExecutionPlan>> {
238 for extension in self.snapshot_mappers() {
239 let outcome =
240 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| extension.map(plan)));
241 match outcome {
242 Ok(Some(result)) => return Some(result),
243 Ok(None) => continue,
244 Err(_) => {
245 log::error!(
246 "mapper extension {} panicked; falling back to builtin",
247 extension.name()
248 );
249 }
250 }
251 }
252 None
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259 use crate::parser::Parser;
260 use std::sync::atomic::{AtomicUsize, Ordering};
261
262 fn parse_ast(query: &str) -> Arc<Ast> {
263 let mut parser = Parser::new(query);
264 let result = parser.parse().expect("test query parses");
265 assert!(!parser.has_errors());
266 result.ast
267 }
268
269 #[derive(Debug)]
270 struct ShowInterceptor {
271 hits: Arc<AtomicUsize>,
272 }
273
274 impl BinderExtension for ShowInterceptor {
275 fn bind(
276 &self,
277 ast: &Arc<Ast>,
278 _ctx: &BinderExtensionContext,
279 ) -> Option<DBResult<BoundStatement>> {
280 if matches!(ast.stmt(), crate::parser::ast::Stmt::Show(_)) {
281 self.hits.fetch_add(1, Ordering::SeqCst);
282 return Some(Ok(BoundStatement::Other(Box::new(ast.stmt().clone()))));
285 }
286 None
287 }
288 }
289
290 #[derive(Debug)]
291 struct PanickingBinder;
292
293 impl BinderExtension for PanickingBinder {
294 fn bind(
295 &self,
296 _ast: &Arc<Ast>,
297 _ctx: &BinderExtensionContext,
298 ) -> Option<DBResult<BoundStatement>> {
299 panic!("extension boom");
300 }
301 }
302
303 #[derive(Debug)]
304 struct PanickingPlanner;
305
306 impl PlannerExtension for PanickingPlanner {
307 fn plan(&self, _bound: &BoundStatement) -> Option<DBResult<SubPlan>> {
308 panic!("planner boom");
309 }
310 }
311
312 #[derive(Debug)]
313 struct OtherClaimingPlanner {
314 hits: Arc<AtomicUsize>,
315 }
316
317 impl PlannerExtension for OtherClaimingPlanner {
318 fn plan(&self, bound: &BoundStatement) -> Option<DBResult<SubPlan>> {
319 if matches!(bound, BoundStatement::Other(_)) {
320 self.hits.fetch_add(1, Ordering::SeqCst);
321 return Some(Ok(SubPlan::new(None, None)));
322 }
323 None
324 }
325 }
326
327 #[derive(Debug)]
328 struct IdentityMapper {
329 hits: Arc<AtomicUsize>,
330 }
331
332 impl MapperExtension for IdentityMapper {
333 fn map(&self, plan: &ExecutionPlan) -> Option<DBResult<ExecutionPlan>> {
334 self.hits.fetch_add(1, Ordering::SeqCst);
335 Some(Ok(plan.clone()))
336 }
337 }
338
339 #[derive(Debug)]
340 struct PrefixParser;
341
342 impl ParserExtension for PrefixParser {
343 fn transform(&self, query_text: &str) -> Option<DBResult<String>> {
344 if query_text.starts_with("/* ext */") {
345 return Some(Ok(query_text.trim_start_matches("/* ext */").to_string()));
346 }
347 None
348 }
349 }
350
351 #[test]
352 fn binder_extension_intercepts_and_rewrites() {
353 let registry = ExtensionRegistry::new();
354 let hits = Arc::new(AtomicUsize::new(0));
355 registry.add_binder_extension(Arc::new(ShowInterceptor {
356 hits: Arc::clone(&hits),
357 }));
358
359 let ast = parse_ast("SHOW SPACES");
360 let rewritten = registry
361 .try_bind(&ast, &BinderExtensionContext::default())
362 .expect("interceptor claims SHOW");
363 assert!(rewritten.is_ok());
364 assert_eq!(hits.load(Ordering::SeqCst), 1);
365
366 let other = parse_ast("MATCH (n) RETURN n");
368 assert!(registry
369 .try_bind(&other, &BinderExtensionContext::default())
370 .is_none());
371 }
372
373 #[test]
374 fn panicking_extensions_fall_back_to_builtin() {
375 let registry = ExtensionRegistry::new();
376 registry.add_binder_extension(Arc::new(PanickingBinder));
377 registry.add_planner_extension(Arc::new(PanickingPlanner));
378
379 let ast = parse_ast("MATCH (n) RETURN n");
380 assert!(registry
381 .try_bind(&ast, &BinderExtensionContext::default())
382 .is_none());
383
384 let bound = BoundStatement::Other(Box::new(ast.stmt().clone()));
385 assert!(registry.try_plan(&bound).is_none());
386 }
387
388 #[test]
389 fn planner_extension_intercepts_matching_bound_statement() {
390 let registry = ExtensionRegistry::new();
391 let hits = Arc::new(AtomicUsize::new(0));
392 registry.add_planner_extension(Arc::new(OtherClaimingPlanner {
393 hits: Arc::clone(&hits),
394 }));
395
396 let ast = parse_ast("SHOW SPACES");
397 let bound = BoundStatement::Other(Box::new(ast.stmt().clone()));
398 let planned = registry
399 .try_plan(&bound)
400 .expect("other-claiming planner handles Other");
401 assert!(planned.is_ok());
402 assert_eq!(hits.load(Ordering::SeqCst), 1);
403 }
404
405 #[test]
406 fn parser_and_mapper_stubs_register_and_invoke() {
407 let registry = ExtensionRegistry::new();
408 registry.add_parser_extension(Arc::new(PrefixParser));
409 assert_eq!(registry.parser_extension_count(), 1);
410
411 let rewritten = registry
412 .try_parse_transform("/* ext */MATCH (n) RETURN n")
413 .expect("prefix parser claims marked queries");
414 assert_eq!(rewritten.unwrap(), "MATCH (n) RETURN n");
415 assert!(registry.try_parse_transform("MATCH (n) RETURN n").is_none());
416
417 let hits = Arc::new(AtomicUsize::new(0));
420 registry.add_mapper_extension(Arc::new(IdentityMapper {
421 hits: Arc::clone(&hits),
422 }));
423 let plan = ExecutionPlan::new(None);
424 let mapped = registry
425 .try_map(&plan)
426 .expect("identity mapper claims everything");
427 assert!(mapped.is_ok());
428 assert_eq!(hits.load(Ordering::SeqCst), 1);
429 }
430}