]*>([\s\S]*?)<\/tr>/gi;
+ const rows: any[][] = [];
+
+ let rowMatch;
+ let startFromSecondRow = false; // Skip header row
+
+ while ((rowMatch = rowRegex.exec(tableHtml)) !== null) {
+ if (!startFromSecondRow) {
+ startFromSecondRow = true;
+ continue; // Skip header row
+ }
+
+ const rowHtml = rowMatch[1];
+ const cellRegex = /]*>(.*?)<\/td>/gi;
+ const rowData: any[] = [];
+
+ let cellMatch;
+ while ((cellMatch = cellRegex.exec(rowHtml)) !== null) {
+ let value = cellMatch[1].trim();
+ // Convert NULL string to null
+ if (value === 'NULL') {
+ rowData.push(null);
+ } else {
+ // Try to convert to number if applicable
+ const numValue = Number(value);
+ rowData.push(isNaN(numValue) ? value : numValue);
+ }
+ }
+
+ rows.push(rowData);
+ }
+
+ return { headers, rows };
+ } catch (error) {
+ console.error('Error extracting table data from HTML:', error);
+ return null;
+ }
+ }
+
+ private _formatAsCsv(data: { headers: string[], rows: any[][] }): string {
+ const { headers, rows } = data;
+
+ // Escape and quote function for CSV
+ const escapeCSV = (value: any): string => {
+ if (value === null || value === undefined) return '';
+ const strValue = String(value);
+ // If the value contains a comma, quote, or newline, wrap it in quotes and escape existing quotes
+ if (strValue.includes(',') || strValue.includes('"') || strValue.includes('\n')) {
+ return `"${strValue.replace(/"/g, '""')}"`;
+ }
+ return strValue;
+ };
+
+ // Format the headers
+ const csvRows = [
+ headers.map(escapeCSV).join(',')
+ ];
+
+ // Format each row
+ for (const row of rows) {
+ csvRows.push(row.map(escapeCSV).join(','));
+ }
+
+ return csvRows.join('\n');
+ }
+
+ private _formatAsJson(data: { headers: string[], rows: any[][] }): string {
+ const { headers, rows } = data;
+
+ // Convert to array of objects with named properties
+ const jsonData = rows.map(row => {
+ const obj: Record = {};
+ headers.forEach((header, index) => {
+ obj[header] = row[index];
+ });
+ return obj;
+ });
+
+ return JSON.stringify(jsonData, null, 2);
+ }
+
+ private _setNotebookCellLimit(cell: vscode.NotebookCell, limitValue: number): void {
+ // We use VSCode's cell metadata to store our limit value
+ const edit = new vscode.WorkspaceEdit();
+ const cellMetadata = { ...cell.metadata, sqltools_limit: limitValue };
+
+ const nbEdit = vscode.NotebookEdit.updateCellMetadata(cell.index, cellMetadata);
+ edit.set(cell.notebook.uri, [nbEdit]);
+
+ vscode.workspace.applyEdit(edit);
+ }
+
+ private _setCellConnection(cell: vscode.NotebookCell, connectionId: string): void {
+ // Store the connection ID in cell metadata
+ const edit = new vscode.WorkspaceEdit();
+ const cellMetadata = { ...cell.metadata, sqltools_connection: connectionId };
+
+ const nbEdit = vscode.NotebookEdit.updateCellMetadata(cell.index, cellMetadata);
+ edit.set(cell.notebook.uri, [nbEdit]);
+
+ vscode.workspace.applyEdit(edit);
+ }
+
+ // Method to apply limit to a query if needed
+ private _applyLimitToQuery(query: string, limitValue: number, driver?: string): string {
+ if (!limitValue || limitValue <= 0) return query;
+
+ // Skip adding LIMIT for non-SELECT queries or if there's already a LIMIT clause
+ const trimmedQuery = query.trim().toLowerCase();
+
+ // Check if it's an action query (non-SELECT)
+ if (!trimmedQuery.startsWith('select')) return query;
+
+ // Check if query already has a LIMIT clause
+ if (trimmedQuery.includes(' limit ')) return query;
+
+ // Remove trailing semicolon before adding LIMIT
+ let processedQuery = query.trim();
+ if (processedQuery.endsWith(';')) {
+ processedQuery = processedQuery.slice(0, -1);
+ }
+
+ // Add LIMIT based on database driver
+ switch (driver?.toLowerCase()) {
+ case 'mssql':
+ // For SQL Server, we use a different approach
+ if (trimmedQuery.includes(' top ')) return query;
+ // Add TOP clause for SQL Server
+ return processedQuery.replace(/select\s+/i, `SELECT TOP ${limitValue} `);
+ default:
+ // Most databases use a LIMIT clause
+ return `${processedQuery} LIMIT ${limitValue}`;
+ }
+ }
+
+ // Add a method to set cell connection based on last used connection
+ private _setDefaultConnectionForNewCell(cell: vscode.NotebookCell): void {
+ // Only set default connection for SQL code cells that don't already have a connection
+ if (cell.kind !== vscode.NotebookCellKind.Code ||
+ cell.document.languageId !== 'sql' ||
+ cell.metadata?.sqltools_connection) {
+ return;
+ }
+
+ // If there's a last used connection, apply it to this cell
+ if (this._lastUsedConnection) {
+ this._setCellConnection(cell, this._lastUsedConnection);
+ }
+ }
+
+ private async _selectConnection(): Promise {
+ try {
+ // Get available connections with proper parameters
+ // Changed to connectedOnly: false to show all available connections
+ const connections = await vscode.commands.executeCommand(
+ `${EXT_NAMESPACE}.getConnections`,
+ { connectedOnly: false, sort: 'connectedFirst' }
+ ) || [];
+
+ if (connections.length === 0) {
+ vscode.window.showErrorMessage('No database connections available. Please add a database connection first.');
+ return null;
+ }
+
+ // Create connection items with proper ID extraction and more details
+ const connectionItems = connections.map(conn => {
+ // Extract connection ID and name
+ const id = typeof conn === 'string' ? conn : conn.id || conn.name;
+ const name = typeof conn === 'string' ? conn : conn.name || conn.id;
+ const driver = typeof conn === 'string' ? '' : conn.driver || '';
+ const server = typeof conn === 'string' ? '' : conn.server || conn.host || '';
+ const database = typeof conn === 'string' ? '' : conn.database || '';
+ const isConnected = typeof conn === 'string' ? false : conn.isConnected || false;
+
+ // Create a description that shows more connection details
+ let description = '';
+ if (driver) description += driver;
+ if (server) description += description ? ` - ${server}` : server;
+ if (database) description += description ? ` - ${database}` : database;
+ if (isConnected) description += ' (Connected)';
+
+ return {
+ label: name,
+ description: description,
+ id: id,
+ isConnected: isConnected
+ };
+ });
+
+ // Show connection picker with improved descriptions
+ const selectedConn = await vscode.window.showQuickPick(connectionItems, {
+ placeHolder: 'Select a database connection for this notebook',
+ title: 'SQLTools: Select Connection for Notebook',
+ });
+
+ if (selectedConn) {
+ console.log(`SQLTools Notebook: Selected connection ID: ${selectedConn.id}`);
+
+ // If the connection is not already connected, connect to it
+ if (!selectedConn.isConnected) {
+ vscode.window.showInformationMessage(`Connecting to ${selectedConn.label}...`);
+ // Use the selectConnection command to connect to the database
+ await vscode.commands.executeCommand(`${EXT_NAMESPACE}.selectConnection`, selectedConn.id);
+ }
+
+ vscode.window.showInformationMessage(
+ `Using connection: ${selectedConn.label}`,
+ { modal: false }
+ );
+ return selectedConn.id;
+ }
+
+ return null;
+ } catch (error) {
+ console.error('SQLTools Notebook: Error selecting connection:', error);
+ vscode.window.showErrorMessage(`Error selecting connection: ${error}`);
+ return null;
+ }
+ }
+
+ dispose() {
+ console.log('SQLTools Notebook: Disposing controller');
+ this._controller.dispose();
+ }
+}
\ No newline at end of file
diff --git a/packages/extension/src/notebook/index.ts b/packages/extension/src/notebook/index.ts
new file mode 100644
index 000000000..0ac607e00
--- /dev/null
+++ b/packages/extension/src/notebook/index.ts
@@ -0,0 +1,37 @@
+import * as vscode from 'vscode';
+import { SQLNotebookSerializer } from './serializer';
+import { SQLNotebookController } from './controller';
+
+let notebookController: SQLNotebookController | undefined;
+
+export function registerSQLNotebook(context: vscode.ExtensionContext): void {
+ // Register the notebook serializer
+ const notebookType = 'sqltools-notebook';
+ const serializer = new SQLNotebookSerializer();
+
+ context.subscriptions.push(
+ vscode.workspace.registerNotebookSerializer(notebookType, serializer)
+ );
+
+ // Register the notebook controller
+ notebookController = new SQLNotebookController();
+
+ // Add to subscriptions for proper cleanup
+ context.subscriptions.push({
+ dispose: () => {
+ if (notebookController) {
+ notebookController.dispose();
+ notebookController = undefined;
+ }
+ }
+ });
+
+ console.log('SQLTools Notebook feature registered successfully');
+}
+
+export function deactivateSQLNotebook(): void {
+ if (notebookController) {
+ notebookController.dispose();
+ notebookController = undefined;
+ }
+}
\ No newline at end of file
diff --git a/packages/extension/src/notebook/serializer.ts b/packages/extension/src/notebook/serializer.ts
new file mode 100644
index 000000000..1fae7eabd
--- /dev/null
+++ b/packages/extension/src/notebook/serializer.ts
@@ -0,0 +1,72 @@
+import { TextDecoder, TextEncoder } from 'util';
+import * as vscode from 'vscode';
+
+interface SQLNotebookCell {
+ kind: vscode.NotebookCellKind;
+ value: string;
+ language: string;
+}
+
+interface SQLNotebook {
+ cells: SQLNotebookCell[];
+}
+
+export class SQLNotebookSerializer implements vscode.NotebookSerializer {
+ async deserializeNotebook(
+ content: Uint8Array,
+ _token: vscode.CancellationToken
+ ): Promise {
+ try {
+ const contents = new TextDecoder().decode(content);
+
+ let raw: SQLNotebookCell[];
+ try {
+ const parsed = JSON.parse(contents);
+ raw = parsed.cells || [];
+ console.log(`SQLTools Notebook: Deserialized notebook with ${raw.length} cells`);
+ } catch (parseError) {
+ console.error('SQLTools Notebook: Error parsing notebook content:', parseError);
+ // Return an empty notebook if we can't parse the content
+ raw = [];
+ }
+
+ const cells = raw.map(
+ item => new vscode.NotebookCellData(
+ item.kind,
+ item.value,
+ item.language
+ )
+ );
+
+ return new vscode.NotebookData(cells);
+ } catch (error) {
+ console.error('SQLTools Notebook: Error deserializing notebook:', error);
+ // Return an empty notebook on error
+ return new vscode.NotebookData([]);
+ }
+ }
+
+ async serializeNotebook(
+ data: vscode.NotebookData,
+ _token: vscode.CancellationToken
+ ): Promise {
+ try {
+ const contents: SQLNotebookCell[] = [];
+
+ for (const cell of data.cells) {
+ contents.push({
+ kind: cell.kind,
+ value: cell.value,
+ language: cell.languageId
+ });
+ }
+
+ console.log(`SQLTools Notebook: Serialized notebook with ${contents.length} cells`);
+ return new TextEncoder().encode(JSON.stringify({ cells: contents }));
+ } catch (error) {
+ console.error('SQLTools Notebook: Error serializing notebook:', error);
+ // Return empty notebook data on error
+ return new TextEncoder().encode(JSON.stringify({ cells: [] }));
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/plugins/connection-manager/extension.ts b/packages/plugins/connection-manager/extension.ts
index 279473e80..6a154756d 100644
--- a/packages/plugins/connection-manager/extension.ts
+++ b/packages/plugins/connection-manager/extension.ts
@@ -255,7 +255,7 @@ export class ConnectionManagerPlugin implements IExtensionPlugin {
return query;
}
- private ext_executeQuery = async (query?: string, { connNameOrId, connId, ...opt }: IQueryOptions = {}) => {
+ private ext_executeQuery = async (query?: string, { connNameOrId, connId, runInNotebook, ...opt }: IQueryOptions & { runInNotebook?: boolean } = {}) => {
try {
query = typeof query === 'string' ? query : await getSelectedText('execute query');
connNameOrId = connId || connNameOrId;
@@ -267,7 +267,14 @@ export class ConnectionManagerPlugin implements IExtensionPlugin {
connNameOrId = getAttachedConnection(window.activeTextEditor.document.uri);
}
- if (connNameOrId && connNameOrId.trim()) {
+ // Convert connNameOrId to string if it's an object
+ if (connNameOrId && typeof connNameOrId === 'object') {
+ connNameOrId = connNameOrId.id || connNameOrId.name;
+ log.info(`Connection identifier converted from object to: ${connNameOrId}`);
+ }
+
+ // Now check if we have a valid string connection identifier
+ if (connNameOrId && typeof connNameOrId === 'string' && connNameOrId.trim()) {
connNameOrId = connNameOrId.trim();
const conn = (await this.ext_getConnections({ connectedOnly: false, sort: 'connectedFirst' })).find(c => getConnectionId(c) === connNameOrId || c.name === connNameOrId);
if (!conn) {
@@ -281,10 +288,17 @@ export class ConnectionManagerPlugin implements IExtensionPlugin {
const conn = await this.explorer.getActive()
query = await this.replaceParams(query, conn);
- const view = await this._openResultsWebview(conn && conn.id, opt.requestId);
- const payload = await this._runConnectionCommandWithArgs('query', query, { ...opt, requestId: view.requestId });
- this.updateViewResults(view, payload);
- return payload;
+ // If runInNotebook is true, skip opening the results webview
+ if (runInNotebook) {
+ // Just run the query and return the results directly
+ return await this._runConnectionCommandWithArgs('query', query, { ...opt });
+ } else {
+ // Normal execution with results webview
+ const view = await this._openResultsWebview(conn && conn.id, opt.requestId);
+ const payload = await this._runConnectionCommandWithArgs('query', query, { ...opt, requestId: view.requestId });
+ this.updateViewResults(view, payload);
+ return payload;
+ }
} catch (e) {
this.errorHandler('Error fetching records.', e);
}
|