From 5c0379389d8af6aa1fffbc72b8c3ec984ec33ce2 Mon Sep 17 00:00:00 2001 From: jankuca Date: Tue, 21 Oct 2025 12:51:02 +0200 Subject: [PATCH 1/3] fix: add sql metadata output serialization support --- .../deepnote/deepnoteDataConverter.ts | 13 +++ .../deepnoteDataConverter.unit.test.ts | 89 +++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/src/notebooks/deepnote/deepnoteDataConverter.ts b/src/notebooks/deepnote/deepnoteDataConverter.ts index 1567e91ab4..e37a564b49 100644 --- a/src/notebooks/deepnote/deepnoteDataConverter.ts +++ b/src/notebooks/deepnote/deepnoteDataConverter.ts @@ -220,6 +220,10 @@ export class DeepnoteDataConverter { ); } else if (item.mime === 'application/vnd.vega.v5+json') { data['application/vnd.vega.v5+json'] = JSON.parse(new TextDecoder().decode(item.data)); + } else if (item.mime === 'application/vnd.deepnote.sql-output-metadata+json') { + data['application/vnd.deepnote.sql-output-metadata+json'] = JSON.parse( + new TextDecoder().decode(item.data) + ); } } @@ -305,6 +309,15 @@ export class DeepnoteDataConverter { ); } + if (data['application/vnd.deepnote.sql-output-metadata+json']) { + items.push( + NotebookCellOutputItem.json( + data['application/vnd.deepnote.sql-output-metadata+json'], + 'application/vnd.deepnote.sql-output-metadata+json' + ) + ); + } + if (data['application/vnd.vegalite.v5+json']) { const patchedVegaLiteSpec = produce( data['application/vnd.vegalite.v5+json'] as TopLevel>, diff --git a/src/notebooks/deepnote/deepnoteDataConverter.unit.test.ts b/src/notebooks/deepnote/deepnoteDataConverter.unit.test.ts index 8efc7b0734..8546f27032 100644 --- a/src/notebooks/deepnote/deepnoteDataConverter.unit.test.ts +++ b/src/notebooks/deepnote/deepnoteDataConverter.unit.test.ts @@ -431,6 +431,48 @@ suite('DeepnoteDataConverter', () => { assert.strictEqual(outputs[0].items[0].mime, 'text/plain'); assert.strictEqual(new TextDecoder().decode(outputs[0].items[0].data), 'fallback text'); }); + + test('converts SQL metadata output', () => { + const sqlMetadata = { + status: 'read_from_cache_success', + cache_created_at: '2024-10-21T10:30:00Z', + compiled_query: 'SELECT * FROM users', + variable_type: 'dataframe', + integration_id: 'postgres-prod', + size_in_bytes: 2621440 + }; + + const deepnoteOutputs: DeepnoteOutput[] = [ + { + output_type: 'execute_result', + execution_count: 1, + data: { + 'application/vnd.deepnote.sql-output-metadata+json': sqlMetadata + } + } + ]; + + const blocks: DeepnoteBlock[] = [ + { + blockGroup: 'test-group', + id: 'block1', + type: 'code', + content: 'SELECT * FROM users', + sortingKey: 'a0', + outputs: deepnoteOutputs + } + ]; + + const cells = converter.convertBlocksToCells(blocks); + const outputs = cells[0].outputs!; + + assert.strictEqual(outputs.length, 1); + assert.strictEqual(outputs[0].items.length, 1); + assert.strictEqual(outputs[0].items[0].mime, 'application/vnd.deepnote.sql-output-metadata+json'); + + const outputData = JSON.parse(new TextDecoder().decode(outputs[0].items[0].data)); + assert.deepStrictEqual(outputData, sqlMetadata); + }); }); suite('round trip conversion', () => { @@ -468,6 +510,53 @@ suite('DeepnoteDataConverter', () => { assert.deepStrictEqual(roundTripBlocks, originalBlocks); }); + test('SQL metadata output round-trips correctly', () => { + const sqlMetadata = { + status: 'read_from_cache_success', + cache_created_at: '2024-10-21T10:30:00Z', + compiled_query: 'SELECT * FROM users WHERE active = true', + variable_type: 'dataframe', + integration_id: 'postgres-prod', + size_in_bytes: 2621440 + }; + + const originalBlocks: DeepnoteBlock[] = [ + { + blockGroup: 'test-group', + id: 'sql-block', + type: 'code', + content: 'SELECT * FROM users WHERE active = true', + sortingKey: 'a0', + executionCount: 1, + metadata: {}, + outputs: [ + { + output_type: 'execute_result', + execution_count: 1, + data: { + 'application/vnd.deepnote.sql-output-metadata+json': sqlMetadata + } + } + ] + } + ]; + + const cells = converter.convertBlocksToCells(originalBlocks); + const roundTripBlocks = converter.convertCellsToBlocks(cells); + + // The round-trip should preserve the SQL metadata output + assert.strictEqual(roundTripBlocks.length, 1); + assert.strictEqual(roundTripBlocks[0].id, 'sql-block'); + assert.strictEqual(roundTripBlocks[0].outputs?.length, 1); + + const output = roundTripBlocks[0].outputs![0] as { + output_type: string; + data?: Record; + }; + assert.strictEqual(output.output_type, 'execute_result'); + assert.deepStrictEqual(output.data?.['application/vnd.deepnote.sql-output-metadata+json'], sqlMetadata); + }); + test('real deepnote notebook round-trips without losing data', () => { // Inline test data representing a real Deepnote notebook with various block types // blockGroup is an optional field not in the DeepnoteBlock interface, so we cast as any From 85cfd9eab3d01e623e3a1c78bb3ef2b3b2f85208 Mon Sep 17 00:00:00 2001 From: jankuca Date: Tue, 21 Oct 2025 12:50:38 +0200 Subject: [PATCH 2/3] feat: add sql metadata output renderer --- build/esbuild/build.ts | 12 +++ package.json | 9 ++ .../SqlMetadataRenderer.tsx | 96 +++++++++++++++++++ .../sql-metadata-renderer/index.ts | 39 ++++++++ 4 files changed, 156 insertions(+) create mode 100644 src/webviews/webview-side/sql-metadata-renderer/SqlMetadataRenderer.tsx create mode 100644 src/webviews/webview-side/sql-metadata-renderer/index.ts diff --git a/build/esbuild/build.ts b/build/esbuild/build.ts index 4e5bdbb7b9..7b826f8589 100644 --- a/build/esbuild/build.ts +++ b/build/esbuild/build.ts @@ -336,6 +336,18 @@ async function buildAll() { path.join(extensionFolder, 'dist', 'webviews', 'webview-side', 'vegaRenderer', 'vegaRenderer.js'), { target: 'web', watch: isWatchMode } ), + build( + path.join(extensionFolder, 'src', 'webviews', 'webview-side', 'sql-metadata-renderer', 'index.ts'), + path.join( + extensionFolder, + 'dist', + 'webviews', + 'webview-side', + 'sqlMetadataRenderer', + 'sqlMetadataRenderer.js' + ), + { target: 'web', watch: isWatchMode } + ), build( path.join(extensionFolder, 'src', 'webviews', 'webview-side', 'variable-view', 'index.tsx'), path.join(extensionFolder, 'dist', 'webviews', 'webview-side', 'viewers', 'variableView.js'), diff --git a/package.json b/package.json index 575cc95d56..e99aaa26b2 100644 --- a/package.json +++ b/package.json @@ -1856,6 +1856,15 @@ "application/vnd.vega.v5+json" ], "requiresMessaging": "optional" + }, + { + "id": "deepnote-sql-metadata-renderer", + "displayName": "Deepnote SQL Metadata Renderer", + "entrypoint": "./dist/webviews/webview-side/sqlMetadataRenderer/sqlMetadataRenderer.js", + "mimeTypes": [ + "application/vnd.deepnote.sql-output-metadata+json" + ], + "requiresMessaging": "optional" } ], "viewsContainers": { diff --git a/src/webviews/webview-side/sql-metadata-renderer/SqlMetadataRenderer.tsx b/src/webviews/webview-side/sql-metadata-renderer/SqlMetadataRenderer.tsx new file mode 100644 index 0000000000..8a49939510 --- /dev/null +++ b/src/webviews/webview-side/sql-metadata-renderer/SqlMetadataRenderer.tsx @@ -0,0 +1,96 @@ +import React, { memo } from 'react'; + +export interface SqlMetadataRendererProps { + data: { + cache_created_at?: string; + compiled_query?: string; + integration_id?: string; + size_in_bytes?: number; + status: string; + variable_type?: string; + }; +} + +export const SqlMetadataRenderer = memo(function SqlMetadataRenderer({ data }: SqlMetadataRendererProps) { + const getStatusMessage = () => { + switch (data.status) { + case 'read_from_cache_success': + return { + icon: '✓', + text: 'Query result loaded from cache', + color: 'var(--vscode-testing-iconPassed)' + }; + case 'success_no_cache': + return { + icon: 'ℹ', + text: 'Query executed successfully', + color: 'var(--vscode-notificationsInfoIcon-foreground)' + }; + case 'cache_not_supported_for_query': + return { + icon: 'ℹ', + text: 'Caching not supported for this query type', + color: 'var(--vscode-notificationsInfoIcon-foreground)' + }; + default: + return { + icon: 'ℹ', + text: `Status: ${data.status}`, + color: 'var(--vscode-foreground)' + }; + } + }; + + const statusInfo = getStatusMessage(); + + const formatBytes = (bytes: number) => { + if (bytes < 1024) { + return `${bytes} B`; + } + if (bytes < 1024 * 1024) { + return `${(bytes / 1024).toFixed(2)} KB`; + } + if (bytes < 1024 * 1024 * 1024) { + return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; + } + return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; + }; + + return ( +
+
+ {statusInfo.icon} + {statusInfo.text} +
+ + {data.cache_created_at && ( +
+ Cache created: {new Date(data.cache_created_at).toLocaleString()} +
+ )} + + {data.size_in_bytes !== undefined && ( +
+ Result size: {formatBytes(data.size_in_bytes)} +
+ )} +
+ ); +}); diff --git a/src/webviews/webview-side/sql-metadata-renderer/index.ts b/src/webviews/webview-side/sql-metadata-renderer/index.ts new file mode 100644 index 0000000000..4c5cfc166a --- /dev/null +++ b/src/webviews/webview-side/sql-metadata-renderer/index.ts @@ -0,0 +1,39 @@ +import type { ActivationFunction, OutputItem, RendererContext } from 'vscode-notebook-renderer'; +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +import { SqlMetadataRenderer } from './SqlMetadataRenderer'; + +/** + * Renderer for SQL metadata output (application/vnd.deepnote.sql-output-metadata+json). + * This renderer displays information about SQL query execution, including cache status, + * query size, and other metadata. + */ +export const activate: ActivationFunction = (_context: RendererContext) => { + return { + renderOutputItem(outputItem: OutputItem, element: HTMLElement) { + console.log(`SQL metadata renderer - rendering output item: ${outputItem.id}`); + try { + const data = outputItem.json(); + + console.log(`SQL metadata renderer - received data:`, data); + + const root = document.createElement('div'); + element.appendChild(root); + + ReactDOM.render(React.createElement(SqlMetadataRenderer, { data }), root); + } catch (error) { + console.error(`Error rendering SQL metadata: ${error}`); + const errorDiv = document.createElement('div'); + errorDiv.style.padding = '10px'; + errorDiv.style.color = 'var(--vscode-errorForeground)'; + errorDiv.textContent = `Error rendering SQL metadata: ${error}`; + element.appendChild(errorDiv); + } + }, + + disposeOutputItem(_id?: string) { + // Cleanup if needed + } + }; +}; From 2dcacce055a7251c805512ab0da9165d6354ce4b Mon Sep 17 00:00:00 2001 From: jankuca Date: Tue, 21 Oct 2025 17:39:05 +0200 Subject: [PATCH 3/3] code review changes --- .../SqlMetadataRenderer.tsx | 84 +++++++++---------- .../sql-metadata-renderer/index.ts | 16 ++-- 2 files changed, 53 insertions(+), 47 deletions(-) diff --git a/src/webviews/webview-side/sql-metadata-renderer/SqlMetadataRenderer.tsx b/src/webviews/webview-side/sql-metadata-renderer/SqlMetadataRenderer.tsx index 8a49939510..7c5e3ec341 100644 --- a/src/webviews/webview-side/sql-metadata-renderer/SqlMetadataRenderer.tsx +++ b/src/webviews/webview-side/sql-metadata-renderer/SqlMetadataRenderer.tsx @@ -11,50 +11,50 @@ export interface SqlMetadataRendererProps { }; } -export const SqlMetadataRenderer = memo(function SqlMetadataRenderer({ data }: SqlMetadataRendererProps) { - const getStatusMessage = () => { - switch (data.status) { - case 'read_from_cache_success': - return { - icon: '✓', - text: 'Query result loaded from cache', - color: 'var(--vscode-testing-iconPassed)' - }; - case 'success_no_cache': - return { - icon: 'ℹ', - text: 'Query executed successfully', - color: 'var(--vscode-notificationsInfoIcon-foreground)' - }; - case 'cache_not_supported_for_query': - return { - icon: 'ℹ', - text: 'Caching not supported for this query type', - color: 'var(--vscode-notificationsInfoIcon-foreground)' - }; - default: - return { - icon: 'ℹ', - text: `Status: ${data.status}`, - color: 'var(--vscode-foreground)' - }; - } - }; +const getStatusMessage = (status: string) => { + switch (status) { + case 'read_from_cache_success': + return { + icon: '✓', + text: 'Query result loaded from cache', + color: 'var(--vscode-testing-iconPassed)' + }; + case 'success_no_cache': + return { + icon: 'ℹ', + text: 'Query executed successfully', + color: 'var(--vscode-notificationsInfoIcon-foreground)' + }; + case 'cache_not_supported_for_query': + return { + icon: 'ℹ', + text: 'Caching not supported for this query type', + color: 'var(--vscode-notificationsInfoIcon-foreground)' + }; + default: + return { + icon: 'ℹ', + text: `Status: ${status}`, + color: 'var(--vscode-foreground)' + }; + } +}; - const statusInfo = getStatusMessage(); +const formatBytes = (bytes: number) => { + if (bytes < 1024) { + return `${bytes} B`; + } + if (bytes < 1024 * 1024) { + return `${(bytes / 1024).toFixed(2)} KB`; + } + if (bytes < 1024 * 1024 * 1024) { + return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; + } + return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; +}; - const formatBytes = (bytes: number) => { - if (bytes < 1024) { - return `${bytes} B`; - } - if (bytes < 1024 * 1024) { - return `${(bytes / 1024).toFixed(2)} KB`; - } - if (bytes < 1024 * 1024 * 1024) { - return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; - } - return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; - }; +export const SqlMetadataRenderer = memo(function SqlMetadataRenderer({ data }: SqlMetadataRendererProps) { + const statusInfo = getStatusMessage(data.status); return (
) => { + const roots = new Map(); + return { renderOutputItem(outputItem: OutputItem, element: HTMLElement) { - console.log(`SQL metadata renderer - rendering output item: ${outputItem.id}`); try { const data = outputItem.json(); - console.log(`SQL metadata renderer - received data:`, data); - const root = document.createElement('div'); element.appendChild(root); + roots.set(outputItem.id, root); ReactDOM.render(React.createElement(SqlMetadataRenderer, { data }), root); } catch (error) { @@ -32,8 +32,14 @@ export const activate: ActivationFunction = (_context: RendererContext) } }, - disposeOutputItem(_id?: string) { - // Cleanup if needed + disposeOutputItem(id?: string) { + if (id) { + const root = roots.get(id); + if (root) { + ReactDOM.unmountComponentAtNode(root); + roots.delete(id); + } + } } }; };