KiCad PCB EDA Suite
Loading...
Searching...
No Matches
sch_io_database.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2022 Jon Evans <[email protected]>
5 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
6 *
7 * This program is free software: you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the
9 * Free Software Foundation, either version 3 of the License, or (at your
10 * option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 */
20
21#include <chrono>
22#include <iostream>
23#include <string_view>
24#include <set>
25#include <utility>
26#include <bs_thread_pool.hpp>
27#include <wx/datetime.h>
28#include <wx/log.h>
29#include <wx/tokenzr.h>
30
31#include <boost/algorithm/string.hpp>
32#include <json_common.h>
33#include <pin_map.h>
34
38#include <fmt.h>
39#include <hash.h>
40#include <ki_exception.h>
41#include <lib_symbol.h>
42
43#include "sch_io_database.h"
44
46
47
48thread_local std::unordered_set<wxString> SCH_IO_DATABASE::s_inProgressLoads;
49
50
52 SCH_IO( wxS( "Database library" ) ),
53 m_adapter( nullptr ),
54 m_settings(),
55 m_conn()
56{
59}
60
61
66
67
68void SCH_IO_DATABASE::EnumerateSymbolLib( wxArrayString& aSymbolNameList,
69 const wxString& aLibraryPath,
70 const std::map<std::string, UTF8>* aProperties )
71{
72 std::vector<LIB_SYMBOL*> symbols;
73 EnumerateSymbolLib( symbols, aLibraryPath, aProperties );
74
75 for( LIB_SYMBOL* symbol : symbols )
76 {
77 aSymbolNameList.Add( symbol->GetName() );
78 delete symbol;
79 }
80}
81
82
83void SCH_IO_DATABASE::EnumerateSymbolLib( std::vector<LIB_SYMBOL*>& aSymbolList,
84 const wxString& aLibraryPath,
85 const std::map<std::string, UTF8>* aProperties )
86{
87 wxCHECK_RET( m_adapter, "Database plugin missing library manager adapter handle!" );
88 ensureSettings( aLibraryPath );
90 cacheLib();
91
92 if( !m_conn )
94
95 bool powerSymbolsOnly = ( aProperties && aProperties->contains( SYMBOL_LIBRARY_ADAPTER::PropPowerSymsOnly ) );
96
97 std::shared_lock lock( m_cacheMutex );
98
99 for( auto const& pair : m_nameToSymbolcache )
100 {
101 LIB_SYMBOL* symbol = pair.second.get();
102
103 if( !powerSymbolsOnly || symbol->IsPower() )
104 aSymbolList.emplace_back( symbol->Duplicate() );
105 }
106}
107
108
109void SCH_IO_DATABASE::CheckLibrary( const wxString& aLibraryPath,
110 const std::map<std::string, UTF8>* aProperties )
111{
112 ensureSettings( aLibraryPath );
114}
115
116
117LIB_SYMBOL* SCH_IO_DATABASE::LoadSymbol( const wxString& aLibraryPath,
118 const wxString& aAliasName,
119 const std::map<std::string, UTF8>* aProperties )
120{
121 wxCHECK_MSG( m_adapter, nullptr, "Database plugin missing library manager adapter handle!" );
122 ensureSettings( aLibraryPath );
124
125 if( !m_conn )
127
128 cacheLib();
129
130 std::string tableName;
131 std::string symbolName( aAliasName.ToUTF8() );
132
133 {
134 std::shared_lock lock( m_cacheMutex );
135
136 if( auto cacheIt = m_nameToSymbolcache.find( aAliasName ); cacheIt != m_nameToSymbolcache.end() )
137 {
138 LIB_SYMBOL* cached = cacheIt->second.get();
139 return cached->Duplicate();
140 }
141
142 auto sanitizedIt = m_sanitizedNameMap.find( aAliasName );
143
144 if( sanitizedIt != m_sanitizedNameMap.end() )
145 {
146 tableName = sanitizedIt->second.first;
147 symbolName = sanitizedIt->second.second;
148 }
149 }
150
151 /*
152 * Table names are tricky, in order to allow maximum flexibility to the user.
153 * The slash character is used as a separator between a table name and symbol name, but symbol
154 * names may also contain slashes and table names may now also be empty (which results in the
155 * slash being dropped in the symbol name when placing a new symbol). So, if a slash is found,
156 * we check if the string before the slash is a valid table name. If not, we assume the table
157 * name is blank if our config has an entry for the null table.
158 */
159
160 if( tableName.empty() && aAliasName.Contains( '/' ) )
161 {
162 tableName = std::string( aAliasName.BeforeFirst( '/' ).ToUTF8() );
163 symbolName = std::string( aAliasName.AfterFirst( '/' ).ToUTF8() );
164 }
165
166 std::vector<const DATABASE_LIB_TABLE*> tablesToTry;
167
168 for( const DATABASE_LIB_TABLE& tableIter : m_settings->m_Tables )
169 {
170 // no table means globally unique keys, try all tables
171 if( tableName.empty() || tableIter.name == tableName )
172 tablesToTry.emplace_back( &tableIter );
173 }
174
175 if( tablesToTry.empty() )
176 {
177 wxLogTrace( traceDatabase, wxT( "LoadSymbol: table '%s' not found in config" ), tableName );
178 return nullptr;
179 }
180
181 const DATABASE_LIB_TABLE* foundTable = nullptr;
183
184 for( const DATABASE_LIB_TABLE* table : tablesToTry )
185 {
186 if( m_conn->SelectOne( table->table, std::make_pair( table->key_col, symbolName ), result ) )
187 {
188 foundTable = table;
189 wxLogTrace( traceDatabase, wxT( "LoadSymbol: SelectOne (%s, %s) found in %s" ),
190 table->key_col, symbolName, table->table );
191 }
192 else
193 {
194 wxLogTrace( traceDatabase, wxT( "LoadSymbol: SelectOne (%s, %s) failed for table %s" ),
195 table->key_col, symbolName, table->table );
196 }
197 }
198
199 if( !foundTable )
200 return nullptr;
201
202 return loadSymbolFromRow( aAliasName, *foundTable, result ).release();
203}
204
205
206void SCH_IO_DATABASE::GetSubLibraryNames( std::vector<wxString>& aNames )
207{
208 ensureSettings( wxEmptyString );
209
210 aNames.clear();
211
212 std::set<wxString> tableNames;
213
214 for( const DATABASE_LIB_TABLE& tableIter : m_settings->m_Tables )
215 {
216 if( tableNames.count( tableIter.name ) )
217 continue;
218
219 aNames.emplace_back( tableIter.name );
220 tableNames.insert( tableIter.name );
221 }
222}
223
224
225void SCH_IO_DATABASE::GetAvailableSymbolFields( std::vector<wxString>& aNames )
226{
227 std::lock_guard<std::mutex> lock( m_symbolLoadMutex );
228
229 std::copy( m_customFields.begin(), m_customFields.end(), std::back_inserter( aNames ) );
230}
231
232
233void SCH_IO_DATABASE::GetDefaultSymbolFields( std::vector<wxString>& aNames )
234{
235 std::lock_guard<std::mutex> lock( m_symbolLoadMutex );
236
237 std::copy( m_defaultShownFields.begin(), m_defaultShownFields.end(), std::back_inserter( aNames ) );
238}
239
240
241bool SCH_IO_DATABASE::TestConnection( wxString* aErrorMsg )
242{
243 if( m_conn && m_conn->IsConnected() )
244 return true;
245
246 connect();
247
248 if( aErrorMsg && ( !m_conn || !m_conn->IsConnected() ) )
249 *aErrorMsg = m_lastError;
250
251 return m_conn && m_conn->IsConnected();
252}
253
254
255size_t SCH_IO_DATABASE::computeSignature( const TABLE_RESULT_LIST& aTableResults ) const
256{
257 size_t signature = 0;
258
259 for( const auto& [table, results] : aTableResults )
260 {
261 for( const DATABASE_CONNECTION::ROW& result : results )
262 {
263 size_t rowSignature = 0;
264
265 for( const auto& [column, value] : result )
266 {
267 hash_combine( signature, std::string_view( column ) );
268
269 if( const std::string* str = std::any_cast<std::string>( &value ) )
270 hash_combine( rowSignature, std::string_view( *str ) );
271 }
272
273 hash_combine( signature, rowSignature );
274
275 if( auto it = result.find( table->symbols_col ); it != result.end() )
276 {
277 if( const std::string* str = std::any_cast<std::string>( &it->second ) )
278 {
279 LIB_ID symbolId;
280 symbolId.Parse( *str );
281
282 if( symbolId.IsValid() )
283 {
284 const UTF8& nickname = symbolId.GetLibNickname();
285 hash_combine( signature, std::string_view( nickname.c_str() ) );
286
287 if( std::optional<int> libHash = m_adapter->GetLibraryModifyHash( nickname ) )
288 hash_combine( signature, *libHash );
289 }
290 }
291 }
292 }
293 }
294
295 return signature;
296}
297
298
300 std::map<wxString, std::unique_ptr<LIB_SYMBOL>>& aSymbolCache,
301 std::map<wxString, std::pair<std::string, std::string>>& aSanitizedNameMap )
302{
303 for( const auto& [table, results] : aTableResults )
304 {
305 for( const DATABASE_CONNECTION::ROW& result : results )
306 {
307 if( !result.contains( table->key_col ) )
308 continue;
309
310 std::string rawName = std::any_cast<std::string>( result.at( table->key_col ) );
311 UTF8 sanitizedName = LIB_ID::FixIllegalChars( rawName, false );
312 std::string sanitizedKey = sanitizedName.c_str();
313 std::string prefix = ( m_settings->m_GloballyUniqueKeys || table->name.empty() )
314 ? ""
315 : fmt::format( "{}/", table->name );
316 std::string sanitizedDisplayName = fmt::format( "{}{}", prefix, sanitizedKey );
317 wxString name( sanitizedDisplayName );
318
319 aSanitizedNameMap[name] = std::make_pair( table->name, rawName );
320
321 std::unique_ptr<LIB_SYMBOL> symbol = loadSymbolFromRow( name, *table, result );
322
323 if( symbol )
324 aSymbolCache[symbol->GetName()] = std::move( symbol );
325 }
326 }
327
328 return !aSymbolCache.empty();
329}
330
331
333{
334 // Guard against re-entrant cacheLib() calls. A self-referential symbol row (issue #24249)
335 // causes m_adapter->LoadSymbol to route back into SCH_IO_DATABASE::LoadSymbol, which would
336 // otherwise call cacheLib() again while it is in the middle of populating its caches.
337 if( m_inCacheLib )
338 return;
339
340 long long currentTimestampSeconds = wxDateTime::Now().GetValue().GetValue() / 1000;
341
342 // After the initial load, the background refresh thread handles all cache updates.
343 {
344 std::shared_lock lock( m_cacheMutex );
345
346 if( m_cachePopulated )
347 return;
348 }
349
350 m_inCacheLib = true;
351
352 struct CACHE_LIB_GUARD
353 {
354 bool* flag;
355 ~CACHE_LIB_GUARD() { *flag = false; }
356 } cacheLibGuard{ &m_inCacheLib };
357
358 // Re-query the database (the connection layer caches results subject to its own max_age) and
359 // compute a lightweight signature of the raw rows so we can skip the costly materialization
360 // when nothing relevant has changed.
361 TABLE_RESULT_LIST tableResults;
362
363 for( const DATABASE_LIB_TABLE& table : m_settings->m_Tables )
364 {
365 std::vector<DATABASE_CONNECTION::ROW> results;
366
367 if( !m_conn->SelectAll( table.table, table.key_col, results ) )
368 {
369 if( !m_conn->GetLastError().empty() )
370 THROW_IO_ERRORF( _( "Error reading database table %s: %s" ), table.table, m_conn->GetLastError() );
371
372 continue;
373 }
374
375 tableResults.emplace_back( &table, std::move( results ) );
376 }
377
378 size_t signature = computeSignature( tableResults );
379
380 {
381 std::unique_lock lock( m_cacheMutex );
382
383 if( m_cachePopulated && signature == m_cacheSignature )
384 {
385 m_cacheTimestamp = currentTimestampSeconds;
386 return;
387 }
388 }
389
390 std::map<wxString, std::unique_ptr<LIB_SYMBOL>> newSymbolCache;
391 std::map<wxString, std::pair<std::string, std::string>> newSanitizedNameMap;
392
393 materializeCache( tableResults, newSymbolCache, newSanitizedNameMap );
394
395 {
396 std::unique_lock lock( m_cacheMutex );
397
398 m_nameToSymbolcache = std::move( newSymbolCache );
399 m_sanitizedNameMap = std::move( newSanitizedNameMap );
400
401 m_cacheTimestamp = currentTimestampSeconds;
402 m_cacheSignature = signature;
403 m_cachePopulated = true;
404 m_modifyHash++;
405 }
406
407 if( !m_refreshRunning.load() )
409}
410
411
413{
414 if( m_refreshRunning.exchange( true ) )
415 return;
416
417 wxLogTrace( traceDatabase, wxT( "Starting background refresh thread" ) );
419}
420
421
423{
424 m_refreshRunning = false;
425 m_refreshCV.notify_all();
426
427 if( m_refreshThread.joinable() )
428 m_refreshThread.join();
429}
430
431
433{
434 BS::this_thread::set_os_thread_name( "dblib bg" );
435
436 while( m_refreshRunning.load() )
437 {
438 long long maxAge = 0;
439
440 {
441 std::unique_lock lock( m_cacheMutex );
442
443 if( m_settings )
444 maxAge = m_settings->m_Cache.max_age;
445 }
446
447 if( maxAge <= 0 )
448 maxAge = 1;
449
450 std::shared_lock connGuard( m_cacheMutex );
451
452 if( m_conn && m_cachePopulated.load() )
453 {
454 wxLogTrace( traceDatabase, wxT( "Initiating background refresh" ) );
455
456 try
457 {
458 std::vector<std::pair<const DATABASE_LIB_TABLE*, std::vector<DATABASE_CONNECTION::ROW>>> tableResults;
459 bool querySuccess = true;
460
461 for( const DATABASE_LIB_TABLE& table : m_settings->m_Tables )
462 {
463 m_conn->ClearCache( table.table );
464
465 std::vector<DATABASE_CONNECTION::ROW> results;
466
467 if( !m_conn->SelectAll( table.table, table.key_col, results ) )
468 {
469 wxLogTrace( traceDatabase, wxT( "Background refresh: SelectAll failed for table %s" ),
470 table.table );
471 querySuccess = false;
472 break;
473 }
474
475 tableResults.emplace_back( &table, std::move( results ) );
476 }
477
478 if( querySuccess )
479 {
480 size_t signature = computeSignature( tableResults );
481
482 bool dataChanged = false;
483
484 {
485 // Upgrade to unique lock for cache mutation
486 connGuard.unlock();
487 std::unique_lock lock( m_cacheMutex );
488
489 if( signature == m_cacheSignature )
490 m_cacheTimestamp = wxDateTime::Now().GetValue().GetValue() / 1000;
491 else
492 dataChanged = true;
493 }
494
495 if( dataChanged )
496 {
497 std::map<wxString, std::unique_ptr<LIB_SYMBOL>> newSymbolCache;
498 std::map<wxString, std::pair<std::string, std::string>> newSanitizedNameMap;
499
500 materializeCache( tableResults, newSymbolCache, newSanitizedNameMap );
501
502 wxLogTrace( traceDatabase, wxT( "Background refresh: new data" ) );
503
504 {
505 std::unique_lock lock( m_cacheMutex );
506
507 m_nameToSymbolcache = std::move( newSymbolCache );
508 m_sanitizedNameMap = std::move( newSanitizedNameMap );
509
510 m_cacheTimestamp = wxDateTime::Now().GetValue().GetValue() / 1000;
511 m_cacheSignature = signature;
512 m_cachePopulated = true;
513 m_modifyHash++;
514 }
515 }
516 else
517 {
518 wxLogTrace( traceDatabase, wxT( "Background refresh: no new data" ) );
519 }
520 }
521 }
522 catch( const IO_ERROR& e )
523 {
524 wxLogTrace( traceDatabase, wxT( "Background refresh failed: %s; cache preserved" ), e.What() );
525 }
526 catch( const std::exception& e )
527 {
528 wxLogTrace( traceDatabase, wxT( "Background refresh failed: %s; cache preserved" ), e.what() );
529 }
530 }
531
532 {
533 std::unique_lock lock( m_refreshMutex );
534 m_refreshCV.wait_for( lock, std::chrono::seconds( maxAge ),
535 [this]()
536 {
537 return !m_refreshRunning.load();
538 } );
539 }
540 }
541}
542
543
544void SCH_IO_DATABASE::ensureSettings( const wxString& aSettingsPath )
545{
546 auto tryLoad =
547 [&]()
548 {
549 if( !m_settings->LoadFromFile() )
550 {
551 THROW_IO_ERRORF( _( "Could not load database library: settings file %s missing or invalid" ),
552 aSettingsPath );
553 }
554 };
555
556 if( !m_settings && !aSettingsPath.IsEmpty() )
557 {
558 std::string path( aSettingsPath.ToUTF8() );
559 m_settings = std::make_unique<DATABASE_LIB_SETTINGS>( path );
560 m_settings->SetReadOnly( true );
561
562 tryLoad();
563 }
564 else if( !m_conn && m_settings )
565 {
566 // If we have valid settings but no connection yet; reload settings in case user is editing
567 tryLoad();
568 }
569 else if( m_conn && m_settings && !aSettingsPath.IsEmpty() )
570 {
571 wxASSERT_MSG( aSettingsPath == m_settings->GetFilename(),
572 "Path changed for database library without re-initializing plugin!" );
573 }
574 else if( !m_settings )
575 {
576 wxLogTrace( traceDatabase, wxT( "ensureSettings: no settings but no valid path!" ) );
577 }
578}
579
580
582{
583 wxCHECK_RET( m_settings, "Call ensureSettings before ensureConnection!" );
584
585 connect();
586
587 if( !m_conn || !m_conn->IsConnected() )
588 {
589 THROW_IO_ERRORF( _( "Could not load database library: could not connect to database %s (%s)" ),
590 m_settings->m_Source.dsn, m_lastError );
591 }
592}
593
594
596{
597 wxCHECK_RET( m_settings, "Call ensureSettings before connect()!" );
598
599 {
600 std::unique_lock connLock( m_cacheMutex );
601
602 if( m_conn && !m_conn->IsConnected() )
603 m_conn.reset();
604 }
605
606 if( !m_conn )
607 {
608 std::unique_ptr<DATABASE_CONNECTION> newConn;
609
610 if( m_settings->m_Source.connection_string.empty() )
611 {
612 newConn = std::make_unique<DATABASE_CONNECTION>( m_settings->m_Source.dsn,
613 m_settings->m_Source.username,
614 m_settings->m_Source.password,
615 m_settings->m_Source.timeout );
616 }
617 else
618 {
619 std::string cs = m_settings->m_Source.connection_string;
620 std::string basePath( wxFileName( m_settings->GetFilename() ).GetPath().ToUTF8() );
621
622 // Database drivers that use files operate on absolute paths, so provide a mechanism
623 // for specifying on-disk databases that live next to the kicad_dbl file
624 boost::replace_all( cs, "${CWD}", basePath );
625
626 newConn = std::make_unique<DATABASE_CONNECTION>( cs, m_settings->m_Source.timeout );
627 }
628
629 if( !newConn->IsConnected() )
630 {
631 m_lastError = newConn->GetLastError();
632 return;
633 }
634
635 for( const DATABASE_LIB_TABLE& tableIter : m_settings->m_Tables )
636 {
637 // Trusted even if unreported by the driver, to support generated columns (#16952)
638 std::set<std::string> requiredColumns{ tableIter.key_col,
639 tableIter.footprints_col,
640 tableIter.symbols_col };
641
642 for( const DATABASE_FIELD_MAPPING& field : tableIter.fields )
643 requiredColumns.insert( field.column );
644
645 if( !tableIter.pins_col.empty() )
646 requiredColumns.insert( tableIter.pins_col );
647
648 // Only used if confirmed present, so a misconfigured mapping can't break the table (#23532)
649 std::set<std::string> optionalColumns{ tableIter.properties.description,
651 tableIter.properties.keywords,
654 tableIter.properties.exclude_from_board };
655
656 newConn->CacheTableInfo( tableIter.table, requiredColumns, optionalColumns );
657 }
658
659 newConn->SetCacheParams( m_settings->m_Cache.max_size, m_settings->m_Cache.max_age );
660
661 std::unique_lock connLock( m_cacheMutex );
662 m_conn = std::move( newConn );
663 }
664}
665
666
667std::optional<bool> SCH_IO_DATABASE::boolFromAny( const std::any& aVal )
668{
669 try
670 {
671 bool val = std::any_cast<bool>( aVal );
672 return val;
673 }
674 catch( const std::bad_any_cast& )
675 {
676 }
677
678 try
679 {
680 int val = std::any_cast<int>( aVal );
681 return static_cast<bool>( val );
682 }
683 catch( const std::bad_any_cast& )
684 {
685 }
686
687 try
688 {
689 wxString strval( std::any_cast<std::string>( aVal ).c_str(), wxConvUTF8 );
690
691 if( strval.IsEmpty() )
692 return std::nullopt;
693
694 strval.MakeLower();
695
696 for( const auto& trueVal : { wxS( "true" ), wxS( "yes" ), wxS( "y" ), wxS( "1" ) } )
697 {
698 if( strval.Matches( trueVal ) )
699 return true;
700 }
701
702 for( const auto& falseVal : { wxS( "false" ), wxS( "no" ), wxS( "n" ), wxS( "0" ) } )
703 {
704 if( strval.Matches( falseVal ) )
705 return false;
706 }
707 }
708 catch( const std::bad_any_cast& )
709 {
710 }
711
712 return std::nullopt;
713}
714
715
716std::unique_ptr<LIB_SYMBOL> SCH_IO_DATABASE::loadSymbolFromRow( const wxString& aSymbolName,
717 const DATABASE_LIB_TABLE& aTable,
718 const DATABASE_CONNECTION::ROW& aRow )
719{
720 std::unique_ptr<LIB_SYMBOL> symbol = nullptr;
721
722 if( aRow.contains( aTable.symbols_col ) )
723 {
724 std::string symbols = std::any_cast<std::string>( aRow.at( aTable.symbols_col ) );
725 wxString symbolsStr = wxString( symbols.c_str(), wxConvUTF8 );
726 wxStringTokenizer tokenizer( symbolsStr, ";\t\r\n", wxTOKEN_STRTOK );
727
728 std::vector<LIB_ID> symbolIds;
729
730 while( tokenizer.HasMoreTokens() )
731 {
732 wxString token = tokenizer.GetNextToken();
733 LIB_ID id;
734 id.Parse( std::string( token.ToUTF8() ) );
735
736 if( id.IsValid() )
737 symbolIds.push_back( id );
738 }
739
740 // A row's Symbols column may resolve back into the same database library (issue #24249,
741 // e.g. a mistyped library nickname). The adapter would route that lookup back into
742 // SCH_IO_DATABASE::LoadSymbol and re-enter loadSymbolFromRow on the same row until the
743 // stack overflows. Track in-flight LIB_IDs and skip the recursive load on re-entry.
744 std::vector<LIB_SYMBOL*> sourceSymbols;
745 std::vector<wxString> sourceSymbolNames;
746
747 for( const LIB_ID& symbolId : symbolIds )
748 {
749 wxString symbolIdStr = symbolId.Format().wx_str();
750
751 if( !s_inProgressLoads.insert( symbolIdStr ).second )
752 {
753 wxLogTrace( traceDatabase, wxT( "loadSymbolFromRow: cycle detected resolving '%s' "
754 "(row '%s' in table '%s'); skipping recursive load" ),
755 symbolIdStr, aSymbolName, aTable.name );
756 continue;
757 }
758
759 struct CYCLE_GUARD
760 {
761 std::unordered_set<wxString>* set;
762 wxString key;
763 ~CYCLE_GUARD() { set->erase( key ); }
764 } guard{ &s_inProgressLoads, symbolIdStr };
765
766 LIB_SYMBOL* src = nullptr;
767
768 {
769 std::lock_guard lock( m_symbolLoadMutex );
770 src = m_adapter->LoadSymbol( symbolId );
771 }
772
773 if( src )
774 {
775 sourceSymbols.push_back( src );
776 sourceSymbolNames.push_back( src->GetName() );
777 }
778 else
779 {
780 wxLogTrace( traceDatabase, wxT( "loadSymbolFromRow: source symbol '%s' not found" ), symbolIdStr );
781 }
782 }
783
784 if( sourceSymbols.empty() )
785 {
786 // Actual symbol not found: return metadata only; error will be indicated in the
787 // symbol chooser
788 symbol.reset( new LIB_SYMBOL( aSymbolName ) );
789 }
790 else if( sourceSymbols.size() == 1 )
791 {
792 symbol.reset( sourceSymbols[0]->Duplicate() );
793 symbol->SetSourceLibId( symbolIds[0] );
794 symbol->SetName( aSymbolName );
795 }
796 else
797 {
798 // If the database row specifies multiple symbols, put together a composite,
799 // but since source symbols may already have multiple body styles, we
800 // need to flatten out any common to all body styles items on source symbols.
801 // Since we don't support varying all properties, take those from the first one.
802
803 symbol.reset( new LIB_SYMBOL( aSymbolName ) );
804 symbol->SetSourceLibId( symbolIds[0] );
805
806 symbol->SetUnitCount( sourceSymbols[0]->GetUnitCount(), false );
807 symbol->SetPowerSymbolProp( sourceSymbols[0]->IsPower() );
808 symbol->SetShowPinNames( sourceSymbols[0]->GetShowPinNames() );
809 symbol->SetShowPinNumbers( sourceSymbols[0]->GetShowPinNumbers() );
810 symbol->SetPinNameOffset( sourceSymbols[0]->GetPinNameOffset() );
811
812 for( FIELD_T fieldId : MANDATORY_FIELDS )
813 {
814 if( SCH_FIELD* srcField = sourceSymbols[0]->GetField( fieldId ) )
815 {
816 SCH_FIELD* dstField = symbol->GetField( fieldId );
817 *dstField = *srcField;
818 dstField->SetParent( symbol.get() );
819 }
820 }
821
822 std::vector<wxString> bodyStyleNames;
823 std::vector<int> sourceBodyStyleCounts;
824 std::vector<int> compositeBodyStyleBase;
825
826 int nextBodyStyle = 1;
827
828 for( size_t i = 0; i < sourceSymbols.size(); ++i )
829 {
830 int srcCount = std::max( 1, sourceSymbols[i]->GetBodyStyleCount() );
831 sourceBodyStyleCounts.push_back( srcCount );
832 compositeBodyStyleBase.push_back( nextBodyStyle );
833
834 if( srcCount == 1 )
835 {
836 bodyStyleNames.push_back( sourceSymbolNames[i] );
837 }
838 else
839 {
840 for( int style = 1; style <= srcCount; ++style )
841 {
842 wxString styleName = sourceSymbols[i]->GetBodyStyleDescription( style, false );
843 bodyStyleNames.push_back( sourceSymbolNames[i] + wxT( " (" ) + styleName + wxT( ")" ) );
844 }
845 }
846
847 nextBodyStyle += srcCount;
848 }
849
850 int totalBodyStyles = nextBodyStyle - 1;
851
852 symbol->SetHasDeMorganBodyStyles( false );
853 symbol->SetBodyStyleNames( bodyStyleNames );
854
855 std::set<wxString> mergedFieldNames;
856
857 for( size_t i = 0; i < sourceSymbols.size(); ++i )
858 {
859 std::unique_ptr<LIB_SYMBOL> srcSymbol = sourceSymbols[i]->Flatten();
860 int srcCount = sourceBodyStyleCounts[i];
861 int base = compositeBodyStyleBase[i];
862
863 for( SCH_ITEM& item : srcSymbol->GetDrawItems() )
864 {
865 if( item.Type() == SCH_FIELD_T )
866 {
867 SCH_FIELD& field = static_cast<SCH_FIELD&>( item );
868
869 if( field.IsMandatory() || !mergedFieldNames.insert( field.GetName() ).second )
870 continue;
871
872 int targetStyle;
873
874 if( item.GetBodyStyle() == ALL_BODY_STYLES )
875 targetStyle = base;
876 else
877 targetStyle = base + ( item.GetBodyStyle() - 1 );
878
879 if( targetStyle > totalBodyStyles )
880 continue;
881
882 SCH_ITEM* newItem = item.Duplicate( IGNORE_PARENT_GROUP );
883 newItem->SetParent( symbol.get() );
884 newItem->SetBodyStyle( targetStyle );
885 symbol->AddDrawItem( newItem, false );
886 }
887 else if( item.GetBodyStyle() == ALL_BODY_STYLES )
888 {
889 for( int bodyStyle = 0; bodyStyle < srcCount; ++bodyStyle )
890 {
891 SCH_ITEM* newItem = item.Duplicate( IGNORE_PARENT_GROUP );
892 newItem->SetParent( symbol.get() );
893 newItem->SetBodyStyle( base + bodyStyle );
894 symbol->AddDrawItem( newItem, false );
895 }
896 }
897 else
898 {
899 int targetStyle = base + ( item.GetBodyStyle() - 1 );
900
901 if( targetStyle > totalBodyStyles )
902 continue;
903
904 SCH_ITEM* newItem = item.Duplicate( IGNORE_PARENT_GROUP );
905 newItem->SetParent( symbol.get() );
906 newItem->SetBodyStyle( targetStyle );
907 symbol->AddDrawItem( newItem, false );
908 }
909 }
910 }
911
912 symbol->GetDrawItems().sort();
913 }
914 }
915 else
916 {
917 symbol.reset( new LIB_SYMBOL( aSymbolName ) );
918 }
919
920 LIB_ID libId = symbol->GetLibId();
921 libId.SetSubLibraryName( aTable.name );
922 symbol->SetLibId( libId );
923
924 wxArrayString footprintsList;
925
926 if( aRow.count( aTable.footprints_col ) )
927 {
928 std::string footprints = std::any_cast<std::string>( aRow.at( aTable.footprints_col ) );
929
930 wxString footprintsStr = wxString( footprints.c_str(), wxConvUTF8 );
931 wxStringTokenizer tokenizer( footprintsStr, ";\t\r\n", wxTOKEN_STRTOK );
932
933 while( tokenizer.HasMoreTokens() )
934 footprintsList.Add( tokenizer.GetNextToken() );
935
936 if( footprintsList.size() > 0 )
937 symbol->GetFootprintField().SetText( footprintsList[0] );
938 }
939 else
940 {
941 wxLogTrace( traceDatabase, wxT( "loadSymboFromRow: footprint field %s not found." ), aTable.footprints_col );
942 }
943
944 // Pin-to-pad maps (issue #2282): attach non-destructively. The pins column carries either the
945 // spec-form named object { "pin_maps": [...], "associated_footprints": [...] } or the legacy
946 // flat MR !2540 array (read for one release, bound to the row's footprints).
947 if( !aTable.pins_col.empty() && aRow.count( aTable.pins_col ) )
948 {
949 try
950 {
951 std::string jsonStr = std::any_cast<std::string>( aRow.at( aTable.pins_col ) );
952
953 if( !jsonStr.empty() )
954 {
955 nlohmann::json json = nlohmann::json::parse( jsonStr );
956
957 if( json.is_object() && json.contains( "pin_maps" ) )
958 {
959 symbol->SetPinMaps( ParsePinMapSet( json ) );
960 symbol->SetAssociatedFootprints( ParseAssociatedFootprints( json ) );
961 }
962 else if( !footprintsList.IsEmpty() )
963 {
964 std::unordered_map<wxString, std::vector<wxString>> assignments = ParseLegacyPinAssignments( json );
965
966 if( !assignments.empty() )
967 {
968 const wxString mapName = wxS( "Database" );
969
970 symbol->PinMaps().AddOrReplace( MakeLegacyPinMap( mapName, assignments ) );
971
972 // Bind the one named map to every footprint the row offers, mirroring the
973 // old behaviour where the assignment applied regardless of footprint.
974 std::vector<ASSOCIATED_FOOTPRINT> associations;
975
976 for( const wxString& footprint : footprintsList )
977 {
978 LIB_ID fpId;
979 fpId.Parse( footprint );
980 associations.push_back( { fpId, mapName } );
981 }
982
983 symbol->SetAssociatedFootprints( std::move( associations ) );
984 }
985 }
986 }
987 }
988 catch( const std::exception& e )
989 {
990 // Surface a malformed pin-map payload to the user instead of silently dropping it; the
991 // symbol still loads without the map (issue #2282).
992 Report( wxString::Format( _( "Error parsing pin map for database symbol '%s': %s" ),
993 aSymbolName, e.what() ),
995 }
996 }
997
998 if( !aTable.properties.description.empty() && aRow.count( aTable.properties.description ) )
999 {
1000 wxString value(
1001 std::any_cast<std::string>( aRow.at( aTable.properties.description ) ).c_str(),
1002 wxConvUTF8 );
1003 symbol->SetDescription( value );
1004 }
1005
1006 if( !aTable.properties.keywords.empty() && aRow.count( aTable.properties.keywords ) )
1007 {
1008 wxString value( std::any_cast<std::string>( aRow.at( aTable.properties.keywords ) ).c_str(),
1009 wxConvUTF8 );
1010 symbol->SetKeyWords( value );
1011 }
1012
1013 if( !aTable.properties.footprint_filters.empty()
1014 && aRow.count( aTable.properties.footprint_filters ) )
1015 {
1016 wxString value( std::any_cast<std::string>( aRow.at( aTable.properties.footprint_filters ) )
1017 .c_str(),
1018 wxConvUTF8 );
1019 footprintsList.push_back( value );
1020 }
1021
1022 symbol->SetFPFilters( footprintsList );
1023
1024 if( !aTable.properties.exclude_from_sim.empty()
1025 && aRow.count( aTable.properties.exclude_from_sim ) )
1026 {
1027 std::optional<bool> val = boolFromAny( aRow.at( aTable.properties.exclude_from_sim ) );
1028
1029 if( val )
1030 {
1031 symbol->SetExcludedFromSim( *val );
1032 }
1033 else
1034 {
1035 wxLogTrace( traceDatabase, wxT( "loadSymbolFromRow: exclude_from_sim value for %s "
1036 "could not be cast to a boolean" ), aSymbolName );
1037 }
1038 }
1039
1040 if( !aTable.properties.exclude_from_board.empty()
1041 && aRow.count( aTable.properties.exclude_from_board ) )
1042 {
1043 std::optional<bool> val = boolFromAny( aRow.at( aTable.properties.exclude_from_board ) );
1044
1045 if( val )
1046 {
1047 symbol->SetExcludedFromBoard( *val );
1048 }
1049 else
1050 {
1051 wxLogTrace( traceDatabase, wxT( "loadSymbolFromRow: exclude_from_board value for %s "
1052 "could not be cast to a boolean" ), aSymbolName );
1053 }
1054 }
1055
1056 if( !aTable.properties.exclude_from_bom.empty()
1057 && aRow.count( aTable.properties.exclude_from_bom ) )
1058 {
1059 std::optional<bool> val = boolFromAny( aRow.at( aTable.properties.exclude_from_bom ) );
1060
1061 if( val )
1062 {
1063 symbol->SetExcludedFromBOM( *val );
1064 }
1065 else
1066 {
1067 wxLogTrace( traceDatabase, wxT( "loadSymbolFromRow: exclude_from_bom value for %s "
1068 "could not be cast to a boolean" ), aSymbolName );
1069 }
1070 }
1071
1072 std::vector<SCH_FIELD*> fields;
1073 symbol->GetFields( fields );
1074
1075 std::unordered_map<wxString, SCH_FIELD*> fieldsMap;
1076
1077 for( SCH_FIELD* field : fields )
1078 fieldsMap[field->GetName()] = field;
1079
1080 static const wxString c_valueFieldName( wxS( "Value" ) );
1081 static const wxString c_datasheetFieldName( wxS( "Datasheet" ) );
1082 static const wxString c_footprintFieldName( wxS( "Footprint" ) );
1083
1084 // User fields produced from the database mapping must appear in the order they are
1085 // declared in the .kicad_dbl file, not in lexicographic order of their values. Start
1086 // assigning ordinals above whatever the source LIB_SYMBOL already uses so that any
1087 // pre-existing user fields keep their relative position before the database fields.
1088 int dbFieldOrdinal = symbol->GetNextFieldOrdinal();
1089
1090 for( const DATABASE_FIELD_MAPPING& mapping : aTable.fields )
1091 {
1092 if( !aRow.count( mapping.column ) )
1093 {
1094 wxLogTrace( traceDatabase, wxT( "loadSymbolFromRow: field %s not found in result" ), mapping.column );
1095 continue;
1096 }
1097
1098 // Skip footprint field if it maps to the footprints column, since that column is
1099 // already processed above with tokenization for semicolon-separated multiple footprints.
1100 if( mapping.name_wx == c_footprintFieldName && mapping.column == aTable.footprints_col )
1101 continue;
1102
1103 std::string strValue;
1104
1105 try
1106 {
1107 strValue = std::any_cast<std::string>( aRow.at( mapping.column ) );
1108 }
1109 catch( std::bad_any_cast& )
1110 {
1111 }
1112
1113 wxString value( strValue.c_str(), wxConvUTF8 );
1114
1115 if( mapping.name_wx == c_valueFieldName )
1116 {
1117 SCH_FIELD& field = symbol->GetValueField();
1118 field.SetText( value );
1119
1120 if( !mapping.inherit_properties )
1121 {
1122 field.SetVisible( mapping.visible_on_add );
1123 field.SetNameShown( mapping.show_name );
1124 }
1125
1126 continue;
1127 }
1128 else if( mapping.name_wx == c_datasheetFieldName )
1129 {
1130 SCH_FIELD& field = symbol->GetDatasheetField();
1131 field.SetText( value );
1132
1133 if( !mapping.inherit_properties )
1134 {
1135 field.SetVisible( mapping.visible_on_add );
1136 field.SetNameShown( mapping.show_name );
1137
1138 if( mapping.visible_on_add )
1139 field.SetAutoAdded( true );
1140 }
1141
1142 continue;
1143 }
1144
1145 SCH_FIELD* field;
1146 bool isNew = false;
1147
1148 if( fieldsMap.count( mapping.name_wx ) )
1149 {
1150 field = fieldsMap[mapping.name_wx];
1151 }
1152 else
1153 {
1154 field = new SCH_FIELD( nullptr, FIELD_T::USER );
1155 field->SetName( mapping.name_wx );
1156 isNew = true;
1157 fieldsMap[mapping.name_wx] = field;
1158 }
1159
1160 // Assign a sort-order ordinal so the property editor and BOM see the fields in the
1161 // order declared in the .kicad_dbl file. Without this, all USER fields share the
1162 // same FIELD_T::USER id and fall through to value-based comparison in operator<.
1163 // NB: a DB mapping that lands on a mandatory field by name (e.g. Reference or
1164 // Description) must keep its FIELD_T identity for downstream lookups.
1165 field->SetOrdinal( dbFieldOrdinal++, field->IsMandatory() ? field->GetId() : FIELD_T::USER );
1166
1167 if( !mapping.inherit_properties || isNew )
1168 {
1169 field->SetVisible( mapping.visible_on_add );
1170 field->SetAutoAdded( true );
1171 field->SetNameShown( mapping.show_name );
1172 }
1173
1174 field->SetText( value );
1175
1176 if( isNew )
1177 symbol->AddDrawItem( field, false );
1178
1179 m_customFields.insert( mapping.name_wx );
1180
1181 if( mapping.visible_in_chooser )
1182 m_defaultShownFields.insert( mapping.name_wx );
1183 }
1184
1185 symbol->GetDrawItems().sort();
1186
1187 // Field mappings (including Description) are applied with SCH_FIELD::SetText, which does not
1188 // refresh the cached values the library tree and chooser read. Without this the upper chooser
1189 // panel keeps the source symbol's description while the details panel shows the database value.
1190 symbol->RefreshLibraryTreeCaches();
1191
1192 return symbol;
1193}
1194
1195
1197{
1198 return new DIALOG_DATABASE_LIB_SETTINGS( aParent, this );
1199}
const char * name
std::map< std::string, std::any > ROW
Dialog helper object to sit in the inheritance tree between wxDialog and any class written by wxFormB...
Definition dialog_shim.h:80
virtual void SetParent(EDA_ITEM *aParent)
Definition eda_item.cpp:153
virtual void SetVisible(bool aVisible)
Definition eda_text.cpp:347
virtual void Report(const wxString &aText, SEVERITY aSeverity=RPT_SEVERITY_UNDEFINED) const
Definition io_base.cpp:124
Hold an error message and may be used when throwing exceptions containing meaningful error messages.
virtual const wxString What() const
A composite of Problem() and Where()
virtual const char * what() const override
std::exception interface, returned as UTF-8
A logical library item identifier and consists of various portions much like a URI.
Definition lib_id.h:45
int Parse(const UTF8 &aId, bool aFix=false)
Parse LIB_ID with the information from aId.
Definition lib_id.cpp:65
bool IsValid() const
Check if this LID_ID is valid.
Definition lib_id.h:168
void SetSubLibraryName(const UTF8 &aName)
Definition lib_id.h:127
const UTF8 & GetLibNickname() const
Return the logical library name portion of a LIB_ID.
Definition lib_id.h:83
static UTF8 FixIllegalChars(const UTF8 &aLibItemName, bool aLib)
Replace illegal LIB_ID item name characters with underscores '_'.
Definition lib_id.cpp:205
Define a library symbol object.
Definition lib_symbol.h:114
bool IsPower() const override
virtual LIB_SYMBOL * Duplicate() const
Create a copy of a LIB_SYMBOL and assigns unique KIIDs to the copy and its children.
Definition lib_symbol.h:130
wxString GetName() const override
Definition lib_symbol.h:183
bool IsMandatory() const
void SetOrdinal(int aOrdinal, FIELD_T aType)
Definition sch_field.h:149
FIELD_T GetId() const
Definition sch_field.h:143
void SetAutoAdded(bool aAutoAdded)
Definition sch_field.h:248
wxString GetName(bool aUseDefaultName=true) const
Return the field name (not translated).
void SetName(const wxString &aName)
void SetText(const wxString &aText) override
void SetNameShown(bool aShown=true)
Definition sch_field.h:230
void EnumerateSymbolLib(wxArrayString &aSymbolNameList, const wxString &aLibraryPath, const std::map< std::string, UTF8 > *aProperties=nullptr) override
Populate a list of LIB_SYMBOL alias names contained within the library aLibraryPath.
std::shared_mutex m_cacheMutex
std::unique_ptr< DATABASE_CONNECTION > m_conn
Generally will be null if no valid connection is established.
void GetDefaultSymbolFields(std::vector< wxString > &aNames) override
Retrieves a list of (custom) field names that should be shown by default for this library in the symb...
void ensureSettings(const wxString &aSettingsPath)
static thread_local std::unordered_set< wxString > s_inProgressLoads
LIB_IDs whose resolution is in flight on the calling thread, used to break self-referential cycles wh...
std::atomic< bool > m_cachePopulated
True once the LIB_SYMBOL cache has been materialized at least once.
bool TestConnection(wxString *aErrorMsg=nullptr)
bool m_inCacheLib
Re-entrancy guard for cacheLib(), tripped when a self-referential load routes back through the adapte...
std::map< wxString, std::unique_ptr< LIB_SYMBOL > > m_nameToSymbolcache
std::map< wxString, std::pair< std::string, std::string > > m_sanitizedNameMap
std::set< wxString > m_defaultShownFields
virtual ~SCH_IO_DATABASE()
size_t m_cacheSignature
Signature of the raw database rows at last materialization; used to skip rebuilding the LIB_SYMBOL ca...
std::vector< std::pair< const DATABASE_LIB_TABLE *, std::vector< DATABASE_CONNECTION::ROW > > > TABLE_RESULT_LIST
void GetSubLibraryNames(std::vector< wxString > &aNames) override
Retrieves a list of sub-libraries in this library.
std::unique_ptr< DATABASE_LIB_SETTINGS > m_settings
static std::optional< bool > boolFromAny(const std::any &aVal)
size_t computeSignature(const TABLE_RESULT_LIST &aTableResults) const
void CheckLibrary(const wxString &aLibraryPath, const std::map< std::string, UTF8 > *aProperties=nullptr) override
Validate that the library at aLibraryPath is reachable and well-formed, without necessarily loading s...
SYMBOL_LIBRARY_ADAPTER * m_adapter
std::thread m_refreshThread
long long m_cacheTimestamp
DIALOG_SHIM * CreateConfigurationDialog(wxWindow *aParent) override
std::condition_variable m_refreshCV
void GetAvailableSymbolFields(std::vector< wxString > &aNames) override
Retrieves a list of (custom) field names that are present on symbols in this library.
std::atomic< bool > m_refreshRunning
bool materializeCache(const TABLE_RESULT_LIST &aTableResults, std::map< wxString, std::unique_ptr< LIB_SYMBOL > > &aSymbolCache, std::map< wxString, std::pair< std::string, std::string > > &aSanitizedNameMap)
std::unique_ptr< LIB_SYMBOL > loadSymbolFromRow(const wxString &aSymbolName, const DATABASE_LIB_TABLE &aTable, const DATABASE_CONNECTION::ROW &aRow)
std::set< wxString > m_customFields
std::mutex m_symbolLoadMutex
LIB_SYMBOL * LoadSymbol(const wxString &aLibraryPath, const wxString &aAliasName, const std::map< std::string, UTF8 > *aProperties=nullptr) override
Load a LIB_SYMBOL object having aPartName from the aLibraryPath containing a library format that this...
std::mutex m_refreshMutex
SCH_IO(const wxString &aName)
Definition sch_io.h:406
Base class for any item which can be embedded within the SCHEMATIC container class,...
Definition sch_item.h:170
SCH_ITEM * Duplicate(bool addToParentGroup, SCH_COMMIT *aCommit=nullptr, bool aDoClone=false) const
Routine to create a new copy of given item.
Definition sch_item.cpp:191
virtual void SetBodyStyle(int aBodyStyle)
Definition sch_item.h:252
static const char * PropPowerSymsOnly
An 8 bit string that is assuredly encoded in UTF8, and supplies special conversion support to and fro...
Definition utf8.h:67
const char * c_str() const
Definition utf8.h:104
const char *const traceDatabase
#define _(s)
#define IGNORE_PARENT_GROUP
Definition eda_item.h:55
nlohmann::json json
Definition gerbview.cpp:50
#define THROW_IO_ERROR(msg)
macro which captures the "call site" values of FILE_, __FUNCTION & LINE
#define THROW_IO_ERRORF(msg,...)
static constexpr void hash_combine(std::size_t &seed)
This is a dummy function to take the final case of hash_combine below.
Definition hash.h:28
PIN_MAP MakeLegacyPinMap(const wxString &aName, const std::unordered_map< wxString, std::vector< wxString > > &aAssignments)
Build a single named PIN_MAP from a legacy symbol-pin to footprint-pad(s) assignment table (the flat ...
Definition pin_map.cpp:151
std::vector< ASSOCIATED_FOOTPRINT > ParseAssociatedFootprints(const nlohmann::json &aParent)
Parse the spec-form footprint associations from aParent["associated_footprints"] (issue #2282).
Definition pin_map.cpp:254
std::unordered_map< wxString, std::vector< wxString > > ParseLegacyPinAssignments(const nlohmann::json &aArray)
Parse the legacy flat pin-assignment JSON array (MR !2540 form) into a symbol-pin to footprint-pad(s)...
Definition pin_map.cpp:187
PIN_MAP_SET ParsePinMapSet(const nlohmann::json &aParent)
Parse the spec-form named maps from aParent["pin_maps"] into a PIN_MAP_SET (issue #2282).
Definition pin_map.cpp:221
@ RPT_SEVERITY_ERROR
static std::string strValue(double aValue)
bool visible_in_chooser
Whether the column is shown by default in the chooser.
std::string column
Database column name.
bool inherit_properties
Whether or not to inherit properties from symbol field.
bool visible_on_add
Whether to show the field when placing the symbol.
bool show_name
Whether or not to show the field name as well as its value.
wxString name_wx
KiCad field name (converted)
A database library table will be mapped to a sub-library provided by the database library entry in th...
std::string key_col
Unique key column name (will form part of the LIB_ID)
std::string name
KiCad library nickname (will form part of the LIB_ID)
std::string symbols_col
Column name containing KiCad symbol refs.
std::string footprints_col
Column name containing KiCad footprint refs.
std::vector< DATABASE_FIELD_MAPPING > fields
std::string table
Database table to pull content from.
MAPPABLE_SYMBOL_PROPERTIES properties
std::string pins_col
Column name containing JSON pin assignments (optional)
#define ALL_BODY_STYLES
Definition symbol.h:151
#define MANDATORY_FIELDS
FIELD_T
The set of all field indices assuming an array like sequence that a SCH_COMPONENT or LIB_PART can hol...
@ USER
The field ID hasn't been set yet; field is invalid.
std::string path
wxString result
Test unit parsing edge cases and error handling.
@ SCH_FIELD_T
Definition typeinfo.h:146