mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
SQL Expressions: Re-implement feature using go-mysql-server (#99521)
* Under feature flag `sqlExpressions` and is experimental * Excluded from arm32 * Will not work with the Query Service yet * Does not have limits in place yet * Does not working with alerting yet * Currently requires "prepare time series" Transform for time series viz --------- Co-authored-by: Sam Jewell <sam.jewell@grafana.com>
This commit is contained in:
parent
4e6bdce41c
commit
d64f41afdc
12
go.mod
12
go.mod
@ -41,6 +41,8 @@ require (
|
||||
github.com/centrifugal/centrifuge v0.33.3 // @grafana/grafana-app-platform-squad
|
||||
github.com/crewjam/saml v0.4.13 // @grafana/identity-access-team
|
||||
github.com/dlmiddlecote/sqlstats v1.0.2 // @grafana/grafana-backend-group
|
||||
github.com/dolthub/go-mysql-server v0.19.0 // @grafana/grafana-datasources-core-services
|
||||
github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54 // @grafana/grafana-datasources-core-services
|
||||
github.com/fatih/color v1.17.0 // @grafana/grafana-backend-group
|
||||
github.com/fullstorydev/grpchan v1.1.1 // @grafana/grafana-backend-group
|
||||
github.com/gchaincl/sqlhooks v1.3.0 // @grafana/grafana-search-and-storage
|
||||
@ -104,7 +106,6 @@ require (
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.13.0 // @grafana/partner-datasources
|
||||
github.com/influxdata/influxql v1.4.0 // @grafana/partner-datasources
|
||||
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // @grafana/grafana-app-platform-squad
|
||||
github.com/jeremywohl/flatten v1.0.1 // @grafana/grafana-app-platform-squad
|
||||
github.com/jmespath-community/go-jmespath v1.1.1 // @grafana/identity-access-team
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect; // @grafana/grafana-backend-group
|
||||
github.com/jmoiron/sqlx v1.3.5 // @grafana/grafana-backend-group
|
||||
@ -315,6 +316,9 @@ require (
|
||||
github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 // indirect
|
||||
github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90 // indirect
|
||||
github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 // indirect
|
||||
github.com/dolthub/maphash v0.1.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/edsrzf/mmap-go v1.2.0 // indirect
|
||||
@ -399,6 +403,7 @@ require (
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/lestrrat-go/strftime v1.0.4 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 // indirect
|
||||
@ -466,9 +471,10 @@ require (
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||
github.com/shadowspore/fossil-delta v0.0.0-20240102155221-e3a8590b820b // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // @grafana/grafana-datasources-core-services
|
||||
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
|
||||
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/sony/gobreaker v0.5.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
@ -477,6 +483,7 @@ require (
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.8.2 // indirect
|
||||
github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
|
||||
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
|
||||
github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 // indirect
|
||||
@ -517,6 +524,7 @@ require (
|
||||
gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/src-d/go-errors.v1 v1.0.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.32.1 // indirect
|
||||
k8s.io/kms v0.32.1 // indirect
|
||||
|
21
go.sum
21
go.sum
@ -1057,8 +1057,18 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 h1:u3PMzfF8RkKd3lB9pZ2bfn0qEG+1Gms9599cr0REMww=
|
||||
github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2/go.mod h1:mIEZOHnFx4ZMQeawhw9rhsj+0zwQj7adVsnBX7t+eKY=
|
||||
github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90 h1:Sni8jrP0sy/w9ZYXoff4g/ixe+7bFCZlfCqXKJSU+zM=
|
||||
github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90/go.mod h1:ylU4XjUpsMcvl/BKeRRMXSH7e7WBrPXdSLvnRJYrxEA=
|
||||
github.com/dolthub/go-mysql-server v0.19.0 h1:NdcXyGt9v7m4sQOahU+ss++iyPy4Q3viuVvbnn3rUTQ=
|
||||
github.com/dolthub/go-mysql-server v0.19.0/go.mod h1:elfIatfq2fkU5lqTBrTcpL0RcHZOgYPE8EzBD7yQFiY=
|
||||
github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 h1:bMGS25NWAGTEtT5tOBsCuCrlYnLRKpbJVJkDbrTRhwQ=
|
||||
github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71/go.mod h1:2/2zjLQ/JOOSbbSboojeg+cAwcRV0fDLzIiWch/lhqI=
|
||||
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
|
||||
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
|
||||
github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54 h1:nzBnC0Rt1gFtscJEz4veYd/mazZEdbdmed+tujdaKOo=
|
||||
github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
@ -1731,8 +1741,6 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/jeremywohl/flatten v1.0.1 h1:LrsxmB3hfwJuE+ptGOijix1PIfOoKLJ3Uee/mzbgtrs=
|
||||
github.com/jeremywohl/flatten v1.0.1/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ=
|
||||
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
|
||||
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||
github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI=
|
||||
@ -1826,6 +1834,10 @@ github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6Fm
|
||||
github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353 h1:X/79QL0b4YJVO5+OsPH9rF2u428CIrGL/jLmPsoOQQ4=
|
||||
github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353/go.mod h1:N0SVk0uhy+E1PZ3C9ctsPRlvOPAFPkCNlcPBDkt0N3U=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=
|
||||
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
|
||||
github.com/lestrrat-go/strftime v1.0.4 h1:T1Rb9EPkAhgxKqbcMIPguPq8glqXTA1koF8n9BHElA8=
|
||||
github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
@ -2308,6 +2320,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA=
|
||||
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0=
|
||||
github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4=
|
||||
github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
|
||||
github.com/thanos-io/objstore v0.0.0-20240818203309-0363dadfdfb1 h1:z0v9BB/p7s4J6R//+0a5M3wCld8KzNjrGRLIwXfrAZk=
|
||||
github.com/thanos-io/objstore v0.0.0-20240818203309-0363dadfdfb1/go.mod h1:3ukSkG4rIRUGkKM4oIz+BSuUx2e3RlQVVv3Cc3W+Tv4=
|
||||
github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
@ -2847,6 +2861,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@ -3346,6 +3361,8 @@ gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/src-d/go-errors.v1 v1.0.0 h1:cooGdZnCjYbeS1zb1s6pVAAimTdKceRrpn7aKOnNIfc=
|
||||
gopkg.in/src-d/go-errors.v1 v1.0.0/go.mod h1:q1cBlomlw2FnDBDNGlnh6X0jPihy+QxZfMMNxPCbdYg=
|
||||
gopkg.in/telebot.v3 v3.2.1/go.mod h1:GJKwwWqp9nSkIVN51eRKU78aB5f5OnQuWdwiIZfPbko=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
|
@ -1321,6 +1321,8 @@ github.com/docker/go-plugins-helpers v0.0.0-20240701071450-45e2431495c8 h1:IMfrF
|
||||
github.com/docker/go-plugins-helpers v0.0.0-20240701071450-45e2431495c8/go.mod h1:LFyLie6XcDbyKGeVK6bHe+9aJTYCxWLBg5IrJZOaXKA=
|
||||
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
|
||||
github.com/dolthub/sqllogictest/go v0.0.0-20201107003712-816f3ae12d81 h1:7/v8q9XGFa6q5Ap4Z/OhNkAMBaK5YeuEzwJt+NZdhiE=
|
||||
github.com/dolthub/sqllogictest/go v0.0.0-20201107003712-816f3ae12d81/go.mod h1:siLfyv2c92W1eN/R4QqG/+RjjX5W2+gCTRjZxBjI3TY=
|
||||
github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw=
|
||||
github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0=
|
||||
github.com/drone/funcmap v0.0.0-20220929084810-72602997d16f h1:/jEs7lulqVO2u1+XI5rW4oFwIIusxuDOVKD9PAzlW2E=
|
||||
@ -1444,6 +1446,8 @@ github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54=
|
||||
github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng=
|
||||
github.com/gocql/gocql v0.0.0-20200526081602-cd04bd7f22a7 h1:TvUE5vjfoa7fFHMlmGOk0CsauNj1w4yJjR9+/GnWVCw=
|
||||
github.com/gocql/gocql v0.0.0-20200526081602-cd04bd7f22a7/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY=
|
||||
github.com/gocraft/dbr/v2 v2.7.2 h1:ccUxMuz6RdZvD7VPhMRRMSS/ECF3gytPhPtcavjktHk=
|
||||
github.com/gocraft/dbr/v2 v2.7.2/go.mod h1:5bCqyIXO5fYn3jEp/L06QF4K1siFdhxChMjdNu6YJrg=
|
||||
github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
|
||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
@ -1512,7 +1516,6 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafana/alerting v0.0.0-20250115195200-209e052dba64/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU=
|
||||
github.com/grafana/alerting v0.0.0-20250129195454-3e5b80036b7a/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU=
|
||||
github.com/grafana/authlib v0.0.0-20250120144156-d6737a7dc8f5/go.mod h1:V63rh3udd7sqXJeaG+nGUmViwVnM/bY6t8U9Tols2GU=
|
||||
github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8=
|
||||
github.com/grafana/authlib/types v0.0.0-20250120144156-d6737a7dc8f5/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM=
|
||||
@ -1608,6 +1611,8 @@ github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInw
|
||||
github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
|
||||
github.com/jedib0t/go-pretty/v6 v6.2.4 h1:wdaj2KHD2W+mz8JgJ/Q6L/T5dB7kyqEFI16eLq7GEmk=
|
||||
github.com/jedib0t/go-pretty/v6 v6.2.4/go.mod h1:+nE9fyyHGil+PuISTCrp7avEdo6bqoMwqZnuiK2r2a0=
|
||||
github.com/jeremywohl/flatten v1.0.1 h1:LrsxmB3hfwJuE+ptGOijix1PIfOoKLJ3Uee/mzbgtrs=
|
||||
github.com/jeremywohl/flatten v1.0.1/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.4.1-0.20181029123624-5de817a9aa20/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jhump/gopoet v0.1.0 h1:gYjOPnzHd2nzB37xYQZxj4EIQNpBrBskRqQQ3q4ZgSg=
|
||||
|
311
pkg/expr/convert_to_long.go
Normal file
311
pkg/expr/convert_to_long.go
Normal file
@ -0,0 +1,311 @@
|
||||
package expr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
)
|
||||
|
||||
func ConvertToLong(frames data.Frames) (data.Frames, error) {
|
||||
if len(frames) == 0 {
|
||||
// general empty case for now
|
||||
return frames, nil
|
||||
}
|
||||
// Four Conversion Possible Cases
|
||||
// 1. NumericMulti -> NumericLong
|
||||
// 2. NumericWide -> NumericLong
|
||||
// 3. TimeSeriesMulti -> TimeSeriesLong
|
||||
// 4. TimeSeriesWide -> TimeSeriesLong
|
||||
|
||||
// Detect if input type is declared
|
||||
// First Check Frame Meta Type
|
||||
|
||||
var inputType data.FrameType
|
||||
if frames[0].Meta != nil && frames[0].Meta.Type != "" {
|
||||
inputType = frames[0].Meta.Type
|
||||
}
|
||||
|
||||
// TODO: Add some guessing of Type if not declared
|
||||
if inputType == "" {
|
||||
return frames, fmt.Errorf("no input dataframe type set")
|
||||
}
|
||||
|
||||
if !supportedToLongConversion(inputType) {
|
||||
return frames, fmt.Errorf("unsupported input dataframe type %s for SQL expression", inputType)
|
||||
}
|
||||
|
||||
toLong := getToLongConversionFunc(inputType)
|
||||
if toLong == nil {
|
||||
return frames, fmt.Errorf("could not get conversion function for input type %s", inputType)
|
||||
}
|
||||
|
||||
return toLong(frames)
|
||||
}
|
||||
|
||||
func convertNumericMultiToNumericLong(frames data.Frames) (data.Frames, error) {
|
||||
// Apart from metadata, NumericMulti is basically NumericWide, except one frame per thing
|
||||
// so we collapse into wide and call the wide conversion
|
||||
wide := convertNumericMultiToNumericWide(frames)
|
||||
return convertNumericWideToNumericLong(wide)
|
||||
}
|
||||
|
||||
func convertNumericMultiToNumericWide(frames data.Frames) data.Frames {
|
||||
newFrame := data.NewFrame("")
|
||||
for _, frame := range frames {
|
||||
for _, field := range frame.Fields {
|
||||
if !field.Type().Numeric() {
|
||||
continue
|
||||
}
|
||||
newField := data.NewFieldFromFieldType(field.Type(), field.Len())
|
||||
newField.Name = field.Name
|
||||
newField.Labels = field.Labels.Copy()
|
||||
if field.Len() == 1 {
|
||||
newField.Set(0, field.CopyAt(0))
|
||||
}
|
||||
newFrame.Fields = append(newFrame.Fields, newField)
|
||||
}
|
||||
}
|
||||
return data.Frames{newFrame}
|
||||
}
|
||||
|
||||
func convertNumericWideToNumericLong(frames data.Frames) (data.Frames, error) {
|
||||
// Wide should only be one frame
|
||||
if len(frames) != 1 {
|
||||
return nil, fmt.Errorf("expected exactly one frame for wide format, but got %d", len(frames))
|
||||
}
|
||||
inputFrame := frames[0]
|
||||
|
||||
// The Frame should have no more than one row
|
||||
if inputFrame.Rows() > 1 {
|
||||
return nil, fmt.Errorf("expected no more than one row in the frame, but got %d", inputFrame.Rows())
|
||||
}
|
||||
|
||||
// Gather:
|
||||
// - unique numeric Field Names, and
|
||||
// - unique Label Keys (from Numeric Fields only)
|
||||
// each one maps to a field in the output long Frame.
|
||||
uniqueNames := make([]string, 0)
|
||||
uniqueKeys := make([]string, 0)
|
||||
|
||||
uniqueNamesMap := make(map[string]data.FieldType)
|
||||
uniqueKeysMap := make(map[string]struct{})
|
||||
|
||||
prints := make(map[string]int)
|
||||
|
||||
registerPrint := func(labels data.Labels) {
|
||||
fp := labels.Fingerprint().String()
|
||||
if _, ok := prints[fp]; !ok {
|
||||
prints[fp] = len(prints)
|
||||
}
|
||||
}
|
||||
|
||||
for _, field := range inputFrame.Fields {
|
||||
if field.Type().Numeric() {
|
||||
if _, ok := uniqueNamesMap[field.Name]; !ok {
|
||||
uniqueNames = append(uniqueNames, field.Name)
|
||||
uniqueNamesMap[field.Name] = field.Type()
|
||||
}
|
||||
|
||||
if field.Labels != nil {
|
||||
registerPrint(field.Labels)
|
||||
for key := range field.Labels {
|
||||
if _, ok := uniqueKeysMap[key]; !ok {
|
||||
uniqueKeys = append(uniqueKeys, key)
|
||||
}
|
||||
uniqueKeysMap[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create new fields for output Long frame
|
||||
fields := make([]*data.Field, 0, len(uniqueNames)+len(uniqueKeys))
|
||||
|
||||
// Create the Numeric Fields, tracking the index of each field by name
|
||||
// Note: May want to use FloatAt and and prepopulate with NaN so missing
|
||||
// combinations of value can be NA instead of the zero value of 0.
|
||||
var nameIndexMap = make(map[string]int, len(uniqueNames))
|
||||
for i, name := range uniqueNames {
|
||||
field := data.NewFieldFromFieldType(uniqueNamesMap[name], len(prints))
|
||||
field.Name = name
|
||||
fields = append(fields, field)
|
||||
nameIndexMap[name] = i
|
||||
}
|
||||
|
||||
// Create the String fields, tracking the index of each field by key
|
||||
var keyIndexMap = make(map[string]int, len(uniqueKeys))
|
||||
for i, k := range uniqueKeys {
|
||||
fields = append(fields, data.NewField(k, nil, make([]string, len(prints))))
|
||||
keyIndexMap[k] = len(nameIndexMap) + i
|
||||
}
|
||||
|
||||
longFrame := data.NewFrame("", fields...)
|
||||
|
||||
if inputFrame.Rows() == 0 {
|
||||
return data.Frames{longFrame}, nil
|
||||
}
|
||||
|
||||
// Add Rows to the fields
|
||||
for _, field := range inputFrame.Fields {
|
||||
if !field.Type().Numeric() {
|
||||
continue
|
||||
}
|
||||
fieldIdx := prints[field.Labels.Fingerprint().String()]
|
||||
longFrame.Fields[nameIndexMap[field.Name]].Set(fieldIdx, field.CopyAt(0))
|
||||
for key, value := range field.Labels {
|
||||
longFrame.Fields[keyIndexMap[key]].Set(fieldIdx, value)
|
||||
}
|
||||
}
|
||||
|
||||
return data.Frames{longFrame}, nil
|
||||
}
|
||||
|
||||
func convertTimeSeriesMultiToTimeSeriesLong(frames data.Frames) (data.Frames, error) {
|
||||
// Collect all time values and ensure no duplicates
|
||||
timeSet := make(map[time.Time]struct{})
|
||||
labelKeys := make(map[string]struct{}) // Collect all unique label keys
|
||||
numericFields := make(map[string]struct{}) // Collect unique numeric field names
|
||||
|
||||
for _, frame := range frames {
|
||||
for _, field := range frame.Fields {
|
||||
if field.Type() == data.FieldTypeTime {
|
||||
for i := 0; i < field.Len(); i++ {
|
||||
t := field.At(i).(time.Time)
|
||||
timeSet[t] = struct{}{}
|
||||
}
|
||||
} else if field.Type().Numeric() {
|
||||
numericFields[field.Name] = struct{}{}
|
||||
if field.Labels != nil {
|
||||
for key := range field.Labels {
|
||||
labelKeys[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a sorted slice of unique time values
|
||||
times := make([]time.Time, 0, len(timeSet))
|
||||
for t := range timeSet {
|
||||
times = append(times, t)
|
||||
}
|
||||
sort.Slice(times, func(i, j int) bool { return times[i].Before(times[j]) })
|
||||
|
||||
// Create output fields: Time, one numeric field per unique numeric name, and label fields
|
||||
timeField := data.NewField("Time", nil, times)
|
||||
outputNumericFields := make(map[string]*data.Field)
|
||||
for name := range numericFields {
|
||||
outputNumericFields[name] = data.NewField(name, nil, make([]float64, len(times)))
|
||||
}
|
||||
outputLabelFields := make(map[string]*data.Field)
|
||||
for key := range labelKeys {
|
||||
outputLabelFields[key] = data.NewField(key, nil, make([]string, len(times)))
|
||||
}
|
||||
|
||||
// Map time to index for quick lookup
|
||||
timeIndexMap := make(map[time.Time]int, len(times))
|
||||
for i, t := range times {
|
||||
timeIndexMap[t] = i
|
||||
}
|
||||
|
||||
// Populate output fields
|
||||
for _, frame := range frames {
|
||||
var timeField *data.Field
|
||||
for _, field := range frame.Fields {
|
||||
if field.Type() == data.FieldTypeTime {
|
||||
timeField = field
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if timeField == nil {
|
||||
return nil, fmt.Errorf("no time field found in frame")
|
||||
}
|
||||
|
||||
for _, field := range frame.Fields {
|
||||
if field.Type().Numeric() {
|
||||
for i := 0; i < field.Len(); i++ {
|
||||
t := timeField.At(i).(time.Time)
|
||||
val, err := field.FloatAt(i)
|
||||
if err != nil {
|
||||
val = 0 // Default value for missing data
|
||||
}
|
||||
idx := timeIndexMap[t]
|
||||
if outputField, exists := outputNumericFields[field.Name]; exists {
|
||||
outputField.Set(idx, val)
|
||||
}
|
||||
|
||||
// Add labels for the numeric field
|
||||
for key, value := range field.Labels {
|
||||
if outputField, exists := outputLabelFields[key]; exists {
|
||||
outputField.Set(idx, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the output frame
|
||||
outputFields := []*data.Field{timeField}
|
||||
for _, field := range outputNumericFields {
|
||||
outputFields = append(outputFields, field)
|
||||
}
|
||||
for _, field := range outputLabelFields {
|
||||
outputFields = append(outputFields, field)
|
||||
}
|
||||
outputFrame := data.NewFrame("time_series_long", outputFields...)
|
||||
|
||||
// Set metadata
|
||||
if outputFrame.Meta == nil {
|
||||
outputFrame.Meta = &data.FrameMeta{}
|
||||
}
|
||||
outputFrame.Meta.Type = data.FrameTypeTimeSeriesLong
|
||||
|
||||
return data.Frames{outputFrame}, nil
|
||||
}
|
||||
|
||||
func convertTimeSeriesWideToTimeSeriesLong(frames data.Frames) (data.Frames, error) {
|
||||
// Wide should only be one frame
|
||||
if len(frames) != 1 {
|
||||
return nil, fmt.Errorf("expected exactly one frame for wide format, but got %d", len(frames))
|
||||
}
|
||||
inputFrame := frames[0]
|
||||
longFrame, err := data.WideToLong(inputFrame)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert wide time series to long timeseries for sql expression: %w", err)
|
||||
}
|
||||
return data.Frames{longFrame}, nil
|
||||
}
|
||||
|
||||
func getToLongConversionFunc(inputType data.FrameType) func(data.Frames) (data.Frames, error) {
|
||||
switch inputType {
|
||||
case data.FrameTypeNumericMulti:
|
||||
return convertNumericMultiToNumericLong
|
||||
case data.FrameTypeNumericWide:
|
||||
return convertNumericWideToNumericLong
|
||||
case data.FrameTypeTimeSeriesMulti:
|
||||
return convertTimeSeriesMultiToTimeSeriesLong
|
||||
case data.FrameTypeTimeSeriesWide:
|
||||
return convertTimeSeriesWideToTimeSeriesLong
|
||||
default:
|
||||
return convertErr
|
||||
}
|
||||
}
|
||||
|
||||
func convertErr(_ data.Frames) (data.Frames, error) {
|
||||
return nil, fmt.Errorf("unsupported input type for SQL expression")
|
||||
}
|
||||
|
||||
func supportedToLongConversion(inputType data.FrameType) bool {
|
||||
switch inputType {
|
||||
case data.FrameTypeNumericMulti, data.FrameTypeNumericWide:
|
||||
return true
|
||||
case data.FrameTypeTimeSeriesMulti, data.FrameTypeTimeSeriesWide:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
48
pkg/expr/convert_to_long_test.go
Normal file
48
pkg/expr/convert_to_long_test.go
Normal file
@ -0,0 +1,48 @@
|
||||
package expr
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConvertNumericMultiToLong(t *testing.T) {
|
||||
input := data.Frames{
|
||||
data.NewFrame("test",
|
||||
data.NewField("Value", data.Labels{"city": "MIA"}, []int64{5})),
|
||||
data.NewFrame("test",
|
||||
data.NewField("Value", data.Labels{"city": "LGA"}, []int64{7}),
|
||||
),
|
||||
}
|
||||
expectedFrame := data.NewFrame("",
|
||||
data.NewField("Value", nil, []int64{5, 7}),
|
||||
data.NewField("city", nil, []string{"MIA", "LGA"}),
|
||||
)
|
||||
output, err := convertNumericMultiToNumericLong(input)
|
||||
require.NoError(t, err)
|
||||
|
||||
if diff := cmp.Diff(expectedFrame, output[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertNumericWideToLong(t *testing.T) {
|
||||
input := data.Frames{
|
||||
data.NewFrame("test",
|
||||
data.NewField("Value", data.Labels{"city": "MIA"}, []int64{5}),
|
||||
data.NewField("Value", data.Labels{"city": "LGA"}, []int64{7}),
|
||||
),
|
||||
}
|
||||
expectedFrame := data.NewFrame("",
|
||||
data.NewField("Value", nil, []int64{5, 7}),
|
||||
data.NewField("city", nil, []string{"MIA", "LGA"}),
|
||||
)
|
||||
output, err := convertNumericWideToNumericLong(input)
|
||||
require.NoError(t, err)
|
||||
|
||||
if diff := cmp.Diff(expectedFrame, output[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff)
|
||||
}
|
||||
}
|
@ -23,7 +23,6 @@ type ResultConverter struct {
|
||||
func (c *ResultConverter) Convert(ctx context.Context,
|
||||
datasourceType string,
|
||||
frames data.Frames,
|
||||
allowLongFrames bool,
|
||||
) (string, mathexp.Results, error) {
|
||||
if len(frames) == 0 {
|
||||
return "no-data", mathexp.Results{Values: mathexp.Values{mathexp.NewNoData()}}, nil
|
||||
@ -80,7 +79,7 @@ func (c *ResultConverter) Convert(ctx context.Context,
|
||||
continue
|
||||
}
|
||||
|
||||
if schema.Type != data.TimeSeriesTypeWide && !allowLongFrames {
|
||||
if schema.Type != data.TimeSeriesTypeWide {
|
||||
return "", mathexp.Results{}, fmt.Errorf("%w but got type %s (input refid)", ErrSeriesMustBeWide, schema.Type)
|
||||
}
|
||||
filtered = append(filtered, frame)
|
||||
|
@ -40,7 +40,7 @@ func TestConvertDataFramesToResults(t *testing.T) {
|
||||
|
||||
for _, dtype := range supported {
|
||||
t.Run(dtype, func(t *testing.T) {
|
||||
resultType, res, err := converter.Convert(context.Background(), dtype, frames, s.allowLongFrames)
|
||||
resultType, res, err := converter.Convert(context.Background(), dtype, frames)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "single frame series", resultType)
|
||||
require.Len(t, res.Values, 2)
|
||||
@ -68,7 +68,7 @@ func TestConvertDataFramesToResults(t *testing.T) {
|
||||
|
||||
for _, dtype := range supported {
|
||||
t.Run(dtype, func(t *testing.T) {
|
||||
resultType, res, err := converter.Convert(context.Background(), dtype, frames, s.allowLongFrames)
|
||||
resultType, res, err := converter.Convert(context.Background(), dtype, frames)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "multi frame series", resultType)
|
||||
require.Len(t, res.Values, 2)
|
||||
@ -101,7 +101,7 @@ func TestConvertDataFramesToResults(t *testing.T) {
|
||||
|
||||
for _, dtype := range supported {
|
||||
t.Run(dtype, func(t *testing.T) {
|
||||
resultType, res, err := converter.Convert(context.Background(), dtype, frames, s.allowLongFrames)
|
||||
resultType, res, err := converter.Convert(context.Background(), dtype, frames)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "multi frame series", resultType)
|
||||
require.Len(t, res.Values, 2)
|
||||
|
@ -77,8 +77,6 @@ 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
|
||||
@ -321,12 +319,26 @@ func buildGraphEdges(dp *simple.DirectedGraph, registry map[string]Node) error {
|
||||
neededNode, ok := registry[neededVar]
|
||||
if !ok {
|
||||
_, ok := cmdNode.Command.(*SQLCommand)
|
||||
// If the SSE is a SQL expression, and the node can't be found, it might be a CTE table name
|
||||
// CTEs are calculated during the evaluation of the SQL, so we won't have a node for them
|
||||
// So we `continue` in order to support CTE functionality
|
||||
// TODO: remove CTE table names from the list of table names during parsing of the SQL
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("unable to find dependent node '%v'", neededVar)
|
||||
}
|
||||
|
||||
// If the input is SQL, conversion is handled differently
|
||||
if _, ok := cmdNode.Command.(*SQLCommand); ok {
|
||||
if dsNode, ok := neededNode.(*DSNode); ok {
|
||||
dsNode.isInputToSQLExpr = true
|
||||
} else {
|
||||
// Only allow data source nodes as SQL expression inputs for now
|
||||
return fmt.Errorf("only data source queries may be inputs to a sql expression, %v is the input for %v", neededVar, cmdNode.RefID())
|
||||
}
|
||||
}
|
||||
|
||||
if neededNode.ID() == cmdNode.ID() {
|
||||
return fmt.Errorf("expression '%v' cannot reference itself. Must be query or another expression", neededVar)
|
||||
}
|
||||
@ -343,6 +355,13 @@ func buildGraphEdges(dp *simple.DirectedGraph, registry map[string]Node) error {
|
||||
}
|
||||
}
|
||||
|
||||
if neededNode.NodeType() == TypeCMDNode {
|
||||
if neededNode.(*CMDNode).CMDType == TypeSQL {
|
||||
// Do not allow SQL expressions to be inputs for other expressions for now
|
||||
return fmt.Errorf("sql expressions can not be the input for other expressions, but %v in the input for %v", neededVar, cmdNode.RefID())
|
||||
}
|
||||
}
|
||||
|
||||
edge := dp.NewEdge(neededNode, cmdNode)
|
||||
|
||||
dp.SetEdge(edge)
|
||||
@ -370,37 +389,3 @@ 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
|
||||
// }
|
||||
|
@ -250,7 +250,7 @@ func NewNoData() NoData {
|
||||
return NoData{data.NewFrame("no data")}
|
||||
}
|
||||
|
||||
// TableData is an untyped no data response.
|
||||
// TableData is a single table data frame with no labels on any fields.
|
||||
type TableData struct{ Frame *data.Frame }
|
||||
|
||||
// Type returns the Value type and allows it to fulfill the Value interface.
|
||||
|
@ -130,7 +130,7 @@ func (m *MLNode) Execute(ctx context.Context, now time.Time, _ mathexp.Vars, s *
|
||||
}
|
||||
|
||||
// process the response the same way DSNode does. Use plugin ID as data source type. Semantically, they are the same.
|
||||
responseType, result, err = s.converter.Convert(ctx, mlPluginID, dataFrames, s.allowLongFrames)
|
||||
responseType, result, err = s.converter.Convert(ctx, mlPluginID, dataFrames)
|
||||
return result, err
|
||||
}
|
||||
|
||||
|
@ -112,6 +112,12 @@ func buildCMDNode(rn *rawNode, toggles featuremgmt.FeatureToggles) (*CMDNode, er
|
||||
return nil, fmt.Errorf("invalid command type in expression '%v': %w", rn.RefID, err)
|
||||
}
|
||||
|
||||
if commandType == TypeSQL {
|
||||
if !toggles.IsEnabledGlobally(featuremgmt.FlagSqlExpressions) {
|
||||
return nil, fmt.Errorf("sql expressions are disabled")
|
||||
}
|
||||
}
|
||||
|
||||
node := &CMDNode{
|
||||
baseNode: baseNode{
|
||||
id: rn.idx,
|
||||
@ -185,6 +191,8 @@ type DSNode struct {
|
||||
intervalMS int64
|
||||
maxDP int64
|
||||
request Request
|
||||
|
||||
isInputToSQLExpr bool
|
||||
}
|
||||
|
||||
func (dn *DSNode) String() string {
|
||||
@ -333,7 +341,7 @@ func executeDSNodesGrouped(ctx context.Context, now time.Time, vars mathexp.Vars
|
||||
}
|
||||
|
||||
var result mathexp.Results
|
||||
responseType, result, err := s.converter.Convert(ctx, dn.datasource.Type, dataFrames, s.allowLongFrames)
|
||||
responseType, result, err := s.converter.Convert(ctx, dn.datasource.Type, dataFrames)
|
||||
if err != nil {
|
||||
result.Error = makeConversionError(dn.RefID(), err)
|
||||
}
|
||||
@ -401,7 +409,44 @@ func (dn *DSNode) Execute(ctx context.Context, now time.Time, _ mathexp.Vars, s
|
||||
}
|
||||
|
||||
var result mathexp.Results
|
||||
responseType, result, err = s.converter.Convert(ctx, dn.datasource.Type, dataFrames, s.allowLongFrames)
|
||||
// If the datasource node is an input to a SQL expression,
|
||||
// the data must be in the Long format
|
||||
if dn.isInputToSQLExpr {
|
||||
var needsConversion bool
|
||||
// Convert it if Multi:
|
||||
if len(dataFrames) > 1 {
|
||||
needsConversion = true
|
||||
}
|
||||
|
||||
// Convert it if Wide (has labels):
|
||||
if len(dataFrames) == 1 {
|
||||
for _, field := range dataFrames[0].Fields {
|
||||
if len(field.Labels) > 0 {
|
||||
needsConversion = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if needsConversion {
|
||||
convertedFrames, err := ConvertToLong(dataFrames)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to convert data frames to long format for sql: %w", err)
|
||||
}
|
||||
result.Values = mathexp.Values{
|
||||
mathexp.TableData{Frame: convertedFrames[0]},
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Otherwise it is already Long format; return as is
|
||||
result.Values = mathexp.Values{
|
||||
mathexp.TableData{Frame: dataFrames[0]},
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
responseType, result, err = s.converter.Convert(ctx, dn.datasource.Type, dataFrames)
|
||||
if err != nil {
|
||||
err = makeConversionError(dn.refID, err)
|
||||
}
|
||||
|
@ -64,9 +64,8 @@ type Service struct {
|
||||
|
||||
pluginsClient backend.CallResourceHandler
|
||||
|
||||
tracer tracing.Tracer
|
||||
metrics *metrics
|
||||
allowLongFrames bool
|
||||
tracer tracing.Tracer
|
||||
metrics *metrics
|
||||
}
|
||||
|
||||
type pluginContextProvider interface {
|
||||
|
104
pkg/expr/service_sql_test.go
Normal file
104
pkg/expr/service_sql_test.go
Normal file
@ -0,0 +1,104 @@
|
||||
package expr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSQLService(t *testing.T) {
|
||||
inputFrame := data.NewFrame("",
|
||||
data.NewField("time", nil, []time.Time{time.Unix(1, 0)}),
|
||||
data.NewField("value", nil, []*float64{fp(2)}),
|
||||
)
|
||||
|
||||
resp := map[string]backend.DataResponse{
|
||||
"A": {Frames: data.Frames{inputFrame}},
|
||||
}
|
||||
|
||||
newABSQLQueries := func(q string) []Query {
|
||||
q, err := jsonEscape(q)
|
||||
require.NoError(t, err)
|
||||
return []Query{
|
||||
{
|
||||
RefID: "A",
|
||||
DataSource: &datasources.DataSource{
|
||||
OrgID: 1,
|
||||
UID: "test",
|
||||
Type: "test",
|
||||
},
|
||||
JSON: json.RawMessage(`{ "datasource": { "uid": "1" }, "intervalMs": 1000, "maxDataPoints": 1000 }`),
|
||||
TimeRange: AbsoluteTimeRange{
|
||||
From: time.Time{},
|
||||
To: time.Time{},
|
||||
},
|
||||
},
|
||||
{
|
||||
RefID: "B",
|
||||
DataSource: dataSourceModel(),
|
||||
JSON: json.RawMessage(fmt.Sprintf(`{ "datasource": { "uid": "__expr__", "type": "__expr__"}, "type": "sql", "expression": "%s" }`, q)),
|
||||
TimeRange: AbsoluteTimeRange{
|
||||
From: time.Time{},
|
||||
To: time.Time{},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
t.Run("no feature flag no queries for you", func(t *testing.T) {
|
||||
s, req := newMockQueryService(resp, newABSQLQueries(""))
|
||||
|
||||
_, err := s.BuildPipeline(req)
|
||||
require.Error(t, err, "should not be able to build pipeline without feature flag")
|
||||
})
|
||||
|
||||
t.Run("with feature flag basic select works", func(t *testing.T) {
|
||||
s, req := newMockQueryService(resp, newABSQLQueries("SELECT * FROM A"))
|
||||
s.features = featuremgmt.WithFeatures(featuremgmt.FlagSqlExpressions)
|
||||
pl, err := s.BuildPipeline(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := s.ExecutePipeline(context.Background(), time.Now(), pl)
|
||||
require.NoError(t, err)
|
||||
|
||||
inputFrame.RefID = "B"
|
||||
inputFrame.Name = "B"
|
||||
if diff := cmp.Diff(res.Responses["B"].Frames[0], inputFrame, data.FrameTestCompareOptions()...); diff != "" {
|
||||
require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("load_file is blocked", func(t *testing.T) {
|
||||
s, req := newMockQueryService(resp,
|
||||
newABSQLQueries(`SELECT CAST(load_file('/etc/topSecretz') AS CHAR(10000) CHARACTER SET utf8)`),
|
||||
)
|
||||
|
||||
s.features = featuremgmt.WithFeatures(featuremgmt.FlagSqlExpressions)
|
||||
|
||||
pl, err := s.BuildPipeline(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
rsp, err := s.ExecutePipeline(context.Background(), time.Now(), pl)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Error(t, rsp.Responses["B"].Error, "should return invalid sql error")
|
||||
require.ErrorContains(t, rsp.Responses["B"].Error, "blocked function load_file")
|
||||
})
|
||||
}
|
||||
|
||||
func jsonEscape(input string) (string, error) {
|
||||
escaped, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// json.Marshal returns the escaped string with quotes, so we need to trim them
|
||||
return string(escaped[1 : len(escaped)-1]), nil
|
||||
}
|
@ -29,32 +29,11 @@ import (
|
||||
func TestService(t *testing.T) {
|
||||
dsDF := data.NewFrame("test",
|
||||
data.NewField("time", nil, []time.Time{time.Unix(1, 0)}),
|
||||
data.NewField("value", data.Labels{"test": "label"}, []*float64{fp(2)}))
|
||||
data.NewField("value", data.Labels{"test": "label"}, []*float64{fp(2)}),
|
||||
)
|
||||
|
||||
me := &mockEndpoint{
|
||||
Responses: map[string]backend.DataResponse{
|
||||
"A": {Frames: data.Frames{dsDF}},
|
||||
},
|
||||
}
|
||||
|
||||
pCtxProvider := plugincontext.ProvideService(setting.NewCfg(), nil, &pluginstore.FakePluginStore{
|
||||
PluginList: []pluginstore.Plugin{
|
||||
{JSONData: plugins.JSONData{ID: "test"}},
|
||||
},
|
||||
}, &datafakes.FakeCacheService{}, &datafakes.FakeDataSourceService{}, nil, pluginconfig.NewFakePluginRequestConfigProvider())
|
||||
|
||||
features := featuremgmt.WithFeatures()
|
||||
s := Service{
|
||||
cfg: setting.NewCfg(),
|
||||
dataService: me,
|
||||
pCtxProvider: pCtxProvider,
|
||||
features: features,
|
||||
tracer: tracing.InitializeTracerForTest(),
|
||||
metrics: newMetrics(nil),
|
||||
converter: &ResultConverter{
|
||||
Features: features,
|
||||
Tracer: tracing.InitializeTracerForTest(),
|
||||
},
|
||||
resp := map[string]backend.DataResponse{
|
||||
"A": {Frames: data.Frames{dsDF}},
|
||||
}
|
||||
|
||||
queries := []Query{
|
||||
@ -78,7 +57,7 @@ func TestService(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
req := &Request{Queries: queries, User: &user.SignedInUser{}}
|
||||
s, req := newMockQueryService(resp, queries)
|
||||
|
||||
pl, err := s.BuildPipeline(req)
|
||||
require.NoError(t, err)
|
||||
@ -121,26 +100,9 @@ func TestService(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDSQueryError(t *testing.T) {
|
||||
me := &mockEndpoint{
|
||||
Responses: map[string]backend.DataResponse{
|
||||
"A": {Error: fmt.Errorf("womp womp")},
|
||||
"B": {Frames: data.Frames{}},
|
||||
},
|
||||
}
|
||||
|
||||
pCtxProvider := plugincontext.ProvideService(setting.NewCfg(), nil, &pluginstore.FakePluginStore{
|
||||
PluginList: []pluginstore.Plugin{
|
||||
{JSONData: plugins.JSONData{ID: "test"}},
|
||||
},
|
||||
}, &datafakes.FakeCacheService{}, &datafakes.FakeDataSourceService{}, nil, pluginconfig.NewFakePluginRequestConfigProvider())
|
||||
|
||||
s := Service{
|
||||
cfg: setting.NewCfg(),
|
||||
dataService: me,
|
||||
pCtxProvider: pCtxProvider,
|
||||
features: featuremgmt.WithFeatures(),
|
||||
tracer: tracing.InitializeTracerForTest(),
|
||||
metrics: newMetrics(nil),
|
||||
resp := map[string]backend.DataResponse{
|
||||
"A": {Error: fmt.Errorf("womp womp")},
|
||||
"B": {Frames: data.Frames{}},
|
||||
}
|
||||
|
||||
queries := []Query{
|
||||
@ -169,19 +131,19 @@ func TestDSQueryError(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
req := &Request{Queries: queries, User: &user.SignedInUser{}}
|
||||
s, req := newMockQueryService(resp, queries)
|
||||
|
||||
pl, err := s.BuildPipeline(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := s.ExecutePipeline(context.Background(), time.Now(), pl)
|
||||
res, err := s.ExecutePipeline(context.Background(), time.Now(), pl)
|
||||
require.NoError(t, err)
|
||||
|
||||
var utilErr errutil.Error
|
||||
require.ErrorContains(t, resp.Responses["A"].Error, "womp womp")
|
||||
require.ErrorAs(t, resp.Responses["B"].Error, &utilErr)
|
||||
require.ErrorContains(t, res.Responses["A"].Error, "womp womp")
|
||||
require.ErrorAs(t, res.Responses["B"].Error, &utilErr)
|
||||
require.ErrorIs(t, utilErr, DependencyError)
|
||||
require.Equal(t, fp(42), resp.Responses["C"].Frames[0].Fields[0].At(0))
|
||||
require.Equal(t, fp(42), res.Responses["C"].Frames[0].Fields[0].At(0))
|
||||
}
|
||||
|
||||
func fp(f float64) *float64 {
|
||||
@ -204,3 +166,28 @@ func dataSourceModel() *datasources.DataSource {
|
||||
d, _ := DataSourceModelFromNodeType(TypeCMDNode)
|
||||
return d
|
||||
}
|
||||
|
||||
func newMockQueryService(responses map[string]backend.DataResponse, queries []Query) (*Service, *Request) {
|
||||
me := &mockEndpoint{
|
||||
Responses: responses,
|
||||
}
|
||||
pCtxProvider := plugincontext.ProvideService(setting.NewCfg(), nil, &pluginstore.FakePluginStore{
|
||||
PluginList: []pluginstore.Plugin{
|
||||
{JSONData: plugins.JSONData{ID: "test"}},
|
||||
},
|
||||
}, &datafakes.FakeCacheService{}, &datafakes.FakeDataSourceService{}, nil, pluginconfig.NewFakePluginRequestConfigProvider())
|
||||
|
||||
features := featuremgmt.WithFeatures()
|
||||
return &Service{
|
||||
cfg: setting.NewCfg(),
|
||||
dataService: me,
|
||||
pCtxProvider: pCtxProvider,
|
||||
features: featuremgmt.WithFeatures(),
|
||||
tracer: tracing.InitializeTracerForTest(),
|
||||
metrics: newMetrics(nil),
|
||||
converter: &ResultConverter{
|
||||
Features: features,
|
||||
Tracer: tracing.InitializeTracerForTest(),
|
||||
},
|
||||
}, &Request{Queries: queries, User: &user.SignedInUser{}}
|
||||
}
|
||||
|
@ -1,22 +1,61 @@
|
||||
//go:build !arm
|
||||
|
||||
package sql
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"context"
|
||||
|
||||
sqle "github.com/dolthub/go-mysql-server"
|
||||
mysql "github.com/dolthub/go-mysql-server/sql"
|
||||
|
||||
"github.com/dolthub/go-mysql-server/sql/analyzer"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
}
|
||||
// DB is a database that can execute SQL queries against a set of Frames.
|
||||
type DB struct{}
|
||||
|
||||
func (db *DB) RunCommands(commands []string) (string, error) {
|
||||
return "", errors.New("not implemented")
|
||||
}
|
||||
// QueryFrames runs the sql query query against a database created from frames, and returns the frame.
|
||||
// The RefID of each frame becomes a table in the database.
|
||||
// It is expected that there is only one frame per RefID.
|
||||
// The name becomes the name and RefID of the returned frame.
|
||||
func (db *DB) QueryFrames(ctx context.Context, name string, query string, frames []*data.Frame) (*data.Frame, error) {
|
||||
// We are parsing twice due to TablesList, but don't care fow now. We can save the parsed query and reuse it later if we want.
|
||||
if allow, err := AllowQuery(query); err != nil || !allow {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (db *DB) QueryFramesInto(name string, query string, frames []*data.Frame, f *data.Frame) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
pro := NewFramesDBProvider(frames)
|
||||
session := mysql.NewBaseSession()
|
||||
mCtx := mysql.NewContext(ctx, mysql.WithSession(session))
|
||||
|
||||
func NewInMemoryDB() *DB {
|
||||
return &DB{}
|
||||
// Select the database in the context
|
||||
mCtx.SetCurrentDatabase(dbName)
|
||||
|
||||
// Empty dir does not disable secure_file_priv
|
||||
//ctx.SetSessionVariable(ctx, "secure_file_priv", "")
|
||||
|
||||
// TODO: Check if it's wise to reuse the existing provider, rather than creating a new one
|
||||
a := analyzer.NewDefault(pro)
|
||||
|
||||
engine := sqle.New(a, &sqle.Config{
|
||||
IsReadOnly: true,
|
||||
})
|
||||
|
||||
schema, iter, _, err := engine.Query(mCtx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f, err := convertToDataFrame(mCtx, iter, schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.Name = name
|
||||
f.RefID = name
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
187
pkg/expr/sql/db_test.go
Normal file
187
pkg/expr/sql/db_test.go
Normal file
@ -0,0 +1,187 @@
|
||||
//go:build !arm
|
||||
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestQueryFrames(t *testing.T) {
|
||||
db := DB{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
query string
|
||||
input_frames []*data.Frame
|
||||
expected *data.Frame
|
||||
}{
|
||||
{
|
||||
name: "valid query with no input frames, one row one column",
|
||||
query: `SELECT '1' AS 'n';`,
|
||||
input_frames: []*data.Frame{},
|
||||
expected: data.NewFrame(
|
||||
"sqlExpressionRefId",
|
||||
data.NewField("n", nil, []string{"1"}),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "valid query with no input frames, one row two columns",
|
||||
query: `SELECT 'sam' AS 'name', 40 AS 'age';`,
|
||||
input_frames: []*data.Frame{},
|
||||
expected: data.NewFrame(
|
||||
"sqlExpressionRefId",
|
||||
data.NewField("name", nil, []string{"sam"}),
|
||||
data.NewField("age", nil, []int8{40}),
|
||||
),
|
||||
},
|
||||
{
|
||||
// TODO: Also ORDER BY to ensure the order is preserved
|
||||
name: "query all rows from single input frame",
|
||||
query: `SELECT * FROM inputFrameRefId LIMIT 1;`,
|
||||
input_frames: []*data.Frame{
|
||||
setRefID(data.NewFrame(
|
||||
"",
|
||||
//nolint:misspell
|
||||
data.NewField("OSS Projects with Typos", nil, []string{"Garfana", "Pormetheus"}),
|
||||
), "inputFrameRefId"),
|
||||
},
|
||||
expected: data.NewFrame(
|
||||
"sqlExpressionRefId",
|
||||
data.NewField("OSS Projects with Typos", nil, []string{"Garfana"}),
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
frame, err := db.QueryFrames(context.Background(), "sqlExpressionRefId", tt.query, tt.input_frames)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, frame.Fields)
|
||||
|
||||
require.Equal(t, tt.expected.Name, frame.RefID)
|
||||
require.Equal(t, len(tt.expected.Fields), len(frame.Fields))
|
||||
for i := range tt.expected.Fields {
|
||||
require.Equal(t, tt.expected.Fields[i].Name, frame.Fields[i].Name)
|
||||
require.Equal(t, tt.expected.Fields[i].At(0), frame.Fields[i].At(0))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryFramesInOut(t *testing.T) {
|
||||
frameA := &data.Frame{
|
||||
RefID: "a",
|
||||
Name: "a",
|
||||
Fields: []*data.Field{
|
||||
data.NewField("time", nil, []time.Time{time.Now(), time.Now()}),
|
||||
data.NewField("time_nullable", nil, []*time.Time{p(time.Now()), nil}),
|
||||
|
||||
data.NewField("string", nil, []string{"cat", "dog"}),
|
||||
data.NewField("null_nullable", nil, []*string{p("cat"), nil}),
|
||||
|
||||
data.NewField("float64", nil, []float64{1, 3}),
|
||||
data.NewField("float64_nullable", nil, []*float64{p(2.0), nil}),
|
||||
|
||||
data.NewField("int64", nil, []int64{1, 3}),
|
||||
data.NewField("int64_nullable", nil, []*int64{p(int64(2)), nil}),
|
||||
|
||||
data.NewField("bool", nil, []bool{true, false}),
|
||||
data.NewField("bool_nullable", nil, []*bool{p(true), nil}),
|
||||
},
|
||||
}
|
||||
|
||||
db := DB{}
|
||||
qry := `SELECT * from a`
|
||||
|
||||
resultFrame, err := db.QueryFrames(context.Background(), "a", qry, []*data.Frame{frameA})
|
||||
require.NoError(t, err)
|
||||
|
||||
if diff := cmp.Diff(frameA, resultFrame, data.FrameTestCompareOptions()...); diff != "" {
|
||||
require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryFramesNumericSelect(t *testing.T) {
|
||||
expectedFrame := &data.Frame{
|
||||
RefID: "a",
|
||||
Name: "a",
|
||||
Fields: []*data.Field{
|
||||
data.NewField("decimal", nil, []float64{2.35}),
|
||||
data.NewField("tinySigned", nil, []int8{-128}),
|
||||
data.NewField("smallSigned", nil, []int16{-32768}),
|
||||
data.NewField("mediumSigned", nil, []int32{-8388608}),
|
||||
data.NewField("intSigned", nil, []int32{-2147483648}),
|
||||
data.NewField("bigSigned", nil, []int64{-9223372036854775808}),
|
||||
data.NewField("tinyUnsigned", nil, []uint8{255}),
|
||||
data.NewField("smallUnsigned", nil, []uint16{65535}),
|
||||
data.NewField("mediumUnsigned", nil, []int32{16777215}),
|
||||
data.NewField("intUnsigned", nil, []uint32{4294967295}),
|
||||
data.NewField("bigUnsigned", nil, []uint64{18446744073709551615}),
|
||||
},
|
||||
}
|
||||
|
||||
db := DB{}
|
||||
qry := `SELECT 2.35 AS 'decimal',
|
||||
-128 AS 'tinySigned',
|
||||
-32768 AS 'smallSigned',
|
||||
-8388608 AS 'mediumSigned',
|
||||
-2147483648 AS 'intSigned',
|
||||
-9223372036854775808 AS 'bigSigned',
|
||||
255 AS 'tinyUnsigned',
|
||||
65535 AS 'smallUnsigned',
|
||||
16777215 AS 'mediumUnsigned',
|
||||
4294967295 AS 'intUnsigned',
|
||||
18446744073709551615 AS 'bigUnsigned'`
|
||||
|
||||
resultFrame, err := db.QueryFrames(context.Background(), "a", qry, []*data.Frame{})
|
||||
require.NoError(t, err)
|
||||
|
||||
if diff := cmp.Diff(expectedFrame, resultFrame, data.FrameTestCompareOptions()...); diff != "" {
|
||||
require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryFramesDateTimeSelect(t *testing.T) {
|
||||
t.Skip("need a fix in go-mysql-server, and then handle the datetime strings (or figure out why strings and not time.Time)")
|
||||
expectedFrame := &data.Frame{
|
||||
RefID: "a",
|
||||
Name: "a",
|
||||
Fields: []*data.Field{
|
||||
data.NewField("ts", nil, []time.Time{}),
|
||||
},
|
||||
}
|
||||
|
||||
db := DB{}
|
||||
|
||||
// It doesn't like the T in the time string
|
||||
qry := `SELECT str_to_date('2025-02-03T03:00:00','%Y-%m-%dT%H:%i:%s') as ts`
|
||||
|
||||
// This comes back as a string, which needs to be dealt with?
|
||||
//qry := `SELECT str_to_date('2025-02-03-03:00:00','%Y-%m-%d-%H:%i:%s') as ts`
|
||||
|
||||
// This is a datetime(6), need to deal with that as well
|
||||
//qry := `SELECT current_timestamp() as ts`
|
||||
|
||||
f, err := db.QueryFrames(context.Background(), "b", qry, []*data.Frame{})
|
||||
require.NoError(t, err)
|
||||
|
||||
if diff := cmp.Diff(expectedFrame, f, data.FrameTestCompareOptions()...); diff != "" {
|
||||
require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff)
|
||||
}
|
||||
}
|
||||
|
||||
// p is a utility for pointers from constants
|
||||
func p[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
func setRefID(f *data.Frame, refID string) *data.Frame {
|
||||
f.RefID = refID
|
||||
return f
|
||||
}
|
18
pkg/expr/sql/dummy_arm.go
Normal file
18
pkg/expr/sql/dummy_arm.go
Normal file
@ -0,0 +1,18 @@
|
||||
//go:build arm
|
||||
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
)
|
||||
|
||||
type DB struct{}
|
||||
|
||||
// Stub out the QueryFrames method for ARM builds
|
||||
// See github.com/dolthub/go-mysql-server/issues/2837
|
||||
func (db *DB) QueryFrames(_ context.Context, _, _ string, _ []*data.Frame) (*data.Frame, error) {
|
||||
return nil, fmt.Errorf("sql expressions not supported in arm")
|
||||
}
|
65
pkg/expr/sql/frame_db.go
Normal file
65
pkg/expr/sql/frame_db.go
Normal file
@ -0,0 +1,65 @@
|
||||
//go:build !arm
|
||||
|
||||
package sql
|
||||
|
||||
import (
|
||||
mysql "github.com/dolthub/go-mysql-server/sql"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
)
|
||||
|
||||
var dbName = "frames"
|
||||
|
||||
// FramesDBProvider is a go-mysql-server DatabaseProvider that provides access to a set of Frames.
|
||||
type FramesDBProvider struct {
|
||||
db mysql.Database
|
||||
}
|
||||
|
||||
func (p *FramesDBProvider) Database(_ *mysql.Context, _ string) (mysql.Database, error) {
|
||||
return p.db, nil
|
||||
}
|
||||
|
||||
func (p *FramesDBProvider) HasDatabase(_ *mysql.Context, _ string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *FramesDBProvider) AllDatabases(_ *mysql.Context) []mysql.Database {
|
||||
return []mysql.Database{p.db}
|
||||
}
|
||||
|
||||
// NewFramesDBProvider creates a new FramesDBProvider with the given set of Frames.
|
||||
func NewFramesDBProvider(frames data.Frames) mysql.DatabaseProvider {
|
||||
fMap := make(map[string]mysql.Table, len(frames))
|
||||
for _, frame := range frames {
|
||||
fMap[frame.RefID] = &FrameTable{Frame: frame}
|
||||
}
|
||||
return &FramesDBProvider{
|
||||
db: &framesDB{
|
||||
frames: fMap,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// framesDB is a go-mysql-server Database that provides access to a set of Frames.
|
||||
type framesDB struct {
|
||||
frames map[string]mysql.Table
|
||||
}
|
||||
|
||||
func (db *framesDB) GetTableInsensitive(_ *mysql.Context, tblName string) (mysql.Table, bool, error) {
|
||||
tbl, ok := mysql.GetTableInsensitive(tblName, db.frames)
|
||||
if !ok {
|
||||
return nil, false, nil
|
||||
}
|
||||
return tbl, ok, nil
|
||||
}
|
||||
|
||||
func (db *framesDB) GetTableNames(_ *mysql.Context) ([]string, error) {
|
||||
s := make([]string, 0, len(db.frames))
|
||||
for k := range db.frames {
|
||||
s = append(s, k)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (db *framesDB) Name() string {
|
||||
return dbName
|
||||
}
|
474
pkg/expr/sql/frame_db_conv.go
Normal file
474
pkg/expr/sql/frame_db_conv.go
Normal file
@ -0,0 +1,474 @@
|
||||
//go:build !arm
|
||||
|
||||
package sql
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
mysql "github.com/dolthub/go-mysql-server/sql"
|
||||
"github.com/dolthub/go-mysql-server/sql/types"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// TODO: Should this accept a row limit and converters, like sqlutil.FrameFromRows?
|
||||
func convertToDataFrame(ctx *mysql.Context, iter mysql.RowIter, schema mysql.Schema) (*data.Frame, error) {
|
||||
f := &data.Frame{}
|
||||
// Create fields based on the schema
|
||||
for _, col := range schema {
|
||||
fT, err := MySQLColToFieldType(col)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
field := data.NewFieldFromFieldType(fT, 0)
|
||||
field.Name = col.Name
|
||||
f.Fields = append(f.Fields, field)
|
||||
}
|
||||
|
||||
// Iterate through the rows and append data to fields
|
||||
for {
|
||||
row, err := iter.Next(ctx)
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading row: %v", err)
|
||||
}
|
||||
|
||||
for i, val := range row {
|
||||
v, err := fieldValFromRowVal(f.Fields[i].Type(), val)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unexpected type for column %s: %w", schema[i].Name, err)
|
||||
}
|
||||
f.Fields[i].Append(v)
|
||||
}
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// MySQLColToFieldType converts a MySQL column to a data.FieldType
|
||||
func MySQLColToFieldType(col *mysql.Column) (data.FieldType, error) {
|
||||
var fT data.FieldType
|
||||
|
||||
switch col.Type {
|
||||
case types.Int8:
|
||||
fT = data.FieldTypeInt8
|
||||
case types.Uint8:
|
||||
fT = data.FieldTypeUint8
|
||||
case types.Int16:
|
||||
fT = data.FieldTypeInt16
|
||||
case types.Uint16:
|
||||
fT = data.FieldTypeUint16
|
||||
case types.Int32:
|
||||
fT = data.FieldTypeInt32
|
||||
case types.Uint32:
|
||||
fT = data.FieldTypeUint32
|
||||
case types.Int64:
|
||||
fT = data.FieldTypeInt64
|
||||
case types.Uint64:
|
||||
fT = data.FieldTypeUint64
|
||||
case types.Float64:
|
||||
fT = data.FieldTypeFloat64
|
||||
// StringType represents all string types, including VARCHAR and BLOB.
|
||||
case types.Text, types.LongText:
|
||||
fT = data.FieldTypeString
|
||||
case types.Timestamp:
|
||||
fT = data.FieldTypeTime
|
||||
case types.Datetime:
|
||||
fT = data.FieldTypeTime
|
||||
case types.Boolean:
|
||||
fT = data.FieldTypeBool
|
||||
default:
|
||||
if types.IsDecimal(col.Type) {
|
||||
fT = data.FieldTypeFloat64
|
||||
} else {
|
||||
return fT, fmt.Errorf("unsupported type for column %s of type %v", col.Name, col.Type)
|
||||
}
|
||||
}
|
||||
|
||||
if col.Nullable {
|
||||
fT = fT.NullableType()
|
||||
}
|
||||
|
||||
return fT, nil
|
||||
}
|
||||
|
||||
// Helper function to convert data.FieldType to types.Type
|
||||
func convertDataType(fieldType data.FieldType) mysql.Type {
|
||||
switch fieldType {
|
||||
case data.FieldTypeInt8, data.FieldTypeNullableInt8:
|
||||
return types.Int8
|
||||
case data.FieldTypeUint8, data.FieldTypeNullableUint8:
|
||||
return types.Uint8
|
||||
case data.FieldTypeInt16, data.FieldTypeNullableInt16:
|
||||
return types.Int16
|
||||
case data.FieldTypeUint16, data.FieldTypeNullableUint16:
|
||||
return types.Uint16
|
||||
case data.FieldTypeInt32, data.FieldTypeNullableInt32:
|
||||
return types.Int32
|
||||
case data.FieldTypeUint32, data.FieldTypeNullableUint32:
|
||||
return types.Uint32
|
||||
case data.FieldTypeInt64, data.FieldTypeNullableInt64:
|
||||
return types.Int64
|
||||
case data.FieldTypeUint64, data.FieldTypeNullableUint64:
|
||||
return types.Uint64
|
||||
case data.FieldTypeFloat32, data.FieldTypeNullableFloat32:
|
||||
return types.Float32
|
||||
case data.FieldTypeFloat64, data.FieldTypeNullableFloat64:
|
||||
return types.Float64
|
||||
case data.FieldTypeString, data.FieldTypeNullableString:
|
||||
return types.Text
|
||||
case data.FieldTypeBool, data.FieldTypeNullableBool:
|
||||
return types.Boolean
|
||||
case data.FieldTypeTime, data.FieldTypeNullableTime:
|
||||
return types.Timestamp
|
||||
default:
|
||||
fmt.Printf("------- Unsupported field type: %v", fieldType)
|
||||
return types.JSON
|
||||
}
|
||||
}
|
||||
|
||||
// fieldValFromRowVal converts a go-mysql-server row value to a data.field value
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func fieldValFromRowVal(fieldType data.FieldType, val interface{}) (interface{}, error) {
|
||||
// the input val may be nil, it also may not be a pointer even if the fieldtype is a nullable pointer type
|
||||
if val == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch fieldType {
|
||||
// ----------------------------
|
||||
// Int8 / Nullable Int8
|
||||
// ----------------------------
|
||||
case data.FieldTypeInt8:
|
||||
v, ok := val.(int8)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int8", val, val)
|
||||
}
|
||||
return v, nil
|
||||
|
||||
case data.FieldTypeNullableInt8:
|
||||
vP, ok := val.(*int8)
|
||||
if ok {
|
||||
return vP, nil
|
||||
}
|
||||
v, ok := val.(int8)
|
||||
if ok {
|
||||
return &v, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int8 or *int8", val, val)
|
||||
|
||||
// ----------------------------
|
||||
// Uint8 / Nullable Uint8
|
||||
// ----------------------------
|
||||
case data.FieldTypeUint8:
|
||||
v, ok := val.(uint8)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint8", val, val)
|
||||
}
|
||||
return v, nil
|
||||
|
||||
case data.FieldTypeNullableUint8:
|
||||
vP, ok := val.(*uint8)
|
||||
if ok {
|
||||
return vP, nil
|
||||
}
|
||||
v, ok := val.(uint8)
|
||||
if ok {
|
||||
return &v, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint8 or *uint8", val, val)
|
||||
|
||||
// ----------------------------
|
||||
// Int16 / Nullable Int16
|
||||
// ----------------------------
|
||||
case data.FieldTypeInt16:
|
||||
v, ok := val.(int16)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int16", val, val)
|
||||
}
|
||||
return v, nil
|
||||
|
||||
case data.FieldTypeNullableInt16:
|
||||
vP, ok := val.(*int16)
|
||||
if ok {
|
||||
return vP, nil
|
||||
}
|
||||
v, ok := val.(int16)
|
||||
if ok {
|
||||
return &v, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int16 or *int16", val, val)
|
||||
|
||||
// ----------------------------
|
||||
// Uint16 / Nullable Uint16
|
||||
// ----------------------------
|
||||
case data.FieldTypeUint16:
|
||||
v, ok := val.(uint16)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint16", val, val)
|
||||
}
|
||||
return v, nil
|
||||
|
||||
case data.FieldTypeNullableUint16:
|
||||
vP, ok := val.(*uint16)
|
||||
if ok {
|
||||
return vP, nil
|
||||
}
|
||||
v, ok := val.(uint16)
|
||||
if ok {
|
||||
return &v, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint16 or *uint16", val, val)
|
||||
|
||||
// ----------------------------
|
||||
// Int32 / Nullable Int32
|
||||
// ----------------------------
|
||||
case data.FieldTypeInt32:
|
||||
v, ok := val.(int32)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int32", val, val)
|
||||
}
|
||||
return v, nil
|
||||
|
||||
case data.FieldTypeNullableInt32:
|
||||
vP, ok := val.(*int32)
|
||||
if ok {
|
||||
return vP, nil
|
||||
}
|
||||
v, ok := val.(int32)
|
||||
if ok {
|
||||
return &v, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int32 or *int32", val, val)
|
||||
|
||||
// ----------------------------
|
||||
// Uint32 / Nullable Uint32
|
||||
// ----------------------------
|
||||
case data.FieldTypeUint32:
|
||||
v, ok := val.(uint32)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint32", val, val)
|
||||
}
|
||||
return v, nil
|
||||
|
||||
case data.FieldTypeNullableUint32:
|
||||
vP, ok := val.(*uint32)
|
||||
if ok {
|
||||
return vP, nil
|
||||
}
|
||||
v, ok := val.(uint32)
|
||||
if ok {
|
||||
return &v, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint32 or *uint32", val, val)
|
||||
|
||||
// ----------------------------
|
||||
// Int64 / Nullable Int64
|
||||
// ----------------------------
|
||||
case data.FieldTypeInt64:
|
||||
v, ok := val.(int64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int64", val, val)
|
||||
}
|
||||
return v, nil
|
||||
|
||||
case data.FieldTypeNullableInt64:
|
||||
vP, ok := val.(*int64)
|
||||
if ok {
|
||||
return vP, nil
|
||||
}
|
||||
v, ok := val.(int64)
|
||||
if ok {
|
||||
return &v, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected int64 or *int64", val, val)
|
||||
|
||||
// ----------------------------
|
||||
// Uint64 / Nullable Uint64
|
||||
// ----------------------------
|
||||
case data.FieldTypeUint64:
|
||||
v, ok := val.(uint64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint64", val, val)
|
||||
}
|
||||
return v, nil
|
||||
|
||||
case data.FieldTypeNullableUint64:
|
||||
vP, ok := val.(*uint64)
|
||||
if ok {
|
||||
return vP, nil
|
||||
}
|
||||
v, ok := val.(uint64)
|
||||
if ok {
|
||||
return &v, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected uint64 or *uint64", val, val)
|
||||
|
||||
// ----------------------------
|
||||
// Float64 / Nullable Float64
|
||||
// ----------------------------
|
||||
case data.FieldTypeFloat64:
|
||||
// Accept float64 or decimal.Decimal, convert decimal.Decimal -> float64
|
||||
if v, ok := val.(float64); ok {
|
||||
return v, nil
|
||||
}
|
||||
if d, ok := val.(decimal.Decimal); ok {
|
||||
return d.InexactFloat64(), nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected float64 or decimal.Decimal", val, val)
|
||||
|
||||
case data.FieldTypeNullableFloat64:
|
||||
// Possibly already *float64
|
||||
if vP, ok := val.(*float64); ok {
|
||||
return vP, nil
|
||||
}
|
||||
// Possibly float64
|
||||
if v, ok := val.(float64); ok {
|
||||
return &v, nil
|
||||
}
|
||||
// Possibly decimal.Decimal
|
||||
if d, ok := val.(decimal.Decimal); ok {
|
||||
f := d.InexactFloat64()
|
||||
return &f, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected float64, *float64, or decimal.Decimal", val, val)
|
||||
|
||||
// ----------------------------
|
||||
// Time / Nullable Time
|
||||
// ----------------------------
|
||||
case data.FieldTypeTime:
|
||||
v, ok := val.(time.Time)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected time.Time", val, val)
|
||||
}
|
||||
return v, nil
|
||||
|
||||
case data.FieldTypeNullableTime:
|
||||
vP, ok := val.(*time.Time)
|
||||
if ok {
|
||||
return vP, nil
|
||||
}
|
||||
v, ok := val.(time.Time)
|
||||
if ok {
|
||||
return &v, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected time.Time or *time.Time", val, val)
|
||||
|
||||
// ----------------------------
|
||||
// String / Nullable String
|
||||
// ----------------------------
|
||||
case data.FieldTypeString:
|
||||
v, ok := val.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected string", val, val)
|
||||
}
|
||||
return v, nil
|
||||
|
||||
case data.FieldTypeNullableString:
|
||||
vP, ok := val.(*string)
|
||||
if ok {
|
||||
return vP, nil
|
||||
}
|
||||
v, ok := val.(string)
|
||||
if ok {
|
||||
return &v, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected string or *string", val, val)
|
||||
|
||||
// ----------------------------
|
||||
// Bool / Nullable Bool
|
||||
// ----------------------------
|
||||
case data.FieldTypeBool:
|
||||
v, ok := val.(bool)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected bool", val, val)
|
||||
}
|
||||
return v, nil
|
||||
|
||||
case data.FieldTypeNullableBool:
|
||||
vP, ok := val.(*bool)
|
||||
if ok {
|
||||
return vP, nil
|
||||
}
|
||||
v, ok := val.(bool)
|
||||
if ok {
|
||||
return &v, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected value type for interface %v of type %T, expected bool or *bool", val, val)
|
||||
|
||||
// ----------------------------
|
||||
// Fallback / Unsupported
|
||||
// ----------------------------
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported field type %s for val %v", fieldType, val)
|
||||
}
|
||||
}
|
||||
|
||||
// Is the field nilAt the index. Can panic if out of range.
|
||||
// TODO: Maybe this should be a method on data.Field?
|
||||
func nilAt(field data.Field, at int) bool {
|
||||
if !field.Nullable() {
|
||||
return false
|
||||
}
|
||||
|
||||
switch field.Type() {
|
||||
case data.FieldTypeNullableInt8:
|
||||
v := field.At(at).(*int8)
|
||||
return v == nil
|
||||
|
||||
case data.FieldTypeNullableUint8:
|
||||
v := field.At(at).(*uint8)
|
||||
return v == nil
|
||||
|
||||
case data.FieldTypeNullableInt16:
|
||||
v := field.At(at).(*int16)
|
||||
return v == nil
|
||||
|
||||
case data.FieldTypeNullableUint16:
|
||||
v := field.At(at).(*uint16)
|
||||
return v == nil
|
||||
|
||||
case data.FieldTypeNullableInt32:
|
||||
v := field.At(at).(*int32)
|
||||
return v == nil
|
||||
|
||||
case data.FieldTypeNullableUint32:
|
||||
v := field.At(at).(*uint32)
|
||||
return v == nil
|
||||
|
||||
case data.FieldTypeNullableInt64:
|
||||
v := field.At(at).(*int64)
|
||||
return v == nil
|
||||
|
||||
case data.FieldTypeNullableUint64:
|
||||
v := field.At(at).(*uint64)
|
||||
return v == nil
|
||||
|
||||
case data.FieldTypeNullableFloat64:
|
||||
v := field.At(at).(*float64)
|
||||
return v == nil
|
||||
|
||||
case data.FieldTypeNullableString:
|
||||
v := field.At(at).(*string)
|
||||
return v == nil
|
||||
|
||||
case data.FieldTypeNullableTime:
|
||||
v := field.At(at).(*time.Time)
|
||||
return v == nil
|
||||
|
||||
case data.FieldTypeNullableBool:
|
||||
v := field.At(at).(*bool)
|
||||
return v == nil
|
||||
|
||||
default:
|
||||
// Either it's not a nullable type or it's unsupported
|
||||
return false
|
||||
}
|
||||
}
|
126
pkg/expr/sql/frame_table.go
Normal file
126
pkg/expr/sql/frame_table.go
Normal file
@ -0,0 +1,126 @@
|
||||
//go:build !arm
|
||||
|
||||
package sql
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
mysql "github.com/dolthub/go-mysql-server/sql"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
)
|
||||
|
||||
// FrameTable fulfills the mysql.Table interface for a data.Frame.
|
||||
type FrameTable struct {
|
||||
Frame *data.Frame
|
||||
schema mysql.Schema
|
||||
}
|
||||
|
||||
// Name implements the sql.Nameable interface
|
||||
func (ft *FrameTable) Name() string {
|
||||
return ft.Frame.RefID
|
||||
}
|
||||
|
||||
// String implements the fmt.Stringer interface
|
||||
func (ft *FrameTable) String() string {
|
||||
return ft.Name()
|
||||
}
|
||||
|
||||
func schemaFromFrame(frame *data.Frame) mysql.Schema {
|
||||
schema := make(mysql.Schema, len(frame.Fields))
|
||||
|
||||
for i, field := range frame.Fields {
|
||||
schema[i] = &mysql.Column{
|
||||
Name: field.Name,
|
||||
Type: convertDataType(field.Type()),
|
||||
Nullable: field.Type().Nullable(),
|
||||
Source: strings.ToLower(frame.RefID),
|
||||
}
|
||||
}
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
// Schema implements the mysql.Table interface
|
||||
func (ft *FrameTable) Schema() mysql.Schema {
|
||||
if ft.schema == nil {
|
||||
ft.schema = schemaFromFrame(ft.Frame)
|
||||
}
|
||||
return ft.schema
|
||||
}
|
||||
|
||||
// Collation implements the mysql.Table interface
|
||||
func (ft *FrameTable) Collation() mysql.CollationID {
|
||||
return mysql.Collation_Unspecified
|
||||
}
|
||||
|
||||
// Partitions implements the mysql.Table interface
|
||||
func (ft *FrameTable) Partitions(ctx *mysql.Context) (mysql.PartitionIter, error) {
|
||||
return &noopPartitionIter{}, nil
|
||||
}
|
||||
|
||||
// PartitionRows implements the mysql.Table interface
|
||||
func (ft *FrameTable) PartitionRows(ctx *mysql.Context, _ mysql.Partition) (mysql.RowIter, error) {
|
||||
return &rowIter{ft: ft, row: 0}, nil
|
||||
}
|
||||
|
||||
type rowIter struct {
|
||||
ft *FrameTable
|
||||
row int
|
||||
}
|
||||
|
||||
func (ri *rowIter) Next(_ *mysql.Context) (mysql.Row, error) {
|
||||
// We assume each field in the Frame has the same number of rows.
|
||||
numRows := 0
|
||||
if len(ri.ft.Frame.Fields) > 0 {
|
||||
numRows = ri.ft.Frame.Fields[0].Len()
|
||||
}
|
||||
|
||||
// If we've already exhausted all rows, return EOF
|
||||
if ri.row >= numRows {
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
// Construct a Row (which is []interface{} under the hood) by pulling
|
||||
// the value from each column at the current row index.
|
||||
row := make(mysql.Row, len(ri.ft.Frame.Fields))
|
||||
for colIndex, field := range ri.ft.Frame.Fields {
|
||||
if nilAt(*field, ri.row) {
|
||||
continue
|
||||
}
|
||||
row[colIndex], _ = field.ConcreteAt(ri.row)
|
||||
}
|
||||
|
||||
ri.row++
|
||||
return row, nil
|
||||
}
|
||||
|
||||
// Close implements the mysql.RowIter interface.
|
||||
// In this no-op example, there isn't anything to do here.
|
||||
func (ri *rowIter) Close(*mysql.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type noopPartitionIter struct {
|
||||
done bool
|
||||
}
|
||||
|
||||
func (i *noopPartitionIter) Next(*mysql.Context) (mysql.Partition, error) {
|
||||
if !i.done {
|
||||
i.done = true
|
||||
return noopParition, nil
|
||||
}
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
func (i *noopPartitionIter) Close(*mysql.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var noopParition = partition(nil)
|
||||
|
||||
type partition []byte
|
||||
|
||||
func (p partition) Key() []byte {
|
||||
return p
|
||||
}
|
@ -1,89 +1,62 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/dolthub/vitess/go/vt/sqlparser"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/jeremywohl/flatten"
|
||||
)
|
||||
|
||||
const (
|
||||
TABLE_NAME = "table_name"
|
||||
ERROR = ".error"
|
||||
ERROR_MESSAGE = ".error_message"
|
||||
)
|
||||
|
||||
var logger = log.New("sql_expr")
|
||||
|
||||
// TablesList returns a list of tables for the sql statement
|
||||
func TablesList(rawSQL string) ([]string, error) {
|
||||
db := NewInMemoryDB()
|
||||
rawSQL = strings.Replace(rawSQL, "'", "''", -1)
|
||||
cmd := fmt.Sprintf("SELECT json_serialize_sql('%s')", rawSQL)
|
||||
ret, err := db.RunCommands([]string{cmd})
|
||||
stmt, err := sqlparser.Parse(rawSQL)
|
||||
if err != nil {
|
||||
logger.Error("error serializing sql", "error", err.Error(), "sql", rawSQL, "cmd", cmd)
|
||||
return nil, fmt.Errorf("error serializing sql: %s", err.Error())
|
||||
logger.Error("error parsing sql: %s", err.Error(), "sql", rawSQL)
|
||||
return nil, fmt.Errorf("error parsing sql: %s", err.Error())
|
||||
}
|
||||
|
||||
ast := []map[string]any{}
|
||||
err = json.Unmarshal([]byte(ret), &ast)
|
||||
if err != nil {
|
||||
logger.Error("error converting json sql to ast", "error", err.Error(), "ret", ret)
|
||||
return nil, fmt.Errorf("error converting json to ast: %s", err.Error())
|
||||
}
|
||||
tables := make(map[string]struct{})
|
||||
|
||||
return tablesFromAST(ast)
|
||||
}
|
||||
|
||||
// tablesFromAST returns a list of tables from the ast
|
||||
func tablesFromAST(ast []map[string]any) ([]string, error) {
|
||||
flat, err := flatten.Flatten(ast[0], "", flatten.DotStyle)
|
||||
if err != nil {
|
||||
logger.Error("error flattening ast", "error", err.Error(), "ast", ast)
|
||||
return nil, fmt.Errorf("error flattening ast: %s", err.Error())
|
||||
}
|
||||
|
||||
tables := []string{}
|
||||
for k, v := range flat {
|
||||
if strings.HasSuffix(k, ERROR) {
|
||||
v, ok := v.(bool)
|
||||
if ok && v {
|
||||
logger.Error("error in sql", "error", k)
|
||||
return nil, astError(k, flat)
|
||||
walkSubtree := func(node sqlparser.SQLNode) error {
|
||||
err = sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) {
|
||||
switch v := node.(type) {
|
||||
case *sqlparser.AliasedTableExpr:
|
||||
if tableName, ok := v.Expr.(sqlparser.TableName); ok {
|
||||
tables[tableName.Name.String()] = struct{}{}
|
||||
}
|
||||
case *sqlparser.TableName:
|
||||
tables[v.Name.String()] = struct{}{}
|
||||
}
|
||||
return true, nil
|
||||
}, node)
|
||||
|
||||
if err != nil {
|
||||
logger.Error("error walking sql", "error", err, "node", node)
|
||||
return fmt.Errorf("failed to parse SQL expression: %w", err)
|
||||
}
|
||||
if strings.Contains(k, TABLE_NAME) {
|
||||
table, ok := v.(string)
|
||||
if ok && !existsInList(table, tables) {
|
||||
tables = append(tables, v.(string))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := walkSubtree(stmt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(tables))
|
||||
for table := range tables {
|
||||
// Remove 'dual' table if it exists
|
||||
// This is a special table in MySQL that always returns a single row with a single column
|
||||
// See: https://dev.mysql.com/doc/refman/5.7/en/select.html#:~:text=You%20are%20permitted%20to%20specify%20DUAL%20as%20a%20dummy%20table%20name%20in%20situations%20where%20no%20tables%20are%20referenced
|
||||
if table != "dual" {
|
||||
result = append(result, table)
|
||||
}
|
||||
}
|
||||
sort.Strings(tables)
|
||||
|
||||
sort.Strings(result)
|
||||
|
||||
logger.Debug("tables found in sql", "tables", tables)
|
||||
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
func astError(k string, flat map[string]any) error {
|
||||
key := strings.Replace(k, ERROR, "", 1)
|
||||
message, ok := flat[key+ERROR_MESSAGE]
|
||||
if !ok {
|
||||
message = "unknown error in sql"
|
||||
}
|
||||
return fmt.Errorf("error in sql: %s", message)
|
||||
}
|
||||
|
||||
func existsInList(table string, list []string) bool {
|
||||
for _, t := range list {
|
||||
if t == table {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return result, nil
|
||||
}
|
||||
|
136
pkg/expr/sql/parser_allow.go
Normal file
136
pkg/expr/sql/parser_allow.go
Normal file
@ -0,0 +1,136 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/dolthub/vitess/go/vt/sqlparser"
|
||||
)
|
||||
|
||||
// AllowQuery parses the query and checks it against an allow list of allowed SQL nodes
|
||||
// and functions.
|
||||
func AllowQuery(rawSQL string) (bool, error) {
|
||||
s, err := sqlparser.Parse(rawSQL)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error parsing sql: %s", err.Error())
|
||||
}
|
||||
|
||||
walkSubtree := func(node sqlparser.SQLNode) error {
|
||||
err := sqlparser.Walk(func(node sqlparser.SQLNode) (bool, error) {
|
||||
if !allowedNode(node) {
|
||||
if fT, ok := node.(*sqlparser.FuncExpr); ok {
|
||||
return false, fmt.Errorf("blocked function %s - not supported in queries", fT.Name)
|
||||
}
|
||||
return false, fmt.Errorf("blocked node %T - not supported in queries", node)
|
||||
}
|
||||
return true, nil
|
||||
}, node)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse SQL expression: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := walkSubtree(s); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// nolint:gocyclo,nakedret
|
||||
func allowedNode(node sqlparser.SQLNode) (b bool) {
|
||||
b = true // so don't have to return true in every case but default
|
||||
|
||||
switch v := node.(type) {
|
||||
case *sqlparser.FuncExpr:
|
||||
return allowedFunction(v)
|
||||
|
||||
case *sqlparser.AsOf:
|
||||
return
|
||||
|
||||
case *sqlparser.AliasedExpr, *sqlparser.AliasedTableExpr:
|
||||
return
|
||||
|
||||
case *sqlparser.BinaryExpr:
|
||||
return
|
||||
|
||||
case sqlparser.ColIdent, *sqlparser.ColName, sqlparser.Columns:
|
||||
return
|
||||
|
||||
case sqlparser.Comments: // TODO: understand why some are pointer vs not
|
||||
return
|
||||
|
||||
case *sqlparser.CommonTableExpr:
|
||||
return
|
||||
|
||||
case *sqlparser.ComparisonExpr:
|
||||
return
|
||||
|
||||
case *sqlparser.ConvertExpr:
|
||||
return
|
||||
|
||||
case sqlparser.GroupBy:
|
||||
return
|
||||
|
||||
case *sqlparser.IndexHints:
|
||||
return
|
||||
|
||||
case *sqlparser.Into:
|
||||
return
|
||||
|
||||
case *sqlparser.JoinTableExpr, sqlparser.JoinCondition:
|
||||
return
|
||||
|
||||
case *sqlparser.Select, sqlparser.SelectExprs:
|
||||
return
|
||||
|
||||
case *sqlparser.StarExpr:
|
||||
return
|
||||
|
||||
case *sqlparser.SQLVal:
|
||||
return
|
||||
|
||||
case *sqlparser.Limit:
|
||||
return
|
||||
|
||||
case *sqlparser.Order, sqlparser.OrderBy:
|
||||
return
|
||||
|
||||
case *sqlparser.Over:
|
||||
return
|
||||
|
||||
case *sqlparser.Subquery:
|
||||
return
|
||||
|
||||
case sqlparser.TableName, sqlparser.TableExprs, sqlparser.TableIdent:
|
||||
return
|
||||
|
||||
case *sqlparser.With:
|
||||
return
|
||||
|
||||
case *sqlparser.Where:
|
||||
return
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// nolint:gocyclo,nakedret
|
||||
func allowedFunction(f *sqlparser.FuncExpr) (b bool) {
|
||||
b = true // so don't have to return true in every case but default
|
||||
|
||||
switch strings.ToLower(f.Name.String()) {
|
||||
case "sum", "avg", "count", "min", "max":
|
||||
return
|
||||
|
||||
case "coalesce":
|
||||
return
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
80
pkg/expr/sql/parser_allow_test.go
Normal file
80
pkg/expr/sql/parser_allow_test.go
Normal file
@ -0,0 +1,80 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAllowQuery(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
q string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "a big catch all for now",
|
||||
q: example_metrics_query,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := AllowQuery(tc.q)
|
||||
if tc.err != nil {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var example_metrics_query = `WITH
|
||||
metrics_this_month AS (
|
||||
SELECT
|
||||
Month,
|
||||
namespace,
|
||||
sum(BillableSeries) AS billable_series
|
||||
FROM metrics
|
||||
WHERE
|
||||
Month = "2024-11"
|
||||
GROUP BY
|
||||
Month,
|
||||
namespace
|
||||
ORDER BY billable_series DESC
|
||||
),
|
||||
total_metrics AS (
|
||||
SELECT SUM(billable_series) AS metrics_billable_series_total
|
||||
FROM metrics_this_month
|
||||
),
|
||||
total_traces AS (
|
||||
-- "usage" is a reserved keyword in MySQL. Quote it with backticks.
|
||||
SELECT SUM(value) AS traces_usage_total
|
||||
FROM traces
|
||||
),
|
||||
usage_by_team AS (
|
||||
SELECT
|
||||
COALESCE(teams.team, 'unaccounted') AS team,
|
||||
1 + 0 AS team_count,
|
||||
-- Metrics
|
||||
SUM(COALESCE(metrics_this_month.billable_series, 0)) AS metrics_billable_series,
|
||||
-- Traces
|
||||
SUM(COALESCE(traces.value, 0)) AS traces_usage
|
||||
-- FROM teams
|
||||
-- FULL OUTER JOIN metrics_this_month
|
||||
FROM metrics_this_month
|
||||
FULL OUTER JOIN teams
|
||||
ON teams.namespace = metrics_this_month.namespace
|
||||
FULL OUTER JOIN traces
|
||||
ON teams.namespace = traces.namespace
|
||||
GROUP BY
|
||||
-- COALESCE(teams.team, 'unaccounted')
|
||||
teams.team
|
||||
ORDER BY metrics_billable_series DESC
|
||||
)
|
||||
|
||||
SELECT *
|
||||
FROM usage_by_team
|
||||
CROSS JOIN total_metrics
|
||||
CROSS JOIN total_traces`
|
@ -3,214 +3,131 @@ package sql
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
t.Skip()
|
||||
sql := "select * from foo"
|
||||
tables, err := TablesList((sql))
|
||||
assert.Nil(t, err)
|
||||
func TestTablesList(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
sql string
|
||||
expected []string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "simple select",
|
||||
sql: "select * from foo",
|
||||
expected: []string{"foo"},
|
||||
},
|
||||
{
|
||||
name: "select with comma",
|
||||
sql: "select * from foo,bar",
|
||||
expected: []string{"bar", "foo"},
|
||||
},
|
||||
{
|
||||
name: "select with multiple commas",
|
||||
sql: "select * from foo,bar,baz",
|
||||
expected: []string{"bar", "baz", "foo"},
|
||||
},
|
||||
{
|
||||
name: "no table",
|
||||
sql: "select 1 as 'n'",
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "json array",
|
||||
sql: "SELECT JSON_ARRAY(1, 2, 3) AS array_value",
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "json extract",
|
||||
sql: "SELECT JSON_EXTRACT(JSON_ARRAY(1, 2, 3), '$[0]') AS first_element;",
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "json int array",
|
||||
sql: "SELECT JSON_ARRAY(3, 2, 1) AS int_array;",
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "subquery",
|
||||
sql: "select * from (select * from people limit 1) AS subquery",
|
||||
expected: []string{"people"},
|
||||
},
|
||||
{
|
||||
name: "join",
|
||||
sql: `select * from A
|
||||
JOIN B ON A.name = B.name
|
||||
LIMIT 10`,
|
||||
expected: []string{"A", "B"},
|
||||
},
|
||||
{
|
||||
name: "right join",
|
||||
sql: `select * from A
|
||||
RIGHT JOIN B ON A.name = B.name
|
||||
LIMIT 10`,
|
||||
expected: []string{"A", "B"},
|
||||
},
|
||||
{
|
||||
name: "alias with join",
|
||||
sql: `select * from A as X
|
||||
RIGHT JOIN B ON A.name = X.name
|
||||
LIMIT 10`,
|
||||
expected: []string{"A", "B"},
|
||||
},
|
||||
{
|
||||
name: "alias",
|
||||
sql: "select * from A as X LIMIT 10",
|
||||
expected: []string{"A"},
|
||||
},
|
||||
{
|
||||
name: "error case",
|
||||
sql: "select * from zzz aaa zzz",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "parens",
|
||||
sql: `SELECT t1.Col1,
|
||||
t2.Col1,
|
||||
t3.Col1
|
||||
FROM table1 AS t1
|
||||
LEFT JOIN (
|
||||
table2 AS t2
|
||||
INNER JOIN table3 AS t3 ON t3.Col1 = t2.Col1
|
||||
) ON t2.Col1 = t1.Col1;`,
|
||||
expected: []string{"table1", "table2", "table3"},
|
||||
},
|
||||
{
|
||||
name: "with clause",
|
||||
sql: `WITH top_products AS (
|
||||
SELECT * FROM products
|
||||
ORDER BY price DESC
|
||||
LIMIT 5
|
||||
)
|
||||
SELECT name, price
|
||||
FROM top_products;`,
|
||||
expected: []string{"products", "top_products"},
|
||||
},
|
||||
{
|
||||
name: "with quote",
|
||||
sql: "select *,'junk' from foo",
|
||||
expected: []string{"foo"},
|
||||
},
|
||||
{
|
||||
name: "with quote 2",
|
||||
sql: "SELECT json_serialize_sql('SELECT 1')",
|
||||
expected: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, "foo", tables[0])
|
||||
}
|
||||
|
||||
func TestParseWithComma(t *testing.T) {
|
||||
t.Skip()
|
||||
sql := "select * from foo,bar"
|
||||
tables, err := TablesList((sql))
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, "bar", tables[0])
|
||||
assert.Equal(t, "foo", tables[1])
|
||||
}
|
||||
|
||||
func TestParseWithCommas(t *testing.T) {
|
||||
t.Skip()
|
||||
sql := "select * from foo,bar,baz"
|
||||
tables, err := TablesList((sql))
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, "bar", tables[0])
|
||||
assert.Equal(t, "baz", tables[1])
|
||||
assert.Equal(t, "foo", tables[2])
|
||||
}
|
||||
|
||||
func TestArray(t *testing.T) {
|
||||
t.Skip()
|
||||
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) {
|
||||
t.Skip()
|
||||
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) {
|
||||
t.Skip()
|
||||
sql := "SELECT [3, 2, 1]::INT[3];"
|
||||
tables, err := TablesList((sql))
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 0, len(tables))
|
||||
}
|
||||
|
||||
func TestParseSubquery(t *testing.T) {
|
||||
t.Skip()
|
||||
sql := "select * from (select * from people limit 1)"
|
||||
tables, err := TablesList((sql))
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 1, len(tables))
|
||||
assert.Equal(t, "people", tables[0])
|
||||
}
|
||||
|
||||
func TestJoin(t *testing.T) {
|
||||
t.Skip()
|
||||
sql := `select * from A
|
||||
JOIN B ON A.name = B.name
|
||||
LIMIT 10`
|
||||
tables, err := TablesList((sql))
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 2, len(tables))
|
||||
assert.Equal(t, "A", tables[0])
|
||||
assert.Equal(t, "B", tables[1])
|
||||
}
|
||||
|
||||
func TestRightJoin(t *testing.T) {
|
||||
t.Skip()
|
||||
sql := `select * from A
|
||||
RIGHT JOIN B ON A.name = B.name
|
||||
LIMIT 10`
|
||||
tables, err := TablesList((sql))
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 2, len(tables))
|
||||
assert.Equal(t, "A", tables[0])
|
||||
assert.Equal(t, "B", tables[1])
|
||||
}
|
||||
|
||||
func TestAliasWithJoin(t *testing.T) {
|
||||
t.Skip()
|
||||
sql := `select * from A as X
|
||||
RIGHT JOIN B ON A.name = X.name
|
||||
LIMIT 10`
|
||||
tables, err := TablesList((sql))
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 2, len(tables))
|
||||
assert.Equal(t, "A", tables[0])
|
||||
assert.Equal(t, "B", tables[1])
|
||||
}
|
||||
|
||||
func TestAlias(t *testing.T) {
|
||||
t.Skip()
|
||||
sql := `select * from A as X LIMIT 10`
|
||||
tables, err := TablesList((sql))
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 1, len(tables))
|
||||
assert.Equal(t, "A", tables[0])
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
t.Skip()
|
||||
sql := `select * from zzz aaa zzz`
|
||||
_, err := TablesList((sql))
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestParens(t *testing.T) {
|
||||
t.Skip()
|
||||
sql := `SELECT t1.Col1,
|
||||
t2.Col1,
|
||||
t3.Col1
|
||||
FROM table1 AS t1
|
||||
LEFT JOIN (
|
||||
table2 AS t2
|
||||
INNER JOIN table3 AS t3 ON t3.Col1 = t2.Col1
|
||||
) ON t2.Col1 = t1.Col1;`
|
||||
tables, err := TablesList((sql))
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 3, len(tables))
|
||||
assert.Equal(t, "table1", tables[0])
|
||||
assert.Equal(t, "table2", tables[1])
|
||||
assert.Equal(t, "table3", tables[2])
|
||||
}
|
||||
|
||||
func TestWith(t *testing.T) {
|
||||
t.Skip()
|
||||
sql := `WITH
|
||||
|
||||
current_month AS (
|
||||
select
|
||||
distinct "Month(ISO)" as mth
|
||||
from A
|
||||
ORDER BY mth DESC
|
||||
LIMIT 1
|
||||
),
|
||||
|
||||
last_month_bill AS (
|
||||
select
|
||||
CAST (
|
||||
sum(
|
||||
CAST(BillableSeries AS INTEGER)
|
||||
) AS INTEGER
|
||||
) AS BillableSeries,
|
||||
"Month(ISO)",
|
||||
label_namespace
|
||||
-- , B.activeseries_count
|
||||
from A
|
||||
JOIN current_month
|
||||
ON current_month.mth = A."Month(ISO)"
|
||||
JOIN B
|
||||
ON B.namespace = A.label_namespace
|
||||
GROUP BY
|
||||
label_namespace,
|
||||
"Month(ISO)"
|
||||
ORDER BY BillableSeries DESC
|
||||
)
|
||||
|
||||
SELECT
|
||||
last_month_bill.*,
|
||||
BEE.activeseries_count
|
||||
FROM last_month_bill
|
||||
JOIN BEE
|
||||
ON BEE.namespace = last_month_bill.label_namespace`
|
||||
|
||||
tables, err := TablesList((sql))
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 5, len(tables))
|
||||
assert.Equal(t, "A", tables[0])
|
||||
assert.Equal(t, "B", tables[1])
|
||||
assert.Equal(t, "BEE", tables[2])
|
||||
}
|
||||
|
||||
func TestWithQuote(t *testing.T) {
|
||||
t.Skip()
|
||||
sql := "select *,'junk' from foo"
|
||||
tables, err := TablesList((sql))
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, "foo", tables[0])
|
||||
}
|
||||
|
||||
func TestWithQuote2(t *testing.T) {
|
||||
t.Skip()
|
||||
sql := "SELECT json_serialize_sql('SELECT 1')"
|
||||
tables, err := TablesList((sql))
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 0, len(tables))
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tables, err := TablesList(tc.sql)
|
||||
if tc.expectError {
|
||||
require.NotNil(t, err, "expected error for SQL: %s", tc.sql)
|
||||
} else {
|
||||
require.Nil(t, err, "unexpected error for SQL: %s", tc.sql)
|
||||
require.Equal(t, tc.expected, tables, "mismatched tables for SQL: %s", tc.sql)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -93,11 +93,10 @@ func (gr *SQLCommand) Execute(ctx context.Context, now time.Time, vars mathexp.V
|
||||
|
||||
rsp := mathexp.Results{}
|
||||
|
||||
db := sql.NewInMemoryDB()
|
||||
var frame = &data.Frame{}
|
||||
db := sql.DB{}
|
||||
|
||||
logger.Debug("Executing query", "query", gr.query, "frames", len(allFrames))
|
||||
err := db.QueryFramesInto(gr.refID, gr.query, allFrames, frame)
|
||||
frame, err := db.QueryFrames(ctx, gr.refID, gr.query, allFrames)
|
||||
if err != nil {
|
||||
logger.Error("Failed to query frames", "error", err.Error())
|
||||
rsp.Error = err
|
||||
@ -105,12 +104,11 @@ func (gr *SQLCommand) Execute(ctx context.Context, now time.Time, vars mathexp.V
|
||||
}
|
||||
logger.Debug("Done Executing query", "query", gr.query, "rows", frame.Rows())
|
||||
|
||||
frame.RefID = gr.refID
|
||||
|
||||
if frame.Rows() == 0 {
|
||||
rsp.Values = mathexp.Values{
|
||||
mathexp.NoData{Frame: frame},
|
||||
}
|
||||
return rsp, nil
|
||||
}
|
||||
|
||||
rsp.Values = mathexp.Values{
|
||||
|
@ -368,8 +368,7 @@ func (b *QueryAPIBuilder) handleExpressions(ctx context.Context, req parsedReque
|
||||
if !ok {
|
||||
dr, ok := qdr.Responses[refId]
|
||||
if ok {
|
||||
allowLongFrames := false // TODO -- depends on input type and only if SQL?
|
||||
_, res, err := b.converter.Convert(ctx, req.RefIDTypes[refId], dr.Frames, allowLongFrames)
|
||||
_, res, err := b.converter.Convert(ctx, req.RefIDTypes[refId], dr.Frames)
|
||||
if err != nil {
|
||||
expressionsLogger.Error("error converting frames for expressions", "error", err)
|
||||
res.Error = err
|
||||
@ -409,13 +408,12 @@ func (b *QueryAPIBuilder) convertQueryWithoutExpression(ctx context.Context, req
|
||||
if qdr == nil {
|
||||
return nil, errors.New("queryDataResponse is nil")
|
||||
}
|
||||
allowLongFrames := false
|
||||
refID := req.Request.Queries[0].RefID
|
||||
if _, exist := qdr.Responses[refID]; !exist {
|
||||
return nil, fmt.Errorf("refID '%s' does not exist", refID)
|
||||
}
|
||||
frames := qdr.Responses[refID].Frames
|
||||
_, results, err := b.converter.Convert(ctx, req.PluginId, frames, allowLongFrames)
|
||||
_, results, err := b.converter.Convert(ctx, req.PluginId, frames)
|
||||
if err != nil {
|
||||
results.Error = err
|
||||
}
|
||||
|
@ -29,4 +29,5 @@ const (
|
||||
grafanaDatabasesFrontend codeowner = "@grafana/databases-frontend"
|
||||
grafanaOSSBigTent codeowner = "@grafana/oss-big-tent"
|
||||
growthAndOnboarding codeowner = "@grafana/growth-and-onboarding"
|
||||
grafanaDatasourcesCoreServicesSquad codeowner = "@grafana/grafana-datasources-core-services"
|
||||
)
|
||||
|
@ -1054,7 +1054,7 @@ var (
|
||||
Description: "Enables using SQL and DuckDB functions as Expressions.",
|
||||
Stage: FeatureStageExperimental,
|
||||
FrontendOnly: false,
|
||||
Owner: grafanaAppPlatformSquad,
|
||||
Owner: grafanaDatasourcesCoreServicesSquad,
|
||||
},
|
||||
{
|
||||
Name: "nodeGraphDotLayout",
|
||||
|
@ -138,7 +138,7 @@ alertingSaveStateCompressed,experimental,@grafana/alerting-squad,false,false,fal
|
||||
scopeApi,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
||||
promQLScope,GA,@grafana/oss-big-tent,false,false,false
|
||||
logQLScope,privatePreview,@grafana/observability-logs,false,false,false
|
||||
sqlExpressions,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
||||
sqlExpressions,experimental,@grafana/grafana-datasources-core-services,false,false,false
|
||||
nodeGraphDotLayout,experimental,@grafana/observability-traces-and-profiling,false,false,true
|
||||
groupToNestedTableTransformation,GA,@grafana/dataviz-squad,false,false,true
|
||||
newPDFRendering,GA,@grafana/sharing-squad,false,false,false
|
||||
|
|
@ -3658,13 +3658,16 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "sqlExpressions",
|
||||
"resourceVersion": "1718727528075",
|
||||
"creationTimestamp": "2024-02-27T21:16:00Z"
|
||||
"resourceVersion": "1738589190784",
|
||||
"creationTimestamp": "2024-02-27T21:16:00Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2025-02-03 13:26:30.784245615 +0000 UTC"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables using SQL and DuckDB functions as Expressions.",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/grafana-app-platform-squad"
|
||||
"codeowner": "@grafana/grafana-datasources-core-services"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -117,6 +117,11 @@ require (
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/dlmiddlecote/sqlstats v1.0.2 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 // indirect
|
||||
github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90 // indirect
|
||||
github.com/dolthub/go-mysql-server v0.19.0 // indirect
|
||||
github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 // indirect
|
||||
github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/elazarl/goproxy v1.3.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
@ -205,7 +210,6 @@ require (
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.2 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jeremywohl/flatten v1.0.1 // indirect
|
||||
github.com/jessevdk/go-flags v1.5.0 // indirect
|
||||
github.com/jhump/protoreflect v1.15.1 // indirect
|
||||
github.com/jmespath-community/go-jmespath v1.1.1 // indirect
|
||||
@ -220,6 +224,7 @@ require (
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/lestrrat-go/strftime v1.0.4 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/magefile/mage v1.15.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
@ -286,6 +291,7 @@ require (
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
|
||||
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
@ -296,6 +302,7 @@ require (
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.8.2 // indirect
|
||||
github.com/tjhop/slog-gokit v0.1.3 // indirect
|
||||
github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
|
||||
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
|
||||
@ -352,6 +359,7 @@ require (
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/mail.v2 v2.3.1 // indirect
|
||||
gopkg.in/src-d/go-errors.v1 v1.0.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/api v0.32.1 // indirect
|
||||
|
@ -312,6 +312,16 @@ github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6
|
||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 h1:u3PMzfF8RkKd3lB9pZ2bfn0qEG+1Gms9599cr0REMww=
|
||||
github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2/go.mod h1:mIEZOHnFx4ZMQeawhw9rhsj+0zwQj7adVsnBX7t+eKY=
|
||||
github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90 h1:Sni8jrP0sy/w9ZYXoff4g/ixe+7bFCZlfCqXKJSU+zM=
|
||||
github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90/go.mod h1:ylU4XjUpsMcvl/BKeRRMXSH7e7WBrPXdSLvnRJYrxEA=
|
||||
github.com/dolthub/go-mysql-server v0.19.0 h1:NdcXyGt9v7m4sQOahU+ss++iyPy4Q3viuVvbnn3rUTQ=
|
||||
github.com/dolthub/go-mysql-server v0.19.0/go.mod h1:elfIatfq2fkU5lqTBrTcpL0RcHZOgYPE8EzBD7yQFiY=
|
||||
github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 h1:bMGS25NWAGTEtT5tOBsCuCrlYnLRKpbJVJkDbrTRhwQ=
|
||||
github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71/go.mod h1:2/2zjLQ/JOOSbbSboojeg+cAwcRV0fDLzIiWch/lhqI=
|
||||
github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54 h1:nzBnC0Rt1gFtscJEz4veYd/mazZEdbdmed+tujdaKOo=
|
||||
github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
@ -654,8 +664,6 @@ github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
|
||||
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jeremywohl/flatten v1.0.1 h1:LrsxmB3hfwJuE+ptGOijix1PIfOoKLJ3Uee/mzbgtrs=
|
||||
github.com/jeremywohl/flatten v1.0.1/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ=
|
||||
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
|
||||
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||
github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI=
|
||||
@ -725,6 +733,10 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
|
||||
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=
|
||||
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
|
||||
github.com/lestrrat-go/strftime v1.0.4 h1:T1Rb9EPkAhgxKqbcMIPguPq8glqXTA1koF8n9BHElA8=
|
||||
github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
@ -1004,6 +1016,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA=
|
||||
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0=
|
||||
github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4=
|
||||
github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
|
||||
github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tjhop/slog-gokit v0.1.3 h1:6SdexP3UIeg93KLFeiM1Wp1caRwdTLgsD/THxBUy1+o=
|
||||
github.com/tjhop/slog-gokit v0.1.3/go.mod h1:Bbu5v2748qpAWH7k6gse/kw3076IJf6owJmh7yArmJs=
|
||||
@ -1306,6 +1320,7 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@ -1536,6 +1551,8 @@ gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
|
||||
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/src-d/go-errors.v1 v1.0.0 h1:cooGdZnCjYbeS1zb1s6pVAAimTdKceRrpn7aKOnNIfc=
|
||||
gopkg.in/src-d/go-errors.v1 v1.0.0/go.mod h1:q1cBlomlw2FnDBDNGlnh6X0jPihy+QxZfMMNxPCbdYg=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
@ -210,6 +210,16 @@ github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5Xh
|
||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 h1:u3PMzfF8RkKd3lB9pZ2bfn0qEG+1Gms9599cr0REMww=
|
||||
github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2/go.mod h1:mIEZOHnFx4ZMQeawhw9rhsj+0zwQj7adVsnBX7t+eKY=
|
||||
github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90 h1:Sni8jrP0sy/w9ZYXoff4g/ixe+7bFCZlfCqXKJSU+zM=
|
||||
github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90/go.mod h1:ylU4XjUpsMcvl/BKeRRMXSH7e7WBrPXdSLvnRJYrxEA=
|
||||
github.com/dolthub/go-mysql-server v0.19.0 h1:NdcXyGt9v7m4sQOahU+ss++iyPy4Q3viuVvbnn3rUTQ=
|
||||
github.com/dolthub/go-mysql-server v0.19.0/go.mod h1:elfIatfq2fkU5lqTBrTcpL0RcHZOgYPE8EzBD7yQFiY=
|
||||
github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 h1:bMGS25NWAGTEtT5tOBsCuCrlYnLRKpbJVJkDbrTRhwQ=
|
||||
github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71/go.mod h1:2/2zjLQ/JOOSbbSboojeg+cAwcRV0fDLzIiWch/lhqI=
|
||||
github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54 h1:nzBnC0Rt1gFtscJEz4veYd/mazZEdbdmed+tujdaKOo=
|
||||
github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
@ -490,8 +500,6 @@ github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
|
||||
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jeremywohl/flatten v1.0.1 h1:LrsxmB3hfwJuE+ptGOijix1PIfOoKLJ3Uee/mzbgtrs=
|
||||
github.com/jeremywohl/flatten v1.0.1/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ=
|
||||
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
|
||||
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||
github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI=
|
||||
@ -553,6 +561,8 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
|
||||
github.com/lestrrat-go/strftime v1.0.4 h1:T1Rb9EPkAhgxKqbcMIPguPq8glqXTA1koF8n9BHElA8=
|
||||
github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
@ -747,6 +757,8 @@ github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJV
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY=
|
||||
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w=
|
||||
@ -788,6 +800,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA=
|
||||
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0=
|
||||
github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4=
|
||||
github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
|
||||
github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tjhop/slog-gokit v0.1.3 h1:6SdexP3UIeg93KLFeiM1Wp1caRwdTLgsD/THxBUy1+o=
|
||||
github.com/tjhop/slog-gokit v0.1.3/go.mod h1:Bbu5v2748qpAWH7k6gse/kw3076IJf6owJmh7yArmJs=
|
||||
@ -1103,6 +1117,8 @@ gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
|
||||
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
||||
gopkg.in/src-d/go-errors.v1 v1.0.0 h1:cooGdZnCjYbeS1zb1s6pVAAimTdKceRrpn7aKOnNIfc=
|
||||
gopkg.in/src-d/go-errors.v1 v1.0.0/go.mod h1:q1cBlomlw2FnDBDNGlnh6X0jPihy+QxZfMMNxPCbdYg=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
Loading…
Reference in New Issue
Block a user