Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions src/controllers/queryRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1250,9 +1250,12 @@ export default class QueryRunner {

let cellObjects = row.slice(rowSelection.fromCell, rowSelection.toCell + 1);
let cells = cellObjects.map((x) => {
let displayValue = this.shouldRemoveNewLines()
? this.removeNewLines(x.displayValue)
: x.displayValue;
// For null values, use empty string instead of the displayValue (which contains "null")
let displayValue = x.isNull
? ""
: this.shouldRemoveNewLines()
? this.removeNewLines(x.displayValue)
: x.displayValue;
return this.escapeCsvValue(displayValue, textIdentifier);
});

Expand Down Expand Up @@ -1334,12 +1337,18 @@ export default class QueryRunner {

let cellObjects = row.slice(rowSelection.fromCell, rowSelection.toCell + 1);
for (let cellObject of cellObjects) {
let displayValue = this.shouldRemoveNewLines()
? this.removeNewLines(cellObject.displayValue)
: cellObject.displayValue;

// Try to parse numeric and boolean values
let value = this.parseJsonValue(displayValue);
let value: any;
if (cellObject.isNull) {
// For null values, use proper JSON null instead of parsing displayValue
value = null;
} else {
let displayValue = this.shouldRemoveNewLines()
? this.removeNewLines(cellObject.displayValue)
: cellObject.displayValue;

// Try to parse numeric and boolean values
value = this.parseJsonValue(displayValue);
}
jsonObject[columnHeaders[columnIndex]] = value;
columnIndex++;
}
Expand Down
261 changes: 245 additions & 16 deletions test/unit/queryRunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -981,25 +981,9 @@ suite("Query Runner tests", () => {
};

setup(() => {
testSqlToolsServerClient
.setup((x) => x.sendRequest(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.callback(() => {
// testing
})
.returns(() => {
return Promise.resolve(testresult);
});
testStatusView.setup((x) => x.executingQuery(TypeMoq.It.isAnyString()));
testStatusView.setup((x) => x.executedQuery(TypeMoq.It.isAnyString()));
testVscodeWrapper.setup((x) => x.logToOutputChannel(TypeMoq.It.isAnyString()));
testVscodeWrapper
.setup((x) => x.clipboardWriteText(TypeMoq.It.isAnyString()))
.callback(() => {
// testing
})
.returns(() => {
return Promise.resolve();
});
});

function setupMockConfig(): void {
Expand All @@ -1012,6 +996,11 @@ suite("Query Runner tests", () => {

// ------ Copy tests with multiple selections -------
test("Correctly copy pastes a selection", async () => {
testSqlToolsServerClient
.setup((x) => x.sendRequest(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(() => {
return Promise.resolve(testresult);
});
setupMockConfig();
let queryRunner = new QueryRunner(
testuri,
Expand Down Expand Up @@ -1041,6 +1030,11 @@ suite("Query Runner tests", () => {
});

test("Copies selection with column headers set in user config", async () => {
testSqlToolsServerClient
.setup((x) => x.sendRequest(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(() => {
return Promise.resolve(testresult);
});
setupMockConfig();
// Set column headers in the user config settings
let queryRunner = new QueryRunner(
Expand Down Expand Up @@ -1072,6 +1066,11 @@ suite("Query Runner tests", () => {
});

test("Copies selection with headers when true passed as parameter", async () => {
testSqlToolsServerClient
.setup((x) => x.sendRequest(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(() => {
return Promise.resolve(testresult);
});
setupMockConfig();
// Do not set column config in user settings
let queryRunner = new QueryRunner(
Expand Down Expand Up @@ -1104,6 +1103,11 @@ suite("Query Runner tests", () => {
});

test("Copies selection without headers when false passed as parameter", async () => {
testSqlToolsServerClient
.setup((x) => x.sendRequest(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(() => {
return Promise.resolve(testresult);
});
setupMockConfig();
// Set column config in user settings
let queryRunner = new QueryRunner(
Expand Down Expand Up @@ -1136,6 +1140,11 @@ suite("Query Runner tests", () => {
});

test("Copies selection as CSV with headers", async () => {
testSqlToolsServerClient
.setup((x) => x.sendRequest(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(() => {
return Promise.resolve(testresult);
});
setupMockConfig();
let configResult: { [key: string]: any } = {};
configResult[Constants.configSaveAsCsv] = {
Expand All @@ -1162,7 +1171,111 @@ suite("Query Runner tests", () => {
);
});

test("Copies selection as CSV with null values", async () => {
let configResult: { [key: string]: any } = {};
configResult[Constants.configSaveAsCsv] = {
delimiter: ",",
textIdentifier: '"',
lineSeperator: "\n",
};

// Create test data with null values for CSV export
let testResultWithNulls: QueryExecuteSubsetResult = {
resultSubset: {
rowCount: 3,
rows: [
[
{ isNull: false, displayValue: "1" },
{ isNull: true, displayValue: "null" },
{ isNull: false, displayValue: "3" },
],
[
{ isNull: true, displayValue: "null" },
{ isNull: false, displayValue: "5" },
{ isNull: true, displayValue: "null" },
],
[
{ isNull: false, displayValue: "7" },
{ isNull: false, displayValue: "8" },
{ isNull: false, displayValue: "9" },
],
],
},
};

let testRangeWithNulls: ISlickRange[] = [
{ fromCell: 0, fromRow: 0, toCell: 2, toRow: 2 },
];

// Setup testSqlToolsServerClient to return null data
testSqlToolsServerClient
.setup((x) => x.sendRequest(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(() => Promise.resolve(testResultWithNulls));

// Setup configuration mock
let mockConfig = TypeMoq.Mock.ofType<vscode.WorkspaceConfiguration>();
mockConfig
.setup((c) => c.get(TypeMoq.It.isAnyString()))
.returns((key: string) => {
return configResult[key] || false;
});
testVscodeWrapper
.setup((x) => x.getConfiguration(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(() => mockConfig.object);
testVscodeWrapper
.setup((x) => x.getConfiguration(TypeMoq.It.isAny()))
.returns(() => mockConfig.object);

// Capture the CSV content
let capturedCsvContent: string = "";
testVscodeWrapper
.setup((x) => x.clipboardWriteText(TypeMoq.It.isAnyString()))
.callback((text: string) => {
capturedCsvContent = text;
})
.returns(() => Promise.resolve());

let queryRunner = new QueryRunner(
testuri,
testuri,
testStatusView.object,
testSqlToolsServerClient.object,
testQueryNotificationHandler.object,
testVscodeWrapper.object,
);
queryRunner.uri = testuri;
queryRunner.handleQueryComplete(result);

await queryRunner.copyResultsAsCsv(testRangeWithNulls, 0, 0, true);

testVscodeWrapper.verify<void>(
(x) => x.clipboardWriteText(TypeMoq.It.isAnyString()),
TypeMoq.Times.once(),
);

// Verify that null values are exported as empty strings, not "null"
console.log("Captured CSV content:", JSON.stringify(capturedCsvContent));
assert.ok(
capturedCsvContent.includes("1,,3"),
"First row should have empty value for null cell. Actual content: " +
capturedCsvContent,
);
assert.ok(
capturedCsvContent.includes(",5,"),
"Second row should have empty values for null cells",
);
assert.ok(
!capturedCsvContent.includes(",null,"),
"CSV should not contain literal 'null' strings",
);
});

test("Copies selection as JSON with headers", async () => {
testSqlToolsServerClient
.setup((x) => x.sendRequest(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(() => {
return Promise.resolve(testresult);
});
setupMockConfig();
let queryRunner = new QueryRunner(
testuri,
Expand All @@ -1181,6 +1294,122 @@ suite("Query Runner tests", () => {
TypeMoq.Times.once(),
);
});

test("Copies selection as JSON with null values", async () => {
setupMockConfig();

// Create test data with null values
let testResultWithNulls: QueryExecuteSubsetResult = {
resultSubset: {
rowCount: 2,
rows: [
[
{ isNull: false, displayValue: "1" },
{ isNull: true, displayValue: "null" },
{ isNull: false, displayValue: "test" },
],
[
{ isNull: true, displayValue: "null" },
{ isNull: false, displayValue: "42" },
{ isNull: true, displayValue: "null" },
],
],
},
};

let resultWithNulls: QueryExecuteCompleteNotificationResult = {
ownerUri: testuri,
batchSummaries: [
{
hasError: false,
id: 0,
selection: {
startLine: 0,
endLine: 0,
startColumn: 3,
endColumn: 3,
},
resultSetSummaries: <ResultSetSummary[]>[
{
id: 0,
rowCount: 2,
columnInfo: [
{ columnName: "Col1" },
{ columnName: "Col2" },
{ columnName: "Col3" },
],
},
],
executionElapsed: undefined,
executionStart: new Date().toISOString(),
executionEnd: new Date().toISOString(),
},
],
};

let testRangeWithNulls: ISlickRange[] = [
{ fromCell: 0, fromRow: 0, toCell: 2, toRow: 1 },
];

// Setup mock to capture the actual JSON content
let capturedJsonContent: string = "";
testVscodeWrapper
.setup((x) => x.clipboardWriteText(TypeMoq.It.isAnyString()))
.callback((text: string) => {
capturedJsonContent = text;
})
.returns(() => Promise.resolve());

testSqlToolsServerClient
.setup((x) => x.sendRequest(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(() => Promise.resolve(testResultWithNulls));

let queryRunner = new QueryRunner(
testuri,
testuri,
testStatusView.object,
testSqlToolsServerClient.object,
testQueryNotificationHandler.object,
testVscodeWrapper.object,
);
queryRunner.uri = testuri;
queryRunner.handleQueryComplete(resultWithNulls);

await queryRunner.copyResultsAsJson(testRangeWithNulls, 0, 0, true);

testVscodeWrapper.verify<void>(
(x) => x.clipboardWriteText(TypeMoq.It.isAnyString()),
TypeMoq.Times.once(),
);

// Verify that null values are exported as proper JSON null, not "null" strings
let jsonData;
try {
jsonData = JSON.parse(capturedJsonContent);
} catch (e) {
assert.fail(
`Generated JSON is invalid: ${e.message}. Content: ${capturedJsonContent}`,
);
}

assert.equal(jsonData.length, 2, "Should have 2 rows");

// First row: {Col1: 1, Col2: null, Col3: "test"}
assert.equal(jsonData[0].Col1, 1, "First row Col1 should be number 1");
assert.strictEqual(jsonData[0].Col2, null, "First row Col2 should be null");
assert.equal(jsonData[0].Col3, "test", "First row Col3 should be 'test'");

// Second row: {Col1: null, Col2: 42, Col3: null}
assert.strictEqual(jsonData[1].Col1, null, "Second row Col1 should be null");
assert.equal(jsonData[1].Col2, 42, "Second row Col2 should be number 42");
assert.strictEqual(jsonData[1].Col3, null, "Second row Col3 should be null");

// Ensure JSON string doesn't contain literal "null" strings
assert.ok(
!capturedJsonContent.includes('"null"'),
"JSON should not contain literal 'null' strings",
);
});
});
});

Expand Down