From 60677d1bdbed228ddb4fb3b9df3b43999fd4f82d Mon Sep 17 00:00:00 2001 From: Florian Bernd Date: Mon, 15 May 2023 11:51:21 +0200 Subject: [PATCH] Add basic auto-completion support (#4) --- go.mod | 7 +- go.sum | 6 +- package.json | 3 +- pkg/plugin/api.go | 65 ++++ pkg/plugin/datasource.go | 33 ++ src/components/QueryEditor.tsx | 94 +++-- src/sneller_sql.ts | 641 +++++++++++++++++++++++++++++++++ yarn.lock | 13 + 8 files changed, 816 insertions(+), 46 deletions(-) create mode 100644 src/sneller_sql.ts diff --git a/go.mod b/go.mod index f60d7b1..64f224e 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,16 @@ module github.com/sneller-inc/sneller go 1.19 require ( + github.com/SnellerInc/sneller v0.0.0-20230505151417-5806cd3a42c7 github.com/grafana/grafana-plugin-sdk-go v0.159.0 + github.com/patrickmn/go-cache v2.1.0+incompatible go.opentelemetry.io/otel v1.14.0 go.opentelemetry.io/otel/trace v1.14.0 + golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 ) require ( github.com/BurntSushi/toml v1.2.1 // indirect - github.com/SnellerInc/sneller v0.0.0-20230505151417-5806cd3a42c7 // indirect github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.2.0 // indirect @@ -18,7 +20,6 @@ require ( github.com/cheekybits/genny v1.0.0 // indirect github.com/chromedp/cdproto v0.0.0-20220208224320-6efb837e6bc2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect - github.com/dchest/siphash v1.2.3 // indirect github.com/elazarl/goproxy v0.0.0-20220115173737-adb46da277ac // indirect github.com/fatih/color v1.7.0 // indirect github.com/getkin/kin-openapi v0.112.0 // indirect @@ -73,8 +74,6 @@ require ( go.opentelemetry.io/otel/metric v0.37.0 // indirect go.opentelemetry.io/otel/sdk v1.14.0 // indirect go.opentelemetry.io/proto/otlp v0.19.0 // indirect - golang.org/x/crypto v0.8.0 // indirect - golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect golang.org/x/net v0.9.0 // indirect golang.org/x/sys v0.7.0 // indirect golang.org/x/text v0.9.0 // indirect diff --git a/go.sum b/go.sum index 49346d7..6547609 100644 --- a/go.sum +++ b/go.sum @@ -43,8 +43,6 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE github.com/SnellerInc/sneller v0.0.0-20230505151417-5806cd3a42c7 h1:3GAYAlEQoiW3q8x88cbi140EM1p3g8vBHxqWS75Oro8= github.com/SnellerInc/sneller v0.0.0-20230505151417-5806cd3a42c7/go.mod h1:BaisUxgu4MI9tL6dwYPccGKsXkBP7+BnMz8fL/F/TNk= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= -github.com/amazon-ion/ion-go v1.2.0 h1:EgFy23/7gRxRYdUkJARh/7eZc8BYkFFDZZSqB3PwVqQ= -github.com/amazon-ion/ion-go v1.2.0/go.mod h1:3ZEje8i20TiIPVZlN+KE3B2ppZ1B8d9F/KaT7Dtec+k= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 h1:q4dksr6ICHXqG5hm0ZW5IHyeEJXoIJSOZeBLmWPNeIQ= github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= @@ -83,7 +81,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= -github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= github.com/elazarl/goproxy v0.0.0-20220115173737-adb46da277ac h1:XDAn206aIqKPdF5YczuuJXSQPx+WOen0Pxbxp5Fq8Pg= github.com/elazarl/goproxy v0.0.0-20220115173737-adb46da277ac/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= @@ -274,6 +271,8 @@ github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pierrec/lz4/v4 v4.1.8 h1:ieHkV+i2BRzngO4Wd/3HGowuZStgq6QkPsD1eolNAO4= @@ -372,7 +371,6 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= -golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= diff --git a/package.json b/package.json index ddbf215..f2d88b9 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,8 @@ "sass-loader": "13.2.0", "style-loader": "3.3.1", "swc-loader": "^0.2.3", - "tsconfig-paths": "^4.1.0", "ts-node": "^10.5.0", + "tsconfig-paths": "^4.1.0", "typescript": "^4.4.0", "webpack": "^5.69.1", "webpack-cli": "^4.9.2", @@ -59,6 +59,7 @@ "dependencies": { "@emotion/css": "^11.1.3", "@grafana/data": "9.3.8", + "@grafana/experimental": "^1.1.0", "@grafana/runtime": "9.3.8", "@grafana/ui": "9.3.8", "react": "17.0.2", diff --git a/pkg/plugin/api.go b/pkg/plugin/api.go index 6e62627..b546ff0 100644 --- a/pkg/plugin/api.go +++ b/pkg/plugin/api.go @@ -8,8 +8,11 @@ import ( "io" "net/http" "strings" + "time" + "github.com/SnellerInc/sneller/ion" "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "golang.org/x/exp/maps" ) // executeQuery executes a Sneller query and returns the HTTP response. @@ -21,6 +24,12 @@ func (d *Datasource) executeQuery(ctx context.Context, database, sql string) (*h // getDatabases returns a list of database names. func (d *Datasource) getDatabases(ctx context.Context) ([]string, int, error) { + key := "databases" + cached, found := d.cache.Get(key) + if found { + return cached.([]string), 0, nil + } + resp, err := d.executeRequest(ctx, http.MethodGet, "/databases", nil, map[string]string{"Accept": "application/json"}, nil) @@ -48,11 +57,19 @@ func (d *Datasource) getDatabases(ctx context.Context) ([]string, int, error) { return t.Name }) + d.cache.Set(key, names, time.Minute*1) + return names, 0, nil } // getTables returns a list of table names for the given database. func (d *Datasource) getTables(ctx context.Context, database string) ([]string, int, error) { + key := fmt.Sprintf("tables_%s", database) + cached, found := d.cache.Get(key) + if found { + return cached.([]string), 0, nil + } + resp, err := d.executeRequest(ctx, http.MethodGet, "/tables", nil, map[string]string{"Accept": "application/json"}, map[string]string{"database": database}) @@ -76,9 +93,57 @@ func (d *Datasource) getTables(ctx context.Context, database string) ([]string, return nil, 500, err } + d.cache.Set(key, result, time.Minute*1) + return result, 0, nil } +// getColumns returns a list of column names for the given database and table. +func (d *Datasource) getColumns(ctx context.Context, database, table string) ([]string, int, error) { + key := fmt.Sprintf("columns_%s_%s", database, table) + cached, found := d.cache.Get(key) + if found { + return cached.([]string), 0, nil + } + + resp, err := d.executeQuery(ctx, database, fmt.Sprintf(`SELECT SNELLER_DATASHAPE(*) FROM (SELECT * FROM %q LIMIT 1000)`, table)) + if err != nil { + if resp != nil { + return nil, resp.StatusCode, err + } + return nil, 500, err + } + + defer func() { + if err := resp.Body.Close(); err != nil { + log.DefaultLogger.Error("failed to close response body", "err", err) + } + }() + + payload := map[string]any{} + + err = ion.NewDecoder(resp.Body, 1024*1024*10).Decode(&payload) + if err != nil { + return nil, 500, err + } + + fields, ok := payload["fields"] + if !ok { + return []string{}, 0, nil + } + + vals, ok := fields.(map[string]any) + if !ok { + return []string{}, 0, nil + } + + cols := maps.Keys(vals) + + d.cache.Set(key, cols, time.Minute*1) + + return cols, 0, nil +} + // newRequest creates a new HTTP request and initializes the 'Authentication' header from the // configured Sneller authentication token in the 'Authentication' header. func (d *Datasource) newRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) { diff --git a/pkg/plugin/datasource.go b/pkg/plugin/datasource.go index 4f1405f..68c8969 100644 --- a/pkg/plugin/datasource.go +++ b/pkg/plugin/datasource.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/backend/tracing" "github.com/grafana/grafana-plugin-sdk-go/data" + cache "github.com/patrickmn/go-cache" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) @@ -58,6 +59,7 @@ func NewDatasource(settings backend.DataSourceInstanceSettings) (instancemgmt.In settings: settings, endpoint: jsonData.Endpoint, client: client, + cache: cache.New(5*time.Minute, 5*time.Minute), } mux := datasource.NewQueryTypeMux() @@ -76,6 +78,7 @@ type Datasource struct { handler backend.QueryDataHandler endpoint string client *http.Client + cache *cache.Cache } // Dispose here tells plugin SDK that plugin wants to clean up resources when a new instance @@ -154,6 +157,13 @@ func (d *Datasource) CallResource(ctx context.Context, req *backend.CallResource }) } return sender.Send(d.handleCallResourceTables(ctx, segments[1])) + case "columns": + if len(segments) != 3 { + return sender.Send(&backend.CallResourceResponse{ + Status: http.StatusBadRequest, + }) + } + return sender.Send(d.handleCallResourceColumns(ctx, segments[1], segments[2])) default: return sender.Send(&backend.CallResourceResponse{ Status: http.StatusNotFound, @@ -203,6 +213,27 @@ func (d *Datasource) handleCallResourceTables(ctx context.Context, database stri } } +func (d *Datasource) handleCallResourceColumns(ctx context.Context, database, table string) *backend.CallResourceResponse { + databases, status, err := d.getColumns(ctx, database, table) + if err != nil { + return &backend.CallResourceResponse{ + Status: status, + Body: []byte(err.Error()), + } + } + result, err := json.Marshal(databases) + if err != nil { + return &backend.CallResourceResponse{ + Status: status, + Body: []byte(err.Error()), + } + } + return &backend.CallResourceResponse{ + Status: http.StatusOK, + Body: result, + } +} + func (d *Datasource) handleQuery(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { response := backend.NewQueryDataResponse() @@ -274,6 +305,8 @@ func (d *Datasource) query(ctx context.Context, _ backend.PluginContext, query b switch resp.StatusCode { case http.StatusUnauthorized: return backend.ErrDataResponse(backend.StatusUnauthorized, fmt.Sprintf("unauthorized: %s", err)) + case http.StatusForbidden: + return backend.ErrDataResponse(backend.StatusForbidden, fmt.Sprintf("forbidden: %s", err)) case http.StatusBadRequest: return backend.ErrDataResponse(backend.StatusValidationFailed, fmt.Sprintf("bad request: %s", err)) } diff --git a/src/components/QueryEditor.tsx b/src/components/QueryEditor.tsx index 9831658..1088c2f 100644 --- a/src/components/QueryEditor.tsx +++ b/src/components/QueryEditor.tsx @@ -1,31 +1,29 @@ -import React, { useState } from 'react'; -import { InlineField, AsyncSelect, CodeEditor, ActionMeta } from '@grafana/ui'; +import React, {useRef, useState} from 'react'; +import {InlineField, AsyncSelect, ActionMeta, monacoTypes, Monaco} from '@grafana/ui'; import { QueryEditorProps, SelectableValue } from '@grafana/data'; import { DataSource } from '../datasource'; import { SnellerDataSourceOptions, SnellerQuery } from '../types'; +import { getStandardSQLCompletionProvider, LanguageDefinition, SQLEditor, SQLMonarchLanguage } from '@grafana/experimental'; +import { language, conf } from "../sneller_sql"; +import { TableIdentifier } from "@grafana/experimental/dist/sql-editor/types"; type Props = QueryEditorProps; export function QueryEditor({ datasource, query, onChange, onRunQuery }: Props) { - const [database, setDatabase] = useState(query.database || ""); - const [table, setTable] = useState(""); + + const [database, setDatabase] = useState(query.database) + const databaseRef = useRef(database) const onDatabaseChange = (value: SelectableValue, actionMeta: ActionMeta) => { onChange({ ...query, database: value?.value }); - setDatabase(value?.value || "") - setTable("") - }; - - const onTableChange = (value: SelectableValue, actionMeta: ActionMeta) => { - setTable(value?.value || "") + setDatabase(value?.value) + databaseRef.current = value?.value }; - const onSqlChange = (sql: string) => { - onChange({ ...query, sql }); + const onQueryChange = (q: string, processQuery: boolean) => { + onChange({ ...query, sql: q }); }; - const { sql } = query; - const loadDatabaseOptions = async () => { try { const response = await datasource.getResource('databases') as string[]; @@ -38,21 +36,56 @@ export function QueryEditor({ datasource, query, onChange, onRunQuery }: Props) } }; - const loadTableOptions = async () => { + const loadTableOptions = async (database?: string) => { if (!database) { return [] } try { - const response = await datasource.getResource('tables/' + database) as string[]; + const response = await datasource.getResource('tables/' + encodeURIComponent(database)) as string[]; return (response.map((x) => ({ - label: x, - value: x, + name: x, }))); } catch { return [] } }; + const loadColumnOptions = async (table: TableIdentifier) => { + if (!database || !table || !table.table) { + return [] + } + try { + const response = await datasource.getResource( + 'columns/' + encodeURIComponent(database) + '/' + encodeURIComponent(table.table) + ) as string[]; + return (response.map((x) => ({ + name: x, + }))); + } catch { + return [] + } + }; + + const snellerLanguageDefinition: LanguageDefinition = { + id: 'sneller_sql', + loader: () => new Promise<{ + language: SQLMonarchLanguage; + conf: monacoTypes.languages.LanguageConfiguration; + }>((resolve) => resolve({ language: language, conf: conf })), + completionProvider: (m: Monaco, language?: SQLMonarchLanguage) => { + let provider = getStandardSQLCompletionProvider(m, language!) + provider.tables = { + resolve: async () => { + return loadTableOptions(databaseRef.current) + } + }; + provider.columns = { + resolve: loadColumnOptions + }; + return provider; + } + } + return (
@@ -61,29 +94,16 @@ export function QueryEditor({ datasource, query, onChange, onRunQuery }: Props) cacheOptions defaultOptions onChange={onDatabaseChange} - value={{ label: database, value: database }} - isClearable - isSearchable={false} - /> - - - -
); diff --git a/src/sneller_sql.ts b/src/sneller_sql.ts new file mode 100644 index 0000000..c8ff456 --- /dev/null +++ b/src/sneller_sql.ts @@ -0,0 +1,641 @@ +import { monacoTypes } from '@grafana/ui'; +import { SQLMonarchLanguage } from '@grafana/experimental'; + +export const conf: monacoTypes.languages.LanguageConfiguration = { + comments: { + lineComment: '--', + blockComment: ['/*', '*/'], + }, + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ], + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: "'", close: "'" }, + ], + surroundingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: "'", close: "'" }, + ], +}; + +// based on https://github.com/microsoft/monaco-editor/blob/main/src/basic-languages/sql/sql.ts +export const language: SQLMonarchLanguage = { + defaultToken: '', + tokenPostfix: '.sql', + ignoreCase: true, + + brackets: [ + { open: '[', close: ']', token: 'delimiter.square' }, + { open: '(', close: ')', token: 'delimiter.parenthesis' }, + ], + + keywords: [ + 'ABORT', + 'ABSOLUTE', + 'ACTION', + 'ADA', + 'ADD', + 'AFTER', + 'ALL', + 'ALLOCATE', + 'ALTER', + 'ALWAYS', + 'ANALYZE', + 'AND', + 'ANY', + 'ARE', + 'AS', + 'ASC', + 'ASSERTION', + 'AT', + 'ATTACH', + 'AUTHORIZATION', + 'AUTOINCREMENT', + 'AVG', + 'BACKUP', + 'BEFORE', + 'BEGIN', + 'BETWEEN', + 'BIT', + 'BIT_LENGTH', + 'BOTH', + 'BREAK', + 'BROWSE', + 'BULK', + 'BY', + 'CASCADE', + 'CASCADED', + 'CASE', + 'CAST', + 'CATALOG', + 'CHAR', + 'CHARACTER', + 'CHARACTER_LENGTH', + 'CHAR_LENGTH', + 'CHECK', + 'CHECKPOINT', + 'CLOSE', + 'CLUSTERED', + 'COALESCE', + 'COLLATE', + 'COLLATION', + 'COLUMN', + 'COMMIT', + 'COMPUTE', + 'CONFLICT', + 'CONNECT', + 'CONNECTION', + 'CONSTRAINT', + 'CONSTRAINTS', + 'CONTAINS', + 'CONTAINSTABLE', + 'CONTINUE', + 'CONVERT', + 'CORRESPONDING', + 'COUNT', + 'CREATE', + 'CROSS', + 'CURRENT', + 'CURRENT_DATE', + 'CURRENT_TIME', + 'CURRENT_TIMESTAMP', + 'CURRENT_USER', + 'CURSOR', + 'DATABASE', + 'DATE', + 'DAY', + 'DBCC', + 'DEALLOCATE', + 'DEC', + 'DECIMAL', + 'DECLARE', + 'DEFAULT', + 'DEFERRABLE', + 'DEFERRED', + 'DELETE', + 'DENY', + 'DESC', + 'DESCRIBE', + 'DESCRIPTOR', + 'DETACH', + 'DIAGNOSTICS', + 'DISCONNECT', + 'DISK', + 'DISTINCT', + 'DISTRIBUTED', + 'DO', + 'DOMAIN', + 'DOUBLE', + 'DROP', + 'DUMP', + 'EACH', + 'ELSE', + 'END', + 'END-EXEC', + 'ERRLVL', + 'ESCAPE', + 'EXCEPT', + 'EXCEPTION', + 'EXCLUDE', + 'EXCLUSIVE', + 'EXEC', + 'EXECUTE', + 'EXISTS', + 'EXIT', + 'EXPLAIN', + 'EXTERNAL', + 'EXTRACT', + 'FAIL', + 'FALSE', + 'FETCH', + 'FILE', + 'FILLFACTOR', + 'FILTER', + 'FIRST', + 'FLOAT', + 'FOLLOWING', + 'FOR', + 'FOREIGN', + 'FORTRAN', + 'FOUND', + 'FREETEXT', + 'FREETEXTTABLE', + 'FROM', + 'FULL', + 'FUNCTION', + 'GENERATED', + 'GET', + 'GLOB', + 'GLOBAL', + 'GO', + 'GOTO', + 'GRANT', + 'GROUP', + 'GROUPS', + 'HAVING', + 'HOLDLOCK', + 'HOUR', + 'IDENTITY', + 'IDENTITYCOL', + 'IDENTITY_INSERT', + 'IF', + 'IGNORE', + 'ILIKE', + 'IMMEDIATE', + 'IN', + 'INCLUDE', + 'INDEX', + 'INDEXED', + 'INDICATOR', + 'INITIALLY', + 'INNER', + 'INPUT', + 'INSENSITIVE', + 'INSERT', + 'INSTEAD', + 'INT', + 'INTEGER', + 'INTERSECT', + 'INTERVAL', + 'INTO', + 'IS', + 'ISNULL', + 'ISOLATION', + 'JOIN', + 'KEY', + 'KILL', + 'LANGUAGE', + 'LAST', + 'LEADING', + 'LEFT', + 'LEVEL', + 'LIKE', + 'LIMIT', + 'LINENO', + 'LOAD', + 'LOCAL', + 'LOWER', + 'MATCH', + 'MATERIALIZED', + 'MAX', + 'MERGE', + 'MIN', + 'MINUTE', + 'MODULE', + 'MONTH', + 'NAMES', + 'NATIONAL', + 'NATURAL', + 'NCHAR', + 'NEXT', + 'NO', + 'NOCHECK', + 'NONCLUSTERED', + 'NONE', + 'NOT', + 'NOTHING', + 'NOTNULL', + 'NULL', + 'NULLIF', + 'NULLS', + 'NUMERIC', + 'OCTET_LENGTH', + 'OF', + 'OFF', + 'OFFSET', + 'OFFSETS', + 'ON', + 'ONLY', + 'OPEN', + 'OPENDATASOURCE', + 'OPENQUERY', + 'OPENROWSET', + 'OPENXML', + 'OPTION', + 'OR', + 'ORDER', + 'OTHERS', + 'OUTER', + 'OUTPUT', + 'OVER', + 'OVERLAPS', + 'PAD', + 'PARTIAL', + 'PARTITION', + 'PASCAL', + 'PERCENT', + 'PIVOT', + 'PLAN', + 'POSITION', + 'PRAGMA', + 'PRECEDING', + 'PRECISION', + 'PREPARE', + 'PRESERVE', + 'PRIMARY', + 'PRINT', + 'PRIOR', + 'PRIVILEGES', + 'PROC', + 'PROCEDURE', + 'PUBLIC', + 'QUERY', + 'RAISE', + 'RAISERROR', + 'RANGE', + 'READ', + 'READTEXT', + 'REAL', + 'RECONFIGURE', + 'RECURSIVE', + 'REFERENCES', + 'REGEXP', + 'REINDEX', + 'RELATIVE', + 'RELEASE', + 'RENAME', + 'REPLACE', + 'REPLICATION', + 'RESTORE', + 'RESTRICT', + 'RETURN', + 'RETURNING', + 'REVERT', + 'REVOKE', + 'RIGHT', + 'ROLLBACK', + 'ROW', + 'ROWCOUNT', + 'ROWGUIDCOL', + 'ROWS', + 'RULE', + 'SAVE', + 'SAVEPOINT', + 'SCHEMA', + 'SCROLL', + 'SECOND', + 'SECTION', + 'SECURITYAUDIT', + 'SELECT', + 'SEMANTICKEYPHRASETABLE', + 'SEMANTICSIMILARITYDETAILSTABLE', + 'SEMANTICSIMILARITYTABLE', + 'SESSION', + 'SESSION_USER', + 'SET', + 'SETUSER', + 'SHUTDOWN', + 'SIMILAR TO', + 'SIZE', + 'SMALLINT', + 'SOME', + 'SPACE', + 'SQL', + 'SQLCA', + 'SQLCODE', + 'SQLERROR', + 'SQLSTATE', + 'SQLWARNING', + 'STATISTICS', + 'SUBSTRING', + 'SUM', + 'SYSTEM_USER', + 'TABLE', + 'TABLESAMPLE', + 'TEMP', + 'TEMPORARY', + 'TEXTSIZE', + 'THEN', + 'TIES', + 'TIME', + 'TIMESTAMP', + 'TIMEZONE_HOUR', + 'TIMEZONE_MINUTE', + 'TO', + 'TOP', + 'TRAILING', + 'TRAN', + 'TRANSACTION', + 'TRANSLATE', + 'TRANSLATION', + 'TRIGGER', + 'TRIM', + 'TRUE', + 'TRUNCATE', + 'TRY_CONVERT', + 'TSEQUAL', + 'UNBOUNDED', + 'UNION', + 'UNIQUE', + 'UNKNOWN', + 'UNPIVOT', + 'UPDATE', + 'UPDATETEXT', + 'UPPER', + 'USAGE', + 'USE', + 'USER', + 'USING', + 'VACUUM', + 'VALUE', + 'VALUES', + 'VARCHAR', + 'VARYING', + 'VIEW', + 'VIRTUAL', + 'WAITFOR', + 'WHEN', + 'WHENEVER', + 'WHERE', + 'WHILE', + 'WINDOW', + 'WITH', + 'WITHIN GROUP', + 'WITHOUT', + 'WORK', + 'WRITE', + 'WRITETEXT', + 'YEAR', + 'ZONE', + ], + operators: [ + // Set + 'EXCEPT', + 'INTERSECT', + 'UNION', + // Join + 'CROSS', + 'INNER', + 'JOIN', + // Predicates + 'CONTAINS', + 'IS', + 'NULL', + // Pivoting + 'PIVOT', + 'UNPIVOT', + // Merging + 'MATCHED', + ], + logicalOperators: ['ALL', 'AND', 'ANY', 'BETWEEN', 'EXISTS', 'IN', 'LIKE', 'NOT', 'OR', 'SOME'], + comparisonOperators: ['<>', '>', '<', '>=', '<=', '=', '!=', '&', '~', '^', '%'], + + builtinFunctions: [ + // Aggregations + 'COUNT', + 'MIN', + 'MAX', + 'EARLIEST', + 'LATEST', + 'SUM', + 'AVG', + 'VARIANCE', + 'VARIANCE_POP', + 'STDDEV', + 'STDDEV_POP', + 'BIT_AND', + 'BIT_OR', + 'BIT_XOR', + 'BOOL_AND', + 'EVERY', + 'BOOL_OR', + 'APPROX_COUNT_DISTINCT', + 'ROW_NUMBER', + 'RANK', + 'DENSE_RANK', + 'SNELLER_DATASHAPE', + // Bit Manipulation + 'BIT_COUNT', + // Math Functions + 'ABS', + 'CBRT', + 'EXP', + 'EXPM1', + 'EXP2', + 'EXP10', + 'HYPOT', + 'LN', + 'LN1P', + 'LOG', + 'LOG2', + 'LOG10', + 'POW', + 'POWER', + 'SIGN', + 'SQRT', + // Trigonometric Functions + 'DEGREES', + 'RADIANS', + 'SIN', + 'COS', + 'TAN', + 'ASIN', + 'ACOS', + 'ATAN', + 'ATAN2', + // Rounding Functions + 'ROUND', + 'ROUND_EVEN', + 'TRUNC', + 'FLOOR', + 'CEIL', + 'CEILING', + // GEO Functions + 'GEO_DISTANCE', + 'GEO_HASH', + 'GEO_TILE_X', + 'GEO_TILE_Y', + 'GEO_TILE_ES', + // Built-in Functions + 'DATE_ADD', + 'DATE_BIN', + 'DATE_DIFF', + 'DATE_TRUNC', + 'EXTRACT', + 'UTCNOW', + 'LEAST', + 'GREATEST', + 'WIDTH_BUCKET', + 'TIME_BUCKET', + 'TO_UNIX_EPOCH', + 'TO_UNIX_MICRO', + 'TRIM', + 'LTRIM', + 'RTRIM', + 'SIZE', + 'ARRAY_SIZE', + 'ARRAY_CONTAINS', + 'ARRAY_POSITION', + 'OCTET_LENGTH', + 'CHAR_LENGTH', + 'CHARACTER_LENGTH', + 'LOWER', + 'UPPER', + 'EQUALS_CI', + 'SUBSTRING', + 'SLIT_PART', + 'IS_SUBNET_OF', + 'EQUALS_FUZZY', + 'EQUALS_FUZZY_UNICODE', + 'CONTAINS_FUZZY', + 'CONTAINS_FUZZY_UNICODE', + 'CAST', + 'TYPE_BIT', + 'TABLE_GLOB', + 'TABLE_PATTERN' + ], + builtinVariables: [ + // empty + ], + pseudoColumns: [ + // empty + ], + tokenizer: { + root: [ + { include: '@templateVariables' }, + { include: '@macros' }, + { include: '@comments' }, + { include: '@whitespace' }, + { include: '@pseudoColumns' }, + { include: '@numbers' }, + { include: '@strings' }, + { include: '@complexIdentifiers' }, + { include: '@scopes' }, + { include: '@schemaTable' }, + [/[;,.]/, 'delimiter'], + [/[()]/, '@brackets'], + [ + /[\w@#$|<|>|=|!|%|&|+|\|-|*|/|~|^]+/, + { + cases: { + '@operators': 'operator', + '@comparisonOperators': 'operator', + '@logicalOperators': 'operator', + '@builtinVariables': 'predefined', + '@builtinFunctions': 'predefined', + '@keywords': 'keyword', + '@default': 'identifier', + }, + }, + ], + ], + templateVariables: [[/\$[a-zA-Z0-9]+/, 'variable']], + macros: [[/\$__[a-zA-Z0-9-_]+/, 'type']], + schemaTable: [ + [/(\w+)\./, 'identifier'], + [/(\w+\.\w+)/, 'identifier'], + ], + whitespace: [[/\s+/, 'white']], + comments: [ + [/--+.*/, 'comment'], + [/\/\*/, { token: 'comment.quote', next: '@comment' }], + ], + comment: [ + [/[^*/]+/, 'comment'], + // Not supporting nested comments, as nested comments seem to not be standard? + // i.e. http://stackoverflow.com/questions/728172/are-there-multiline-comment-delimiters-in-sql-that-are-vendor-agnostic + // [/\/\*/, { token: 'comment.quote', next: '@push' }], // nested comment not allowed :-( + [/\*\//, { token: 'comment.quote', next: '@pop' }], + [/./, 'comment'], + ], + pseudoColumns: [ + [ + /[$][A-Za-z_][\w@#$]*/, + { + cases: { + '@pseudoColumns': 'predefined', + '@default': 'identifier', + }, + }, + ], + ], + numbers: [ + [/0[xX][0-9a-fA-F]*/, 'number'], + [/[$][+-]*\d*(\.\d*)?/, 'number'], + [/((\d+(\.\d*)?)|(\.\d+))([eE][\-+]?\d+)?/, 'number'], + ], + strings: [ + [/N'/, { token: 'string', next: '@string' }], + [/'/, { token: 'string', next: '@string' }], + ], + string: [ + [/[^']+/, 'string'], + [/''/, 'string'], + [/'/, { token: 'string', next: '@pop' }], + ], + complexIdentifiers: [ + [/\[/, { token: 'identifier.quote', next: '@bracketedIdentifier' }], + [/"/, { token: 'identifier.quote', next: '@quotedIdentifier' }], + ], + bracketedIdentifier: [ + [/[^\]]+/, 'identifier'], + [/]]/, 'identifier'], + [/]/, { token: 'identifier.quote', next: '@pop' }], + ], + quotedIdentifier: [ + [/[^"]+/, 'identifier'], + [/""/, 'identifier'], + [/"/, { token: 'identifier.quote', next: '@pop' }], + ], + scopes: [ + [/BEGIN\s+(DISTRIBUTED\s+)?TRAN(SACTION)?\b/i, 'keyword'], + [/BEGIN\s+TRY\b/i, { token: 'keyword.try' }], + [/END\s+TRY\b/i, { token: 'keyword.try' }], + [/BEGIN\s+CATCH\b/i, { token: 'keyword.catch' }], + [/END\s+CATCH\b/i, { token: 'keyword.catch' }], + [/(BEGIN|CASE)\b/i, { token: 'keyword.block' }], + [/END\b/i, { token: 'keyword.block' }], + [/WHEN\b/i, { token: 'keyword.choice' }], + [/THEN\b/i, { token: 'keyword.choice' }], + ], + }, +}; diff --git a/yarn.lock b/yarn.lock index 723e47d..70e97fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1316,6 +1316,14 @@ eslint-plugin-react-hooks "4.6.0" typescript "4.8.4" +"@grafana/experimental@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@grafana/experimental/-/experimental-1.1.0.tgz#36b9644b1e61c782ed42b4805c5e297f2cc3f8bf" + integrity sha512-pQhYhw+jB7Q+t8rLcd1jcx91BiFDNslBATJkNIgO9I2Bah+ww+2RH1hUGVoJNPL84vW7WRU7w9k/L7FJs7/L6Q== + dependencies: + "@types/uuid" "^8.3.3" + uuid "^8.3.2" + "@grafana/faro-core@^1.0.0-beta2": version "1.0.2" resolved "https://registry.yarnpkg.com/@grafana/faro-core/-/faro-core-1.0.2.tgz#ac3d635a4ac464ab577c7e475c78211acd09bf02" @@ -2692,6 +2700,11 @@ resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw== +"@types/uuid@^8.3.3": + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== + "@types/yargs-parser@*": version "21.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"