diff --git a/CHANGELOG.md b/CHANGELOG.md index 760ff62..c8f560d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ # Changelog -## 0.0.3 (Unreleased) +## 0.0.5 (Unreleased) Initial release. diff --git a/go.mod b/go.mod index 10197c8..cc38339 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( github.com/hashicorp/go-hclog v0.14.1 // indirect github.com/hashicorp/go-plugin v1.4.3 // indirect github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect + github.com/jmoiron/sqlx v1.3.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.13.1 // indirect @@ -40,6 +41,7 @@ require ( github.com/mattn/go-colorable v0.1.4 // indirect github.com/mattn/go-isatty v0.0.10 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mattn/go-sqlite3 v1.14.16 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mitchellh/go-testing-interface v1.0.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect diff --git a/go.sum b/go.sum index 1e90621..2e31acc 100644 --- a/go.sum +++ b/go.sum @@ -112,6 +112,7 @@ github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34 github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -199,6 +200,8 @@ github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKe github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= @@ -225,6 +228,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magefile/mage v1.13.0 h1:XtLJl8bcCM7EFoO8FyH8XK3t7G5hQAeK+i4tq+veT9M= github.com/magefile/mage v1.13.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -241,6 +245,9 @@ github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW1 github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= diff --git a/package.json b/package.json index 2a874c5..9967712 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "climeworks-databricks", - "version": "0.0.4", + "version": "0.0.5", "description": "Databricks SQL Connector", "scripts": { "build": "grafana-toolkit plugin:build", diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index 500aaeb..e655a6f 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -11,7 +11,9 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/live" + "math" "regexp" + "strings" "time" ) @@ -93,12 +95,107 @@ type queryModel struct { RawSqlSelected bool `json:"rawSqlSelected"` } +func getIntervalString(duration time.Duration) string { + hours := int(math.Floor(duration.Hours())) + minutes := int(math.Floor(duration.Minutes())) + seconds := int(math.Floor(duration.Seconds())) + + returnString := "" + + deliminator := "" + if hours > 0 { + returnString = fmt.Sprintf("%s%s%d HOURS", returnString, deliminator, hours) + deliminator = " " + } + + remainingMinutes := minutes - (hours * 60) + if remainingMinutes > 0 { + returnString = fmt.Sprintf("%s%s%d MINUTES", returnString, deliminator, remainingMinutes) + deliminator = " " + } + + remainingSeconds := seconds - (minutes * 60) + if remainingSeconds > 0 { + returnString = fmt.Sprintf("%s%s%d SECONDS", returnString, deliminator, remainingSeconds) + deliminator = " " + } + + return returnString +} + +func replaceMacros(sqlQuery string, query backend.DataQuery) string { + + queryString := sqlQuery + log.DefaultLogger.Info("Raw SQL Query selected", "query", queryString) + + interval_string := getIntervalString(query.Interval) + + var rgx = regexp.MustCompile(`\$__timeWindow\(([a-zA-Z0-9_-]+)\)`) + if rgx.MatchString(queryString) { + log.DefaultLogger.Info("__timeWindow placeholder found") + rs := rgx.FindStringSubmatch(queryString) + timeColumnName := rs[1] + queryString = rgx.ReplaceAllString(queryString, fmt.Sprintf("window(%s, '%s')", timeColumnName, interval_string)) + + rgx = regexp.MustCompile(`\$__time\(([a-zA-Z0-9_-]+)\)`) + if rgx.MatchString(queryString) { + log.DefaultLogger.Info("__time placeholder found") + queryString = rgx.ReplaceAllString(queryString, "window.start") + } + + rgx = regexp.MustCompile(`\$__value\(([a-zA-Z0-9_-]+)\)`) + if rgx.MatchString(queryString) { + log.DefaultLogger.Info("__value placeholder found") + rs = rgx.FindStringSubmatch(queryString) + valueColumnName := rs[1] + queryString = rgx.ReplaceAllString(queryString, fmt.Sprintf("avg(%s) AS value", valueColumnName)) + } + } else { + rgx = regexp.MustCompile(`\$__time\(([a-zA-Z0-9_-]+)\)`) + if rgx.MatchString(queryString) { + log.DefaultLogger.Info("__time placeholder found") + rs := rgx.FindStringSubmatch(queryString) + timeColumnName := rs[1] + queryString = rgx.ReplaceAllString(queryString, fmt.Sprintf("%s AS time", timeColumnName)) + } + + rgx = regexp.MustCompile(`\$__value\(([a-zA-Z0-9_-]+)\)`) + if rgx.MatchString(queryString) { + log.DefaultLogger.Info("__value placeholder found") + rs := rgx.FindStringSubmatch(queryString) + valueColumnName := rs[1] + queryString = rgx.ReplaceAllString(queryString, fmt.Sprintf("%s AS value", valueColumnName)) + } + } + + rgx = regexp.MustCompile(`\$__timeFilter\(([a-zA-Z0-9_-]+)\)`) + if rgx.MatchString(queryString) { + rs := rgx.FindStringSubmatch(queryString) + timeColumnName := rs[1] + timeRangeFilter := fmt.Sprintf("%s BETWEEN '%s' AND '%s'", + timeColumnName, + query.TimeRange.From.UTC().Format("2006-01-02 15:04:05"), + query.TimeRange.To.UTC().Format("2006-01-02 15:04:05"), + ) + queryString = rgx.ReplaceAllString(queryString, timeRangeFilter) + } + + queryString = strings.ReplaceAll(queryString, "$__timeFrom", query.TimeRange.From.UTC().Format("2006-01-02 15:04:05")) + + queryString = strings.ReplaceAll(queryString, "$__timeTo", query.TimeRange.To.UTC().Format("2006-01-02 15:04:05")) + + queryString = strings.ReplaceAll(queryString, "$__interval", interval_string) + + return queryString +} + func (d *SampleDatasource) query(_ context.Context, pCtx backend.PluginContext, query backend.DataQuery) backend.DataResponse { response := backend.DataResponse{} // Unmarshal the JSON into our queryModel. var qm queryModel + log.DefaultLogger.Info("Query Ful", "query", query) err := json.Unmarshal(query.JSON, &qm) if err != nil { response.Error = err @@ -111,67 +208,16 @@ func (d *SampleDatasource) query(_ context.Context, pCtx backend.PluginContext, if seconds_interval <= 0 { seconds_interval = 1 } + interval_string := getIntervalString(query.Interval) queryString := "" if qm.RawSqlSelected { - queryString = qm.RawSqlQuery - log.DefaultLogger.Info("Raw SQL Query selected", "query", queryString) - - var rgx = regexp.MustCompile(`\$__timeWindow\(([a-zA-Z0-9_-]+)\)`) - if rgx.MatchString(queryString) { - log.DefaultLogger.Info("__timeWindow placeholder found") - rs := rgx.FindStringSubmatch(queryString) - timeColumnName := rs[1] - queryString = rgx.ReplaceAllString(queryString, fmt.Sprintf("window(%s, '%d SECONDS')", timeColumnName, seconds_interval)) - - rgx = regexp.MustCompile(`\$__time\(([a-zA-Z0-9_-]+)\)`) - if rgx.MatchString(queryString) { - log.DefaultLogger.Info("__time placeholder found") - queryString = rgx.ReplaceAllString(queryString, "window.start") - } - - rgx = regexp.MustCompile(`\$__value\(([a-zA-Z0-9_-]+)\)`) - if rgx.MatchString(queryString) { - log.DefaultLogger.Info("__value placeholder found") - rs = rgx.FindStringSubmatch(queryString) - valueColumnName := rs[1] - queryString = rgx.ReplaceAllString(queryString, fmt.Sprintf("avg(%s) AS value", valueColumnName)) - } - } else { - rgx = regexp.MustCompile(`\$__time\(([a-zA-Z0-9_-]+)\)`) - if rgx.MatchString(queryString) { - log.DefaultLogger.Info("__time placeholder found") - rs := rgx.FindStringSubmatch(queryString) - timeColumnName := rs[1] - queryString = rgx.ReplaceAllString(queryString, fmt.Sprintf("%s AS time", timeColumnName)) - } - - rgx = regexp.MustCompile(`\$__value\(([a-zA-Z0-9_-]+)\)`) - if rgx.MatchString(queryString) { - log.DefaultLogger.Info("__value placeholder found") - rs := rgx.FindStringSubmatch(queryString) - valueColumnName := rs[1] - queryString = rgx.ReplaceAllString(queryString, fmt.Sprintf("%s AS value", valueColumnName)) - } - } - - rgx = regexp.MustCompile(`\$__timeFilter\(([a-zA-Z0-9_-]+)\)`) - if rgx.MatchString(queryString) { - rs := rgx.FindStringSubmatch(queryString) - timeColumnName := rs[1] - timeRangeFilter := fmt.Sprintf("%s BETWEEN '%s' AND '%s'", - timeColumnName, - query.TimeRange.From.UTC().Format("2006-01-02 15:04:05"), - query.TimeRange.To.UTC().Format("2006-01-02 15:04:05"), - ) - queryString = rgx.ReplaceAllString(queryString, timeRangeFilter) - } - + queryString = replaceMacros(qm.RawSqlQuery, query) } else { whereQuery := "" if qm.WhereQuery != "" { whereQuery = fmt.Sprintf(" %s AND", qm.WhereQuery) } - queryString = fmt.Sprintf("SELECT window.start, avg(%s) AS value FROM %s WHERE%s %s BETWEEN '%s' AND '%s' GROUP BY window(%s, '%d SECONDS')", + queryString = fmt.Sprintf("SELECT window.start, avg(%s) AS value FROM %s WHERE%s %s BETWEEN '%s' AND '%s' GROUP BY window(%s, '%s')", qm.ValueColumnName, qm.TableName, whereQuery, @@ -179,7 +225,7 @@ func (d *SampleDatasource) query(_ context.Context, pCtx backend.PluginContext, query.TimeRange.From.UTC().Format("2006-01-02 15:04:05"), query.TimeRange.To.UTC().Format("2006-01-02 15:04:05"), qm.TimeColumnName, - seconds_interval) + interval_string) } log.DefaultLogger.Info("Query", "query", queryString) diff --git a/src/QueryEditor.tsx b/src/QueryEditor.tsx index 7adee86..6556cce 100644 --- a/src/QueryEditor.tsx +++ b/src/QueryEditor.tsx @@ -1,29 +1,26 @@ import { defaults } from 'lodash'; -import React, {FormEvent, useState} from 'react'; -import { AutoSizeInput, InlineFieldRow, InlineField, useTheme, InlineSwitch } from '@grafana/ui'; +import React, {FormEvent } from 'react'; +import { AutoSizeInput, InlineFieldRow, InlineField, InlineSwitch, CodeEditor } from '@grafana/ui'; import { QueryEditorProps } from '@grafana/data'; import { DataSource } from './datasource'; -import Editor from "@monaco-editor/react"; import { defaultQuery, MyDataSourceOptions, MyQuery } from './types'; -import * as monaco from "monaco-editor"; type Props = QueryEditorProps; export function QueryEditor(props: Props) { - const [, setQueryText] = useState("") - const [sqlEditorSelected, setSqlEditorSelected] = useState(false) - const onQuerryChange = (value: string | undefined, ev: monaco.editor.IModelContentChangedEvent) => { - setQueryText(value || "") + const query = defaults(props.query, defaultQuery); + const { timeColumnName, valueColumnName, whereQuery, tableName, rawSqlQuery, rawSqlSelected } = query; + + const onSQLQueryChange = (value: string) => { const { onChange, query } = props; onChange({ ...query, rawSqlQuery: value }); }; const onSQLSwitchChange = (event: any) => { const { onChange, query } = props; - onChange({ ...query, rawSqlSelected: !sqlEditorSelected }); - setSqlEditorSelected(!sqlEditorSelected); + onChange({ ...query, rawSqlSelected: !rawSqlSelected }); }; const ontableNameChange = (event: FormEvent) => { console.log(event); @@ -52,19 +49,22 @@ export function QueryEditor(props: Props) { onChange({ ...query, whereQuery: event.currentTarget.value }); }; - const query = defaults(props.query, defaultQuery); - const { timeColumnName, valueColumnName, whereQuery, tableName } = query; - const theme = useTheme() + return (
- {sqlEditorSelected ? ( - + + width="100%" + onBlur={onSQLQueryChange} + onSave={onSQLQueryChange} + showMiniMap={false} + showLineNumbers={false} + /> +
) : ( <> @@ -115,7 +115,7 @@ export function QueryEditor(props: Props) { diff --git a/src/QueryEditorHelp.tsx b/src/QueryEditorHelp.tsx new file mode 100644 index 0000000..f490c5e --- /dev/null +++ b/src/QueryEditorHelp.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { QueryEditorHelpProps} from '@grafana/data'; +import { MyQuery } from './types'; + +const examples = [ + { + title: 'Time Range & Windowing', + expression: "SELECT $__time(time_column), avg(value_column) FROM catalog.default.table_name WHERE $__timeFilter(time_column) GROUP BY $__timeWindow(time_column)", + label: 'Inserts Time range filters and window aggregation into the query. The resulting query with macros replaced would look like this:', + resultingQuery: "SELECT window.start, avg(value_column) FROM catalog.default.table_name WHERE time_column BETWEEN '2021-12-31 23:00:00' AND '2022-01-01 22:59:59' GROUP BY window(time_column, '2 HOURS')", + rawSqlQuery: "SELECT $__time(time_column), avg(value_column) FROM catalog.default.table_name WHERE $__timeFilter(time_column) GROUP BY $__timeWindow(time_column)", + rawSqlSelected: true, + }, + { + title: 'Time From/To', + expression: 'SELECT time_column, value_column FROM catalog.default.table_name WHERE time_column BETWEEN $__timeFrom AND $__timeTo', + label: 'Insert Time range filters from and to time values.', + rawSqlQuery: "SELECT time_column, value_column FROM catalog.default.table_name WHERE time_column BETWEEN $__timeFrom AND $__timeTo", + resultingQuery: "SELECT time_column, value_column FROM catalog.default.table_name WHERE time_column BETWEEN '2021-12-31 23:00:00' AND '2022-01-01 22:59:59'", + rawSqlSelected: true, + }, +]; + + +type Props = QueryEditorHelpProps; +export function QueryEditorHelp(props: Props) { + return ( +
+

SQL Query Documentation

+

Macros

+ {examples.map((item, index) => ( +
+
{item.title}
+ {item.expression ? ( +
props.onClickExample({ rawSqlQuery: item.rawSqlQuery, rawSqlSelected: item.rawSqlSelected } as MyQuery)} + > + {item.expression} +
+ ) : null} +
{item.label}
+ {item.resultingQuery && ({item.resultingQuery})} +
+ ))} +
+ ); +}; diff --git a/src/VariableQueryEditor.tsx b/src/VariableQueryEditor.tsx deleted file mode 100644 index 2bbf6e3..0000000 --- a/src/VariableQueryEditor.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React, { useState } from 'react'; -import { MyVariableQuery } from './types'; - -interface VariableQueryProps { - query: MyVariableQuery; - onChange: (query: MyVariableQuery, definition: string) => void; -} - -export const VariableQueryEditor: React.FC = ({ onChange, query }) => { - const [state, setState] = useState(query); - - const saveQuery = () => { - onChange(state, `${state.rawQuery} (${state.namespace})`); - }; - - const handleChange = (event: React.FormEvent) => - setState({ - ...state, - [event.currentTarget.name]: event.currentTarget.value, - }); - - return ( - <> -
- Namespace - -
-
- Query - -
- - ); -}; diff --git a/src/module.ts b/src/module.ts index 4ac674c..2bb2988 100644 --- a/src/module.ts +++ b/src/module.ts @@ -2,10 +2,10 @@ import { DataSourcePlugin } from '@grafana/data'; import { DataSource } from './datasource'; import { ConfigEditor } from './ConfigEditor'; import { QueryEditor } from './QueryEditor'; +import { QueryEditorHelp } from './QueryEditorHelp'; import { MyQuery, MyDataSourceOptions } from './types'; -import { VariableQueryEditor } from './VariableQueryEditor'; export const plugin = new DataSourcePlugin(DataSource) .setConfigEditor(ConfigEditor) - .setVariableQueryEditor(VariableQueryEditor) - .setQueryEditor(QueryEditor); + .setQueryEditor(QueryEditor) + .setQueryEditorHelp(QueryEditorHelp);