Skip to main content

altium_format/query/
mod.rs

1//! Query systems for schematic records and documents.
2//!
3//! This module provides two complementary query systems:
4//!
5//! ## 1. Record Selector System (Low-Level)
6//!
7//! Domain-specific selector syntax for querying raw schematic records in a `RecordTree`.
8//! Optimized for engineers working directly with component designators and part numbers.
9//!
10//! ### Syntax Overview
11//!
12//! | Pattern | Meaning | Example |
13//! |---------|---------|---------|
14//! | `U1` | Component by designator | Match component U1 |
15//! | `R*` | Designator pattern | Match R1, R2, R100, etc. |
16//! | `$LM358` | Component by part number | Match by lib_reference |
17//! | `~VCC` | Net by name | Match net labels/power objects |
18//! | `@10K` | Component by value | Match by Value parameter |
19//! | `#Power` | Sheet by name | Match sheet (reserved) |
20//! | `U1:3` | Pin by number | Pin 3 of component U1 |
21//! | `U1:VCC` | Pin by name | VCC pin of component U1 |
22//! | `R*@10K` | Combined query | 10K resistors |
23//!
24//! ```ignore
25//! use altium_format::query::{query_records, SelectorParser, SelectorEngine};
26//! let results = query_records(&tree, "R*@10K").unwrap();
27//! ```
28//!
29//! ## 2. SchQL System (High-Level)
30//!
31//! CSS-style query language for querying schematic documents with computed connectivity.
32//! Provides a `SchematicView` abstraction with nets, connections, and relationship queries.
33//!
34//! ### Syntax Overview
35//!
36//! | Pattern | Meaning |
37//! |---------|---------|
38//! | `component` | All components |
39//! | `#U1` | Component with ID "U1" |
40//! | `pin[type=input]` | Input pins |
41//! | `net:power` | Power nets |
42//! | `#U1 > pin` | Direct children (pins of U1) |
43//! | `#U1 >> component` | Electrically connected components |
44//! | `#VCC :: pin` | Pins on VCC net |
45//! | `component:count` | Count of components |
46//!
47//! ```ignore
48//! use altium_format::query::{SchematicView, SchematicQuery};
49//! let view = SchematicView::from_schdoc(&doc);
50//! let query = SchematicQuery::new(&view);
51//! let result = query.query("component[part*=7805] > pin[type=input]").unwrap();
52//! ```
53
54// =============================================================================
55// Shared Types (used by both systems)
56// =============================================================================
57
58mod common;
59
60pub use common::{
61    ElectricalFilter, ElectricalType, FilterOp, FilterValue as CommonFilterValue, VisibilityFilter,
62    compare_filter,
63};
64
65// =============================================================================
66// Record Selector System (raw RecordTree queries)
67// =============================================================================
68
69mod engine;
70mod parser;
71mod pattern;
72mod selector;
73
74pub use engine::{
75    QueryMatch as RecordQueryMatch, SelectorEngine, query_records, query_records_with_doc_name,
76};
77pub use parser::{SelectorParser, parse as parse_selector};
78pub use pattern::Pattern;
79pub use selector::{
80    Combinator, FilterOperator, FilterValue, NetConnectedTarget, PropertyFilter,
81    PseudoSelector as RecordPseudoSelector, RecordMatcher, RecordType, Selector as RecordSelector,
82    SelectorChain, SelectorSegment,
83};
84
85// =============================================================================
86// SchQL System (high-level SchematicView queries)
87// =============================================================================
88
89mod ast;
90mod executor;
91mod schql_parser;
92mod view;
93
94pub use ast::*;
95pub use executor::QueryExecutor;
96pub use schql_parser::{QueryError, QueryParser};
97pub use view::{
98    ComponentView, ConnectionPoint, NetView, PinView, PortView, PowerView, SchematicView,
99};
100
101use crate::io::schdoc::SchDoc;
102
103/// Query result containing matched elements from SchQL
104#[derive(Debug, Clone)]
105pub struct QueryResult {
106    /// Matched elements
107    pub matches: Vec<QueryMatch>,
108    /// Original query string
109    pub query: String,
110    /// Execution time in microseconds
111    pub execution_time_us: u64,
112}
113
114impl QueryResult {
115    /// Check if query returned any results
116    pub fn is_empty(&self) -> bool {
117        self.matches.is_empty()
118    }
119
120    /// Get number of matches
121    pub fn len(&self) -> usize {
122        self.matches.len()
123    }
124
125    /// Render as concise list
126    pub fn to_text(&self) -> String {
127        if self.matches.is_empty() {
128            return format!("Query `{}`: No matches\n", self.query);
129        }
130
131        // Check for count result
132        if let Some(QueryMatch::Count(n)) = self.matches.first() {
133            return format!("{}\n", n);
134        }
135
136        let mut output = format!(
137            "Query `{}`: {} match{}\n",
138            self.query,
139            self.matches.len(),
140            if self.matches.len() == 1 { "" } else { "es" }
141        );
142        for m in &self.matches {
143            output.push_str(&format!("  {}\n", m.to_short_text()));
144        }
145        output
146    }
147
148    /// Render with full details
149    pub fn to_detail_text(&self) -> String {
150        if self.matches.is_empty() {
151            return format!("Query `{}`: No matches\n", self.query);
152        }
153
154        let mut output = format!(
155            "Query `{}`: {} match{}\n\n",
156            self.query,
157            self.matches.len(),
158            if self.matches.len() == 1 { "" } else { "es" }
159        );
160        for m in &self.matches {
161            output.push_str(&m.to_detail_text());
162            output.push('\n');
163        }
164        output
165    }
166}
167
168/// A matched element from a SchQL query
169#[derive(Debug, Clone)]
170pub enum QueryMatch {
171    /// Component match
172    Component {
173        designator: String,
174        part: String,
175        description: String,
176        value: Option<String>,
177        footprint: Option<String>,
178        pin_count: usize,
179    },
180
181    /// Pin match
182    Pin {
183        component_designator: String,
184        designator: String,
185        name: String,
186        electrical_type: String,
187        connected_net: Option<String>,
188        is_hidden: bool,
189    },
190
191    /// Net match
192    Net {
193        name: String,
194        is_power: bool,
195        is_ground: bool,
196        connection_count: usize,
197        connections: Vec<String>,
198    },
199
200    /// Port match
201    Port {
202        name: String,
203        io_type: String,
204        connected_net: Option<String>,
205    },
206
207    /// Wire match
208    Wire {
209        index: usize,
210        vertex_count: usize,
211        start: (i32, i32),
212        end: (i32, i32),
213    },
214
215    /// Power symbol match
216    Power {
217        net_name: String,
218        style: String,
219        is_ground: bool,
220    },
221
222    /// Net label match
223    Label { text: String, location: (i32, i32) },
224
225    /// Junction match
226    Junction { location: (i32, i32) },
227
228    /// Parameter match
229    Parameter {
230        component_designator: String,
231        name: String,
232        value: String,
233    },
234
235    /// Count result (for :count pseudo-selector)
236    Count(usize),
237}
238
239impl QueryMatch {
240    /// Render match as concise text
241    pub fn to_short_text(&self) -> String {
242        match self {
243            QueryMatch::Component {
244                designator,
245                part,
246                value,
247                ..
248            } => {
249                if let Some(v) = value {
250                    format!("{} ({}, {})", designator, part, v)
251                } else {
252                    format!("{} ({})", designator, part)
253                }
254            }
255            QueryMatch::Pin {
256                component_designator,
257                designator,
258                name,
259                electrical_type,
260                connected_net,
261                ..
262            } => {
263                let net_str = connected_net.as_deref().unwrap_or("NC");
264                format!(
265                    "{}.{} \"{}\" [{}] -> {}",
266                    component_designator, designator, name, electrical_type, net_str
267                )
268            }
269            QueryMatch::Net {
270                name,
271                connection_count,
272                is_power,
273                is_ground,
274                ..
275            } => {
276                let suffix = if *is_power {
277                    " [PWR]"
278                } else if *is_ground {
279                    " [GND]"
280                } else {
281                    ""
282                };
283                format!("{}{} ({} connections)", name, suffix, connection_count)
284            }
285            QueryMatch::Port {
286                name,
287                io_type,
288                connected_net,
289            } => {
290                let net_str = connected_net.as_deref().unwrap_or("?");
291                format!("PORT {} [{}] -> {}", name, io_type, net_str)
292            }
293            QueryMatch::Wire {
294                index,
295                vertex_count,
296                ..
297            } => {
298                format!("Wire #{} ({} vertices)", index, vertex_count)
299            }
300            QueryMatch::Power {
301                net_name,
302                style,
303                is_ground,
304            } => {
305                let kind = if *is_ground { "GND" } else { "PWR" };
306                format!("{} [{}] ({})", net_name, kind, style)
307            }
308            QueryMatch::Label { text, .. } => {
309                format!("Label \"{}\"", text)
310            }
311            QueryMatch::Junction { location } => {
312                format!(
313                    "Junction @ ({}, {})",
314                    location.0 / 10000,
315                    location.1 / 10000
316                )
317            }
318            QueryMatch::Parameter {
319                component_designator,
320                name,
321                value,
322            } => {
323                format!("{}.{} = \"{}\"", component_designator, name, value)
324            }
325            QueryMatch::Count(n) => {
326                format!("{}", n)
327            }
328        }
329    }
330
331    /// Render match as detailed text
332    pub fn to_detail_text(&self) -> String {
333        match self {
334            QueryMatch::Component {
335                designator,
336                part,
337                description,
338                value,
339                footprint,
340                pin_count,
341            } => {
342                let mut s = format!("Component {}\n", designator);
343                s.push_str(&format!("  Part: {}\n", part));
344                if !description.is_empty() {
345                    s.push_str(&format!("  Description: {}\n", description));
346                }
347                if let Some(v) = value {
348                    s.push_str(&format!("  Value: {}\n", v));
349                }
350                if let Some(fp) = footprint {
351                    s.push_str(&format!("  Footprint: {}\n", fp));
352                }
353                s.push_str(&format!("  Pins: {}\n", pin_count));
354                s
355            }
356            QueryMatch::Net {
357                name,
358                connections,
359                is_power,
360                is_ground,
361                ..
362            } => {
363                let mut s = format!("Net: {}", name);
364                if *is_power {
365                    s.push_str(" [POWER]");
366                }
367                if *is_ground {
368                    s.push_str(" [GROUND]");
369                }
370                s.push('\n');
371                for conn in connections {
372                    s.push_str(&format!("  - {}\n", conn));
373                }
374                s
375            }
376            QueryMatch::Pin {
377                component_designator,
378                designator,
379                name,
380                electrical_type,
381                connected_net,
382                is_hidden,
383            } => {
384                let mut s = format!("Pin {}.{}\n", component_designator, designator);
385                s.push_str(&format!("  Name: {}\n", name));
386                s.push_str(&format!("  Type: {}\n", electrical_type));
387                if let Some(net) = connected_net {
388                    s.push_str(&format!("  Net: {}\n", net));
389                } else {
390                    s.push_str("  Net: (unconnected)\n");
391                }
392                if *is_hidden {
393                    s.push_str("  Hidden: yes\n");
394                }
395                s
396            }
397            _ => self.to_short_text(),
398        }
399    }
400}
401
402/// Main query engine for schematic documents using SchQL
403pub struct SchematicQuery<'a> {
404    view: &'a SchematicView,
405}
406
407impl<'a> SchematicQuery<'a> {
408    /// Create a new query engine for a schematic view
409    pub fn new(view: &'a SchematicView) -> Self {
410        Self { view }
411    }
412
413    /// Execute a query and return results
414    pub fn query(&self, query_str: &str) -> Result<QueryResult, QueryError> {
415        let start = std::time::Instant::now();
416
417        // Parse the query
418        let parser = QueryParser::new();
419        let selector = parser.parse(query_str)?;
420
421        // Execute the query
422        let executor = QueryExecutor::new(self.view);
423        let matches = executor.execute(&selector)?;
424
425        Ok(QueryResult {
426            matches,
427            query: query_str.to_string(),
428            execution_time_us: start.elapsed().as_micros() as u64,
429        })
430    }
431
432    /// Execute multiple queries and return combined results
433    pub fn query_batch(&self, queries: &[&str]) -> Vec<Result<QueryResult, QueryError>> {
434        queries.iter().map(|q| self.query(q)).collect()
435    }
436}
437
438/// Convenience function to query a SchDoc directly using SchQL
439pub fn query_schdoc(doc: &SchDoc, query_str: &str) -> Result<QueryResult, QueryError> {
440    let view = SchematicView::from_schdoc(doc);
441    let engine = SchematicQuery::new(&view);
442    engine.query(query_str)
443}
444
445#[cfg(test)]
446mod tests;