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
12 changes: 12 additions & 0 deletions build/esbuild/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
13 changes: 13 additions & 0 deletions src/notebooks/deepnote/deepnoteDataConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
}
}

Expand Down Expand Up @@ -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<LayerSpec<Field>>,
Expand Down
89 changes: 89 additions & 0 deletions src/notebooks/deepnote/deepnoteDataConverter.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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<string, unknown>;
};
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
};
}

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 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 (
<div
style={{
padding: '8px 12px',
margin: '4px 0',
borderLeft: `3px solid ${statusInfo.color}`,
backgroundColor: 'var(--vscode-textBlockQuote-background)',
fontSize: '12px',
fontFamily: 'var(--vscode-font-family)',
color: 'var(--vscode-foreground)'
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: data.cache_created_at || data.size_in_bytes ? '6px' : '0'
}}
>
<span style={{ color: statusInfo.color, fontSize: '14px', fontWeight: 'bold' }}>{statusInfo.icon}</span>
<span style={{ fontWeight: 500 }}>{statusInfo.text}</span>
</div>

{data.cache_created_at && (
<div style={{ marginLeft: '22px', opacity: 0.8, fontSize: '11px' }}>
Cache created: {new Date(data.cache_created_at).toLocaleString()}
</div>
)}

{data.size_in_bytes !== undefined && (
<div style={{ marginLeft: '22px', opacity: 0.8, fontSize: '11px' }}>
Result size: {formatBytes(data.size_in_bytes)}
</div>
)}
</div>
);
});
45 changes: 45 additions & 0 deletions src/webviews/webview-side/sql-metadata-renderer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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<unknown>) => {
const roots = new Map<string, HTMLElement>();

return {
renderOutputItem(outputItem: OutputItem, element: HTMLElement) {
try {
const data = outputItem.json();

const root = document.createElement('div');
element.appendChild(root);
roots.set(outputItem.id, 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) {
if (id) {
const root = roots.get(id);
if (root) {
ReactDOM.unmountComponentAtNode(root);
roots.delete(id);
}
}
}
};
};