1use serde::{Deserialize, Serialize};
34use std::collections::{HashMap, HashSet};
35use std::fmt;
36use std::time::{Duration, Instant};
37
38#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
44pub enum LateralValue {
45 Iri(String),
47 Literal {
49 value: String,
51 datatype: Option<String>,
53 lang: Option<String>,
55 },
56 BlankNode(String),
58}
59
60impl fmt::Display for LateralValue {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 match self {
63 Self::Iri(iri) => write!(f, "<{iri}>"),
64 Self::Literal {
65 value,
66 datatype,
67 lang,
68 } => {
69 write!(f, "\"{value}\"")?;
70 if let Some(dt) = datatype {
71 write!(f, "^^<{dt}>")?;
72 }
73 if let Some(l) = lang {
74 write!(f, "@{l}")?;
75 }
76 Ok(())
77 }
78 Self::BlankNode(id) => write!(f, "_:{id}"),
79 }
80 }
81}
82
83pub type SolutionMapping = HashMap<String, LateralValue>;
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct LateralSubquery {
89 pub description: String,
91 pub correlated_vars: Vec<String>,
93 pub projected_vars: Vec<String>,
95 pub has_aggregates: bool,
97 pub limit: Option<usize>,
99 pub order_by: Vec<OrderSpec>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct OrderSpec {
106 pub variable: String,
108 pub ascending: bool,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct LateralJoin {
123 pub left_description: String,
125 pub subquery: LateralSubquery,
127 pub strategy: LateralStrategy,
129 pub pushed_filters: Vec<String>,
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
135pub enum LateralStrategy {
136 NestedLoop,
138 BatchedValues,
141 Decorrelate,
143 CachedCorrelation,
145}
146
147impl fmt::Display for LateralStrategy {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 match self {
150 Self::NestedLoop => write!(f, "NestedLoop"),
151 Self::BatchedValues => write!(f, "BatchedValues"),
152 Self::Decorrelate => write!(f, "Decorrelate"),
153 Self::CachedCorrelation => write!(f, "CachedCorrelation"),
154 }
155 }
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct LateralJoinConfig {
165 pub batch_size: usize,
167 pub cache_capacity: usize,
169 pub subquery_timeout: Duration,
171 pub auto_decorrelate: bool,
173 pub max_nesting_depth: usize,
175}
176
177impl Default for LateralJoinConfig {
178 fn default() -> Self {
179 Self {
180 batch_size: 128,
181 cache_capacity: 4096,
182 subquery_timeout: Duration::from_secs(30),
183 auto_decorrelate: true,
184 max_nesting_depth: 4,
185 }
186 }
187}
188
189#[derive(Debug, Clone, Default, Serialize, Deserialize)]
191pub struct LateralJoinStats {
192 pub left_rows: u64,
194 pub result_rows: u64,
196 pub subquery_evaluations: u64,
198 pub cache_hits: u64,
200 pub cache_misses: u64,
202 pub batches_submitted: u64,
204 pub subquery_time_ms: u64,
206 pub decorrelated: bool,
208 pub rows_filtered: u64,
210}
211
212impl LateralJoinStats {
213 pub fn cache_hit_ratio(&self) -> f64 {
215 let total = self.cache_hits + self.cache_misses;
216 if total == 0 {
217 return 0.0;
218 }
219 (self.cache_hits as f64 / total as f64) * 100.0
220 }
221
222 pub fn avg_subquery_time_ms(&self) -> f64 {
224 if self.subquery_evaluations == 0 {
225 return 0.0;
226 }
227 self.subquery_time_ms as f64 / self.subquery_evaluations as f64
228 }
229}
230
231pub struct LateralJoinExecutor {
233 config: LateralJoinConfig,
234 stats: LateralJoinStats,
235 cache: HashMap<String, Vec<SolutionMapping>>,
237}
238
239impl LateralJoinExecutor {
240 pub fn new(config: LateralJoinConfig) -> Self {
242 Self {
243 config,
244 stats: LateralJoinStats::default(),
245 cache: HashMap::new(),
246 }
247 }
248
249 pub fn with_defaults() -> Self {
251 Self::new(LateralJoinConfig::default())
252 }
253
254 pub fn stats(&self) -> &LateralJoinStats {
256 &self.stats
257 }
258
259 pub fn reset(&mut self) {
261 self.stats = LateralJoinStats::default();
262 self.cache.clear();
263 }
264
265 pub fn execute<F>(
271 &mut self,
272 lateral: &LateralJoin,
273 left_rows: &[SolutionMapping],
274 subquery_evaluator: F,
275 ) -> Result<Vec<SolutionMapping>, LateralJoinError>
276 where
277 F: Fn(&SolutionMapping) -> Result<Vec<SolutionMapping>, LateralJoinError>,
278 {
279 self.stats.left_rows = left_rows.len() as u64;
280
281 match lateral.strategy {
282 LateralStrategy::NestedLoop => {
283 self.execute_nested_loop(lateral, left_rows, subquery_evaluator)
284 }
285 LateralStrategy::BatchedValues => {
286 self.execute_batched(lateral, left_rows, subquery_evaluator)
287 }
288 LateralStrategy::CachedCorrelation => {
289 self.execute_cached(lateral, left_rows, subquery_evaluator)
290 }
291 LateralStrategy::Decorrelate => {
292 self.execute_cached(lateral, left_rows, subquery_evaluator)
295 }
296 }
297 }
298
299 fn execute_nested_loop<F>(
302 &mut self,
303 lateral: &LateralJoin,
304 left_rows: &[SolutionMapping],
305 evaluator: F,
306 ) -> Result<Vec<SolutionMapping>, LateralJoinError>
307 where
308 F: Fn(&SolutionMapping) -> Result<Vec<SolutionMapping>, LateralJoinError>,
309 {
310 let mut results = Vec::new();
311
312 for left_row in left_rows {
313 let correlated =
315 Self::extract_correlated_bindings(left_row, &lateral.subquery.correlated_vars);
316
317 if !self.passes_pushed_filters(left_row, &lateral.pushed_filters) {
319 self.stats.rows_filtered += 1;
320 continue;
321 }
322
323 let start = Instant::now();
324 let sub_results = evaluator(&correlated)?;
325 self.stats.subquery_time_ms += start.elapsed().as_millis() as u64;
326 self.stats.subquery_evaluations += 1;
327
328 for sub_row in &sub_results {
330 let merged = Self::merge_mappings(left_row, sub_row)?;
331 results.push(merged);
332 }
333 }
334
335 self.stats.result_rows = results.len() as u64;
336 Ok(results)
337 }
338
339 fn execute_batched<F>(
342 &mut self,
343 lateral: &LateralJoin,
344 left_rows: &[SolutionMapping],
345 evaluator: F,
346 ) -> Result<Vec<SolutionMapping>, LateralJoinError>
347 where
348 F: Fn(&SolutionMapping) -> Result<Vec<SolutionMapping>, LateralJoinError>,
349 {
350 let mut results = Vec::new();
351 let batch_size = self.config.batch_size.max(1);
352
353 for chunk in left_rows.chunks(batch_size) {
354 self.stats.batches_submitted += 1;
355
356 let batch_bindings =
358 Self::build_batch_bindings(chunk, &lateral.subquery.correlated_vars);
359
360 let start = Instant::now();
361 let batch_results = evaluator(&batch_bindings)?;
362 self.stats.subquery_time_ms += start.elapsed().as_millis() as u64;
363 self.stats.subquery_evaluations += 1;
364
365 for left_row in chunk {
369 if !self.passes_pushed_filters(left_row, &lateral.pushed_filters) {
370 self.stats.rows_filtered += 1;
371 continue;
372 }
373
374 for sub_row in &batch_results {
375 if Self::is_compatible(left_row, sub_row, &lateral.subquery.correlated_vars) {
376 let merged = Self::merge_mappings(left_row, sub_row)?;
377 results.push(merged);
378 }
379 }
380 }
381 }
382
383 self.stats.result_rows = results.len() as u64;
384 Ok(results)
385 }
386
387 fn execute_cached<F>(
390 &mut self,
391 lateral: &LateralJoin,
392 left_rows: &[SolutionMapping],
393 evaluator: F,
394 ) -> Result<Vec<SolutionMapping>, LateralJoinError>
395 where
396 F: Fn(&SolutionMapping) -> Result<Vec<SolutionMapping>, LateralJoinError>,
397 {
398 let mut results = Vec::new();
399
400 for left_row in left_rows {
401 if !self.passes_pushed_filters(left_row, &lateral.pushed_filters) {
402 self.stats.rows_filtered += 1;
403 continue;
404 }
405
406 let correlated =
407 Self::extract_correlated_bindings(left_row, &lateral.subquery.correlated_vars);
408 let cache_key = Self::cache_key(&correlated, &lateral.subquery.correlated_vars);
409
410 let sub_results = if let Some(cached) = self.cache.get(&cache_key) {
411 self.stats.cache_hits += 1;
412 cached.clone()
413 } else {
414 self.stats.cache_misses += 1;
415
416 let start = Instant::now();
417 let fresh = evaluator(&correlated)?;
418 self.stats.subquery_time_ms += start.elapsed().as_millis() as u64;
419 self.stats.subquery_evaluations += 1;
420
421 if self.cache.len() >= self.config.cache_capacity {
423 if let Some(first_key) = self.cache.keys().next().cloned() {
424 self.cache.remove(&first_key);
425 }
426 }
427 self.cache.insert(cache_key, fresh.clone());
428 fresh
429 };
430
431 for sub_row in &sub_results {
432 let merged = Self::merge_mappings(left_row, sub_row)?;
433 results.push(merged);
434 }
435 }
436
437 self.stats.result_rows = results.len() as u64;
438 Ok(results)
439 }
440
441 fn extract_correlated_bindings(
445 row: &SolutionMapping,
446 correlated_vars: &[String],
447 ) -> SolutionMapping {
448 let mut bindings = SolutionMapping::new();
449 for var in correlated_vars {
450 if let Some(val) = row.get(var) {
451 bindings.insert(var.clone(), val.clone());
452 }
453 }
454 bindings
455 }
456
457 fn build_batch_bindings(
461 rows: &[SolutionMapping],
462 correlated_vars: &[String],
463 ) -> SolutionMapping {
464 let mut combined = SolutionMapping::new();
465 for var in correlated_vars {
468 let mut seen = HashSet::new();
470 for row in rows {
471 if let Some(val) = row.get(var) {
472 let key = format!("{val}");
473 if seen.insert(key) {
474 combined.entry(var.clone()).or_insert_with(|| val.clone());
476 }
477 }
478 }
479 }
480 combined
481 }
482
483 fn is_compatible(
486 left: &SolutionMapping,
487 right: &SolutionMapping,
488 correlated_vars: &[String],
489 ) -> bool {
490 for var in correlated_vars {
491 match (left.get(var), right.get(var)) {
492 (Some(l), Some(r)) => {
493 if l != r {
494 return false;
495 }
496 }
497 (None, Some(_)) | (Some(_), None) => {
498 }
501 (None, None) => {}
502 }
503 }
504 true
505 }
506
507 fn merge_mappings(
510 left: &SolutionMapping,
511 right: &SolutionMapping,
512 ) -> Result<SolutionMapping, LateralJoinError> {
513 let mut merged = left.clone();
514 for (var, val) in right {
515 merged.insert(var.clone(), val.clone());
518 }
519 Ok(merged)
520 }
521
522 fn cache_key(correlated: &SolutionMapping, vars: &[String]) -> String {
524 let mut parts = Vec::with_capacity(vars.len());
525 for var in vars {
526 match correlated.get(var) {
527 Some(val) => parts.push(format!("{var}={val}")),
528 None => parts.push(format!("{var}=UNDEF")),
529 }
530 }
531 parts.join("|")
532 }
533
534 fn passes_pushed_filters(&self, row: &SolutionMapping, filters: &[String]) -> bool {
537 for filter in filters {
538 if let Some((var, expected)) = Self::parse_equality_filter(filter) {
539 if let Some(actual) = row.get(&var) {
540 let actual_str = format!("{actual}");
541 if actual_str != expected {
542 return false;
543 }
544 }
545 }
546 }
547 true
548 }
549
550 fn parse_equality_filter(filter: &str) -> Option<(String, String)> {
556 let parts: Vec<&str> = filter.splitn(3, ' ').collect();
557 if parts.len() == 3 && parts[1] == "=" {
558 let var = parts[0].trim_start_matches('?').to_string();
559 let val = parts[2].to_string();
563 Some((var, val))
564 } else {
565 None
566 }
567 }
568}
569
570#[derive(Default)]
576pub struct LateralOptimizer {
577 config: LateralOptimizerConfig,
579}
580
581#[derive(Debug, Clone, Serialize, Deserialize)]
583pub struct LateralOptimizerConfig {
584 pub cache_threshold: usize,
587 pub batch_threshold: usize,
589 pub decorrelate_min_improvement: f64,
591}
592
593impl Default for LateralOptimizerConfig {
594 fn default() -> Self {
595 Self {
596 cache_threshold: 1000,
597 batch_threshold: 500,
598 decorrelate_min_improvement: 0.3,
599 }
600 }
601}
602
603#[derive(Debug, Clone, Serialize, Deserialize)]
605pub struct LateralCostEstimate {
606 pub strategy: LateralStrategy,
608 pub estimated_cost: f64,
610 pub estimated_evaluations: u64,
612 pub cacheable: bool,
614 pub decorrelatable: bool,
616}
617
618impl LateralOptimizer {
619 pub fn new() -> Self {
621 Self::default()
622 }
623
624 pub fn with_config(config: LateralOptimizerConfig) -> Self {
626 Self { config }
627 }
628
629 pub fn choose_strategy(
631 &self,
632 left_cardinality: u64,
633 distinct_keys: u64,
634 subquery: &LateralSubquery,
635 ) -> LateralCostEstimate {
636 let mut candidates = Vec::new();
637
638 let nl_cost = left_cardinality as f64 * self.estimate_subquery_cost(subquery);
640 candidates.push(LateralCostEstimate {
641 strategy: LateralStrategy::NestedLoop,
642 estimated_cost: nl_cost,
643 estimated_evaluations: left_cardinality,
644 cacheable: false,
645 decorrelatable: false,
646 });
647
648 let cache_cost = distinct_keys as f64 * self.estimate_subquery_cost(subquery)
650 + (left_cardinality.saturating_sub(distinct_keys)) as f64 * 0.01;
651 candidates.push(LateralCostEstimate {
652 strategy: LateralStrategy::CachedCorrelation,
653 estimated_cost: cache_cost,
654 estimated_evaluations: distinct_keys,
655 cacheable: distinct_keys < self.config.cache_threshold as u64,
656 decorrelatable: false,
657 });
658
659 let batch_size = self.config.batch_threshold.max(1) as f64;
663 let batch_evals = (left_cardinality as f64 / batch_size).ceil();
664 let per_row_correlation_cost = left_cardinality as f64 * 0.5;
668 let batch_cost =
669 batch_evals * self.estimate_subquery_cost(subquery) + per_row_correlation_cost;
670 candidates.push(LateralCostEstimate {
671 strategy: LateralStrategy::BatchedValues,
672 estimated_cost: batch_cost,
673 estimated_evaluations: batch_evals as u64,
674 cacheable: false,
675 decorrelatable: false,
676 });
677
678 if self.can_decorrelate(subquery) {
680 let decorrelate_cost = left_cardinality as f64 * 0.5; candidates.push(LateralCostEstimate {
682 strategy: LateralStrategy::Decorrelate,
683 estimated_cost: decorrelate_cost,
684 estimated_evaluations: 1,
685 cacheable: false,
686 decorrelatable: true,
687 });
688 }
689
690 candidates.sort_by(|a, b| {
692 a.estimated_cost
693 .partial_cmp(&b.estimated_cost)
694 .unwrap_or(std::cmp::Ordering::Equal)
695 });
696
697 candidates
698 .into_iter()
699 .next()
700 .expect("at least one candidate strategy")
701 }
702
703 fn estimate_subquery_cost(&self, subquery: &LateralSubquery) -> f64 {
705 let mut cost = 1.0;
706 if subquery.has_aggregates {
707 cost *= 2.0;
708 }
709 if let Some(limit) = subquery.limit {
710 cost *= (limit as f64).min(100.0) / 100.0;
711 }
712 if !subquery.order_by.is_empty() {
713 cost *= 1.5;
714 }
715 cost
716 }
717
718 fn can_decorrelate(&self, subquery: &LateralSubquery) -> bool {
725 subquery.correlated_vars.len() == 1 && subquery.has_aggregates
726 }
727
728 pub fn analyze(
730 &self,
731 left_cardinality: u64,
732 distinct_keys: u64,
733 subquery: &LateralSubquery,
734 ) -> Vec<LateralCostEstimate> {
735 let mut estimates = vec![
736 LateralCostEstimate {
737 strategy: LateralStrategy::NestedLoop,
738 estimated_cost: left_cardinality as f64 * self.estimate_subquery_cost(subquery),
739 estimated_evaluations: left_cardinality,
740 cacheable: false,
741 decorrelatable: false,
742 },
743 LateralCostEstimate {
744 strategy: LateralStrategy::CachedCorrelation,
745 estimated_cost: distinct_keys as f64 * self.estimate_subquery_cost(subquery)
746 + (left_cardinality.saturating_sub(distinct_keys)) as f64 * 0.01,
747 estimated_evaluations: distinct_keys,
748 cacheable: distinct_keys < self.config.cache_threshold as u64,
749 decorrelatable: false,
750 },
751 {
752 let batch_size = self.config.batch_threshold.max(1) as f64;
753 let batch_evals = (left_cardinality as f64 / batch_size).ceil();
754 let per_row_correlation_cost = left_cardinality as f64 * 0.5;
755 LateralCostEstimate {
756 strategy: LateralStrategy::BatchedValues,
757 estimated_cost: batch_evals * self.estimate_subquery_cost(subquery)
758 + per_row_correlation_cost,
759 estimated_evaluations: batch_evals as u64,
760 cacheable: false,
761 decorrelatable: false,
762 }
763 },
764 ];
765
766 if self.can_decorrelate(subquery) {
767 estimates.push(LateralCostEstimate {
768 strategy: LateralStrategy::Decorrelate,
769 estimated_cost: left_cardinality as f64 * 0.5,
770 estimated_evaluations: 1,
771 cacheable: false,
772 decorrelatable: true,
773 });
774 }
775
776 estimates.sort_by(|a, b| {
777 a.estimated_cost
778 .partial_cmp(&b.estimated_cost)
779 .unwrap_or(std::cmp::Ordering::Equal)
780 });
781 estimates
782 }
783}
784
785pub struct LateralValidator;
791
792#[derive(Debug, Clone, Serialize, Deserialize)]
794pub struct LateralValidationResult {
795 pub is_valid: bool,
797 pub errors: Vec<LateralValidationError>,
799 pub warnings: Vec<String>,
801 pub detected_correlated_vars: Vec<String>,
803 pub output_vars: Vec<String>,
805}
806
807#[derive(Debug, Clone, Serialize, Deserialize)]
809pub struct LateralValidationError {
810 pub message: String,
812 pub code: LateralErrorCode,
814}
815
816#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
818pub enum LateralErrorCode {
819 NoCorrelation,
821 UnboundCorrelatedVar,
823 ExcessiveNesting,
825 VariableConflict,
827 DisallowedConstruct,
829}
830
831impl LateralValidator {
832 pub fn validate(
834 subquery: &LateralSubquery,
835 left_vars: &[String],
836 nesting_depth: usize,
837 max_depth: usize,
838 ) -> LateralValidationResult {
839 let mut result = LateralValidationResult {
840 is_valid: true,
841 errors: Vec::new(),
842 warnings: Vec::new(),
843 detected_correlated_vars: Vec::new(),
844 output_vars: Vec::new(),
845 };
846
847 if nesting_depth > max_depth {
849 result.is_valid = false;
850 result.errors.push(LateralValidationError {
851 message: format!(
852 "LATERAL nesting depth {nesting_depth} exceeds maximum {max_depth}"
853 ),
854 code: LateralErrorCode::ExcessiveNesting,
855 });
856 }
857
858 let left_set: HashSet<&str> = left_vars.iter().map(|s| s.as_str()).collect();
859
860 for var in &subquery.correlated_vars {
862 if left_set.contains(var.as_str()) {
863 result.detected_correlated_vars.push(var.clone());
864 } else {
865 result.is_valid = false;
866 result.errors.push(LateralValidationError {
867 message: format!("Correlated variable ?{var} is not bound by the left operand"),
868 code: LateralErrorCode::UnboundCorrelatedVar,
869 });
870 }
871 }
872
873 if subquery.correlated_vars.is_empty() {
875 result.warnings.push(
876 "LATERAL subquery has no correlated variables; consider using a regular join"
877 .to_string(),
878 );
879 }
880
881 for proj_var in &subquery.projected_vars {
883 if left_set.contains(proj_var.as_str()) && !subquery.correlated_vars.contains(proj_var)
884 {
885 result.errors.push(LateralValidationError {
886 message: format!(
887 "Projected variable ?{proj_var} conflicts with left operand binding"
888 ),
889 code: LateralErrorCode::VariableConflict,
890 });
891 result.warnings.push(format!(
893 "Variable ?{proj_var} will be overridden by LATERAL subquery"
894 ));
895 }
896 }
897
898 let mut output = HashSet::new();
900 for var in left_vars {
901 output.insert(var.clone());
902 }
903 for var in &subquery.projected_vars {
904 output.insert(var.clone());
905 }
906 result.output_vars = output.into_iter().collect();
907 result.output_vars.sort();
908
909 result
910 }
911}
912
913#[derive(Debug, Clone, Serialize, Deserialize)]
919pub enum LateralJoinError {
920 SubqueryError(String),
922 Timeout {
924 description: String,
926 elapsed_ms: u64,
928 },
929 IncompatibleBindings {
931 variable: String,
933 left_value: String,
935 right_value: String,
937 },
938 NestingDepthExceeded {
940 depth: usize,
942 max: usize,
944 },
945}
946
947impl fmt::Display for LateralJoinError {
948 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
949 match self {
950 Self::SubqueryError(msg) => write!(f, "Lateral subquery error: {msg}"),
951 Self::Timeout {
952 description,
953 elapsed_ms,
954 } => {
955 write!(
956 f,
957 "Lateral subquery timed out after {elapsed_ms}ms: {description}"
958 )
959 }
960 Self::IncompatibleBindings {
961 variable,
962 left_value,
963 right_value,
964 } => {
965 write!(
966 f,
967 "Incompatible bindings for ?{variable}: left={left_value}, right={right_value}"
968 )
969 }
970 Self::NestingDepthExceeded { depth, max } => {
971 write!(
972 f,
973 "LATERAL nesting depth {depth} exceeds maximum allowed {max}"
974 )
975 }
976 }
977 }
978}
979
980impl std::error::Error for LateralJoinError {}
981
982pub struct LateralParser;
988
989#[derive(Debug, Clone, Serialize, Deserialize)]
991pub struct ParsedLateral {
992 pub outer_vars: Vec<String>,
994 pub correlated_vars: Vec<String>,
996 pub projected_vars: Vec<String>,
998 pub has_aggregates: bool,
1000 pub has_order_by: bool,
1002 pub has_limit: bool,
1004 pub subquery_text: String,
1006}
1007
1008impl LateralParser {
1009 pub fn detect_lateral_clauses(query: &str) -> Vec<LateralClausePosition> {
1013 let mut positions = Vec::new();
1014 let upper = query.to_uppercase();
1015 let mut search_from = 0;
1016
1017 while let Some(idx) = upper[search_from..].find("LATERAL") {
1018 let abs_idx = search_from + idx;
1019 let before_ok = abs_idx == 0 || !query.as_bytes()[abs_idx - 1].is_ascii_alphanumeric();
1021 let after_idx = abs_idx + 7;
1022 let after_ok =
1023 after_idx >= query.len() || !query.as_bytes()[after_idx].is_ascii_alphanumeric();
1024
1025 if before_ok && after_ok {
1026 if let Some(brace_start) = query[after_idx..].find('{') {
1028 let open = after_idx + brace_start;
1029 if let Some(close) = Self::find_matching_brace(query, open) {
1030 let body = &query[open + 1..close];
1031 positions.push(LateralClausePosition {
1032 start: abs_idx,
1033 end: close + 1,
1034 body: body.trim().to_string(),
1035 has_select: body.to_uppercase().contains("SELECT"),
1036 });
1037 }
1038 }
1039 }
1040 search_from = abs_idx + 7;
1041 }
1042
1043 positions
1044 }
1045
1046 fn find_matching_brace(s: &str, pos: usize) -> Option<usize> {
1048 let bytes = s.as_bytes();
1049 if pos >= bytes.len() || bytes[pos] != b'{' {
1050 return None;
1051 }
1052 let mut depth = 0i32;
1053 for (i, &b) in bytes[pos..].iter().enumerate() {
1054 match b {
1055 b'{' => depth += 1,
1056 b'}' => {
1057 depth -= 1;
1058 if depth == 0 {
1059 return Some(pos + i);
1060 }
1061 }
1062 _ => {}
1063 }
1064 }
1065 None
1066 }
1067
1068 pub fn extract_variables(fragment: &str) -> Vec<String> {
1070 let mut vars = HashSet::new();
1071 let bytes = fragment.as_bytes();
1072 let mut i = 0;
1073 while i < bytes.len() {
1074 if bytes[i] == b'?' || bytes[i] == b'$' {
1075 let start = i + 1;
1076 i += 1;
1077 while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
1078 i += 1;
1079 }
1080 if i > start {
1081 let var = String::from_utf8_lossy(&bytes[start..i]).to_string();
1082 vars.insert(var);
1083 }
1084 } else {
1085 i += 1;
1086 }
1087 }
1088 let mut result: Vec<_> = vars.into_iter().collect();
1089 result.sort();
1090 result
1091 }
1092
1093 pub fn detect_aggregates(fragment: &str) -> bool {
1095 let upper = fragment.to_uppercase();
1096 [
1097 "COUNT(",
1098 "SUM(",
1099 "AVG(",
1100 "MIN(",
1101 "MAX(",
1102 "GROUP_CONCAT(",
1103 "SAMPLE(",
1104 ]
1105 .iter()
1106 .any(|agg| upper.contains(agg))
1107 }
1108
1109 pub fn detect_order_by(fragment: &str) -> bool {
1111 fragment.to_uppercase().contains("ORDER BY")
1112 }
1113
1114 pub fn detect_limit(fragment: &str) -> bool {
1116 fragment.to_uppercase().contains("LIMIT")
1117 }
1118}
1119
1120#[derive(Debug, Clone, Serialize, Deserialize)]
1122pub struct LateralClausePosition {
1123 pub start: usize,
1125 pub end: usize,
1127 pub body: String,
1129 pub has_select: bool,
1131}
1132
1133#[cfg(test)]
1138mod lateral_join_tests;