Add MySQL REPL to the web UI (#57169)

This commit is contained in:
Gavin Frazar
2025-08-07 14:50:50 -04:00
committed by GitHub
parent dc394bde98
commit a97e656db6
26 changed files with 3476 additions and 4 deletions
+2
View File
@@ -9,3 +9,5 @@ docs/theme/js/theme.js linguist-vendored=false
*_pb.ts linguist-generated
*_pb.grpc-*.ts linguist-generated
*_pb.client.ts linguist-generated
*.golden -text
+10
View File
@@ -206,6 +206,8 @@ require (
github.com/spiffe/aws-spiffe-workload-helper v0.0.4
github.com/spiffe/go-spiffe/v2 v2.5.0
github.com/stretchr/testify v1.10.0
github.com/testcontainers/testcontainers-go v0.37.0
github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0
github.com/tidwall/pretty v1.2.1
github.com/tidwall/sjson v1.2.5
github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb
@@ -337,6 +339,7 @@ require (
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/crewjam/httperr v0.2.0 // indirect
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
@@ -454,6 +457,7 @@ require (
github.com/lithammer/dedent v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mailgun/errors v0.4.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
@@ -469,11 +473,17 @@ require (
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.1.0 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/spdystream v0.5.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/montanaflynn/stats v0.7.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/mtibben/percent v0.2.1 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
+20 -4
View File
@@ -653,8 +653,8 @@ github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMb
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0=
github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg=
github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM=
github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/provider v0.14.0 h1:kcnfY4vljxXliXDBrA9K9lwF8IoEZ4Up6Eg9kWTIm28=
@@ -1111,6 +1111,8 @@ github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -1806,6 +1808,8 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2
github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailgun/errors v0.4.0 h1:6LFBvod6VIW83CMIOT9sYNp28TCX0NejFPP4dSX++i8=
github.com/mailgun/errors v0.4.0/go.mod h1:xGBaaKdEdQT0/FhwvoXv4oBaqqmVZz9P1XEnvD/onc0=
github.com/mailgun/mailgun-go/v4 v4.23.0 h1:jPEMJzzin2s7lvehcfv/0UkyBu18GvcURPr2+xtZRbk=
@@ -1873,12 +1877,20 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -2191,6 +2203,10 @@ github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDd
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
github.com/tchap/go-patricia/v2 v2.3.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/Yc7nM=
github.com/tchap/go-patricia/v2 v2.3.2/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
github.com/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw7Z/PtHS/QzZZ5Ra/hg=
github.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM=
github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0 h1:LqUos1oR5iuuzorFnSvxsHNdYdCHB/DfI82CuT58wbI=
github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0/go.mod h1:vHEEHx5Kf+uq5hveaVAMrTzPY8eeRZcKcl23MRw5Tkc=
github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg=
github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU=
github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI=
@@ -3189,8 +3205,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
helm.sh/helm/v3 v3.18.4 h1:pNhnHM3nAmDrxz6/UC+hfjDY4yeDATQCka2/87hkZXQ=
+37
View File
@@ -1006,6 +1006,8 @@ github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -1059,6 +1061,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY=
github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/elimity-com/scim v0.0.0-20240320110924-172bf2aee9c8 h1:0+BTyxIYgiVAry/P5s8R4dYuLkhB9Nhso8ogFWNr4IQ=
github.com/elimity-com/scim v0.0.0-20240320110924-172bf2aee9c8/go.mod h1:JkjcmqbLW+khwt2fmBPJFBhx2zGZ8XobRZ+O0VhlwWo=
github.com/emicklei/go-restful/v3 v3.11.3 h1:yagOQz/38xJmcNeZJtrUcKjkHRltIaIFXKWeG1SkWGE=
@@ -1166,6 +1170,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
github.com/go-openapi/errors v0.22.1 h1:kslMRRnK7NCb/CvR1q1VWuEQCEIsBGn5GgKD9e+HYhU=
@@ -1710,9 +1716,13 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mark3labs/mcp-go v0.36.0 h1:rIZaijrRYPeSbJG8/qNDe0hWlGrCJ7FWHNMz2SQpTis=
@@ -1767,8 +1777,18 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -1781,6 +1801,8 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU=
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs=
github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns=
github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM=
@@ -1878,6 +1900,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
@@ -1953,6 +1977,9 @@ github.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df h1:S77Pf5fIG
github.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df/go.mod h1:dcuzJZ83w/SqN9k4eQqwKYMgmKWzg/KzJAURBhRL1tc=
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
@@ -2035,6 +2062,10 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw7Z/PtHS/QzZZ5Ra/hg=
github.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM=
github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0 h1:LqUos1oR5iuuzorFnSvxsHNdYdCHB/DfI82CuT58wbI=
github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0/go.mod h1:vHEEHx5Kf+uq5hveaVAMrTzPY8eeRZcKcl23MRw5Tkc=
github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg=
github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU=
github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI=
@@ -2062,6 +2093,10 @@ github.com/tink-crypto/tink-go/v2 v2.4.0 h1:8VPZeZI4EeZ8P/vB6SIkhlStrJfivTJn+cQ4
github.com/tink-crypto/tink-go/v2 v2.4.0/go.mod h1:l//evrF2Y3MjdbpNDNGnKgCpo5zSmvUvnQ4MU+yE2sw=
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0=
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/transparency-dev/formats v0.0.0-20250421220931-bb8ad4d07c26 h1:YTbkeFbzcer+42bIgo6Za2194nKwhZPgaZKsP76QffE=
github.com/transparency-dev/formats v0.0.0-20250421220931-bb8ad4d07c26/go.mod h1:ODywn0gGarHMMdSkWT56ULoK8Hk71luOyRseKek9COw=
github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4=
@@ -2120,6 +2155,8 @@ github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUei
github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms=
github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk=
+37
View File
@@ -1029,6 +1029,8 @@ github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -1082,6 +1084,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY=
github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/elimity-com/scim v0.0.0-20240320110924-172bf2aee9c8 h1:0+BTyxIYgiVAry/P5s8R4dYuLkhB9Nhso8ogFWNr4IQ=
github.com/elimity-com/scim v0.0.0-20240320110924-172bf2aee9c8/go.mod h1:JkjcmqbLW+khwt2fmBPJFBhx2zGZ8XobRZ+O0VhlwWo=
github.com/emicklei/go-restful/v3 v3.11.3 h1:yagOQz/38xJmcNeZJtrUcKjkHRltIaIFXKWeG1SkWGE=
@@ -1198,6 +1202,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
github.com/go-openapi/errors v0.22.1 h1:kslMRRnK7NCb/CvR1q1VWuEQCEIsBGn5GgKD9e+HYhU=
@@ -1766,9 +1772,13 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mark3labs/mcp-go v0.36.0 h1:rIZaijrRYPeSbJG8/qNDe0hWlGrCJ7FWHNMz2SQpTis=
@@ -1840,8 +1850,18 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -1854,6 +1874,8 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU=
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs=
github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns=
github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM=
@@ -1954,6 +1976,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
@@ -2033,6 +2057,9 @@ github.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df h1:S77Pf5fIG
github.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df/go.mod h1:dcuzJZ83w/SqN9k4eQqwKYMgmKWzg/KzJAURBhRL1tc=
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
@@ -2116,6 +2143,10 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw7Z/PtHS/QzZZ5Ra/hg=
github.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM=
github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0 h1:LqUos1oR5iuuzorFnSvxsHNdYdCHB/DfI82CuT58wbI=
github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0/go.mod h1:vHEEHx5Kf+uq5hveaVAMrTzPY8eeRZcKcl23MRw5Tkc=
github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg=
github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU=
github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI=
@@ -2143,6 +2174,10 @@ github.com/tink-crypto/tink-go/v2 v2.4.0 h1:8VPZeZI4EeZ8P/vB6SIkhlStrJfivTJn+cQ4
github.com/tink-crypto/tink-go/v2 v2.4.0/go.mod h1:l//evrF2Y3MjdbpNDNGnKgCpo5zSmvUvnQ4MU+yE2sw=
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0=
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/transparency-dev/formats v0.0.0-20250421220931-bb8ad4d07c26 h1:YTbkeFbzcer+42bIgo6Za2194nKwhZPgaZKsP76QffE=
github.com/transparency-dev/formats v0.0.0-20250421220931-bb8ad4d07c26/go.mod h1:ODywn0gGarHMMdSkWT56ULoK8Hk71luOyRseKek9COw=
github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4=
@@ -2205,6 +2240,8 @@ github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUei
github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms=
github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
+280
View File
@@ -0,0 +1,280 @@
// Teleport
// Copyright (C) 2025 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package repl
import (
"cmp"
"fmt"
"maps"
"slices"
"strings"
"unicode/utf8"
"github.com/go-mysql-org/go-mysql/mysql"
"github.com/gravitational/trace"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/asciitable"
)
type commandManager struct {
byName map[string]*command
byShortcut map[rune]*command
// shortcutPrefix is the prefix for command shortcuts, e.g. \h for help.
shortcutPrefix string
}
// command represents a command that can be executed in the REPL.
type command struct {
// name is the command's full name.
name string
// shortcut is the command's escaped single char name: \x.
shortcut rune
// description provides a user-friendly explanation of what the command
// does.
description string
// execFunc is the function to execute the command. The commands can either
// return a reply (that will be sent back to the client) as a string. It can
// terminate the REPL by returning bool on the second argument.
execFunc func(r *REPL, args string) (reply string, exit bool)
}
func (c *command) checkAndSetDefaults() error {
if len(c.name) == 0 {
return trace.BadParameter("missing command name")
}
if c.shortcut == 0 {
return trace.BadParameter("missing command short name")
}
if len(c.description) == 0 {
return trace.BadParameter("missing command description")
}
if c.execFunc == nil {
return trace.BadParameter("missing command exec func")
}
c.name = normalizeCommandName(c.name)
return nil
}
func normalizeCommandName(name string) string {
return strings.ToLower(name)
}
func newCommands() (*commandManager, error) {
c := &commandManager{
byName: make(map[string]*command),
byShortcut: make(map[rune]*command),
shortcutPrefix: `\`,
}
for _, cmd := range []*command{
{
name: "delimiter",
shortcut: 'd',
description: "Set statement delimiter. With no argument the delimiter resets to the default.",
execFunc: func(r *REPL, args string) (reply string, exit bool) {
delim := getArg(args)
if err := r.parser.lex.setDelimiter(delim); err != nil {
return err.Error(), false
}
return "", false
},
},
{
name: "help",
shortcut: 'h',
description: "Show the list of supported commands.",
execFunc: func(_ *REPL, _ string) (string, bool) {
return c.help(), false
},
},
{
name: "quit",
shortcut: 'q',
description: "Terminates the session.",
execFunc: func(_ *REPL, _ string) (string, bool) { return "", true },
},
{
name: "status",
shortcut: 's',
description: "Get status information.",
execFunc: func(r *REPL, _ string) (string, bool) {
table := asciitable.MakeHeadlessTable(2)
table.AddRow([]string{"Teleport database:", r.route.ServiceName})
table.AddRow([]string{"Current database:", formatDatabaseName(r.route.Database)})
table.AddRow([]string{"Current user:", r.route.Username})
table.AddRow([]string{"Server version:", r.myConn.GetServerVersion()})
table.AddRow([]string{"Using delimiter:", r.parser.lex.delimiter()})
return table.String(), false
},
},
{
name: "teleport",
shortcut: 't',
description: "Show Teleport interactive shell information, such as execution limitations.",
execFunc: func(_ *REPL, _ string) (string, bool) {
var sb strings.Builder
for _, l := range descriptiveLimitations {
sb.WriteString("- " + strings.ReplaceAll(l, "\n", "\n ") + lineBreak)
}
return fmt.Sprintf(
"Teleport MySQL interactive shell (v%s)\n\nLimitations: \n%s",
teleport.Version,
sb.String(),
), false
},
},
{
name: "use",
shortcut: 'u',
description: "Use another database.",
execFunc: func(r *REPL, args string) (string, bool) {
args = strings.TrimSpace(args)
if len(args) == 0 {
return "USE must be followed by a database name", false
}
dbName, err := getDatabaseName(r.myConn, args)
if err != nil {
return err.Error(), false
}
if err := r.myConn.UseDB(dbName); err != nil {
return err.Error(), false
}
r.route.Database = dbName
return fmt.Sprintf(`Default database changed to %q`, dbName), false
},
},
} {
if err := cmd.checkAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
if err := c.add(cmd); err != nil {
return nil, trace.Wrap(err)
}
}
return c, nil
}
func (c *commandManager) add(cmd *command) error {
if _, ok := c.byName[cmd.name]; ok {
return trace.BadParameter("overlapping command name: %s", cmd.name)
}
c.byName[cmd.name] = cmd
if _, ok := c.byShortcut[cmd.shortcut]; ok {
return trace.BadParameter("overlapping command shortcut: %c", cmd.shortcut)
}
c.byShortcut[cmd.shortcut] = cmd
return nil
}
// findCommand tries to find a command at the start of a line. If a command name
// or short name matches, then it returns the command and the rest of the line.
// If no command is found and the line does not start with the short name prefix
// then the command and error returned will both be nil.
func (c *commandManager) findCommand(line string) (*command, string, error) {
line = strings.TrimSpace(line)
if before, after, found := strings.Cut(line, c.shortcutPrefix); found && len(before) == 0 {
shortcut, width := utf8.DecodeRuneInString(after)
if width > 0 && shortcut != utf8.RuneError {
if cmd, ok := c.byShortcut[shortcut]; ok {
return cmd, after[1:], nil
}
}
return nil, "", trace.Errorf("Unknown command.\n%v", c.help())
}
name, after, _ := strings.Cut(line, " ")
if cmd, ok := c.byName[normalizeCommandName(name)]; ok {
return cmd, after, nil
}
return nil, "", nil
}
func (c *commandManager) help() string {
var res strings.Builder
cmds := slices.Collect(maps.Values(c.byName))
slices.SortStableFunc(cmds, func(a, b *command) int {
return strings.Compare(a.name, b.name)
})
table := asciitable.MakeHeadlessTable(3)
for _, cmd := range cmds {
short := fmt.Sprintf("%s%c", c.shortcutPrefix, cmd.shortcut)
row := []string{cmd.name, short, cmd.description}
table.AddRow(row)
}
table.WriteTo(&res)
res.WriteString(lineBreak)
return res.String()
}
// getArg trims spaces from the line and returns the first space separated arg.
func getArg(line string) string {
line = strings.TrimSpace(line)
before, _, _ := strings.Cut(line, " ")
return before
}
// getDatabaseName extracts a database name from args.
// Since USE is both a command (COM_INIT_DB) and a statement, as a client
// we have to parse the database argument for COM_INIT_DB.
// To handle quotation, we send the args to the server as a statement and then
// ask the server what the current database is.
func getDatabaseName(conn mysql.Executer, args string) (string, error) {
result, err := conn.Execute("USE " + args)
if err != nil {
return "", trace.Wrap(err)
}
result.Close()
return getCurrentDatabase(conn)
}
func getCurrentDatabase(conn mysql.Executer) (string, error) {
result, err := conn.Execute("SELECT DATABASE() LIMIT 1")
if err != nil {
return "", trace.Wrap(err)
}
defer result.Close()
if result.Resultset == nil || len(result.Resultset.Values) == 0 || len(result.Resultset.Values[0]) == 0 {
return "", trace.NotFound("missing result")
}
val := result.Resultset.Values[0][0]
if val.Type == mysql.FieldValueTypeString {
return string(val.AsString()), nil
}
return "", nil
}
func formatDatabaseName(name string) string {
return cmp.Or(name, "(none)")
}
// descriptiveLimitations defines a user-friendly text containing the REPL
// limitations.
var descriptiveLimitations = []string{
`Query cancellation is not supported. Once a query is sent, its execution
can only be canceled by killing it from another session.
You can identify the query process ID with "SHOW PROCESSLIST" and then kill the
query with "KILL QUERY <pid>".
`,
// This limitation is due to our terminal emulator not fully supporting this
// shortcut's custom handler. Instead, it will close the terminal, leading
// to terminating the session. To avoid having users accidentally
// terminating their sessions, we're turning this off until we have a better
// solution and propose the behavior for it.
//
// This shortcut filtered out by the WebUI key handler.
"Pressing CTRL-C will have no effect in this shell.",
}
+181
View File
@@ -0,0 +1,181 @@
// Teleport
// Copyright (C) 2025 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package repl
import (
"maps"
"slices"
"testing"
"github.com/go-mysql-org/go-mysql/mysql"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport"
clientproto "github.com/gravitational/teleport/api/client/proto"
)
func TestCommands(t *testing.T) {
t.Parallel()
parser, err := newParser()
require.NoError(t, err)
commands := parser.commands
require.NotEmpty(t, commands.byName)
require.ElementsMatch(t,
slices.Collect(maps.Values(commands.byName)),
slices.Collect(maps.Values(commands.byShortcut)),
)
for name, cmd := range commands.byName {
require.NotNil(t, cmd)
require.NotEmpty(t, cmd.name)
require.NotZero(t, cmd.shortcut)
require.NotEmpty(t, cmd.description)
require.NotNil(t, cmd.execFunc)
require.Equal(t, cmd.name, name, "expected command to be registered by name")
byShortcut, ok := commands.byShortcut[cmd.shortcut]
require.True(t, ok)
require.Equal(t, cmd, byShortcut, "expected commands from name and shortcut indices to match")
}
repl := &REPL{
parser: parser,
myConn: &fakeMySQLConn{
exec: func(command string, args ...any) (*mysql.Result, error) {
resultSet, err := mysql.BuildSimpleTextResultset([]string{"current_database"}, [][]any{{"example"}})
if err != nil {
return nil, err
}
resultSet.Values = make([][]mysql.FieldValue, 1)
resultSet.Values[0], err = resultSet.RowDatas[0].Parse(resultSet.Fields, false, nil)
return &mysql.Result{
Resultset: resultSet,
}, err
},
},
route: clientproto.RouteToDatabase{
ServiceName: "test-service",
Username: "test-user",
Database: "test-database",
},
}
tests := []struct {
desc string
cmdName string
args string
expectExit bool
assertCommandReply require.ValueAssertionFunc
}{
{
desc: "delimiter command has an empty reply",
cmdName: "delimiter",
assertCommandReply: func(t require.TestingT, val any, _ ...any) {
require.Empty(t, val)
},
},
{
desc: "delimiter fails regex",
cmdName: "delimiter",
args: "abc",
assertCommandReply: func(t require.TestingT, val any, _ ...any) {
require.Equal(t, `DELIMITER "abc" does not match regex used for validation "^(;|[/]{2}|[$]{2})$"`, val)
},
},
{
desc: "help",
cmdName: "help",
assertCommandReply: func(t require.TestingT, val any, _ ...any) {
for name := range commands.byName {
require.Contains(t, val, name, `expected help command to include information about the %q command`, name)
}
},
},
{
desc: "quit",
cmdName: "quit",
expectExit: true,
},
{
desc: "status",
cmdName: "status",
assertCommandReply: func(t require.TestingT, val any, _ ...any) {
require.Contains(t, val, "test-service", "expected session command to contain service name")
require.Contains(t, val, "test-user", "expected session command to contain username")
require.Contains(t, val, "test-database", "expected session command to contain database name")
},
},
{
desc: "teleport",
cmdName: "teleport",
assertCommandReply: func(t require.TestingT, val any, _ ...any) {
require.Contains(t, val, teleport.Version, "expected teleport command to include current Teleport version")
},
},
{
desc: "use requires an arg",
cmdName: "use",
assertCommandReply: func(t require.TestingT, val any, _ ...any) {
require.Equal(t, "USE must be followed by a database name", val)
},
},
{
desc: "use with an unquoted argument",
cmdName: "use",
args: "example",
assertCommandReply: func(t require.TestingT, val any, _ ...any) {
require.Equal(t, `Default database changed to "example"`, val)
},
},
{
desc: "use with a quoted argument",
cmdName: "use",
args: `"example"`,
assertCommandReply: func(t require.TestingT, val any, _ ...any) {
require.Equal(t, `Default database changed to "example"`, val)
},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
cmd, ok := commands.byName[test.cmdName]
require.True(t, ok, "expected command to be registered")
reply, exit := cmd.execFunc(repl, test.args)
if test.expectExit {
require.True(t, exit, "expected command to exit the REPL")
return
}
test.assertCommandReply(t, reply)
})
}
}
type fakeMySQLConn struct {
exec func(command string, args ...any) (*mysql.Result, error)
}
func (c *fakeMySQLConn) Execute(command string, args ...any) (*mysql.Result, error) {
return c.exec(command, args...)
}
func (c *fakeMySQLConn) UseDB(dbName string) error {
return nil
}
func (c *fakeMySQLConn) GetServerVersion() string {
return "1.2.3 Fake MySQL Server"
}
@@ -0,0 +1,113 @@
-- temporarily disable note warnings for test consistency
set session sql_notes = 0;
CREATE DATABASE IF NOT EXISTS all_data_types;
set session sql_notes = 1;
USE all_data_types;
-- without ANSI_QUOTES double quotes are literals
SET SESSION sql_mode='';
SELECT "thing";
-- with ANSI_QUOTES double quotes are identifiers, which is what we want when
-- selecting from the table below
SET SESSION sql_mode='ANSI_QUOTES';
SELECT "thing";
CREATE TABLE all_types_demo (
"c tinyint" TINYINT,
c_smallint SMALLINT,
c_mediumint MEDIUMINT,
c_int INT,
c_bigint BIGINT,
c_decimal DECIMAL(10,2),
c_float FLOAT,
c_double DOUBLE,
c_bit BIT(8),
c_bool BOOL,
c_date DATE,
c_time TIME,
c_datetime DATETIME,
c_timestamp TIMESTAMP NULL DEFAULT NULL,
c_year YEAR,
c_char CHAR(5),
c_varchar VARCHAR(10),
c_binary BINARY(3),
c_varbinary VARBINARY(4),
c_tinyblob TINYBLOB,
c_blob BLOB,
c_mediumblob MEDIUMBLOB,
c_longblob LONGBLOB,
c_tinytext TINYTEXT,
c_text TEXT,
c_mediumtext MEDIUMTEXT,
c_longtext LONGTEXT,
c_enum ENUM('foo','bar','baz'),
c_set SET('a','b','c'),
c_geometry GEOMETRY,
c_point POINT,
c_linestring LINESTRING,
c_polygon POLYGON,
c_multipoint MULTIPOINT,
c_multilinestring MULTILINESTRING,
c_multipolygon MULTIPOLYGON,
c_geometrycoll GEOMETRYCOLLECTION,
c_json JSON
);
INSERT INTO all_types_demo VALUES (
127,
32767,
8388607,
2147483647,
9223372036854775807,
12345.67,
3.14,
2.7182818284,
b'10101010',
1,
'2024-07-09',
'12:34:56',
'2024-07-09 12:34:56',
'2024-07-09 12:34:56',
2025,
'abcde',
'abcdefghij',
'abc',
'defg',
'a',
'blob',
'medblob',
'longblob',
'tiny',
'text',
'mediumtext',
'longtext',
'foo',
'a,b',
ST_GeomFromText('POINT(1 1)'),
ST_GeomFromText('POINT(2 2)'),
ST_GeomFromText('LINESTRING(0 0,1 1,2 2)'),
ST_GeomFromText('POLYGON((0 0,1 0,1 1,0 1,0 0))'),
ST_GeomFromText('MULTIPOINT((0 0),(1 1))'),
ST_GeomFromText('MULTILINESTRING((0 0,1 1),(2 2,3 3))'),
ST_GeomFromText('MULTIPOLYGON(((0 0,1 0,1 1,0 1,0 0)))'),
ST_GeomFromText('GEOMETRYCOLLECTION(POINT(1 2),LINESTRING(0 0,1 1))'),
JSON_OBJECT('x', 1, 'y', 2)
);
SELECT "c tinyint", `c_smallint`, "c_mediumint" FROM `all_types_demo`;
SELECT c_int, c_bigint, c_decimal FROM all_types_demo;
SELECT c_float, c_double, c_bit FROM all_types_demo;
SELECT c_bool, c_date, c_time FROM all_types_demo;
SELECT c_datetime, c_timestamp, c_year FROM all_types_demo;
SELECT c_char, c_varchar, c_binary FROM all_types_demo;
SELECT c_varbinary, c_tinyblob, c_blob FROM all_types_demo;
SELECT "c_mediumblob",
`c_longblob`,
c_tinytext
FROM all_types_demo;
SELECT c_text, c_mediumtext, c_longtext FROM all_types_demo;
SELECT c_enum, c_set, ST_AsText(c_geometry) AS c_geometry FROM all_types_demo;
SELECT ST_AsText(c_point) AS c_point, ST_AsText(c_linestring) AS c_linestring, ST_AsText(c_polygon) AS c_polygon FROM all_types_demo;
SELECT ST_AsText(c_multipoint) AS c_multipoint, ST_AsText(c_multilinestring) AS c_multilinestring, ST_AsText(c_multipolygon) AS c_multipolygon FROM all_types_demo;
SELECT ST_AsText(c_geometrycoll) AS c_geometrycoll, c_json FROM all_types_demo;
DROP DATABASE all_data_types;
@@ -0,0 +1,85 @@
-- temporarily disable note warnings for test consistency
set session sql_notes = 0;
create database if not exists test_commands;
create database if not exists foo;
create database if not exists bar;
set session sql_notes = 1;
use test_commands;
-- leading spaces/comments before commands
/* just a comment */
use foo;
use foo
/*
llama
*/
use foo;
/*
llama
*/
use foo
-- llama
use foo;
-- llama
use foo
-- llama
use foo;
-- llama
use foo
/* llama */use foo;
/* llama */use foo
/* llama */ use foo;
/* llama */ use foo
/* llama */
use foo;
/* llama */
use foo
/* llama
*/use foo;
/* llama
*/use foo
/* llama
*/ use foo;
/* llama
*/ use foo
/* llama
-- llama
*/use foo;
/* llama
-- llama
*/use foo
/* llama
-- llama
*/ use foo;
/* llama
-- llama
*/ use foo
/* llama
-- */use foo;
/* llama
-- */use foo
/* llama
-- */ use foo;
/* llama
-- */ use foo
/* llama */
use foo;use bar; use foo
-- this isn't a command, but that should just print an error message and help info.
\xyz select 1
\xyz;select 1;
delimiter $$
help$$
\d //
\h
help
status
\s
teleport
\t
use foo
\u mysql//
-- reset the delimiter to default ;
\d
teleport;\s;\h
use `bar`;drop database foo;drop database bar;drop database test_commands;quit
+92
View File
@@ -0,0 +1,92 @@
-- temporarily disable note warnings for test consistency
set session sql_notes = 0;
DROP DATABASE IF EXISTS test_ddl_db;
CREATE DATABASE test_ddl_db;
set session sql_notes = 1;
-- this should produce a note level warning because it does exist
CREATE DATABASE if not exists test_ddl_db;
-- we clear the warning by selecting the warning count
select @@warning_count;
USE test_ddl_db;
CREATE TABLE test_table (
id INT PRIMARY KEY AUTO_INCREMENT,
value VARCHAR(50) NOT NULL,
note VARCHAR(100) DEFAULT NULL
) ENGINE=InnoDB;
CREATE VIEW test_view AS (
SELECT id, value
FROM test_table
);
DELIMITER $$
CREATE FUNCTION test_func(x INT) RETURNS INT DETERMINISTIC
BEGIN
RETURN x * 2;
END $$
DELIMITER ;
DELIMITER $$
CREATE PROCEDURE test_proc()
BEGIN
INSERT INTO test_table(value) VALUES ('from proc');
END $$
DELIMITER ;
CALL test_proc();
DELIMITER $$
CREATE TRIGGER test_trigger_before_insert
BEFORE INSERT ON test_table
FOR EACH ROW
BEGIN
SET NEW.value = UPPER(NEW.value);
END $$
DELIMITER ;
CALL test_proc();
CREATE EVENT test_event
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 HOUR
DO INSERT INTO test_table(value) VALUES ('from event');
set session sql_notes = 0;
-- roles are not database scoped, let's disable note warnings in case some other
-- test has already created the role or user.
CREATE ROLE IF NOT EXISTS test_role;
DROP ROLE test_role;
CREATE USER IF NOT EXISTS test_user@'localhost'
IDENTIFIED BY 'some password'
ACCOUNT LOCK;
ALTER USER test_user@'localhost' PASSWORD EXPIRE;
DROP USER test_user@'localhost';
set session sql_notes = 1;
ALTER DATABASE test_ddl_db CHARACTER SET utf8mb4;
ALTER TABLE test_table ADD COLUMN note2 VARCHAR(100) DEFAULT NULL;
ALTER EVENT test_event DISABLE;
ALTER FUNCTION test_func COMMENT 'Test function';
ALTER PROCEDURE test_proc COMMENT 'Test procedure';
RENAME TABLE test_table
TO test_table_renamed;
SELECT * from test_table_renamed;
RENAME
TABLE test_table_renamed
TO test_table;
SELECT * from test_table;
-- there is no "test_server", this should make an error.
ALTER SERVER test_server OPTIONS (HOST '127.0.0.1');
DROP TRIGGER test_trigger_before_insert;
TRUNCATE TABLE test_table ;
DROP TABLE test_table;
DROP DATABASE test_ddl_db
;
@@ -0,0 +1,50 @@
-- with ANSI_QUOTES double quotes are identifiers
SET SESSION sql_mode='ANSI_QUOTES';
SELECT "thing";
-- without ANSI_QUOTES double quotes are literals
SET SESSION sql_mode='';
SELECT "thing";
select 1, -1,1.24, -1.24, 'foo', null, 'null';
select
1 as 'unsigned int',
-1 as 'negative int',
1.24 as "float",
-1.24 as 'negative float',
'foo' as 'string',
null as 'actual NULL',
"null" as 'string null';
select 1 as 'multiple rows of literals'
union
select -1
union
select 1.24
union
select -1.24
union
select 'foo'
union
select null
union
select 'null';
select 'a
b
c' as 'multiline-string';
select 'a
;
b
;
c' as 'multiline with delimiters';
/* foo bar */
select 1;
/* foo bar */select /*
baz */ 42;
delimiter $$
select '$$1$$'$$
select '$$;$$';$$
delimiter ;
select 1;
+314
View File
@@ -0,0 +1,314 @@
// Teleport
// Copyright (C) 2025 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package repl
import (
"cmp"
"io"
"regexp"
"strings"
"unicode"
"github.com/gravitational/trace"
)
// lexer scans REPL input lines distinguishing between comments, strings,
// whitespace, delimiter, and normal query text.
type lexer struct {
delim string
inComment bool
inStringToken token
queryBuf strings.Builder
lineReader strings.Reader
line string
}
// clientDefaultStatementDelimiter is the default client-side delimiter between
// statements. This is the only delimiter that is recognized server-side, but it
// is sometimes necessary for the client to switch delimiters while typing at a
// REPL.
const clientDefaultStatementDelimiter = ";"
// delimiter returns the current client-side delimiter.
func (l *lexer) delimiter() string {
return cmp.Or(l.delim, clientDefaultStatementDelimiter)
}
// validDelimiterRegex only allows somewhat sane choices for delimiters.
var validDelimiterRegex = regexp.MustCompile(`^(;|[/]{2}|[$]{2})$`)
// setDelimiter sets the client-side statement delimiter.
func (l *lexer) setDelimiter(delimiter string) error {
delimiter = strings.TrimSpace(delimiter)
if delimiter == "" {
l.delim = ""
return nil
}
if validDelimiterRegex.MatchString(delimiter) {
l.delim = delimiter
return nil
}
return trace.BadParameter("DELIMITER %q does not match regex used for validation %q",
delimiter, validDelimiterRegex.String(),
)
}
// writeString writes a string to the query buffer.
func (l *lexer) writeString(s string) {
l.queryBuf.WriteString(s)
}
// writeRune writes a single rune to the query buffer.
func (l *lexer) writeRune(r rune) {
l.queryBuf.WriteRune(r)
}
// getQuery builds the buffered query string and empties the query buffer.
func (l *lexer) getQuery() string {
out := l.queryBuf.String()
l.queryBuf.Reset()
return out
}
// isInQuery returns true if the lexer is reading inside of a MySQL query.
func (l *lexer) isInQuery() bool {
return l.queryBuf.Len() > 0
}
// isInComment returns true if the lexer is reading inside of a MySQL comment.
func (l *lexer) isInComment() bool {
return l.inComment
}
// isInString returns true if the lexer is reading inside of a MySQL string.
func (l *lexer) isInString() bool {
return len(l.inStringToken.text) > 0
}
// inStringKind returns the kind of MySQL string the lexer is currently reading
// inside of, i.e., the opening/closing rune: "'`.
func (l *lexer) inStringKind() string {
return l.inStringToken.text
}
// isMultilineQuery returns true if the lexer is reading a multline query.
func (l *lexer) isMultilineQuery() bool {
return l.isInQuery() && strings.Contains(l.queryBuf.String(), lineBreak)
}
// setCloseComment marks the start of a comment.
func (l *lexer) setOpenComment() {
l.inComment = true
}
// setCloseComment marks the end of a comment.
func (l *lexer) setCloseComment() {
l.inComment = false
}
// setCloseString marks the start of a MySQL string.
func (l *lexer) setOpenString(tok token) {
l.inStringToken = tok
}
// setCloseString closes the currently open string state.
func (l *lexer) setCloseString() {
l.inStringToken = token{}
}
// setLine sets the input line to read from.
func (l *lexer) setLine(line string) {
l.lineReader.Reset(line)
l.line = line
}
// isEmpty returns whether the line has been completely read.
func (l *lexer) isEmpty() bool {
return l.lineReader.Len() == 0
}
// readRune reads a single rune and returns true if there was no error.
func (l *lexer) readRune() (rune, bool) {
r, _, err := l.lineReader.ReadRune()
ok := err == nil
return r, ok
}
// unreadRune unreads the last rune that was read.
func (l *lexer) unreadRune() {
l.lineReader.UnreadRune()
}
// peekString returns the remainder of the line without advancing the reader.
func (l *lexer) peekString() string {
offset := len(l.line) - l.lineReader.Len()
return l.line[offset:]
}
// advanceByRune advances the line reader by the given rune if and only if it
// matches.
func (l *lexer) advanceByRune(want rune) bool {
if r, ok := l.readRune(); ok {
if r == want {
return true
}
l.unreadRune()
}
return false
}
// advanceByString advances the line reader by the given string if and only if
// it is a prefix of the remaining string.
func (l *lexer) advanceByDelimiter() bool {
return l.advanceByString(l.delimiter())
}
// advanceByString advances the line reader by the given string if and only if
// it is a prefix of the remaining string.
func (l *lexer) advanceByString(s string) bool {
if strings.HasPrefix(l.peekString(), s) {
l.lineReader.Seek(int64(len(s)), io.SeekCurrent)
return true
}
return false
}
// scan scans a token from the line reader.
func (l *lexer) scan() token {
r, ok := l.readRune()
if !ok {
return token{kind: tokenEOF}
}
if unicode.IsSpace(r) {
l.unreadRune()
return token{kind: tokenSpace, text: l.readMatching(unicode.IsSpace)}
}
switch r {
case '\\':
return token{kind: tokenBackslash, text: `\`}
case '\'':
return token{kind: tokenSingleQuote, text: "'"}
case '"':
return token{kind: tokenDoubleQuote, text: `"`}
case '`':
return token{kind: tokenBacktick, text: "`"}
case '-':
if l.advanceByRune('-') {
return token{kind: tokenSingleComment, text: "--"}
}
case '#':
return token{kind: tokenSingleComment, text: "#"}
case '/':
if l.advanceByRune('*') {
return token{kind: tokenOpenComment, text: "/*"}
}
case '*':
if l.advanceByRune('/') {
return token{kind: tokenCloseComment, text: "*/"}
}
}
return token{kind: tokenText, text: string(r)}
}
// readMatching reads runes matching matchFn into the query buffer.
func (l *lexer) readMatching(matchFn func(rune) bool) string {
var runes []rune
for {
r, ok := l.readRune()
if !ok {
break
}
if !matchFn(r) {
l.unreadRune()
break
}
runes = append(runes, r)
}
return string(runes)
}
// acceptEscapedRune reads the next character following an escape backslash and
// saves it to the query buffer.
func (l *lexer) acceptEscapedRune() {
if r, ok := l.readRune(); ok {
l.writeRune(r)
}
}
// acceptString reads a MySQL string bounded by single-quotes, double-quotes, or
// backticks and saves it to the query buffer.
func (l *lexer) acceptString() {
for !l.isEmpty() {
tok := l.scan()
l.writeString(tok.text)
switch tok.kind {
case tokenBackslash:
l.acceptEscapedRune()
case l.inStringToken.kind:
// this must be a closing quote
l.setCloseString()
return
}
}
}
// discardRemaining discards the remainder of the line.
func (l *lexer) discardRemaining() {
l.lineReader.Reset("")
}
// discardMatching discards runes matching matchFn.
func (l *lexer) discardMatching(matchFn func(rune) bool) {
for {
r, ok := l.readRune()
if !ok {
break
}
if !matchFn(r) {
l.unreadRune()
break
}
}
}
// discardWhitespace discards leading whitespace.
func (l *lexer) discardWhitespace() {
l.discardMatching(unicode.IsSpace)
}
// discardSingleLineComment discards the rest of the line following a comment.
func (l *lexer) discardSingleLineComment() {
l.discardRemaining()
}
// discardMultiLineComment discards everything inside of a multiline comment.
func (l *lexer) discardMultiLineComment() {
for !l.isEmpty() {
l.discardMatching(func(r rune) bool {
// shortcut: scan until we get a potential end of comment token
// note that escapes don't work in comments, so we don't check for them
return r != '*'
})
if tok := l.scan(); tok.kind == tokenCloseComment {
l.setCloseComment()
return
}
}
}
+298
View File
@@ -0,0 +1,298 @@
// Teleport
// Copyright (C) 2025 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package repl
import (
"testing"
"unicode"
"github.com/stretchr/testify/require"
)
func TestDelimiter(t *testing.T) {
t.Parallel()
var lex lexer
require.Equal(t, clientDefaultStatementDelimiter, lex.delimiter())
tests := []struct {
desc string
args string
wantDelimiter string
wantErrContains string
}{
{
desc: "passing no arg is ok",
wantDelimiter: ";",
},
{
desc: "passing the default delimiter is ok",
args: ";",
wantDelimiter: ";",
},
{
desc: "passing two dollars is ok",
args: "$$",
wantDelimiter: "$$",
},
{
desc: "passing two slashes is ok",
args: "//",
wantDelimiter: "//",
},
{
desc: "passing too many delimiter runes is invalid",
args: ";;;;;;",
wantErrContains: `DELIMITER ";;;;;;" does not match regex used for validation "^(;|[/]{2}|[$]{2})$"`,
},
{
desc: "passing rejected runes is invalid",
args: "x",
wantErrContains: `DELIMITER "x" does not match regex used for validation "^(;|[/]{2}|[$]{2})$"`,
},
{
desc: "passing rejected runes is invalid",
args: "\\",
wantErrContains: `DELIMITER "\\" does not match regex used for validation "^(;|[/]{2}|[$]{2})$"`,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
var lex lexer
err := lex.setDelimiter(test.args)
if test.wantErrContains != "" {
require.ErrorContains(t, err, test.wantErrContains)
return
}
require.NoError(t, err)
require.Equal(t, test.wantDelimiter, lex.delimiter())
})
}
}
func TestLexer_setLine(t *testing.T) {
t.Parallel()
var l lexer
l.setLine("SELECT * FROM t;")
require.Equal(t, "SELECT * FROM t;", l.peekString())
require.False(t, l.isEmpty())
}
func TestLexer_advanceByRune(t *testing.T) {
t.Parallel()
var l lexer
l.setLine("abc")
require.True(t, l.advanceByRune('a'), "should advance on matching rune")
require.Equal(t, "bc", l.peekString())
require.False(t, l.advanceByRune('z'), "should not advance on non-matching rune")
require.Equal(t, "bc", l.peekString())
}
func TestLexer_advanceByString(t *testing.T) {
t.Parallel()
var l lexer
l.setLine("foobar")
require.True(t, l.advanceByString("foo"), "should advance by matching prefix")
require.Equal(t, "bar", l.peekString())
require.False(t, l.advanceByString("baz"), "should not advance if prefix doesn't match")
require.Equal(t, "bar", l.peekString())
}
func TestLexer_advanceByDelimiter(t *testing.T) {
t.Parallel()
var l lexer
l.setDelimiter(";")
l.setLine("; SELECT")
require.True(t, l.advanceByDelimiter())
require.Equal(t, " SELECT", l.peekString())
require.False(t, l.advanceByDelimiter())
require.Equal(t, " SELECT", l.peekString())
l.setLine("//")
l.setDelimiter("//")
require.True(t, l.advanceByDelimiter())
require.Empty(t, l.peekString())
require.False(t, l.advanceByDelimiter())
}
func TestLexer_getQuery(t *testing.T) {
t.Parallel()
var l lexer
l.writeString("hello")
l.writeRune(' ')
l.writeString("world!")
require.Equal(t, "hello world!", l.getQuery())
require.Empty(t, l.getQuery(), "getQuery should reset buffer")
}
func TestLexer_unread(t *testing.T) {
t.Parallel()
var l lexer
l.setLine("xy")
r, ok := l.readRune()
require.True(t, ok)
require.Equal(t, 'x', r)
l.unreadRune()
r, ok = l.readRune()
require.True(t, ok)
require.Equal(t, 'x', r)
r, ok = l.readRune()
require.True(t, ok)
require.Equal(t, 'y', r)
_, ok = l.readRune()
require.False(t, ok, "should be false at end of input")
l.unreadRune()
_, ok = l.readRune()
require.False(t, ok, "cant unread eof")
}
func TestLexer_readMatching(t *testing.T) {
t.Parallel()
var l lexer
l.setLine("123abcXYZ")
require.Equal(t, "123", l.readMatching(unicode.IsDigit))
require.Equal(t, "abc", l.readMatching(unicode.IsLower))
require.Equal(t, "XYZ", l.readMatching(unicode.IsUpper))
require.Empty(t, l.readMatching(unicode.IsDigit))
}
func TestLexer_acceptEscapedRune(t *testing.T) {
t.Parallel()
var l lexer
l.setLine(`\xyz`)
tok := l.scan()
require.Equal(t, tokenBackslash, tok.kind)
l.writeString(tok.text)
l.acceptEscapedRune()
require.Equal(t, "yz", l.peekString())
require.Equal(t, `\x`, l.getQuery())
}
func TestLexer_acceptString(t *testing.T) {
t.Parallel()
var l lexer
l.setLine(`"inside string "; SELECT`)
tok := l.scan()
require.Equal(t, tokenDoubleQuote, tok.kind)
l.writeString(tok.text)
l.setOpenString(tok)
l.acceptString()
require.Equal(t, "; SELECT", l.peekString())
require.Equal(t, `"inside string "`, l.getQuery())
}
func TestLexer_discardRemaining(t *testing.T) {
t.Parallel()
var l lexer
l.setLine("DROP TABLE test;")
l.discardRemaining()
require.True(t, l.isEmpty())
require.Empty(t, l.peekString())
}
func TestLexer_discardMatching(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
matchFn func(rune) bool
expected string
}{
{
name: "discard digits",
input: "123abc",
matchFn: unicode.IsDigit,
expected: "abc",
},
{
name: "discard lowercase",
input: "abcDEF",
matchFn: unicode.IsLower,
expected: "DEF",
},
{
name: "nothing to discard",
input: "ABC123",
matchFn: unicode.IsLower,
expected: "ABC123",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var l lexer
l.setLine(tc.input)
l.discardMatching(tc.matchFn)
require.Equal(t, tc.expected, l.peekString())
})
}
}
func TestLexer_discardWhitespace(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
expected string
}{
{
name: "leading whitespace only",
input: " SELECT",
expected: "SELECT",
},
{
name: "no whitespace",
input: "SELECT",
expected: "SELECT",
},
{
name: "whitespace and tabs",
input: "\t \n abc",
expected: "abc",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var l lexer
l.setLine(tc.input)
l.discardWhitespace()
require.Equal(t, tc.expected, l.peekString())
})
}
}
func TestLexer_discardSingleLineComment(t *testing.T) {
t.Parallel()
var l lexer
l.setLine("-- This is a comment")
l.discardSingleLineComment()
require.True(t, l.isEmpty())
require.Empty(t, l.peekString())
}
func TestLexer_discardMultiLineComment(t *testing.T) {
t.Parallel()
var l lexer
l.setLine("inside comment */ SELECT 1;")
l.setOpenComment()
l.discardMultiLineComment()
require.False(t, l.isInComment(), "should have exited comment mode")
remaining := l.peekString()
require.Equal(t, " SELECT 1;", remaining)
}
+195
View File
@@ -0,0 +1,195 @@
// Teleport
// Copyright (C) 2025 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package repl
import (
"iter"
"strings"
"time"
"github.com/gravitational/trace"
)
func newParser() (*parser, error) {
commands, err := newCommands()
if err != nil {
return nil, trace.Wrap(err)
}
return &parser{commands: commands}, nil
}
// parser parses commands and queries given one or more lines of input.
type parser struct {
commands *commandManager
lex lexer
}
// parse returns an iterator over the input line yielding evaluators for each
// command or query found.
func (p *parser) parse(line string) iter.Seq[evaluator] {
p.lex.setLine(line)
return func(yield func(evaluator) bool) {
defer func() {
if p.lex.isEmpty() && p.lex.isInQuery() {
// must be a multiline query, so insert a linebreak in the query buf
p.lex.writeString(lineBreak)
}
}()
for !p.lex.isEmpty() {
if p.lex.isInComment() {
// dont preserve comments
p.lex.discardMultiLineComment()
continue
}
if !p.lex.isInQuery() {
// not in a comment or query: is the rest of the line a command?
if cmd := p.tryParseCommand(); cmd != nil {
if !yield(cmd) {
return
}
continue
}
}
if p.lex.isInString() {
p.lex.acceptString()
continue
}
// not in comment nor string: do we have a command/query delimiter?
if cmdOrQuery := p.tryParseQuery(); cmdOrQuery != nil {
if !yield(cmdOrQuery) {
return
}
continue
}
// not in comment nor string and no delimiter found: scan text
tok := p.lex.scan()
switch tok.kind {
case tokenSingleComment:
// dont preserve comments
p.lex.discardSingleLineComment()
case tokenOpenComment:
p.lex.setOpenComment()
p.lex.discardMultiLineComment()
case tokenBackslash:
p.lex.writeString(tok.text)
p.lex.acceptEscapedRune()
case tokenSingleQuote, tokenDoubleQuote, tokenBacktick:
p.lex.writeString(tok.text)
p.lex.setOpenString(tok)
p.lex.acceptString()
default:
p.lex.writeString(tok.text)
}
}
}
}
// tryParseCommand tries to parse a command and its args on the current line.
// If the line contains the current delimiter and the command is not the special
// DELIMITER command, then give up and let the query parser handle it.
func (p *parser) tryParseCommand() evaluator {
p.lex.discardWhitespace()
line := p.lex.peekString()
cmd, args, err := p.commands.findCommand(line)
switch {
case err != nil:
if strings.Contains(line, p.lex.delimiter()) {
return nil
}
p.lex.discardRemaining()
return errorEvaluator{err: err}
case cmd != nil:
if cmd.name != "delimiter" && strings.Contains(line, p.lex.delimiter()) {
return nil
}
p.lex.discardRemaining()
return commandEvaluator{cmd: cmd, args: args}
default:
return nil
}
}
// tryParseQuery looks for a delimiter in the input. If it finds one and the
// query buffer is all from one line, then it first looks for a command in the
// query buffer. If there is no command or the query buffer is multiline, then
// it returns a query evaluator.
func (p *parser) tryParseQuery() evaluator {
if p.lex.advanceByDelimiter() {
query := p.lex.getQuery()
if !p.lex.isMultilineQuery() || strings.HasPrefix(query, p.commands.shortcutPrefix) {
cmd, args, err := p.commands.findCommand(query)
switch {
case err != nil:
// don't discard the rest of the line, since we found the
// command in the query buffer, unlike the command parser which
// looks ahead in the current line for a command
return errorEvaluator{err: err}
case cmd != nil:
return commandEvaluator{cmd: cmd, args: args}
}
}
return queryEvaluator(query)
}
return nil
}
type evaluator interface {
eval(r *REPL) (reply string, exit bool)
}
// queryEvaluator executes a single query and formats the response as a reply
// to the REPL client.
type queryEvaluator string
func (query queryEvaluator) eval(r *REPL) (string, bool) {
start := time.Now()
result, err := r.myConn.Execute(string(query))
if err != nil {
return errorReplyPrefix + err.Error(), false
}
defer result.Close()
if r.disableQueryTimings {
return formatResult(result, nil), false
}
elapsed := time.Since(start)
return formatResult(result, &elapsed), false
}
// commandEvaluator executes a command.
type commandEvaluator struct {
cmd *command
args string
}
func (e commandEvaluator) eval(r *REPL) (string, bool) {
return e.cmd.execFunc(r, e.args)
}
type errorEvaluator struct {
err error
}
// errorEvaluator formats an error as a reply to the REPL client.
func (e errorEvaluator) eval(_ *REPL) (string, bool) {
return e.err.Error(), false
}
+281
View File
@@ -0,0 +1,281 @@
// Teleport
// Copyright (C) 2025 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package repl
import (
"cmp"
"context"
"errors"
"fmt"
"io"
"iter"
"net"
"strings"
"time"
"github.com/go-mysql-org/go-mysql/client"
"github.com/go-mysql-org/go-mysql/mysql"
"github.com/gravitational/trace"
"golang.org/x/term"
"github.com/gravitational/teleport"
clientproto "github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/lib/asciitable"
dbrepl "github.com/gravitational/teleport/lib/client/db/repl"
)
// REPL implements [dbrepl.REPLInstance] for MySQL.
type REPL struct {
client io.ReadWriteCloser
serverConn net.Conn
route clientproto.RouteToDatabase
term *term.Terminal
parser *parser
myConn mysqlConn
// teleportVersion is used in golden tests to fake the current Teleport
// version and prevent test failures when the real version is incremented.
teleportVersion string
// testPassword is normally blank, only used in tests where the REPL connects
// directly to a MySQL instance without Teleport proxying.
testPassword string
// disableQueryTimings is used in golden tests to disable query timings for
// test consistency.
disableQueryTimings bool
}
type mysqlConn interface {
Execute(command string, args ...any) (*mysql.Result, error)
UseDB(dbName string) error
GetServerVersion() string
}
// New implements [dbrepl.REPLNewFunc].
func New(_ context.Context, cfg *dbrepl.NewREPLConfig) (dbrepl.REPLInstance, error) {
parser, err := newParser()
if err != nil {
return nil, trace.Wrap(err)
}
return &REPL{
client: cfg.Client,
serverConn: cfg.ServerConn,
route: cfg.Route,
term: term.NewTerminal(cfg.Client, ""),
parser: parser,
}, nil
}
// Run starts and run the PostgreSQL REPL session. The provided context is used
// to interrupt the execution and clean up resources.
func (r *REPL) Run(ctx context.Context) error {
dialer := func(context.Context, string, string) (net.Conn, error) {
return r.serverConn, nil
}
const hostnamePlaceholder = "repl"
myConn, err := client.ConnectWithDialer(ctx, "tcp", hostnamePlaceholder,
r.route.GetUsername(),
r.testPassword,
r.route.GetDatabase(),
dialer,
withClientCapabilities(
mysql.CLIENT_MULTI_RESULTS,
mysql.CLIENT_MULTI_STATEMENTS,
),
func(c *client.Conn) error {
c.SetAttributes(map[string]string{clientNameParamName: clientNameParamValue})
return nil
},
)
if err != nil {
return trace.ConnectionProblem(err, "Unable to connect to database: %v", err)
}
r.myConn = myConn
defer func() {
_ = myConn.Close()
}()
// term.Terminal blocks reads/writes without respecting the context. The
// only thing that unblocks it is closing the underlaying connection (in
// our case r.client). On this goroutine we only watch for context
// cancelation and close the connection. This will unblocks all terminal
// reads/writes.
stop := context.AfterFunc(ctx, func() {
_ = r.client.Close()
})
defer stop()
if err := r.presentBanner(); err != nil {
return trace.Wrap(err)
}
for {
line, err := r.readLine()
if err != nil {
return trace.Wrap(rewriteTermError(ctx, err))
}
for reply, exit := range r.eval(line) {
if exit {
return nil
}
if err := r.print(reply); err != nil {
return trace.Wrap(err)
}
}
}
}
func withClientCapabilities(caps ...uint32) client.Option {
return func(conn *client.Conn) error {
for _, cap := range caps {
conn.SetCapability(cap)
}
return nil
}
}
func (r *REPL) presentBanner() error {
_, err := fmt.Fprintf(
r.term,
`Teleport MySQL interactive shell (v%s)
Connected to instance %q as user %q.
Type 'help' or '\h' for help.
`,
cmp.Or(r.teleportVersion, teleport.Version),
r.route.GetServiceName(),
r.route.GetUsername())
return trace.Wrap(err)
}
func (r *REPL) readLine() (string, error) {
r.term.SetPrompt(r.getPrompt())
return r.term.ReadLine()
}
func (r *REPL) getPrompt() string {
dbName := formatDatabaseName(r.route.Database)
var suffix string
switch {
case r.parser.lex.isInComment():
suffix = "/*"
case r.parser.lex.isInString():
suffix = r.parser.lex.inStringKind()
case r.parser.lex.isInQuery():
suffix = "-"
default:
suffix = dbName
}
padding := len(dbName)
// pad the suffix with leading spaces until it is len(dbName), for example:
// default: "mysql>"
// comment: " */>"
// string: " '>"
// query: " ->"
return fmt.Sprintf("%*s> ", padding, suffix)
}
func (r *REPL) eval(line string) iter.Seq2[string, bool] {
return func(yield func(string, bool) bool) {
for evaluator := range r.parser.parse(line) {
if !yield(evaluator.eval(r)) {
return
}
}
}
}
// print writes a reply to the REPL client.
func (r *REPL) print(reply string) error {
reply = strings.TrimSpace(reply)
if reply == "" {
return nil
}
_, err := r.term.Write([]byte(lineBreak + reply + lineBreak + lineBreak))
return trace.Wrap(err)
}
// rewriteTermError changes the term.Terminal error to match caller expectations.
func rewriteTermError(ctx context.Context, err error) error {
// When context is canceled it will immediately lead read/write errors due
// to the closed connection. For this cases we return the context error.
if ctx.Err() != nil && (errors.Is(err, io.ErrClosedPipe) || errors.Is(err, net.ErrClosed)) {
return ctx.Err()
}
if errors.Is(err, io.EOF) {
return nil
}
return err
}
// formatResult formats a query result.
func formatResult(result *mysql.Result, elapsed *time.Duration) string {
var out string
if result.Resultset != nil {
out = formatResultset(result.Resultset)
} else {
out = fmt.Sprintf("%v warnings, %v rows affected", result.Warnings, result.AffectedRows)
}
if elapsed != nil {
return fmt.Sprintf("%s (%.2f sec)", out, elapsed.Seconds())
}
return out
}
// formatResultset formats a query result that contains rows.
func formatResultset(set *mysql.Resultset) string {
if set == nil || len(set.Values) == 0 {
return "Empty set"
}
var sb strings.Builder
columns := make([]string, 0, len(set.Fields))
for _, f := range set.Fields {
columns = append(columns, string(f.Name))
}
table := asciitable.MakeTable(columns)
for _, rowValues := range set.Values {
row := make([]string, 0, len(rowValues))
for _, val := range rowValues {
row = append(row, val.String())
}
table.AddRow(row)
}
table.WriteTo(&sb)
sb.WriteString(lineBreak)
if numRows := len(set.Values); numRows == 1 {
sb.WriteString("1 row in set")
} else {
// pluralize 0 or multiple rows
fmt.Fprintf(&sb, "%d rows in set", numRows)
}
return sb.String()
}
const (
// lineBreak represents a line break on the REPL.
lineBreak = "\n"
// errorReplyPrefix is the prefix presented when there is a execution error.
errorReplyPrefix = "ERR "
// clientNameParamName defines the application name parameter name.
//
// https://dev.mysql.com/doc/refman/8.0/en/performance-schema-connection-attribute-tables.html
clientNameParamName = "_client_name"
// clientNameParamValue defines the application name parameter value.
clientNameParamValue = "teleport-repl"
)
+455
View File
@@ -0,0 +1,455 @@
// Teleport
// Copyright (C) 2025 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package repl
import (
"bytes"
"cmp"
"context"
"crypto/rand"
_ "embed"
"io"
"net"
"os"
"strings"
"sync"
"testing"
"time"
"github.com/go-mysql-org/go-mysql/mysql"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
mysqlcontainer "github.com/testcontainers/testcontainers-go/modules/mysql"
"golang.org/x/term"
clientproto "github.com/gravitational/teleport/api/client/proto"
dbrepl "github.com/gravitational/teleport/lib/client/db/repl"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/utils/testutils/golden"
)
var testSrv *testServer
var testSrvMu sync.Mutex
func TestMain(m *testing.M) {
code := m.Run()
// The "Ryuk" container will cleanup everything anyhow, but only after 10s
// without any client connections.
// We can speed up container cleanup after a -count=N run, e.g., a flaky
// test detector run, by terminating them immediately after tests have run.
// https://github.com/testcontainers/moby-ryuk
testSrvMu.Lock()
defer testSrvMu.Unlock()
// if we don't clean up after 10 seconds, RYUK will get them anyway.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// ignore errors, Ryuk or worst case VM termination in CI will deal with it
_ = testSrv.container.Terminate(ctx)
os.Exit(code)
}
func TestREPL(t *testing.T) {
testSrv := newTestServer(t)
route := clientproto.RouteToDatabase{
ServiceName: "mysql-test-container",
Protocol: defaults.ProtocolMySQL,
Username: testSrv.username,
// Pass a blank database name, as if a user who didn't specify one.
// The REPL should handle this case gracefully.
Database: "",
}
for _, test := range []struct {
name string
input string
}{
{
name: "ddl",
input: testDDL,
},
{
name: "all data types",
input: testDataTypes,
},
{
name: "literals",
input: testLiterals,
},
{
name: "commands",
input: testCommands,
},
} {
t.Run(test.name, func(t *testing.T) {
// the term only recognizes line breaks with a carriage-return
const crlf = "\r\n"
input := strings.ReplaceAll(test.input, "\n", crlf) + crlf
recorder := newRecordingClient(t, io.NopCloser(strings.NewReader(input)))
testREPL, err := New(t.Context(), &dbrepl.NewREPLConfig{
Client: recorder,
ServerConn: testSrv.connectTCP(t, 10*time.Second),
Route: route,
})
require.NoError(t, err)
testREPL.(*REPL).testPassword = testSrv.password
testREPL.(*REPL).disableQueryTimings = true
err = testREPL.Run(t.Context())
require.NoError(t, err)
goldenName := strings.ReplaceAll(test.name, " ", "-")
if golden.ShouldSet() {
golden.SetNamed(t, goldenName, recorder.buf.Bytes())
}
require.Equal(t, string(golden.GetNamed(t, goldenName)), recorder.buf.String())
})
}
}
var (
//go:embed fixtures/ddl.sql
testDDL string
//go:embed fixtures/all-data-types.sql
testDataTypes string
//go:embed fixtures/literals.sql
testLiterals string
//go:embed fixtures/commands.sql
testCommands string
)
func TestGetPrompt(t *testing.T) {
t.Parallel()
tests := []struct {
desc string
inQuery bool
inComment bool
inStringKind string
database string
want string
}{
{
desc: "no default database",
want: "(none)> ",
},
{
desc: "default",
database: "llama",
want: "llama> ",
},
{
desc: "in a query",
inQuery: true,
database: "llama",
want: " -> ",
},
{
desc: "in a comment",
inComment: true,
database: "llama",
want: " /*> ",
},
{
desc: "in a quote string",
inStringKind: "'",
database: "llama",
want: " '> ",
},
{
desc: "in a double quote string",
inStringKind: `"`,
database: "llama",
want: ` "> `,
},
{
desc: "in a backtick string",
inStringKind: "`",
database: "llama",
want: " `> ",
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
r := &REPL{
route: clientproto.RouteToDatabase{
Database: test.database,
},
parser: &parser{
lex: lexer{
inComment: test.inComment,
inStringToken: token{text: test.inStringKind},
},
},
teleportVersion: "v19.0.0-dev",
}
if test.inQuery {
r.parser.lex.queryBuf.WriteString("select")
}
require.Equal(t, test.want, r.getPrompt())
})
}
}
func FuzzEval(f *testing.F) {
parser, err := newParser()
require.NoError(f, err)
for _, cmd := range parser.commands.byName {
f.Add(cmd.name)
f.Add(parser.commands.shortcutPrefix + string(cmd.shortcut))
}
f.Add("") // eof
f.Add(" ") // space
// MySQL string literal quotes
f.Add("'")
f.Add(`"`)
f.Add("`")
// single line comments
f.Add("--")
f.Add("#")
// multiline comments
f.Add("/*")
f.Add("*/")
f.Add("*")
// valid delimiters
f.Add(";")
f.Add("$")
f.Add("/")
// some statement syntax
f.Add("select")
f.Add("create")
f.Add("drop")
f.Add("replace")
f.Add("alter")
f.Add("(")
f.Add(")")
// multiline
f.Add("\n")
f.Fuzz(func(t *testing.T, line string) {
repl := &REPL{
myConn: &fakeMySQLConn{
exec: func(command string, args ...any) (*mysql.Result, error) {
return nil, io.EOF
},
},
parser: parser,
}
require.NotPanics(t, func() {
for line := range strings.SplitSeq(line, "\n") {
for _, exit := range repl.eval(line) {
if exit {
return
}
}
}
})
})
}
func TestInteractively(t *testing.T) {
// You can get an interactive REPL to experiment with like this:
// $ export MYSQL_TEST_REPL_INTERACTIVELY=1
// $ go test -c -o ./test ./lib/client/db/mysql/repl
// $ ./test -test.v -test.count=1 -test.run TestInteractively
if os.Getenv("MYSQL_TEST_REPL_INTERACTIVELY") == "" {
t.Skip()
}
testSrv := newTestServer(t)
route := clientproto.RouteToDatabase{
ServiceName: "mysql-test-container",
Protocol: defaults.ProtocolMySQL,
Username: testSrv.username,
Database: testSrv.database,
Roles: []string{},
}
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
require.NoError(t, err)
defer term.Restore(int(os.Stdin.Fd()), oldState)
recorder := newRecordingClient(t, os.Stdin, os.Stdout)
repl, err := New(t.Context(), &dbrepl.NewREPLConfig{
Client: recorder,
ServerConn: testSrv.connectTCP(t, 10*time.Second),
Route: route,
})
require.NoError(t, err)
repl.(*REPL).testPassword = testSrv.password
require.NoError(t, repl.Run(t.Context()))
}
func Test_formatResult(t *testing.T) {
resultRows, err := mysql.BuildSimpleTextResultset(
[]string{"one", "two", "three"},
[][]any{{1, 2.2, "3"}, {4, 5.5, "6"}, {7, 8.8, "8"}},
)
require.NoError(t, err)
noRows, err := mysql.BuildSimpleTextResultset(
[]string{"one", "two", "three"},
nil,
)
require.NoError(t, err)
tests := []struct {
desc string
input *mysql.Result
elapsed time.Duration
}{
{
desc: "result rows",
input: &mysql.Result{Resultset: resultRows},
elapsed: time.Millisecond * 1234,
},
{
desc: "long elapsed time",
input: &mysql.Result{Resultset: resultRows},
elapsed: time.Millisecond * 123456,
},
{
desc: "empty set",
input: &mysql.Result{Resultset: noRows},
elapsed: time.Millisecond * 42,
},
{
desc: "nil set",
input: &mysql.Result{Warnings: 2, AffectedRows: 42},
elapsed: 0,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
mustParseResult(t, test.input)
got := formatResult(test.input, &test.elapsed)
goldenName := strings.ReplaceAll(test.desc, " ", "-")
if golden.ShouldSet() {
golden.SetNamed(t, goldenName, []byte(got))
}
require.Equal(t, string(golden.GetNamed(t, goldenName)), got)
})
}
}
func mustParseResult(t *testing.T, result *mysql.Result) {
t.Helper()
if result.Resultset == nil {
return
}
result.Values = make([][]mysql.FieldValue, len(result.RowDatas))
for i := range result.RowDatas {
const isBinary = false
v, err := result.RowDatas[i].Parse(result.Fields, isBinary, result.Values[i])
result.Values[i] = v
require.NoError(t, err)
}
}
func newTestServer(t *testing.T) *testServer {
t.Helper()
ctx := t.Context()
reuseName := cmp.Or(os.Getenv("MYSQL_TEST_SERVER_REUSE_CONTAINER_BY_NAME"), "default-mysql-test-server")
testSrvMu.Lock()
// hold the lock for the entire func to avoid parallel container requests
defer testSrvMu.Unlock()
if testSrv != nil {
return testSrv
}
user := cmp.Or(os.Getenv("MYSQL_TEST_SERVER_USER"), "root")
db := cmp.Or(os.Getenv("MYSQL_TEST_SERVER_DB"), "mysql")
pass := cmp.Or(os.Getenv("MYSQL_TEST_SERVER_PASS"), rand.Text())
opts := []testcontainers.ContainerCustomizer{
mysqlcontainer.WithDatabase(db),
mysqlcontainer.WithUsername(user),
mysqlcontainer.WithPassword(pass),
testcontainers.WithReuseByName(reuseName),
}
// MySQL 8.4.6 index digest
// https://docs.docker.com/dhi/core-concepts/digests/#multi-platform-images-and-manifests
// https://hub.docker.com/layers/library/mysql/8.4.6/images/sha256-9b9413211d004062e6d5be1118e1051773fe66d832ec9ebcae1c2fcce5af5f5b
const img = "mysql@sha256:d2c60b1b225c6d7845f0abdb596fc35c2d4122bcad6ec219588035a118f75d93"
container, err := mysqlcontainer.Run(ctx, img, opts...)
require.NoError(t, err)
host, err := container.Host(ctx)
require.NoError(t, err)
mappedPort, err := container.MappedPort(ctx, "3306/tcp")
require.NoError(t, err)
srv := &testServer{
container: container,
host: host,
port: mappedPort.Port(),
username: user,
password: pass,
database: db,
}
testSrv = srv
return srv
}
// testServer is a MySQL server for tests.
type testServer struct {
// container is the container hosting the server.
container testcontainers.Container
// host is the MySQL connection endpoint host.
host string
// port is the MySQL connection endpoint port.
port string
// username is the database superuser.
username string
// password is the database superuser password.
password string
// database is a database schema provisioned in the database.
database string
}
// hostPort returns the server host:port.
func (s *testServer) hostPort() string {
return net.JoinHostPort(s.host, s.port)
}
func (s *testServer) connectTCP(t *testing.T, timeout time.Duration) net.Conn {
t.Helper()
conn, err := net.DialTimeout("tcp", s.hostPort(), timeout)
require.NoError(t, err)
return conn
}
func newRecordingClient(t *testing.T, reader io.ReadCloser, writers ...io.Writer) *recordingClient {
t.Helper()
var buf bytes.Buffer
writers = append(writers, &buf)
return &recordingClient{
ReadCloser: reader,
Writer: io.MultiWriter(writers...),
buf: &buf,
}
}
type recordingClient struct {
io.ReadCloser
io.Writer
buf *bytes.Buffer
}
@@ -0,0 +1,255 @@
Teleport MySQL interactive shell (v19.0.0-dev)
Connected to instance "mysql-test-container" as user "root".
Type 'help' or '\h' for help.
(none)> -- temporarily disable note warnings for test consistency
(none)> set session sql_notes = 0;
0 warnings, 0 rows affected
(none)> CREATE DATABASE IF NOT EXISTS all_data_types;
0 warnings, 1 rows affected
(none)> set session sql_notes = 1;
0 warnings, 0 rows affected
(none)> USE all_data_types;
Default database changed to "all_data_types"
all_data_types>
all_data_types> -- without ANSI_QUOTES double quotes are literals
all_data_types> SET SESSION sql_mode='';
0 warnings, 0 rows affected
all_data_types> SELECT "thing";
thing
-------
'thing'
1 row in set
all_data_types> -- with ANSI_QUOTES double quotes are identifiers, which is what
we want when
all_data_types> -- selecting from the table below
all_data_types> SET SESSION sql_mode='ANSI_QUOTES';
0 warnings, 0 rows affected
all_data_types> SELECT "thing";
ERR ERROR 1054 (42S22): Unknown column 'thing' in 'field list'
all_data_types>
all_data_types> CREATE TABLE all_types_demo (
-> "c tinyint" TINYINT,
-> c_smallint SMALLINT,
-> c_mediumint MEDIUMINT,
-> c_int INT,
-> c_bigint BIGINT,
-> c_decimal DECIMAL(10,2),
-> c_float FLOAT,
-> c_double DOUBLE,
-> c_bit BIT(8),
-> c_bool BOOL,
-> c_date DATE,
-> c_time TIME,
-> c_datetime DATETIME,
-> c_timestamp TIMESTAMP NULL DEFAULT NULL,
-> c_year YEAR,
-> c_char CHAR(5),
-> c_varchar VARCHAR(10),
-> c_binary BINARY(3),
-> c_varbinary VARBINARY(4),
-> c_tinyblob TINYBLOB,
-> c_blob BLOB,
-> c_mediumblob MEDIUMBLOB,
-> c_longblob LONGBLOB,
-> c_tinytext TINYTEXT,
-> c_text TEXT,
-> c_mediumtext MEDIUMTEXT,
-> c_longtext LONGTEXT,
-> c_enum ENUM('foo','bar','baz'),
-> c_set SET('a','b','c'),
-> c_geometry GEOMETRY,
-> c_point POINT,
-> c_linestring LINESTRING,
-> c_polygon POLYGON,
-> c_multipoint MULTIPOINT,
-> c_multilinestring MULTILINESTRING,
-> c_multipolygon MULTIPOLYGON,
-> c_geometrycoll GEOMETRYCOLLECTION,
-> c_json JSON
-> );
0 warnings, 0 rows affected
all_data_types>
all_data_types> INSERT INTO all_types_demo VALUES (
-> 127,
-> 32767,
-> 8388607,
-> 2147483647,
-> 9223372036854775807,
-> 12345.67,
-> 3.14,
-> 2.7182818284,
-> b'10101010',
-> 1,
-> '2024-07-09',
-> '12:34:56',
-> '2024-07-09 12:34:56',
-> '2024-07-09 12:34:56',
-> 2025,
-> 'abcde',
-> 'abcdefghij',
-> 'abc',
-> 'defg',
-> 'a',
-> 'blob',
-> 'medblob',
-> 'longblob',
-> 'tiny',
-> 'text',
-> 'mediumtext',
-> 'longtext',
-> 'foo',
-> 'a,b',
-> ST_GeomFromText('POINT(1 1)'),
-> ST_GeomFromText('POINT(2 2)'),
-> ST_GeomFromText('LINESTRING(0 0,1 1,2 2)'),
-> ST_GeomFromText('POLYGON((0 0,1 0,1 1,0 1,0 0))'),
-> ST_GeomFromText('MULTIPOINT((0 0),(1 1))'),
-> ST_GeomFromText('MULTILINESTRING((0 0,1 1),(2 2,3 3))'),
-> ST_GeomFromText('MULTIPOLYGON(((0 0,1 0,1 1,0 1,0 0)))'),
-> ST_GeomFromText('GEOMETRYCOLLECTION(POINT(1 2),LINESTRING(0
0,1 1))'),
-> JSON_OBJECT('x', 1, 'y', 2)
-> );
0 warnings, 1 rows affected
all_data_types>
all_data_types> SELECT "c tinyint", `c_smallint`, "c_mediumint" FROM `all_types_
demo`;
c tinyint c_smallint c_mediumint
--------- ---------- -----------
127 32767 8388607
1 row in set
all_data_types> SELECT c_int, c_bigint, c_decimal FROM all_types_demo;
c_int c_bigint c_decimal
---------- ------------------- ----------
2147483647 9223372036854775807 '12345.67'
1 row in set
all_data_types> SELECT c_float, c_double, c_bit FROM all_types_demo;
c_float c_double c_bit
------- ------------ -----
3.14 2.7182818284 'ª'
1 row in set
all_data_types> SELECT c_bool, c_date, c_time FROM all_types_demo;
c_bool c_date c_time
------ ------------ ----------
1 '2024-07-09' '12:34:56'
1 row in set
all_data_types> SELECT c_datetime, c_timestamp, c_year FROM all_types_demo;
c_datetime c_timestamp c_year
--------------------- --------------------- ------
'2024-07-09 12:34:56' '2024-07-09 12:34:56' 2025
1 row in set
all_data_types> SELECT c_char, c_varchar, c_binary FROM all_types_demo;
c_char c_varchar c_binary
------- ------------ --------
'abcde' 'abcdefghij' 'abc'
1 row in set
all_data_types> SELECT c_varbinary, c_tinyblob, c_blob FROM all_types_demo;
c_varbinary c_tinyblob c_blob
----------- ---------- ------
'defg' 'a' 'blob'
1 row in set
all_data_types> SELECT "c_mediumblob",
-> `c_longblob`,
-> c_tinytext
-> FROM all_types_demo;
c_mediumblob c_longblob c_tinytext
------------ ---------- ----------
'medblob' 'longblob' 'tiny'
1 row in set
all_data_types> SELECT c_text, c_mediumtext, c_longtext FROM all_types_demo;
c_text c_mediumtext c_longtext
------ ------------ ----------
'text' 'mediumtext' 'longtext'
1 row in set
all_data_types> SELECT c_enum, c_set, ST_AsText(c_geometry) AS c_geometry FROM a
ll_types_demo;
c_enum c_set c_geometry
------ ----- ------------
'foo' 'a,b' 'POINT(1 1)'
1 row in set
all_data_types> SELECT ST_AsText(c_point) AS c_point, ST_AsText(c_linestring) AS
c_linestring, ST_AsText(c_polygon) AS c_polygon FROM all_types_demo;
c_point c_linestring c_polygon
------------ ------------------------- --------------------------------
'POINT(2 2)' 'LINESTRING(0 0,1 1,2 2)' 'POLYGON((0 0,1 0,1 1,0 1,0 0))'
1 row in set
all_data_types> SELECT ST_AsText(c_multipoint) AS c_multipoint, ST_AsText(c_mult
ilinestring) AS c_multilinestring, ST_AsText(c_multipolygon) AS c_multipolygon F
ROM all_types_demo;
c_multipoint c_multilinestring c_multipolygon
------------------------- -------------------------------------- ---------------------------------------
'MULTIPOINT((0 0),(1 1))' 'MULTILINESTRING((0 0,1 1),(2 2,3 3))' 'MULTIPOLYGON(((0 0,1 0,1 1,0 1,0 0)))'
1 row in set
all_data_types> SELECT ST_AsText(c_geometrycoll) AS c_geometrycoll, c_json FROM
all_types_demo;
c_geometrycoll c_json
---------------------------------------------------- ------------------
'GEOMETRYCOLLECTION(POINT(1 2),LINESTRING(0 0,1 1))' '{"x": 1, "y": 2}'
1 row in set
all_data_types> DROP DATABASE all_data_types;
0 warnings, 1 rows affected
all_data_types>
all_data_types>
@@ -0,0 +1,325 @@
Teleport MySQL interactive shell (v19.0.0-dev)
Connected to instance "mysql-test-container" as user "root".
Type 'help' or '\h' for help.
(none)> -- temporarily disable note warnings for test consistency
(none)> set session sql_notes = 0;
0 warnings, 0 rows affected
(none)> create database if not exists test_commands;
0 warnings, 1 rows affected
(none)> create database if not exists foo;
0 warnings, 1 rows affected
(none)> create database if not exists bar;
0 warnings, 1 rows affected
(none)> set session sql_notes = 1;
0 warnings, 0 rows affected
(none)> use test_commands;
Default database changed to "test_commands"
test_commands>
test_commands> -- leading spaces/comments before commands
test_commands> /* just a comment */
test_commands> use foo;
Default database changed to "foo"
foo> use foo
Default database changed to "foo"
foo> /*
/*> llama
/*> */
foo> use foo;
Default database changed to "foo"
foo> /*
/*> llama
/*> */
foo> use foo
Default database changed to "foo"
foo> -- llama
foo> use foo;
Default database changed to "foo"
foo> -- llama
foo> use foo
Default database changed to "foo"
foo> -- llama
foo> use foo;
Default database changed to "foo"
foo> -- llama
foo> use foo
Default database changed to "foo"
foo> /* llama */use foo;
Default database changed to "foo"
foo> /* llama */use foo
Default database changed to "foo"
foo> /* llama */ use foo;
Default database changed to "foo"
foo> /* llama */ use foo
Default database changed to "foo"
foo> /* llama */
foo> use foo;
Default database changed to "foo"
foo> /* llama */
foo> use foo
Default database changed to "foo"
foo> /* llama
/*> */use foo;
Default database changed to "foo"
foo> /* llama
/*> */use foo
Default database changed to "foo"
foo> /* llama
/*> */ use foo;
Default database changed to "foo"
foo> /* llama
/*> */ use foo
Default database changed to "foo"
foo> /* llama
/*> -- llama
/*> */use foo;
Default database changed to "foo"
foo> /* llama
/*> -- llama
/*> */use foo
Default database changed to "foo"
foo> /* llama
/*> -- llama
/*> */ use foo;
Default database changed to "foo"
foo> /* llama
/*> -- llama
/*> */ use foo
Default database changed to "foo"
foo> /* llama
/*> -- */use foo;
Default database changed to "foo"
foo> /* llama
/*> -- */use foo
Default database changed to "foo"
foo> /* llama
/*> -- */ use foo;
Default database changed to "foo"
foo> /* llama
/*> -- */ use foo
Default database changed to "foo"
foo> /* llama */
foo>
foo> use foo;use bar; use foo
Default database changed to "foo"
Default database changed to "bar"
Default database changed to "foo"
foo> -- this isn't a command, but that should just print an error message and he
lp info.
foo> \xyz select 1
Unknown command.
delimiter \d Set statement delimiter. With no argument the delimiter resets to the default.
help \h Show the list of supported commands.
quit \q Terminates the session.
status \s Get status information.
teleport \t Show Teleport interactive shell information, such as execution limitations.
use \u Use another database.
foo> \xyz;select 1;
Unknown command.
delimiter \d Set statement delimiter. With no argument the delimiter resets to the default.
help \h Show the list of supported commands.
quit \q Terminates the session.
status \s Get status information.
teleport \t Show Teleport interactive shell information, such as execution limitations.
use \u Use another database.
1
-
1
1 row in set
foo> delimiter $$
foo> help$$
delimiter \d Set statement delimiter. With no argument the delimiter resets to the default.
help \h Show the list of supported commands.
quit \q Terminates the session.
status \s Get status information.
teleport \t Show Teleport interactive shell information, such as execution limitations.
use \u Use another database.
foo> \d //
foo> \h
delimiter \d Set statement delimiter. With no argument the delimiter resets to the default.
help \h Show the list of supported commands.
quit \q Terminates the session.
status \s Get status information.
teleport \t Show Teleport interactive shell information, such as execution limitations.
use \u Use another database.
foo> help
delimiter \d Set statement delimiter. With no argument the delimiter resets to the default.
help \h Show the list of supported commands.
quit \q Terminates the session.
status \s Get status information.
teleport \t Show Teleport interactive shell information, such as execution limitations.
use \u Use another database.
foo> status
Teleport database: mysql-test-container
Current database: foo
Current user: root
Server version: 8.4.6
Using delimiter: //
foo> \s
Teleport database: mysql-test-container
Current database: foo
Current user: root
Server version: 8.4.6
Using delimiter: //
foo> teleport
Teleport MySQL interactive shell (v19.0.0-dev)
Limitations:
- Query cancellation is not supported. Once a query is sent, its execution
can only be canceled by killing it from another session.
You can identify the query process ID with "SHOW PROCESSLIST" and then kill the
query with "KILL QUERY <pid>".
- Pressing CTRL-C will have no effect in this shell.
foo> \t
Teleport MySQL interactive shell (v19.0.0-dev)
Limitations:
- Query cancellation is not supported. Once a query is sent, its execution
can only be canceled by killing it from another session.
You can identify the query process ID with "SHOW PROCESSLIST" and then kill the
query with "KILL QUERY <pid>".
- Pressing CTRL-C will have no effect in this shell.
foo> use foo
Default database changed to "foo"
foo> \u mysql//
Default database changed to "mysql"
mysql> -- reset the delimiter to default ;
mysql> \d
mysql> teleport;\s;\h
Teleport MySQL interactive shell (v19.0.0-dev)
Limitations:
- Query cancellation is not supported. Once a query is sent, its execution
can only be canceled by killing it from another session.
You can identify the query process ID with "SHOW PROCESSLIST" and then kill the
query with "KILL QUERY <pid>".
- Pressing CTRL-C will have no effect in this shell.
Teleport database: mysql-test-container
Current database: mysql
Current user: root
Server version: 8.4.6
Using delimiter: ;
delimiter \d Set statement delimiter. With no argument the delimiter resets to the default.
help \h Show the list of supported commands.
quit \q Terminates the session.
status \s Get status information.
teleport \t Show Teleport interactive shell information, such as execution limitations.
use \u Use another database.
mysql> use `bar`;drop database foo;drop database bar;drop database test_commands
;quit
Default database changed to "bar"
0 warnings, 0 rows affected
0 warnings, 0 rows affected
0 warnings, 0 rows affected
@@ -0,0 +1,221 @@
Teleport MySQL interactive shell (v19.0.0-dev)
Connected to instance "mysql-test-container" as user "root".
Type 'help' or '\h' for help.
(none)> -- temporarily disable note warnings for test consistency
(none)> set session sql_notes = 0;
0 warnings, 0 rows affected
(none)> DROP DATABASE IF EXISTS test_ddl_db;
0 warnings, 0 rows affected
(none)> CREATE DATABASE test_ddl_db;
0 warnings, 1 rows affected
(none)> set session sql_notes = 1;
0 warnings, 0 rows affected
(none)> -- this should produce a note level warning because it does exist
(none)> CREATE DATABASE if not exists test_ddl_db;
1 warnings, 1 rows affected
(none)> -- we clear the warning by selecting the warning count
(none)> select @@warning_count;
@@warning_count
---------------
1
1 row in set
(none)> USE test_ddl_db;
Default database changed to "test_ddl_db"
test_ddl_db>
test_ddl_db> CREATE TABLE test_table (
-> id INT PRIMARY KEY AUTO_INCREMENT,
-> value VARCHAR(50) NOT NULL,
-> note VARCHAR(100) DEFAULT NULL
-> ) ENGINE=InnoDB;
0 warnings, 0 rows affected
test_ddl_db>
test_ddl_db> CREATE VIEW test_view AS (
-> SELECT id, value
-> FROM test_table
-> );
0 warnings, 0 rows affected
test_ddl_db>
test_ddl_db> DELIMITER $$
test_ddl_db> CREATE FUNCTION test_func(x INT) RETURNS INT DETERMINISTIC
-> BEGIN
-> RETURN x * 2;
-> END $$
0 warnings, 0 rows affected
test_ddl_db> DELIMITER ;
test_ddl_db>
test_ddl_db> DELIMITER $$
test_ddl_db> CREATE PROCEDURE test_proc()
-> BEGIN
-> INSERT INTO test_table(value) VALUES ('from proc');
-> END $$
0 warnings, 0 rows affected
test_ddl_db> DELIMITER ;
test_ddl_db>
test_ddl_db> CALL test_proc();
0 warnings, 1 rows affected
test_ddl_db>
test_ddl_db> DELIMITER $$
test_ddl_db> CREATE TRIGGER test_trigger_before_insert
-> BEFORE INSERT ON test_table
-> FOR EACH ROW
-> BEGIN
-> SET NEW.value = UPPER(NEW.value);
-> END $$
0 warnings, 0 rows affected
test_ddl_db> DELIMITER ;
test_ddl_db>
test_ddl_db> CALL test_proc();
0 warnings, 1 rows affected
test_ddl_db>
test_ddl_db> CREATE EVENT test_event
-> ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 HOUR
-> DO INSERT INTO test_table(value) VALUES ('from event');
0 warnings, 0 rows affected
test_ddl_db>
test_ddl_db> set session sql_notes = 0;
0 warnings, 0 rows affected
test_ddl_db> -- roles are not database scoped, let's disable note warnings in ca
se some other
test_ddl_db> -- test has already created the role or user.
test_ddl_db> CREATE ROLE IF NOT EXISTS test_role;
0 warnings, 0 rows affected
test_ddl_db> DROP ROLE test_role;
0 warnings, 0 rows affected
test_ddl_db>
test_ddl_db> CREATE USER IF NOT EXISTS test_user@'localhost'
-> IDENTIFIED BY 'some password'
-> ACCOUNT LOCK;
0 warnings, 0 rows affected
test_ddl_db> ALTER USER test_user@'localhost' PASSWORD EXPIRE;
0 warnings, 0 rows affected
test_ddl_db> DROP USER test_user@'localhost';
0 warnings, 0 rows affected
test_ddl_db> set session sql_notes = 1;
0 warnings, 0 rows affected
test_ddl_db>
test_ddl_db> ALTER DATABASE test_ddl_db CHARACTER SET utf8mb4;
0 warnings, 1 rows affected
test_ddl_db> ALTER TABLE test_table ADD COLUMN note2 VARCHAR(100) DEFAULT NULL;
0 warnings, 0 rows affected
test_ddl_db> ALTER EVENT test_event DISABLE;
0 warnings, 0 rows affected
test_ddl_db> ALTER FUNCTION test_func COMMENT 'Test function';
0 warnings, 0 rows affected
test_ddl_db> ALTER PROCEDURE test_proc COMMENT 'Test procedure';
0 warnings, 0 rows affected
test_ddl_db>
test_ddl_db> RENAME TABLE test_table
-> TO test_table_renamed;
0 warnings, 0 rows affected
test_ddl_db> SELECT * from test_table_renamed;
id value note note2
-- ----------- ---- -----
1 'from proc' NULL NULL
2 'FROM PROC' NULL NULL
2 rows in set
test_ddl_db>
test_ddl_db> RENAME
-> TABLE test_table_renamed
-> TO test_table;
0 warnings, 0 rows affected
test_ddl_db>
test_ddl_db> SELECT * from test_table;
id value note note2
-- ----------- ---- -----
1 'from proc' NULL NULL
2 'FROM PROC' NULL NULL
2 rows in set
test_ddl_db>
test_ddl_db> -- there is no "test_server", this should make an error.
test_ddl_db> ALTER SERVER test_server OPTIONS (HOST '127.0.0.1');
ERR ERROR 1477 (HY000): The foreign server name you are trying to reference does not exist. Data source error: test_server
test_ddl_db>
test_ddl_db> DROP TRIGGER test_trigger_before_insert;
0 warnings, 0 rows affected
test_ddl_db>
test_ddl_db> TRUNCATE TABLE test_table ;
0 warnings, 0 rows affected
test_ddl_db> DROP TABLE test_table;
0 warnings, 0 rows affected
test_ddl_db>
test_ddl_db> DROP DATABASE test_ddl_db
-> ;
0 warnings, 1 rows affected
test_ddl_db>
test_ddl_db>
@@ -0,0 +1,154 @@
Teleport MySQL interactive shell (v19.0.0-dev)
Connected to instance "mysql-test-container" as user "root".
Type 'help' or '\h' for help.
(none)> -- with ANSI_QUOTES double quotes are identifiers
(none)> SET SESSION sql_mode='ANSI_QUOTES';
0 warnings, 0 rows affected
(none)> SELECT "thing";
ERR ERROR 1054 (42S22): Unknown column 'thing' in 'field list'
(none)> -- without ANSI_QUOTES double quotes are literals
(none)> SET SESSION sql_mode='';
0 warnings, 0 rows affected
(none)> SELECT "thing";
thing
-------
'thing'
1 row in set
(none)> select 1, -1,1.24, -1.24, 'foo', null, 'null';
1 -1 1.24 -1.24 foo NULL null
- -- ------ ------- ----- ---- ------
1 -1 '1.24' '-1.24' 'foo' NULL 'null'
1 row in set
(none)> select
-> 1 as 'unsigned int',
-> -1 as 'negative int',
-> 1.24 as "float",
-> -1.24 as 'negative float',
-> 'foo' as 'string',
-> null as 'actual NULL',
-> "null" as 'string null';
unsigned int negative int float negative float string actual NULL string null
------------ ------------ ------ -------------- ------ ----------- -----------
1 -1 '1.24' '-1.24' 'foo' NULL 'null'
1 row in set
(none)>
(none)> select 1 as 'multiple rows of literals'
-> union
-> select -1
-> union
-> select 1.24
-> union
-> select -1.24
-> union
-> select 'foo'
-> union
-> select null
-> union
-> select 'null';
multiple rows of literals
-------------------------
'1'
'-1'
'1.24'
'-1.24'
'foo'
NULL
'null'
7 rows in set
(none)>
(none)> select 'a
'> b
'> c' as 'multiline-string';
multiline-string
----------------
'a
b
c'
1 row in set
(none)>
(none)> select 'a
'> ;
'> b
'> ;
'> c' as 'multiline with delimiters';
multiline with delimiters
-------------------------
'a
;
b
;
c'
1 row in set
(none)>
(none)> /* foo bar */
(none)> select 1;
1
-
1
1 row in set
(none)> /* foo bar */select /*
/*> baz */ 42;
42
--
42
1 row in set
(none)>
(none)> delimiter $$
(none)> select '$$1$$'$$
$$1$$
-------
'$$1$$'
1 row in set
(none)> select '$$;$$';$$
$$;$$
-------
'$$;$$'
1 row in set
(none)> delimiter ;
(none)> select 1;
1
-
1
1 row in set
(none)>
(none)>
@@ -0,0 +1 @@
Empty set (0.04 sec)
@@ -0,0 +1,7 @@
one two three
--- --- -----
1 2.2 '3'
4 5.5 '6'
7 8.8 '8'
3 rows in set (123.46 sec)
@@ -0,0 +1 @@
2 warnings, 42 rows affected (0.00 sec)
@@ -0,0 +1,7 @@
one two three
--- --- -----
1 2.2 '3'
4 5.5 '6'
7 8.8 '8'
3 rows in set (1.23 sec)
+53
View File
@@ -0,0 +1,53 @@
// Teleport
// Copyright (C) 2025 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package repl
// token is a lexical token.
type token struct {
// kind is a token kind enum.
kind tokenKind
// text is the text that makes up the token.
text string
}
type tokenKind int
const (
// EOF indicates end of token stream
tokenEOF tokenKind = iota
// whitespace
tokenSpace
// escape
tokenBackslash // \
// quotation syntax
tokenSingleQuote // '
tokenDoubleQuote // "
tokenBacktick // `
// single-line comments
tokenSingleComment // -- or #
// multi-line comment open/close
tokenOpenComment // /*
tokenCloseComment // */
// misc text that isn't special to us
tokenText
)
+2
View File
@@ -111,6 +111,7 @@ import (
_ "github.com/gravitational/teleport/lib/backend/pgbk"
"github.com/gravitational/teleport/lib/bpf"
"github.com/gravitational/teleport/lib/cache"
myrepl "github.com/gravitational/teleport/lib/client/db/mysql/repl"
pgrepl "github.com/gravitational/teleport/lib/client/db/postgres/repl"
dbrepl "github.com/gravitational/teleport/lib/client/db/repl"
"github.com/gravitational/teleport/lib/cloud"
@@ -1163,6 +1164,7 @@ func NewTeleport(cfg *servicecfg.Config) (_ *TeleportProcess, err error) {
cfg.DatabaseREPLRegistry = dbrepl.NewREPLGetter(map[string]dbrepl.REPLNewFunc{
defaults.ProtocolPostgres: pgrepl.New,
defaults.ProtocolCockroachDB: pgrepl.New,
defaults.ProtocolMySQL: myrepl.New,
})
}