dbeaver/pro#7854 feat: display query with parameters (#4064)

* dbeaver/pro#7854 feat: display query with parameters

* fix: use replaceAll instead of replace

* dbeaver/pro#7854 fix: support parameters and variables

---------

Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
This commit is contained in:
Alexey Potsetsuev
2026-01-22 00:18:25 +08:00
committed by GitHub
co-authored by Daria Marutkina
parent 1deffb984b
commit 90fcbe6b7f
39 changed files with 286 additions and 89 deletions
@@ -1,63 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { Decoration, type DecorationSet, EditorView, StateEffect, StateField } from '@cloudbeaver/plugin-codemirror6';
const ACTIVE_QUERY_EFFECT_ADD = StateEffect.define<{ from: number; to: number | undefined }>({
map: (val, mapping) => ({
from: mapping.mapPos(val.from),
to: val.to === undefined ? undefined : mapping.mapPos(val.to),
}),
});
const ACTIVE_QUERY_EFFECT_CLEAR = StateEffect.define();
const ACTIVE_QUERY_DECORATION = Decoration.mark({
class: 'active-query',
});
export const ACTIVE_QUERY_EXTENSION: StateField<DecorationSet> = StateField.define({
create() {
return Decoration.none;
},
update(value, transaction) {
value = value.map(transaction.changes);
for (const effect of transaction.effects) {
if (effect.is(ACTIVE_QUERY_EFFECT_ADD)) {
if (effect.value.from === effect.value.to) {
return value;
}
value = value.update({
add: [ACTIVE_QUERY_DECORATION.range(effect.value.from, effect.value.to)],
});
}
if (effect.is(ACTIVE_QUERY_EFFECT_CLEAR)) {
value = value.update({
filter: () => false,
});
}
}
return value;
},
provide: f => EditorView.decorations.from(f),
});
export function highlightActiveQuery(view: EditorView, from: number, to: number | undefined) {
view.dispatch({
effects: ACTIVE_QUERY_EFFECT_ADD.of({ from, to }),
});
}
export function clearActiveQueryHighlight(view: EditorView) {
view.dispatch({
effects: ACTIVE_QUERY_EFFECT_CLEAR.of(null),
});
}
@@ -1,78 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { type EditorView, gutter, GutterMarker, RangeSet, StateEffect, StateField } from '@cloudbeaver/plugin-codemirror6';
type QueryGutterEffectType = 'run' | 'error';
const QUERY_STATUS_SIZE_MARKER = new (class extends GutterMarker {})();
const RUN_QUERY_MARKER = new (class extends GutterMarker {
override toDOM(): Node {
const span = document.createElement('div');
span.className = 'running-query-line';
return span;
}
})();
const ERROR_QUERY_MARKER = new (class extends GutterMarker {
override toDOM(): Node {
const span = document.createElement('div');
span.className = 'running-query-error-line';
return span;
}
})();
const QUERY_GUTTER_EFFECT = StateEffect.define<{ pos: number; on: boolean; type: QueryGutterEffectType }>({
map: (val, mapping) => ({ pos: mapping.mapPos(val.pos), on: val.on, type: val.type }),
});
const statusGutterState = StateField.define<RangeSet<GutterMarker>>({
create() {
return RangeSet.empty;
},
update(set, transaction) {
set = set.map(transaction.changes);
for (const effect of transaction.effects) {
if (effect.is(QUERY_GUTTER_EFFECT)) {
if (effect.value.on) {
const markers = [];
if (effect.value.type === 'run') {
markers.push(RUN_QUERY_MARKER.range(effect.value.pos));
}
if (effect.value.type === 'error') {
markers.push(ERROR_QUERY_MARKER.range(effect.value.pos));
}
set = set.update({ add: markers });
} else {
set = set.update({ filter: () => false });
}
}
}
return set;
},
});
export function setGutter(view: EditorView, pos: number, type: QueryGutterEffectType, value: boolean) {
view.dispatch({
effects: QUERY_GUTTER_EFFECT.of({ pos, on: value, type }),
});
}
export const QUERY_STATUS_GUTTER_EXTENSION = [
statusGutterState,
gutter({
class: 'query-status',
markers: view => view.state.field(statusGutterState),
initialSpacer: () => QUERY_STATUS_SIZE_MARKER,
}),
];
@@ -1,17 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import { forwardRef } from 'react';
import { EditorLoader, type IDefaultExtensions, type IEditorProps, type IEditorRef } from '@cloudbeaver/plugin-codemirror6';
export const SQLCodeEditor = observer<IEditorProps & IDefaultExtensions, IEditorRef>(
forwardRef(function SQLCodeEditor(props, ref) {
return <EditorLoader {...props} ref={ref} />;
}),
);
@@ -1,10 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { importLazyComponent } from '@cloudbeaver/core-blocks';
export const SQLCodeEditorLoader = importLazyComponent(() => import('./SQLCodeEditor.js').then(m => m.SQLCodeEditor));
@@ -1,74 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observable } from 'mobx';
import { useObservableRef } from '@cloudbeaver/core-blocks';
import type { EditorState, EditorView, IEditorRef } from '@cloudbeaver/plugin-codemirror6';
import { clearActiveQueryHighlight, highlightActiveQuery } from '../ACTIVE_QUERY_EXTENSION.js';
import { setGutter } from '../QUERY_STATUS_GUTTER_EXTENSION.js';
export interface IEditor {
readonly view: EditorView | null;
readonly state: EditorState | null;
highlightActiveQuery: (from: number, to?: number) => void;
clearActiveQueryHighlight: () => void;
highlightExecutingLine: (line: number, state: boolean) => void;
highlightExecutingErrorLine: (line: number, state: boolean) => void;
resetLineStateHighlight: () => void;
}
export function useSQLCodeEditor(editorRef: IEditorRef | null) {
const state: IEditor = useObservableRef(
() => ({
get view() {
return this.editorRef?.view ?? null;
},
get state() {
return this.editorRef?.view?.state ?? null;
},
highlightActiveQuery(from: number, to?: number) {
if (!this.view) {
return;
}
// If the 'to' parameter is provided, ensure it doesn't exceed the document length
if (to && to > this.view.state.doc.length) {
return;
}
highlightActiveQuery(this.view, from, to);
},
clearActiveQueryHighlight() {
if (this.view) {
clearActiveQueryHighlight(this.view);
}
},
highlightExecutingLine(line: number, state: boolean) {
if (this.view) {
setGutter(this.view, line, 'run', state);
}
},
highlightExecutingErrorLine(line: number, state: boolean) {
if (this.view) {
setGutter(this.view, line, 'error', state);
}
},
resetLineStateHighlight() {
if (this.view) {
setGutter(this.view, 0, 'run', false);
setGutter(this.view, 0, 'error', false);
}
},
}),
{ editorRef: observable.ref },
{ editorRef },
);
return state;
}
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -16,13 +16,15 @@ import { type TabContainerPanelComponent, useDNDBox } from '@cloudbeaver/core-ui
import { closeCompletion, type IEditorRef, Prec, ReactCodemirrorPanel, useCodemirrorExtensions } from '@cloudbeaver/plugin-codemirror6';
import { SqlEditorSettingsService, type ISqlEditorModeProps } from '@cloudbeaver/plugin-sql-editor';
import { ACTIVE_QUERY_EXTENSION } from '../ACTIVE_QUERY_EXTENSION.js';
import { QUERY_STATUS_GUTTER_EXTENSION } from '../QUERY_STATUS_GUTTER_EXTENSION.js';
import { SQLCodeEditorLoader } from '../SQLCodeEditor/SQLCodeEditorLoader.js';
import { useSQLCodeEditor } from '../SQLCodeEditor/useSQLCodeEditor.js';
import {
ACTIVE_QUERY_EXTENSION,
QUERY_STATUS_GUTTER_EXTENSION,
SQLCodeEditor,
useSQLCodeEditor,
useSqlDialectExtension,
} from '@cloudbeaver/plugin-sql-editor-codemirror';
import { useHighlightExtensions } from '../useHighlightExtensions.js';
import { useSqlDialectAutocompletion } from '../useSqlDialectAutocompletion.js';
import { useSqlDialectExtension } from '../useSqlDialectExtension.js';
import style from './SQLCodeEditorPanel.module.css';
import { SqlEditorInfoBar } from './SqlEditorInfoBar.js';
import { useSQLCodeEditorPanel } from './useSQLCodeEditorPanel.js';
@@ -109,7 +111,7 @@ export const SQLCodeEditorPanel: TabContainerPanelComponent<ISqlEditorModeProps>
return (
<div ref={dndBox.setRef} className={styles['box']}>
<SQLCodeEditorLoader
<SQLCodeEditor
ref={setEditorRef}
getValue={() => data.value}
cursor={{
@@ -143,7 +145,7 @@ export const SQLCodeEditorPanel: TabContainerPanelComponent<ISqlEditorModeProps>
<SqlEditorInfoBar state={editor.state} />
</ReactCodemirrorPanel>
)}
</SQLCodeEditorLoader>
</SQLCodeEditor>
</div>
);
});
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -10,13 +10,14 @@ import { useCallback } from 'react';
import { useExecutor, useObservableRef } from '@cloudbeaver/core-blocks';
import { throttle } from '@cloudbeaver/core-utils';
import type { ISqlEditorCursor, ISQLEditorData } from '@cloudbeaver/plugin-sql-editor';
import type { ISQLEditorData } from '@cloudbeaver/plugin-sql-editor';
import type { IEditor } from '../SQLCodeEditor/useSQLCodeEditor.js';
import type { IEditor } from '@cloudbeaver/plugin-sql-editor-codemirror';
import type { SelectionRange } from '@cloudbeaver/plugin-codemirror6';
interface State {
highlightActiveQuery: () => void;
onQueryChange: (query: string, selection: ISqlEditorCursor) => void;
onQueryChange: (query: string, selection: SelectionRange) => void;
onCursorChange: (anchor: number, head?: number) => void;
}
@@ -34,9 +35,8 @@ export function useSQLCodeEditorPanel(data: ISQLEditorData, editor: IEditor) {
this.editor.highlightActiveQuery(segment.begin, segment.end);
}
},
onQueryChange(query: string, selection: ISqlEditorCursor) {
this.data.setScript(query, ON_QUERY_CHANGE_SOURCE, selection);
this.onCursorChange(selection.anchor, selection.head);
onQueryChange(query: string, selection: SelectionRange) {
this.data.setScript(query, ON_QUERY_CHANGE_SOURCE, { anchor: selection.anchor, head: selection.head }); // do not pass selection directly because anchor and head are getters
},
onCursorChange(anchor: number, head?: number) {
this.data.setCursor(anchor, head);
@@ -1,80 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { useMemo } from 'react';
import { createLazyLoader, useLazyImport } from '@cloudbeaver/core-blocks';
import type { SqlDialectInfo } from '@cloudbeaver/core-sdk';
import type { Compartment, Extension } from '@cloudbeaver/plugin-codemirror6';
const codemirrorPluginLoader = createLazyLoader(() => import('@cloudbeaver/plugin-codemirror6'));
export function useSqlDialectExtension(dialectInfo: SqlDialectInfo | undefined): [Compartment, Extension] | null {
const codemirror = useLazyImport(codemirrorPluginLoader);
const loader = getDialectLoader(codemirror, dialectInfo?.name);
const dialect = useLazyImport(loader);
const SQL_EDITOR_COMPARTMENT = useMemo(() => {
if (!codemirror) {
return null;
}
return new codemirror.Compartment();
}, [codemirror]);
return useMemo(() => {
if (!dialect || !codemirror || !SQL_EDITOR_COMPARTMENT) {
return null;
}
let dialectInner = dialect;
if (dialectInfo) {
dialectInner = codemirror.SQLDialect.define({
keywords: dialectInfo.reservedWords.join(' ').toLowerCase(),
builtin: dialectInfo.functions.join(' ').toUpperCase(),
types: dialectInfo.dataTypes.join(' ').toUpperCase(),
hashComments: dialectInfo.singleLineComments.includes('#'),
slashComments: dialectInfo.singleLineComments.includes('//'),
doubleDollarQuotedStrings: dialectInfo.quoteStrings.some(v => v.includes('$$')),
doubleQuotedStrings: dialectInfo.quoteStrings.some(v => v.includes('"')),
});
}
return [
SQL_EDITOR_COMPARTMENT,
codemirror.SQL_EDITOR({
dialect: dialectInner,
}),
];
}, [SQL_EDITOR_COMPARTMENT, codemirror, dialect, dialectInfo]);
}
const emptyDialectLoader = createLazyLoader(() => Promise.resolve(null));
function getDialectLoader(plugin: typeof import('@cloudbeaver/plugin-codemirror6') | null, name?: string) {
if (!plugin) {
return emptyDialectLoader;
}
switch (name) {
case 'PostgreSQL':
return plugin.PostgreSQLLoader;
case 'MySQL':
return plugin.MySQLLoader;
case 'MariaSQL':
return plugin.MariaSQLLoader;
case 'SQLServer':
return plugin.MSSQLLoader;
case 'SQLite':
return plugin.SQLiteLoader;
case 'CQL':
return plugin.CassandraLoader;
case 'PLSQL':
return plugin.PLSQLLoader;
default:
return plugin.StandardSQLLoader;
}
}
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -8,6 +8,4 @@
import './module.js';
export * from './SQLEditor/SQLCodeEditor/SQLCodeEditorLoader.js';
export * from './SQLEditor/useSqlDialectAutocompletion.js';
export * from './SQLEditor/useSqlDialectExtension.js';