Skip to content

Commit 0c8346e

Browse files
feat(Query): support PostgreSQL syntax (#515)
1 parent e315ca4 commit 0c8346e

File tree

15 files changed

+148
-47
lines changed

15 files changed

+148
-47
lines changed

src/containers/Tenant/Query/QueriesHistory/QueriesHistory.scss

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@
99
@include flex-container();
1010
@include table-styles;
1111

12+
&__table-row {
13+
cursor: pointer;
14+
}
15+
1216
&__query {
1317
overflow: hidden;
1418
flex-grow: 1;
1519

16-
cursor: pointer;
1720
white-space: pre;
1821
text-overflow: ellipsis;
1922
}

src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ import block from 'bem-cn-lite';
33

44
import DataTable, {Column} from '@gravity-ui/react-data-table';
55

6+
import type {QueryInHistory} from '../../../../types/store/executeQuery';
67
import {TruncatedQuery} from '../../../../components/TruncatedQuery/TruncatedQuery';
78
import {setQueryTab} from '../../../../store/reducers/tenant/tenant';
9+
import {selectQueriesHistory} from '../../../../store/reducers/executeQuery';
810
import {TENANT_QUERY_TABS_ID} from '../../../../store/reducers/tenant/constants';
9-
import {useTypedSelector} from '../../../../utils/hooks';
11+
import {useQueryModes, useTypedSelector} from '../../../../utils/hooks';
12+
import {QUERY_MODES, QUERY_MODES_TITLES, QUERY_SYNTAX} from '../../../../utils/query';
1013
import {MAX_QUERY_HEIGHT, QUERY_TABLE_SETTINGS} from '../../utils/constants';
1114

1215
import i18n from '../i18n';
@@ -22,24 +25,51 @@ interface QueriesHistoryProps {
2225
function QueriesHistory({changeUserInput}: QueriesHistoryProps) {
2326
const dispatch = useDispatch();
2427

25-
const queriesHistory = useTypedSelector((state) => state.executeQuery.history.queries) ?? [];
28+
const [queryMode, setQueryMode] = useQueryModes();
29+
30+
const queriesHistory = useTypedSelector(selectQueriesHistory);
2631
const reversedHistory = [...queriesHistory].reverse();
2732

28-
const onQueryClick = (queryText: string) => {
29-
changeUserInput({input: queryText});
30-
dispatch(setQueryTab(TENANT_QUERY_TABS_ID.newQuery));
33+
const onQueryClick = (query: QueryInHistory) => {
34+
let isQueryModeSet = true;
35+
36+
if (query.syntax === QUERY_SYNTAX.pg && queryMode !== QUERY_MODES.pg) {
37+
isQueryModeSet = setQueryMode(
38+
QUERY_MODES.pg,
39+
i18n('history.cannot-set-mode', {mode: QUERY_MODES_TITLES[QUERY_MODES.pg]}),
40+
);
41+
} else if (query.syntax !== QUERY_SYNTAX.pg && queryMode === QUERY_MODES.pg) {
42+
// Set query mode for queries with yql syntax
43+
isQueryModeSet = setQueryMode(QUERY_MODES.script);
44+
}
45+
46+
if (isQueryModeSet) {
47+
changeUserInput({input: query.queryText});
48+
dispatch(setQueryTab(TENANT_QUERY_TABS_ID.newQuery));
49+
}
3150
};
3251

33-
const columns: Column<string>[] = [
52+
const columns: Column<QueryInHistory>[] = [
3453
{
3554
name: 'queryText',
3655
header: 'Query Text',
37-
render: ({row: query}) => (
38-
<div className={b('query')}>
39-
<TruncatedQuery value={query} maxQueryHeight={MAX_QUERY_HEIGHT} />
40-
</div>
41-
),
56+
render: ({row}) => {
57+
return (
58+
<div className={b('query')}>
59+
<TruncatedQuery value={row.queryText} maxQueryHeight={MAX_QUERY_HEIGHT} />
60+
</div>
61+
);
62+
},
63+
sortable: false,
64+
},
65+
{
66+
name: 'syntax',
67+
header: 'Syntax',
68+
render: ({row}) => {
69+
return row.syntax === QUERY_SYNTAX.pg ? 'PostgreSQL' : 'YQL';
70+
},
4271
sortable: false,
72+
width: 200,
4373
},
4474
];
4575

@@ -52,6 +82,7 @@ function QueriesHistory({changeUserInput}: QueriesHistoryProps) {
5282
settings={QUERY_TABLE_SETTINGS}
5383
emptyDataMessage={i18n('history.empty')}
5484
onRowClick={(row) => onQueryClick(row)}
85+
rowClassName={() => b('table-row')}
5586
/>
5687
</div>
5788
);

src/containers/Tenant/Query/QueryEditor/QueryEditor.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ function QueryEditor(props) {
264264

265265
const {queries, currentIndex} = history;
266266
if (input !== queries[currentIndex]) {
267-
saveQueryToHistory(input);
267+
saveQueryToHistory(input, mode);
268268
}
269269
dispatchResultVisibilityState(PaneVisibilityActionTypes.triggerExpand);
270270
};

src/containers/Tenant/Query/QueryEditorControls/QueryEditorControls.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {Button, ButtonView, DropdownMenu} from '@gravity-ui/uikit';
44
import {useMemo} from 'react';
55

66
import type {QueryAction, QueryMode} from '../../../../types/store/query';
7-
import {QUERY_MODES} from '../../../../utils/query';
7+
import {QUERY_MODES, QUERY_MODES_TITLES} from '../../../../utils/query';
88
import {Icon} from '../../../../components/Icon';
99
import {LabelWithPopover} from '../../../../components/LabelWithPopover';
1010

@@ -21,32 +21,36 @@ const b = block('ydb-query-editor-controls');
2121

2222
const OldQueryModeSelectorOptions = {
2323
[QUERY_MODES.script]: {
24-
title: 'YQL Script',
24+
title: QUERY_MODES_TITLES[QUERY_MODES.script],
2525
description: i18n('method-description.script'),
2626
},
2727
[QUERY_MODES.scan]: {
28-
title: 'Scan',
28+
title: QUERY_MODES_TITLES[QUERY_MODES.scan],
2929
description: i18n('method-description.scan'),
3030
},
3131
} as const;
3232

3333
const QueryModeSelectorOptions = {
3434
[QUERY_MODES.script]: {
35-
title: 'YQL Script',
35+
title: QUERY_MODES_TITLES[QUERY_MODES.script],
3636
description: i18n('method-description.script'),
3737
},
3838
[QUERY_MODES.scan]: {
39-
title: 'Scan',
39+
title: QUERY_MODES_TITLES[QUERY_MODES.scan],
4040
description: i18n('method-description.scan'),
4141
},
4242
[QUERY_MODES.data]: {
43-
title: 'Data',
43+
title: QUERY_MODES_TITLES[QUERY_MODES.data],
4444
description: i18n('method-description.data'),
4545
},
4646
[QUERY_MODES.query]: {
47-
title: 'YQL - QueryService',
47+
title: QUERY_MODES_TITLES[QUERY_MODES.query],
4848
description: i18n('method-description.query'),
4949
},
50+
[QUERY_MODES.pg]: {
51+
title: QUERY_MODES_TITLES[QUERY_MODES.pg],
52+
description: i18n('method-description.pg'),
53+
},
5054
} as const;
5155

5256
interface QueryEditorControlsProps {

src/containers/Tenant/Query/i18n/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
"history.empty": "History is empty",
99
"saved.empty": "There are no saved queries",
1010

11+
"history.cannot-set-mode": "This query is available only with '{{mode}}' query mode. You need to turn in additional query modes in settings to enable it",
12+
1113
"delete-dialog.header": "Delete query",
1214
"delete-dialog.question": "Are you sure you want to delete query",
1315
"delete-dialog.delete": "Delete",
@@ -21,6 +23,7 @@
2123
"method-description.scan": "Read-only queries, potentially reading a lot of data.\nAPI call: table.ExecuteScan",
2224
"method-description.data": "DML queries for changing and fetching data in serialization mode.\nAPI call: table.executeDataQuery",
2325
"method-description.query": "Any query. An experimental API call supposed to replace all existing methods.\nAPI Call: query.ExecuteScript",
26+
"method-description.pg": "Queries in postgresql syntax.\nAPI call: query.ExecuteScript",
2427

2528
"query-duration.description": "Duration of server-side query execution"
2629
}

src/containers/Tenant/Query/i18n/ru.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
"history.empty": "История пуста",
99
"saved.empty": "Нет сохраненных запросов",
1010

11+
"history.cannot-set-mode": "Этот запрос доступен только в режиме '{{mode}}'. Вам необходимо включить дополнительные режимы выполнения запросов в настройках",
12+
1113
"delete-dialog.header": "Удалить запрос",
1214
"delete-dialog.question": "Вы уверены что хотите удалить запрос",
1315
"delete-dialog.delete": "Удалить",
@@ -21,6 +23,7 @@
2123
"method-description.scan": "Только читающие запросы, потенциально читающие много данных.\nAPI call: table.ExecuteScan",
2224
"method-description.data": "DML-запросы для изменения и выборки данных в режиме изоляции Serializable.\nAPI call: table.executeDataQuery",
2325
"method-description.query": "Любые запросы. Экспериментальный перспективный метод, который в будущем заменит все остальные.\nAPI call: query.ExecuteScript",
26+
"method-description.pg": "Запросы в синтаксисе postgresql.\nAPI call: query.ExecuteScript",
2427

2528
"query-duration.description": "Время выполнения запроса на стороне сервера"
2629
}

src/services/api.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import type {DescribeTopicResult} from '../types/api/topic';
2828
import type {TEvPDiskStateResponse} from '../types/api/pdisk';
2929
import type {TEvVDiskStateResponse} from '../types/api/vdisk';
3030
import type {TUserToken} from '../types/api/whoami';
31+
import type {QuerySyntax} from '../types/store/query';
3132
import type {ComputeApiRequestParams, NodesApiRequestParams} from '../store/reducers/nodes/types';
3233
import type {StorageApiRequestParams} from '../store/reducers/storage/types';
3334

@@ -281,17 +282,15 @@ export class YdbEmbeddedAPI extends AxiosWrapper {
281282
}
282283
sendQuery<Action extends Actions, Schema extends Schemas = undefined>(
283284
{
284-
query,
285-
database,
286-
action,
287-
stats,
288285
schema,
286+
...params
289287
}: {
290288
query?: string;
291289
database?: string;
292290
action?: Action;
293291
stats?: string;
294292
schema?: Schema;
293+
syntax?: QuerySyntax;
295294
},
296295
{concurrentId}: AxiosOptions = {},
297296
) {
@@ -303,12 +302,7 @@ export class YdbEmbeddedAPI extends AxiosWrapper {
303302
this.getPath(
304303
`/viewer/json/query?timeout=${backendTimeout}${schema ? `&schema=${schema}` : ''}`,
305304
),
306-
{
307-
query,
308-
database,
309-
action,
310-
stats,
311-
},
305+
params,
312306
{},
313307
{
314308
concurrentId,
@@ -320,13 +314,15 @@ export class YdbEmbeddedAPI extends AxiosWrapper {
320314
query: string,
321315
database: string,
322316
action: Action,
317+
syntax?: QuerySyntax,
323318
) {
324319
return this.post<ExplainResponse<Action>>(
325320
this.getPath('/viewer/json/query'),
326321
{
327322
query,
328323
database,
329324
action: action || 'explain',
325+
syntax,
330326
timeout: 600000,
331327
},
332328
{},

src/store/reducers/executeQuery.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import type {ExecuteActions} from '../../types/api/query';
44
import type {
55
ExecuteQueryAction,
66
ExecuteQueryState,
7+
ExecuteQueryStateSlice,
78
MonacoHotKeyAction,
9+
QueryInHistory,
810
} from '../../types/store/executeQuery';
9-
import type {QueryRequestParams, QueryMode} from '../../types/store/query';
11+
import type {QueryRequestParams, QueryMode, QuerySyntax} from '../../types/store/query';
1012
import {getValueFromLS, parseJson} from '../../utils/utils';
1113
import {QUERIES_HISTORY_KEY} from '../../utils/constants';
12-
import {parseQueryAPIExecuteResponse} from '../../utils/query';
14+
import {QUERY_MODES, QUERY_SYNTAX, parseQueryAPIExecuteResponse} from '../../utils/query';
1315
import {parseQueryError} from '../../utils/error';
1416
import '../../services/api';
1517

@@ -87,8 +89,12 @@ const executeQuery: Reducer<ExecuteQueryState, ExecuteQueryAction> = (
8789
}
8890

8991
case SAVE_QUERY_TO_HISTORY: {
90-
const query = action.data;
91-
const newQueries = [...state.history.queries, query].slice(
92+
const queryText = action.data.queryText;
93+
94+
// Do not save explicit yql syntax value for easier further support (use yql by default)
95+
const syntax = action.data.mode === QUERY_MODES.pg ? QUERY_SYNTAX.pg : undefined;
96+
97+
const newQueries = [...state.history.queries, {queryText, syntax}].slice(
9298
state.history.queries.length >= MAXIMUM_QUERIES_IN_HISTORY ? 1 : 0,
9399
);
94100
window.localStorage.setItem(QUERIES_HISTORY_KEY, JSON.stringify(newQueries));
@@ -151,25 +157,34 @@ interface SendQueryParams extends QueryRequestParams {
151157
}
152158

153159
export const sendExecuteQuery = ({query, database, mode}: SendQueryParams) => {
154-
const action: ExecuteActions = mode ? `execute-${mode}` : 'execute';
160+
let action: ExecuteActions = 'execute';
161+
let syntax: QuerySyntax = QUERY_SYNTAX.yql;
162+
163+
if (mode === 'pg') {
164+
action = 'execute-query';
165+
syntax = QUERY_SYNTAX.pg;
166+
} else if (mode) {
167+
action = `execute-${mode}`;
168+
}
155169

156170
return createApiRequest({
157171
request: window.api.sendQuery({
158172
schema: 'modern',
159173
query,
160174
database,
161175
action,
176+
syntax,
162177
stats: 'profile',
163178
}),
164179
actions: SEND_QUERY,
165180
dataHandler: parseQueryAPIExecuteResponse,
166181
});
167182
};
168183

169-
export const saveQueryToHistory = (query: string) => {
184+
export const saveQueryToHistory = (queryText: string, mode: QueryMode) => {
170185
return {
171186
type: SAVE_QUERY_TO_HISTORY,
172-
data: query,
187+
data: {queryText, mode},
173188
} as const;
174189
};
175190

@@ -206,4 +221,15 @@ export const setTenantPath = (value: string) => {
206221
} as const;
207222
};
208223

224+
export const selectQueriesHistory = (state: ExecuteQueryStateSlice): QueryInHistory[] => {
225+
return state.executeQuery.history.queries.map((rawQuery) => {
226+
if (typeof rawQuery === 'string') {
227+
return {
228+
queryText: rawQuery,
229+
};
230+
}
231+
return rawQuery;
232+
});
233+
};
234+
209235
export default executeQuery;

src/store/reducers/explainQuery.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import type {
99
ExplainQueryState,
1010
PreparedExplainResponse,
1111
} from '../../types/store/explainQuery';
12-
import type {QueryRequestParams, QueryMode} from '../../types/store/query';
12+
import type {QueryRequestParams, QueryMode, QuerySyntax} from '../../types/store/query';
1313

1414
import {preparePlan} from '../../utils/prepareQueryExplain';
15-
import {parseQueryAPIExplainResponse, parseQueryExplainPlan} from '../../utils/query';
15+
import {QUERY_SYNTAX, parseQueryAPIExplainResponse, parseQueryExplainPlan} from '../../utils/query';
1616
import {parseQueryError} from '../../utils/error';
1717

1818
import {createRequestActionTypes, createApiRequest} from '../utils';
@@ -103,10 +103,18 @@ interface ExplainQueryParams extends QueryRequestParams {
103103
}
104104

105105
export const getExplainQuery = ({query, database, mode}: ExplainQueryParams) => {
106-
const action: ExplainActions = mode ? `explain-${mode}` : 'explain';
106+
let action: ExplainActions = 'explain';
107+
let syntax: QuerySyntax = QUERY_SYNTAX.yql;
108+
109+
if (mode === 'pg') {
110+
action = 'explain-query';
111+
syntax = QUERY_SYNTAX.pg;
112+
} else if (mode) {
113+
action = `explain-${mode}`;
114+
}
107115

108116
return createApiRequest({
109-
request: window.api.getExplainQuery(query, database, action),
117+
request: window.api.getExplainQuery(query, database, action, syntax),
110118
actions: GET_EXPLAIN_QUERY,
111119
dataHandler: (response): PreparedExplainResponse => {
112120
const {plan: rawPlan, ast} = parseQueryAPIExplainResponse(response);

0 commit comments

Comments
 (0)