diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
index 0c10036bdd2..60d1418a526 100644
--- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
+++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
@@ -173,6 +173,7 @@ Experimental features might be changed or removed without prior notice.
| `newFolderPicker` | Enables the nested folder picker without having nested folders enabled |
| `onPremToCloudMigrations` | In-development feature that will allow users to easily migrate their on-prem Grafana instances to Grafana Cloud. |
| `promQLScope` | In-development feature that will allow injection of labels into prometheus queries. |
+| `sqlExpressions` | Enables using SQL and DuckDB functions as Expressions. |
| `nodeGraphDotLayout` | Changed the layout algorithm for the node graph |
| `newPDFRendering` | New implementation for the dashboard to PDF rendering |
| `kubernetesAggregator` | Enable grafana aggregator |
diff --git a/go.mod b/go.mod
index 0c8c0e11ae5..061044a1a84 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module github.com/grafana/grafana
-go 1.21
+go 1.21.0
// Override docker/docker to avoid:
// go: github.com/drone-runners/drone-runner-docker@v1.8.2 requires
@@ -92,6 +92,7 @@ require (
github.com/prometheus/prometheus v1.8.2-0.20221021121301-51a44e6657c3 // @grafana/alerting-squad-backend
github.com/robfig/cron/v3 v3.0.1 // @grafana/backend-platform
github.com/russellhaering/goxmldsig v1.4.0 // @grafana/backend-platform
+ github.com/scottlepp/go-duck v0.0.15 // @grafana/grafana-app-platform-squad
github.com/stretchr/testify v1.8.4 // @grafana/backend-platform
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf // @grafana/backend-platform
github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f // @grafana/backend-platform
@@ -463,6 +464,9 @@ require github.com/spyzhov/ajson v0.9.0 // @grafana/grafana-app-platform-squad
require github.com/fullstorydev/grpchan v1.1.1 // @grafana/backend-platform
require (
+ github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c // indirect
+ github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect
+ github.com/apache/thrift v0.18.1 // indirect
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240226124929-648abdbd0ea4 // @grafana/grafana-app-platform-squad
github.com/grafana/grafana/pkg/apiserver v0.0.0-20240226124929-648abdbd0ea4 // @grafana/grafana-app-platform-squad
)
@@ -471,12 +475,17 @@ require (
github.com/bufbuild/protocompile v0.4.0 // indirect
github.com/grafana/sqlds/v3 v3.2.0 // indirect
github.com/jhump/protoreflect v1.15.1 // indirect
+ github.com/klauspost/asmfmt v1.3.2 // indirect
+ github.com/krasun/gosqlparser v1.0.5 // @grafana/grafana-app-platform-squad
+ github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect
+ github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mithrandie/csvq v1.17.10 // indirect
github.com/mithrandie/csvq-driver v1.6.8 // indirect
github.com/mithrandie/go-file/v2 v2.1.0 // indirect
github.com/mithrandie/go-text v1.5.4 // indirect
github.com/mithrandie/ternary v1.1.1 // indirect
+ github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2 // @grafana/grafana-app-platform-squad
)
// Use fork of crewjam/saml with fixes for some issues until changes get merged into upstream
diff --git a/go.sum b/go.sum
index 988032666be..a6aa605d076 100644
--- a/go.sum
+++ b/go.sum
@@ -1288,6 +1288,7 @@ github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0/go.mod h1:spvB9eLJH9dutlbP
github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM=
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
+github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU=
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
@@ -1354,6 +1355,8 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18=
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM=
github.com/apache/arrow/go/arrow v0.0.0-20210223225224-5bea62493d91/go.mod h1:c9sxoIT3YgLxH4UhLOCKaBlEojuMhVYpk4Ntv3opUTQ=
+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=
github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0=
github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI=
github.com/apache/arrow/go/v12 v12.0.0/go.mod h1:d+tV/eHZZ7Dz7RPrFKtPK02tpr+c9/PEd/zm8mDS9Vg=
@@ -1362,10 +1365,14 @@ github.com/apache/arrow/go/v15 v15.0.0/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+ye
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU=
+github.com/apache/thrift v0.18.1 h1:lNhK/1nqjbwbiOPDBPFJVKxgDEGSepKuTh6OLiXW8kg=
+github.com/apache/thrift v0.18.1/go.mod h1:rdQn/dCcDKEWjjylUeueum4vQEjG2v8v2PqriUnbr+I=
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
+github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
+github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
@@ -2026,6 +2033,7 @@ github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76
github.com/google/cel-go v0.17.7 h1:6ebJFzu1xO2n7TLtN+UBqShGBhlD85bhvglh5DpcfqQ=
github.com/google/cel-go v0.17.7/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY=
github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
+github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg=
github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
@@ -2447,10 +2455,12 @@ github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4=
github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
@@ -2480,6 +2490,8 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
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/krasun/gosqlparser v1.0.5 h1:sHaexkxGb9NrAcjZ3mUs6u33iJ9qhR2fH7XrpZekMt8=
+github.com/krasun/gosqlparser v1.0.5/go.mod h1:aXCTW1xnPl4qAaNROeqESauGJ8sqhoB4OFEIOVIDYI4=
github.com/kshvakov/clickhouse v1.3.5/go.mod h1:DMzX7FxRymoNkVgizH0DWAL8Cur7wHLgx3MUnGwJqpE=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
@@ -2586,7 +2598,9 @@ github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJys
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
+github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
+github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI=
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
@@ -2779,6 +2793,7 @@ github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk
github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
+github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
@@ -2920,6 +2935,9 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.21 h1:yWfiTPwYxB0l5fGMhl/G+liULugVIHD9AU77iNLrURQ=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.21/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
+github.com/scottlepp/go-duck v0.0.14 h1:fKrhE31OiwMJkS8byu0CWUfuWMfdxZYJNp5FUgHil4M=
+github.com/scottlepp/go-duck v0.0.14/go.mod h1:GL+hHuKdueJRrFCduwBc7A7TQk+Tetc5BPXPVtduihY=
+github.com/scottlepp/go-duck v0.0.15/go.mod h1:GL+hHuKdueJRrFCduwBc7A7TQk+Tetc5BPXPVtduihY=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
@@ -3085,6 +3103,8 @@ github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
+github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2 h1:zzrxE1FKn5ryBNl9eKOeqQ58Y/Qpo3Q9QNxKHX5uzzQ=
+github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2/go.mod h1:hzfGeIUDq/j97IG+FhNqkowIyEcD88LrW6fyU3K3WqY=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
github.com/yalue/merged_fs v1.2.2 h1:vXHTpJBluJryju7BBpytr3PDIkzsPMpiEknxVGPhN/I=
github.com/yalue/merged_fs v1.2.2/go.mod h1:WqqchfVYQyclV2tnR7wtRhBddzBvLVR83Cjw9BKQw0M=
@@ -3992,6 +4012,7 @@ google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxH
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
+google.golang.org/genproto v0.0.0-20210630183607-d20f26d13c79/go.mod h1:yiaVoXHpRzHGyxV3o4DktVWY4mSUErTKaeEOq6C3t3U=
google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
diff --git a/go.work.sum b/go.work.sum
index 57235ef9f2c..d8aa300bdfa 100644
--- a/go.work.sum
+++ b/go.work.sum
@@ -566,6 +566,7 @@ github.com/performancecopilot/speed/v4 v4.0.0 h1:VxEDCmdkfbQYDlcr/GC9YoN9PQ6p8ul
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/phpdave11/gofpdf v1.4.2 h1:KPKiIbfwbvC/wOncwhrpRdXVj2CZTCFlw4wnoyjtHfQ=
github.com/phpdave11/gofpdi v1.0.13 h1:o61duiW8M9sMlkVXWlvP92sZJtGKENvW3VExs6dZukQ=
+github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE=
@@ -596,6 +597,8 @@ github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCL
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk=
github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e h1:uO75wNGioszjmIzcY/tvdDYKRLVvzggtAmmJkn9j4GQ=
+github.com/scottlepp/go-duck v0.0.15 h1:qrSF3pXlXAA4a7uxAfLYajqXLkeBjv8iW1wPdSfkMj0=
+github.com/scottlepp/go-duck v0.0.15/go.mod h1:GL+hHuKdueJRrFCduwBc7A7TQk+Tetc5BPXPVtduihY=
github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e/go.mod h1:tm/wZFQ8e24NYaBGIlnO2WGCAi67re4HHuOm0sftE/M=
github.com/segmentio/parquet-go v0.0.0-20230427215636-d483faba23a5 h1:7CWCjaHrXSUCHrRhIARMGDVKdB82tnPAQMmANeflKOw=
github.com/segmentio/parquet-go v0.0.0-20230427215636-d483faba23a5/go.mod h1:+J0xQnJjm8DuQUHBO7t57EnmPbstT6+b45+p3DC9k1Q=
@@ -747,6 +750,8 @@ gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI=
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
+gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
+gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
gopkg.in/telebot.v3 v3.2.1 h1:3I4LohaAyJBiivGmkfB+CiVu7QFOWkuZ4+KHgO/G3rs=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts
index 48f8b5b5f95..0d8527653b8 100644
--- a/packages/grafana-data/src/types/featureToggles.gen.ts
+++ b/packages/grafana-data/src/types/featureToggles.gen.ts
@@ -171,6 +171,7 @@ export interface FeatureToggles {
onPremToCloudMigrations?: boolean;
alertingSaveStatePeriodic?: boolean;
promQLScope?: boolean;
+ sqlExpressions?: boolean;
nodeGraphDotLayout?: boolean;
groupToNestedTableTransformation?: boolean;
newPDFRendering?: boolean;
diff --git a/pkg/expr/commands.go b/pkg/expr/commands.go
index 32bc9b86361..1f8926102c4 100644
--- a/pkg/expr/commands.go
+++ b/pkg/expr/commands.go
@@ -324,6 +324,8 @@ const (
TypeClassicConditions
// TypeThreshold is the CMDType for checking if a threshold has been crossed
TypeThreshold
+ // TypeSQL is the CMDType for running SQL expressions
+ TypeSQL
)
func (gt CommandType) String() string {
@@ -336,6 +338,8 @@ func (gt CommandType) String() string {
return "resample"
case TypeClassicConditions:
return "classic_conditions"
+ case TypeSQL:
+ return "sql"
default:
return "unknown"
}
@@ -354,6 +358,8 @@ func ParseCommandType(s string) (CommandType, error) {
return TypeClassicConditions, nil
case "threshold":
return TypeThreshold, nil
+ case "sql":
+ return TypeSQL, nil
default:
return TypeUnknown, fmt.Errorf("'%v' is not a recognized expression type", s)
}
diff --git a/pkg/expr/graph.go b/pkg/expr/graph.go
index 2b09143110b..49c90b6221b 100644
--- a/pkg/expr/graph.go
+++ b/pkg/expr/graph.go
@@ -75,6 +75,8 @@ func (dp *DataPipeline) execute(c context.Context, now time.Time, s *Service) (m
executeDSNodesGrouped(c, now, vars, s, dsNodes)
}
+ s.allowLongFrames = hasSqlExpression(*dp)
+
for _, node := range *dp {
if groupByDSFlag && node.NodeType() == TypeDatasourceNode {
continue // already executed via executeDSNodesGrouped
@@ -266,6 +268,10 @@ func buildGraphEdges(dp *simple.DirectedGraph, registry map[string]Node) error {
for _, neededVar := range cmdNode.Command.NeedsVars() {
neededNode, ok := registry[neededVar]
if !ok {
+ _, ok := cmdNode.Command.(*SQLCommand)
+ if ok {
+ continue
+ }
return fmt.Errorf("unable to find dependent node '%v'", neededVar)
}
@@ -312,3 +318,37 @@ func GetCommandsFromPipeline[T Command](pipeline DataPipeline) []T {
}
return results
}
+
+func hasSqlExpression(dp DataPipeline) bool {
+ for _, node := range dp {
+ if node.NodeType() == TypeCMDNode {
+ cmdNode := node.(*CMDNode)
+ _, ok := cmdNode.Command.(*SQLCommand)
+ if ok {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+// func graphHasSqlExpresssion(dp *simple.DirectedGraph) bool {
+// node := dp.Nodes()
+// for node.Next() {
+// if cmdNode, ok := node.Node().(*CMDNode); ok {
+// // res[dpNode.RefID()] = dpNode
+// _, ok := cmdNode.Command.(*SQLCommand)
+// if ok {
+// return true
+// }
+// }
+// // if node.NodeType() == TypeCMDNode {
+// // cmdNode := node.(*CMDNode)
+// // _, ok := cmdNode.Command.(*SQLCommand)
+// // if ok {
+// // return true
+// // }
+// // }
+// }
+// return false
+// }
diff --git a/pkg/expr/mathexp/parse/node.go b/pkg/expr/mathexp/parse/node.go
index 41feeb70266..02c297dde96 100644
--- a/pkg/expr/mathexp/parse/node.go
+++ b/pkg/expr/mathexp/parse/node.go
@@ -405,6 +405,8 @@ const (
TypeVariantSet
// TypeNoData is a no data response without a known data type.
TypeNoData
+ // TypeTableData is a tabular data response.
+ TypeTableData
)
// String returns a string representation of the ReturnType.
@@ -422,6 +424,8 @@ func (f ReturnType) String() string {
return "variant"
case TypeNoData:
return "noData"
+ case TypeTableData:
+ return "tableData"
default:
return "unknown"
}
diff --git a/pkg/expr/mathexp/types.go b/pkg/expr/mathexp/types.go
index ca5069650b7..a5b5ba456a4 100644
--- a/pkg/expr/mathexp/types.go
+++ b/pkg/expr/mathexp/types.go
@@ -246,3 +246,48 @@ func (s NoData) New() NoData {
func NewNoData() NoData {
return NoData{data.NewFrame("no data")}
}
+
+// TableData is an untyped no data response.
+type TableData struct{ Frame *data.Frame }
+
+// Type returns the Value type and allows it to fulfill the Value interface.
+func (s TableData) Type() parse.ReturnType { return parse.TypeTableData }
+
+// Value returns the actual value allows it to fulfill the Value interface.
+func (s TableData) Value() any { return s }
+
+func (s TableData) GetLabels() data.Labels { return nil }
+
+func (s TableData) SetLabels(ls data.Labels) {}
+
+func (s TableData) GetMeta() any {
+ return s.Frame.Meta.Custom
+}
+
+func (s TableData) SetMeta(v any) {
+ m := s.Frame.Meta
+ if m == nil {
+ m = &data.FrameMeta{}
+ s.Frame.SetMeta(m)
+ }
+ m.Custom = v
+}
+
+func (s TableData) AddNotice(notice data.Notice) {
+ m := s.Frame.Meta
+ if m == nil {
+ m = &data.FrameMeta{}
+ s.Frame.SetMeta(m)
+ }
+ m.Notices = append(m.Notices, notice)
+}
+
+func (s TableData) AsDataFrame() *data.Frame { return s.Frame }
+
+func (s TableData) New() TableData {
+ return NewTableData()
+}
+
+func NewTableData() TableData {
+ return TableData{data.NewFrame("")}
+}
diff --git a/pkg/expr/models.go b/pkg/expr/models.go
index 4480331a51f..dec7e3ce3a0 100644
--- a/pkg/expr/models.go
+++ b/pkg/expr/models.go
@@ -24,6 +24,9 @@ const (
// Threshold
QueryTypeThreshold QueryType = "threshold"
+
+ // SQL query via DuckDB
+ QueryTypeSQL QueryType = "sql"
)
type MathQuery struct {
@@ -69,6 +72,11 @@ type ClassicQuery struct {
Conditions []classic.ConditionJSON `json:"conditions"`
}
+// SQLQuery requires the sqlExpression feature flag
+type SQLExpression struct {
+ Expression string `json:"expression" jsonschema:"minLength=1,example=SELECT * FROM A LIMIT 1"`
+}
+
//-------------------------------
// Non-query commands
//-------------------------------
diff --git a/pkg/expr/nodes.go b/pkg/expr/nodes.go
index 3f20267d71a..8ecf303cc4f 100644
--- a/pkg/expr/nodes.go
+++ b/pkg/expr/nodes.go
@@ -155,6 +155,8 @@ func buildCMDNode(rn *rawNode, toggles featuremgmt.FeatureToggles) (*CMDNode, er
node.Command, err = classic.UnmarshalConditionsCmd(rn.Query, rn.RefID)
case TypeThreshold:
node.Command, err = UnmarshalThresholdCommand(rn, toggles)
+ case TypeSQL:
+ node.Command, err = UnmarshalSQLCommand(rn)
default:
return nil, fmt.Errorf("expression command type '%v' in expression '%v' not implemented", commandType, rn.RefID)
}
@@ -471,7 +473,8 @@ func convertDataFramesToResults(ctx context.Context, frames data.Frames, datasou
logger.Warn("Ignoring InfluxDB data frame due to missing numeric fields")
continue
}
- if schema.Type != data.TimeSeriesTypeWide {
+
+ if schema.Type != data.TimeSeriesTypeWide && !s.allowLongFrames {
return "", mathexp.Results{}, fmt.Errorf("input data must be a wide series but got type %s (input refid)", schema.Type)
}
filtered = append(filtered, frame)
@@ -484,20 +487,29 @@ func convertDataFramesToResults(ctx context.Context, frames data.Frames, datasou
maybeFixerFn := checkIfSeriesNeedToBeFixed(filtered, datasourceType)
- vals := make([]mathexp.Value, 0, totalLen)
- for _, frame := range filtered {
- series, err := WideToMany(frame, maybeFixerFn)
- if err != nil {
- return "", mathexp.Results{}, err
- }
- for _, ser := range series {
- vals = append(vals, ser)
- }
- }
dataType := "single frame series"
if len(filtered) > 1 {
dataType = "multi frame series"
}
+
+ vals := make([]mathexp.Value, 0, totalLen)
+ for _, frame := range filtered {
+ schema := frame.TimeSeriesSchema()
+ if schema.Type == data.TimeSeriesTypeWide {
+ series, err := WideToMany(frame, maybeFixerFn)
+ if err != nil {
+ return "", mathexp.Results{}, err
+ }
+ for _, ser := range series {
+ vals = append(vals, ser)
+ }
+ } else {
+ v := mathexp.TableData{Frame: frame}
+ vals = append(vals, v)
+ dataType = "single frame"
+ }
+ }
+
return dataType, mathexp.Results{
Values: vals,
}, nil
diff --git a/pkg/expr/reader.go b/pkg/expr/reader.go
index e5563bb95d7..b92e7044859 100644
--- a/pkg/expr/reader.go
+++ b/pkg/expr/reader.go
@@ -30,6 +30,7 @@ func NewExpressionQueryReader(features featuremgmt.FeatureToggles) (*ExpressionQ
}
// ReadQuery implements query.TypedQueryHandler.
+// nolint:gocyclo
func (h *ExpressionQueryReader) ReadQuery(
// Properties that have been parsed off the same node
common *rawNode, // common query.CommonQueryProperties
@@ -102,6 +103,13 @@ func (h *ExpressionQueryReader) ReadQuery(
eq.Command, err = classic.NewConditionCmd(common.RefID, q.Conditions)
}
+ case QueryTypeSQL:
+ q := &SQLExpression{}
+ err = iter.ReadVal(q)
+ if err == nil {
+ eq.Command, err = NewSQLCommand(common.RefID, q.Expression, common.TimeRange)
+ }
+
case QueryTypeThreshold:
q := &ThresholdQuery{}
err = iter.ReadVal(q)
diff --git a/pkg/expr/service.go b/pkg/expr/service.go
index ffd7e3ffd66..978ee147211 100644
--- a/pkg/expr/service.go
+++ b/pkg/expr/service.go
@@ -63,8 +63,9 @@ type Service struct {
pluginsClient backend.CallResourceHandler
- tracer tracing.Tracer
- metrics *metrics
+ tracer tracing.Tracer
+ metrics *metrics
+ allowLongFrames bool
}
type pluginContextProvider interface {
diff --git a/pkg/expr/sql/parser.go b/pkg/expr/sql/parser.go
new file mode 100644
index 00000000000..d5ea2d1f6be
--- /dev/null
+++ b/pkg/expr/sql/parser.go
@@ -0,0 +1,99 @@
+package sql
+
+import (
+ "errors"
+ "strings"
+
+ parser "github.com/krasun/gosqlparser"
+ "github.com/xwb1989/sqlparser"
+)
+
+// TablesList returns a list of tables for the sql statement
+func TablesList(rawSQL string) ([]string, error) {
+ stmt, err := sqlparser.Parse(rawSQL)
+ if err != nil {
+ tables, err := parse(rawSQL)
+ if err != nil {
+ return parseTables(rawSQL)
+ }
+ return tables, nil
+ }
+
+ tables := []string{}
+ switch kind := stmt.(type) {
+ case *sqlparser.Select:
+ for _, t := range kind.From {
+ buf := sqlparser.NewTrackedBuffer(nil)
+ t.Format(buf)
+ table := buf.String()
+ if table != "dual" {
+ tables = append(tables, buf.String())
+ }
+ }
+ default:
+ return nil, errors.New("not a select statement")
+ }
+ return tables, nil
+}
+
+// uses a simple tokenizer
+func parse(rawSQL string) ([]string, error) {
+ query, err := parser.Parse(rawSQL)
+ if err != nil {
+ return nil, err
+ }
+ if query.GetType() == parser.StatementSelect {
+ sel, ok := query.(*parser.Select)
+ if ok {
+ return []string{sel.Table}, nil
+ }
+ }
+ return nil, err
+}
+
+func parseTables(rawSQL string) ([]string, error) {
+ checkSql := strings.ToUpper(rawSQL)
+ if strings.HasPrefix(checkSql, "SELECT") || strings.HasPrefix(rawSQL, "WITH") {
+ tables := []string{}
+ tokens := strings.Split(rawSQL, " ")
+ checkNext := false
+ takeNext := false
+ for _, t := range tokens {
+ t = strings.ToUpper(t)
+ t = strings.TrimSpace(t)
+
+ if takeNext {
+ tables = append(tables, t)
+ checkNext = false
+ takeNext = false
+ continue
+ }
+ if checkNext {
+ if strings.Contains(t, "(") {
+ checkNext = false
+ continue
+ }
+ if strings.Contains(t, ",") {
+ values := strings.Split(t, ",")
+ for _, v := range values {
+ v := strings.TrimSpace(v)
+ if v != "" {
+ tables = append(tables, v)
+ } else {
+ takeNext = true
+ break
+ }
+ }
+ continue
+ }
+ tables = append(tables, t)
+ checkNext = false
+ }
+ if t == "FROM" {
+ checkNext = true
+ }
+ }
+ return tables, nil
+ }
+ return nil, errors.New("not a select statement")
+}
diff --git a/pkg/expr/sql/parser_test.go b/pkg/expr/sql/parser_test.go
new file mode 100644
index 00000000000..2c8e43681e6
--- /dev/null
+++ b/pkg/expr/sql/parser_test.go
@@ -0,0 +1,58 @@
+package sql
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestParse(t *testing.T) {
+ sql := "select * from foo"
+ tables, err := parseTables((sql))
+ assert.Nil(t, err)
+
+ assert.Equal(t, "FOO", tables[0])
+}
+
+func TestParseWithComma(t *testing.T) {
+ sql := "select * from foo,bar"
+ tables, err := parseTables((sql))
+ assert.Nil(t, err)
+
+ assert.Equal(t, "FOO", tables[0])
+ assert.Equal(t, "BAR", tables[1])
+}
+
+func TestParseWithCommas(t *testing.T) {
+ sql := "select * from foo,bar,baz"
+ tables, err := parseTables((sql))
+ assert.Nil(t, err)
+
+ assert.Equal(t, "FOO", tables[0])
+ assert.Equal(t, "BAR", tables[1])
+ assert.Equal(t, "BAZ", tables[2])
+}
+
+func TestArray(t *testing.T) {
+ sql := "SELECT array_value(1, 2, 3)"
+ tables, err := TablesList((sql))
+ assert.Nil(t, err)
+
+ assert.Equal(t, 0, len(tables))
+}
+
+func TestArray2(t *testing.T) {
+ sql := "SELECT array_value(1, 2, 3)[2]"
+ tables, err := TablesList((sql))
+ assert.Nil(t, err)
+
+ assert.Equal(t, 0, len(tables))
+}
+
+func TestXxx(t *testing.T) {
+ sql := "SELECT [3, 2, 1]::INT[3];"
+ tables, err := TablesList((sql))
+ assert.Nil(t, err)
+
+ assert.Equal(t, 0, len(tables))
+}
diff --git a/pkg/expr/sql_command.go b/pkg/expr/sql_command.go
new file mode 100644
index 00000000000..ce69bd550d5
--- /dev/null
+++ b/pkg/expr/sql_command.go
@@ -0,0 +1,107 @@
+package expr
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/grafana/grafana-plugin-sdk-go/data"
+ "github.com/scottlepp/go-duck/duck"
+
+ "github.com/grafana/grafana/pkg/expr/mathexp"
+ "github.com/grafana/grafana/pkg/expr/sql"
+ "github.com/grafana/grafana/pkg/infra/tracing"
+ "github.com/grafana/grafana/pkg/util/errutil"
+)
+
+// SQLCommand is an expression to run SQL over results
+type SQLCommand struct {
+ query string
+ varsToQuery []string
+ timeRange TimeRange
+ refID string
+}
+
+// NewSQLCommand creates a new SQLCommand.
+func NewSQLCommand(refID, rawSQL string, tr TimeRange) (*SQLCommand, error) {
+ if rawSQL == "" {
+ return nil, errutil.BadRequest("sql-missing-query",
+ errutil.WithPublicMessage("missing SQL query"))
+ }
+ tables, err := sql.TablesList(rawSQL)
+ if err != nil {
+ logger.Warn("invalid sql query", "sql", rawSQL, "error", err)
+ return nil, errutil.BadRequest("sql-invalid-sql",
+ errutil.WithPublicMessage("error reading SQL command"),
+ )
+ }
+ return &SQLCommand{
+ query: rawSQL,
+ varsToQuery: tables,
+ timeRange: tr,
+ refID: refID,
+ }, nil
+}
+
+// UnmarshalSQLCommand creates a SQLCommand from Grafana's frontend query.
+func UnmarshalSQLCommand(rn *rawNode) (*SQLCommand, error) {
+ if rn.TimeRange == nil {
+ return nil, fmt.Errorf("time range must be specified for refID %s", rn.RefID)
+ }
+
+ expressionRaw, ok := rn.Query["expression"]
+ if !ok {
+ return nil, errors.New("no expression in the query")
+ }
+ expression, ok := expressionRaw.(string)
+ if !ok {
+ return nil, fmt.Errorf("expected sql expression to be type string, but got type %T", expressionRaw)
+ }
+
+ return NewSQLCommand(rn.RefID, expression, rn.TimeRange)
+}
+
+// NeedsVars returns the variable names (refIds) that are dependencies
+// to execute the command and allows the command to fulfill the Command interface.
+func (gr *SQLCommand) NeedsVars() []string {
+ return gr.varsToQuery
+}
+
+// Execute runs the command and returns the results or an error if the command
+// failed to execute.
+func (gr *SQLCommand) Execute(ctx context.Context, now time.Time, vars mathexp.Vars, tracer tracing.Tracer) (mathexp.Results, error) {
+ _, span := tracer.Start(ctx, "SSE.ExecuteSQL")
+ defer span.End()
+
+ allFrames := []*data.Frame{}
+ for _, ref := range gr.varsToQuery {
+ results := vars[ref]
+ frames := results.Values.AsDataFrames(ref)
+ allFrames = append(allFrames, frames...)
+ }
+
+ rsp := mathexp.Results{}
+
+ duckDB := duck.NewInMemoryDB()
+ var frame = &data.Frame{}
+ err := duckDB.QueryFramesInto(gr.refID, gr.query, allFrames, frame)
+ if err != nil {
+ rsp.Error = err
+ return rsp, nil
+ }
+
+ frame.RefID = gr.refID
+
+ if frame.Rows() == 0 {
+ rsp.Values = mathexp.Values{
+ mathexp.NoData{Frame: frame},
+ }
+ }
+
+ rsp.Values = mathexp.Values{
+ mathexp.TableData{Frame: frame},
+ }
+
+ return rsp, nil
+}
diff --git a/pkg/expr/sql_command_test.go b/pkg/expr/sql_command_test.go
new file mode 100644
index 00000000000..90ba470ae07
--- /dev/null
+++ b/pkg/expr/sql_command_test.go
@@ -0,0 +1,26 @@
+package expr
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestNewCommand(t *testing.T) {
+ cmd, err := NewSQLCommand("a", "select a from foo, bar", nil)
+ if err != nil && strings.Contains(err.Error(), "feature is not enabled") {
+ return
+ }
+
+ if err != nil {
+ t.Fail()
+ return
+ }
+
+ for _, v := range cmd.varsToQuery {
+ if strings.Contains("foo bar", v) {
+ continue
+ }
+ t.Fail()
+ return
+ }
+}
diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go
index 6d768fbfecb..b56ef5784ed 100644
--- a/pkg/services/featuremgmt/registry.go
+++ b/pkg/services/featuremgmt/registry.go
@@ -1139,6 +1139,13 @@ var (
Stage: FeatureStageExperimental,
Owner: grafanaObservabilityMetricsSquad,
},
+ {
+ Name: "sqlExpressions",
+ Description: "Enables using SQL and DuckDB functions as Expressions.",
+ Stage: FeatureStageExperimental,
+ FrontendOnly: false,
+ Owner: grafanaAppPlatformSquad,
+ },
{
Name: "nodeGraphDotLayout",
Description: "Changed the layout algorithm for the node graph",
diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv
index bbf88310eca..0a083ba800e 100644
--- a/pkg/services/featuremgmt/toggles_gen.csv
+++ b/pkg/services/featuremgmt/toggles_gen.csv
@@ -152,6 +152,7 @@ jitterAlertRulesWithinGroups,preview,@grafana/alerting-squad,false,true,false
onPremToCloudMigrations,experimental,@grafana/grafana-operator-experience-squad,false,false,false
alertingSaveStatePeriodic,privatePreview,@grafana/alerting-squad,false,false,false
promQLScope,experimental,@grafana/observability-metrics,false,false,false
+sqlExpressions,experimental,@grafana/grafana-app-platform-squad,false,false,false
nodeGraphDotLayout,experimental,@grafana/observability-traces-and-profiling,false,false,true
groupToNestedTableTransformation,preview,@grafana/dataviz-squad,false,false,true
newPDFRendering,experimental,@grafana/sharing-squad,false,false,false
diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go
index 17fa7f7610b..09e1a7657f3 100644
--- a/pkg/services/featuremgmt/toggles_gen.go
+++ b/pkg/services/featuremgmt/toggles_gen.go
@@ -619,6 +619,10 @@ const (
// In-development feature that will allow injection of labels into prometheus queries.
FlagPromQLScope = "promQLScope"
+ // FlagSqlExpressions
+ // Enables using SQL and DuckDB functions as Expressions.
+ FlagSqlExpressions = "sqlExpressions"
+
// FlagNodeGraphDotLayout
// Changed the layout algorithm for the node graph
FlagNodeGraphDotLayout = "nodeGraphDotLayout"
diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json
index 23a5c2204a7..e15db658b22 100644
--- a/pkg/services/featuremgmt/toggles_gen.json
+++ b/pkg/services/featuremgmt/toggles_gen.json
@@ -83,7 +83,7 @@
"name": "pluginsInstrumentationStatusSource",
"resourceVersion": "1708108588074",
"creationTimestamp": "2024-02-16T18:36:28Z",
- "deletionTimestamp": "2024-02-23T13:23:30Z"
+ "deletionTimestamp": "2024-02-27T14:43:01Z"
},
"spec": {
"description": "Include a status source label for plugin request metrics and logs",
@@ -511,7 +511,7 @@
"name": "displayAnonymousStats",
"resourceVersion": "1708108588074",
"creationTimestamp": "2024-02-16T18:36:28Z",
- "deletionTimestamp": "2024-02-23T13:23:30Z"
+ "deletionTimestamp": "2024-02-27T14:43:01Z"
},
"spec": {
"description": "Enables anonymous stats to be shown in the UI for Grafana",
@@ -1400,7 +1400,7 @@
"name": "traceToMetrics",
"resourceVersion": "1708108588074",
"creationTimestamp": "2024-02-16T18:36:28Z",
- "deletionTimestamp": "2024-02-23T13:23:30Z"
+ "deletionTimestamp": "2024-02-27T14:43:01Z"
},
"spec": {
"description": "Enable trace to metrics links",
@@ -1529,7 +1529,7 @@
"name": "splitScopes",
"resourceVersion": "1708108588074",
"creationTimestamp": "2024-02-16T18:36:28Z",
- "deletionTimestamp": "2024-02-23T13:23:30Z"
+ "deletionTimestamp": "2024-02-27T14:43:01Z"
},
"spec": {
"description": "Support faster dashboard and folder search by splitting permission scopes into parts",
@@ -1570,7 +1570,7 @@
"name": "externalServiceAuth",
"resourceVersion": "1708108588074",
"creationTimestamp": "2024-02-16T18:36:28Z",
- "deletionTimestamp": "2024-02-21T10:10:41Z"
+ "deletionTimestamp": "2024-02-27T14:43:01Z"
},
"spec": {
"description": "Starts an OAuth2 authentication provider for external services",
@@ -2131,6 +2131,21 @@
"codeowner": "@grafana/dataviz-squad",
"frontend": true
}
+ },
+ {
+ "metadata": {
+ "name": "sqlExpressions",
+ "resourceVersion": "1709044973784",
+ "creationTimestamp": "2024-02-19T22:46:11Z",
+ "annotations": {
+ "grafana.app/updatedTimestamp": "2024-02-27 14:42:53.784398 +0000 UTC"
+ }
+ },
+ "spec": {
+ "description": "Enables using SQL and DuckDB functions as Expressions.",
+ "stage": "experimental",
+ "codeowner": "@grafana/grafana-app-platform-squad"
+ }
}
]
}
\ No newline at end of file
diff --git a/public/app/features/alerting/unified/GrafanaRuleQueryViewer.tsx b/public/app/features/alerting/unified/GrafanaRuleQueryViewer.tsx
index 52ba9bbb731..ff0a2c2fc9c 100644
--- a/public/app/features/alerting/unified/GrafanaRuleQueryViewer.tsx
+++ b/public/app/features/alerting/unified/GrafanaRuleQueryViewer.tsx
@@ -5,6 +5,7 @@ import React from 'react';
import { DataSourceInstanceSettings, GrafanaTheme2, PanelData, RelativeTimeRange } from '@grafana/data';
import { config } from '@grafana/runtime';
+import { Preview } from '@grafana/sql/src/components/visual-query-builder/Preview';
import { Badge, Stack, useStyles2 } from '@grafana/ui';
import { mapRelativeTimeRangeToOption } from '@grafana/ui/src/components/DateTimePickers/RelativeTimeRangePicker/utils';
@@ -182,6 +183,9 @@ function ExpressionPreview({ refId, model, evalData, isAlertCondition }: Express
case ExpressionQueryType.threshold:
return ;
+ case ExpressionQueryType.sql:
+ return ;
+
default:
return <>Expression not supported: {model.type}>;
}
diff --git a/public/app/features/alerting/unified/components/expressions/Expression.tsx b/public/app/features/alerting/unified/components/expressions/Expression.tsx
index f05b56596ba..73db0a2f5bf 100644
--- a/public/app/features/alerting/unified/components/expressions/Expression.tsx
+++ b/public/app/features/alerting/unified/components/expressions/Expression.tsx
@@ -9,6 +9,7 @@ import { ClassicConditions } from 'app/features/expressions/components/ClassicCo
import { Math } from 'app/features/expressions/components/Math';
import { Reduce } from 'app/features/expressions/components/Reduce';
import { Resample } from 'app/features/expressions/components/Resample';
+import { SqlExpr } from 'app/features/expressions/components/SqlExpr';
import { Threshold } from 'app/features/expressions/components/Threshold';
import {
ExpressionQuery,
@@ -110,6 +111,9 @@ export const Expression: FC = ({
/>
);
+ case ExpressionQueryType.sql:
+ return ;
+
default:
return <>Expression not supported: {query.type}>;
}
diff --git a/public/app/features/alerting/unified/utils/timeRange.ts b/public/app/features/alerting/unified/utils/timeRange.ts
index dd7b3370bd1..11edb6c9034 100644
--- a/public/app/features/alerting/unified/utils/timeRange.ts
+++ b/public/app/features/alerting/unified/utils/timeRange.ts
@@ -29,6 +29,7 @@ const getReferencedIds = (model: ExpressionQuery, queries: AlertQuery[]): string
case ExpressionQueryType.classic:
return getReferencedIdsForClassicCondition(model);
case ExpressionQueryType.math:
+ case ExpressionQueryType.sql:
return getReferencedIdsForMath(model, queries);
case ExpressionQueryType.resample:
case ExpressionQueryType.reduce:
diff --git a/public/app/features/expressions/ExpressionQueryEditor.tsx b/public/app/features/expressions/ExpressionQueryEditor.tsx
index 90c6ba95de0..419fbe067fd 100644
--- a/public/app/features/expressions/ExpressionQueryEditor.tsx
+++ b/public/app/features/expressions/ExpressionQueryEditor.tsx
@@ -7,6 +7,7 @@ import { ClassicConditions } from './components/ClassicConditions';
import { Math } from './components/Math';
import { Reduce } from './components/Reduce';
import { Resample } from './components/Resample';
+import { SqlExpr } from './components/SqlExpr';
import { Threshold } from './components/Threshold';
import { ExpressionQuery, ExpressionQueryType, expressionTypes } from './types';
import { getDefaults } from './utils/expressionTypes';
@@ -27,6 +28,7 @@ function useExpressionsCache() {
case ExpressionQueryType.reduce:
case ExpressionQueryType.resample:
case ExpressionQueryType.threshold:
+ case ExpressionQueryType.sql:
return expressionCache.current[queryType];
case ExpressionQueryType.classic:
return undefined;
@@ -47,6 +49,8 @@ function useExpressionsCache() {
expressionCache.current.resample = value;
expressionCache.current.threshold = value;
break;
+ case ExpressionQueryType.sql:
+ expressionCache.current.sql = value;
}
}, []);
@@ -89,6 +93,9 @@ export function ExpressionQueryEditor(props: Props) {
case ExpressionQueryType.threshold:
return ;
+
+ case ExpressionQueryType.sql:
+ return ;
}
};
diff --git a/public/app/features/expressions/components/SqlExpr.tsx b/public/app/features/expressions/components/SqlExpr.tsx
new file mode 100644
index 00000000000..f5857f88929
--- /dev/null
+++ b/public/app/features/expressions/components/SqlExpr.tsx
@@ -0,0 +1,27 @@
+import React, { useMemo } from 'react';
+
+import { SelectableValue } from '@grafana/data';
+import { SQLEditor } from '@grafana/experimental';
+
+import { ExpressionQuery } from '../types';
+
+interface Props {
+ refIds: Array>;
+ query: ExpressionQuery;
+ onChange: (query: ExpressionQuery) => void;
+}
+
+export const SqlExpr = ({ onChange, refIds, query }: Props) => {
+ const vars = useMemo(() => refIds.map((v) => v.value!), [refIds]);
+
+ const initialQuery = `select * from ${vars[0]} limit 1`;
+
+ const onEditorChange = (expression: string) => {
+ onChange({
+ ...query,
+ expression,
+ });
+ };
+
+ return ;
+};
diff --git a/public/app/features/expressions/types.ts b/public/app/features/expressions/types.ts
index fe4026903b2..ad4091d608d 100644
--- a/public/app/features/expressions/types.ts
+++ b/public/app/features/expressions/types.ts
@@ -1,4 +1,5 @@
import { DataQuery, ReducerID, SelectableValue } from '@grafana/data';
+import { config } from 'app/core/config';
import { EvalFunction } from '../alerting/state/alertDef';
@@ -13,6 +14,7 @@ export enum ExpressionQueryType {
resample = 'resample',
classic = 'classic_conditions',
threshold = 'threshold',
+ sql = 'sql',
}
export const getExpressionLabel = (type: ExpressionQueryType) => {
@@ -27,6 +29,8 @@ export const getExpressionLabel = (type: ExpressionQueryType) => {
return 'Classic condition';
case ExpressionQueryType.threshold:
return 'Threshold';
+ case ExpressionQueryType.sql:
+ return 'SQL';
}
};
@@ -59,7 +63,17 @@ export const expressionTypes: Array> = [
description:
'Takes one or more time series returned from a query or an expression and checks if any of the series match the threshold condition.',
},
-];
+ {
+ value: ExpressionQueryType.sql,
+ label: 'SQL',
+ description: 'Transform data using SQL. Supports Aggregate/Analytics functions from DuckDB',
+ },
+].filter((expr) => {
+ if (expr.value === ExpressionQueryType.sql) {
+ return config.featureToggles?.sqlExpressions;
+ }
+ return true;
+});
export const reducerTypes: Array> = [
{ value: ReducerID.min, label: 'Min', description: 'Get the minimum value' },