Skip to content

Commit 3b4e872

Browse files
sbroenneStefan Broenner
andauthored
fix(power-query): Handle COM error when regular Excel tables present (#303)
When Power Query commands (View/Update) iterated ListObjects, they failed with COM error 0x800A03EC when regular Excel tables were present because accessing .QueryTable on non-query tables throws. Changes: - PowerQueryCommands.View.cs: Wrap listObj.QueryTable access in try-catch, skip non-query tables - PowerQueryCommands.Update.cs: Wrap listObj.QueryTable access in try-catch, skip non-query tables - Add regression tests for View and Update with manual tables present - Verify ListObjects iteration is robust to mixed table types Regression tests: - View_WorkbookWithManualTable_ReturnsQueryDetails - Update_WorkbookWithManualTable_UpdatesQuerySuccessfully All Power Query integration tests pass (14 tests). Co-authored-by: Stefan Broenner <stefan.broenner@microsoft.comm>
1 parent 7a0de48 commit 3b4e872

File tree

4 files changed

+688
-2
lines changed

4 files changed

+688
-2
lines changed

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,19 @@ public void Update(IExcelBatch batch, string queryName, string mCode, bool refre
146146
try
147147
{
148148
listObj = listObjects.Item(lo);
149-
queryTable = listObj.QueryTable;
149+
150+
// NOTE: Accessing QueryTable on a regular Excel table (not from external data)
151+
// throws COMException 0x800A03EC. We must catch and skip such tables.
152+
try
153+
{
154+
queryTable = listObj.QueryTable;
155+
}
156+
catch (System.Runtime.InteropServices.COMException)
157+
{
158+
// Regular table without QueryTable - skip it
159+
continue;
160+
}
161+
150162
if (queryTable == null) continue;
151163

152164
wbConn = queryTable.WorkbookConnection;

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,19 @@ public PowerQueryViewResult View(IExcelBatch batch, string queryName)
149149
try
150150
{
151151
listObj = listObjects.Item(lo);
152-
queryTable = listObj.QueryTable;
152+
153+
// NOTE: Accessing QueryTable on a regular Excel table (not from external data)
154+
// throws COMException 0x800A03EC. We must catch and skip such tables.
155+
try
156+
{
157+
queryTable = listObj.QueryTable;
158+
}
159+
catch (System.Runtime.InteropServices.COMException)
160+
{
161+
// Regular table without QueryTable - skip it
162+
continue;
163+
}
164+
153165
if (queryTable == null) continue;
154166

155167
wbConn = queryTable.WorkbookConnection;

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

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,186 @@ public void List_WorkbookWithManualTable_ReturnsOnlyQueries()
111111
// Query should be connection-only (manual table shouldn't affect this)
112112
Assert.True(query.IsConnectionOnly);
113113
}
114+
115+
/// <summary>
116+
/// Regression test: Verifies View() handles workbooks with manually created tables
117+
/// Bug: View() was throwing COMException 0x800A03EC when iterating ListObjects
118+
/// because manually created tables don't have QueryTable property.
119+
/// Fix: View() now catches COMException when accessing ListObject.QueryTable and skips non-query tables.
120+
/// </summary>
121+
[Fact]
122+
public void View_WorkbookWithManualTable_ReturnsQueryDetails()
123+
{
124+
// Arrange
125+
var testFile = _fixture.CreateTestFile();
126+
127+
var dataModelCommands = new DataModelCommands();
128+
var commands = new PowerQueryCommands(dataModelCommands);
129+
130+
const string queryName = "TestQuery";
131+
const string mCode = @"let
132+
Source = #table(
133+
{""Column1"", ""Column2""},
134+
{
135+
{""A"", ""B""},
136+
{""C"", ""D""}
137+
}
138+
)
139+
in
140+
Source";
141+
142+
// Act
143+
using var batch = ExcelSession.BeginBatch(testFile);
144+
145+
// Step 1: Create a manually created table (no Power Query connection)
146+
batch.Execute((ctx, ct) =>
147+
{
148+
dynamic? sheet = null;
149+
dynamic? range = null;
150+
dynamic? listObjects = null;
151+
try
152+
{
153+
sheet = ctx.Book.Worksheets.Item(1);
154+
sheet.Name = "TestSheet";
155+
156+
// Add some data
157+
range = sheet.Range["A1:B3"];
158+
range.Value2 = new object[,]
159+
{
160+
{ "Header1", "Header2" },
161+
{ "Data1", "Data2" },
162+
{ "Data3", "Data4" }
163+
};
164+
165+
// Create a manual table (ListObject) - NO QueryTable
166+
listObjects = sheet.ListObjects;
167+
dynamic? listObject = listObjects.Add(
168+
1, // xlSrcRange (manual table from range)
169+
range, // Source range
170+
Type.Missing, // LinkSource
171+
1, // xlYes (has headers)
172+
Type.Missing // Destination
173+
);
174+
listObject.Name = "ManualTable";
175+
ComUtilities.Release(ref listObject!);
176+
}
177+
finally
178+
{
179+
ComUtilities.Release(ref listObjects!);
180+
ComUtilities.Release(ref range!);
181+
ComUtilities.Release(ref sheet!);
182+
}
183+
});
184+
185+
// Step 2: Create a Power Query (connection-only)
186+
commands.Create(batch, queryName, mCode, PowerQueryLoadMode.ConnectionOnly);
187+
188+
// Step 3: View the query - THIS WAS THROWING 0x800A03EC before the fix
189+
var result = commands.View(batch, queryName);
190+
191+
// Assert
192+
Assert.True(result.Success, $"View failed: {result.ErrorMessage}");
193+
Assert.Equal(queryName, result.QueryName);
194+
Assert.NotEmpty(result.MCode);
195+
Assert.Contains("Source = #table", result.MCode);
196+
197+
// Verify load destination detected correctly despite manual table presence
198+
Assert.True(result.IsConnectionOnly);
199+
}
200+
201+
/// <summary>
202+
/// Regression test: Verifies Update() handles workbooks with manually created tables
203+
/// Bug: Update() was throwing COMException 0x800A03EC when iterating ListObjects
204+
/// because manually created tables don't have QueryTable property.
205+
/// Fix: Update() now catches COMException when accessing ListObject.QueryTable and skips non-query tables.
206+
/// </summary>
207+
[Fact]
208+
public void Update_WorkbookWithManualTable_UpdatesQuerySuccessfully()
209+
{
210+
// Arrange
211+
var testFile = _fixture.CreateTestFile();
212+
213+
var dataModelCommands = new DataModelCommands();
214+
var commands = new PowerQueryCommands(dataModelCommands);
215+
216+
const string queryName = "TestQuery";
217+
const string originalMCode = @"let
218+
Source = #table(
219+
{""Column1"", ""Column2""},
220+
{
221+
{""A"", ""B""},
222+
{""C"", ""D""}
223+
}
224+
)
225+
in
226+
Source";
227+
228+
const string updatedMCode = @"let
229+
Source = #table(
230+
{""NewCol1"", ""NewCol2"", ""NewCol3""},
231+
{
232+
{1, 2, 3},
233+
{4, 5, 6}
234+
}
235+
)
236+
in
237+
Source";
238+
239+
// Act
240+
using var batch = ExcelSession.BeginBatch(testFile);
241+
242+
// Step 1: Create a manually created table (no Power Query connection)
243+
batch.Execute((ctx, ct) =>
244+
{
245+
dynamic? sheet = null;
246+
dynamic? range = null;
247+
dynamic? listObjects = null;
248+
try
249+
{
250+
sheet = ctx.Book.Worksheets.Item(1);
251+
sheet.Name = "TestSheet";
252+
253+
// Add some data
254+
range = sheet.Range["A1:B3"];
255+
range.Value2 = new object[,]
256+
{
257+
{ "Header1", "Header2" },
258+
{ "Data1", "Data2" },
259+
{ "Data3", "Data4" }
260+
};
261+
262+
// Create a manual table (ListObject) - NO QueryTable
263+
listObjects = sheet.ListObjects;
264+
dynamic? listObject = listObjects.Add(
265+
1, // xlSrcRange (manual table from range)
266+
range, // Source range
267+
Type.Missing, // LinkSource
268+
1, // xlYes (has headers)
269+
Type.Missing // Destination
270+
);
271+
listObject.Name = "ManualTable";
272+
ComUtilities.Release(ref listObject!);
273+
}
274+
finally
275+
{
276+
ComUtilities.Release(ref listObjects!);
277+
ComUtilities.Release(ref range!);
278+
ComUtilities.Release(ref sheet!);
279+
}
280+
});
281+
282+
// Step 2: Create a Power Query (connection-only)
283+
commands.Create(batch, queryName, originalMCode, PowerQueryLoadMode.ConnectionOnly);
284+
285+
// Step 3: Update the query - THIS WAS THROWING 0x800A03EC before the fix
286+
commands.Update(batch, queryName, updatedMCode);
287+
288+
// Step 4: Verify the update by viewing
289+
var viewResult = commands.View(batch, queryName);
290+
Assert.True(viewResult.Success, $"View after update failed: {viewResult.ErrorMessage}");
291+
Assert.Contains("NewCol1", viewResult.MCode);
292+
Assert.Contains("NewCol2", viewResult.MCode);
293+
Assert.Contains("NewCol3", viewResult.MCode);
294+
Assert.DoesNotContain("Column1", viewResult.MCode);
295+
}
114296
}

0 commit comments

Comments
 (0)