Skip to content

Commit 6282a1b

Browse files
author
Scott Lepper
authored
multi line time series (#40)
* multi line time series
1 parent faeeccf commit 6282a1b

File tree

7 files changed

+126
-83
lines changed

7 files changed

+126
-83
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 0.9.7
4+
5+
Feature - Multi-line time series.
6+
37
## 0.9.6
48

59
Bug - Change time template variable names.

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,21 @@ The query editor allows you to query ClickHouse to return time series or tabular
5252
5353
Time series visualization options are selectable after adding a `datetime` field type to your query. This field will be used as the timestamp You can select time series visualizations using the visualization options. Grafana interprets timestamp rows without explicit time zone as UTC. Any column except time is treated as a value column.
5454

55+
#### Multi-line time series
56+
57+
To create multi-line time series, the query must return at least 3 fields.
58+
- field 1: `datetime` field with an alias of `time`
59+
- field 2: value to group by
60+
- field 3+: the metric values
61+
62+
For example:
63+
```sql
64+
SELECT log_time as time, machine_group, avg(disk_free) as avg_disk_free
65+
from mgbench.logs1
66+
group by machine_group, log_time
67+
order by log_time
68+
```
69+
5570
### Query as table
5671

5772
Table visualizations will always be available for any valid ClickHouse query.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "clickhouse-datasource",
3-
"version": "0.9.6",
3+
"version": "0.9.7",
44
"description": "Clickhouse Datasource",
55
"scripts": {
66
"build": "grafana-toolkit plugin:build",

src/components/SQLEditor.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,35 @@ import { QueryEditorProps } from '@grafana/data';
33
import { CodeEditor } from '@grafana/ui';
44
import { Datasource } from '../data/CHDatasource';
55
import { registerSQL, Range, Fetcher } from './sqlProvider';
6-
import { CHQuery, CHConfig } from '../types';
6+
import { CHQuery, CHConfig, Format } from '../types';
77
import { styles } from '../styles';
88
import { fetchSuggestions as sugg, Schema } from './suggestions';
99
import { selectors } from 'selectors';
10+
import sqlToAST from '../data/ast';
11+
import { isString } from 'lodash';
1012

1113
type SQLEditorProps = QueryEditorProps<Datasource, CHQuery, CHConfig>;
1214

1315
export const SQLEditor = (props: SQLEditorProps) => {
1416
const { query, onRunQuery, onChange, datasource } = props;
17+
18+
const getFormat = (sql: string): Format => {
19+
// convention to format as time series
20+
// first field as "time" alias and requires at least 2 fields (time and metric)
21+
const ast = sqlToAST(sql);
22+
const select = ast.get('SELECT');
23+
if (isString(select)) {
24+
const fields = select.split(',');
25+
if (fields.length > 1) {
26+
return fields[0].toLowerCase().endsWith('as time') ? Format.TIMESERIES : Format.TABLE;
27+
}
28+
}
29+
return Format.TABLE;
30+
};
31+
1532
const onSqlChange = (sql: string) => {
16-
onChange({ ...query, rawSql: sql, format: 1 });
33+
const format = getFormat(sql);
34+
onChange({ ...query, rawSql: sql, format });
1735
onRunQuery();
1836
};
1937

src/data/adHocFilter.ts

Lines changed: 4 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import sqlToAST, { clausesToSql, Clause } from './ast';
2+
13
export class AdHocFilter {
24
private _targetTable = '';
35

@@ -23,48 +25,9 @@ export class AdHocFilter {
2325
}
2426
// Semicolons are not required and cause problems when building the SQL
2527
sql = sql.replace(';', '');
26-
const ast = this.sqlToAST(sql);
28+
const ast = sqlToAST(sql);
2729
const filteredAST = this.applyFiltersToAST(ast, whereClause);
28-
return this.clausesToSql(filteredAST);
29-
}
30-
31-
private sqlToAST(sql: string): Map<string, Clause> {
32-
const ast = this.createStatement();
33-
const re =
34-
/\b(WITH|SELECT|DISTINCT|FROM|SAMPLE|JOIN|PREWHERE|WHERE|GROUP BY|LIMIT BY|HAVING|LIMIT|OFFSET|UNION|INTERSECT|EXCEPT|INTO OUTFILE|FORMAT)\b/gi;
35-
let bracketCount = 0;
36-
let lastBracketCount = 0;
37-
let lastNode = '';
38-
let bracketPhrase = '';
39-
let regExpArray: RegExpExecArray | null;
40-
while ((regExpArray = re.exec(sql)) !== null) {
41-
// Sets foundNode to a SQL keyword from the regular expression
42-
const foundNode = regExpArray[0].toUpperCase();
43-
const phrase = sql.substring(re.lastIndex, sql.length).split(re)[0];
44-
// If there is a greater number of open brackets than closed,
45-
// add the the bracket phrase that will eventually be added the the last node
46-
if (bracketCount > 0) {
47-
bracketPhrase += foundNode + phrase;
48-
} else {
49-
ast.set(foundNode, phrase);
50-
lastNode = foundNode;
51-
}
52-
bracketCount += (phrase.match(/\(/g) || []).length;
53-
bracketCount -= (phrase.match(/\)/g) || []).length;
54-
if (bracketCount <= 0 && lastBracketCount > 0) {
55-
// The phrase brackets is complete
56-
// If it is a FROM phrase lets make a branch node
57-
// If it is anything else lets make a leaf node
58-
if (lastNode === 'FROM') {
59-
ast.set(lastNode, this.sqlToAST(bracketPhrase));
60-
} else {
61-
const p = (ast.get(lastNode) as string).concat(bracketPhrase);
62-
ast.set(lastNode, p);
63-
}
64-
}
65-
lastBracketCount = bracketCount;
66-
}
67-
return ast;
30+
return clausesToSql(filteredAST);
6831
}
6932

7033
private applyFiltersToAST(ast: Map<string, Clause>, whereClause: string): Map<string, Clause> {
@@ -87,19 +50,6 @@ export class AdHocFilter {
8750
return ast.set('FROM', fromAST);
8851
}
8952

90-
private clausesToSql(ast: Map<string, Clause>): string {
91-
let r = '';
92-
ast.forEach((c: Clause, key: string) => {
93-
if (typeof c === 'string') {
94-
r += `${key} ${c.trim()} `;
95-
} else if (c !== null) {
96-
r += `${key} (${this.clausesToSql(c)} `;
97-
}
98-
});
99-
// Remove all of the consecutive spaces to make things more readable when debugging
100-
return r.trim().replace(/\s+/g, ' ');
101-
}
102-
10353
// Returns a table name found in the FROM phrase
10454
// FROM phrases might contain more than just the table name
10555
private getTableName(fromPhrase: string): string {
@@ -108,30 +58,6 @@ export class AdHocFilter {
10858
.split(' ')[0]
10959
.replace(/(;|\(|\))/g, '');
11060
}
111-
112-
// Creates a statement with all the keywords to preserve the keyword order
113-
private createStatement() {
114-
let clauses = new Map<string, Clause>();
115-
clauses.set('WITH', null);
116-
clauses.set('SELECT', null);
117-
clauses.set('DISTINCT', null);
118-
clauses.set('FROM', null);
119-
clauses.set('SAMPLE', null);
120-
clauses.set('JOIN', null);
121-
clauses.set('PREWHERE', null);
122-
clauses.set('WHERE', null);
123-
clauses.set('GROUP BY', null);
124-
clauses.set('LIMIT BY', null);
125-
clauses.set('HAVING', null);
126-
clauses.set('LIMIT', null);
127-
clauses.set('OFFSET', null);
128-
clauses.set('UNION', null);
129-
clauses.set('INTERSECT', null);
130-
clauses.set('EXCEPT', null);
131-
clauses.set('INTO OUTFILE', null);
132-
clauses.set('FORMAT', null);
133-
return clauses;
134-
}
13561
}
13662

13763
export type AdHocVariableFilter = {
@@ -140,5 +66,3 @@ export type AdHocVariableFilter = {
14066
value: string;
14167
condition: string;
14268
};
143-
144-
type Clause = string | Map<string, Clause> | null;

src/data/ast.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
export type Clause = string | Map<string, Clause> | null;
2+
3+
export default function sqlToAST(sql: string): Map<string, Clause> {
4+
const ast = createStatement();
5+
const re =
6+
/\b(WITH|SELECT|DISTINCT|FROM|SAMPLE|JOIN|PREWHERE|WHERE|GROUP BY|LIMIT BY|HAVING|LIMIT|OFFSET|UNION|INTERSECT|EXCEPT|INTO OUTFILE|FORMAT)\b/gi;
7+
let bracketCount = 0;
8+
let lastBracketCount = 0;
9+
let lastNode = '';
10+
let bracketPhrase = '';
11+
let regExpArray: RegExpExecArray | null;
12+
while ((regExpArray = re.exec(sql)) !== null) {
13+
// Sets foundNode to a SQL keyword from the regular expression
14+
const foundNode = regExpArray[0].toUpperCase();
15+
const phrase = sql.substring(re.lastIndex, sql.length).split(re)[0];
16+
// If there is a greater number of open brackets than closed,
17+
// add the the bracket phrase that will eventually be added the the last node
18+
if (bracketCount > 0) {
19+
bracketPhrase += foundNode + phrase;
20+
} else {
21+
ast.set(foundNode, phrase);
22+
lastNode = foundNode;
23+
}
24+
bracketCount += (phrase.match(/\(/g) || []).length;
25+
bracketCount -= (phrase.match(/\)/g) || []).length;
26+
if (bracketCount <= 0 && lastBracketCount > 0) {
27+
// The phrase brackets is complete
28+
// If it is a FROM phrase lets make a branch node
29+
// If it is anything else lets make a leaf node
30+
if (lastNode === 'FROM') {
31+
ast.set(lastNode, sqlToAST(bracketPhrase));
32+
} else {
33+
const p = (ast.get(lastNode) as string).concat(bracketPhrase);
34+
ast.set(lastNode, p);
35+
}
36+
}
37+
lastBracketCount = bracketCount;
38+
}
39+
return ast;
40+
}
41+
42+
export function clausesToSql(ast: Map<string, Clause>): string {
43+
let r = '';
44+
ast.forEach((c: Clause, key: string) => {
45+
if (typeof c === 'string') {
46+
r += `${key} ${c.trim()} `;
47+
} else if (c !== null) {
48+
r += `${key} (${clausesToSql(c)} `;
49+
}
50+
});
51+
// Remove all of the consecutive spaces to make things more readable when debugging
52+
return r.trim().replace(/\s+/g, ' ');
53+
}
54+
55+
// Creates a statement with all the keywords to preserve the keyword order
56+
function createStatement() {
57+
const clauses = new Map<string, Clause>();
58+
clauses.set('WITH', null);
59+
clauses.set('SELECT', null);
60+
clauses.set('DISTINCT', null);
61+
clauses.set('FROM', null);
62+
clauses.set('SAMPLE', null);
63+
clauses.set('JOIN', null);
64+
clauses.set('PREWHERE', null);
65+
clauses.set('WHERE', null);
66+
clauses.set('GROUP BY', null);
67+
clauses.set('LIMIT BY', null);
68+
clauses.set('HAVING', null);
69+
clauses.set('LIMIT', null);
70+
clauses.set('OFFSET', null);
71+
clauses.set('UNION', null);
72+
clauses.set('INTERSECT', null);
73+
clauses.set('EXCEPT', null);
74+
clauses.set('INTO OUTFILE', null);
75+
clauses.set('FORMAT', null);
76+
return clauses;
77+
}

src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,8 @@ export interface CHSecureConfig {
2424
tlsClientCert?: string;
2525
tlsClientKey?: string;
2626
}
27+
28+
export enum Format {
29+
TABLE = 1,
30+
TIMESERIES = 2,
31+
}

0 commit comments

Comments
 (0)