Skip to content

Commit 0ac510e

Browse files
authored
feat: add SQL block rendering as editor (#72)
1 parent 635883e commit 0ac510e

File tree

4 files changed

+267
-2
lines changed

4 files changed

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

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)