Skip to main content

altium_format/query/
engine.rs

1//! Query execution engine for selector-based record queries.
2//!
3//! The `SelectorEngine` executes parsed selectors against record collections,
4//! supporting all selector features including property filters, pseudo-selectors,
5//! and parent/child combinators.
6
7use std::collections::HashMap;
8
9use crate::error::Result;
10use crate::records::sch::{PinElectricalType, SchRecord};
11use crate::tree::{RecordId, RecordTree};
12
13use super::common::{FilterValue as CommonFilterValue, compare_filter};
14use super::pattern::Pattern;
15use super::selector::{
16    Combinator, FilterOperator, FilterValue, NetConnectedTarget, PropertyFilter, PseudoSelector,
17    RecordMatcher, RecordType, Selector, SelectorChain, SelectorSegment,
18};
19
20/// Result of a query: record ID with optional metadata.
21#[derive(Debug, Clone)]
22pub struct QueryMatch {
23    /// The ID of the matched record.
24    pub id: RecordId,
25    /// Depth in the tree (0 for roots).
26    pub depth: usize,
27}
28
29impl QueryMatch {
30    /// Create a new query match.
31    pub fn new(id: RecordId, depth: usize) -> Self {
32        Self { id, depth }
33    }
34}
35
36/// Query execution engine for schematic records.
37///
38/// The engine operates on a `RecordTree<SchRecord>` and provides methods to
39/// execute selector queries against the tree.
40pub struct SelectorEngine<'a> {
41    tree: &'a RecordTree<SchRecord>,
42    /// Cache of component designators: component_id -> designator text
43    designator_cache: HashMap<RecordId, String>,
44    /// Cache of component values: component_id -> value text
45    value_cache: HashMap<RecordId, String>,
46    /// Cache of component part numbers: component_id -> lib_reference
47    part_number_cache: HashMap<RecordId, String>,
48    /// Cache of sheet names: sheet_header_id -> title text
49    sheet_name_cache: HashMap<RecordId, String>,
50}
51
52impl<'a> SelectorEngine<'a> {
53    /// Create a new selector engine for the given record tree.
54    pub fn new(tree: &'a RecordTree<SchRecord>) -> Self {
55        let mut engine = Self {
56            tree,
57            designator_cache: HashMap::new(),
58            value_cache: HashMap::new(),
59            part_number_cache: HashMap::new(),
60            sheet_name_cache: HashMap::new(),
61        };
62        engine.build_caches();
63        engine
64    }
65
66    /// Build lookup caches for efficient querying.
67    fn build_caches(&mut self) {
68        for (id, record) in self.tree.iter() {
69            match record {
70                SchRecord::Component(comp) => {
71                    // Cache part number (lib_reference)
72                    if !comp.lib_reference.is_empty() {
73                        self.part_number_cache
74                            .insert(id, comp.lib_reference.clone());
75                    }
76                }
77                SchRecord::Designator(des) => {
78                    // Cache designator text -> parent component
79                    if let Some(parent_id) = self.tree.parent_id(id) {
80                        self.designator_cache
81                            .insert(parent_id, des.text().to_string());
82                    }
83                }
84                SchRecord::Parameter(param) => {
85                    // Cache "Value" parameter -> parent component
86                    if param.name.eq_ignore_ascii_case("value") {
87                        if let Some(parent_id) = self.tree.parent_id(id) {
88                            self.value_cache
89                                .insert(parent_id, param.value().to_string());
90                        }
91                    }
92                }
93                _ => {}
94            }
95        }
96    }
97
98    /// Set the sheet name for a specific sheet header.
99    /// This should be called with the document name (filename without extension),
100    /// not the Title parameter which is typically the project title.
101    pub fn set_sheet_name(&mut self, sheet_id: RecordId, name: String) {
102        self.sheet_name_cache.insert(sheet_id, name);
103    }
104
105    /// Set the sheet name for all sheet headers in the tree.
106    /// Convenience method for single-sheet documents where the document name
107    /// applies to the single SheetHeader record.
108    pub fn set_document_name(&mut self, name: String) {
109        for (id, record) in self.tree.iter() {
110            if matches!(record, SchRecord::SheetHeader(_)) {
111                self.sheet_name_cache.insert(id, name.clone());
112            }
113        }
114    }
115
116    /// Execute a selector query and return matching record IDs.
117    pub fn query(&self, selector: &Selector) -> Vec<QueryMatch> {
118        if selector.is_empty() {
119            return vec![];
120        }
121
122        let mut results = Vec::new();
123
124        // Union of all alternatives (OR)
125        for chain in &selector.alternatives {
126            let chain_results = self.execute_chain(chain);
127            for result in chain_results {
128                // Avoid duplicates
129                if !results.iter().any(|r: &QueryMatch| r.id == result.id) {
130                    results.push(result);
131                }
132            }
133        }
134
135        results
136    }
137
138    /// Execute a single selector chain.
139    fn execute_chain(&self, chain: &SelectorChain) -> Vec<QueryMatch> {
140        if chain.segments.is_empty() {
141            return vec![];
142        }
143
144        // Start with the first segment - find all matching records
145        let mut current_matches: Vec<RecordId> = self.find_matching_records(&chain.segments[0]);
146
147        // Process remaining segments with combinators
148        for segment in chain.segments.iter().skip(1) {
149            let combinator = chain.segments[chain
150                .segments
151                .iter()
152                .position(|s| std::ptr::eq(s, segment))
153                .map(|i| i.saturating_sub(1))
154                .unwrap_or(0)]
155            .combinator
156            .unwrap_or(Combinator::Descendant);
157
158            current_matches = self.apply_combinator(current_matches, segment, combinator);
159
160            if current_matches.is_empty() {
161                break;
162            }
163        }
164
165        current_matches
166            .into_iter()
167            .map(|id| QueryMatch::new(id, self.tree.depth(id)))
168            .collect()
169    }
170
171    /// Find all records matching a segment (without considering combinators).
172    fn find_matching_records(&self, segment: &SelectorSegment) -> Vec<RecordId> {
173        self.tree
174            .iter()
175            .filter(|(id, record)| self.matches_segment(*id, record, segment))
176            .map(|(id, _)| id)
177            .collect()
178    }
179
180    /// Apply a combinator to narrow down matches.
181    fn apply_combinator(
182        &self,
183        from_ids: Vec<RecordId>,
184        segment: &SelectorSegment,
185        combinator: Combinator,
186    ) -> Vec<RecordId> {
187        let mut results = Vec::new();
188
189        for from_id in from_ids {
190            match combinator {
191                Combinator::DirectChild => {
192                    // Only match direct children
193                    for (child_id, child_record) in self.tree.children(from_id) {
194                        if self.matches_segment(child_id, child_record, segment) {
195                            results.push(child_id);
196                        }
197                    }
198                }
199                Combinator::Descendant => {
200                    // Match any descendant
201                    for (desc_id, desc_record) in self.tree.descendants(from_id) {
202                        if self.matches_segment(desc_id, desc_record, segment) {
203                            results.push(desc_id);
204                        }
205                    }
206                }
207            }
208        }
209
210        results
211    }
212
213    /// Check if a record matches a selector segment.
214    fn matches_segment(&self, id: RecordId, record: &SchRecord, segment: &SelectorSegment) -> bool {
215        // Check main matcher
216        if !self.matches_record_matcher(id, record, &segment.matcher) {
217            return false;
218        }
219
220        // Check property filters
221        for filter in &segment.filters {
222            if !self.matches_property_filter(id, record, filter) {
223                return false;
224            }
225        }
226
227        // Check pseudo-selectors
228        for pseudo in &segment.pseudo {
229            if !self.matches_pseudo_selector(id, record, pseudo) {
230                return false;
231            }
232        }
233
234        true
235    }
236
237    /// Check if a record matches a record matcher.
238    fn matches_record_matcher(
239        &self,
240        id: RecordId,
241        record: &SchRecord,
242        matcher: &RecordMatcher,
243    ) -> bool {
244        match matcher {
245            RecordMatcher::Any => true,
246
247            RecordMatcher::Type(record_type) => self.record_has_type(record, *record_type),
248
249            RecordMatcher::Designator(pattern) => {
250                // Match component by its designator
251                if let SchRecord::Component(_) = record {
252                    if let Some(designator) = self.designator_cache.get(&id) {
253                        return pattern.matches(designator);
254                    }
255                }
256                false
257            }
258
259            RecordMatcher::PartNumber(pattern) => {
260                // Match component by lib_reference
261                if let SchRecord::Component(comp) = record {
262                    return pattern.matches(&comp.lib_reference);
263                }
264                false
265            }
266
267            RecordMatcher::Net(pattern) => {
268                // Match net labels and power objects by net name
269                match record {
270                    SchRecord::NetLabel(nl) => pattern.matches(&nl.label.text),
271                    SchRecord::PowerObject(po) => pattern.matches(&po.text),
272                    _ => false,
273                }
274            }
275
276            RecordMatcher::Value(pattern) => {
277                // Match component by its Value parameter
278                if let SchRecord::Component(_) = record {
279                    if let Some(value) = self.value_cache.get(&id) {
280                        return pattern.matches(value);
281                    }
282                }
283                false
284            }
285
286            RecordMatcher::Sheet(pattern) => {
287                // Match sheet header by name/title
288                if let SchRecord::SheetHeader(_header) = record {
289                    // Check if we have a cached title for this sheet
290                    if let Some(title) = self.sheet_name_cache.get(&id) {
291                        return pattern.matches(title);
292                    }
293                    // If no title found, match against empty string (matches wildcard "*")
294                    return pattern.matches("");
295                }
296                false
297            }
298
299            RecordMatcher::Pin { component, pin } => {
300                // Match pins by component designator and pin designator/name
301                if let SchRecord::Pin(pin_record) = record {
302                    // Get parent component
303                    if let Some(parent_id) = self.tree.parent_id(id) {
304                        // First check if parent is a component
305                        if let Some(SchRecord::Component(_)) = self.tree.get(parent_id) {
306                            // Check component designator
307                            let comp_matches =
308                                if let Some(designator) = self.designator_cache.get(&parent_id) {
309                                    component.matches(designator)
310                                } else {
311                                    // No designator, only match if component pattern is wildcard
312                                    component.as_str() == "*"
313                                };
314
315                            if comp_matches {
316                                // Check pin designator or name
317                                return pin.matches(&pin_record.designator)
318                                    || pin.matches(&pin_record.name);
319                            }
320                        }
321                    }
322                }
323                false
324            }
325
326            RecordMatcher::NetConnected { net, target } => {
327                // This is a complex query that requires net connectivity analysis
328                // For now, we return records connected to the named net
329                self.matches_net_connected(id, record, net, *target)
330            }
331
332            RecordMatcher::DesignatorWithValue { designator, value } => {
333                // Match component by both designator and value
334                if let SchRecord::Component(_) = record {
335                    let des_matches = self
336                        .designator_cache
337                        .get(&id)
338                        .map(|d| designator.matches(d))
339                        .unwrap_or(false);
340
341                    let val_matches = self
342                        .value_cache
343                        .get(&id)
344                        .map(|v| value.matches(v))
345                        .unwrap_or(false);
346
347                    return des_matches && val_matches;
348                }
349                false
350            }
351        }
352    }
353
354    /// Check if a record has the specified type.
355    fn record_has_type(&self, record: &SchRecord, record_type: RecordType) -> bool {
356        matches!(
357            (record, record_type),
358            (SchRecord::Component(_), RecordType::Component)
359                | (SchRecord::Pin(_), RecordType::Pin)
360                | (SchRecord::Wire(_), RecordType::Wire)
361                | (SchRecord::NetLabel(_), RecordType::NetLabel)
362                | (SchRecord::Port(_), RecordType::Port)
363                | (SchRecord::PowerObject(_), RecordType::PowerObject)
364                | (SchRecord::Junction(_), RecordType::Junction)
365                | (SchRecord::Label(_), RecordType::Label)
366                | (SchRecord::Rectangle(_), RecordType::Rectangle)
367                | (SchRecord::Line(_), RecordType::Line)
368                | (SchRecord::Arc(_), RecordType::Arc)
369                | (SchRecord::Ellipse(_), RecordType::Ellipse)
370                | (SchRecord::Polygon(_), RecordType::Polygon)
371                | (SchRecord::Polyline(_), RecordType::Polyline)
372                | (SchRecord::Bezier(_), RecordType::Bezier)
373                | (SchRecord::Image(_), RecordType::Image)
374                | (SchRecord::Parameter(_), RecordType::Parameter)
375                | (SchRecord::SheetHeader(_), RecordType::Sheet)
376                | (SchRecord::Symbol(_), RecordType::Symbol)
377                | (SchRecord::Designator(_), RecordType::Designator)
378                | (SchRecord::TextFrame(_), RecordType::TextFrame)
379                | (SchRecord::TextFrameVariant(_), RecordType::TextFrame)
380        )
381    }
382
383    /// Check if a record matches a net connectivity query.
384    fn matches_net_connected(
385        &self,
386        id: RecordId,
387        record: &SchRecord,
388        net_pattern: &Pattern,
389        target: NetConnectedTarget,
390    ) -> bool {
391        // Find all net labels/power objects matching the pattern
392        let net_names: Vec<String> = self
393            .tree
394            .iter()
395            .filter_map(|(_, r)| match r {
396                SchRecord::NetLabel(nl) if net_pattern.matches(&nl.label.text) => {
397                    Some(nl.label.text.clone())
398                }
399                SchRecord::PowerObject(po) if net_pattern.matches(&po.text) => {
400                    Some(po.text.clone())
401                }
402                _ => None,
403            })
404            .collect();
405
406        if net_names.is_empty() {
407            return false;
408        }
409
410        // Check if current record is connected to one of these nets
411        // This is a simplified implementation - full net connectivity requires
412        // geometric analysis
413        match (target, record) {
414            (NetConnectedTarget::Pins, SchRecord::Pin(pin)) => {
415                // Check if pin has a hidden net name matching
416                net_names
417                    .iter()
418                    .any(|n| n.eq_ignore_ascii_case(&pin.hidden_net_name))
419            }
420            (NetConnectedTarget::Components, SchRecord::Component(_)) => {
421                // Check if any child pins are connected to the net
422                for (_child_id, child) in self.tree.children(id) {
423                    if let SchRecord::Pin(pin) = child {
424                        if net_names
425                            .iter()
426                            .any(|n| n.eq_ignore_ascii_case(&pin.hidden_net_name))
427                        {
428                            return true;
429                        }
430                    }
431                }
432                false
433            }
434            _ => false,
435        }
436    }
437
438    /// Check if a record matches a property filter.
439    fn matches_property_filter(
440        &self,
441        id: RecordId,
442        record: &SchRecord,
443        filter: &PropertyFilter,
444    ) -> bool {
445        // Get the property value from the record
446        let prop_value = self.get_property(id, record, &filter.property);
447
448        match prop_value {
449            Some(value) => self.compare_filter_value(&value, &filter.operator, &filter.value),
450            None => false,
451        }
452    }
453
454    /// Get a property value from a record.
455    fn get_property(&self, id: RecordId, record: &SchRecord, property: &str) -> Option<String> {
456        let prop_lower = property.to_lowercase();
457
458        // Common properties across record types
459        match prop_lower.as_str() {
460            "x" | "location.x" => return self.get_location_x(record),
461            "y" | "location.y" => return self.get_location_y(record),
462            "owner_index" | "ownerindex" => return Some(self.get_owner_index(record).to_string()),
463            "hidden" => return Some(self.is_hidden(record).to_string()),
464            _ => {}
465        }
466
467        // Record-specific properties
468        match record {
469            SchRecord::Component(comp) => match prop_lower.as_str() {
470                "lib_reference" | "libreference" => Some(comp.lib_reference.clone()),
471                "description" | "componentdescription" => Some(comp.component_description.clone()),
472                "unique_id" | "uniqueid" => Some(comp.unique_id.clone()),
473                "part_count" | "partcount" => Some(comp.part_count.to_string()),
474                "designator" => self.designator_cache.get(&id).cloned(),
475                "value" => self.value_cache.get(&id).cloned(),
476                _ => None,
477            },
478            SchRecord::Pin(pin) => match prop_lower.as_str() {
479                "designator" => Some(pin.designator.clone()),
480                "name" => Some(pin.name.clone()),
481                "electrical" => Some(format!("{:?}", pin.electrical)),
482                "length" | "pin_length" | "pinlength" => Some(pin.pin_length.to_string()),
483                "description" => Some(pin.description.clone()),
484                _ => None,
485            },
486            SchRecord::NetLabel(nl) => match prop_lower.as_str() {
487                "text" | "net" | "name" => Some(nl.label.text.clone()),
488                _ => None,
489            },
490            SchRecord::PowerObject(po) => match prop_lower.as_str() {
491                "text" | "net" | "name" => Some(po.text.clone()),
492                "style" => Some(format!("{:?}", po.style)),
493                _ => None,
494            },
495            SchRecord::Parameter(param) => match prop_lower.as_str() {
496                "name" => Some(param.name.clone()),
497                "value" | "text" => Some(param.value().to_string()),
498                _ => None,
499            },
500            SchRecord::Label(label) => match prop_lower.as_str() {
501                "text" => Some(label.text.clone()),
502                _ => None,
503            },
504            SchRecord::Port(port) => match prop_lower.as_str() {
505                "name" => Some(port.name.clone()),
506                "io_type" | "iotype" => Some(format!("{:?}", port.io_type)),
507                "style" => Some(format!("{:?}", port.style)),
508                _ => None,
509            },
510            SchRecord::SheetHeader(_header) => match prop_lower.as_str() {
511                "name" | "title" => self.sheet_name_cache.get(&id).cloned(),
512                _ => None,
513            },
514            _ => None,
515        }
516    }
517
518    /// Get location X from a record.
519    fn get_location_x(&self, record: &SchRecord) -> Option<String> {
520        match record {
521            SchRecord::Component(c) => Some(c.graphical.location_x.to_string()),
522            SchRecord::Pin(p) => Some(p.graphical.location_x.to_string()),
523            SchRecord::Symbol(s) => Some(s.graphical.location_x.to_string()),
524            SchRecord::Label(l) => Some(l.graphical.location_x.to_string()),
525            SchRecord::NetLabel(nl) => Some(nl.label.graphical.location_x.to_string()),
526            SchRecord::PowerObject(po) => Some(po.graphical.location_x.to_string()),
527            SchRecord::Port(p) => Some(p.graphical.location_x.to_string()),
528            SchRecord::Junction(j) => Some(j.graphical.location_x.to_string()),
529            SchRecord::Rectangle(r) => Some(r.graphical.location_x.to_string()),
530            SchRecord::Line(l) => Some(l.graphical.location_x.to_string()),
531            SchRecord::Arc(a) => Some(a.graphical.location_x.to_string()),
532            SchRecord::Ellipse(e) => Some(e.graphical.location_x.to_string()),
533            _ => None,
534        }
535    }
536
537    /// Get location Y from a record.
538    fn get_location_y(&self, record: &SchRecord) -> Option<String> {
539        match record {
540            SchRecord::Component(c) => Some(c.graphical.location_y.to_string()),
541            SchRecord::Pin(p) => Some(p.graphical.location_y.to_string()),
542            SchRecord::Symbol(s) => Some(s.graphical.location_y.to_string()),
543            SchRecord::Label(l) => Some(l.graphical.location_y.to_string()),
544            SchRecord::NetLabel(nl) => Some(nl.label.graphical.location_y.to_string()),
545            SchRecord::PowerObject(po) => Some(po.graphical.location_y.to_string()),
546            SchRecord::Port(p) => Some(p.graphical.location_y.to_string()),
547            SchRecord::Junction(j) => Some(j.graphical.location_y.to_string()),
548            SchRecord::Rectangle(r) => Some(r.graphical.location_y.to_string()),
549            SchRecord::Line(l) => Some(l.graphical.location_y.to_string()),
550            SchRecord::Arc(a) => Some(a.graphical.location_y.to_string()),
551            SchRecord::Ellipse(e) => Some(e.graphical.location_y.to_string()),
552            _ => None,
553        }
554    }
555
556    /// Get owner index from a record.
557    fn get_owner_index(&self, record: &SchRecord) -> i32 {
558        match record {
559            SchRecord::Component(c) => c.graphical.base.owner_index,
560            SchRecord::Pin(p) => p.graphical.base.owner_index,
561            SchRecord::Symbol(s) => s.graphical.base.owner_index,
562            SchRecord::Label(l) => l.graphical.base.owner_index,
563            SchRecord::NetLabel(nl) => nl.label.graphical.base.owner_index,
564            SchRecord::PowerObject(po) => po.graphical.base.owner_index,
565            SchRecord::Port(p) => p.graphical.base.owner_index,
566            SchRecord::Junction(j) => j.graphical.base.owner_index,
567            _ => -1,
568        }
569    }
570
571    /// Check if a record is hidden.
572    fn is_hidden(&self, record: &SchRecord) -> bool {
573        match record {
574            SchRecord::Pin(p) => p.is_hidden(),
575            SchRecord::Label(l) => l.is_hidden,
576            _ => false,
577        }
578    }
579
580    /// Compare a property value against a filter.
581    ///
582    /// Uses the shared comparison logic from common.rs.
583    fn compare_filter_value(
584        &self,
585        actual: &str,
586        operator: &FilterOperator,
587        expected: &FilterValue,
588    ) -> bool {
589        // Convert to common types
590        let filter_op = operator.to_filter_op();
591        let common_value = match expected {
592            FilterValue::String(s) => CommonFilterValue::String(s.clone()),
593            FilterValue::Number(n) => CommonFilterValue::Number(*n),
594            FilterValue::Bool(b) => CommonFilterValue::Bool(*b),
595            FilterValue::Pattern(p) => CommonFilterValue::Pattern(p.clone()),
596        };
597
598        // Special case: Wildcard with Pattern uses pattern matching directly
599        if matches!(operator, FilterOperator::Wildcard) {
600            if let FilterValue::Pattern(p) = expected {
601                return p.matches(actual);
602            }
603            if let FilterValue::String(s) = expected {
604                return Pattern::new(s).map(|p| p.matches(actual)).unwrap_or(false);
605            }
606        }
607
608        compare_filter(Some(actual), filter_op, &common_value, true)
609    }
610
611    /// Check if a record matches a pseudo-selector.
612    fn matches_pseudo_selector(
613        &self,
614        id: RecordId,
615        record: &SchRecord,
616        pseudo: &PseudoSelector,
617    ) -> bool {
618        match pseudo {
619            PseudoSelector::Root => self.tree.is_root(id),
620            PseudoSelector::Empty => !self.tree.has_children(id),
621            PseudoSelector::FirstChild => self.is_first_child(id),
622            PseudoSelector::LastChild => self.is_last_child(id),
623            PseudoSelector::NthChild(n) => self.is_nth_child(id, *n),
624            PseudoSelector::OnlyChild => self.is_only_child(id),
625
626            // Electrical state (for pins)
627            PseudoSelector::Connected => self.is_pin_connected(record),
628            PseudoSelector::Unconnected => !self.is_pin_connected(record),
629            PseudoSelector::Input => self.pin_has_electrical(record, PinElectricalType::Input),
630            PseudoSelector::Output => self.pin_has_electrical(record, PinElectricalType::Output),
631            PseudoSelector::Bidirectional => {
632                self.pin_has_electrical(record, PinElectricalType::InputOutput)
633            }
634            PseudoSelector::Power => self.pin_has_electrical(record, PinElectricalType::Power),
635            PseudoSelector::Passive => self.pin_has_electrical(record, PinElectricalType::Passive),
636            PseudoSelector::OpenCollector => {
637                self.pin_has_electrical(record, PinElectricalType::OpenCollector)
638            }
639            PseudoSelector::OpenEmitter => {
640                self.pin_has_electrical(record, PinElectricalType::OpenEmitter)
641            }
642            PseudoSelector::HiZ => self.pin_has_electrical(record, PinElectricalType::HiZ),
643
644            // Visibility
645            PseudoSelector::Visible => !self.is_hidden(record),
646            PseudoSelector::Hidden => self.is_hidden(record),
647            PseudoSelector::Selected => false, // UI state - not supported in query engine
648
649            // Combinatorial
650            PseudoSelector::Not(selector) => {
651                let matches = self.query(selector);
652                !matches.iter().any(|m| m.id == id)
653            }
654            PseudoSelector::Has(selector) => {
655                // Check if any descendant matches
656                for (desc_id, _) in self.tree.descendants(id) {
657                    let desc_matches = self.query(selector);
658                    if desc_matches.iter().any(|m| m.id == desc_id) {
659                        return true;
660                    }
661                }
662                false
663            }
664            PseudoSelector::Is(selector) => {
665                let matches = self.query(selector);
666                matches.iter().any(|m| m.id == id)
667            }
668        }
669    }
670
671    /// Check if record is the first child of its parent.
672    fn is_first_child(&self, id: RecordId) -> bool {
673        if let Some(parent_id) = self.tree.parent_id(id) {
674            if let Some((first_id, _)) = self.tree.children(parent_id).next() {
675                return first_id == id;
676            }
677        }
678        false
679    }
680
681    /// Check if record is the last child of its parent.
682    fn is_last_child(&self, id: RecordId) -> bool {
683        if let Some(parent_id) = self.tree.parent_id(id) {
684            if let Some((last_id, _)) = self.tree.children(parent_id).last() {
685                return last_id == id;
686            }
687        }
688        false
689    }
690
691    /// Check if record is the nth child (1-indexed).
692    fn is_nth_child(&self, id: RecordId, n: usize) -> bool {
693        if let Some(parent_id) = self.tree.parent_id(id) {
694            if let Some((nth_id, _)) = self.tree.children(parent_id).nth(n.saturating_sub(1)) {
695                return nth_id == id;
696            }
697        }
698        false
699    }
700
701    /// Check if record is the only child of its parent.
702    fn is_only_child(&self, id: RecordId) -> bool {
703        if let Some(parent_id) = self.tree.parent_id(id) {
704            return self.tree.child_count(parent_id) == 1;
705        }
706        false
707    }
708
709    /// Check if a pin is connected to a net.
710    fn is_pin_connected(&self, record: &SchRecord) -> bool {
711        if let SchRecord::Pin(pin) = record {
712            // A pin is considered connected if it has a hidden net name
713            return !pin.hidden_net_name.is_empty();
714        }
715        false
716    }
717
718    /// Check if a pin has a specific electrical type.
719    fn pin_has_electrical(&self, record: &SchRecord, electrical: PinElectricalType) -> bool {
720        if let SchRecord::Pin(pin) = record {
721            return pin.electrical == electrical;
722        }
723        false
724    }
725
726    /// Get the designator of a component by ID.
727    pub fn get_component_designator(&self, id: RecordId) -> Option<&str> {
728        self.designator_cache.get(&id).map(|s| s.as_str())
729    }
730
731    /// Get the value of a component by ID.
732    pub fn get_component_value(&self, id: RecordId) -> Option<&str> {
733        self.value_cache.get(&id).map(|s| s.as_str())
734    }
735
736    /// Get the part number (lib_reference) of a component by ID.
737    pub fn get_component_part_number(&self, id: RecordId) -> Option<&str> {
738        self.part_number_cache.get(&id).map(|s| s.as_str())
739    }
740}
741
742/// Convenience function to parse and execute a selector query.
743pub fn query_records(tree: &RecordTree<SchRecord>, selector_str: &str) -> Result<Vec<QueryMatch>> {
744    query_records_with_doc_name(tree, selector_str, None)
745}
746
747/// Convenience function to parse and execute a selector query with optional document name.
748/// The document name is used as the sheet name for sheet queries (not the Title parameter).
749pub fn query_records_with_doc_name(
750    tree: &RecordTree<SchRecord>,
751    selector_str: &str,
752    document_name: Option<&str>,
753) -> Result<Vec<QueryMatch>> {
754    use super::parser::SelectorParser;
755
756    let parser = SelectorParser::new(selector_str);
757    let selector = parser.parse()?;
758    let mut engine = SelectorEngine::new(tree);
759
760    // Set document name for all sheet headers if provided
761    if let Some(name) = document_name {
762        engine.set_document_name(name.to_string());
763    }
764
765    Ok(engine.query(&selector))
766}
767
768#[cfg(test)]
769mod tests {
770    use super::*;
771    use crate::records::sch::{SchComponent, SchDesignator, SchNetLabel, SchParameter, SchPin};
772
773    // Helper to create a test tree
774    fn create_test_tree() -> RecordTree<SchRecord> {
775        let mut records = Vec::new();
776
777        // Component U1 at index 0
778        let mut comp = SchComponent::default();
779        comp.lib_reference = "LM358".to_string();
780        comp.graphical.base.owner_index = -1;
781        records.push(SchRecord::Component(comp));
782
783        // Designator for U1 at index 1
784        let mut des = SchDesignator::default();
785        des.param.label.text = "U1".to_string();
786        des.param.label.graphical.base.owner_index = 0;
787        records.push(SchRecord::Designator(des));
788
789        // Value parameter at index 2
790        let mut param = SchParameter::default();
791        param.name = "Value".to_string();
792        param.label.text = "OpAmp".to_string();
793        param.label.graphical.base.owner_index = 0;
794        records.push(SchRecord::Parameter(param));
795
796        // Pin 1 at index 3
797        let mut pin1 = SchPin::default();
798        pin1.designator = "1".to_string();
799        pin1.name = "IN+".to_string();
800        pin1.electrical = PinElectricalType::Input;
801        pin1.graphical.base.owner_index = 0;
802        records.push(SchRecord::Pin(pin1));
803
804        // Pin 2 at index 4
805        let mut pin2 = SchPin::default();
806        pin2.designator = "2".to_string();
807        pin2.name = "OUT".to_string();
808        pin2.electrical = PinElectricalType::Output;
809        pin2.graphical.base.owner_index = 0;
810        records.push(SchRecord::Pin(pin2));
811
812        // Component R1 at index 5
813        let mut comp2 = SchComponent::default();
814        comp2.lib_reference = "Resistor".to_string();
815        comp2.graphical.base.owner_index = -1;
816        records.push(SchRecord::Component(comp2));
817
818        // Designator for R1 at index 6
819        let mut des2 = SchDesignator::default();
820        des2.param.label.text = "R1".to_string();
821        des2.param.label.graphical.base.owner_index = 5;
822        records.push(SchRecord::Designator(des2));
823
824        // Value parameter at index 7
825        let mut param2 = SchParameter::default();
826        param2.name = "Value".to_string();
827        param2.label.text = "10K".to_string();
828        param2.label.graphical.base.owner_index = 5;
829        records.push(SchRecord::Parameter(param2));
830
831        // NetLabel at index 8
832        let mut netlabel = SchNetLabel::default();
833        netlabel.label.text = "VCC".to_string();
834        netlabel.label.graphical.base.owner_index = -1;
835        records.push(SchRecord::NetLabel(netlabel));
836
837        RecordTree::from_records(records)
838    }
839
840    #[test]
841    fn test_query_by_designator() {
842        let tree = create_test_tree();
843        let results = query_records(&tree, "U1").unwrap();
844
845        assert_eq!(results.len(), 1);
846        if let Some(SchRecord::Component(comp)) = tree.get(results[0].id) {
847            assert_eq!(comp.lib_reference, "LM358");
848        } else {
849            panic!("Expected component");
850        }
851    }
852
853    #[test]
854    fn test_query_by_designator_pattern() {
855        let tree = create_test_tree();
856        let results = query_records(&tree, "R*").unwrap();
857
858        assert_eq!(results.len(), 1);
859        if let Some(SchRecord::Component(comp)) = tree.get(results[0].id) {
860            assert_eq!(comp.lib_reference, "Resistor");
861        } else {
862            panic!("Expected component");
863        }
864    }
865
866    #[test]
867    fn test_query_by_part_number() {
868        let tree = create_test_tree();
869        let results = query_records(&tree, "$LM358").unwrap();
870
871        assert_eq!(results.len(), 1);
872    }
873
874    #[test]
875    fn test_query_by_value() {
876        let tree = create_test_tree();
877        let results = query_records(&tree, "@10K").unwrap();
878
879        assert_eq!(results.len(), 1);
880    }
881
882    #[test]
883    fn test_query_by_net() {
884        let tree = create_test_tree();
885        let results = query_records(&tree, "~VCC").unwrap();
886
887        assert_eq!(results.len(), 1);
888        if let Some(SchRecord::NetLabel(nl)) = tree.get(results[0].id) {
889            assert_eq!(nl.label.text, "VCC");
890        } else {
891            panic!("Expected net label");
892        }
893    }
894
895    #[test]
896    fn test_query_pin() {
897        let tree = create_test_tree();
898        let results = query_records(&tree, "U1:1").unwrap();
899
900        assert_eq!(results.len(), 1);
901        if let Some(SchRecord::Pin(pin)) = tree.get(results[0].id) {
902            assert_eq!(pin.designator, "1");
903        } else {
904            panic!("Expected pin");
905        }
906    }
907
908    #[test]
909    fn test_query_pin_by_name() {
910        let tree = create_test_tree();
911        let results = query_records(&tree, "U1:OUT").unwrap();
912
913        assert_eq!(results.len(), 1);
914        if let Some(SchRecord::Pin(pin)) = tree.get(results[0].id) {
915            assert_eq!(pin.name, "OUT");
916        } else {
917            panic!("Expected pin");
918        }
919    }
920
921    #[test]
922    fn test_query_by_type() {
923        let tree = create_test_tree();
924        let results = query_records(&tree, "pin").unwrap();
925
926        assert_eq!(results.len(), 2); // Two pins in the test tree
927    }
928
929    #[test]
930    fn test_query_alternatives() {
931        let tree = create_test_tree();
932        let results = query_records(&tree, "U1, R1").unwrap();
933
934        assert_eq!(results.len(), 2); // Both components
935    }
936
937    #[test]
938    fn test_query_with_pseudo_selector() {
939        let tree = create_test_tree();
940        let results = query_records(&tree, "pin:input").unwrap();
941
942        assert_eq!(results.len(), 1);
943        if let Some(SchRecord::Pin(pin)) = tree.get(results[0].id) {
944            assert_eq!(pin.electrical, PinElectricalType::Input);
945        } else {
946            panic!("Expected pin");
947        }
948    }
949
950    #[test]
951    fn test_query_sheet_by_name() {
952        use crate::records::sch::SchSheetHeader;
953
954        let mut records = Vec::new();
955
956        // Create a sheet header at index 0
957        let mut sheet = SchSheetHeader::default();
958        sheet.base.owner_index = -1;
959        records.push(SchRecord::SheetHeader(sheet));
960
961        let tree = RecordTree::from_records(records);
962
963        // Test 1: Match by exact name with property filter
964        let results =
965            query_records_with_doc_name(&tree, "sheet[name='PowerSupply']", Some("PowerSupply"))
966                .unwrap();
967        assert_eq!(results.len(), 1);
968
969        // Test 2: Match by wildcard pattern with property filter
970        let results =
971            query_records_with_doc_name(&tree, "sheet[name~='Power*']", Some("PowerSupply"))
972                .unwrap();
973        assert_eq!(results.len(), 1);
974
975        // Test 3: No match with wrong name
976        let results =
977            query_records_with_doc_name(&tree, "sheet[name='Other']", Some("PowerSupply")).unwrap();
978        assert_eq!(results.len(), 0);
979
980        // Test 4: Match all sheets with type selector
981        let results = query_records(&tree, "sheet").unwrap();
982        assert_eq!(results.len(), 1);
983
984        // Test 5: Match by exact name with # shortcut (SheetName matcher)
985        let results =
986            query_records_with_doc_name(&tree, "#PowerSupply", Some("PowerSupply")).unwrap();
987        assert_eq!(results.len(), 1);
988
989        // Test 6: Match by wildcard with # shortcut
990        let results = query_records_with_doc_name(&tree, "#Power*", Some("PowerSupply")).unwrap();
991        assert_eq!(results.len(), 1);
992
993        // Test 7: No match with # shortcut
994        let results = query_records_with_doc_name(&tree, "#Other", Some("PowerSupply")).unwrap();
995        assert_eq!(results.len(), 0);
996
997        // Test 8: Without document name set, sheet has no name (empty string)
998        let results = query_records(&tree, "sheet[name='PowerSupply']").unwrap();
999        assert_eq!(
1000            results.len(),
1001            0,
1002            "Should not match when document name not set"
1003        );
1004
1005        // Test 9: Wildcard "*" matches sheets even without name
1006        let results = query_records(&tree, "#*").unwrap();
1007        assert_eq!(
1008            results.len(),
1009            1,
1010            "Wildcard should match sheets without name"
1011        );
1012    }
1013}