@@ -19,12 +19,24 @@ internal static class ProviderSource
1919 private const string DbExtension = ".db" ;
2020 private const string EvtxExtension = ".evtx" ;
2121
22+ /// <summary>
23+ /// Conservative cap on the number of parameters in a single <c>Where(... Contains)</c> SQL IN
24+ /// clause. SQLite's default limit is 999 parameters; we stay well under that so the same code
25+ /// works on older SQLite builds too. Larger requests are split into multiple round-trips.
26+ /// </summary>
27+ private const int MaxInClauseParameters = 500 ;
28+
2229 /// <summary>
2330 /// Returns the distinct provider names available from <paramref name="path" />, applying an optional
2431 /// case-insensitive regex <paramref name="filter" />. Does not load full provider details.
2532 /// </summary>
2633 public static IReadOnlyList < string > LoadProviderNames ( string path , ITraceLogger logger , string ? filter = null )
2734 {
35+ if ( ! RegexHelper . TryCreate ( filter , logger , out var regex ) )
36+ {
37+ return [ ] ;
38+ }
39+
2840 var names = new SortedSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
2941
3042 foreach ( var file in EnumerateSourceFiles ( path ) )
@@ -35,7 +47,9 @@ public static IReadOnlyList<string> LoadProviderNames(string path, ITraceLogger
3547 }
3648 }
3749
38- return ApplyFilter ( names , filter ) . ToList ( ) ;
50+ if ( regex is null ) { return names . ToList ( ) ; }
51+
52+ return names . Where ( n => regex . IsMatch ( n ) ) . ToList ( ) ;
3953 }
4054
4155 /// <summary>
@@ -49,19 +63,9 @@ public static IEnumerable<ProviderDetails> LoadProviders(
4963 string path ,
5064 ITraceLogger logger ,
5165 string ? filter = null ,
52- IReadOnlySet < string > ? skipProviderNames = null )
53- {
54- Regex ? regex = string . IsNullOrEmpty ( filter ) ? null : new Regex ( filter , RegexOptions . IgnoreCase ) ;
55- var seen = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
56-
57- foreach ( var file in EnumerateSourceFiles ( path ) )
58- {
59- foreach ( var details in LoadDetailsFromFile ( file , logger , regex , skipProviderNames , seen ) )
60- {
61- yield return details ;
62- }
63- }
64- }
66+ IReadOnlySet < string > ? skipProviderNames = null ) =>
67+ ! RegexHelper . TryCreate ( filter , logger , out var regex ) ? [ ] :
68+ LoadProvidersIterator ( path , logger , regex , skipProviderNames ) ;
6569
6670 /// <summary>Validates that <paramref name="path" /> exists and has a recognized form.</summary>
6771 public static bool TryValidate ( string path , ITraceLogger logger )
@@ -98,15 +102,6 @@ internal static bool ShouldInclude(
98102 return seen . Add ( providerName ) ;
99103 }
100104
101- private static IEnumerable < string > ApplyFilter ( IEnumerable < string > names , string ? filter )
102- {
103- if ( string . IsNullOrEmpty ( filter ) ) { return names ; }
104-
105- var regex = new Regex ( filter , RegexOptions . IgnoreCase ) ;
106-
107- return names . Where ( n => regex . IsMatch ( n ) ) ;
108- }
109-
110105 /// <summary>
111106 /// Expands <paramref name="path" /> into the ordered list of source files: a single .db or .evtx
112107 /// when given a file; or all *.db files (sorted) followed by all *.evtx files (sorted) when given a
@@ -155,14 +150,26 @@ private static IEnumerable<ProviderDetails> LoadDetailsFromFile(
155150
156151 if ( namesToLoad . Count == 0 ) { return [ ] ; }
157152
158- return ctx . ProviderDetails
159- . Where ( p => namesToLoad . Contains ( p . ProviderName ) )
160- . ToList ( ) ;
153+ // Chunk the IN-clause to stay below SQLite's parameter limit (default 999). This keeps
154+ // the optimization in finding #4 working for source DBs with thousands of providers.
155+ var loaded = new List < ProviderDetails > ( namesToLoad . Count ) ;
156+
157+ for ( var offset = 0 ; offset < namesToLoad . Count ; offset += MaxInClauseParameters )
158+ {
159+ var chunk = namesToLoad
160+ . Skip ( offset )
161+ . Take ( MaxInClauseParameters )
162+ . ToList ( ) ;
163+
164+ loaded . AddRange ( ctx . ProviderDetails . Where ( p => chunk . Contains ( p . ProviderName ) ) ) ;
165+ }
166+
167+ return loaded ;
161168 }
162169
163170 if ( string . Equals ( ext , EvtxExtension , StringComparison . OrdinalIgnoreCase ) )
164171 {
165- return MtaProviderSource . LoadProviders ( file , logger , regex , skipProviderNames , seen ) . ToList ( ) ;
172+ return MtaProviderSource . LoadProviders ( file , logger , regex , skipProviderNames , seen ) ;
166173 }
167174
168175 logger . Warn ( $ "Skipping unsupported source file: { file } ") ;
@@ -190,4 +197,21 @@ private static IEnumerable<string> LoadNamesFromFile(string file, ITraceLogger l
190197
191198 return [ ] ;
192199 }
200+
201+ private static IEnumerable < ProviderDetails > LoadProvidersIterator (
202+ string path ,
203+ ITraceLogger logger ,
204+ Regex ? regex ,
205+ IReadOnlySet < string > ? skipProviderNames )
206+ {
207+ var seen = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
208+
209+ foreach ( var file in EnumerateSourceFiles ( path ) )
210+ {
211+ foreach ( var details in LoadDetailsFromFile ( file , logger , regex , skipProviderNames , seen ) )
212+ {
213+ yield return details ;
214+ }
215+ }
216+ }
193217}
0 commit comments