From 26659baf8f10be6294a3e4b4352092bb51ebcf88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Bedi?= Date: Wed, 2 Nov 2022 05:30:35 +0100 Subject: [PATCH] PostgreSQL: Migrate to React (#52831) - Migrate Postgres query editor to react - Add support for field aliasing in SELECT clauses to SQL based datasources Co-authored-by: Kyle Cunningham Co-authored-by: Oscar Kilhed --- .betterer.results | 151 ---- .../plugins/sql/ResponseParser.ts} | 4 +- .../plugins/sql/components/QueryEditor.tsx | 9 +- .../plugins/sql/components/QueryHeader.tsx | 22 +- .../plugins/sql/components/TableSelector.tsx | 5 +- .../visual-query-builder/SQLSelectRow.tsx | 10 +- .../visual-query-builder/SelectRow.tsx | 45 +- .../plugins/sql/datasource/SqlDatasource.ts | 9 +- .../app/features/plugins/sql/expressions.ts | 1 + .../features/plugins/sql/utils/sql.utils.ts | 6 +- .../plugins/datasource/mssql/datasource.ts | 13 +- .../app/plugins/datasource/mssql/sqlUtil.ts | 6 +- .../datasource/mysql/MySqlDatasource.ts | 15 +- .../datasource/mysql/MySqlResponseParser.ts | 27 - .../datasource/mysql/specs/datasource.test.ts | 2 +- public/app/plugins/datasource/mysql/types.ts | 8 - .../datasource/postgres/PostgresQueryModel.ts | 25 + .../datasource/postgres/QueryEditor.tsx | 13 + .../configuration/useAutoDetectFeatures.ts | 2 +- .../datasource/postgres/datasource.test.ts | 648 +++++++++++++++- .../plugins/datasource/postgres/datasource.ts | 282 ++----- .../plugins/datasource/postgres/meta_query.ts | 169 ----- .../plugins/datasource/postgres/mode-sql.js | 114 --- .../app/plugins/datasource/postgres/module.ts | 34 +- .../postgres/partials/annotations.editor.html | 56 -- .../postgres/partials/query.editor.html | 191 ----- .../datasource/postgres/postgresMetaQuery.ts | 41 ++ .../postgres/postgres_query_model.ts | 298 -------- .../plugins/datasource/postgres/query_ctrl.ts | 693 ------------------ .../datasource/postgres/response_parser.ts | 73 -- .../postgres/specs/datasource.test.ts | 659 ----------------- .../postgres/specs/postgres_query.test.ts | 175 ----- .../postgres/sqlCompletionProvider.ts | 50 ++ .../plugins/datasource/postgres/sqlUtil.ts | 112 +++ .../plugins/datasource/postgres/sql_part.ts | 139 ---- .../app/plugins/datasource/postgres/types.ts | 38 +- 36 files changed, 1052 insertions(+), 3093 deletions(-) rename public/app/{plugins/datasource/mssql/response_parser.ts => features/plugins/sql/ResponseParser.ts} (83%) delete mode 100644 public/app/plugins/datasource/mysql/MySqlResponseParser.ts create mode 100644 public/app/plugins/datasource/postgres/PostgresQueryModel.ts create mode 100644 public/app/plugins/datasource/postgres/QueryEditor.tsx delete mode 100644 public/app/plugins/datasource/postgres/meta_query.ts delete mode 100644 public/app/plugins/datasource/postgres/mode-sql.js delete mode 100644 public/app/plugins/datasource/postgres/partials/annotations.editor.html delete mode 100644 public/app/plugins/datasource/postgres/partials/query.editor.html create mode 100644 public/app/plugins/datasource/postgres/postgresMetaQuery.ts delete mode 100644 public/app/plugins/datasource/postgres/postgres_query_model.ts delete mode 100644 public/app/plugins/datasource/postgres/query_ctrl.ts delete mode 100644 public/app/plugins/datasource/postgres/response_parser.ts delete mode 100644 public/app/plugins/datasource/postgres/specs/datasource.test.ts delete mode 100644 public/app/plugins/datasource/postgres/specs/postgres_query.test.ts create mode 100644 public/app/plugins/datasource/postgres/sqlCompletionProvider.ts create mode 100644 public/app/plugins/datasource/postgres/sqlUtil.ts delete mode 100644 public/app/plugins/datasource/postgres/sql_part.ts diff --git a/.betterer.results b/.betterer.results index 6c3ec87ca59..5b3c6e7e05b 100644 --- a/.betterer.results +++ b/.betterer.results @@ -6355,16 +6355,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "1"], [0, 0, 0, "Do not use any type assertions.", "2"] ], - "public/app/plugins/datasource/mysql/specs/datasource.test.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "public/app/plugins/datasource/mysql/types.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"] - ], "public/app/plugins/datasource/opentsdb/datasource.d.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], @@ -6435,147 +6425,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "public/app/plugins/datasource/postgres/datasource.test.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] - ], - "public/app/plugins/datasource/postgres/datasource.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Do not use any type assertions.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"], - [0, 0, 0, "Do not use any type assertions.", "9"], - [0, 0, 0, "Do not use any type assertions.", "10"], - [0, 0, 0, "Do not use any type assertions.", "11"], - [0, 0, 0, "Unexpected any. Specify a different type.", "12"], - [0, 0, 0, "Unexpected any. Specify a different type.", "13"], - [0, 0, 0, "Unexpected any. Specify a different type.", "14"] - ], - "public/app/plugins/datasource/postgres/module.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] - ], - "public/app/plugins/datasource/postgres/postgres_query_model.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"], - [0, 0, 0, "Unexpected any. Specify a different type.", "9"], - [0, 0, 0, "Unexpected any. Specify a different type.", "10"], - [0, 0, 0, "Unexpected any. Specify a different type.", "11"], - [0, 0, 0, "Unexpected any. Specify a different type.", "12"], - [0, 0, 0, "Unexpected any. Specify a different type.", "13"], - [0, 0, 0, "Unexpected any. Specify a different type.", "14"], - [0, 0, 0, "Unexpected any. Specify a different type.", "15"], - [0, 0, 0, "Unexpected any. Specify a different type.", "16"], - [0, 0, 0, "Unexpected any. Specify a different type.", "17"], - [0, 0, 0, "Unexpected any. Specify a different type.", "18"], - [0, 0, 0, "Unexpected any. Specify a different type.", "19"], - [0, 0, 0, "Unexpected any. Specify a different type.", "20"], - [0, 0, 0, "Unexpected any. Specify a different type.", "21"] - ], - "public/app/plugins/datasource/postgres/query_ctrl.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"], - [0, 0, 0, "Unexpected any. Specify a different type.", "9"], - [0, 0, 0, "Unexpected any. Specify a different type.", "10"], - [0, 0, 0, "Unexpected any. Specify a different type.", "11"], - [0, 0, 0, "Unexpected any. Specify a different type.", "12"], - [0, 0, 0, "Unexpected any. Specify a different type.", "13"], - [0, 0, 0, "Unexpected any. Specify a different type.", "14"], - [0, 0, 0, "Unexpected any. Specify a different type.", "15"], - [0, 0, 0, "Unexpected any. Specify a different type.", "16"], - [0, 0, 0, "Unexpected any. Specify a different type.", "17"], - [0, 0, 0, "Unexpected any. Specify a different type.", "18"], - [0, 0, 0, "Unexpected any. Specify a different type.", "19"], - [0, 0, 0, "Unexpected any. Specify a different type.", "20"], - [0, 0, 0, "Unexpected any. Specify a different type.", "21"], - [0, 0, 0, "Unexpected any. Specify a different type.", "22"], - [0, 0, 0, "Unexpected any. Specify a different type.", "23"], - [0, 0, 0, "Unexpected any. Specify a different type.", "24"], - [0, 0, 0, "Unexpected any. Specify a different type.", "25"], - [0, 0, 0, "Unexpected any. Specify a different type.", "26"], - [0, 0, 0, "Unexpected any. Specify a different type.", "27"], - [0, 0, 0, "Do not use any type assertions.", "28"], - [0, 0, 0, "Do not use any type assertions.", "29"], - [0, 0, 0, "Unexpected any. Specify a different type.", "30"], - [0, 0, 0, "Unexpected any. Specify a different type.", "31"], - [0, 0, 0, "Unexpected any. Specify a different type.", "32"], - [0, 0, 0, "Unexpected any. Specify a different type.", "33"], - [0, 0, 0, "Unexpected any. Specify a different type.", "34"], - [0, 0, 0, "Unexpected any. Specify a different type.", "35"], - [0, 0, 0, "Unexpected any. Specify a different type.", "36"], - [0, 0, 0, "Unexpected any. Specify a different type.", "37"], - [0, 0, 0, "Unexpected any. Specify a different type.", "38"], - [0, 0, 0, "Unexpected any. Specify a different type.", "39"], - [0, 0, 0, "Unexpected any. Specify a different type.", "40"], - [0, 0, 0, "Unexpected any. Specify a different type.", "41"], - [0, 0, 0, "Unexpected any. Specify a different type.", "42"], - [0, 0, 0, "Unexpected any. Specify a different type.", "43"], - [0, 0, 0, "Unexpected any. Specify a different type.", "44"], - [0, 0, 0, "Unexpected any. Specify a different type.", "45"], - [0, 0, 0, "Unexpected any. Specify a different type.", "46"], - [0, 0, 0, "Unexpected any. Specify a different type.", "47"], - [0, 0, 0, "Unexpected any. Specify a different type.", "48"], - [0, 0, 0, "Unexpected any. Specify a different type.", "49"], - [0, 0, 0, "Unexpected any. Specify a different type.", "50"], - [0, 0, 0, "Unexpected any. Specify a different type.", "51"], - [0, 0, 0, "Unexpected any. Specify a different type.", "52"], - [0, 0, 0, "Unexpected any. Specify a different type.", "53"], - [0, 0, 0, "Unexpected any. Specify a different type.", "54"], - [0, 0, 0, "Unexpected any. Specify a different type.", "55"], - [0, 0, 0, "Unexpected any. Specify a different type.", "56"], - [0, 0, 0, "Unexpected any. Specify a different type.", "57"], - [0, 0, 0, "Unexpected any. Specify a different type.", "58"], - [0, 0, 0, "Unexpected any. Specify a different type.", "59"] - ], - "public/app/plugins/datasource/postgres/response_parser.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"] - ], - "public/app/plugins/datasource/postgres/specs/datasource.test.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"] - ], - "public/app/plugins/datasource/postgres/specs/postgres_query.test.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] - ], - "public/app/plugins/datasource/postgres/sql_part.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"] - ], - "public/app/plugins/datasource/postgres/types.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"] - ], "public/app/plugins/datasource/prometheus/components/PromExploreExtraField.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/public/app/plugins/datasource/mssql/response_parser.ts b/public/app/features/plugins/sql/ResponseParser.ts similarity index 83% rename from public/app/plugins/datasource/mssql/response_parser.ts rename to public/app/features/plugins/sql/ResponseParser.ts index a23672002e1..3de28c88edd 100644 --- a/public/app/plugins/datasource/mssql/response_parser.ts +++ b/public/app/features/plugins/sql/ResponseParser.ts @@ -1,9 +1,9 @@ import { uniqBy } from 'lodash'; import { DataFrame, MetricFindValue } from '@grafana/data'; -import { ResponseParser } from 'app/features/plugins/sql/types'; +import { ResponseParser as ResponseParserType } from 'app/features/plugins/sql/types'; -export class MSSqlResponseParser implements ResponseParser { +export class ResponseParser implements ResponseParserType { transformMetricFindResponse(frame: DataFrame): MetricFindValue[] { const values: MetricFindValue[] = []; const textField = frame.fields.find((f) => f.name === '__text'); diff --git a/public/app/features/plugins/sql/components/QueryEditor.tsx b/public/app/features/plugins/sql/components/QueryEditor.tsx index 14274be3774..961417552e3 100644 --- a/public/app/features/plugins/sql/components/QueryEditor.tsx +++ b/public/app/features/plugins/sql/components/QueryEditor.tsx @@ -9,13 +9,15 @@ import { applyQueryDefaults } from '../defaults'; import { SQLQuery, QueryRowFilter, SQLOptions, EditorMode } from '../types'; import { haveColumns } from '../utils/sql.utils'; -import { QueryHeader } from './QueryHeader'; +import { QueryHeader, QueryHeaderProps } from './QueryHeader'; import { RawEditor } from './query-editor-raw/RawEditor'; import { VisualEditor } from './visual-query-builder/VisualEditor'; -type Props = QueryEditorProps; +interface Props extends QueryEditorProps { + queryHeaderProps?: Pick; +} -export function SqlQueryEditor({ datasource, query, onChange, onRunQuery, range }: Props) { +export function SqlQueryEditor({ datasource, query, onChange, onRunQuery, range, queryHeaderProps }: Props) { const [isQueryRunnable, setIsQueryRunnable] = useState(true); const db = datasource.getDB(); const { loading, error } = useAsync(async () => { @@ -84,6 +86,7 @@ export function SqlQueryEditor({ datasource, query, onChange, onRunQuery, range queryRowFilter={queryRowFilter} query={queryWithDefaults} isQueryRunnable={isQueryRunnable} + {...queryHeaderProps} /> diff --git a/public/app/features/plugins/sql/components/QueryHeader.tsx b/public/app/features/plugins/sql/components/QueryHeader.tsx index eab9bf3a517..1aa1201570b 100644 --- a/public/app/features/plugins/sql/components/QueryHeader.tsx +++ b/public/app/features/plugins/sql/components/QueryHeader.tsx @@ -14,7 +14,7 @@ import { DatasetSelector } from './DatasetSelector'; import { ErrorBoundary } from './ErrorBoundary'; import { TableSelector } from './TableSelector'; -interface QueryHeaderProps { +export interface QueryHeaderProps { db: DB; query: QueryWithDefaults; onChange: (query: SQLQuery) => void; @@ -22,6 +22,7 @@ interface QueryHeaderProps { onQueryRowChange: (queryRowFilter: QueryRowFilter) => void; queryRowFilter: QueryRowFilter; isQueryRunnable: boolean; + isDatasetSelectorHidden?: boolean; } const editorModes = [ @@ -37,6 +38,7 @@ export function QueryHeader({ onRunQuery, onQueryRowChange, isQueryRunnable, + isDatasetSelectorHidden, }: QueryHeaderProps) { const { editorMode } = query; const [_, copyToClipboard] = useCopyToClipboard(); @@ -218,15 +220,16 @@ export function QueryHeader({ {editorMode === EditorMode.Builder && ( <> - - - - + {isDatasetSelectorHidden ? null : ( + + + + )} diff --git a/public/app/features/plugins/sql/components/TableSelector.tsx b/public/app/features/plugins/sql/components/TableSelector.tsx index 65171e2b918..e2a32dee385 100644 --- a/public/app/features/plugins/sql/components/TableSelector.tsx +++ b/public/app/features/plugins/sql/components/TableSelector.tsx @@ -12,11 +12,12 @@ interface TableSelectorProps extends ResourceSelectorProps { value: string | null; query: QueryWithDefaults; onChange: (v: SelectableValue) => void; + forceFetch?: boolean; } -export const TableSelector: React.FC = ({ db, query, value, className, onChange }) => { +export const TableSelector: React.FC = ({ db, query, value, className, onChange, forceFetch }) => { const state = useAsync(async () => { - if (!query.dataset) { + if (!query.dataset && !forceFetch) { return []; } const tables = await db.tables(query.dataset); diff --git a/public/app/features/plugins/sql/components/visual-query-builder/SQLSelectRow.tsx b/public/app/features/plugins/sql/components/visual-query-builder/SQLSelectRow.tsx index ca3470d0ce0..b486d4998ff 100644 --- a/public/app/features/plugins/sql/components/visual-query-builder/SQLSelectRow.tsx +++ b/public/app/features/plugins/sql/components/visual-query-builder/SQLSelectRow.tsx @@ -24,5 +24,13 @@ export function SQLSelectRow({ fields, query, onQueryChange, db }: SQLSelectRowP return functions.map((f) => toOption(f.name)); }, [db]); - return ; + return ( + + ); } diff --git a/public/app/features/plugins/sql/components/visual-query-builder/SelectRow.tsx b/public/app/features/plugins/sql/components/visual-query-builder/SelectRow.tsx index b11c7e5b357..e1f0970a346 100644 --- a/public/app/features/plugins/sql/components/visual-query-builder/SelectRow.tsx +++ b/public/app/features/plugins/sql/components/visual-query-builder/SelectRow.tsx @@ -7,11 +7,12 @@ import { EditorField, Stack } from '@grafana/experimental'; import { Button, Select, useStyles2 } from '@grafana/ui'; import { QueryEditorExpressionType, QueryEditorFunctionExpression } from '../../expressions'; -import { SQLExpression } from '../../types'; +import { SQLExpression, QueryFormat } from '../../types'; import { createFunctionField } from '../../utils/sql.utils'; interface SelectRowProps { sql: SQLExpression; + format: QueryFormat | undefined; onSqlChange: (sql: SQLExpression) => void; columns?: Array>; functions?: Array>; @@ -19,9 +20,17 @@ interface SelectRowProps { const asteriskValue = { label: '*', value: '*' }; -export function SelectRow({ sql, columns, onSqlChange, functions }: SelectRowProps) { +export function SelectRow({ sql, format, columns, onSqlChange, functions }: SelectRowProps) { const styles = useStyles2(getStyles); const columnsWithAsterisk = [asteriskValue, ...(columns || [])]; + const timeSeriesAliasOpts: Array> = []; + + // Add necessary alias options for time series format + // when that format has been selected + if (format === QueryFormat.Timeseries) { + timeSeriesAliasOpts.push({ label: 'time', value: 'time' }); + timeSeriesAliasOpts.push({ label: 'value', value: 'value' }); + } const onColumnChange = useCallback( (item: QueryEditorFunctionExpression, index: number) => (column: SelectableValue) => { @@ -60,6 +69,26 @@ export function SelectRow({ sql, columns, onSqlChange, functions }: SelectRowPro [onSqlChange, sql] ); + const onAliasChange = useCallback( + (item: QueryEditorFunctionExpression, index: number) => (alias: SelectableValue) => { + let newItem = { ...item }; + + if (alias !== null) { + newItem = { ...item, alias: `"${alias?.value?.trim()}"` }; + } else { + delete newItem.alias; + } + + const newSql: SQLExpression = { + ...sql, + columns: sql.columns?.map((c, i) => (i === index ? newItem : c)), + }; + + onSqlChange(newSql); + }, + [onSqlChange, sql] + ); + const removeColumn = useCallback( (index: number) => () => { const clone = [...sql.columns!]; @@ -79,7 +108,7 @@ export function SelectRow({ sql, columns, onSqlChange, functions }: SelectRowPro }, [onSqlChange, sql]); return ( - + {sql.columns?.map((item, index) => (
@@ -105,6 +134,16 @@ export function SelectRow({ sql, columns, onSqlChange, functions }: SelectRowPro onChange={onAggregationChange(item, index)} /> + + -
- - -
-
- -
-
- -
-
-
Annotation Query Format
-An annotation is an event that is overlaid on top of graphs. The query can have up to four columns per row, the time column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned. - -- column with alias: time for the annotation event time. Use epoch time or any native date data type. -- column with alias: timeend for the annotation event time-end. Use epoch time or any native date data type. -- column with alias: text for the annotation text -- column with alias: tags for annotation tags. This is a comma separated string of tags e.g. 'tag1,tag2' - - -Macros: -- $__time(column) -> column as "time" -- $__timeEpoch -> extract(epoch from column) as "time" -- $__timeFilter(column) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z' -- $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877 -- $__unixEpochNanoFilter(column) -> column >= 1494410783152415214 AND column <= 1494497183142514872 - -Or build your own conditionals using these macros which just return the values: -- $__timeFrom() -> '2017-04-21T05:01:17Z' -- $__timeTo() -> '2017-04-21T05:01:17Z' -- $__unixEpochFrom() -> 1492750877 -- $__unixEpochTo() -> 1492750877 -- $__unixEpochNanoFrom() -> 1494410783152415214 -- $__unixEpochNanoTo() -> 1494497183142514872 -
-
-
- diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html deleted file mode 100644 index ebf670cc4aa..00000000000 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ /dev/null @@ -1,191 +0,0 @@ - - -
-
-
- - -
-
-
- -
-
-
- - - - - - - - -
- -
-
-
- -
- -
-
- -
- -
- - -
- -
- -
- -
-
-
-
- -
-
- -
- -
- - -
- -
- -
- -
-
-
- -
- -
-
- - - - -
- -
- -
- -
-
-
-
- -
- -
-
- -
- -
-
-
- -
-
- -
-
- -
-
-
-
-
- - -
-
Time series:
-- return column named time (UTC in seconds or timestamp)
-- return column(s) with numeric datatype as values
-Optional:
-  - return column named metric to represent the series name.
-  - If multiple value columns are returned the metric column is used as prefix.
-  - If no column named metric is found the column name of the value column is used as series name
-
-Resultsets of time series queries need to be sorted by time.
-
-Table:
-- return any set of columns
-
-Macros:
-- $__time(column) -> column as "time"
-- $__timeEpoch -> extract(epoch from column) as "time"
-- $__timeFilter(column) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z'
-- $__unixEpochFilter(column) ->  column >= 1492750877 AND column <= 1492750877
-- $__unixEpochNanoFilter(column) ->  column >= 1494410783152415214 AND column <= 1494497183142514872
-- $__timeGroup(column,'5m'[, fillvalue]) -> (extract(epoch from column)/300)::bigint*300
-     by setting fillvalue grafana will fill in missing values according to the interval
-     fillvalue can be either a literal value, NULL or previous; previous will fill in the previous seen value or NULL if none has been seen yet
-- $__timeGroupAlias(column,'5m') -> (extract(epoch from column)/300)::bigint*300 AS "time"
-- $__unixEpochGroup(column,'5m') -> floor(column/300)*300
-- $__unixEpochGroupAlias(column,'5m') -> floor(column/300)*300 AS "time"
-
-Example of group by and order by with $__timeGroup:
-SELECT
-  $__timeGroup(date_time_col, '1h'),
-  sum(value) as value
-FROM yourtable
-GROUP BY time
-ORDER BY time
-
-Or build your own conditionals using these macros which just return the values:
-- $__timeFrom() ->  '2017-04-21T05:01:17Z'
-- $__timeTo() ->  '2017-04-21T05:01:17Z'
-- $__unixEpochFrom() ->  1492750877
-- $__unixEpochTo() ->  1492750877
-- $__unixEpochNanoFrom() ->  1494410783152415214
-- $__unixEpochNanoTo() ->  1494497183142514872
-    
-
- - - -
-
{{ctrl.lastQueryMeta.executedQueryString}}
-
- -
-
{{ctrl.lastQueryError}}
-
- -
diff --git a/public/app/plugins/datasource/postgres/postgresMetaQuery.ts b/public/app/plugins/datasource/postgres/postgresMetaQuery.ts new file mode 100644 index 00000000000..7a56a81b441 --- /dev/null +++ b/public/app/plugins/datasource/postgres/postgresMetaQuery.ts @@ -0,0 +1,41 @@ +export function getVersion() { + return "SELECT current_setting('server_version_num')::int/100 as version"; +} + +export function getTimescaleDBVersion() { + return "SELECT extversion FROM pg_extension WHERE extname = 'timescaledb'"; +} + +export function showTables() { + return `select quote_ident(table_name) as "table" from information_schema.tables + where quote_ident(table_schema) not in ('information_schema', + 'pg_catalog', + '_timescaledb_cache', + '_timescaledb_catalog', + '_timescaledb_internal', + '_timescaledb_config', + 'timescaledb_information', + 'timescaledb_experimental') + and table_type = 'BASE TABLE' and ${buildSchemaConstraint()}`; +} + +export function getSchema(table?: string) { + return `select quote_ident(column_name) as "column", data_type as "type" + from information_schema.columns + where quote_ident(table_name) = '${table}'`; +} + +function buildSchemaConstraint() { + // quote_ident protects hyphenated schemes + return ` + quote_ident(table_schema) IN ( + SELECT + CASE WHEN trim(s[i]) = '"$user"' THEN user ELSE trim(s[i]) END + FROM + generate_series( + array_lower(string_to_array(current_setting('search_path'),','),1), + array_upper(string_to_array(current_setting('search_path'),','),1) + ) as i, + string_to_array(current_setting('search_path'),',') s + )`; +} diff --git a/public/app/plugins/datasource/postgres/postgres_query_model.ts b/public/app/plugins/datasource/postgres/postgres_query_model.ts deleted file mode 100644 index 287b4ba1e0a..00000000000 --- a/public/app/plugins/datasource/postgres/postgres_query_model.ts +++ /dev/null @@ -1,298 +0,0 @@ -import { find, map } from 'lodash'; - -import { ScopedVars } from '@grafana/data'; -import { TemplateSrv } from '@grafana/runtime'; - -export default class PostgresQueryModel { - target: any; - templateSrv: any; - scopedVars: any; - - /** @ngInject */ - constructor(target: any, templateSrv?: TemplateSrv, scopedVars?: ScopedVars) { - this.target = target; - this.templateSrv = templateSrv; - this.scopedVars = scopedVars; - - target.format = target.format || 'time_series'; - target.timeColumn = target.timeColumn || 'time'; - target.metricColumn = target.metricColumn || 'none'; - - target.group = target.group || []; - target.where = target.where || [{ type: 'macro', name: '$__timeFilter', params: [] }]; - target.select = target.select || [[{ type: 'column', params: ['value'] }]]; - - // handle pre query gui panels gracefully - if (!('rawQuery' in this.target)) { - if ('rawSql' in target) { - // pre query gui panel - target.rawQuery = true; - } else { - // new panel - target.rawQuery = false; - } - } - - // give interpolateQueryStr access to this - this.interpolateQueryStr = this.interpolateQueryStr.bind(this); - } - - // remove identifier quoting from identifier to use in metadata queries - unquoteIdentifier(value: string) { - if (value[0] === '"' && value[value.length - 1] === '"') { - return value.substring(1, value.length - 1).replace(/""/g, '"'); - } else { - return value; - } - } - - quoteIdentifier(value: any) { - return '"' + String(value).replace(/"/g, '""') + '"'; - } - - quoteLiteral(value: any) { - return "'" + String(value).replace(/'/g, "''") + "'"; - } - - escapeLiteral(value: any) { - return String(value).replace(/'/g, "''"); - } - - hasTimeGroup() { - return find(this.target.group, (g: any) => g.type === 'time'); - } - - hasMetricColumn() { - return this.target.metricColumn !== 'none'; - } - - interpolateQueryStr(value: any, variable: { multi: any; includeAll: any }, defaultFormatFn: any) { - // if no multi or include all do not regexEscape - if (!variable.multi && !variable.includeAll) { - return this.escapeLiteral(value); - } - - if (typeof value === 'string') { - return this.quoteLiteral(value); - } - - const escapedValues = map(value, this.quoteLiteral); - return escapedValues.join(','); - } - - render(interpolate?: any) { - const target = this.target; - - // new query with no table set yet - if (!this.target.rawQuery && !('table' in this.target)) { - return ''; - } - - if (!target.rawQuery) { - target.rawSql = this.buildQuery(); - } - - if (interpolate) { - return this.templateSrv.replace(target.rawSql, this.scopedVars, this.interpolateQueryStr); - } else { - return target.rawSql; - } - } - - hasUnixEpochTimecolumn() { - return ['int4', 'int8', 'float4', 'float8', 'numeric'].indexOf(this.target.timeColumnType) > -1; - } - - buildTimeColumn(alias = true) { - const timeGroup = this.hasTimeGroup(); - let query; - let macro = '$__timeGroup'; - - if (timeGroup) { - let args; - if (timeGroup.params.length > 1 && timeGroup.params[1] !== 'none') { - args = timeGroup.params.join(','); - } else { - args = timeGroup.params[0]; - } - if (this.hasUnixEpochTimecolumn()) { - macro = '$__unixEpochGroup'; - } - if (alias) { - macro += 'Alias'; - } - query = macro + '(' + this.target.timeColumn + ',' + args + ')'; - } else { - query = this.target.timeColumn; - if (alias) { - query += ' AS "time"'; - } - } - - return query; - } - - buildMetricColumn() { - if (this.hasMetricColumn()) { - return this.target.metricColumn + ' AS metric'; - } - - return ''; - } - - buildValueColumns() { - let query = ''; - for (const column of this.target.select) { - query += ',\n ' + this.buildValueColumn(column); - } - - return query; - } - - buildValueColumn(column: any) { - let query = ''; - - const columnName: any = find(column, (g: any) => g.type === 'column'); - query = columnName.params[0]; - - const aggregate: any = find(column, (g: any) => g.type === 'aggregate' || g.type === 'percentile'); - const windows: any = find(column, (g: any) => g.type === 'window' || g.type === 'moving_window'); - - if (aggregate) { - const func = aggregate.params[0]; - switch (aggregate.type) { - case 'aggregate': - if (func === 'first' || func === 'last') { - query = func + '(' + query + ',' + this.target.timeColumn + ')'; - } else { - query = func + '(' + query + ')'; - } - break; - case 'percentile': - query = func + '(' + aggregate.params[1] + ') WITHIN GROUP (ORDER BY ' + query + ')'; - break; - } - } - - if (windows) { - const overParts = []; - if (this.hasMetricColumn()) { - overParts.push('PARTITION BY ' + this.target.metricColumn); - } - overParts.push('ORDER BY ' + this.buildTimeColumn(false)); - - const over = overParts.join(' '); - let curr: string; - let prev: string; - switch (windows.type) { - case 'window': - switch (windows.params[0]) { - case 'delta': - curr = query; - prev = 'lag(' + curr + ') OVER (' + over + ')'; - query = curr + ' - ' + prev; - break; - case 'increase': - curr = query; - prev = 'lag(' + curr + ') OVER (' + over + ')'; - query = '(CASE WHEN ' + curr + ' >= ' + prev + ' THEN ' + curr + ' - ' + prev; - query += ' WHEN ' + prev + ' IS NULL THEN NULL ELSE ' + curr + ' END)'; - break; - case 'rate': - let timeColumn = this.target.timeColumn; - if (aggregate) { - timeColumn = 'min(' + timeColumn + ')'; - } - - curr = query; - prev = 'lag(' + curr + ') OVER (' + over + ')'; - query = '(CASE WHEN ' + curr + ' >= ' + prev + ' THEN ' + curr + ' - ' + prev; - query += ' WHEN ' + prev + ' IS NULL THEN NULL ELSE ' + curr + ' END)'; - query += '/extract(epoch from ' + timeColumn + ' - lag(' + timeColumn + ') OVER (' + over + '))'; - break; - default: - query = windows.params[0] + '(' + query + ') OVER (' + over + ')'; - break; - } - break; - case 'moving_window': - query = windows.params[0] + '(' + query + ') OVER (' + over + ' ROWS ' + windows.params[1] + ' PRECEDING)'; - break; - } - } - - const alias: any = find(column, (g: any) => g.type === 'alias'); - if (alias) { - query += ' AS ' + this.quoteIdentifier(alias.params[0]); - } - - return query; - } - - buildWhereClause() { - let query = ''; - const conditions = map(this.target.where, (tag, index) => { - switch (tag.type) { - case 'macro': - return tag.name + '(' + this.target.timeColumn + ')'; - break; - case 'expression': - return tag.params.join(' '); - break; - } - }); - - if (conditions.length > 0) { - query = '\nWHERE\n ' + conditions.join(' AND\n '); - } - - return query; - } - - buildGroupClause() { - let query = ''; - let groupSection = ''; - - for (let i = 0; i < this.target.group.length; i++) { - const part = this.target.group[i]; - if (i > 0) { - groupSection += ', '; - } - if (part.type === 'time') { - groupSection += '1'; - } else { - groupSection += part.params[0]; - } - } - - if (groupSection.length) { - query = '\nGROUP BY ' + groupSection; - if (this.hasMetricColumn()) { - query += ',2'; - } - } - return query; - } - - buildQuery() { - let query = 'SELECT'; - - query += '\n ' + this.buildTimeColumn(); - if (this.hasMetricColumn()) { - query += ',\n ' + this.buildMetricColumn(); - } - query += this.buildValueColumns(); - - query += '\nFROM ' + this.target.table; - - query += this.buildWhereClause(); - query += this.buildGroupClause(); - - query += '\nORDER BY 1'; - if (this.hasMetricColumn()) { - query += ',2'; - } - - return query; - } -} diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts deleted file mode 100644 index 9e8e2cd31ec..00000000000 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ /dev/null @@ -1,693 +0,0 @@ -import { auto } from 'angular'; -import { clone, filter, find, findIndex, indexOf, map } from 'lodash'; - -import { PanelEvents, QueryResultMeta } from '@grafana/data'; -import { TemplateSrv } from '@grafana/runtime'; -import { SqlPart } from 'app/angular/components/sql_part/sql_part'; -import appEvents from 'app/core/app_events'; -import { VariableWithMultiSupport } from 'app/features/variables/types'; -import { QueryCtrl } from 'app/plugins/sdk'; -import { ShowConfirmModalEvent } from 'app/types/events'; - -import { PostgresMetaQuery } from './meta_query'; -import PostgresQueryModel from './postgres_query_model'; -import sqlPart from './sql_part'; - -const defaultQuery = `SELECT - $__time(time_column), - value1 -FROM - metric_table -WHERE - $__timeFilter(time_column) -`; - -export class PostgresQueryCtrl extends QueryCtrl { - static templateUrl = 'partials/query.editor.html'; - - formats: any[]; - queryModel: PostgresQueryModel; - metaBuilder: PostgresMetaQuery; - lastQueryMeta?: QueryResultMeta; - lastQueryError?: string; - showHelp = false; - tableSegment: any; - whereAdd: any; - timeColumnSegment: any; - metricColumnSegment: any; - selectMenu: any[] = []; - selectParts: SqlPart[][] = [[]]; - groupParts: SqlPart[] = []; - whereParts: SqlPart[] = []; - groupAdd: any; - - /** @ngInject */ - constructor( - $scope: any, - $injector: auto.IInjectorService, - private templateSrv: TemplateSrv, - private uiSegmentSrv: any - ) { - super($scope, $injector); - this.target = this.target; - this.queryModel = new PostgresQueryModel(this.target, templateSrv, this.panel.scopedVars); - this.metaBuilder = new PostgresMetaQuery(this.target, this.queryModel); - this.updateProjection(); - - this.formats = [ - { text: 'Time series', value: 'time_series' }, - { text: 'Table', value: 'table' }, - ]; - - if (!this.target.rawSql) { - // special handling when in table panel - if (this.panelCtrl.panel.type === 'table') { - this.target.format = 'table'; - this.target.rawSql = 'SELECT 1'; - this.target.rawQuery = true; - } else { - this.target.rawSql = defaultQuery; - this.datasource.metricFindQuery(this.metaBuilder.findMetricTable()).then((result: any) => { - if (result.length > 0) { - this.target.table = result[0].text; - let segment = this.uiSegmentSrv.newSegment(this.target.table); - this.tableSegment.html = segment.html; - this.tableSegment.value = segment.value; - - this.target.timeColumn = result[1].text; - segment = this.uiSegmentSrv.newSegment(this.target.timeColumn); - this.timeColumnSegment.html = segment.html; - this.timeColumnSegment.value = segment.value; - - this.target.timeColumnType = 'timestamp'; - this.target.select = [[{ type: 'column', params: [result[2].text] }]]; - this.updateProjection(); - this.updateRawSqlAndRefresh(); - } - }); - } - } - - if (!this.target.table) { - this.tableSegment = uiSegmentSrv.newSegment({ value: 'select table', fake: true }); - } else { - this.tableSegment = uiSegmentSrv.newSegment(this.target.table); - } - - this.timeColumnSegment = uiSegmentSrv.newSegment(this.target.timeColumn); - this.metricColumnSegment = uiSegmentSrv.newSegment(this.target.metricColumn); - - this.buildSelectMenu(); - this.whereAdd = this.uiSegmentSrv.newPlusButton(); - this.groupAdd = this.uiSegmentSrv.newPlusButton(); - - this.panelCtrl.events.on(PanelEvents.dataReceived, this.onDataReceived.bind(this), $scope); - this.panelCtrl.events.on(PanelEvents.dataError, this.onDataError.bind(this), $scope); - } - - updateRawSqlAndRefresh() { - if (!this.target.rawQuery) { - this.target.rawSql = this.queryModel.buildQuery(); - } - - this.panelCtrl.refresh(); - } - - timescaleAggCheck() { - const aggIndex = this.findAggregateIndex(this.selectParts[0]); - - // add or remove TimescaleDB aggregate functions as needed - if (aggIndex !== -1) { - const baseOpts = this.selectParts[0][aggIndex].def.params[0].baseOptions; - const timescaleOpts = baseOpts.concat(this.selectParts[0][aggIndex].def.params[0].timescaleOptions); - - if (this.datasource.jsonData.timescaledb === true) { - this.selectParts[0][aggIndex].def.params[0].options = timescaleOpts; - } else { - this.selectParts[0][aggIndex].def.params[0].options = baseOpts; - } - } - } - - updateProjection() { - this.selectParts = map(this.target.select, (parts: any) => { - return map(parts, sqlPart.create).filter((n) => n); - }); - this.timescaleAggCheck(); - this.whereParts = map(this.target.where, sqlPart.create).filter((n) => n); - this.groupParts = map(this.target.group, sqlPart.create).filter((n) => n); - } - - updatePersistedParts() { - this.target.select = map(this.selectParts, (selectParts) => { - return map(selectParts, (part: any) => { - return { type: part.def.type, datatype: part.datatype, params: part.params }; - }); - }); - this.timescaleAggCheck(); - this.target.where = map(this.whereParts, (part: any) => { - return { type: part.def.type, datatype: part.datatype, name: part.name, params: part.params }; - }); - this.target.group = map(this.groupParts, (part: any) => { - return { type: part.def.type, datatype: part.datatype, params: part.params }; - }); - } - - buildSelectMenu() { - this.selectMenu = []; - const aggregates = { - text: 'Aggregate Functions', - value: 'aggregate', - submenu: [ - { text: 'Average', value: 'avg' }, - { text: 'Count', value: 'count' }, - { text: 'Maximum', value: 'max' }, - { text: 'Minimum', value: 'min' }, - { text: 'Sum', value: 'sum' }, - { text: 'Standard deviation', value: 'stddev' }, - { text: 'Variance', value: 'variance' }, - ], - }; - - // first and last aggregate are timescaledb specific - if (this.datasource.jsonData.timescaledb === true) { - aggregates.submenu.push({ text: 'First', value: 'first' }); - aggregates.submenu.push({ text: 'Last', value: 'last' }); - } - - this.selectMenu.push(aggregates); - - // ordered set aggregates require postgres 9.4+ - if (this.datasource.jsonData.postgresVersion >= 904) { - const aggregates2 = { - text: 'Ordered-Set Aggregate Functions', - value: 'percentile', - submenu: [ - { text: 'Percentile (continuous)', value: 'percentile_cont' }, - { text: 'Percentile (discrete)', value: 'percentile_disc' }, - ], - }; - this.selectMenu.push(aggregates2); - } - - const windows = { - text: 'Window Functions', - value: 'window', - submenu: [ - { text: 'Delta', value: 'delta' }, - { text: 'Increase', value: 'increase' }, - { text: 'Rate', value: 'rate' }, - { text: 'Sum', value: 'sum' }, - { text: 'Moving Average', value: 'avg', type: 'moving_window' }, - ], - }; - this.selectMenu.push(windows); - - this.selectMenu.push({ text: 'Alias', value: 'alias' }); - this.selectMenu.push({ text: 'Column', value: 'column' }); - } - - toggleEditorMode() { - if (this.target.rawQuery) { - appEvents.publish( - new ShowConfirmModalEvent({ - title: 'Warning', - text2: 'Switching to query builder may overwrite your raw SQL.', - icon: 'exclamation-triangle', - yesText: 'Switch', - onConfirm: () => { - // This could be called from React, so wrap in $evalAsync. - // Will then either run as part of the current digest cycle or trigger a new one. - this.$scope.$evalAsync(() => { - this.target.rawQuery = !this.target.rawQuery; - }); - }, - }) - ); - } else { - // This could be called from React, so wrap in $evalAsync. - // Will then either run as part of the current digest cycle or trigger a new one. - this.$scope.$evalAsync(() => { - this.target.rawQuery = !this.target.rawQuery; - }); - } - } - - resetPlusButton(button: { html: any; value: any; type: any; fake: any }) { - const plusButton = this.uiSegmentSrv.newPlusButton(); - button.html = plusButton.html; - button.value = plusButton.value; - button.type = plusButton.type; - button.fake = plusButton.fake; - } - - getTableSegments() { - return this.datasource - .metricFindQuery(this.metaBuilder.buildTableQuery()) - .then(this.transformToSegments({})) - .catch(this.handleQueryError.bind(this)); - } - - tableChanged() { - this.target.table = this.tableSegment.value; - this.target.where = []; - this.target.group = []; - this.updateProjection(); - - const segment = this.uiSegmentSrv.newSegment('none'); - this.metricColumnSegment.html = segment.html; - this.metricColumnSegment.value = segment.value; - this.target.metricColumn = 'none'; - - const task1 = this.datasource.metricFindQuery(this.metaBuilder.buildColumnQuery('time')).then((result: any) => { - // check if time column is still valid - if (result.length > 0 && !find(result, (r: any) => r.text === this.target.timeColumn)) { - const segment = this.uiSegmentSrv.newSegment(result[0].text); - this.timeColumnSegment.html = segment.html; - this.timeColumnSegment.value = segment.value; - } - return this.timeColumnChanged(false); - }); - const task2 = this.datasource.metricFindQuery(this.metaBuilder.buildColumnQuery('value')).then((result: any) => { - if (result.length > 0) { - this.target.select = [[{ type: 'column', params: [result[0].text] }]]; - this.updateProjection(); - } - }); - - Promise.all([task1, task2]).then(() => { - this.updateRawSqlAndRefresh(); - }); - } - - getTimeColumnSegments() { - return this.datasource - .metricFindQuery(this.metaBuilder.buildColumnQuery('time')) - .then(this.transformToSegments({})) - .catch(this.handleQueryError.bind(this)); - } - - timeColumnChanged(refresh?: boolean) { - this.target.timeColumn = this.timeColumnSegment.value; - return this.datasource - .metricFindQuery(this.metaBuilder.buildDatatypeQuery(this.target.timeColumn)) - .then((result: any) => { - if (result.length === 1) { - if (this.target.timeColumnType !== result[0].text) { - this.target.timeColumnType = result[0].text; - } - let partModel; - if (this.queryModel.hasUnixEpochTimecolumn()) { - partModel = sqlPart.create({ type: 'macro', name: '$__unixEpochFilter', params: [] }); - } else { - partModel = sqlPart.create({ type: 'macro', name: '$__timeFilter', params: [] }); - } - - if (this.whereParts.length >= 1 && this.whereParts[0].def.type === 'macro') { - // replace current macro - this.whereParts[0] = partModel; - } else { - this.whereParts.splice(0, 0, partModel); - } - } - - this.updatePersistedParts(); - if (refresh !== false) { - this.updateRawSqlAndRefresh(); - } - }); - } - - getMetricColumnSegments() { - return this.datasource - .metricFindQuery(this.metaBuilder.buildColumnQuery('metric')) - .then(this.transformToSegments({ addNone: true })) - .catch(this.handleQueryError.bind(this)); - } - - metricColumnChanged() { - this.target.metricColumn = this.metricColumnSegment.value; - this.updateRawSqlAndRefresh(); - } - - onDataReceived(dataList: any) { - this.lastQueryError = undefined; - this.lastQueryMeta = dataList[0]?.meta; - } - - onDataError(err: any) { - if (err.data && err.data.results) { - const queryRes = err.data.results[this.target.refId]; - if (queryRes) { - this.lastQueryError = queryRes.error; - } - } - } - - transformToSegments(config: { addNone?: any; addTemplateVars?: any; templateQuoter?: any }) { - return (results: any) => { - const segments = map(results, (segment) => { - return this.uiSegmentSrv.newSegment({ - value: segment.text, - expandable: segment.expandable, - }); - }); - - if (config.addTemplateVars) { - for (const variable of this.templateSrv.getVariables()) { - let value; - value = '$' + variable.name; - if (config.templateQuoter && (variable as unknown as VariableWithMultiSupport).multi === false) { - value = config.templateQuoter(value); - } - - segments.unshift( - this.uiSegmentSrv.newSegment({ - type: 'template', - value: value, - expandable: true, - }) - ); - } - } - - if (config.addNone) { - segments.unshift(this.uiSegmentSrv.newSegment({ type: 'template', value: 'none', expandable: true })); - } - - return segments; - }; - } - - findAggregateIndex(selectParts: any) { - return findIndex(selectParts, (p: any) => p.def.type === 'aggregate' || p.def.type === 'percentile'); - } - - findWindowIndex(selectParts: any) { - return findIndex(selectParts, (p: any) => p.def.type === 'window' || p.def.type === 'moving_window'); - } - - addSelectPart(selectParts: any[], item: { value: any }, subItem: { type: any; value: any }) { - let partType = item.value; - if (subItem && subItem.type) { - partType = subItem.type; - } - let partModel = sqlPart.create({ type: partType }); - if (subItem) { - partModel.params[0] = subItem.value; - } - let addAlias = false; - - switch (partType) { - case 'column': - const parts = map(selectParts, (part: any) => { - return sqlPart.create({ type: part.def.type, params: clone(part.params) }); - }); - this.selectParts.push(parts); - break; - case 'percentile': - case 'aggregate': - // add group by if no group by yet - if (this.target.group.length === 0) { - this.addGroup('time', '$__interval'); - } - const aggIndex = this.findAggregateIndex(selectParts); - if (aggIndex !== -1) { - // replace current aggregation - selectParts[aggIndex] = partModel; - } else { - selectParts.splice(1, 0, partModel); - } - if (!find(selectParts, (p: any) => p.def.type === 'alias')) { - addAlias = true; - } - break; - case 'moving_window': - case 'window': - const windowIndex = this.findWindowIndex(selectParts); - if (windowIndex !== -1) { - // replace current window function - selectParts[windowIndex] = partModel; - } else { - const aggIndex = this.findAggregateIndex(selectParts); - if (aggIndex !== -1) { - selectParts.splice(aggIndex + 1, 0, partModel); - } else { - selectParts.splice(1, 0, partModel); - } - } - if (!find(selectParts, (p: any) => p.def.type === 'alias')) { - addAlias = true; - } - break; - case 'alias': - addAlias = true; - break; - } - - if (addAlias) { - // set initial alias name to column name - partModel = sqlPart.create({ type: 'alias', params: [selectParts[0].params[0].replace(/"/g, '')] }); - if (selectParts[selectParts.length - 1].def.type === 'alias') { - selectParts[selectParts.length - 1] = partModel; - } else { - selectParts.push(partModel); - } - } - - this.updatePersistedParts(); - this.updateRawSqlAndRefresh(); - } - - removeSelectPart(selectParts: any, part: { def: { type: string } }) { - if (part.def.type === 'column') { - // remove all parts of column unless its last column - if (this.selectParts.length > 1) { - const modelsIndex = indexOf(this.selectParts, selectParts); - this.selectParts.splice(modelsIndex, 1); - } - } else { - const partIndex = indexOf(selectParts, part); - selectParts.splice(partIndex, 1); - } - - this.updatePersistedParts(); - } - - handleSelectPartEvent(selectParts: any, part: { def: any }, evt: { name: any }) { - switch (evt.name) { - case 'get-param-options': { - switch (part.def.type) { - case 'aggregate': - return this.datasource - .metricFindQuery(this.metaBuilder.buildAggregateQuery()) - .then(this.transformToSegments({})) - .catch(this.handleQueryError.bind(this)); - case 'column': - return this.datasource - .metricFindQuery(this.metaBuilder.buildColumnQuery('value')) - .then(this.transformToSegments({})) - .catch(this.handleQueryError.bind(this)); - } - } - case 'part-param-changed': { - this.updatePersistedParts(); - this.updateRawSqlAndRefresh(); - break; - } - case 'action': { - this.removeSelectPart(selectParts, part); - this.updateRawSqlAndRefresh(); - break; - } - case 'get-part-actions': { - return Promise.resolve([{ text: 'Remove', value: 'remove-part' }]); - } - } - } - - handleGroupPartEvent(part: any, index: any, evt: { name: any }) { - switch (evt.name) { - case 'get-param-options': { - return this.datasource - .metricFindQuery(this.metaBuilder.buildColumnQuery()) - .then(this.transformToSegments({})) - .catch(this.handleQueryError.bind(this)); - } - case 'part-param-changed': { - this.updatePersistedParts(); - this.updateRawSqlAndRefresh(); - break; - } - case 'action': { - this.removeGroup(part, index); - this.updateRawSqlAndRefresh(); - break; - } - case 'get-part-actions': { - return Promise.resolve([{ text: 'Remove', value: 'remove-part' }]); - } - } - } - - addGroup(partType: string, value: string) { - let params = [value]; - if (partType === 'time') { - params = ['$__interval', 'none']; - } - const partModel = sqlPart.create({ type: partType, params: params }); - - if (partType === 'time') { - // put timeGroup at start - this.groupParts.splice(0, 0, partModel); - } else { - this.groupParts.push(partModel); - } - - // add aggregates when adding group by - for (const selectParts of this.selectParts) { - if (!selectParts.some((part) => part.def.type === 'aggregate')) { - const aggregate = sqlPart.create({ type: 'aggregate', params: ['avg'] }); - selectParts.splice(1, 0, aggregate); - if (!selectParts.some((part) => part.def.type === 'alias')) { - const alias = sqlPart.create({ type: 'alias', params: [selectParts[0].part.params[0]] }); - selectParts.push(alias); - } - } - } - - this.updatePersistedParts(); - } - - removeGroup(part: { def: { type: string } }, index: number) { - if (part.def.type === 'time') { - // remove aggregations - this.selectParts = map(this.selectParts, (s: any) => { - return filter(s, (part: any) => { - if (part.def.type === 'aggregate' || part.def.type === 'percentile') { - return false; - } - return true; - }); - }); - } - - this.groupParts.splice(index, 1); - this.updatePersistedParts(); - } - - handleWherePartEvent(whereParts: any, part: any, evt: any, index: any) { - switch (evt.name) { - case 'get-param-options': { - switch (evt.param.name) { - case 'left': - return this.datasource - .metricFindQuery(this.metaBuilder.buildColumnQuery()) - .then(this.transformToSegments({})) - .catch(this.handleQueryError.bind(this)); - case 'right': - if (['int4', 'int8', 'float4', 'float8', 'timestamp', 'timestamptz'].indexOf(part.datatype) > -1) { - // don't do value lookups for numerical fields - return Promise.resolve([]); - } else { - return this.datasource - .metricFindQuery(this.metaBuilder.buildValueQuery(part.params[0])) - .then( - this.transformToSegments({ - addTemplateVars: true, - templateQuoter: (v: string) => { - return this.queryModel.quoteLiteral(v); - }, - }) - ) - .catch(this.handleQueryError.bind(this)); - } - case 'op': - return Promise.resolve(this.uiSegmentSrv.newOperators(this.metaBuilder.getOperators(part.datatype))); - default: - return Promise.resolve([]); - } - } - case 'part-param-changed': { - this.updatePersistedParts(); - this.datasource.metricFindQuery(this.metaBuilder.buildDatatypeQuery(part.params[0])).then((d: any) => { - if (d.length === 1) { - part.datatype = d[0].text; - } - }); - this.updateRawSqlAndRefresh(); - break; - } - case 'action': { - // remove element - whereParts.splice(index, 1); - this.updatePersistedParts(); - this.updateRawSqlAndRefresh(); - break; - } - case 'get-part-actions': { - return Promise.resolve([{ text: 'Remove', value: 'remove-part' }]); - } - } - } - - getWhereOptions() { - const options = []; - if (this.queryModel.hasUnixEpochTimecolumn()) { - options.push(this.uiSegmentSrv.newSegment({ type: 'macro', value: '$__unixEpochFilter' })); - } else { - options.push(this.uiSegmentSrv.newSegment({ type: 'macro', value: '$__timeFilter' })); - } - options.push(this.uiSegmentSrv.newSegment({ type: 'expression', value: 'Expression' })); - return Promise.resolve(options); - } - - addWhereAction(part: any, index: any) { - switch (this.whereAdd.type) { - case 'macro': { - const partModel = sqlPart.create({ type: 'macro', name: this.whereAdd.value, params: [] }); - if (this.whereParts.length >= 1 && this.whereParts[0].def.type === 'macro') { - // replace current macro - this.whereParts[0] = partModel; - } else { - this.whereParts.splice(0, 0, partModel); - } - break; - } - default: { - this.whereParts.push(sqlPart.create({ type: 'expression', params: ['value', '=', 'value'] })); - } - } - - this.updatePersistedParts(); - this.resetPlusButton(this.whereAdd); - this.updateRawSqlAndRefresh(); - } - - getGroupOptions() { - return this.datasource - .metricFindQuery(this.metaBuilder.buildColumnQuery('group')) - .then((tags: any) => { - const options = []; - if (!this.queryModel.hasTimeGroup()) { - options.push(this.uiSegmentSrv.newSegment({ type: 'time', value: 'time($__interval,none)' })); - } - for (const tag of tags) { - options.push(this.uiSegmentSrv.newSegment({ type: 'column', value: tag.text })); - } - return options; - }) - .catch(this.handleQueryError.bind(this)); - } - - addGroupAction() { - this.addGroup(this.groupAdd.type, this.groupAdd.value); - this.resetPlusButton(this.groupAdd); - this.updateRawSqlAndRefresh(); - } - - handleQueryError(err: any): any[] { - this.error = err.message || 'Failed to issue metric query'; - return []; - } -} diff --git a/public/app/plugins/datasource/postgres/response_parser.ts b/public/app/plugins/datasource/postgres/response_parser.ts deleted file mode 100644 index c5a6e8af852..00000000000 --- a/public/app/plugins/datasource/postgres/response_parser.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { uniqBy } from 'lodash'; - -import { AnnotationEvent, DataFrame, MetricFindValue } from '@grafana/data'; -import { BackendDataSourceResponse, FetchResponse, toDataQueryResponse } from '@grafana/runtime'; - -export default class ResponseParser { - transformMetricFindResponse(raw: FetchResponse): MetricFindValue[] { - const frames = toDataQueryResponse(raw).data as DataFrame[]; - - if (!frames || !frames.length) { - return []; - } - - const frame = frames[0]; - - const values: MetricFindValue[] = []; - const textField = frame.fields.find((f) => f.name === '__text'); - const valueField = frame.fields.find((f) => f.name === '__value'); - - if (textField && valueField) { - for (let i = 0; i < textField.values.length; i++) { - values.push({ text: '' + textField.values.get(i), value: '' + valueField.values.get(i) }); - } - } else { - values.push( - ...frame.fields - .flatMap((f) => f.values.toArray()) - .map((v) => ({ - text: v, - })) - ); - } - - return uniqBy(values, 'text'); - } - - async transformAnnotationResponse(options: any, data: BackendDataSourceResponse): Promise { - const frames = toDataQueryResponse({ data: data }).data as DataFrame[]; - if (!frames || !frames.length) { - return []; - } - const frame = frames[0]; - const timeField = frame.fields.find((f) => f.name === 'time'); - - if (!timeField) { - throw new Error('Missing mandatory time column (with time column alias) in annotation query'); - } - - const timeEndField = frame.fields.find((f) => f.name === 'timeend'); - const textField = frame.fields.find((f) => f.name === 'text'); - const tagsField = frame.fields.find((f) => f.name === 'tags'); - - const list: AnnotationEvent[] = []; - for (let i = 0; i < frame.length; i++) { - const timeEnd = timeEndField && timeEndField.values.get(i) ? Math.floor(timeEndField.values.get(i)) : undefined; - list.push({ - annotation: options.annotation, - time: Math.floor(timeField.values.get(i)), - timeEnd, - text: textField && textField.values.get(i) ? textField.values.get(i) : '', - tags: - tagsField && tagsField.values.get(i) - ? tagsField.values - .get(i) - .trim() - .split(/\s*,\s*/) - : [], - }); - } - - return list; - } -} diff --git a/public/app/plugins/datasource/postgres/specs/datasource.test.ts b/public/app/plugins/datasource/postgres/specs/datasource.test.ts deleted file mode 100644 index c83486e15e4..00000000000 --- a/public/app/plugins/datasource/postgres/specs/datasource.test.ts +++ /dev/null @@ -1,659 +0,0 @@ -import { of } from 'rxjs'; -import { TestScheduler } from 'rxjs/testing'; - -import { - dataFrameToJSON, - DataQueryRequest, - DataSourceInstanceSettings, - dateTime, - MutableDataFrame, -} from '@grafana/data'; -import { FetchResponse } from '@grafana/runtime'; -import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ -import { TemplateSrv } from 'app/features/templating/template_srv'; - -import { initialCustomVariableModelState } from '../../../../features/variables/custom/reducer'; -import { PostgresDatasource } from '../datasource'; -import { PostgresOptions, PostgresQuery } from '../types'; - -jest.mock('@grafana/runtime', () => ({ - ...(jest.requireActual('@grafana/runtime') as unknown as object), - getBackendSrv: () => backendSrv, -})); - -jest.mock('@grafana/runtime/src/services', () => ({ - ...(jest.requireActual('@grafana/runtime/src/services') as unknown as object), - getBackendSrv: () => backendSrv, - getDataSourceSrv: () => { - return { - getInstanceSettings: () => ({ id: 8674 }), - }; - }, -})); - -describe('PostgreSQLDatasource', () => { - const fetchMock = jest.spyOn(backendSrv, 'fetch'); - const setupTestContext = (data: any) => { - jest.clearAllMocks(); - fetchMock.mockImplementation(() => of(createFetchResponse(data))); - const instanceSettings = { - jsonData: { - defaultProject: 'testproject', - }, - } as unknown as DataSourceInstanceSettings; - const templateSrv: TemplateSrv = new TemplateSrv(); - const variable = { ...initialCustomVariableModelState }; - const ds = new PostgresDatasource(instanceSettings, templateSrv); - - return { ds, templateSrv, variable }; - }; - - // https://rxjs-dev.firebaseapp.com/guide/testing/marble-testing - const runMarbleTest = (args: { - options: any; - values: { [marble: string]: FetchResponse }; - marble: string; - expectedValues: { [marble: string]: any }; - expectedMarble: string; - }) => { - const { expectedValues, expectedMarble, options, values, marble } = args; - const scheduler: TestScheduler = new TestScheduler((actual, expected) => { - expect(actual).toEqual(expected); - }); - - const { ds } = setupTestContext({}); - - scheduler.run(({ cold, expectObservable }) => { - const source = cold(marble, values); - jest.clearAllMocks(); - fetchMock.mockImplementation(() => source); - - const result = ds.query(options); - expectObservable(result).toBe(expectedMarble, expectedValues); - }); - }; - - describe('When performing a time series query', () => { - it('should transform response correctly', () => { - const options = { - range: { - from: dateTime(1432288354), - to: dateTime(1432288401), - }, - targets: [ - { - format: 'time_series', - rawQuery: true, - rawSql: 'select time, metric from grafana_metric', - refId: 'A', - datasource: 'gdev-ds', - }, - ], - }; - const response = { - results: { - A: { - refId: 'A', - frames: [ - dataFrameToJSON( - new MutableDataFrame({ - fields: [ - { name: 'time', values: [1599643351085] }, - { name: 'metric', values: [30.226249741223704], labels: { metric: 'America' } }, - ], - meta: { - executedQueryString: 'select time, metric from grafana_metric', - }, - }) - ), - ], - }, - }, - }; - - const values = { a: createFetchResponse(response) }; - const marble = '-a|'; - const expectedMarble = '-a|'; - const expectedValues = { - a: { - data: [ - { - fields: [ - { - config: {}, - entities: {}, - name: 'time', - type: 'time', - values: { - buffer: [1599643351085], - }, - }, - { - config: {}, - entities: {}, - labels: { - metric: 'America', - }, - name: 'metric', - type: 'number', - values: { - buffer: [30.226249741223704], - }, - }, - ], - length: 1, - meta: { - executedQueryString: 'select time, metric from grafana_metric', - }, - name: undefined, - refId: 'A', - }, - ], - state: 'Done', - }, - }; - - runMarbleTest({ options, marble, values, expectedMarble, expectedValues }); - }); - }); - - describe('When performing a table query', () => { - it('should transform response correctly', () => { - const options = { - range: { - from: dateTime(1432288354), - to: dateTime(1432288401), - }, - targets: [ - { - format: 'table', - rawQuery: true, - rawSql: 'select time, metric, value from grafana_metric', - refId: 'A', - datasource: 'gdev-ds', - }, - ], - }; - const response = { - results: { - A: { - refId: 'A', - frames: [ - dataFrameToJSON( - new MutableDataFrame({ - fields: [ - { name: 'time', values: [1599643351085] }, - { name: 'metric', values: ['America'] }, - { name: 'value', values: [30.226249741223704] }, - ], - meta: { - executedQueryString: 'select time, metric, value from grafana_metric', - }, - }) - ), - ], - }, - }, - }; - - const values = { a: createFetchResponse(response) }; - const marble = '-a|'; - const expectedMarble = '-a|'; - const expectedValues = { - a: { - data: [ - { - fields: [ - { - config: {}, - entities: {}, - name: 'time', - type: 'time', - values: { - buffer: [1599643351085], - }, - }, - { - config: {}, - entities: {}, - name: 'metric', - type: 'string', - values: { - buffer: ['America'], - }, - }, - { - config: {}, - entities: {}, - name: 'value', - type: 'number', - values: { - buffer: [30.226249741223704], - }, - }, - ], - length: 1, - meta: { - executedQueryString: 'select time, metric, value from grafana_metric', - }, - name: undefined, - refId: 'A', - }, - ], - state: 'Done', - }, - }; - - runMarbleTest({ options, marble, values, expectedMarble, expectedValues }); - }); - }); - - describe('When performing a query with hidden target', () => { - it('should return empty result and backendSrv.fetch should not be called', async () => { - const options = { - range: { - from: dateTime(1432288354), - to: dateTime(1432288401), - }, - targets: [ - { - format: 'table', - rawQuery: true, - rawSql: 'select time, metric, value from grafana_metric', - refId: 'A', - datasource: 'gdev-ds', - hide: true, - }, - ], - } as unknown as DataQueryRequest; - - const { ds } = setupTestContext({}); - - await expect(ds.query(options)).toEmitValuesWith((received) => { - expect(received[0]).toEqual({ data: [] }); - expect(fetchMock).not.toHaveBeenCalled(); - }); - }); - }); - - describe('When performing annotationQuery', () => { - let results: any; - const annotationName = 'MyAnno'; - const options = { - annotation: { - name: annotationName, - rawQuery: 'select time, title, text, tags from table;', - }, - range: { - from: dateTime(1432288354), - to: dateTime(1432288401), - }, - }; - const response = { - results: { - MyAnno: { - frames: [ - dataFrameToJSON( - new MutableDataFrame({ - fields: [ - { name: 'time', values: [1432288355, 1432288390, 1432288400] }, - { name: 'text', values: ['some text', 'some text2', 'some text3'] }, - { name: 'tags', values: ['TagA,TagB', ' TagB , TagC', null] }, - ], - }) - ), - ], - }, - }, - }; - - beforeEach(async () => { - const { ds } = setupTestContext(response); - results = await ds.annotationQuery(options); - }); - - it('should return annotation list', async () => { - expect(results.length).toBe(3); - - expect(results[0].text).toBe('some text'); - expect(results[0].tags[0]).toBe('TagA'); - expect(results[0].tags[1]).toBe('TagB'); - - expect(results[1].tags[0]).toBe('TagB'); - expect(results[1].tags[1]).toBe('TagC'); - - expect(results[2].tags.length).toBe(0); - }); - }); - - describe('When performing metricFindQuery that returns multiple string fields', () => { - it('should return list of all string field values', async () => { - const query = 'select * from atable'; - const response = { - results: { - tempvar: { - refId: 'tempvar', - frames: [ - dataFrameToJSON( - new MutableDataFrame({ - fields: [ - { name: 'title', values: ['aTitle', 'aTitle2', 'aTitle3'] }, - { name: 'text', values: ['some text', 'some text2', 'some text3'] }, - ], - meta: { - executedQueryString: 'select * from atable', - }, - }) - ), - ], - }, - }, - }; - - const { ds } = setupTestContext(response); - const results = await ds.metricFindQuery(query, {}); - - expect(results.length).toBe(6); - expect(results[0].text).toBe('aTitle'); - expect(results[5].text).toBe('some text3'); - }); - }); - - describe('When performing metricFindQuery with $__searchFilter and a searchFilter is given', () => { - it('should return list of all column values', async () => { - const query = "select title from atable where title LIKE '$__searchFilter'"; - const response = { - results: { - tempvar: { - refId: 'tempvar', - frames: [ - dataFrameToJSON( - new MutableDataFrame({ - fields: [ - { name: 'title', values: ['aTitle', 'aTitle2', 'aTitle3'] }, - { name: 'text', values: ['some text', 'some text2', 'some text3'] }, - ], - meta: { - executedQueryString: 'select * from atable', - }, - }) - ), - ], - }, - }, - }; - - const { ds } = setupTestContext(response); - const results = await ds.metricFindQuery(query, { searchFilter: 'aTit' }); - - expect(fetchMock).toBeCalledTimes(1); - expect(fetchMock.mock.calls[0][0].data.queries[0].rawSql).toBe( - "select title from atable where title LIKE 'aTit%'" - ); - expect(results).toEqual([ - { text: 'aTitle' }, - { text: 'aTitle2' }, - { text: 'aTitle3' }, - { text: 'some text' }, - { text: 'some text2' }, - { text: 'some text3' }, - ]); - }); - }); - - describe('When performing metricFindQuery with $__searchFilter but no searchFilter is given', () => { - it('should return list of all column values', async () => { - const query = "select title from atable where title LIKE '$__searchFilter'"; - const response = { - results: { - tempvar: { - refId: 'tempvar', - frames: [ - dataFrameToJSON( - new MutableDataFrame({ - fields: [ - { name: 'title', values: ['aTitle', 'aTitle2', 'aTitle3'] }, - { name: 'text', values: ['some text', 'some text2', 'some text3'] }, - ], - meta: { - executedQueryString: 'select * from atable', - }, - }) - ), - ], - }, - }, - }; - - const { ds } = setupTestContext(response); - const results = await ds.metricFindQuery(query, {}); - - expect(fetchMock).toBeCalledTimes(1); - expect(fetchMock.mock.calls[0][0].data.queries[0].rawSql).toBe("select title from atable where title LIKE '%'"); - expect(results).toEqual([ - { text: 'aTitle' }, - { text: 'aTitle2' }, - { text: 'aTitle3' }, - { text: 'some text' }, - { text: 'some text2' }, - { text: 'some text3' }, - ]); - }); - }); - - describe('When performing metricFindQuery with key, value columns', () => { - it('should return list of as text, value', async () => { - const query = 'select * from atable'; - const response = { - results: { - tempvar: { - refId: 'tempvar', - frames: [ - dataFrameToJSON( - new MutableDataFrame({ - fields: [ - { name: '__value', values: ['value1', 'value2', 'value3'] }, - { name: '__text', values: ['aTitle', 'aTitle2', 'aTitle3'] }, - ], - meta: { - executedQueryString: 'select * from atable', - }, - }) - ), - ], - }, - }, - }; - const { ds } = setupTestContext(response); - const results = await ds.metricFindQuery(query, {}); - - expect(results).toEqual([ - { text: 'aTitle', value: 'value1' }, - { text: 'aTitle2', value: 'value2' }, - { text: 'aTitle3', value: 'value3' }, - ]); - }); - }); - - describe('When performing metricFindQuery without key, value columns', () => { - it('should return list of all field values as text', async () => { - const query = 'select id, values from atable'; - const response = { - results: { - tempvar: { - refId: 'tempvar', - frames: [ - dataFrameToJSON( - new MutableDataFrame({ - fields: [ - { name: 'id', values: [1, 2, 3] }, - { name: 'values', values: ['test1', 'test2', 'test3'] }, - ], - meta: { - executedQueryString: 'select id, values from atable', - }, - }) - ), - ], - }, - }, - }; - const { ds } = setupTestContext(response); - const results = await ds.metricFindQuery(query, {}); - - expect(results).toEqual([ - { text: 1 }, - { text: 2 }, - { text: 3 }, - { text: 'test1' }, - { text: 'test2' }, - { text: 'test3' }, - ]); - }); - }); - - describe('When performing metricFindQuery with key, value columns and with duplicate keys', () => { - it('should return list of unique keys', async () => { - const query = 'select * from atable'; - const response = { - results: { - tempvar: { - refId: 'tempvar', - frames: [ - dataFrameToJSON( - new MutableDataFrame({ - fields: [ - { name: '__text', values: ['aTitle', 'aTitle', 'aTitle'] }, - { name: '__value', values: ['same', 'same', 'diff'] }, - ], - meta: { - executedQueryString: 'select * from atable', - }, - }) - ), - ], - }, - }, - }; - const { ds } = setupTestContext(response); - const results = await ds.metricFindQuery(query, {}); - - expect(results).toEqual([{ text: 'aTitle', value: 'same' }]); - }); - }); - - describe('When interpolating variables', () => { - describe('and value is a string', () => { - it('should return an unquoted value', () => { - const { ds, variable } = setupTestContext({}); - expect(ds.interpolateVariable('abc', variable)).toEqual('abc'); - }); - }); - - describe('and value is a number', () => { - it('should return an unquoted value', () => { - const { ds, variable } = setupTestContext({}); - expect(ds.interpolateVariable(1000 as unknown as string, variable)).toEqual(1000); - }); - }); - - describe('and value is an array of strings', () => { - it('should return comma separated quoted values', () => { - const { ds, variable } = setupTestContext({}); - expect(ds.interpolateVariable(['a', 'b', 'c'], variable)).toEqual("'a','b','c'"); - }); - }); - - describe('and variable allows multi-value and is a string', () => { - it('should return a quoted value', () => { - const { ds, variable } = setupTestContext({}); - variable.multi = true; - expect(ds.interpolateVariable('abc', variable)).toEqual("'abc'"); - }); - }); - - describe('and variable contains single quote', () => { - it('should return a quoted value', () => { - const { ds, variable } = setupTestContext({}); - variable.multi = true; - expect(ds.interpolateVariable("a'bc", variable)).toEqual("'a''bc'"); - expect(ds.interpolateVariable("a'b'c", variable)).toEqual("'a''b''c'"); - }); - }); - - describe('and variable allows all and is a string', () => { - it('should return a quoted value', () => { - const { ds, variable } = setupTestContext({}); - variable.includeAll = true; - expect(ds.interpolateVariable('abc', variable)).toEqual("'abc'"); - }); - }); - }); - - describe('targetContainsTemplate', () => { - it('given query that contains template variable it should return true', () => { - const rawSql = `SELECT - $__timeGroup("createdAt",'$summarize'), - avg(value) as "value", - hostname as "metric" - FROM - grafana_metric - WHERE - $__timeFilter("createdAt") AND - measurement = 'logins.count' AND - hostname IN($host) - GROUP BY time, metric - ORDER BY time`; - const query = { - rawSql, - rawQuery: true, - }; - const { templateSrv, ds } = setupTestContext({}); - - templateSrv.init([ - { type: 'query', name: 'summarize', current: { value: '1m' } }, - { type: 'query', name: 'host', current: { value: 'a' } }, - ]); - - expect(ds.targetContainsTemplate(query)).toBeTruthy(); - }); - - it('given query that only contains global template variable it should return false', () => { - const rawSql = `SELECT - $__timeGroup("createdAt",'$__interval'), - avg(value) as "value", - hostname as "metric" - FROM - grafana_metric - WHERE - $__timeFilter("createdAt") AND - measurement = 'logins.count' - GROUP BY time, metric - ORDER BY time`; - const query = { - rawSql, - rawQuery: true, - }; - const { templateSrv, ds } = setupTestContext({}); - - templateSrv.init([ - { type: 'query', name: 'summarize', current: { value: '1m' } }, - { type: 'query', name: 'host', current: { value: 'a' } }, - ]); - - expect(ds.targetContainsTemplate(query)).toBeFalsy(); - }); - }); -}); - -const createFetchResponse = (data: T): FetchResponse => ({ - data, - status: 200, - url: 'http://localhost:3000/api/query', - config: { url: 'http://localhost:3000/api/query' }, - type: 'basic', - statusText: 'Ok', - redirected: false, - headers: {} as unknown as Headers, - ok: true, -}); diff --git a/public/app/plugins/datasource/postgres/specs/postgres_query.test.ts b/public/app/plugins/datasource/postgres/specs/postgres_query.test.ts deleted file mode 100644 index a1c65523f92..00000000000 --- a/public/app/plugins/datasource/postgres/specs/postgres_query.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { TemplateSrv } from 'app/features/templating/template_srv'; - -import PostgresQueryModel from '../postgres_query_model'; - -describe('PostgresQuery', () => { - // @ts-ignore - const templateSrv: TemplateSrv = { - replace: jest.fn((text) => text) as any, - }; - - describe('When initializing', () => { - it('should not be in SQL mode', () => { - const query = new PostgresQueryModel({}, templateSrv); - expect(query.target.rawQuery).toBe(false); - }); - it('should be in SQL mode for pre query builder queries', () => { - const query = new PostgresQueryModel({ rawSql: 'SELECT 1' }, templateSrv); - expect(query.target.rawQuery).toBe(true); - }); - }); - - describe('When generating time column SQL', () => { - const query = new PostgresQueryModel({}, templateSrv); - - query.target.timeColumn = 'time'; - expect(query.buildTimeColumn()).toBe('time AS "time"'); - query.target.timeColumn = '"time"'; - expect(query.buildTimeColumn()).toBe('"time" AS "time"'); - }); - - describe('When generating time column SQL with group by time', () => { - let query = new PostgresQueryModel( - { timeColumn: 'time', group: [{ type: 'time', params: ['5m', 'none'] }] }, - templateSrv - ); - expect(query.buildTimeColumn()).toBe('$__timeGroupAlias(time,5m)'); - expect(query.buildTimeColumn(false)).toBe('$__timeGroup(time,5m)'); - - query = new PostgresQueryModel( - { timeColumn: 'time', group: [{ type: 'time', params: ['5m', 'NULL'] }] }, - templateSrv - ); - expect(query.buildTimeColumn()).toBe('$__timeGroupAlias(time,5m,NULL)'); - - query = new PostgresQueryModel( - { timeColumn: 'time', timeColumnType: 'int4', group: [{ type: 'time', params: ['5m', 'none'] }] }, - templateSrv - ); - expect(query.buildTimeColumn()).toBe('$__unixEpochGroupAlias(time,5m)'); - expect(query.buildTimeColumn(false)).toBe('$__unixEpochGroup(time,5m)'); - }); - - describe('When generating metric column SQL', () => { - const query = new PostgresQueryModel({}, templateSrv); - - query.target.metricColumn = 'host'; - expect(query.buildMetricColumn()).toBe('host AS metric'); - query.target.metricColumn = '"host"'; - expect(query.buildMetricColumn()).toBe('"host" AS metric'); - }); - - describe('When generating value column SQL', () => { - const query = new PostgresQueryModel({}, templateSrv); - - let column = [{ type: 'column', params: ['value'] }]; - expect(query.buildValueColumn(column)).toBe('value'); - column = [ - { type: 'column', params: ['value'] }, - { type: 'alias', params: ['alias'] }, - ]; - expect(query.buildValueColumn(column)).toBe('value AS "alias"'); - column = [ - { type: 'column', params: ['v'] }, - { type: 'alias', params: ['a'] }, - { type: 'aggregate', params: ['max'] }, - ]; - expect(query.buildValueColumn(column)).toBe('max(v) AS "a"'); - column = [ - { type: 'column', params: ['v'] }, - { type: 'alias', params: ['a'] }, - { type: 'window', params: ['increase'] }, - ]; - expect(query.buildValueColumn(column)).toBe( - '(CASE WHEN v >= lag(v) OVER (ORDER BY time) ' + - 'THEN v - lag(v) OVER (ORDER BY time) ' + - 'WHEN lag(v) OVER (ORDER BY time) IS NULL THEN NULL ELSE v END) AS "a"' - ); - }); - - describe('When generating value column SQL with metric column', () => { - const query = new PostgresQueryModel({}, templateSrv); - query.target.metricColumn = 'host'; - - let column = [{ type: 'column', params: ['value'] }]; - expect(query.buildValueColumn(column)).toBe('value'); - column = [ - { type: 'column', params: ['value'] }, - { type: 'alias', params: ['alias'] }, - ]; - expect(query.buildValueColumn(column)).toBe('value AS "alias"'); - column = [ - { type: 'column', params: ['v'] }, - { type: 'alias', params: ['a'] }, - { type: 'aggregate', params: ['max'] }, - ]; - expect(query.buildValueColumn(column)).toBe('max(v) AS "a"'); - column = [ - { type: 'column', params: ['v'] }, - { type: 'alias', params: ['a'] }, - { type: 'window', params: ['increase'] }, - ]; - expect(query.buildValueColumn(column)).toBe( - '(CASE WHEN v >= lag(v) OVER (PARTITION BY host ORDER BY time) ' + - 'THEN v - lag(v) OVER (PARTITION BY host ORDER BY time) ' + - 'WHEN lag(v) OVER (PARTITION BY host ORDER BY time) IS NULL THEN NULL ELSE v END) AS "a"' - ); - column = [ - { type: 'column', params: ['v'] }, - { type: 'alias', params: ['a'] }, - { type: 'aggregate', params: ['max'] }, - { type: 'window', params: ['increase'] }, - ]; - expect(query.buildValueColumn(column)).toBe( - '(CASE WHEN max(v) >= lag(max(v)) OVER (PARTITION BY host ORDER BY time) ' + - 'THEN max(v) - lag(max(v)) OVER (PARTITION BY host ORDER BY time) ' + - 'WHEN lag(max(v)) OVER (PARTITION BY host ORDER BY time) IS NULL THEN NULL ELSE max(v) END) AS "a"' - ); - }); - - describe('When generating WHERE clause', () => { - const query = new PostgresQueryModel({ where: [] }, templateSrv); - - expect(query.buildWhereClause()).toBe(''); - - query.target.timeColumn = 't'; - query.target.where = [{ type: 'macro', name: '$__timeFilter' }]; - expect(query.buildWhereClause()).toBe('\nWHERE\n $__timeFilter(t)'); - - query.target.where = [{ type: 'expression', params: ['v', '=', '1'] }]; - expect(query.buildWhereClause()).toBe('\nWHERE\n v = 1'); - - query.target.where = [ - { type: 'macro', name: '$__timeFilter' }, - { type: 'expression', params: ['v', '=', '1'] }, - ]; - expect(query.buildWhereClause()).toBe('\nWHERE\n $__timeFilter(t) AND\n v = 1'); - }); - - describe('When generating GROUP BY clause', () => { - const query = new PostgresQueryModel({ group: [], metricColumn: 'none' }, templateSrv); - - expect(query.buildGroupClause()).toBe(''); - query.target.group = [{ type: 'time', params: ['5m'] }]; - expect(query.buildGroupClause()).toBe('\nGROUP BY 1'); - query.target.metricColumn = 'm'; - expect(query.buildGroupClause()).toBe('\nGROUP BY 1,2'); - }); - - describe('When generating complete statement', () => { - const target: any = { - timeColumn: 't', - table: 'table', - select: [[{ type: 'column', params: ['value'] }]], - where: [], - }; - let result = 'SELECT\n t AS "time",\n value\nFROM table\nORDER BY 1'; - const query = new PostgresQueryModel(target, templateSrv); - - expect(query.buildQuery()).toBe(result); - - query.target.metricColumn = 'm'; - result = 'SELECT\n t AS "time",\n m AS metric,\n value\nFROM table\nORDER BY 1,2'; - expect(query.buildQuery()).toBe(result); - }); -}); diff --git a/public/app/plugins/datasource/postgres/sqlCompletionProvider.ts b/public/app/plugins/datasource/postgres/sqlCompletionProvider.ts new file mode 100644 index 00000000000..85f9c4a7133 --- /dev/null +++ b/public/app/plugins/datasource/postgres/sqlCompletionProvider.ts @@ -0,0 +1,50 @@ +import { TableIdentifier } from '@grafana/experimental'; +import { AGGREGATE_FNS, OPERATORS } from 'app/features/plugins/sql/constants'; +import { + ColumnDefinition, + DB, + LanguageCompletionProvider, + SQLQuery, + TableDefinition, +} from 'app/features/plugins/sql/types'; + +import { FUNCTIONS } from '../mysql/functions'; + +interface CompletionProviderGetterArgs { + getColumns: React.MutableRefObject<(t: SQLQuery) => Promise>; + getTables: React.MutableRefObject<(d?: string) => Promise>; +} + +export const getSqlCompletionProvider: (args: CompletionProviderGetterArgs) => LanguageCompletionProvider = + ({ getColumns, getTables }) => + () => ({ + triggerCharacters: ['.', ' ', '$', ',', '(', "'"], + tables: { + resolve: async () => { + return await getTables.current(); + }, + }, + columns: { + resolve: async (t?: TableIdentifier) => { + return await getColumns.current({ table: t?.table, refId: 'A' }); + }, + }, + supportedFunctions: () => [...AGGREGATE_FNS, ...FUNCTIONS], + supportedOperators: () => OPERATORS, + }); + +export async function fetchColumns(db: DB, q: SQLQuery) { + const cols = await db.fields(q); + if (cols.length > 0) { + return cols.map((c) => { + return { name: c.value, type: c.value, description: c.value }; + }); + } else { + return []; + } +} + +export async function fetchTables(db: DB) { + const tables = await db.lookup(); + return tables; +} diff --git a/public/app/plugins/datasource/postgres/sqlUtil.ts b/public/app/plugins/datasource/postgres/sqlUtil.ts new file mode 100644 index 00000000000..81fec6dcf5f --- /dev/null +++ b/public/app/plugins/datasource/postgres/sqlUtil.ts @@ -0,0 +1,112 @@ +import { isEmpty } from 'lodash'; + +import { RAQBFieldTypes, SQLExpression, SQLQuery } from 'app/features/plugins/sql/types'; + +export function getFieldConfig(type: string): { raqbFieldType: RAQBFieldTypes; icon: string } { + switch (type) { + case 'boolean': { + return { raqbFieldType: 'boolean', icon: 'toggle-off' }; + } + case 'bit': + case 'bit varying': + case 'character': + case 'character varying': + case 'text': { + return { raqbFieldType: 'text', icon: 'text' }; + } + case 'smallint': + case 'integer': + case 'bigint': + case 'decimal': + case 'numeric': + case 'real': + case 'double precision': + case 'serial': + case 'bigserial': + case 'smallserial': { + return { raqbFieldType: 'number', icon: 'calculator-alt' }; + } + case 'date': { + return { raqbFieldType: 'date', icon: 'clock-nine' }; + } + case 'time': + case 'time with time zone': + case 'time without time zone': + case 'interval': { + return { raqbFieldType: 'time', icon: 'clock-nine' }; + } + case 'timestamp': + case 'timestamp with time zone': + case 'timestamp without time zone': { + return { raqbFieldType: 'datetime', icon: 'clock-nine' }; + } + default: + return { raqbFieldType: 'text', icon: 'text' }; + } +} + +export function toRawSql({ sql, table }: SQLQuery): string { + let rawQuery = ''; + + // Return early with empty string if there is no sql column + if (!sql || !haveColumns(sql.columns)) { + return rawQuery; + } + + rawQuery += createSelectClause(sql.columns); + + if (table) { + rawQuery += `FROM ${table} `; + } + + if (sql.whereString) { + rawQuery += `WHERE ${sql.whereString} `; + } + + if (sql.groupBy?.[0]?.property.name) { + const groupBy = sql.groupBy.map((g) => g.property.name).filter((g) => !isEmpty(g)); + rawQuery += `GROUP BY ${groupBy.join(', ')} `; + } + + if (sql.orderBy?.property.name) { + rawQuery += `ORDER BY ${sql.orderBy.property.name} `; + } + + if (sql.orderBy?.property.name && sql.orderByDirection) { + rawQuery += `${sql.orderByDirection} `; + } + + // Altough LIMIT 0 doesn't make sense, it is still possible to have LIMIT 0 + if (sql.limit !== undefined && sql.limit >= 0) { + rawQuery += `LIMIT ${sql.limit} `; + } + return rawQuery; +} + +function createSelectClause(sqlColumns: NonNullable): string { + const columns = sqlColumns.map((c) => { + let rawColumn = ''; + if (c.name && c.alias) { + rawColumn += `${c.name}(${c.parameters?.map((p) => `${p.name}`)}) AS ${c.alias}`; + } else if (c.name) { + rawColumn += `${c.name}(${c.parameters?.map((p) => `${p.name}`)})`; + } else if (c.alias) { + rawColumn += `${c.parameters?.map((p) => `${p.name}`)} AS ${c.alias}`; + } else { + rawColumn += `${c.parameters?.map((p) => `${p.name}`)}`; + } + return rawColumn; + }); + + return `SELECT ${columns.join(', ')} `; +} + +export const haveColumns = (columns: SQLExpression['columns']): columns is NonNullable => { + if (!columns) { + return false; + } + + const haveColumn = columns.some((c) => c.parameters?.length || c.parameters?.some((p) => p.name)); + const haveFunction = columns.some((c) => c.name); + return haveColumn || haveFunction; +}; diff --git a/public/app/plugins/datasource/postgres/sql_part.ts b/public/app/plugins/datasource/postgres/sql_part.ts deleted file mode 100644 index 13266f783dc..00000000000 --- a/public/app/plugins/datasource/postgres/sql_part.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { SqlPartDef, SqlPart } from 'app/angular/components/sql_part/sql_part'; - -const index: any[] = []; - -function createPart(part: any): any { - const def = index[part.type]; - if (!def) { - return null; - } - - return new SqlPart(part, def); -} - -function register(options: any) { - index[options.type] = new SqlPartDef(options); -} - -register({ - type: 'column', - style: 'label', - params: [{ type: 'column', dynamicLookup: true }], - defaultParams: ['value'], -}); - -register({ - type: 'expression', - style: 'expression', - label: 'Expr:', - params: [ - { name: 'left', type: 'string', dynamicLookup: true }, - { name: 'op', type: 'string', dynamicLookup: true }, - { name: 'right', type: 'string', dynamicLookup: true }, - ], - defaultParams: ['value', '=', 'value'], -}); - -register({ - type: 'macro', - style: 'label', - label: 'Macro:', - params: [], - defaultParams: [], -}); - -register({ - type: 'aggregate', - style: 'label', - params: [ - { - name: 'name', - type: 'string', - options: [], - baseOptions: ['avg', 'count', 'min', 'max', 'sum', 'stddev', 'variance'], - timescaleOptions: ['first', 'last'], - }, - ], - defaultParams: ['avg'], -}); - -register({ - type: 'percentile', - label: 'Aggregate:', - style: 'label', - params: [ - { - name: 'name', - type: 'string', - options: ['percentile_cont', 'percentile_disc'], - }, - { - name: 'fraction', - type: 'number', - options: ['0.5', '0.75', '0.9', '0.95', '0.99'], - }, - ], - defaultParams: ['percentile_cont', '0.95'], -}); - -register({ - type: 'alias', - style: 'label', - params: [{ name: 'name', type: 'string', quote: 'double' }], - defaultParams: ['alias'], -}); - -register({ - type: 'time', - style: 'function', - label: 'time', - params: [ - { - name: 'interval', - type: 'interval', - options: ['$__interval', '1s', '10s', '1m', '5m', '10m', '15m', '1h'], - }, - { - name: 'fill', - type: 'string', - options: ['none', 'NULL', 'previous', '0'], - }, - ], - defaultParams: ['$__interval', 'none'], -}); - -register({ - type: 'window', - style: 'label', - params: [ - { - name: 'function', - type: 'string', - options: ['delta', 'increase', 'rate', 'sum'], - }, - ], - defaultParams: ['increase'], -}); - -register({ - type: 'moving_window', - style: 'label', - label: 'Moving Window:', - params: [ - { - name: 'function', - type: 'string', - options: ['avg'], - }, - { - name: 'window_size', - type: 'number', - options: ['3', '5', '7', '10', '20'], - }, - ], - defaultParams: ['avg', '5'], -}); - -export default { - create: createPart, -}; diff --git a/public/app/plugins/datasource/postgres/types.ts b/public/app/plugins/datasource/postgres/types.ts index a07ff7a831a..6bb435e4cbb 100644 --- a/public/app/plugins/datasource/postgres/types.ts +++ b/public/app/plugins/datasource/postgres/types.ts @@ -1,5 +1,4 @@ -import { DataQuery, DataSourceJsonData } from '@grafana/data'; -import { SQLConnectionLimits } from 'app/features/plugins/sql/types'; +import { SQLOptions } from 'app/features/plugins/sql/types'; export enum PostgresTLSModes { disable = 'disable', @@ -12,35 +11,16 @@ export enum PostgresTLSMethods { filePath = 'file-path', fileContent = 'file-content', } -export interface PostgresOptions extends DataSourceJsonData, SQLConnectionLimits { - url: string; - timeInterval: string; - database: string; - user: string; - tlsConfigurationMethod: PostgresTLSMethods; - sslmode: PostgresTLSModes; - sslRootCertFile: string; - sslCertFile: string; - sslKeyFile: string; - postgresVersion: number; - timescaledb: boolean; +export interface PostgresOptions extends SQLOptions { + tlsConfigurationMethod?: PostgresTLSMethods; + sslmode?: PostgresTLSModes; + sslRootCertFile?: string; + sslCertFile?: string; + sslKeyFile?: string; + postgresVersion?: number; + timescaledb?: boolean; } export interface SecureJsonData { password: string; } - -export type ResultFormat = 'time_series' | 'table'; -export interface PostgresQuery extends DataQuery { - alias?: string; - format?: ResultFormat; - rawSql?: any; -} - -export interface PostgresQueryForInterpolation { - alias?: any; - format?: any; - rawSql?: any; - refId: any; - hide?: any; -}