mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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 <kyle@codeincarnate.com> Co-authored-by: Oscar Kilhed <oscar.kilhed@grafana.com>
This commit is contained in:
parent
0c3ed0219e
commit
26659baf8f
@ -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"]
|
||||
],
|
||||
|
@ -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');
|
@ -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<SqlDatasource, SQLQuery, SQLOptions>;
|
||||
interface Props extends QueryEditorProps<SqlDatasource, SQLQuery, SQLOptions> {
|
||||
queryHeaderProps?: Pick<QueryHeaderProps, 'isDatasetSelectorHidden'>;
|
||||
}
|
||||
|
||||
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}
|
||||
/>
|
||||
|
||||
<Space v={0.5} />
|
||||
|
@ -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 && (
|
||||
<>
|
||||
<Space v={0.5} />
|
||||
|
||||
<EditorRow>
|
||||
<EditorField label="Dataset" width={25}>
|
||||
<DatasetSelector
|
||||
db={db}
|
||||
value={query.dataset === undefined ? null : query.dataset}
|
||||
onChange={onDatasetChange}
|
||||
/>
|
||||
</EditorField>
|
||||
{isDatasetSelectorHidden ? null : (
|
||||
<EditorField label="Dataset" width={25}>
|
||||
<DatasetSelector
|
||||
db={db}
|
||||
value={query.dataset === undefined ? null : query.dataset}
|
||||
onChange={onDatasetChange}
|
||||
/>
|
||||
</EditorField>
|
||||
)}
|
||||
|
||||
<EditorField label="Table" width={25}>
|
||||
<TableSelector
|
||||
@ -234,6 +237,7 @@ export function QueryHeader({
|
||||
query={query}
|
||||
value={query.table === undefined ? null : query.table}
|
||||
onChange={onTableChange}
|
||||
forceFetch={isDatasetSelectorHidden}
|
||||
applyDefault
|
||||
/>
|
||||
</EditorField>
|
||||
|
@ -12,11 +12,12 @@ interface TableSelectorProps extends ResourceSelectorProps {
|
||||
value: string | null;
|
||||
query: QueryWithDefaults;
|
||||
onChange: (v: SelectableValue) => void;
|
||||
forceFetch?: boolean;
|
||||
}
|
||||
|
||||
export const TableSelector: React.FC<TableSelectorProps> = ({ db, query, value, className, onChange }) => {
|
||||
export const TableSelector: React.FC<TableSelectorProps> = ({ 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);
|
||||
|
@ -24,5 +24,13 @@ export function SQLSelectRow({ fields, query, onQueryChange, db }: SQLSelectRowP
|
||||
return functions.map((f) => toOption(f.name));
|
||||
}, [db]);
|
||||
|
||||
return <SelectRow columns={fields} sql={query.sql!} functions={state.value} onSqlChange={onSqlChange} />;
|
||||
return (
|
||||
<SelectRow
|
||||
columns={fields}
|
||||
sql={query.sql!}
|
||||
format={query.format}
|
||||
functions={state.value}
|
||||
onSqlChange={onSqlChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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<SelectableValue<string>>;
|
||||
functions?: Array<SelectableValue<string>>;
|
||||
@ -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<SelectableValue<string>> = [];
|
||||
|
||||
// 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<string>) => {
|
||||
@ -60,6 +69,26 @@ export function SelectRow({ sql, columns, onSqlChange, functions }: SelectRowPro
|
||||
[onSqlChange, sql]
|
||||
);
|
||||
|
||||
const onAliasChange = useCallback(
|
||||
(item: QueryEditorFunctionExpression, index: number) => (alias: SelectableValue<string>) => {
|
||||
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 (
|
||||
<Stack gap={2} alignItems="end" wrap direction="column">
|
||||
<Stack gap={2} wrap direction="column">
|
||||
{sql.columns?.map((item, index) => (
|
||||
<div key={index}>
|
||||
<Stack gap={2} alignItems="end">
|
||||
@ -105,6 +134,16 @@ export function SelectRow({ sql, columns, onSqlChange, functions }: SelectRowPro
|
||||
onChange={onAggregationChange(item, index)}
|
||||
/>
|
||||
</EditorField>
|
||||
<EditorField label="Alias" optional width={15}>
|
||||
<Select
|
||||
value={item.alias ? toOption(item.alias) : null}
|
||||
options={timeSeriesAliasOpts}
|
||||
onChange={onAliasChange(item, index)}
|
||||
isClearable
|
||||
menuShouldPortal
|
||||
allowCustomValue
|
||||
/>
|
||||
</EditorField>
|
||||
<Button
|
||||
aria-label="Remove"
|
||||
type="button"
|
||||
|
@ -24,11 +24,13 @@ import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
|
||||
import { VariableWithMultiSupport } from '../../../variables/types';
|
||||
import { getSearchFilterScopedVar, SearchFilterOptions } from '../../../variables/utils';
|
||||
import { ResponseParser } from '../ResponseParser';
|
||||
import { MACRO_NAMES } from '../constants';
|
||||
import { DB, SQLQuery, SQLOptions, ResponseParser, SqlQueryModel, QueryFormat } from '../types';
|
||||
import { DB, SQLQuery, SQLOptions, SqlQueryModel, QueryFormat } from '../types';
|
||||
|
||||
export abstract class SqlDatasource extends DataSourceWithBackend<SQLQuery, SQLOptions> {
|
||||
id: number;
|
||||
responseParser: ResponseParser;
|
||||
name: string;
|
||||
interval: string;
|
||||
db: DB;
|
||||
@ -40,6 +42,7 @@ export abstract class SqlDatasource extends DataSourceWithBackend<SQLQuery, SQLO
|
||||
) {
|
||||
super(instanceSettings);
|
||||
this.name = instanceSettings.name;
|
||||
this.responseParser = new ResponseParser();
|
||||
this.id = instanceSettings.id;
|
||||
const settingsData = instanceSettings.jsonData || {};
|
||||
this.interval = settingsData.timeInterval || '1m';
|
||||
@ -50,7 +53,9 @@ export abstract class SqlDatasource extends DataSourceWithBackend<SQLQuery, SQLO
|
||||
|
||||
abstract getQueryModel(target?: SQLQuery, templateSrv?: TemplateSrv, scopedVars?: ScopedVars): SqlQueryModel;
|
||||
|
||||
abstract getResponseParser(): ResponseParser;
|
||||
getResponseParser() {
|
||||
return this.responseParser;
|
||||
}
|
||||
|
||||
interpolateVariable = (value: string | string[] | number, variable: VariableWithMultiSupport) => {
|
||||
if (typeof value === 'string') {
|
||||
|
@ -57,6 +57,7 @@ export interface QueryEditorGroupByExpression {
|
||||
export interface QueryEditorFunctionExpression {
|
||||
type: QueryEditorExpressionType.Function;
|
||||
name?: string;
|
||||
alias?: string;
|
||||
parameters?: QueryEditorFunctionParameterExpression[];
|
||||
}
|
||||
|
||||
|
@ -50,8 +50,12 @@ export function defaultToRawSql({ sql, dataset, table }: SQLQuery): string {
|
||||
function createSelectClause(sqlColumns: NonNullable<SQLExpression['columns']>): string {
|
||||
const columns = sqlColumns.map((c) => {
|
||||
let rawColumn = '';
|
||||
if (c.name) {
|
||||
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}`)}`;
|
||||
}
|
||||
|
@ -2,17 +2,10 @@ import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
|
||||
import { TemplateSrv } from '@grafana/runtime';
|
||||
import { AGGREGATE_FNS } from 'app/features/plugins/sql/constants';
|
||||
import { SqlDatasource } from 'app/features/plugins/sql/datasource/SqlDatasource';
|
||||
import {
|
||||
DB,
|
||||
LanguageCompletionProvider,
|
||||
ResponseParser,
|
||||
SQLQuery,
|
||||
SQLSelectableValue,
|
||||
} from 'app/features/plugins/sql/types';
|
||||
import { DB, LanguageCompletionProvider, SQLQuery, SQLSelectableValue } from 'app/features/plugins/sql/types';
|
||||
|
||||
import { getSchema, showDatabases, getSchemaAndName } from './MSSqlMetaQuery';
|
||||
import { MSSqlQueryModel } from './MSSqlQueryModel';
|
||||
import { MSSqlResponseParser } from './response_parser';
|
||||
import { fetchColumns, fetchTables, getSqlCompletionProvider } from './sqlCompletionProvider';
|
||||
import { getIcon, getRAQBType, toRawSql } from './sqlUtil';
|
||||
import { MssqlOptions } from './types';
|
||||
@ -27,10 +20,6 @@ export class MssqlDatasource extends SqlDatasource {
|
||||
return new MSSqlQueryModel(target, templateSrv, scopedVars);
|
||||
}
|
||||
|
||||
getResponseParser(): ResponseParser {
|
||||
return new MSSqlResponseParser();
|
||||
}
|
||||
|
||||
async fetchDatasets(): Promise<string[]> {
|
||||
const datasets = await this.runSql<{ name: string[] }>(showDatabases(), { refId: 'datasets' });
|
||||
return datasets.fields.name.values.toArray().flat();
|
||||
|
@ -116,8 +116,12 @@ export function toRawSql({ sql, dataset, table }: SQLQuery): string {
|
||||
function createSelectClause(sqlColumns: NonNullable<SQLExpression['columns']>, limit?: number): string {
|
||||
const columns = sqlColumns.map((c) => {
|
||||
let rawColumn = '';
|
||||
if (c.name) {
|
||||
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}`)}`;
|
||||
}
|
||||
|
@ -1,28 +1,19 @@
|
||||
import { DataSourceInstanceSettings, ScopedVars, TimeRange } from '@grafana/data';
|
||||
import { TemplateSrv } from '@grafana/runtime';
|
||||
import { SqlDatasource } from 'app/features/plugins/sql/datasource/SqlDatasource';
|
||||
import {
|
||||
CompletionItemKind,
|
||||
DB,
|
||||
LanguageCompletionProvider,
|
||||
ResponseParser,
|
||||
SQLQuery,
|
||||
} from 'app/features/plugins/sql/types';
|
||||
import { CompletionItemKind, DB, LanguageCompletionProvider, SQLQuery } from 'app/features/plugins/sql/types';
|
||||
|
||||
import MySQLQueryModel from './MySqlQueryModel';
|
||||
import MySqlResponseParser from './MySqlResponseParser';
|
||||
import { mapFieldsToTypes } from './fields';
|
||||
import { buildColumnQuery, buildTableQuery, showDatabases } from './mySqlMetaQuery';
|
||||
import { fetchColumns, fetchTables, getFunctions, getSqlCompletionProvider } from './sqlCompletionProvider';
|
||||
import { MySQLOptions } from './types';
|
||||
|
||||
export class MySqlDatasource extends SqlDatasource {
|
||||
responseParser: MySqlResponseParser;
|
||||
completionProvider: LanguageCompletionProvider | undefined;
|
||||
|
||||
constructor(private instanceSettings: DataSourceInstanceSettings<MySQLOptions>) {
|
||||
super(instanceSettings);
|
||||
this.responseParser = new MySqlResponseParser();
|
||||
this.completionProvider = undefined;
|
||||
}
|
||||
|
||||
@ -30,10 +21,6 @@ export class MySqlDatasource extends SqlDatasource {
|
||||
return new MySQLQueryModel(target!, templateSrv, scopedVars);
|
||||
}
|
||||
|
||||
getResponseParser(): ResponseParser {
|
||||
return this.responseParser;
|
||||
}
|
||||
|
||||
getSqlCompletionProvider(db: DB): LanguageCompletionProvider {
|
||||
if (this.completionProvider !== undefined) {
|
||||
return this.completionProvider;
|
||||
|
@ -1,27 +0,0 @@
|
||||
import { uniqBy } from 'lodash';
|
||||
|
||||
import { DataFrame, MetricFindValue } from '@grafana/data';
|
||||
|
||||
export default class ResponseParser {
|
||||
transformMetricFindResponse(frame: DataFrame): MetricFindValue[] {
|
||||
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');
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@ import { MySqlDatasource } from '../MySqlDatasource';
|
||||
import { MySQLOptions } from '../types';
|
||||
|
||||
describe('MySQLDatasource', () => {
|
||||
const setupTextContext = (response: any) => {
|
||||
const setupTextContext = (response: unknown) => {
|
||||
jest.clearAllMocks();
|
||||
setBackendSrv(backendSrv);
|
||||
const fetchMock = jest.spyOn(backendSrv, 'fetch');
|
||||
|
@ -1,13 +1,5 @@
|
||||
import { SQLOptions, SQLQuery } from 'app/features/plugins/sql/types';
|
||||
|
||||
export interface MysqlQueryForInterpolation {
|
||||
alias?: any;
|
||||
format?: any;
|
||||
rawSql?: any;
|
||||
refId: any;
|
||||
hide?: any;
|
||||
}
|
||||
|
||||
export interface MySQLOptions extends SQLOptions {}
|
||||
|
||||
export interface MySQLQuery extends SQLQuery {}
|
||||
|
25
public/app/plugins/datasource/postgres/PostgresQueryModel.ts
Normal file
25
public/app/plugins/datasource/postgres/PostgresQueryModel.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { ScopedVars } from '@grafana/data';
|
||||
import { TemplateSrv } from '@grafana/runtime';
|
||||
import { applyQueryDefaults } from 'app/features/plugins/sql/defaults';
|
||||
import { SQLQuery, SqlQueryModel } from 'app/features/plugins/sql/types';
|
||||
import { FormatRegistryID } from 'app/features/templating/formatRegistry';
|
||||
|
||||
export class PostgresQueryModel implements SqlQueryModel {
|
||||
target: SQLQuery;
|
||||
templateSrv?: TemplateSrv;
|
||||
scopedVars?: ScopedVars;
|
||||
|
||||
constructor(target?: SQLQuery, templateSrv?: TemplateSrv, scopedVars?: ScopedVars) {
|
||||
this.target = applyQueryDefaults(target || { refId: 'A' });
|
||||
this.templateSrv = templateSrv;
|
||||
this.scopedVars = scopedVars;
|
||||
}
|
||||
|
||||
interpolate() {
|
||||
return this.templateSrv?.replace(this.target.rawSql, this.scopedVars, FormatRegistryID.sqlString) || '';
|
||||
}
|
||||
|
||||
quoteLiteral(value: string) {
|
||||
return "'" + value.replace(/'/g, "''") + "'";
|
||||
}
|
||||
}
|
13
public/app/plugins/datasource/postgres/QueryEditor.tsx
Normal file
13
public/app/plugins/datasource/postgres/QueryEditor.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
import { QueryEditorProps } from '@grafana/data';
|
||||
import { SqlQueryEditor } from 'app/features/plugins/sql/components/QueryEditor';
|
||||
import { SQLOptions, SQLQuery } from 'app/features/plugins/sql/types';
|
||||
|
||||
import { PostgresDatasource } from './datasource';
|
||||
|
||||
const queryHeaderProps = { isDatasetSelectorHidden: true };
|
||||
|
||||
export function QueryEditor(props: QueryEditorProps<PostgresDatasource, SQLQuery, SQLOptions>) {
|
||||
return <SqlQueryEditor {...props} queryHeaderProps={queryHeaderProps} />;
|
||||
}
|
@ -47,7 +47,7 @@ export function useAutoDetectFeatures({ props, setVersionOptions }: Options) {
|
||||
// timescaledb is only available for 9.6+
|
||||
if (versionNumber >= 906 && !options.jsonData.timescaledb) {
|
||||
const timescaledbVersion = await datasource.getTimescaleDBVersion();
|
||||
if (timescaledbVersion?.length) {
|
||||
if (timescaledbVersion) {
|
||||
updateDatasourcePluginJsonDataOption({ options, onOptionsChange }, 'timescaledb', true);
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +1,86 @@
|
||||
import { of, throwError } from 'rxjs';
|
||||
import { createFetchResponse } from 'test/helpers/createFetchResponse';
|
||||
import { Observable, of, throwError } from 'rxjs';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import {
|
||||
dataFrameToJSON,
|
||||
DataQueryRequest,
|
||||
DataQueryResponse,
|
||||
DataSourceInstanceSettings,
|
||||
dateTime,
|
||||
LoadingState,
|
||||
MutableDataFrame,
|
||||
} from '@grafana/data';
|
||||
import { FetchResponse } from '@grafana/runtime';
|
||||
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
|
||||
import { QueryFormat, SQLQuery } from 'app/features/plugins/sql/types';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
|
||||
import { initialCustomVariableModelState } from '../../../features/variables/custom/reducer';
|
||||
|
||||
import { PostgresDatasource } from './datasource';
|
||||
import { PostgresOptions } from './types';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getBackendSrv: () => backendSrv,
|
||||
getTemplateSrv: () => ({
|
||||
replace: (val: string): string => {
|
||||
return val;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Postgres datasource', () => {
|
||||
beforeEach(() => {
|
||||
jest.mock('@grafana/runtime/src/services', () => ({
|
||||
...jest.requireActual('@grafana/runtime/src/services'),
|
||||
getBackendSrv: () => backendSrv,
|
||||
getDataSourceSrv: () => {
|
||||
return {
|
||||
getInstanceSettings: () => ({ id: 8674 }),
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
describe('PostgreSQLDatasource', () => {
|
||||
const fetchMock = jest.spyOn(backendSrv, 'fetch');
|
||||
const setupTestContext = (data: unknown, mock?: Observable<FetchResponse<unknown>>) => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
const defaultMock = () => mock ?? of(createFetchResponse(data));
|
||||
fetchMock.mockImplementation(defaultMock);
|
||||
const instanceSettings = {
|
||||
jsonData: {
|
||||
defaultProject: 'testproject',
|
||||
},
|
||||
} as unknown as DataSourceInstanceSettings<PostgresOptions>;
|
||||
const templateSrv: TemplateSrv = new TemplateSrv();
|
||||
const variable = { ...initialCustomVariableModelState };
|
||||
const ds = new PostgresDatasource(instanceSettings);
|
||||
Reflect.set(ds, 'templateSrv', templateSrv);
|
||||
return { ds, templateSrv, variable };
|
||||
};
|
||||
|
||||
// https://rxjs-dev.firebaseapp.com/guide/testing/marble-testing
|
||||
const runMarbleTest = (args: {
|
||||
options: DataQueryRequest<SQLQuery>;
|
||||
values: { [marble: string]: FetchResponse };
|
||||
marble: string;
|
||||
expectedValues: { [marble: string]: DataQueryResponse };
|
||||
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 testDatasource call', () => {
|
||||
it('should return the error from the server', async () => {
|
||||
setupFetchMock(
|
||||
setupTestContext(
|
||||
undefined,
|
||||
throwError(() => ({
|
||||
status: 400,
|
||||
@ -51,18 +109,568 @@ describe('Postgres datasource', () => {
|
||||
}))
|
||||
);
|
||||
|
||||
const ds = new PostgresDatasource({ name: '', id: 0, jsonData: {} } as any);
|
||||
const ds = new PostgresDatasource({ name: '', id: 0 } as DataSourceInstanceSettings<PostgresOptions>);
|
||||
const result = await ds.testDatasource();
|
||||
expect(result.status).toEqual('error');
|
||||
expect(result.message).toEqual('db query error: pq: password authentication failed for user "postgres"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing a time series query', () => {
|
||||
it('should transform response correctly', () => {
|
||||
const options: DataQueryRequest<SQLQuery> = {
|
||||
range: {
|
||||
from: dateTime(1432288354),
|
||||
to: dateTime(1432288401),
|
||||
raw: {
|
||||
from: 'now-24h',
|
||||
to: 'now',
|
||||
},
|
||||
},
|
||||
targets: [
|
||||
{
|
||||
format: QueryFormat.Timeseries,
|
||||
rawQuery: true,
|
||||
rawSql: 'select time, metric from grafana_metric',
|
||||
refId: 'A',
|
||||
datasource: { type: 'gdev-ds', uid: 'gdev-ds' },
|
||||
},
|
||||
],
|
||||
requestId: 'test',
|
||||
interval: '1m',
|
||||
intervalMs: 60000,
|
||||
scopedVars: {},
|
||||
timezone: 'Etc/UTC',
|
||||
app: 'Grafana',
|
||||
startTime: 1432288354,
|
||||
};
|
||||
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: DataQueryResponse } = {
|
||||
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: LoadingState.Done,
|
||||
},
|
||||
};
|
||||
|
||||
runMarbleTest({ options, marble, values, expectedMarble, expectedValues });
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing a table query', () => {
|
||||
it('should transform response correctly', () => {
|
||||
const options: DataQueryRequest<SQLQuery> = {
|
||||
range: {
|
||||
from: dateTime(1432288354),
|
||||
to: dateTime(1432288401),
|
||||
raw: {
|
||||
from: 'now-24h',
|
||||
to: 'now',
|
||||
},
|
||||
},
|
||||
targets: [
|
||||
{
|
||||
format: QueryFormat.Table,
|
||||
rawQuery: true,
|
||||
rawSql: 'select time, metric, value from grafana_metric',
|
||||
refId: 'A',
|
||||
datasource: { type: 'gdev-ds', uid: 'gdev-ds' },
|
||||
},
|
||||
],
|
||||
requestId: 'test',
|
||||
interval: '1m',
|
||||
intervalMs: 60000,
|
||||
scopedVars: {},
|
||||
timezone: 'Etc/UTC',
|
||||
app: 'Grafana',
|
||||
startTime: 1432288354,
|
||||
};
|
||||
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: DataQueryResponse } = {
|
||||
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: LoadingState.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<SQLQuery>;
|
||||
|
||||
const { ds } = setupTestContext({});
|
||||
|
||||
await expect(ds.query(options)).toEmitValuesWith((received) => {
|
||||
expect(received[0]).toEqual({ data: [] });
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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: SQLQuery = {
|
||||
rawSql,
|
||||
refId: 'A',
|
||||
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: SQLQuery = {
|
||||
rawSql,
|
||||
refId: 'A',
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setupFetchMock(response: any, mock?: any) {
|
||||
const defaultMock = () => mock ?? of(createFetchResponse(response));
|
||||
|
||||
const fetchMock = jest.spyOn(backendSrv, 'fetch');
|
||||
fetchMock.mockImplementation(defaultMock);
|
||||
return fetchMock;
|
||||
}
|
||||
const createFetchResponse = <T>(data: T): FetchResponse<T> => ({
|
||||
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,
|
||||
});
|
||||
|
@ -1,232 +1,92 @@
|
||||
import { map as _map } from 'lodash';
|
||||
import { lastValueFrom, of } from 'rxjs';
|
||||
import { map, catchError } from 'rxjs/operators';
|
||||
import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
|
||||
import { AGGREGATE_FNS } from 'app/features/plugins/sql/constants';
|
||||
import { SqlDatasource } from 'app/features/plugins/sql/datasource/SqlDatasource';
|
||||
import { DB, LanguageCompletionProvider, SQLQuery, SQLSelectableValue } from 'app/features/plugins/sql/types';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
|
||||
import { AnnotationEvent, DataSourceInstanceSettings, MetricFindValue, ScopedVars, TimeRange } from '@grafana/data';
|
||||
import { BackendDataSourceResponse, DataSourceWithBackend, FetchResponse, getBackendSrv } from '@grafana/runtime';
|
||||
import { toTestingStatus } from '@grafana/runtime/src/utils/queryResponse';
|
||||
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import PostgresQueryModel from 'app/plugins/datasource/postgres/postgres_query_model';
|
||||
import { FUNCTIONS } from '../mysql/functions';
|
||||
|
||||
import { getSearchFilterScopedVar } from '../../../features/variables/utils';
|
||||
import { PostgresQueryModel } from './PostgresQueryModel';
|
||||
import { getSchema, getTimescaleDBVersion, getVersion, showTables } from './postgresMetaQuery';
|
||||
import { fetchColumns, fetchTables, getSqlCompletionProvider } from './sqlCompletionProvider';
|
||||
import { getFieldConfig, toRawSql } from './sqlUtil';
|
||||
import { PostgresOptions } from './types';
|
||||
|
||||
import ResponseParser from './response_parser';
|
||||
import { PostgresOptions, PostgresQuery, PostgresQueryForInterpolation } from './types';
|
||||
export class PostgresDatasource extends SqlDatasource {
|
||||
completionProvider: LanguageCompletionProvider | undefined = undefined;
|
||||
|
||||
export class PostgresDatasource extends DataSourceWithBackend<PostgresQuery, PostgresOptions> {
|
||||
id: any;
|
||||
name: any;
|
||||
jsonData: any;
|
||||
responseParser: ResponseParser;
|
||||
queryModel: PostgresQueryModel;
|
||||
interval: string;
|
||||
|
||||
constructor(
|
||||
instanceSettings: DataSourceInstanceSettings<PostgresOptions>,
|
||||
private readonly templateSrv: TemplateSrv = getTemplateSrv()
|
||||
) {
|
||||
constructor(instanceSettings: DataSourceInstanceSettings<PostgresOptions>) {
|
||||
super(instanceSettings);
|
||||
this.name = instanceSettings.name;
|
||||
this.id = instanceSettings.id;
|
||||
this.jsonData = instanceSettings.jsonData;
|
||||
this.responseParser = new ResponseParser();
|
||||
this.queryModel = new PostgresQueryModel({});
|
||||
const settingsData = instanceSettings.jsonData || ({} as PostgresOptions);
|
||||
this.interval = settingsData.timeInterval || '1m';
|
||||
}
|
||||
|
||||
interpolateVariable = (value: string | string[], variable: { multi: any; includeAll: any }) => {
|
||||
if (typeof value === 'string') {
|
||||
if (variable.multi || variable.includeAll) {
|
||||
return this.queryModel.quoteLiteral(value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return value;
|
||||
}
|
||||
|
||||
const quotedValues = _map(value, (v) => {
|
||||
return this.queryModel.quoteLiteral(v);
|
||||
});
|
||||
return quotedValues.join(',');
|
||||
};
|
||||
|
||||
interpolateVariablesInQueries(
|
||||
queries: PostgresQueryForInterpolation[],
|
||||
scopedVars: ScopedVars
|
||||
): PostgresQueryForInterpolation[] {
|
||||
let expandedQueries = queries;
|
||||
if (queries && queries.length > 0) {
|
||||
expandedQueries = queries.map((query) => {
|
||||
const expandedQuery = {
|
||||
...query,
|
||||
datasource: this.getRef(),
|
||||
rawSql: this.templateSrv.replace(query.rawSql, scopedVars, this.interpolateVariable),
|
||||
rawQuery: true,
|
||||
};
|
||||
return expandedQuery;
|
||||
});
|
||||
}
|
||||
return expandedQueries;
|
||||
}
|
||||
|
||||
filterQuery(query: PostgresQuery): boolean {
|
||||
return !query.hide;
|
||||
}
|
||||
|
||||
applyTemplateVariables(target: PostgresQuery, scopedVars: ScopedVars): Record<string, any> {
|
||||
const queryModel = new PostgresQueryModel(target, this.templateSrv, scopedVars);
|
||||
return {
|
||||
refId: target.refId,
|
||||
datasource: this.getRef(),
|
||||
rawSql: queryModel.render(this.interpolateVariable),
|
||||
format: target.format,
|
||||
};
|
||||
}
|
||||
|
||||
async annotationQuery(options: any): Promise<AnnotationEvent[]> {
|
||||
if (!options.annotation.rawQuery) {
|
||||
return Promise.reject({
|
||||
message: 'Query missing in annotation definition',
|
||||
});
|
||||
}
|
||||
|
||||
const query = {
|
||||
refId: options.annotation.name,
|
||||
datasource: this.getRef(),
|
||||
rawSql: this.templateSrv.replace(options.annotation.rawQuery, options.scopedVars, this.interpolateVariable),
|
||||
format: 'table',
|
||||
};
|
||||
|
||||
return lastValueFrom(
|
||||
getBackendSrv()
|
||||
.fetch<BackendDataSourceResponse>({
|
||||
url: '/api/ds/query',
|
||||
method: 'POST',
|
||||
data: {
|
||||
from: options.range.from.valueOf().toString(),
|
||||
to: options.range.to.valueOf().toString(),
|
||||
queries: [query],
|
||||
},
|
||||
requestId: options.annotation.name,
|
||||
})
|
||||
.pipe(
|
||||
map(
|
||||
async (res: FetchResponse<BackendDataSourceResponse>) =>
|
||||
await this.responseParser.transformAnnotationResponse(options, res.data)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
metricFindQuery(query: string, optionalOptions: any): Promise<MetricFindValue[]> {
|
||||
let refId = 'tempvar';
|
||||
if (optionalOptions && optionalOptions.variable && optionalOptions.variable.name) {
|
||||
refId = optionalOptions.variable.name;
|
||||
}
|
||||
|
||||
const rawSql = this.templateSrv.replace(
|
||||
query,
|
||||
getSearchFilterScopedVar({ query, wildcardChar: '%', options: optionalOptions }),
|
||||
this.interpolateVariable
|
||||
);
|
||||
|
||||
const interpolatedQuery = {
|
||||
refId: refId,
|
||||
datasource: this.getRef(),
|
||||
rawSql,
|
||||
format: 'table',
|
||||
};
|
||||
|
||||
const range = optionalOptions?.range as TimeRange;
|
||||
|
||||
return lastValueFrom(
|
||||
getBackendSrv()
|
||||
.fetch<BackendDataSourceResponse>({
|
||||
url: '/api/ds/query',
|
||||
method: 'POST',
|
||||
data: {
|
||||
from: range?.from?.valueOf()?.toString(),
|
||||
to: range?.to?.valueOf()?.toString(),
|
||||
queries: [interpolatedQuery],
|
||||
},
|
||||
requestId: refId,
|
||||
})
|
||||
.pipe(
|
||||
map((rsp) => {
|
||||
return this.responseParser.transformMetricFindResponse(rsp);
|
||||
}),
|
||||
catchError((err) => {
|
||||
return of([]);
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _metaRequest(rawSql: string) {
|
||||
const refId = 'meta';
|
||||
const query = {
|
||||
refId: refId,
|
||||
datasource: this.getRef(),
|
||||
rawSql,
|
||||
format: 'table',
|
||||
};
|
||||
return getBackendSrv().fetch<BackendDataSourceResponse>({
|
||||
url: '/api/ds/query',
|
||||
method: 'POST',
|
||||
data: {
|
||||
queries: [query],
|
||||
},
|
||||
requestId: refId,
|
||||
});
|
||||
getQueryModel(target?: SQLQuery, templateSrv?: TemplateSrv, scopedVars?: ScopedVars): PostgresQueryModel {
|
||||
return new PostgresQueryModel(target, templateSrv, scopedVars);
|
||||
}
|
||||
|
||||
async getVersion(): Promise<string> {
|
||||
const value = await lastValueFrom(this._metaRequest("SELECT current_setting('server_version_num')::int/100"));
|
||||
const results = value.data.results['meta'];
|
||||
if (results.frames) {
|
||||
// This returns number
|
||||
return (results.frames[0].data?.values[0] as number[])[0].toString();
|
||||
}
|
||||
return '';
|
||||
const value = await this.runSql<{ version: number }>(getVersion());
|
||||
const results = value.fields.version.values.toArray();
|
||||
return results[0].toString();
|
||||
}
|
||||
|
||||
async getTimescaleDBVersion(): Promise<string[] | undefined> {
|
||||
const value = await lastValueFrom(
|
||||
this._metaRequest("SELECT extversion FROM pg_extension WHERE extname = 'timescaledb'")
|
||||
);
|
||||
const results = value.data.results['meta'];
|
||||
if (results.frames) {
|
||||
return results.frames[0].data?.values[0][0] as string[];
|
||||
}
|
||||
return undefined;
|
||||
async getTimescaleDBVersion(): Promise<string | undefined> {
|
||||
const value = await this.runSql<{ extversion: string }>(getTimescaleDBVersion());
|
||||
const results = value.fields.extversion.values.toArray();
|
||||
return results[0];
|
||||
}
|
||||
|
||||
testDatasource(): Promise<any> {
|
||||
return lastValueFrom(this._metaRequest('SELECT 1'))
|
||||
.then(() => {
|
||||
return { status: 'success', message: 'Database Connection OK' };
|
||||
})
|
||||
.catch((err: any) => {
|
||||
return toTestingStatus(err);
|
||||
});
|
||||
async fetchTables(): Promise<string[]> {
|
||||
const tables = await this.runSql<{ table: string[] }>(showTables(), { refId: 'tables' });
|
||||
return tables.fields.table.values.toArray().flat();
|
||||
}
|
||||
|
||||
targetContainsTemplate(target: any) {
|
||||
let rawSql = '';
|
||||
|
||||
if (target.rawQuery) {
|
||||
rawSql = target.rawSql;
|
||||
} else {
|
||||
const query = new PostgresQueryModel(target);
|
||||
rawSql = query.buildQuery();
|
||||
getSqlCompletionProvider(db: DB): LanguageCompletionProvider {
|
||||
if (this.completionProvider !== undefined) {
|
||||
return this.completionProvider;
|
||||
}
|
||||
|
||||
rawSql = rawSql.replace('$__', '');
|
||||
const args = {
|
||||
getColumns: { current: (query: SQLQuery) => fetchColumns(db, query) },
|
||||
getTables: { current: () => fetchTables(db) },
|
||||
//TODO: Add aggregate functions
|
||||
getFunctions: { current: () => [...AGGREGATE_FNS, ...FUNCTIONS] },
|
||||
};
|
||||
this.completionProvider = getSqlCompletionProvider(args);
|
||||
return this.completionProvider;
|
||||
}
|
||||
|
||||
return this.templateSrv.containsTemplate(rawSql);
|
||||
async fetchFields(query: SQLQuery): Promise<SQLSelectableValue[]> {
|
||||
const schema = await this.runSql<{ column: string; type: string }>(getSchema(query.table), { refId: 'columns' });
|
||||
const result: SQLSelectableValue[] = [];
|
||||
for (let i = 0; i < schema.length; i++) {
|
||||
const column = schema.fields.column.values.get(i);
|
||||
const type = schema.fields.type.values.get(i);
|
||||
result.push({ label: column, value: column, type, ...getFieldConfig(type) });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
getDB(): DB {
|
||||
return {
|
||||
init: () => Promise.resolve(true),
|
||||
datasets: () => Promise.resolve([]),
|
||||
tables: () => this.fetchTables(),
|
||||
getSqlCompletionProvider: () => this.getSqlCompletionProvider(this.db),
|
||||
fields: async (query: SQLQuery) => {
|
||||
if (!query?.table) {
|
||||
return [];
|
||||
}
|
||||
return this.fetchFields(query);
|
||||
},
|
||||
validateQuery: (query) =>
|
||||
Promise.resolve({ isError: false, isValid: true, query, error: '', rawSql: query.rawSql }),
|
||||
dsID: () => this.id,
|
||||
toRawSql,
|
||||
lookup: async () => {
|
||||
const tables = await this.fetchTables();
|
||||
return tables.map((t) => ({ name: t, completion: t }));
|
||||
},
|
||||
functions: async () => AGGREGATE_FNS,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,169 +0,0 @@
|
||||
import QueryModel from './postgres_query_model';
|
||||
|
||||
export class PostgresMetaQuery {
|
||||
constructor(private target: { table: string; timeColumn: string }, private queryModel: QueryModel) {}
|
||||
|
||||
getOperators(datatype: string) {
|
||||
switch (datatype) {
|
||||
case 'float4':
|
||||
case 'float8': {
|
||||
return ['=', '!=', '<', '<=', '>', '>='];
|
||||
}
|
||||
case 'text':
|
||||
case 'varchar':
|
||||
case 'char': {
|
||||
return ['=', '!=', '<', '<=', '>', '>=', 'IN', 'NOT IN', 'LIKE', 'NOT LIKE', '~', '~*', '!~', '!~*'];
|
||||
}
|
||||
default: {
|
||||
return ['=', '!=', '<', '<=', '>', '>=', 'IN', 'NOT IN'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// quote identifier as literal to use in metadata queries
|
||||
quoteIdentAsLiteral(value: string) {
|
||||
return this.queryModel.quoteLiteral(this.queryModel.unquoteIdentifier(value));
|
||||
}
|
||||
|
||||
findMetricTable() {
|
||||
// query that returns first table found that has a timestamp(tz) column and a float column
|
||||
let query = `
|
||||
SELECT
|
||||
quote_ident(table_name) as table_name,
|
||||
( SELECT
|
||||
quote_ident(column_name) as column_name
|
||||
FROM information_schema.columns c
|
||||
WHERE
|
||||
c.table_schema = t.table_schema AND
|
||||
c.table_name = t.table_name AND
|
||||
udt_name IN ('timestamptz','timestamp')
|
||||
ORDER BY ordinal_position LIMIT 1
|
||||
) AS time_column,
|
||||
( SELECT
|
||||
quote_ident(column_name) AS column_name
|
||||
FROM information_schema.columns c
|
||||
WHERE
|
||||
c.table_schema = t.table_schema AND
|
||||
c.table_name = t.table_name AND
|
||||
udt_name='float8'
|
||||
ORDER BY ordinal_position LIMIT 1
|
||||
) AS value_column
|
||||
FROM information_schema.tables t
|
||||
WHERE `;
|
||||
query += this.buildSchemaConstraint();
|
||||
query += ` AND
|
||||
EXISTS
|
||||
( SELECT 1
|
||||
FROM information_schema.columns c
|
||||
WHERE
|
||||
c.table_schema = t.table_schema AND
|
||||
c.table_name = t.table_name AND
|
||||
udt_name IN ('timestamptz','timestamp')
|
||||
) AND
|
||||
EXISTS
|
||||
( SELECT 1
|
||||
FROM information_schema.columns c
|
||||
WHERE
|
||||
c.table_schema = t.table_schema AND
|
||||
c.table_name = t.table_name AND
|
||||
udt_name='float8'
|
||||
)
|
||||
LIMIT 1
|
||||
;`;
|
||||
return query;
|
||||
}
|
||||
|
||||
buildSchemaConstraint() {
|
||||
// quote_ident protects hyphenated schemes
|
||||
const query = `
|
||||
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
|
||||
)`;
|
||||
return query;
|
||||
}
|
||||
|
||||
buildTableConstraint(table: string) {
|
||||
let query = '';
|
||||
|
||||
// check for schema qualified table
|
||||
if (table.includes('.')) {
|
||||
const parts = table.split('.');
|
||||
query = 'table_schema = ' + this.quoteIdentAsLiteral(parts[0]);
|
||||
query += ' AND table_name = ' + this.quoteIdentAsLiteral(parts[1]);
|
||||
return query;
|
||||
} else {
|
||||
query = this.buildSchemaConstraint();
|
||||
query += ' AND table_name = ' + this.quoteIdentAsLiteral(table);
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
|
||||
buildTableQuery() {
|
||||
let query = 'SELECT quote_ident(table_name) FROM information_schema.tables WHERE ';
|
||||
query += this.buildSchemaConstraint();
|
||||
query += ' ORDER BY table_name';
|
||||
return query;
|
||||
}
|
||||
|
||||
buildColumnQuery(type?: string) {
|
||||
let query = 'SELECT quote_ident(column_name) FROM information_schema.columns WHERE ';
|
||||
query += this.buildTableConstraint(this.target.table);
|
||||
|
||||
switch (type) {
|
||||
case 'time': {
|
||||
query +=
|
||||
" AND data_type IN ('timestamp without time zone','timestamp with time zone','bigint','integer','double precision','real')";
|
||||
break;
|
||||
}
|
||||
case 'metric': {
|
||||
query += " AND data_type IN ('text','character','character varying')";
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
query += " AND data_type IN ('bigint','integer','double precision','real','numeric')";
|
||||
query += ' AND column_name <> ' + this.quoteIdentAsLiteral(this.target.timeColumn);
|
||||
break;
|
||||
}
|
||||
case 'group': {
|
||||
query += " AND data_type IN ('text','character','character varying','uuid')";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
query += ' ORDER BY column_name';
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
buildValueQuery(column: string) {
|
||||
let query = 'SELECT DISTINCT quote_literal(' + column + ')';
|
||||
query += ' FROM ' + this.target.table;
|
||||
query += ' WHERE $__timeFilter(' + this.target.timeColumn + ')';
|
||||
query += ' AND ' + column + ' IS NOT NULL';
|
||||
query += ' ORDER BY 1 LIMIT 100';
|
||||
return query;
|
||||
}
|
||||
|
||||
buildDatatypeQuery(column: string) {
|
||||
let query = 'SELECT udt_name FROM information_schema.columns WHERE ';
|
||||
query += this.buildTableConstraint(this.target.table);
|
||||
query += ' AND column_name = ' + this.quoteIdentAsLiteral(column);
|
||||
return query;
|
||||
}
|
||||
|
||||
buildAggregateQuery() {
|
||||
let query = 'SELECT DISTINCT proname FROM pg_aggregate ';
|
||||
query += 'INNER JOIN pg_proc ON pg_aggregate.aggfnoid = pg_proc.oid ';
|
||||
query += 'INNER JOIN pg_type ON pg_type.oid=pg_proc.prorettype ';
|
||||
query += "WHERE pronargs=1 AND typname IN ('float8') AND aggkind='n' ORDER BY 1";
|
||||
return query;
|
||||
}
|
||||
}
|
@ -1,114 +0,0 @@
|
||||
ace.define(
|
||||
'ace/mode/sql_highlight_rules',
|
||||
['require', 'exports', 'module', 'ace/lib/oop', 'ace/mode/text_highlight_rules'],
|
||||
function (require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
const oop = require('../lib/oop');
|
||||
const TextHighlightRules = require('./text_highlight_rules').TextHighlightRules;
|
||||
|
||||
const SqlHighlightRules = function () {
|
||||
const keywords =
|
||||
'select|insert|update|delete|from|where|and|or|group|by|order|limit|offset|having|as|case|' +
|
||||
'when|else|end|type|left|right|join|on|outer|desc|asc|union|create|table|primary|key|if|' +
|
||||
'foreign|not|references|default|null|inner|cross|natural|database|drop|grant';
|
||||
|
||||
const builtinConstants = 'true|false';
|
||||
|
||||
const builtinFunctions =
|
||||
'avg|count|first|last|max|min|sum|upper|lower|substring|char_length|round|rank|now|' + 'coalesce';
|
||||
|
||||
const dataTypes =
|
||||
'int|int2|int4|int8|numeric|decimal|date|varchar|char|bigint|float|bool|bytea|text|timestamp|' +
|
||||
'time|money|real|integer';
|
||||
|
||||
const keywordMapper = this.createKeywordMapper(
|
||||
{
|
||||
'support.function': builtinFunctions,
|
||||
keyword: keywords,
|
||||
'constant.language': builtinConstants,
|
||||
'storage.type': dataTypes,
|
||||
},
|
||||
'identifier',
|
||||
true
|
||||
);
|
||||
|
||||
this.$rules = {
|
||||
start: [
|
||||
{
|
||||
token: 'comment',
|
||||
regex: '--.*$',
|
||||
},
|
||||
{
|
||||
token: 'comment',
|
||||
start: '/\\*',
|
||||
end: '\\*/',
|
||||
},
|
||||
{
|
||||
token: 'string', // " string
|
||||
regex: '".*?"',
|
||||
},
|
||||
{
|
||||
token: 'string', // ' string
|
||||
regex: "'.*?'",
|
||||
},
|
||||
{
|
||||
token: 'constant.numeric', // float
|
||||
regex: '[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b',
|
||||
},
|
||||
{
|
||||
token: keywordMapper,
|
||||
regex: '[a-zA-Z_$][a-zA-Z0-9_$]*\\b',
|
||||
},
|
||||
{
|
||||
token: 'keyword.operator',
|
||||
regex: '\\+|\\-|\\/|\\/\\/|%|<@>|@>|<@|&|\\^|~|<|>|<=|=>|==|!=|<>|=',
|
||||
},
|
||||
{
|
||||
token: 'paren.lparen',
|
||||
regex: '[\\(]',
|
||||
},
|
||||
{
|
||||
token: 'paren.rparen',
|
||||
regex: '[\\)]',
|
||||
},
|
||||
{
|
||||
token: 'text',
|
||||
regex: '\\s+',
|
||||
},
|
||||
],
|
||||
};
|
||||
this.normalizeRules();
|
||||
};
|
||||
|
||||
oop.inherits(SqlHighlightRules, TextHighlightRules);
|
||||
|
||||
exports.SqlHighlightRules = SqlHighlightRules;
|
||||
}
|
||||
);
|
||||
|
||||
ace.define(
|
||||
'ace/mode/sql',
|
||||
['require', 'exports', 'module', 'ace/lib/oop', 'ace/mode/text', 'ace/mode/sql_highlight_rules'],
|
||||
function (require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
const oop = require('../lib/oop');
|
||||
const TextMode = require('./text').Mode;
|
||||
const SqlHighlightRules = require('./sql_highlight_rules').SqlHighlightRules;
|
||||
|
||||
const Mode = function () {
|
||||
this.HighlightRules = SqlHighlightRules;
|
||||
this.$behaviour = this.$defaultBehaviour;
|
||||
};
|
||||
oop.inherits(Mode, TextMode);
|
||||
|
||||
(function () {
|
||||
this.lineCommentStart = '--';
|
||||
|
||||
this.$id = 'ace/mode/sql';
|
||||
}.call(Mode.prototype));
|
||||
|
||||
exports.Mode = Mode;
|
||||
}
|
||||
);
|
@ -1,35 +1,13 @@
|
||||
import { DataSourcePlugin } from '@grafana/data';
|
||||
import { SQLQuery } from 'app/features/plugins/sql/types';
|
||||
|
||||
import { QueryEditor } from './QueryEditor';
|
||||
import { PostgresConfigEditor } from './configuration/ConfigurationEditor';
|
||||
import { PostgresDatasource } from './datasource';
|
||||
import { PostgresQueryCtrl } from './query_ctrl';
|
||||
import { PostgresOptions, PostgresQuery, SecureJsonData } from './types';
|
||||
import { PostgresOptions, SecureJsonData } from './types';
|
||||
|
||||
const defaultQuery = `SELECT
|
||||
extract(epoch from time_column) AS time,
|
||||
text_column as text,
|
||||
tags_column as tags
|
||||
FROM
|
||||
metric_table
|
||||
WHERE
|
||||
$__timeFilter(time_column)
|
||||
`;
|
||||
|
||||
class PostgresAnnotationsQueryCtrl {
|
||||
static templateUrl = 'partials/annotations.editor.html';
|
||||
|
||||
declare annotation: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope: any) {
|
||||
this.annotation = $scope.ctrl.annotation;
|
||||
this.annotation.rawQuery = this.annotation.rawQuery || defaultQuery;
|
||||
}
|
||||
}
|
||||
|
||||
export const plugin = new DataSourcePlugin<PostgresDatasource, PostgresQuery, PostgresOptions, SecureJsonData>(
|
||||
export const plugin = new DataSourcePlugin<PostgresDatasource, SQLQuery, PostgresOptions, SecureJsonData>(
|
||||
PostgresDatasource
|
||||
)
|
||||
.setQueryCtrl(PostgresQueryCtrl)
|
||||
.setConfigEditor(PostgresConfigEditor)
|
||||
.setAnnotationQueryCtrl(PostgresAnnotationsQueryCtrl);
|
||||
.setQueryEditor(QueryEditor)
|
||||
.setConfigEditor(PostgresConfigEditor);
|
||||
|
@ -1,56 +0,0 @@
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--grow">
|
||||
<textarea
|
||||
rows="10"
|
||||
class="gf-form-input"
|
||||
ng-model="ctrl.annotation.rawQuery"
|
||||
spellcheck="false"
|
||||
placeholder="query expression"
|
||||
data-min-length="0"
|
||||
data-items="100"
|
||||
ng-model-onblur
|
||||
ng-change="ctrl.panelCtrl.refresh()"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword" ng-click="ctrl.showHelp = !ctrl.showHelp">
|
||||
Show Help
|
||||
<icon name="'angle-down'" ng-show="ctrl.showHelp" style="margin-top: 3px;"></icon>
|
||||
<icon name="'angle-right'" ng-hide="ctrl.showHelp" style="margin-top: 3px;"></icon>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-show="ctrl.showHelp">
|
||||
<div class="grafana-info-box">
|
||||
<pre class="pre--no-style"><h6>Annotation Query Format</h6>
|
||||
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: <b>time</b> for the annotation event time. Use epoch time or any native date data type.
|
||||
- column with alias: <b>timeend</b> for the annotation event time-end. Use epoch time or any native date data type.
|
||||
- column with alias: <b>text</b> for the annotation text
|
||||
- column with alias: <b>tags</b> 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
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,191 +0,0 @@
|
||||
<query-editor-row query-ctrl="ctrl" has-text-edit-mode="true">
|
||||
|
||||
<div ng-if="ctrl.target.rawQuery">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--grow">
|
||||
<code-editor content="ctrl.target.rawSql" datasource="ctrl.datasource" on-change="ctrl.panelCtrl.refresh()" data-mode="sql" textarea-label="Query Editor">
|
||||
</code-editor>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="!ctrl.target.rawQuery">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-6">FROM</label>
|
||||
<metric-segment segment="ctrl.tableSegment" get-options="ctrl.getTableSegments()" on-change="ctrl.tableChanged()"></metric-segment>
|
||||
|
||||
<label class="gf-form-label query-keyword width-7">Time column</label>
|
||||
<metric-segment segment="ctrl.timeColumnSegment" get-options="ctrl.getTimeColumnSegments()" on-change="ctrl.timeColumnChanged()"></metric-segment>
|
||||
|
||||
<label class="gf-form-label query-keyword width-9">
|
||||
Metric column
|
||||
<info-popover mode="right-normal">Column to be used as metric name for the value column.</info-popover>
|
||||
</label>
|
||||
<metric-segment segment="ctrl.metricColumnSegment" get-options="ctrl.getMetricColumnSegments()" on-change="ctrl.metricColumnChanged()"></metric-segment>
|
||||
</div>
|
||||
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline" ng-repeat="selectParts in ctrl.selectParts">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-6">
|
||||
<span ng-show="$index === 0">SELECT</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-repeat="part in selectParts">
|
||||
<sql-part-editor class="gf-form-label sql-part" part="part" handle-event="ctrl.handleSelectPartEvent(selectParts, part, $event)">
|
||||
</sql-part-editor>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="dropdown"
|
||||
dropdown-typeahead2="ctrl.selectMenu"
|
||||
dropdown-typeahead-on-select="ctrl.addSelectPart(selectParts, $item, $subItem)"
|
||||
button-template-class="gf-form-label query-part"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-6">WHERE</label>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-repeat="part in ctrl.whereParts">
|
||||
<sql-part-editor class="gf-form-label sql-part" part="part" handle-event="ctrl.handleWherePartEvent(ctrl.whereParts, part, $event, $index)">
|
||||
</sql-part-editor>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<metric-segment segment="ctrl.whereAdd" get-options="ctrl.getWhereOptions()" on-change="ctrl.addWhereAction(part, $index)"></metric-segment>
|
||||
</div>
|
||||
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-6">
|
||||
<span>GROUP BY</span>
|
||||
</label>
|
||||
|
||||
<sql-part-editor ng-repeat="part in ctrl.groupParts"
|
||||
part="part" class="gf-form-label sql-part"
|
||||
handle-event="ctrl.handleGroupPartEvent(part, $index, $event)">
|
||||
</sql-part-editor>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<metric-segment segment="ctrl.groupAdd" get-options="ctrl.getGroupOptions()" on-change="ctrl.addGroupAction(part, $index)"></metric-segment>
|
||||
</div>
|
||||
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword" for="format-select-{{ ctrl.target.refId }}">Format as</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select id="format-select-{{ ctrl.target.refId }}" class="gf-form-input gf-size-auto" ng-model="ctrl.target.format" ng-options="f.value as f.text for f in ctrl.formats" ng-change="ctrl.refresh()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword pointer" ng-click="ctrl.toggleEditorMode()" ng-show="ctrl.panelCtrl.panel.type !== 'table'">
|
||||
<span ng-show="ctrl.target.rawQuery">Query Builder</span>
|
||||
<span ng-hide="ctrl.target.rawQuery">Edit SQL</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword pointer" ng-click="ctrl.showHelp = !ctrl.showHelp">
|
||||
Show Help
|
||||
<icon name="'angle-down'" ng-show="ctrl.showHelp" style="margin-top: 3px;"></icon>
|
||||
<icon name="'angle-right'" ng-hide="ctrl.showHelp" style="margin-top: 3px;"></icon>
|
||||
</label>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.lastQueryMeta">
|
||||
<label class="gf-form-label query-keyword pointer" ng-click="ctrl.showLastQuerySQL = !ctrl.showLastQuerySQL">
|
||||
Generated SQL
|
||||
<icon name="'angle-down'" ng-show="ctrl.showLastQuerySQL" style="margin-top: 3px;"></icon>
|
||||
<icon name="'angle-right'" ng-hide="ctrl.showLastQuerySQL" style="margin-top: 3px;"></icon>
|
||||
</label>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="gf-form" ng-show="ctrl.showHelp">
|
||||
<pre class="gf-form-pre alert alert-info">Time series:
|
||||
- return column named <i>time</i> (UTC in seconds or timestamp)
|
||||
- return column(s) with numeric datatype as values
|
||||
Optional:
|
||||
- return column named <i>metric</i> 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
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-show="ctrl.showLastQuerySQL">
|
||||
<pre class="gf-form-pre">{{ctrl.lastQueryMeta.executedQueryString}}</pre>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-show="ctrl.lastQueryError">
|
||||
<pre class="gf-form-pre alert alert-error">{{ctrl.lastQueryError}}</pre>
|
||||
</div>
|
||||
|
||||
</query-editor-row>
|
41
public/app/plugins/datasource/postgres/postgresMetaQuery.ts
Normal file
41
public/app/plugins/datasource/postgres/postgresMetaQuery.ts
Normal file
@ -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
|
||||
)`;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 [];
|
||||
}
|
||||
}
|
@ -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<BackendDataSourceResponse>): 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<AnnotationEvent[]> {
|
||||
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;
|
||||
}
|
||||
}
|
@ -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<PostgresOptions>;
|
||||
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<PostgresQuery>;
|
||||
|
||||
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 = <T>(data: T): FetchResponse<T> => ({
|
||||
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,
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
@ -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<ColumnDefinition[]>>;
|
||||
getTables: React.MutableRefObject<(d?: string) => Promise<TableDefinition[]>>;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
112
public/app/plugins/datasource/postgres/sqlUtil.ts
Normal file
112
public/app/plugins/datasource/postgres/sqlUtil.ts
Normal file
@ -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<SQLExpression['columns']>): 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<SQLExpression['columns']> => {
|
||||
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;
|
||||
};
|
@ -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,
|
||||
};
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user