Skip to content

Commit ed5cf23

Browse files
committed
fix: use grid instead of cross product (tested with query containing 2 subqueries)
1 parent 0c1ba43 commit ed5cf23

2 files changed

Lines changed: 68 additions & 26 deletions

File tree

packages/salesforcedx-vscode-soql/src/commands/dataQuery.ts

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,11 @@ const isSubQueryResult = (
126126
};
127127

128128
/**
129-
* Flattens a record into one or more rows.
130-
* Sub-query results are expanded: each sub-record produces a separate output row.
131-
* Parent fields are shown only on the first child row; subsequent rows leave them blank,
132-
* matching the Salesforce CLI display style.
129+
* Flattens a record into one or more rows, matching the Salesforce CLI display style.
130+
* Each sub-query's first element shares row 0 with the parent fields and other
131+
* sub-queries' first elements. Remaining elements ("overflow") are stacked in
132+
* subsequent rows in sub-query order, with all other columns blank on those rows.
133+
* Total rows = 1 + sum(len - 1) for each sub-query — never a cross-product.
133134
* When a sub-query has no records the parent row is still emitted with empty sub-columns.
134135
*/
135136
const flattenRecord = (record: Record<string, unknown>): Record<string, unknown>[] => {
@@ -172,32 +173,25 @@ const flattenRecord = (record: Record<string, unknown>): Record<string, unknown>
172173
return [baseRow];
173174
}
174175

175-
// Cross-product of all sub-query expansions merged with the base row.
176-
// Multiple sub-queries in a single SELECT are rare but handled correctly.
177-
let rows: Record<string, unknown>[] = [baseRow];
176+
// Total rows = 1 + sum of (each sub-query's overflow = length - 1)
177+
const totalRows = subQueryExpansions.reduce((sum, exp) => sum + exp.length - 1, 1);
178+
179+
// Allocate the grid. Row 0 starts with the parent fields.
180+
const grid: Record<string, unknown>[] = Array.from({ length: totalRows }, () => ({}));
181+
Object.assign(grid[0], baseRow);
182+
183+
// Place each sub-query: element[0] → row 0, overflow elements → consecutive rows
184+
// starting immediately after all previous sub-queries' overflow rows.
185+
let overflowStart = 1;
178186
for (const expansion of subQueryExpansions) {
179-
const next: Record<string, unknown>[] = [];
180-
for (const existing of rows) {
181-
for (const expanded of expansion) {
182-
next.push({ ...existing, ...expanded });
183-
}
187+
Object.assign(grid[0], expansion[0]);
188+
for (let j = 1; j < expansion.length; j++) {
189+
Object.assign(grid[overflowStart + j - 1], expansion[j]);
184190
}
185-
rows = next;
191+
overflowStart += expansion.length - 1;
186192
}
187193

188-
// Blank out parent fields on all rows after the first, matching the Salesforce CLI
189-
// display style where repeated parent values are omitted for readability.
190-
const baseKeys = Object.keys(baseRow);
191-
return rows.map((row, i) => {
192-
if (i === 0 || baseKeys.length === 0) {
193-
return row;
194-
}
195-
const blanked = { ...row };
196-
for (const key of baseKeys) {
197-
blanked[key] = '';
198-
}
199-
return blanked;
200-
});
194+
return grid;
201195
};
202196

203197
/**

packages/salesforcedx-vscode-soql/test/jest/commands/dataQuery.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,54 @@ describe('DataQuery Pure Functions', () => {
446446
expect(output).not.toContain('[object Object]');
447447
});
448448

449+
it('should stack multiple sub-queries independently (not cross-product)', () => {
450+
// 2 Contacts + 3 Assets → 4 rows, not 6
451+
const records = [
452+
{
453+
Id: '001Rt00001iD52NIAS',
454+
Contacts: {
455+
totalSize: 2,
456+
done: true,
457+
records: [{ Id: '003a' }, { Id: '003b' }]
458+
},
459+
Assets: {
460+
totalSize: 3,
461+
done: true,
462+
records: [{ Id: '02i1' }, { Id: '02i2' }, { Id: '02i3' }]
463+
}
464+
}
465+
];
466+
const output = generateTableOutput(records, 'Test Table');
467+
468+
// Correct columns
469+
expect(output).toContain('Contacts.Id');
470+
expect(output).toContain('Assets.Id');
471+
472+
// All sub-records present
473+
expect(output).toContain('003a');
474+
expect(output).toContain('003b');
475+
expect(output).toContain('02i1');
476+
expect(output).toContain('02i2');
477+
expect(output).toContain('02i3');
478+
479+
// Parent Id appears exactly once (row 0 only)
480+
expect(output.match(/001Rt00001iD52NIAS/g)?.length).toBe(1);
481+
482+
// No cross-product duplication: each sub-record Id appears exactly once
483+
expect(output.match(/003a/g)?.length).toBe(1);
484+
expect(output.match(/003b/g)?.length).toBe(1);
485+
expect(output.match(/02i1/g)?.length).toBe(1);
486+
expect(output.match(/02i2/g)?.length).toBe(1);
487+
expect(output.match(/02i3/g)?.length).toBe(1);
488+
489+
// Exactly 4 data rows: 1 (shared first) + 1 (Contact overflow) + 2 (Asset overflow)
490+
// Data rows follow the ─── separator line; count non-empty lines after it.
491+
const lines = output.split('\n');
492+
const sepIdx = lines.findIndex(line => line.trim().startsWith('─'));
493+
const dataLines = lines.slice(sepIdx + 1).filter(line => line.trim().length > 0);
494+
expect(dataLines).toHaveLength(4);
495+
});
496+
449497
it('should emit the parent row with empty sub-columns when a sub-query has no records', () => {
450498
const records = [
451499
{ Id: '001a', Name: 'No Contacts', Contacts: { totalSize: 0, done: true, records: [] } },

0 commit comments

Comments
 (0)