fix(transport): case-insensitive name lookups, batch preview lookups, correct flattener doc (ScadaBridge#93)
- ApplyTemplatesAsync/PreviewAsync template lookups use a case-insensitive, never-throwing comparer (SQL Server collation); base template wins over derived. Base names are already unique case-insensitively via the filtered unique index on Template.Name, derived templates are not covered by it. - Preview reads database connections, notification lists and API methods with one GetAll query each instead of one by-name query per bundled row. - Component-TemplateEngine.md: the flattener does not call GetAllTemplatesAsync; it loads chains via GetTemplateWithChildrenAsync. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -482,8 +482,7 @@ public sealed class BundleImporter : IBundleImporter
|
||||
var hydratedTemplates = await _templateRepo
|
||||
.GetTemplatesWithChildrenAsync(bundleTemplateNames, ct)
|
||||
.ConfigureAwait(false);
|
||||
var hydratedByName = hydratedTemplates
|
||||
.ToDictionary(t => t.Name, t => t, StringComparer.Ordinal);
|
||||
var hydratedByName = ToTemplateLookup(hydratedTemplates);
|
||||
var templateNameById = await library.GetNameByIdAsync(ct).ConfigureAwait(false);
|
||||
foreach (var tDto in content.Templates)
|
||||
{
|
||||
@@ -521,16 +520,24 @@ public sealed class BundleImporter : IBundleImporter
|
||||
}
|
||||
|
||||
// ---- DatabaseConnections ----
|
||||
// One read of the library instead of one by-name query per bundled row
|
||||
// (ScadaBridge#93); matched case-insensitively like the SQL Server collation.
|
||||
var dbConnectionByName = content.DatabaseConnections.Count == 0
|
||||
? ToNameLookup(Array.Empty<DatabaseConnectionDefinition>(), x => x.Name)
|
||||
: ToNameLookup(await _externalRepo.GetAllDatabaseConnectionsAsync(ct).ConfigureAwait(false), x => x.Name);
|
||||
foreach (var db in content.DatabaseConnections)
|
||||
{
|
||||
var existing = await _externalRepo.GetDatabaseConnectionByNameAsync(db.Name, ct).ConfigureAwait(false);
|
||||
dbConnectionByName.TryGetValue(db.Name, out var existing);
|
||||
items.Add(_diff.CompareDatabaseConnection(db, existing));
|
||||
}
|
||||
|
||||
// ---- NotificationLists ----
|
||||
var notificationListByName = content.NotificationLists.Count == 0
|
||||
? ToNameLookup(Array.Empty<NotificationList>(), x => x.Name)
|
||||
: ToNameLookup(await _notificationRepo.GetAllNotificationListsAsync(ct).ConfigureAwait(false), x => x.Name);
|
||||
foreach (var nl in content.NotificationLists)
|
||||
{
|
||||
var existing = await _notificationRepo.GetListByNameAsync(nl.Name, ct).ConfigureAwait(false);
|
||||
notificationListByName.TryGetValue(nl.Name, out var existing);
|
||||
items.Add(_diff.CompareNotificationList(nl, existing));
|
||||
}
|
||||
|
||||
@@ -560,9 +567,12 @@ public sealed class BundleImporter : IBundleImporter
|
||||
// from a hash). They are counted, ignored, and reported at apply time.
|
||||
|
||||
// ---- ApiMethods ----
|
||||
var apiMethodByName = content.ApiMethods.Count == 0
|
||||
? ToNameLookup(Array.Empty<ApiMethod>(), x => x.Name)
|
||||
: ToNameLookup(await _inboundApiRepo.GetAllApiMethodsAsync(ct).ConfigureAwait(false), x => x.Name);
|
||||
foreach (var m in content.ApiMethods)
|
||||
{
|
||||
var existing = await _inboundApiRepo.GetMethodByNameAsync(m.Name, ct).ConfigureAwait(false);
|
||||
apiMethodByName.TryGetValue(m.Name, out var existing);
|
||||
items.Add(_diff.CompareApiMethod(m, existing));
|
||||
}
|
||||
|
||||
@@ -2313,6 +2323,32 @@ public sealed class BundleImporter : IBundleImporter
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Name lookup that matches the way SQL Server's default case-insensitive
|
||||
/// collation matches (<see cref="StringComparer.OrdinalIgnoreCase"/>) and can never
|
||||
/// throw on a duplicate key: the first row for a name wins. Used wherever a
|
||||
/// by-name DB result is re-keyed in memory (ScadaBridge#93).
|
||||
/// </summary>
|
||||
internal static Dictionary<string, T> ToNameLookup<T>(IEnumerable<T> rows, Func<T, string> name)
|
||||
{
|
||||
var lookup = new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var row in rows)
|
||||
{
|
||||
lookup.TryAdd(name(row), row);
|
||||
}
|
||||
return lookup;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="ToNameLookup{T}"/> for templates. Base templates are unique by name
|
||||
/// (case-insensitively) through the filtered unique index on <c>Name</c>, but
|
||||
/// derived templates hold their composition slot name and are NOT in that index,
|
||||
/// so two rows can differ only in case. A base (user-authored) template wins over
|
||||
/// a derived one, which is the row an import actually targets.
|
||||
/// </summary>
|
||||
internal static Dictionary<string, Template> ToTemplateLookup(IEnumerable<Template> rows) =>
|
||||
ToNameLookup(rows.OrderBy(t => t.IsDerived), t => t.Name);
|
||||
|
||||
private async Task ApplyTemplatesAsync(
|
||||
IReadOnlyList<TemplateDto> dtos,
|
||||
Dictionary<(string, string), ImportResolution> map,
|
||||
@@ -2328,10 +2364,11 @@ public sealed class BundleImporter : IBundleImporter
|
||||
.Where(d => ResolveOrDefault(map, "Template", d.Name).Action == ResolutionAction.Overwrite)
|
||||
.Select(d => d.Name)
|
||||
.ToList();
|
||||
var byName = (overwriteNames.Count == 0
|
||||
? Array.Empty<Template>()
|
||||
: await _templateRepo.GetTemplatesWithChildrenAsync(overwriteNames, ct).ConfigureAwait(false))
|
||||
.ToDictionary(t => t.Name, t => t, StringComparer.Ordinal);
|
||||
// The by-name query runs under SQL Server's case-insensitive collation, so the
|
||||
// in-memory lookup must match case-insensitively too (ScadaBridge#93).
|
||||
var byName = ToTemplateLookup(overwriteNames.Count == 0
|
||||
? Array.Empty<Template>()
|
||||
: await _templateRepo.GetTemplatesWithChildrenAsync(overwriteNames, ct).ConfigureAwait(false));
|
||||
|
||||
// Folder ids must be materialised before we wire templates' FolderId
|
||||
// FKs. ApplyTemplateFoldersAsync staged the rows via AddFolderAsync
|
||||
|
||||
Reference in New Issue
Block a user