Skip to content

Commit

Permalink
multi line time series (#40)
Browse files Browse the repository at this point in the history
* multi line time series
  • Loading branch information
Scott Lepper authored Jan 18, 2022
1 parent faeeccf commit 6282a1b
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 83 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 0.9.7

Feature - Multi-line time series.

## 0.9.6

Bug - Change time template variable names.
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@ The query editor allows you to query ClickHouse to return time series or tabular
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.

#### Multi-line time series

To create multi-line time series, the query must return at least 3 fields.
- field 1: `datetime` field with an alias of `time`
- field 2: value to group by
- field 3+: the metric values

For example:
```sql
SELECT log_time as time, machine_group, avg(disk_free) as avg_disk_free
from mgbench.logs1
group by machine_group, log_time
order by log_time
```

### Query as table

Table visualizations will always be available for any valid ClickHouse query.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "clickhouse-datasource",
"version": "0.9.6",
"version": "0.9.7",
"description": "Clickhouse Datasource",
"scripts": {
"build": "grafana-toolkit plugin:build",
Expand Down
22 changes: 20 additions & 2 deletions src/components/SQLEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,35 @@ import { QueryEditorProps } from '@grafana/data';
import { CodeEditor } from '@grafana/ui';
import { Datasource } from '../data/CHDatasource';
import { registerSQL, Range, Fetcher } from './sqlProvider';
import { CHQuery, CHConfig } from '../types';
import { CHQuery, CHConfig, Format } from '../types';
import { styles } from '../styles';
import { fetchSuggestions as sugg, Schema } from './suggestions';
import { selectors } from 'selectors';
import sqlToAST from '../data/ast';
import { isString } from 'lodash';

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

export const SQLEditor = (props: SQLEditorProps) => {
const { query, onRunQuery, onChange, datasource } = props;

const getFormat = (sql: string): Format => {
// convention to format as time series
// first field as "time" alias and requires at least 2 fields (time and metric)
const ast = sqlToAST(sql);
const select = ast.get('SELECT');
if (isString(select)) {
const fields = select.split(',');
if (fields.length > 1) {
return fields[0].toLowerCase().endsWith('as time') ? Format.TIMESERIES : Format.TABLE;
}
}
return Format.TABLE;
};

const onSqlChange = (sql: string) => {
onChange({ ...query, rawSql: sql, format: 1 });
const format = getFormat(sql);
onChange({ ...query, rawSql: sql, format });
onRunQuery();
};

Expand Down
84 changes: 4 additions & 80 deletions src/data/adHocFilter.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import sqlToAST, { clausesToSql, Clause } from './ast';

export class AdHocFilter {
private _targetTable = '';

Expand All @@ -23,48 +25,9 @@ export class AdHocFilter {
}
// Semicolons are not required and cause problems when building the SQL
sql = sql.replace(';', '');
const ast = this.sqlToAST(sql);
const ast = sqlToAST(sql);
const filteredAST = this.applyFiltersToAST(ast, whereClause);
return this.clausesToSql(filteredAST);
}

private sqlToAST(sql: string): Map<string, Clause> {
const ast = this.createStatement();
const re =
/\b(WITH|SELECT|DISTINCT|FROM|SAMPLE|JOIN|PREWHERE|WHERE|GROUP BY|LIMIT BY|HAVING|LIMIT|OFFSET|UNION|INTERSECT|EXCEPT|INTO OUTFILE|FORMAT)\b/gi;
let bracketCount = 0;
let lastBracketCount = 0;
let lastNode = '';
let bracketPhrase = '';
let regExpArray: RegExpExecArray | null;
while ((regExpArray = re.exec(sql)) !== null) {
// Sets foundNode to a SQL keyword from the regular expression
const foundNode = regExpArray[0].toUpperCase();
const phrase = sql.substring(re.lastIndex, sql.length).split(re)[0];
// If there is a greater number of open brackets than closed,
// add the the bracket phrase that will eventually be added the the last node
if (bracketCount > 0) {
bracketPhrase += foundNode + phrase;
} else {
ast.set(foundNode, phrase);
lastNode = foundNode;
}
bracketCount += (phrase.match(/\(/g) || []).length;
bracketCount -= (phrase.match(/\)/g) || []).length;
if (bracketCount <= 0 && lastBracketCount > 0) {
// The phrase brackets is complete
// If it is a FROM phrase lets make a branch node
// If it is anything else lets make a leaf node
if (lastNode === 'FROM') {
ast.set(lastNode, this.sqlToAST(bracketPhrase));
} else {
const p = (ast.get(lastNode) as string).concat(bracketPhrase);
ast.set(lastNode, p);
}
}
lastBracketCount = bracketCount;
}
return ast;
return clausesToSql(filteredAST);
}

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

private clausesToSql(ast: Map<string, Clause>): string {
let r = '';
ast.forEach((c: Clause, key: string) => {
if (typeof c === 'string') {
r += `${key} ${c.trim()} `;
} else if (c !== null) {
r += `${key} (${this.clausesToSql(c)} `;
}
});
// Remove all of the consecutive spaces to make things more readable when debugging
return r.trim().replace(/\s+/g, ' ');
}

// Returns a table name found in the FROM phrase
// FROM phrases might contain more than just the table name
private getTableName(fromPhrase: string): string {
Expand All @@ -108,30 +58,6 @@ export class AdHocFilter {
.split(' ')[0]
.replace(/(;|\(|\))/g, '');
}

// Creates a statement with all the keywords to preserve the keyword order
private createStatement() {
let clauses = new Map<string, Clause>();
clauses.set('WITH', null);
clauses.set('SELECT', null);
clauses.set('DISTINCT', null);
clauses.set('FROM', null);
clauses.set('SAMPLE', null);
clauses.set('JOIN', null);
clauses.set('PREWHERE', null);
clauses.set('WHERE', null);
clauses.set('GROUP BY', null);
clauses.set('LIMIT BY', null);
clauses.set('HAVING', null);
clauses.set('LIMIT', null);
clauses.set('OFFSET', null);
clauses.set('UNION', null);
clauses.set('INTERSECT', null);
clauses.set('EXCEPT', null);
clauses.set('INTO OUTFILE', null);
clauses.set('FORMAT', null);
return clauses;
}
}

export type AdHocVariableFilter = {
Expand All @@ -140,5 +66,3 @@ export type AdHocVariableFilter = {
value: string;
condition: string;
};

type Clause = string | Map<string, Clause> | null;
77 changes: 77 additions & 0 deletions src/data/ast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
export type Clause = string | Map<string, Clause> | null;

export default function sqlToAST(sql: string): Map<string, Clause> {
const ast = createStatement();
const re =
/\b(WITH|SELECT|DISTINCT|FROM|SAMPLE|JOIN|PREWHERE|WHERE|GROUP BY|LIMIT BY|HAVING|LIMIT|OFFSET|UNION|INTERSECT|EXCEPT|INTO OUTFILE|FORMAT)\b/gi;
let bracketCount = 0;
let lastBracketCount = 0;
let lastNode = '';
let bracketPhrase = '';
let regExpArray: RegExpExecArray | null;
while ((regExpArray = re.exec(sql)) !== null) {
// Sets foundNode to a SQL keyword from the regular expression
const foundNode = regExpArray[0].toUpperCase();
const phrase = sql.substring(re.lastIndex, sql.length).split(re)[0];
// If there is a greater number of open brackets than closed,
// add the the bracket phrase that will eventually be added the the last node
if (bracketCount > 0) {
bracketPhrase += foundNode + phrase;
} else {
ast.set(foundNode, phrase);
lastNode = foundNode;
}
bracketCount += (phrase.match(/\(/g) || []).length;
bracketCount -= (phrase.match(/\)/g) || []).length;
if (bracketCount <= 0 && lastBracketCount > 0) {
// The phrase brackets is complete
// If it is a FROM phrase lets make a branch node
// If it is anything else lets make a leaf node
if (lastNode === 'FROM') {
ast.set(lastNode, sqlToAST(bracketPhrase));
} else {
const p = (ast.get(lastNode) as string).concat(bracketPhrase);
ast.set(lastNode, p);
}
}
lastBracketCount = bracketCount;
}
return ast;
}

export function clausesToSql(ast: Map<string, Clause>): string {
let r = '';
ast.forEach((c: Clause, key: string) => {
if (typeof c === 'string') {
r += `${key} ${c.trim()} `;
} else if (c !== null) {
r += `${key} (${clausesToSql(c)} `;
}
});
// Remove all of the consecutive spaces to make things more readable when debugging
return r.trim().replace(/\s+/g, ' ');
}

// Creates a statement with all the keywords to preserve the keyword order
function createStatement() {
const clauses = new Map<string, Clause>();
clauses.set('WITH', null);
clauses.set('SELECT', null);
clauses.set('DISTINCT', null);
clauses.set('FROM', null);
clauses.set('SAMPLE', null);
clauses.set('JOIN', null);
clauses.set('PREWHERE', null);
clauses.set('WHERE', null);
clauses.set('GROUP BY', null);
clauses.set('LIMIT BY', null);
clauses.set('HAVING', null);
clauses.set('LIMIT', null);
clauses.set('OFFSET', null);
clauses.set('UNION', null);
clauses.set('INTERSECT', null);
clauses.set('EXCEPT', null);
clauses.set('INTO OUTFILE', null);
clauses.set('FORMAT', null);
return clauses;
}
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,8 @@ export interface CHSecureConfig {
tlsClientCert?: string;
tlsClientKey?: string;
}

export enum Format {
TABLE = 1,
TIMESERIES = 2,
}

0 comments on commit 6282a1b

Please sign in to comment.