Skip to content

Commit ce23c76

Browse files
authored
[feat] duckdb module updates (#2927)
- UI for SQL Query plugin - DuckDb logic to ingest and generate datasets Signed-off-by: Ihor Dykhta <[email protected]>
1 parent fc974d8 commit ce23c76

File tree

16 files changed

+1731
-13
lines changed

16 files changed

+1731
-13
lines changed

.eslintrc.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ module.exports = {
3838
'consistent-return': 0,
3939
'comma-dangle': 1,
4040
'enzyme-deprecation/no-shallow': 2,
41-
'enzyme-deprecation/no-mount': 2
41+
'enzyme-deprecation/no-mount': 2,
42+
'no-constant-condition': ['error', {checkLoops: false}]
4243
},
4344
overrides: [
4445
{

src/duckdb/package.json

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@kepler.gl/duckdb",
33
"author": "Shan He <[email protected]>",
44
"version": "3.1.0-alpha.5",
5-
"description": "kepler.gl duckDB plugin",
5+
"description": "DuckDB plugin for Kepler.gl",
66
"license": "MIT",
77
"main": "dist/index.js",
88
"types": "dist/index.d.ts",
@@ -25,8 +25,17 @@
2525
"umd"
2626
],
2727
"dependencies": {
28+
"@duckdb/duckdb-wasm": "^1.28.0",
29+
"@kepler.gl/common-utils": "3.1.0-alpha.5",
2830
"@kepler.gl/constants": "3.1.0-alpha.5",
29-
"@kepler.gl/types": "3.1.0-alpha.5"
31+
"@kepler.gl/processors": "3.1.0-alpha.5",
32+
"@kepler.gl/table": "3.1.0-alpha.5",
33+
"@kepler.gl/types": "3.1.0-alpha.5",
34+
"@monaco-editor/react": "^4.6.0",
35+
"@radix-ui/react-collapsible": "^1.1.0",
36+
"apache-arrow": ">=15.0.0",
37+
"monaco-editor": "^0.52.0",
38+
"react-resizable-panels": "^2.1.7"
3039
},
3140
"nyc": {
3241
"sourceMap": false,

src/duckdb/src/components/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './sql-panel';
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import React, {useCallback, useMemo, useRef} from 'react';
2+
import Editor, {OnChange, OnMount} from '@monaco-editor/react';
3+
import * as monaco from 'monaco-editor';
4+
// import {tableSchema as DEFAULT_SCHEMA} from './table-schema';
5+
import uniq from 'lodash.uniq';
6+
import uniqBy from 'lodash.uniqby';
7+
8+
const MONACO_OPTIONS: monaco.editor.IStandaloneEditorConstructionOptions = {
9+
minimap: {enabled: false},
10+
language: 'sql',
11+
contextmenu: false,
12+
renderLineHighlight: 'none',
13+
scrollBeyondLastLine: false,
14+
scrollbar: {alwaysConsumeMouseWheel: false},
15+
overviewRulerLanes: 0,
16+
automaticLayout: true,
17+
acceptSuggestionOnEnter: 'on',
18+
quickSuggestionsDelay: 400,
19+
matchOnWordStartOnly: false,
20+
tabCompletion: 'off',
21+
lineNumbers: 'off'
22+
};
23+
24+
function parseSqlAndFindTableNameAndAliases(sql: string) {
25+
const regex = /\b(?:FROM|JOIN)\s+([^\s.]+(?:\.[^\s.]+)?)\s*(?:AS)?\s*([^\s,]+)?/gi;
26+
const tables: {table_name: string; alias: string}[] = [];
27+
28+
while (true) {
29+
const match = regex.exec(sql);
30+
if (!match) {
31+
break;
32+
}
33+
const table_name = match[1];
34+
if (!/\(/.test(table_name)) {
35+
// exclude function calls
36+
let alias = match[2] as string | null;
37+
if (alias && /on|where|inner|left|right|join/.test(alias)) {
38+
alias = null;
39+
}
40+
tables.push({
41+
table_name,
42+
alias: alias || table_name
43+
});
44+
}
45+
}
46+
47+
return tables;
48+
}
49+
50+
interface MonacoEditorProps {
51+
code: string;
52+
isReadOnly?: boolean;
53+
onChange: OnChange;
54+
onRunQuery: () => void;
55+
tableSchema?: {table_name: string; column_name: string}[];
56+
}
57+
58+
const MonacoEditor: React.FC<MonacoEditorProps> = ({
59+
onRunQuery,
60+
onChange,
61+
code,
62+
tableSchema,
63+
isReadOnly
64+
}) => {
65+
// private editor?: monaco.editor.IStandaloneCodeEditor;
66+
const schemaTableNames = useMemo(
67+
() => (tableSchema ? uniq(tableSchema.map(d => d.table_name)) : []),
68+
[tableSchema]
69+
);
70+
const schemaTableNamesSet = useMemo(() => new Set(schemaTableNames), [schemaTableNames]);
71+
const handleRunQueryRef = useRef(onRunQuery);
72+
handleRunQueryRef.current = onRunQuery;
73+
74+
const handleEditorDidMount: OnMount = useCallback(
75+
editor => {
76+
// this.editor = editor;
77+
editor.focus();
78+
79+
editor.addAction({
80+
id: 'run-query',
81+
label: 'Run Query',
82+
keybindings: [
83+
monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,
84+
monaco.KeyMod.Shift | monaco.KeyCode.Enter
85+
],
86+
contextMenuGroupId: 'custom',
87+
contextMenuOrder: 0,
88+
run: () => handleRunQueryRef.current()
89+
});
90+
91+
monaco.languages.registerCompletionItemProvider('*', {
92+
provideCompletionItems: (model, position, context, cancelationToken) => {
93+
const suggestions: monaco.languages.CompletionItem[] = [
94+
{
95+
label: 'myCustomSnippet',
96+
kind: monaco.languages.CompletionItemKind.Snippet,
97+
insertText: 'This is a piece of custom code',
98+
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
99+
documentation: 'This is a piece of custom code'
100+
// TODO: range is missing
101+
} as monaco.languages.CompletionItem
102+
];
103+
104+
const fullQueryText = model.getValue();
105+
106+
const tableNamesAndAliases = new Map<string, string>(
107+
parseSqlAndFindTableNameAndAliases(fullQueryText).map(({table_name, alias}) => [
108+
alias,
109+
table_name
110+
])
111+
);
112+
113+
const thisLine = model.getValueInRange({
114+
startLineNumber: position.lineNumber,
115+
startColumn: 1,
116+
endLineNumber: position.lineNumber,
117+
endColumn: position.column
118+
});
119+
const thisToken = thisLine.trim().split(' ').slice(-1)?.[0] || '';
120+
121+
const lastTokenBeforeSpace = /\s?(\w+)\s+\w+$/.exec(thisLine.trim())?.[1];
122+
const lastTokenBeforeDot = /(\w+)\.\w*$/.exec(thisToken)?.[1];
123+
124+
// console.log(tableNamesAndAliases, thisToken, lastTokenBeforeSpace, lastTokenBeforeDot);
125+
126+
if (lastTokenBeforeSpace && /from|join|update|into/.test(lastTokenBeforeSpace)) {
127+
suggestions.push(
128+
...schemaTableNames.map(
129+
table_name =>
130+
({
131+
label: table_name,
132+
kind: monaco.languages.CompletionItemKind.Field,
133+
insertText: table_name
134+
// TODO: range is missing
135+
} as monaco.languages.CompletionItem)
136+
)
137+
);
138+
}
139+
140+
if (lastTokenBeforeDot) {
141+
let table_name = null as string | null;
142+
if (schemaTableNamesSet.has(lastTokenBeforeDot)) {
143+
table_name = lastTokenBeforeDot;
144+
} else if (tableNamesAndAliases.get(lastTokenBeforeDot)) {
145+
table_name = tableNamesAndAliases.get(lastTokenBeforeDot) as string;
146+
}
147+
if (table_name && tableSchema) {
148+
suggestions.push(
149+
...tableSchema
150+
.filter(d => d.table_name === table_name)
151+
.map(
152+
({table_name, column_name}) =>
153+
({
154+
label: column_name,
155+
kind: monaco.languages.CompletionItemKind.Field,
156+
insertText: column_name
157+
// TODO: range is missing
158+
} as monaco.languages.CompletionItem)
159+
)
160+
);
161+
}
162+
}
163+
164+
return {
165+
suggestions: uniqBy(suggestions, s => s.insertText)
166+
};
167+
}
168+
});
169+
},
170+
[tableSchema]
171+
);
172+
173+
return (
174+
<Editor
175+
height="100%"
176+
theme="vs-dark"
177+
defaultLanguage="sql"
178+
defaultValue={code}
179+
onChange={onChange}
180+
onMount={handleEditorDidMount}
181+
options={{
182+
...MONACO_OPTIONS,
183+
readOnly: isReadOnly
184+
}}
185+
/>
186+
);
187+
};
188+
189+
export default MonacoEditor;
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Copyright 2022 Foursquare Labs, Inc. All Rights Reserved.
2+
3+
import React, {useCallback, useMemo, useState, CSSProperties} from 'react';
4+
5+
import {DataTable, renderedSize} from '@kepler.gl/components';
6+
import {parseFieldValue, createDataContainer} from '@kepler.gl/utils';
7+
import {arrowSchemaToFields} from '@kepler.gl/processors';
8+
import {DataForm} from '@kepler.gl/utils';
9+
import {withTheme} from 'styled-components';
10+
11+
type BaseComponentProps = {
12+
className?: string;
13+
style?: CSSProperties;
14+
};
15+
16+
const DEFAULT_ROWS_TO_CALCULATE_PREVIEW = 100;
17+
// min Cellsize should take into account option button and field token
18+
const minCellSize = 80;
19+
// option buttons and field token
20+
const optionButtonWidth = 20;
21+
const pinButton = 20;
22+
const cellPadding = 20;
23+
24+
export type ColMeta = {
25+
[key: string]: {
26+
colIdx: number;
27+
name: string;
28+
displayName: string;
29+
type: string;
30+
};
31+
};
32+
33+
export type DataTableStyle = {
34+
minCellSize?: number;
35+
cellPadding?: number;
36+
fontSize?: number;
37+
font?: string;
38+
optionsButton?: number;
39+
};
40+
41+
export type PreviewDataPanelProps = BaseComponentProps & {
42+
result: any;
43+
rowsToCalculatePreview?: number;
44+
theme?: any;
45+
setColumnDisplayFormat?: (formats: {[key: string]: string}) => void;
46+
defaultPinnedColumns?: string[];
47+
dataTableStyle: DataTableStyle;
48+
onAddResultToMap: (result: any) => void;
49+
};
50+
51+
const PreviewDataPanelWOTheme: React.FC<PreviewDataPanelProps> = ({
52+
result,
53+
rowsToCalculatePreview = DEFAULT_ROWS_TO_CALCULATE_PREVIEW,
54+
setColumnDisplayFormat,
55+
defaultPinnedColumns = [],
56+
theme
57+
}) => {
58+
const [pinnedColumns, setPinnedColumns] = useState<string[]>(defaultPinnedColumns);
59+
const fields = useMemo(() => arrowSchemaToFields(result.schema), [result.schema]);
60+
const dataContainer = useMemo(() => {
61+
// const fields = arrowSchemaToFields(result.schema);
62+
63+
const cols = [...Array(result.numCols).keys()].map(i => result.getChildAt(i));
64+
65+
const dataContainer = createDataContainer(cols, {
66+
fields,
67+
inputDataFormat: DataForm.COLS_ARRAY
68+
});
69+
return dataContainer;
70+
}, [result, fields]);
71+
72+
const columns = useMemo(() => fields.map(f => f.name), [fields]);
73+
const colMeta = useMemo(
74+
() =>
75+
fields.reduce(
76+
(acc, {name, displayName, type, displayFormat}, colIdx) => ({
77+
...acc,
78+
[name]: {
79+
// because '' || 'aaa' = 'aaa'
80+
name: displayName !== undefined ? displayName : name,
81+
displayName,
82+
displayFormat,
83+
type,
84+
colIdx
85+
}
86+
}),
87+
{}
88+
),
89+
[fields]
90+
);
91+
const copyTableColumn = useCallback(
92+
column => {
93+
const {colIdx, type} = colMeta[column];
94+
const text = dataContainer
95+
.mapIndex(row => parseFieldValue(dataContainer.valueAt(row.index, colIdx), type))
96+
.join('\n');
97+
navigator?.clipboard.writeText(text);
98+
},
99+
[colMeta, dataContainer]
100+
);
101+
const pinTableColumn = useCallback(
102+
column =>
103+
pinnedColumns.includes(column)
104+
? setPinnedColumns(pinnedColumns.filter(c => c !== column))
105+
: setPinnedColumns([...pinnedColumns, column]),
106+
[pinnedColumns]
107+
);
108+
109+
// TODO Potentially costly operation for non row based data containers. Revisit sorting below.
110+
const dataTableStyle = useMemo(
111+
() => ({
112+
minCellSize,
113+
cellPadding,
114+
optionsButton:
115+
theme.fieldTokenWidth + theme.fieldTokenRightMargin + optionButtonWidth + pinButton,
116+
fontSize: theme.cellFontSize,
117+
font: theme.fontFamily
118+
}),
119+
[theme]
120+
);
121+
const cellSizeCache = useMemo(() => {
122+
return columns.reduce((acc, column) => {
123+
const {colIdx, displayName, type} = colMeta[column];
124+
return {
125+
...acc,
126+
[column]: renderedSize({
127+
text: {
128+
dataContainer,
129+
column: displayName
130+
},
131+
colIdx,
132+
type,
133+
numRowsToCalculate: rowsToCalculatePreview,
134+
...dataTableStyle
135+
})
136+
};
137+
}, {});
138+
}, [columns, colMeta, dataContainer, rowsToCalculatePreview, dataTableStyle]);
139+
140+
return (
141+
<DataTable
142+
colMeta={colMeta}
143+
columns={columns}
144+
cellSizeCache={cellSizeCache}
145+
dataContainer={dataContainer}
146+
pinnedColumns={pinnedColumns}
147+
// sortColumn={sortColumnConfig}
148+
copyTableColumn={copyTableColumn}
149+
pinTableColumn={pinTableColumn}
150+
// sortTableColumn={setTableSortColumn}
151+
setColumnDisplayFormat={setColumnDisplayFormat ?? (() => null)}
152+
/>
153+
);
154+
};
155+
156+
export const PreviewDataPanel = withTheme(
157+
PreviewDataPanelWOTheme
158+
) as React.FC<PreviewDataPanelProps>;

0 commit comments

Comments
 (0)