Skip to content

Commit fb6350c

Browse files
committed
feat: add SQL block rendering as editor
1 parent 635883e commit fb6350c

File tree

4 files changed

+265
-2
lines changed

4 files changed

+265
-2
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { NotebookCellData, NotebookCellKind } from 'vscode';
2+
3+
import type { BlockConverter } from './blockConverter';
4+
import type { DeepnoteBlock } from '../deepnoteTypes';
5+
6+
/**
7+
* Converter for SQL blocks.
8+
*
9+
* SQL blocks are rendered as code cells with SQL language for proper syntax highlighting.
10+
* The SQL source code is stored in the cell content and displayed in the code editor.
11+
*
12+
* During execution, the createPythonCode function from @deepnote/blocks will generate
13+
* the appropriate Python code to execute the SQL query based on the block's metadata
14+
* (which includes the sql_integration_id and other SQL-specific settings).
15+
*/
16+
export class SqlBlockConverter implements BlockConverter {
17+
applyChangesToBlock(block: DeepnoteBlock, cell: NotebookCellData): void {
18+
// Store the SQL source code from the cell editor back to the block
19+
block.content = cell.value || '';
20+
}
21+
22+
canConvert(blockType: string): boolean {
23+
return blockType.toLowerCase() === 'sql';
24+
}
25+
26+
convertToCell(block: DeepnoteBlock): NotebookCellData {
27+
// Create a code cell with SQL language for syntax highlighting
28+
// The SQL source code is displayed in the editor
29+
const cell = new NotebookCellData(NotebookCellKind.Code, block.content || '', 'sql');
30+
31+
return cell;
32+
}
33+
34+
getSupportedTypes(): string[] {
35+
return ['sql'];
36+
}
37+
}
38+
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { assert } from 'chai';
2+
import { NotebookCellData, NotebookCellKind } from 'vscode';
3+
4+
import type { DeepnoteBlock } from '../deepnoteTypes';
5+
import { SqlBlockConverter } from './sqlBlockConverter';
6+
7+
suite('SqlBlockConverter', () => {
8+
let converter: SqlBlockConverter;
9+
10+
setup(() => {
11+
converter = new SqlBlockConverter();
12+
});
13+
14+
suite('canConvert', () => {
15+
test('returns true for "sql" type', () => {
16+
assert.strictEqual(converter.canConvert('sql'), true);
17+
});
18+
19+
test('returns true for "SQL" type (case insensitive)', () => {
20+
assert.strictEqual(converter.canConvert('SQL'), true);
21+
});
22+
23+
test('returns false for other types', () => {
24+
assert.strictEqual(converter.canConvert('code'), false);
25+
assert.strictEqual(converter.canConvert('markdown'), false);
26+
assert.strictEqual(converter.canConvert('text-cell-h1'), false);
27+
});
28+
});
29+
30+
suite('getSupportedTypes', () => {
31+
test('returns array with "sql"', () => {
32+
const types = converter.getSupportedTypes();
33+
34+
assert.deepStrictEqual(types, ['sql']);
35+
});
36+
});
37+
38+
suite('convertToCell', () => {
39+
test('converts SQL block to code cell with sql language', () => {
40+
const block: DeepnoteBlock = {
41+
blockGroup: 'test-group',
42+
content: 'SELECT * FROM users WHERE age > 18',
43+
id: 'sql-block-123',
44+
sortingKey: 'a0',
45+
type: 'sql'
46+
};
47+
48+
const cell = converter.convertToCell(block);
49+
50+
assert.strictEqual(cell.kind, NotebookCellKind.Code);
51+
assert.strictEqual(cell.value, 'SELECT * FROM users WHERE age > 18');
52+
assert.strictEqual(cell.languageId, 'sql');
53+
});
54+
55+
test('handles empty SQL content', () => {
56+
const block: DeepnoteBlock = {
57+
blockGroup: 'test-group',
58+
content: '',
59+
id: 'sql-block-456',
60+
sortingKey: 'a1',
61+
type: 'sql'
62+
};
63+
64+
const cell = converter.convertToCell(block);
65+
66+
assert.strictEqual(cell.kind, NotebookCellKind.Code);
67+
assert.strictEqual(cell.value, '');
68+
assert.strictEqual(cell.languageId, 'sql');
69+
});
70+
71+
test('handles undefined SQL content', () => {
72+
const block: DeepnoteBlock = {
73+
blockGroup: 'test-group',
74+
id: 'sql-block-789',
75+
sortingKey: 'a2',
76+
type: 'sql'
77+
};
78+
79+
const cell = converter.convertToCell(block);
80+
81+
assert.strictEqual(cell.kind, NotebookCellKind.Code);
82+
assert.strictEqual(cell.value, '');
83+
assert.strictEqual(cell.languageId, 'sql');
84+
});
85+
86+
test('preserves multi-line SQL queries', () => {
87+
const sqlQuery = `SELECT
88+
u.name,
89+
u.email,
90+
COUNT(o.id) as order_count
91+
FROM users u
92+
LEFT JOIN orders o ON u.id = o.user_id
93+
WHERE u.created_at > '2024-01-01'
94+
GROUP BY u.id, u.name, u.email
95+
ORDER BY order_count DESC
96+
LIMIT 10`;
97+
98+
const block: DeepnoteBlock = {
99+
blockGroup: 'test-group',
100+
content: sqlQuery,
101+
id: 'sql-block-complex',
102+
sortingKey: 'a3',
103+
type: 'sql'
104+
};
105+
106+
const cell = converter.convertToCell(block);
107+
108+
assert.strictEqual(cell.kind, NotebookCellKind.Code);
109+
assert.strictEqual(cell.value, sqlQuery);
110+
assert.strictEqual(cell.languageId, 'sql');
111+
});
112+
113+
test('preserves SQL block with metadata', () => {
114+
const block: DeepnoteBlock = {
115+
blockGroup: 'test-group',
116+
content: 'SELECT * FROM products',
117+
id: 'sql-block-with-metadata',
118+
metadata: {
119+
sql_integration_id: 'postgres-prod',
120+
table_state_spec: '{"pageSize": 50}'
121+
},
122+
sortingKey: 'a4',
123+
type: 'sql'
124+
};
125+
126+
const cell = converter.convertToCell(block);
127+
128+
assert.strictEqual(cell.kind, NotebookCellKind.Code);
129+
assert.strictEqual(cell.value, 'SELECT * FROM products');
130+
assert.strictEqual(cell.languageId, 'sql');
131+
// Metadata is handled by the data converter, not the block converter
132+
});
133+
});
134+
135+
suite('applyChangesToBlock', () => {
136+
test('updates block content from cell value', () => {
137+
const block: DeepnoteBlock = {
138+
blockGroup: 'test-group',
139+
content: 'SELECT * FROM old_table',
140+
id: 'sql-block-123',
141+
sortingKey: 'a0',
142+
type: 'sql'
143+
};
144+
const cell = new NotebookCellData(
145+
NotebookCellKind.Code,
146+
'SELECT * FROM new_table WHERE active = true',
147+
'sql'
148+
);
149+
150+
converter.applyChangesToBlock(block, cell);
151+
152+
assert.strictEqual(block.content, 'SELECT * FROM new_table WHERE active = true');
153+
});
154+
155+
test('handles empty cell value', () => {
156+
const block: DeepnoteBlock = {
157+
blockGroup: 'test-group',
158+
content: 'SELECT * FROM users',
159+
id: 'sql-block-456',
160+
sortingKey: 'a1',
161+
type: 'sql'
162+
};
163+
const cell = new NotebookCellData(NotebookCellKind.Code, '', 'sql');
164+
165+
converter.applyChangesToBlock(block, cell);
166+
167+
assert.strictEqual(block.content, '');
168+
});
169+
170+
test('does not modify other block properties', () => {
171+
const block: DeepnoteBlock = {
172+
blockGroup: 'test-group',
173+
content: 'SELECT * FROM old_query',
174+
id: 'sql-block-789',
175+
metadata: {
176+
sql_integration_id: 'postgres-prod',
177+
custom: 'value'
178+
},
179+
sortingKey: 'a2',
180+
type: 'sql'
181+
};
182+
const cell = new NotebookCellData(NotebookCellKind.Code, 'SELECT * FROM new_query', 'sql');
183+
184+
converter.applyChangesToBlock(block, cell);
185+
186+
assert.strictEqual(block.content, 'SELECT * FROM new_query');
187+
assert.strictEqual(block.id, 'sql-block-789');
188+
assert.strictEqual(block.type, 'sql');
189+
assert.strictEqual(block.sortingKey, 'a2');
190+
assert.deepStrictEqual(block.metadata, {
191+
sql_integration_id: 'postgres-prod',
192+
custom: 'value'
193+
});
194+
});
195+
});
196+
});

src/notebooks/deepnote/deepnoteDataConverter.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import { generateBlockId, generateSortingKey } from './dataConversionUtils';
55
import { ConverterRegistry } from './converters/converterRegistry';
66
import { CodeBlockConverter } from './converters/codeBlockConverter';
77
import { addPocketToCellMetadata, createBlockFromPocket } from './pocket';
8-
import { TextBlockConverter } from './converters/textBlockConverter';
98
import { MarkdownBlockConverter } from './converters/markdownBlockConverter';
9+
import { SqlBlockConverter } from './converters/sqlBlockConverter';
10+
import { TextBlockConverter } from './converters/textBlockConverter';
1011

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

1819
constructor() {
1920
this.registry.register(new CodeBlockConverter());
20-
this.registry.register(new TextBlockConverter());
2121
this.registry.register(new MarkdownBlockConverter());
22+
this.registry.register(new SqlBlockConverter());
23+
this.registry.register(new TextBlockConverter());
2224
}
2325

2426
/**

src/notebooks/deepnote/deepnoteDataConverter.unit.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,33 @@ suite('DeepnoteDataConverter', () => {
5959
assert.strictEqual(cells[0].metadata?.__deepnotePocket?.type, 'markdown');
6060
});
6161

62+
test('converts SQL block to cell with sql language', () => {
63+
const blocks: DeepnoteBlock[] = [
64+
{
65+
blockGroup: 'test-group',
66+
id: 'block3',
67+
type: 'sql',
68+
content: 'SELECT * FROM users WHERE id = 1',
69+
sortingKey: 'a2',
70+
metadata: {
71+
sql_integration_id: 'postgres-123'
72+
}
73+
}
74+
];
75+
76+
const cells = converter.convertBlocksToCells(blocks);
77+
78+
assert.strictEqual(cells.length, 1);
79+
assert.strictEqual(cells[0].kind, NotebookCellKind.Code);
80+
assert.strictEqual(cells[0].value, 'SELECT * FROM users WHERE id = 1');
81+
assert.strictEqual(cells[0].languageId, 'sql');
82+
// id should be at top level, not in pocket
83+
assert.strictEqual(cells[0].metadata?.id, 'block3');
84+
assert.strictEqual(cells[0].metadata?.__deepnotePocket?.type, 'sql');
85+
assert.strictEqual(cells[0].metadata?.__deepnotePocket?.sortingKey, 'a2');
86+
assert.strictEqual(cells[0].metadata?.sql_integration_id, 'postgres-123');
87+
});
88+
6289
test('handles execution count', () => {
6390
const blocks: DeepnoteBlock[] = [
6491
{

0 commit comments

Comments
 (0)