Skip to content

Commit 85ca400

Browse files
authored
Fix Issue #170: LoadTo detects existing sheets and returns clear error (#171)
LoadTo silently failed when target worksheet existed - Excel's CreateQueryTableForQuery doesn't update query load configuration when sheet exists. Now detects conflicts and returns clear error requiring explicit sheet deletion before retry. Includes 6 regression tests, fixes CLI pack --no-build error, and corrects pre-commit script path warning.
1 parent c9643b4 commit 85ca400

File tree

7 files changed

+357
-23
lines changed

7 files changed

+357
-23
lines changed

scripts/check-mcp-core-implementations.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ $knownExceptions = @{
3232

3333
# Define mappings: Enum -> Core Interface File
3434
$mappings = @{
35-
"FileAction" = "src/ExcelMcp.Core/Commands/File/IFileCommands.cs"
35+
"FileAction" = "src/ExcelMcp.Core/Commands/IFileCommands.cs"
3636
"PowerQueryAction" = "src/ExcelMcp.Core/Commands/PowerQuery/IPowerQueryCommands.cs"
3737
"WorksheetAction" = "src/ExcelMcp.Core/Commands/Sheet/ISheetCommands.cs"
3838
"TableAction" = "src/ExcelMcp.Core/Commands/Table/ITableCommands.cs"

src/ExcelMcp.CLI/ExcelMcp.CLI.csproj

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,6 @@
4545
<DebugType>none</DebugType>
4646
<DebugSymbols>false</DebugSymbols>
4747

48-
<!-- Generate XML docs for analyzers but exclude from output -->
49-
<GenerateDocumentationFile>true</GenerateDocumentationFile>
50-
5148
<!-- Additional optimization flags -->
5249
<Optimize>true</Optimize>
5350
<TieredCompilation>true</TieredCompilation>
@@ -65,6 +62,14 @@
6562
<Message Text="Removed XML documentation and PDB files from Release build" Importance="high" />
6663
</Target>
6764

65+
<!-- Prevent dotnet pack/publish from expecting XML docs after they're deleted -->
66+
<Target Name="RemoveDocumentationFileFromPublish" BeforeTargets="ComputeFilesToPublish">
67+
<ItemGroup>
68+
<ResolvedFileToPublish Remove="@(ResolvedFileToPublish)" Condition="'%(Extension)' == '.xml'" />
69+
<_IntermediateAssembly Remove="@(_IntermediateAssembly)" Condition="'%(Extension)' == '.xml'" />
70+
</ItemGroup>
71+
</Target>
72+
6873
<ItemGroup>
6974
<PackageReference Include="Spectre.Console" />
7075
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers">

src/ExcelMcp.Core/Commands/PowerQuery/PowerQueryCommands.Lifecycle.cs

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,7 +1028,46 @@ public async Task<PowerQueryLoadResult> LoadToAsync(
10281028
switch (loadMode)
10291029
{
10301030
case PowerQueryLoadMode.LoadToTable:
1031-
sheet = ctx.Book.Worksheets.Item(targetSheet!);
1031+
// Check if sheet already exists - require explicit deletion by user
1032+
// Bug fix for Issue #170: Prevent silent failures when sheet exists
1033+
dynamic? worksheetsCheck = null;
1034+
try
1035+
{
1036+
worksheetsCheck = ctx.Book.Worksheets;
1037+
try
1038+
{
1039+
dynamic? existingSheet = worksheetsCheck.Item(targetSheet!);
1040+
if (existingSheet != null)
1041+
{
1042+
ComUtilities.Release(ref existingSheet!);
1043+
result.Success = false;
1044+
result.ErrorMessage = $"Cannot load query to sheet '{targetSheet}': worksheet already exists.";
1045+
return result;
1046+
}
1047+
}
1048+
catch (COMException)
1049+
{
1050+
// Sheet doesn't exist - this is expected, continue
1051+
}
1052+
}
1053+
finally
1054+
{
1055+
ComUtilities.Release(ref worksheetsCheck!);
1056+
}
1057+
1058+
// Create new sheet for query data
1059+
dynamic? worksheetsLoadToTable = null;
1060+
try
1061+
{
1062+
worksheetsLoadToTable = ctx.Book.Worksheets;
1063+
sheet = worksheetsLoadToTable.Add();
1064+
sheet.Name = targetSheet;
1065+
}
1066+
finally
1067+
{
1068+
ComUtilities.Release(ref worksheetsLoadToTable!);
1069+
}
1070+
10321071
queryTable = CreateQueryTableForQuery(sheet, query);
10331072
queryTable.Refresh(false);
10341073
result.ConfigurationApplied = true;
@@ -1072,7 +1111,46 @@ public async Task<PowerQueryLoadResult> LoadToAsync(
10721111
break;
10731112

10741113
case PowerQueryLoadMode.LoadToBoth:
1075-
sheet = ctx.Book.Worksheets.Item(targetSheet!);
1114+
// Check if sheet already exists - require explicit deletion by user
1115+
// Bug fix for Issue #170: Prevent silent failures when sheet exists
1116+
dynamic? worksheetsCheckBoth = null;
1117+
try
1118+
{
1119+
worksheetsCheckBoth = ctx.Book.Worksheets;
1120+
try
1121+
{
1122+
dynamic? existingSheet = worksheetsCheckBoth.Item(targetSheet!);
1123+
if (existingSheet != null)
1124+
{
1125+
ComUtilities.Release(ref existingSheet!);
1126+
result.Success = false;
1127+
result.ErrorMessage = $"Cannot load query to sheet '{targetSheet}': worksheet already exists.";
1128+
return result;
1129+
}
1130+
}
1131+
catch (COMException)
1132+
{
1133+
// Sheet doesn't exist - this is expected, continue
1134+
}
1135+
}
1136+
finally
1137+
{
1138+
ComUtilities.Release(ref worksheetsCheckBoth!);
1139+
}
1140+
1141+
// Create new sheet for query data
1142+
dynamic? worksheetsLoadToBoth = null;
1143+
try
1144+
{
1145+
worksheetsLoadToBoth = ctx.Book.Worksheets;
1146+
sheet = worksheetsLoadToBoth.Add();
1147+
sheet.Name = targetSheet;
1148+
}
1149+
finally
1150+
{
1151+
ComUtilities.Release(ref worksheetsLoadToBoth!);
1152+
}
1153+
10761154
queryTable = CreateQueryTableForQuery(sheet, query);
10771155
queryTable.Refresh(false);
10781156

src/ExcelMcp.McpServer/Prompts/Content/excel_powerquery.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,24 @@
33
**Action disambiguation**:
44
- create: Import NEW query (FAILS if query already exists - use update instead)
55
- update: Update EXISTING query M code + refresh data (use this if query exists)
6-
- load-to: Applies destination + refreshes (not just config change)
6+
- load-to: Applies destination + refreshes (not just config change) - CHECKS for sheet conflicts
77
- unload: Removes data but keeps query definition (inverse of load-to)
88

99
**When to use create vs update**:
1010
- Query doesn't exist? → Use create
1111
- Query already exists? → Use update (create will error "already exists")
1212
- Not sure? → Check with list action first, then use update if exists or create if new
1313

14+
**LoadTo with existing sheets (Issue #170 fix)**:
15+
- If target worksheet already exists, LoadTo returns clear error
16+
- User must delete existing sheet first using excel_worksheet action='Delete'
17+
- Then retry LoadTo - ensures explicit user control over data deletion
18+
- Example workflow: LoadTo fails → Delete sheet manually → Retry LoadTo succeeds
19+
1420
**Common mistakes**:
1521
- Using create on existing query → ERROR "Query 'X' already exists" (should use update)
1622
- Using update on new query → ERROR "Query 'X' not found" (should use create)
23+
- Calling LoadTo without checking if sheet exists (will error if sheet exists)
1724

1825
**Server-specific quirks**:
1926
- Validation = execution: M code only validated when data loads/refreshes

src/ExcelMcp.McpServer/Tools/ExcelPowerQueryTool.cs

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ LOAD DESTINATIONS (loadDestination parameter):
3131
- 'both': Load to BOTH worksheet AND Data Model
3232
- 'connection-only': Don't load data (M code imported but not executed)
3333
34+
⚠️ SHEET NAME CONFLICTS (LoadTo action):
35+
- If a worksheet with the target name already exists, LoadTo returns an error
36+
- User must delete the existing sheet first using excel_worksheet action='Delete'
37+
- Then retry LoadTo - this ensures explicit user control over data deletion
38+
3439
WHEN TO USE CREATE vs UPDATE:
3540
- Create: For NEW queries only (FAILS with 'already exists' error if query exists)
3641
- Update: For EXISTING queries (updates M code + refreshes data)
@@ -445,6 +450,10 @@ private static async Task<string> LoadToPowerQueryAsync(
445450
save: true,
446451
async (batch) => await commands.LoadToAsync(batch, queryName, loadMode, targetSheet));
447452

453+
// Detect sheet conflict error and provide specific guidance
454+
var isSheetConflict = !result.Success &&
455+
result.ErrorMessage?.Contains("worksheet already exists", StringComparison.OrdinalIgnoreCase) == true;
456+
448457
// Add workflow hints
449458
var inBatch = !string.IsNullOrEmpty(batchId);
450459
var destinationName = loadMode switch
@@ -467,7 +476,9 @@ private static async Task<string> LoadToPowerQueryAsync(
467476
result.RowsLoaded,
468477
workflowHint = result.Success
469478
? $"Query '{queryName}' now loaded to {destinationName}. Data refreshed with {result.RowsLoaded} rows."
470-
: $"Failed to load query '{queryName}': {result.ErrorMessage}",
479+
: isSheetConflict
480+
? $"Cannot load query '{queryName}': sheet '{targetSheet ?? queryName}' already exists. Delete it first, then retry LoadTo."
481+
: $"Failed to load query '{queryName}': {result.ErrorMessage}",
471482
suggestedNextActions = result.Success
472483
? (loadMode == PowerQueryLoadMode.LoadToDataModel || loadMode == PowerQueryLoadMode.LoadToBoth
473484
? new[]
@@ -484,12 +495,19 @@ private static async Task<string> LoadToPowerQueryAsync(
484495
"Use excel_table 'create' to convert range to Excel Table for filtering/sorting",
485496
inBatch ? "Load more queries in this batch" : "Loading multiple queries? Use excel_batch for efficiency"
486497
])
487-
:
488-
[
489-
"Check if query name is correct with excel_powerquery 'list'",
490-
"Verify query is connection-only with excel_powerquery 'get-load-config'",
491-
"Review error message for specific issue"
492-
]
498+
: isSheetConflict
499+
?
500+
[
501+
$"Use excel_worksheet action='Delete' sheetName='{targetSheet ?? queryName}' to delete the existing sheet",
502+
$"Then retry: excel_powerquery action='LoadTo' queryName='{queryName}' loadDestination='{loadDestination ?? "worksheet"}' targetSheet='{targetSheet ?? queryName}'",
503+
"Or rename the target sheet if you want to keep existing data"
504+
]
505+
:
506+
[
507+
"Check if query name is correct with excel_powerquery 'list'",
508+
"Verify query is connection-only with excel_powerquery 'get-load-config'",
509+
"Review error message for specific issue"
510+
]
493511
}, ExcelToolsBase.JsonOptions);
494512
}
495513

tests/ExcelMcp.Core.Tests/Integration/Commands/PowerQuery/PowerQueryCommandsTests.LoadDestination.cs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -215,17 +215,14 @@ public async Task LoadTo_ConnectionOnlyToTable_LoadsDataSuccessfully()
215215
var mCodeFile = CreateUniqueTestQueryFile(nameof(LoadTo_ConnectionOnlyToTable_LoadsDataSuccessfully));
216216
var targetSheet = "LoadSheet";
217217

218+
// Act - Create connection-only query and LoadTo (all in one batch)
219+
await using var batch = await ExcelSession.BeginBatchAsync(testFile);
220+
218221
// Create connection-only query first
219-
await using (var setupBatch = await ExcelSession.BeginBatchAsync(testFile))
220-
{
221-
await _powerQueryCommands.CreateAsync(
222-
setupBatch, queryName, mCodeFile, PowerQueryLoadMode.ConnectionOnly);
223-
await _sheetCommands.CreateAsync(setupBatch, targetSheet);
224-
await setupBatch.SaveAsync();
225-
}
222+
await _powerQueryCommands.CreateAsync(
223+
batch, queryName, mCodeFile, PowerQueryLoadMode.ConnectionOnly);
226224

227-
// Act
228-
await using var batch = await ExcelSession.BeginBatchAsync(testFile);
225+
// LoadTo should create sheet and load data
229226
var loadResult = await _powerQueryCommands.LoadToAsync(
230227
batch, queryName, PowerQueryLoadMode.LoadToTable, targetSheet);
231228

0 commit comments

Comments
 (0)