1mod common;
59
60pub use common::{
61 ElectricalFilter, ElectricalType, FilterOp, FilterValue as CommonFilterValue, VisibilityFilter,
62 compare_filter,
63};
64
65mod 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
85mod 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#[derive(Debug, Clone)]
105pub struct QueryResult {
106 pub matches: Vec<QueryMatch>,
108 pub query: String,
110 pub execution_time_us: u64,
112}
113
114impl QueryResult {
115 pub fn is_empty(&self) -> bool {
117 self.matches.is_empty()
118 }
119
120 pub fn len(&self) -> usize {
122 self.matches.len()
123 }
124
125 pub fn to_text(&self) -> String {
127 if self.matches.is_empty() {
128 return format!("Query `{}`: No matches\n", self.query);
129 }
130
131 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 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#[derive(Debug, Clone)]
170pub enum QueryMatch {
171 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 {
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 {
193 name: String,
194 is_power: bool,
195 is_ground: bool,
196 connection_count: usize,
197 connections: Vec<String>,
198 },
199
200 Port {
202 name: String,
203 io_type: String,
204 connected_net: Option<String>,
205 },
206
207 Wire {
209 index: usize,
210 vertex_count: usize,
211 start: (i32, i32),
212 end: (i32, i32),
213 },
214
215 Power {
217 net_name: String,
218 style: String,
219 is_ground: bool,
220 },
221
222 Label { text: String, location: (i32, i32) },
224
225 Junction { location: (i32, i32) },
227
228 Parameter {
230 component_designator: String,
231 name: String,
232 value: String,
233 },
234
235 Count(usize),
237}
238
239impl QueryMatch {
240 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 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
402pub struct SchematicQuery<'a> {
404 view: &'a SchematicView,
405}
406
407impl<'a> SchematicQuery<'a> {
408 pub fn new(view: &'a SchematicView) -> Self {
410 Self { view }
411 }
412
413 pub fn query(&self, query_str: &str) -> Result<QueryResult, QueryError> {
415 let start = std::time::Instant::now();
416
417 let parser = QueryParser::new();
419 let selector = parser.parse(query_str)?;
420
421 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 pub fn query_batch(&self, queries: &[&str]) -> Vec<Result<QueryResult, QueryError>> {
434 queries.iter().map(|q| self.query(q)).collect()
435 }
436}
437
438pub 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;