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
37 changes: 37 additions & 0 deletions src/notebooks/deepnote/converters/sqlBlockConverter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { NotebookCellData, NotebookCellKind } from 'vscode';

import type { BlockConverter } from './blockConverter';
import type { DeepnoteBlock } from '../deepnoteTypes';

/**
* Converter for SQL blocks.
*
* SQL blocks are rendered as code cells with SQL language for proper syntax highlighting.
* The SQL source code is stored in the cell content and displayed in the code editor.
*
* During execution, the createPythonCode function from @deepnote/blocks will generate
* the appropriate Python code to execute the SQL query based on the block's metadata
* (which includes the sql_integration_id and other SQL-specific settings).
*/
export class SqlBlockConverter implements BlockConverter {
applyChangesToBlock(block: DeepnoteBlock, cell: NotebookCellData): void {
// Store the SQL source code from the cell editor back to the block
block.content = cell.value || '';
}

canConvert(blockType: string): boolean {
return blockType.toLowerCase() === 'sql';
}

convertToCell(block: DeepnoteBlock): NotebookCellData {
// Create a code cell with SQL language for syntax highlighting
// The SQL source code is displayed in the editor
const cell = new NotebookCellData(NotebookCellKind.Code, block.content || '', 'sql');

return cell;
}

getSupportedTypes(): string[] {
return ['sql'];
}
}
199 changes: 199 additions & 0 deletions src/notebooks/deepnote/converters/sqlBlockConverter.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { assert } from 'chai';
import { NotebookCellData, NotebookCellKind } from 'vscode';

import type { DeepnoteBlock } from '../deepnoteTypes';
import { SqlBlockConverter } from './sqlBlockConverter';
import dedent from 'dedent';

suite('SqlBlockConverter', () => {
let converter: SqlBlockConverter;

setup(() => {
converter = new SqlBlockConverter();
});

suite('canConvert', () => {
test('returns true for "sql" type', () => {
assert.strictEqual(converter.canConvert('sql'), true);
});

test('returns true for "SQL" type (case insensitive)', () => {
assert.strictEqual(converter.canConvert('SQL'), true);
});

test('returns false for other types', () => {
assert.strictEqual(converter.canConvert('code'), false);
assert.strictEqual(converter.canConvert('markdown'), false);
assert.strictEqual(converter.canConvert('text-cell-h1'), false);
});
});

suite('getSupportedTypes', () => {
test('returns array with "sql"', () => {
const types = converter.getSupportedTypes();

assert.deepStrictEqual(types, ['sql']);
});
});

suite('convertToCell', () => {
test('converts SQL block to code cell with sql language', () => {
const block: DeepnoteBlock = {
blockGroup: 'test-group',
content: 'SELECT * FROM users WHERE age > 18',
id: 'sql-block-123',
sortingKey: 'a0',
type: 'sql'
};

const cell = converter.convertToCell(block);

assert.strictEqual(cell.kind, NotebookCellKind.Code);
assert.strictEqual(cell.value, 'SELECT * FROM users WHERE age > 18');
assert.strictEqual(cell.languageId, 'sql');
});

test('handles empty SQL content', () => {
const block: DeepnoteBlock = {
blockGroup: 'test-group',
content: '',
id: 'sql-block-456',
sortingKey: 'a1',
type: 'sql'
};

const cell = converter.convertToCell(block);

assert.strictEqual(cell.kind, NotebookCellKind.Code);
assert.strictEqual(cell.value, '');
assert.strictEqual(cell.languageId, 'sql');
});

test('handles undefined SQL content', () => {
const block: DeepnoteBlock = {
blockGroup: 'test-group',
id: 'sql-block-789',
sortingKey: 'a2',
type: 'sql'
};

const cell = converter.convertToCell(block);

assert.strictEqual(cell.kind, NotebookCellKind.Code);
assert.strictEqual(cell.value, '');
assert.strictEqual(cell.languageId, 'sql');
});

test('preserves multi-line SQL queries', () => {
const sqlQuery = dedent`
SELECT
u.name,
u.email,
COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at > '2024-01-01'
GROUP BY u.id, u.name, u.email
ORDER BY order_count DESC
LIMIT 10
`;

const block: DeepnoteBlock = {
blockGroup: 'test-group',
content: sqlQuery,
id: 'sql-block-complex',
sortingKey: 'a3',
type: 'sql'
};

const cell = converter.convertToCell(block);

assert.strictEqual(cell.kind, NotebookCellKind.Code);
assert.strictEqual(cell.value, sqlQuery);
assert.strictEqual(cell.languageId, 'sql');
});

test('preserves SQL block with metadata', () => {
const block: DeepnoteBlock = {
blockGroup: 'test-group',
content: 'SELECT * FROM products',
id: 'sql-block-with-metadata',
metadata: {
sql_integration_id: 'postgres-prod',
table_state_spec: '{"pageSize": 50}'
},
sortingKey: 'a4',
type: 'sql'
};

const cell = converter.convertToCell(block);

assert.strictEqual(cell.kind, NotebookCellKind.Code);
assert.strictEqual(cell.value, 'SELECT * FROM products');
assert.strictEqual(cell.languageId, 'sql');
// Metadata is handled by the data converter, not the block converter
});
});

suite('applyChangesToBlock', () => {
test('updates block content from cell value', () => {
const block: DeepnoteBlock = {
blockGroup: 'test-group',
content: 'SELECT * FROM old_table',
id: 'sql-block-123',
sortingKey: 'a0',
type: 'sql'
};
const cell = new NotebookCellData(
NotebookCellKind.Code,
'SELECT * FROM new_table WHERE active = true',
'sql'
);

converter.applyChangesToBlock(block, cell);

assert.strictEqual(block.content, 'SELECT * FROM new_table WHERE active = true');
});

test('handles empty cell value', () => {
const block: DeepnoteBlock = {
blockGroup: 'test-group',
content: 'SELECT * FROM users',
id: 'sql-block-456',
sortingKey: 'a1',
type: 'sql'
};
const cell = new NotebookCellData(NotebookCellKind.Code, '', 'sql');

converter.applyChangesToBlock(block, cell);

assert.strictEqual(block.content, '');
});

test('does not modify other block properties', () => {
const block: DeepnoteBlock = {
blockGroup: 'test-group',
content: 'SELECT * FROM old_query',
id: 'sql-block-789',
metadata: {
sql_integration_id: 'postgres-prod',
custom: 'value'
},
sortingKey: 'a2',
type: 'sql'
};
const cell = new NotebookCellData(NotebookCellKind.Code, 'SELECT * FROM new_query', 'sql');

converter.applyChangesToBlock(block, cell);

assert.strictEqual(block.content, 'SELECT * FROM new_query');
assert.strictEqual(block.id, 'sql-block-789');
assert.strictEqual(block.type, 'sql');
assert.strictEqual(block.sortingKey, 'a2');
assert.deepStrictEqual(block.metadata, {
sql_integration_id: 'postgres-prod',
custom: 'value'
});
});
});
});
6 changes: 4 additions & 2 deletions src/notebooks/deepnote/deepnoteDataConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import { generateBlockId, generateSortingKey } from './dataConversionUtils';
import { ConverterRegistry } from './converters/converterRegistry';
import { CodeBlockConverter } from './converters/codeBlockConverter';
import { addPocketToCellMetadata, createBlockFromPocket } from './pocket';
import { TextBlockConverter } from './converters/textBlockConverter';
import { MarkdownBlockConverter } from './converters/markdownBlockConverter';
import { SqlBlockConverter } from './converters/sqlBlockConverter';
import { TextBlockConverter } from './converters/textBlockConverter';

/**
* Utility class for converting between Deepnote block structures and VS Code notebook cells.
Expand All @@ -17,8 +18,9 @@ export class DeepnoteDataConverter {

constructor() {
this.registry.register(new CodeBlockConverter());
this.registry.register(new TextBlockConverter());
this.registry.register(new MarkdownBlockConverter());
this.registry.register(new SqlBlockConverter());
this.registry.register(new TextBlockConverter());
}

/**
Expand Down
27 changes: 27 additions & 0 deletions src/notebooks/deepnote/deepnoteDataConverter.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,33 @@ suite('DeepnoteDataConverter', () => {
assert.strictEqual(cells[0].metadata?.__deepnotePocket?.type, 'markdown');
});

test('converts SQL block to cell with sql language', () => {
const blocks: DeepnoteBlock[] = [
{
blockGroup: 'test-group',
id: 'block3',
type: 'sql',
content: 'SELECT * FROM users WHERE id = 1',
sortingKey: 'a2',
metadata: {
sql_integration_id: 'postgres-123'
}
}
];

const cells = converter.convertBlocksToCells(blocks);

assert.strictEqual(cells.length, 1);
assert.strictEqual(cells[0].kind, NotebookCellKind.Code);
assert.strictEqual(cells[0].value, 'SELECT * FROM users WHERE id = 1');
assert.strictEqual(cells[0].languageId, 'sql');
// id should be at top level, not in pocket
assert.strictEqual(cells[0].metadata?.id, 'block3');
assert.strictEqual(cells[0].metadata?.__deepnotePocket?.type, 'sql');
assert.strictEqual(cells[0].metadata?.__deepnotePocket?.sortingKey, 'a2');
assert.strictEqual(cells[0].metadata?.sql_integration_id, 'postgres-123');
});

test('handles execution count', () => {
const blocks: DeepnoteBlock[] = [
{
Expand Down