mirror of https://github.com/grafana/grafana.git
				
				
				
			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= | ||||
|  |  | |||
|  | @ -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 | ||||
| 	} | ||||
| } | ||||
|  | @ -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) | ||||
| 	} | ||||
|  |  | |||
|  | @ -66,7 +66,6 @@ type Service struct { | |||
| 
 | ||||
| 	tracer  tracing.Tracer | ||||
| 	metrics *metrics | ||||
| 	allowLongFrames bool | ||||
| } | ||||
| 
 | ||||
| type pluginContextProvider interface { | ||||
|  |  | |||
|  | @ -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{ | ||||
| 	resp := 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(), | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	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{ | ||||
| 	resp := 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), | ||||
| 	} | ||||
| 
 | ||||
| 	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 | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  | @ -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") | ||||
| } | ||||
|  | @ -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 | ||||
| } | ||||
|  | @ -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 | ||||
| 	} | ||||
| } | ||||
|  | @ -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) | ||||
| 	tables := make(map[string]struct{}) | ||||
| 
 | ||||
| 	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 converting json sql to ast", "error", err.Error(), "ret", ret) | ||||
| 		return nil, fmt.Errorf("error converting json to ast: %s", err.Error()) | ||||
| 			logger.Error("error walking sql", "error", err, "node", node) | ||||
| 			return fmt.Errorf("failed to parse SQL expression: %w", err) | ||||
| 		} | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	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()) | ||||
| 	if err := walkSubtree(stmt); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	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) | ||||
| 	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) | ||||
| 		} | ||||
| 	} | ||||
| 		if strings.Contains(k, TABLE_NAME) { | ||||
| 			table, ok := v.(string) | ||||
| 			if ok && !existsInList(table, tables) { | ||||
| 				tables = append(tables, v.(string)) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	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 | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
| 	} | ||||
| } | ||||
|  | @ -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) | ||||
| 
 | ||||
| 	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 | ||||
| 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` | ||||
| 	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 | ||||
| 			LIMIT 10`, | ||||
| 			expected: []string{"A", "B"}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "right join", | ||||
| 			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 | ||||
| 			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` | ||||
| 	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, | ||||
| 			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;` | ||||
| 	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 | ||||
| 			) 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{}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	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