mirror of
https://github.com/gravitational/teleport.git
synced 2026-08-28 21:12:20 +08:00
Add MySQL REPL to the web UI (#57169)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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.",
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
+255
@@ -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)>
|
||||
+1
@@ -0,0 +1 @@
|
||||
Empty set (0.04 sec)
|
||||
Vendored
+7
@@ -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)
|
||||
+7
@@ -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)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user