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:
Zoltán Bedi 2022-11-02 05:30:35 +01:00 committed by GitHub
parent 0c3ed0219e
commit 26659baf8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1052 additions and 3093 deletions

View File

@ -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"]
],

View File

@ -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');

View File

@ -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} />

View File

@ -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>

View File

@ -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);

View File

@ -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}
/>
);
}

View File

@ -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"

View File

@ -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') {

View File

@ -57,6 +57,7 @@ export interface QueryEditorGroupByExpression {
export interface QueryEditorFunctionExpression {
type: QueryEditorExpressionType.Function;
name?: string;
alias?: string;
parameters?: QueryEditorFunctionParameterExpression[];
}

View File

@ -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}`)}`;
}

View File

@ -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();

View File

@ -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}`)}`;
}

View File

@ -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;

View File

@ -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');
}
}

View File

@ -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');

View File

@ -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 {}

View 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, "''") + "'";
}
}

View 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} />;
}

View File

@ -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);
}
}

View File

@ -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,
});

View File

@ -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,
};
}
}

View File

@ -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;
}
}

View File

@ -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;
}
);

View File

@ -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);

View File

@ -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) -&gt; column as "time"
- $__timeEpoch -&gt; extract(epoch from column) as "time"
- $__timeFilter(column) -&gt; column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z'
- $__unixEpochFilter(column) -&gt; column &gt;= 1492750877 AND column &lt;= 1492750877
- $__unixEpochNanoFilter(column) -&gt; column &gt;= 1494410783152415214 AND column &lt;= 1494497183142514872
Or build your own conditionals using these macros which just return the values:
- $__timeFrom() -&gt; '2017-04-21T05:01:17Z'
- $__timeTo() -&gt; '2017-04-21T05:01:17Z'
- $__unixEpochFrom() -&gt; 1492750877
- $__unixEpochTo() -&gt; 1492750877
- $__unixEpochNanoFrom() -&gt; 1494410783152415214
- $__unixEpochNanoTo() -&gt; 1494497183142514872
</pre>
</div>
</div>
</div>

View File

@ -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>&nbsp;
</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) -&gt; column as "time"
- $__timeEpoch -&gt; extract(epoch from column) as "time"
- $__timeFilter(column) -&gt; column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z'
- $__unixEpochFilter(column) -&gt; column &gt;= 1492750877 AND column &lt;= 1492750877
- $__unixEpochNanoFilter(column) -&gt; column &gt;= 1494410783152415214 AND column &lt;= 1494497183142514872
- $__timeGroup(column,'5m'[, fillvalue]) -&gt; (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') -&gt; (extract(epoch from column)/300)::bigint*300 AS "time"
- $__unixEpochGroup(column,'5m') -&gt; floor(column/300)*300
- $__unixEpochGroupAlias(column,'5m') -&gt; 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() -&gt; '2017-04-21T05:01:17Z'
- $__timeTo() -&gt; '2017-04-21T05:01:17Z'
- $__unixEpochFrom() -&gt; 1492750877
- $__unixEpochTo() -&gt; 1492750877
- $__unixEpochNanoFrom() -&gt; 1494410783152415214
- $__unixEpochNanoTo() -&gt; 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>

View 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
)`;
}

View File

@ -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;
}
}

View File

@ -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 [];
}
}

View File

@ -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;
}
}

View File

@ -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,
});

View File

@ -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);
});
});

View File

@ -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;
}

View 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;
};

View File

@ -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,
};

View File

@ -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;
}