Compare commits
17 Commits
912157fbf0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
36a10e319f
|
|||
|
dfae0ddbcc
|
|||
|
cf99e18a39
|
|||
|
36460a131e
|
|||
|
1d987d238d
|
|||
|
58ae783bf0
|
|||
|
f5027d9bfd
|
|||
|
b1bff4cbf0
|
|||
|
1f8b4ab24f
|
|||
|
d4e021386b
|
|||
|
eb63312542
|
|||
|
0a4986d476
|
|||
|
3be012e780
|
|||
| 11fa6ae522 | |||
|
59eaef2ae3
|
|||
|
ddd21b39a6
|
|||
|
|
cef8659a52 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
test.db
|
test.db
|
||||||
*.mercury
|
*.mercury
|
||||||
sour.is-mercury
|
sour.is-mercury
|
||||||
|
.vscode/
|
||||||
23
.vscode/launch.json
vendored
23
.vscode/launch.json
vendored
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
// Use IntelliSense to learn about possible attributes.
|
|
||||||
// Hover to view descriptions of existing attributes.
|
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Launch Package",
|
|
||||||
"type": "go",
|
|
||||||
"request": "launch",
|
|
||||||
"mode": "auto",
|
|
||||||
"program": "${fileDirname}",
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Attach to Process",
|
|
||||||
"type": "go",
|
|
||||||
"request": "attach",
|
|
||||||
"mode": "local",
|
|
||||||
"processId": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
13
go.mod
13
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module go.sour.is/pkg
|
module go.sour.is/pkg
|
||||||
|
|
||||||
go 1.22.0
|
go 1.23.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/99designs/gqlgen v0.17.44
|
github.com/99designs/gqlgen v0.17.44
|
||||||
@@ -8,7 +8,8 @@ require (
|
|||||||
github.com/gorilla/websocket v1.5.1
|
github.com/gorilla/websocket v1.5.1
|
||||||
github.com/matryer/is v1.4.1
|
github.com/matryer/is v1.4.1
|
||||||
github.com/ravilushqa/otelgqlgen v0.15.0
|
github.com/ravilushqa/otelgqlgen v0.15.0
|
||||||
github.com/vektah/gqlparser/v2 v2.5.11
|
github.com/tursodatabase/go-libsql v0.0.0-20240322134723-08771dcdd2f1
|
||||||
|
github.com/vektah/gqlparser/v2 v2.5.14
|
||||||
go.opentelemetry.io/otel v1.23.1
|
go.opentelemetry.io/otel v1.23.1
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.23.1
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.23.1
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.23.1
|
go.opentelemetry.io/otel/sdk/metric v1.23.1
|
||||||
@@ -53,6 +54,7 @@ require (
|
|||||||
github.com/BurntSushi/toml v1.3.2
|
github.com/BurntSushi/toml v1.3.2
|
||||||
github.com/Masterminds/squirrel v1.5.4
|
github.com/Masterminds/squirrel v1.5.4
|
||||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||||
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
|
||||||
github.com/go-logr/logr v1.4.1 // indirect
|
github.com/go-logr/logr v1.4.1 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f
|
github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f
|
||||||
@@ -60,7 +62,6 @@ require (
|
|||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect
|
||||||
github.com/oklog/ulid/v2 v2.1.0
|
github.com/oklog/ulid/v2 v2.1.0
|
||||||
github.com/prometheus/client_golang v1.18.0
|
github.com/prometheus/client_golang v1.18.0
|
||||||
github.com/tursodatabase/go-libsql v0.0.0-20240322134723-08771dcdd2f1
|
|
||||||
go.nhat.io/otelsql v0.12.0
|
go.nhat.io/otelsql v0.12.0
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0
|
||||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.48.0
|
go.opentelemetry.io/contrib/instrumentation/runtime v0.48.0
|
||||||
@@ -71,10 +72,10 @@ require (
|
|||||||
go.opentelemetry.io/otel/trace v1.23.1
|
go.opentelemetry.io/otel/trace v1.23.1
|
||||||
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
|
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a
|
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a
|
||||||
golang.org/x/net v0.21.0 // indirect
|
golang.org/x/net v0.23.0 // indirect
|
||||||
golang.org/x/sys v0.17.0 // indirect
|
golang.org/x/sys v0.18.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
google.golang.org/grpc v1.61.1 // indirect
|
google.golang.org/grpc v1.61.1 // indirect
|
||||||
google.golang.org/protobuf v1.32.0 // indirect
|
google.golang.org/protobuf v1.33.0 // indirect
|
||||||
modernc.org/sqlite v1.29.1
|
modernc.org/sqlite v1.29.1
|
||||||
)
|
)
|
||||||
|
|||||||
24
go.sum
24
go.sum
@@ -30,6 +30,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
|
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
|
||||||
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||||
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
|
||||||
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
@@ -99,6 +101,8 @@ github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
|||||||
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||||
github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
|
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
|
||||||
@@ -133,6 +137,8 @@ github.com/tursodatabase/go-libsql v0.0.0-20240322134723-08771dcdd2f1 h1:lQwP++j
|
|||||||
github.com/tursodatabase/go-libsql v0.0.0-20240322134723-08771dcdd2f1/go.mod h1:sb520Yr+GHBsfL43FQgQ+rLFfuJkItgRWlTgbIQHVxA=
|
github.com/tursodatabase/go-libsql v0.0.0-20240322134723-08771dcdd2f1/go.mod h1:sb520Yr+GHBsfL43FQgQ+rLFfuJkItgRWlTgbIQHVxA=
|
||||||
github.com/vektah/gqlparser/v2 v2.5.11 h1:JJxLtXIoN7+3x6MBdtIP59TP1RANnY7pXOaDnADQSf8=
|
github.com/vektah/gqlparser/v2 v2.5.11 h1:JJxLtXIoN7+3x6MBdtIP59TP1RANnY7pXOaDnADQSf8=
|
||||||
github.com/vektah/gqlparser/v2 v2.5.11/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc=
|
github.com/vektah/gqlparser/v2 v2.5.11/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc=
|
||||||
|
github.com/vektah/gqlparser/v2 v2.5.14 h1:dzLq75BJe03jjQm6n56PdH1oweB8ana42wj7E4jRy70=
|
||||||
|
github.com/vektah/gqlparser/v2 v2.5.14/go.mod h1:WQQjFc+I1YIzoPvZBhUQX7waZgg3pMLi0r8KymvAE2w=
|
||||||
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
|
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
|
||||||
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
||||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
|
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
|
||||||
@@ -172,23 +178,23 @@ go.sour.is/passwd v0.2.0/go.mod h1:xDqWTLiztFhr1KvUh//lvmJfMg+9piWt7K+d1JX3n0s=
|
|||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||||
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
|
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
|
||||||
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||||
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
|
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
|
||||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||||
golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
@@ -212,8 +218,8 @@ google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY=
|
|||||||
google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
|
google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
@@ -221,6 +227,8 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
|||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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=
|
||||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
|
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
|
||||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||||
modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk=
|
modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk=
|
||||||
|
|||||||
334
go.work.sum
334
go.work.sum
@@ -1,334 +0,0 @@
|
|||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
|
||||||
cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4=
|
|
||||||
cloud.google.com/go/accessapproval v1.7.5/go.mod h1:g88i1ok5dvQ9XJsxpUInWWvUBrIZhyPDPbk4T01OoJ0=
|
|
||||||
cloud.google.com/go/accesscontextmanager v1.8.5/go.mod h1:TInEhcZ7V9jptGNqN3EzZ5XMhT6ijWxTGjzyETwmL0Q=
|
|
||||||
cloud.google.com/go/aiplatform v1.60.0/go.mod h1:eTlGuHOahHprZw3Hio5VKmtThIOak5/qy6pzdsqcQnM=
|
|
||||||
cloud.google.com/go/analytics v0.23.0/go.mod h1:YPd7Bvik3WS95KBok2gPXDqQPHy08TsCQG6CdUCb+u0=
|
|
||||||
cloud.google.com/go/apigateway v1.6.5/go.mod h1:6wCwvYRckRQogyDDltpANi3zsCDl6kWi0b4Je+w2UiI=
|
|
||||||
cloud.google.com/go/apigeeconnect v1.6.5/go.mod h1:MEKm3AiT7s11PqTfKE3KZluZA9O91FNysvd3E6SJ6Ow=
|
|
||||||
cloud.google.com/go/apigeeregistry v0.8.3/go.mod h1:aInOWnqF4yMQx8kTjDqHNXjZGh/mxeNlAf52YqtASUs=
|
|
||||||
cloud.google.com/go/appengine v1.8.5/go.mod h1:uHBgNoGLTS5di7BvU25NFDuKa82v0qQLjyMJLuPQrVo=
|
|
||||||
cloud.google.com/go/area120 v0.8.5/go.mod h1:BcoFCbDLZjsfe4EkCnEq1LKvHSK0Ew/zk5UFu6GMyA0=
|
|
||||||
cloud.google.com/go/artifactregistry v1.14.7/go.mod h1:0AUKhzWQzfmeTvT4SjfI4zjot72EMfrkvL9g9aRjnnM=
|
|
||||||
cloud.google.com/go/asset v1.17.2/go.mod h1:SVbzde67ehddSoKf5uebOD1sYw8Ab/jD/9EIeWg99q4=
|
|
||||||
cloud.google.com/go/assuredworkloads v1.11.5/go.mod h1:FKJ3g3ZvkL2D7qtqIGnDufFkHxwIpNM9vtmhvt+6wqk=
|
|
||||||
cloud.google.com/go/automl v1.13.5/go.mod h1:MDw3vLem3yh+SvmSgeYUmUKqyls6NzSumDm9OJ3xJ1Y=
|
|
||||||
cloud.google.com/go/baremetalsolution v1.2.4/go.mod h1:BHCmxgpevw9IEryE99HbYEfxXkAEA3hkMJbYYsHtIuY=
|
|
||||||
cloud.google.com/go/batch v1.8.0/go.mod h1:k8V7f6VE2Suc0zUM4WtoibNrA6D3dqBpB+++e3vSGYc=
|
|
||||||
cloud.google.com/go/beyondcorp v1.0.4/go.mod h1:Gx8/Rk2MxrvWfn4WIhHIG1NV7IBfg14pTKv1+EArVcc=
|
|
||||||
cloud.google.com/go/bigquery v1.59.1/go.mod h1:VP1UJYgevyTwsV7desjzNzDND5p6hZB+Z8gZJN1GQUc=
|
|
||||||
cloud.google.com/go/billing v1.18.2/go.mod h1:PPIwVsOOQ7xzbADCwNe8nvK776QpfrOAUkvKjCUcpSE=
|
|
||||||
cloud.google.com/go/binaryauthorization v1.8.1/go.mod h1:1HVRyBerREA/nhI7yLang4Zn7vfNVA3okoAR9qYQJAQ=
|
|
||||||
cloud.google.com/go/certificatemanager v1.7.5/go.mod h1:uX+v7kWqy0Y3NG/ZhNvffh0kuqkKZIXdvlZRO7z0VtM=
|
|
||||||
cloud.google.com/go/channel v1.17.5/go.mod h1:FlpaOSINDAXgEext0KMaBq/vwpLMkkPAw9b2mApQeHc=
|
|
||||||
cloud.google.com/go/cloudbuild v1.15.1/go.mod h1:gIofXZSu+XD2Uy+qkOrGKEx45zd7s28u/k8f99qKals=
|
|
||||||
cloud.google.com/go/clouddms v1.7.4/go.mod h1:RdrVqoFG9RWI5AvZ81SxJ/xvxPdtcRhFotwdE79DieY=
|
|
||||||
cloud.google.com/go/cloudtasks v1.12.6/go.mod h1:b7c7fe4+TJsFZfDyzO51F7cjq7HLUlRi/KZQLQjDsaY=
|
|
||||||
cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
|
|
||||||
cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
|
|
||||||
cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40=
|
|
||||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
|
||||||
cloud.google.com/go/contactcenterinsights v1.13.0/go.mod h1:ieq5d5EtHsu8vhe2y3amtZ+BE+AQwX5qAy7cpo0POsI=
|
|
||||||
cloud.google.com/go/container v1.31.0/go.mod h1:7yABn5s3Iv3lmw7oMmyGbeV6tQj86njcTijkkGuvdZA=
|
|
||||||
cloud.google.com/go/containeranalysis v0.11.4/go.mod h1:cVZT7rXYBS9NG1rhQbWL9pWbXCKHWJPYraE8/FTSYPE=
|
|
||||||
cloud.google.com/go/datacatalog v1.19.3/go.mod h1:ra8V3UAsciBpJKQ+z9Whkxzxv7jmQg1hfODr3N3YPJ4=
|
|
||||||
cloud.google.com/go/dataflow v0.9.5/go.mod h1:udl6oi8pfUHnL0z6UN9Lf9chGqzDMVqcYTcZ1aPnCZQ=
|
|
||||||
cloud.google.com/go/dataform v0.9.2/go.mod h1:S8cQUwPNWXo7m/g3DhWHsLBoufRNn9EgFrMgne2j7cI=
|
|
||||||
cloud.google.com/go/datafusion v1.7.5/go.mod h1:bYH53Oa5UiqahfbNK9YuYKteeD4RbQSNMx7JF7peGHc=
|
|
||||||
cloud.google.com/go/datalabeling v0.8.5/go.mod h1:IABB2lxQnkdUbMnQaOl2prCOfms20mcPxDBm36lps+s=
|
|
||||||
cloud.google.com/go/dataplex v1.14.2/go.mod h1:0oGOSFlEKef1cQeAHXy4GZPB/Ife0fz/PxBf+ZymA2U=
|
|
||||||
cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4=
|
|
||||||
cloud.google.com/go/dataproc/v2 v2.4.0/go.mod h1:3B1Ht2aRB8VZIteGxQS/iNSJGzt9+CA0WGnDVMEm7Z4=
|
|
||||||
cloud.google.com/go/dataqna v0.8.5/go.mod h1:vgihg1mz6n7pb5q2YJF7KlXve6tCglInd6XO0JGOlWM=
|
|
||||||
cloud.google.com/go/datastore v1.15.0/go.mod h1:GAeStMBIt9bPS7jMJA85kgkpsMkvseWWXiaHya9Jes8=
|
|
||||||
cloud.google.com/go/datastream v1.10.4/go.mod h1:7kRxPdxZxhPg3MFeCSulmAJnil8NJGGvSNdn4p1sRZo=
|
|
||||||
cloud.google.com/go/deploy v1.17.1/go.mod h1:SXQyfsXrk0fBmgBHRzBjQbZhMfKZ3hMQBw5ym7MN/50=
|
|
||||||
cloud.google.com/go/dialogflow v1.49.0/go.mod h1:dhVrXKETtdPlpPhE7+2/k4Z8FRNUp6kMV3EW3oz/fe0=
|
|
||||||
cloud.google.com/go/dlp v1.11.2/go.mod h1:9Czi+8Y/FegpWzgSfkRlyz+jwW6Te9Rv26P3UfU/h/w=
|
|
||||||
cloud.google.com/go/documentai v1.25.0/go.mod h1:ftLnzw5VcXkLItp6pw1mFic91tMRyfv6hHEY5br4KzY=
|
|
||||||
cloud.google.com/go/domains v0.9.5/go.mod h1:dBzlxgepazdFhvG7u23XMhmMKBjrkoUNaw0A8AQB55Y=
|
|
||||||
cloud.google.com/go/edgecontainer v1.1.5/go.mod h1:rgcjrba3DEDEQAidT4yuzaKWTbkTI5zAMu3yy6ZWS0M=
|
|
||||||
cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU=
|
|
||||||
cloud.google.com/go/essentialcontacts v1.6.6/go.mod h1:XbqHJGaiH0v2UvtuucfOzFXN+rpL/aU5BCZLn4DYl1Q=
|
|
||||||
cloud.google.com/go/eventarc v1.13.4/go.mod h1:zV5sFVoAa9orc/52Q+OuYUG9xL2IIZTbbuTHC6JSY8s=
|
|
||||||
cloud.google.com/go/filestore v1.8.1/go.mod h1:MbN9KcaM47DRTIuLfQhJEsjaocVebNtNQhSLhKCF5GM=
|
|
||||||
cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LRpH9Xp9YZfzQ=
|
|
||||||
cloud.google.com/go/functions v1.16.0/go.mod h1:nbNpfAG7SG7Duw/o1iZ6ohvL7mc6MapWQVpqtM29n8k=
|
|
||||||
cloud.google.com/go/gkebackup v1.3.5/go.mod h1:KJ77KkNN7Wm1LdMopOelV6OodM01pMuK2/5Zt1t4Tvc=
|
|
||||||
cloud.google.com/go/gkeconnect v0.8.5/go.mod h1:LC/rS7+CuJ5fgIbXv8tCD/mdfnlAadTaUufgOkmijuk=
|
|
||||||
cloud.google.com/go/gkehub v0.14.5/go.mod h1:6bzqxM+a+vEH/h8W8ec4OJl4r36laxTs3A/fMNHJ0wA=
|
|
||||||
cloud.google.com/go/gkemulticloud v1.1.1/go.mod h1:C+a4vcHlWeEIf45IB5FFR5XGjTeYhF83+AYIpTy4i2Q=
|
|
||||||
cloud.google.com/go/grafeas v0.3.4/go.mod h1:A5m316hcG+AulafjAbPKXBO/+I5itU4LOdKO2R/uDIc=
|
|
||||||
cloud.google.com/go/gsuiteaddons v1.6.5/go.mod h1:Lo4P2IvO8uZ9W+RaC6s1JVxo42vgy+TX5a6hfBZ0ubs=
|
|
||||||
cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI=
|
|
||||||
cloud.google.com/go/iap v1.9.4/go.mod h1:vO4mSq0xNf/Pu6E5paORLASBwEmphXEjgCFg7aeNu1w=
|
|
||||||
cloud.google.com/go/ids v1.4.5/go.mod h1:p0ZnyzjMWxww6d2DvMGnFwCsSxDJM666Iir1bK1UuBo=
|
|
||||||
cloud.google.com/go/iot v1.7.5/go.mod h1:nq3/sqTz3HGaWJi1xNiX7F41ThOzpud67vwk0YsSsqs=
|
|
||||||
cloud.google.com/go/kms v1.15.7/go.mod h1:ub54lbsa6tDkUwnu4W7Yt1aAIFLnspgh0kPGToDukeI=
|
|
||||||
cloud.google.com/go/language v1.12.3/go.mod h1:evFX9wECX6mksEva8RbRnr/4wi/vKGYnAJrTRXU8+f8=
|
|
||||||
cloud.google.com/go/lifesciences v0.9.5/go.mod h1:OdBm0n7C0Osh5yZB7j9BXyrMnTRGBJIZonUMxo5CzPw=
|
|
||||||
cloud.google.com/go/logging v1.9.0/go.mod h1:1Io0vnZv4onoUnsVUQY3HZ3Igb1nBchky0A0y7BBBhE=
|
|
||||||
cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s=
|
|
||||||
cloud.google.com/go/managedidentities v1.6.5/go.mod h1:fkFI2PwwyRQbjLxlm5bQ8SjtObFMW3ChBGNqaMcgZjI=
|
|
||||||
cloud.google.com/go/maps v1.6.4/go.mod h1:rhjqRy8NWmDJ53saCfsXQ0LKwBHfi6OSh5wkq6BaMhI=
|
|
||||||
cloud.google.com/go/mediatranslation v0.8.5/go.mod h1:y7kTHYIPCIfgyLbKncgqouXJtLsU+26hZhHEEy80fSs=
|
|
||||||
cloud.google.com/go/memcache v1.10.5/go.mod h1:/FcblbNd0FdMsx4natdj+2GWzTq+cjZvMa1I+9QsuMA=
|
|
||||||
cloud.google.com/go/metastore v1.13.4/go.mod h1:FMv9bvPInEfX9Ac1cVcRXp8EBBQnBcqH6gz3KvJ9BAE=
|
|
||||||
cloud.google.com/go/monitoring v1.18.0/go.mod h1:c92vVBCeq/OB4Ioyo+NbN2U7tlg5ZH41PZcdvfc+Lcg=
|
|
||||||
cloud.google.com/go/networkconnectivity v1.14.4/go.mod h1:PU12q++/IMnDJAB+3r+tJtuCXCfwfN+C6Niyj6ji1Po=
|
|
||||||
cloud.google.com/go/networkmanagement v1.9.4/go.mod h1:daWJAl0KTFytFL7ar33I6R/oNBH8eEOX/rBNHrC/8TA=
|
|
||||||
cloud.google.com/go/networksecurity v0.9.5/go.mod h1:KNkjH/RsylSGyyZ8wXpue8xpCEK+bTtvof8SBfIhMG8=
|
|
||||||
cloud.google.com/go/notebooks v1.11.3/go.mod h1:0wQyI2dQC3AZyQqWnRsp+yA+kY4gC7ZIVP4Qg3AQcgo=
|
|
||||||
cloud.google.com/go/optimization v1.6.3/go.mod h1:8ve3svp3W6NFcAEFr4SfJxrldzhUl4VMUJmhrqVKtYA=
|
|
||||||
cloud.google.com/go/orchestration v1.8.5/go.mod h1:C1J7HesE96Ba8/hZ71ISTV2UAat0bwN+pi85ky38Yq8=
|
|
||||||
cloud.google.com/go/orgpolicy v1.12.1/go.mod h1:aibX78RDl5pcK3jA8ysDQCFkVxLj3aOQqrbBaUL2V5I=
|
|
||||||
cloud.google.com/go/osconfig v1.12.5/go.mod h1:D9QFdxzfjgw3h/+ZaAb5NypM8bhOMqBzgmbhzWViiW8=
|
|
||||||
cloud.google.com/go/oslogin v1.13.1/go.mod h1:vS8Sr/jR7QvPWpCjNqy6LYZr5Zs1e8ZGW/KPn9gmhws=
|
|
||||||
cloud.google.com/go/phishingprotection v0.8.5/go.mod h1:g1smd68F7mF1hgQPuYn3z8HDbNre8L6Z0b7XMYFmX7I=
|
|
||||||
cloud.google.com/go/policytroubleshooter v1.10.3/go.mod h1:+ZqG3agHT7WPb4EBIRqUv4OyIwRTZvsVDHZ8GlZaoxk=
|
|
||||||
cloud.google.com/go/privatecatalog v0.9.5/go.mod h1:fVWeBOVe7uj2n3kWRGlUQqR/pOd450J9yZoOECcQqJk=
|
|
||||||
cloud.google.com/go/pubsub v1.36.1/go.mod h1:iYjCa9EzWOoBiTdd4ps7QoMtMln5NwaZQpK1hbRfBDE=
|
|
||||||
cloud.google.com/go/pubsublite v1.8.1/go.mod h1:fOLdU4f5xldK4RGJrBMm+J7zMWNj/k4PxwEZXy39QS0=
|
|
||||||
cloud.google.com/go/recaptchaenterprise/v2 v2.9.2/go.mod h1:trwwGkfhCmp05Ll5MSJPXY7yvnO0p4v3orGANAFHAuU=
|
|
||||||
cloud.google.com/go/recommendationengine v0.8.5/go.mod h1:A38rIXHGFvoPvmy6pZLozr0g59NRNREz4cx7F58HAsQ=
|
|
||||||
cloud.google.com/go/recommender v1.12.1/go.mod h1:gf95SInWNND5aPas3yjwl0I572dtudMhMIG4ni8nr+0=
|
|
||||||
cloud.google.com/go/redis v1.14.2/go.mod h1:g0Lu7RRRz46ENdFKQ2EcQZBAJ2PtJHJLuiiRuEXwyQw=
|
|
||||||
cloud.google.com/go/resourcemanager v1.9.5/go.mod h1:hep6KjelHA+ToEjOfO3garMKi/CLYwTqeAw7YiEI9x8=
|
|
||||||
cloud.google.com/go/resourcesettings v1.6.5/go.mod h1:WBOIWZraXZOGAgoR4ukNj0o0HiSMO62H9RpFi9WjP9I=
|
|
||||||
cloud.google.com/go/retail v1.16.0/go.mod h1:LW7tllVveZo4ReWt68VnldZFWJRzsh9np+01J9dYWzE=
|
|
||||||
cloud.google.com/go/run v1.3.4/go.mod h1:FGieuZvQ3tj1e9GnzXqrMABSuir38AJg5xhiYq+SF3o=
|
|
||||||
cloud.google.com/go/scheduler v1.10.6/go.mod h1:pe2pNCtJ+R01E06XCDOJs1XvAMbv28ZsQEbqknxGOuE=
|
|
||||||
cloud.google.com/go/secretmanager v1.11.5/go.mod h1:eAGv+DaCHkeVyQi0BeXgAHOU0RdrMeZIASKc+S7VqH4=
|
|
||||||
cloud.google.com/go/security v1.15.5/go.mod h1:KS6X2eG3ynWjqcIX976fuToN5juVkF6Ra6c7MPnldtc=
|
|
||||||
cloud.google.com/go/securitycenter v1.24.4/go.mod h1:PSccin+o1EMYKcFQzz9HMMnZ2r9+7jbc+LvPjXhpwcU=
|
|
||||||
cloud.google.com/go/servicedirectory v1.11.4/go.mod h1:Bz2T9t+/Ehg6x+Y7Ycq5xiShYLD96NfEsWNHyitj1qM=
|
|
||||||
cloud.google.com/go/shell v1.7.5/go.mod h1:hL2++7F47/IfpfTO53KYf1EC+F56k3ThfNEXd4zcuiE=
|
|
||||||
cloud.google.com/go/spanner v1.56.0/go.mod h1:DndqtUKQAt3VLuV2Le+9Y3WTnq5cNKrnLb/Piqcj+h0=
|
|
||||||
cloud.google.com/go/speech v1.21.1/go.mod h1:E5GHZXYQlkqWQwY5xRSLHw2ci5NMQNG52FfMU1aZrIA=
|
|
||||||
cloud.google.com/go/storage v1.37.0/go.mod h1:i34TiT2IhiNDmcj65PqwCjcoUX7Z5pLzS8DEmoiFq1k=
|
|
||||||
cloud.google.com/go/storagetransfer v1.10.4/go.mod h1:vef30rZKu5HSEf/x1tK3WfWrL0XVoUQN/EPDRGPzjZs=
|
|
||||||
cloud.google.com/go/talent v1.6.6/go.mod h1:y/WQDKrhVz12WagoarpAIyKKMeKGKHWPoReZ0g8tseQ=
|
|
||||||
cloud.google.com/go/texttospeech v1.7.5/go.mod h1:tzpCuNWPwrNJnEa4Pu5taALuZL4QRRLcb+K9pbhXT6M=
|
|
||||||
cloud.google.com/go/tpu v1.6.5/go.mod h1:P9DFOEBIBhuEcZhXi+wPoVy/cji+0ICFi4TtTkMHSSs=
|
|
||||||
cloud.google.com/go/trace v1.10.5/go.mod h1:9hjCV1nGBCtXbAE4YK7OqJ8pmPYSxPA0I67JwRd5s3M=
|
|
||||||
cloud.google.com/go/translate v1.10.1/go.mod h1:adGZcQNom/3ogU65N9UXHOnnSvjPwA/jKQUMnsYXOyk=
|
|
||||||
cloud.google.com/go/video v1.20.4/go.mod h1:LyUVjyW+Bwj7dh3UJnUGZfyqjEto9DnrvTe1f/+QrW0=
|
|
||||||
cloud.google.com/go/videointelligence v1.11.5/go.mod h1:/PkeQjpRponmOerPeJxNPuxvi12HlW7Em0lJO14FC3I=
|
|
||||||
cloud.google.com/go/vision/v2 v2.8.0/go.mod h1:ocqDiA2j97pvgogdyhoxiQp2ZkDCyr0HWpicywGGRhU=
|
|
||||||
cloud.google.com/go/vmmigration v1.7.5/go.mod h1:pkvO6huVnVWzkFioxSghZxIGcsstDvYiVCxQ9ZH3eYI=
|
|
||||||
cloud.google.com/go/vmwareengine v1.1.1/go.mod h1:nMpdsIVkUrSaX8UvmnBhzVzG7PPvNYc5BszcvIVudYs=
|
|
||||||
cloud.google.com/go/vpcaccess v1.7.5/go.mod h1:slc5ZRvvjP78c2dnL7m4l4R9GwL3wDLcpIWz6P/ziig=
|
|
||||||
cloud.google.com/go/webrisk v1.9.5/go.mod h1:aako0Fzep1Q714cPEM5E+mtYX8/jsfegAuS8aivxy3U=
|
|
||||||
cloud.google.com/go/websecurityscanner v1.6.5/go.mod h1:QR+DWaxAz2pWooylsBF854/Ijvuoa3FCyS1zBa1rAVQ=
|
|
||||||
cloud.google.com/go/workflows v1.12.4/go.mod h1:yQ7HUqOkdJK4duVtMeBCAOPiN1ZF1E9pAMX51vpwB/w=
|
|
||||||
github.com/99designs/gqlgen v0.17.41/go.mod h1:GQ6SyMhwFbgHR0a8r2Wn8fYgEwPxxmndLFPhU63+cJE=
|
|
||||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
|
||||||
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
|
|
||||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
|
|
||||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
|
||||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
|
||||||
github.com/apache/arrow/go/v14 v14.0.2/go.mod h1:u3fgh3EdgN/YQ8cVQRguVW3R+seMybFg8QBQ5LU+eBY=
|
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
|
||||||
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
|
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
|
||||||
github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
|
|
||||||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
|
||||||
github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
|
||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
|
||||||
github.com/envoyproxy/go-control-plane v0.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQLaTEw+YhGluxZkrTmD0g=
|
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
|
||||||
github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
|
|
||||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
|
||||||
github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04=
|
|
||||||
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
|
||||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
|
||||||
github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
|
|
||||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
|
||||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
|
||||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
|
||||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
|
||||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
|
||||||
github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-pkcs11 v0.2.1-0.20230907215043-c6f79328ddf9/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY=
|
|
||||||
github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
|
|
||||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
|
||||||
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
|
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20220221023154-0b2280d3ff96/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
|
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
|
||||||
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
|
||||||
github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM=
|
|
||||||
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
|
||||||
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
|
||||||
github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc=
|
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
|
||||||
github.com/lyft/protoc-gen-star/v2 v2.0.3/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk=
|
|
||||||
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
|
||||||
github.com/matryer/moq v0.3.3/go.mod h1:RJ75ZZZD71hejp39j4crZLsEDszGk6iH4v4YsWFKH4s=
|
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
|
||||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
|
||||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
|
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
|
||||||
github.com/mmcloughlin/professor v0.0.0-20170922221822-6b97112ab8b3/go.mod h1:LQkXsHRSPIEklPCq8OMQAzYNS2NGtYStdNE/ej1oJU8=
|
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
|
||||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
|
||||||
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
|
||||||
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
|
||||||
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
|
|
||||||
github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk=
|
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
|
||||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
|
||||||
github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
|
|
||||||
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
|
||||||
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
|
|
||||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
|
||||||
github.com/spf13/viper v1.10.1/go.mod h1:IGlFPqhNAPKRxohIzWpI5QEy4kuI7tcl5WvR+8qy1rU=
|
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
|
||||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
|
||||||
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
|
||||||
github.com/wblakecaldwell/profiler v0.0.0-20150908040756-6111ef1313a1/go.mod h1:3+0F8oLB1rQlbIcRAuqDgGdzNi9X69un/aPz4cUAFV4=
|
|
||||||
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
|
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
|
||||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
|
||||||
go.einride.tech/aip v0.66.0/go.mod h1:qAhMsfT7plxBX+Oy7Huol6YUvZ0ZzdUz26yZsQwfl1M=
|
|
||||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
|
||||||
go.opentelemetry.io/contrib v1.21.1/go.mod h1:usW9bPlrjHiJFbK0a6yK/M5wNHs3nLmtrT3vzhoD3co=
|
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ=
|
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw=
|
|
||||||
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
|
|
||||||
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
|
|
||||||
go.opentelemetry.io/otel v1.23.0/go.mod h1:YCycw9ZeKhcJFrb34iVSkyT0iczq/zYDtZYFufObyB0=
|
|
||||||
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
|
|
||||||
go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
|
|
||||||
go.opentelemetry.io/otel/metric v1.23.0/go.mod h1:MqUW2X2a6Q8RN96E2/nqNoT+z9BSms20Jb7Bbp+HiTo=
|
|
||||||
go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
|
|
||||||
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
|
|
||||||
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
|
|
||||||
go.opentelemetry.io/otel/trace v1.23.0/go.mod h1:GSGTbIClEsuZrGIzoEHqsVfxgn5UkggkflQwDScNUsk=
|
|
||||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
|
||||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
|
||||||
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
|
|
||||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
|
||||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
|
||||||
golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
|
|
||||||
golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM=
|
|
||||||
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
|
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ=
|
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
|
||||||
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
|
||||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
|
||||||
google.golang.org/api v0.162.0/go.mod h1:6SulDkfoBIg4NFmCuZ39XeeAgSHCPecfSUuDyYlAHs0=
|
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
|
||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
|
||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
|
||||||
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY=
|
|
||||||
google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k=
|
|
||||||
google.golang.org/genproto v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro=
|
|
||||||
google.golang.org/genproto v0.0.0-20240205150955-31a09d347014/go.mod h1:xEgQu1e4stdSSsxPDK8Azkrk/ECl5HvdPf6nbZrTS5M=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917/go.mod h1:CmlNWB9lSezaYELKS5Ym1r44VrrbPUa7JTvw+6MbpJ0=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014/go.mod h1:rbHMSEDyoYX62nRVLOCc4Qt1HbsdytAYoVwgjiOhF3I=
|
|
||||||
google.golang.org/genproto/googleapis/bytestream v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:SCz6T5xjNXM4QFPRwxHcfChp7V+9DcXR3ay2TkHR8Tg=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014/go.mod h1:SaPjaZGWb0lPqs6Ittu0spdfrOArqji4ZdeP5IC/9N4=
|
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
|
||||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
|
||||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
|
||||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
|
||||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
|
||||||
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
|
|
||||||
google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
|
||||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
|
||||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
|
||||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
|
||||||
gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
|
||||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
|
||||||
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
|
|
||||||
modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=
|
|
||||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
|
||||||
@@ -7,17 +7,17 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.sour.is/pkg/ident"
|
||||||
"go.sour.is/pkg/lg"
|
"go.sour.is/pkg/lg"
|
||||||
"go.sour.is/pkg/mercury"
|
"go.sour.is/pkg/mercury"
|
||||||
"go.sour.is/pkg/ident"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const identNS = "ident."
|
const identNS = "ident."
|
||||||
const identSFX = ".credentials"
|
const identSFX = ".credentials"
|
||||||
|
|
||||||
type registry interface {
|
type registry interface {
|
||||||
GetIndex(ctx context.Context, match, search string) (c mercury.Config, err error)
|
GetIndex(ctx context.Context, search mercury.Search) (c mercury.Config, err error)
|
||||||
GetConfig(ctx context.Context, match, search, fields string) (mercury.Config, error)
|
GetConfig(ctx context.Context, search mercury.Search) (mercury.Config, error)
|
||||||
WriteConfig(ctx context.Context, spaces mercury.Config) error
|
WriteConfig(ctx context.Context, spaces mercury.Config) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +50,7 @@ func (id *mercuryIdent) FromConfig(cfg mercury.Config) error {
|
|||||||
switch {
|
switch {
|
||||||
case strings.HasSuffix(s.Space, ".credentials"):
|
case strings.HasSuffix(s.Space, ".credentials"):
|
||||||
id.passwd = []byte(s.FirstValue("passwd").First())
|
id.passwd = []byte(s.FirstValue("passwd").First())
|
||||||
|
id.ed25519 = []byte(s.FirstValue("ed25519").First())
|
||||||
default:
|
default:
|
||||||
id.display = s.FirstValue("displayName").First()
|
id.display = s.FirstValue("displayName").First()
|
||||||
}
|
}
|
||||||
@@ -59,34 +60,29 @@ func (id *mercuryIdent) FromConfig(cfg mercury.Config) error {
|
|||||||
|
|
||||||
func (id *mercuryIdent) ToConfig() mercury.Config {
|
func (id *mercuryIdent) ToConfig() mercury.Config {
|
||||||
space := id.Space()
|
space := id.Space()
|
||||||
|
list := func(values ...mercury.Value) []mercury.Value { return values }
|
||||||
|
value := func(space string, seq uint64, name string, values ...string) mercury.Value {
|
||||||
|
return mercury.Value{
|
||||||
|
Space: space,
|
||||||
|
Seq: seq,
|
||||||
|
Name: name,
|
||||||
|
Values: values,
|
||||||
|
}
|
||||||
|
}
|
||||||
return mercury.Config{
|
return mercury.Config{
|
||||||
&mercury.Space{
|
&mercury.Space{
|
||||||
Space: space,
|
Space: space,
|
||||||
List: []mercury.Value{
|
List: list(
|
||||||
{
|
value(space, 1, "displayName", id.display),
|
||||||
Space: space,
|
value(space, 2, "lastLogin", time.UnixMilli(int64(id.Session().SessionID.Time())).Format(time.RFC3339)),
|
||||||
Seq: 1,
|
),
|
||||||
Name: "displayName",
|
|
||||||
Values: []string{id.display},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Space: space,
|
|
||||||
Seq: 2,
|
|
||||||
Name: "lastLogin",
|
|
||||||
Values: []string{time.UnixMilli(int64(id.Session().SessionID.Time())).Format(time.RFC3339)},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
&mercury.Space{
|
&mercury.Space{
|
||||||
Space: space + identSFX,
|
Space: space + identSFX,
|
||||||
List: []mercury.Value{
|
List: list(
|
||||||
{
|
value(space+identSFX, 1, "passwd", string(id.passwd)),
|
||||||
Space: space + identSFX,
|
value(space+identSFX, 1, "ed25519", string(id.ed25519)),
|
||||||
Seq: 1,
|
),
|
||||||
Name: "passwd",
|
|
||||||
Values: []string{string(id.passwd)},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,7 +136,7 @@ func (s *mercurySource) readIdentURL(r *http.Request) (ident.Ident, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
space := id.Space()
|
space := id.Space()
|
||||||
c, err := s.r.GetConfig(ctx, "trace:"+space+identSFX, "", "")
|
c, err := s.r.GetConfig(ctx, mercury.ParseSearch("trace:"+space+identSFX))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
span.RecordError(err)
|
span.RecordError(err)
|
||||||
return id, err
|
return id, err
|
||||||
@@ -183,7 +179,7 @@ func (s *mercurySource) readIdentBasic(r *http.Request) (ident.Ident, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
space := id.Space()
|
space := id.Space()
|
||||||
c, err := s.r.GetConfig(ctx, "trace:"+space+identSFX, "", "")
|
c, err := s.r.GetConfig(ctx, mercury.ParseSearch("trace:"+space+identSFX))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
span.RecordError(err)
|
span.RecordError(err)
|
||||||
return id, err
|
return id, err
|
||||||
@@ -228,7 +224,7 @@ func (s *mercurySource) readIdentHTTP(r *http.Request) (ident.Ident, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
space := id.Space()
|
space := id.Space()
|
||||||
c, err := s.r.GetConfig(ctx, "trace:"+space+identSFX, "", "")
|
c, err := s.r.GetConfig(ctx, mercury.ParseSearch("trace:"+space+identSFX))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
span.RecordError(err)
|
span.RecordError(err)
|
||||||
return id, err
|
return id, err
|
||||||
@@ -260,9 +256,8 @@ func (s *mercurySource) RegisterIdent(ctx context.Context, identity, display str
|
|||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
id := &mercuryIdent{identity: identity, display: display, passwd: passwd}
|
id := &mercuryIdent{identity: identity, display: display, passwd: passwd}
|
||||||
space := id.Space()
|
|
||||||
|
|
||||||
_, err := s.r.GetIndex(ctx, space, "")
|
_, err := s.r.GetIndex(ctx, mercury.ParseSearch( id.Space()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,9 +38,11 @@ func initMetrics(ctx context.Context, name string) (context.Context, func() erro
|
|||||||
goversion := ""
|
goversion := ""
|
||||||
pkg := ""
|
pkg := ""
|
||||||
host := ""
|
host := ""
|
||||||
|
version := "0.0.1"
|
||||||
if info, ok := debug.ReadBuildInfo(); ok {
|
if info, ok := debug.ReadBuildInfo(); ok {
|
||||||
goversion = info.GoVersion
|
goversion = info.GoVersion
|
||||||
pkg = info.Path
|
pkg = info.Path
|
||||||
|
version = info.Main.Version
|
||||||
}
|
}
|
||||||
if h, err := os.Hostname(); err == nil {
|
if h, err := os.Hostname(); err == nil {
|
||||||
host = h
|
host = h
|
||||||
@@ -69,7 +71,7 @@ func initMetrics(ctx context.Context, name string) (context.Context, func() erro
|
|||||||
)
|
)
|
||||||
|
|
||||||
meter := provider.Meter(name,
|
meter := provider.Meter(name,
|
||||||
api.WithInstrumentationVersion("0.0.1"),
|
api.WithInstrumentationVersion(version),
|
||||||
api.WithInstrumentationAttributes(
|
api.WithInstrumentationAttributes(
|
||||||
attribute.String("app", name),
|
attribute.String("app", name),
|
||||||
attribute.String("host", host),
|
attribute.String("host", host),
|
||||||
|
|||||||
162
libsql_embed/open.go
Normal file
162
libsql_embed/open.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
package libsqlembed
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"database/sql/driver"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tursodatabase/go-libsql"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
sql.Register("libsql+embed", &db{conns: make(map[string]*connector)})
|
||||||
|
}
|
||||||
|
|
||||||
|
type db struct {
|
||||||
|
conns map[string]*connector
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type connector struct {
|
||||||
|
*libsql.Connector
|
||||||
|
dsn string
|
||||||
|
dir string
|
||||||
|
driver *db
|
||||||
|
removeDir bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ io.Closer = (*connector)(nil)
|
||||||
|
|
||||||
|
func (c *connector) Close() error {
|
||||||
|
log.Println("closing db connection", c.dir)
|
||||||
|
defer log.Println("closed db connection", c.dir)
|
||||||
|
|
||||||
|
c.driver.mu.Lock()
|
||||||
|
delete(c.driver.conns, c.dsn)
|
||||||
|
c.driver.mu.Unlock()
|
||||||
|
|
||||||
|
if c.removeDir {
|
||||||
|
defer os.RemoveAll(c.dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("sync db")
|
||||||
|
if err := c.Connector.Sync(); err != nil {
|
||||||
|
return fmt.Errorf("syncing database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Connector.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *db) OpenConnector(dsn string) (driver.Connector, error) {
|
||||||
|
// log.Println("connector", dsn)
|
||||||
|
if dsn == "" {
|
||||||
|
return nil, fmt.Errorf("no dsn")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c, ok := func() (*connector, bool) {
|
||||||
|
db.mu.RLock()
|
||||||
|
defer db.mu.RUnlock()
|
||||||
|
c, ok := db.conns[dsn]
|
||||||
|
return c, ok
|
||||||
|
}(); ok {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
db.mu.Lock()
|
||||||
|
defer db.mu.Unlock()
|
||||||
|
|
||||||
|
u, err := url.Parse(dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var primary url.URL
|
||||||
|
primary.Scheme = strings.TrimSuffix(u.Scheme, "+embed")
|
||||||
|
primary.Host = u.Host
|
||||||
|
|
||||||
|
dbname, _, _ := strings.Cut(u.Host, ".")
|
||||||
|
|
||||||
|
authToken := u.Query().Get("authToken")
|
||||||
|
if authToken == "" {
|
||||||
|
return nil, fmt.Errorf("missing authToken")
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := []libsql.Option{
|
||||||
|
libsql.WithAuthToken(authToken),
|
||||||
|
}
|
||||||
|
|
||||||
|
if refresh, err := strconv.ParseInt(u.Query().Get("refresh"), 10, 64); err == nil {
|
||||||
|
log.Println("refresh: ", refresh)
|
||||||
|
opts = append(opts, libsql.WithSyncInterval(time.Duration(refresh)*time.Minute))
|
||||||
|
}
|
||||||
|
|
||||||
|
if readWrite, err := strconv.ParseBool(u.Query().Get("readYourWrites")); err == nil {
|
||||||
|
log.Println("read your writes: ", readWrite)
|
||||||
|
opts = append(opts, libsql.WithReadYourWrites(readWrite))
|
||||||
|
}
|
||||||
|
if key := u.Query().Get("key"); key != "" {
|
||||||
|
opts = append(opts, libsql.WithEncryption(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
var dir string
|
||||||
|
var removeDir bool
|
||||||
|
if dir = u.Query().Get("store"); dir == "" {
|
||||||
|
removeDir = true
|
||||||
|
dir, err = os.MkdirTemp("", "libsql-*")
|
||||||
|
log.Println("creating temporary directory:", dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating temporary directory: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stat, err := os.Stat(dir)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
if err = os.MkdirAll(dir, 0700); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !stat.IsDir() {
|
||||||
|
return nil, fmt.Errorf("store not directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dbPath := filepath.Join(dir, dbname)
|
||||||
|
|
||||||
|
c, err := libsql.NewEmbeddedReplicaConnector(
|
||||||
|
dbPath,
|
||||||
|
primary.String(),
|
||||||
|
opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating connector: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("sync db")
|
||||||
|
if err := c.Sync(); err != nil {
|
||||||
|
return nil, fmt.Errorf("syncing database: %w", err)
|
||||||
|
}
|
||||||
|
connector := &connector{c, dsn, dir, db, removeDir}
|
||||||
|
db.conns[dsn] = connector
|
||||||
|
|
||||||
|
return connector, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *db) Open(dsn string) (driver.Conn, error) {
|
||||||
|
log.Println("open", dsn)
|
||||||
|
|
||||||
|
c, err := db.OpenConnector(dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.Connect(context.Background())
|
||||||
|
}
|
||||||
286
lsm/cli/main.go
Normal file
286
lsm/cli/main.go
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"iter"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docopt/docopt-go"
|
||||||
|
"go.sour.is/pkg/lsm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var usage = `
|
||||||
|
Usage:
|
||||||
|
lsm create <archive> <files>...
|
||||||
|
lsm append <archive> <files>...
|
||||||
|
lsm read <archive> [<start> [<end>]]
|
||||||
|
lsm serve <archive>
|
||||||
|
lsm client <archive> [<start> [<end>]]`
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
Create bool
|
||||||
|
Append bool
|
||||||
|
Read bool
|
||||||
|
Serve bool
|
||||||
|
Client bool
|
||||||
|
|
||||||
|
Archive string `docopt:"<archive>"`
|
||||||
|
Files []string `docopt:"<files>"`
|
||||||
|
Start int64 `docopt:"<start>"`
|
||||||
|
End int64 `docopt:"<end>"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
opts, err := docopt.ParseDoc(usage)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
args := args{}
|
||||||
|
err = opts.Bind(&args)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
err = run(Console, args)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type console struct {
|
||||||
|
Stdin io.Reader
|
||||||
|
Stdout io.Writer
|
||||||
|
Stderr io.Writer
|
||||||
|
}
|
||||||
|
var Console = console{os.Stdin, os.Stdout, os.Stderr}
|
||||||
|
|
||||||
|
func (c console) Write(b []byte) (int, error) {
|
||||||
|
return c.Stdout.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(console console, a args) error {
|
||||||
|
fmt.Fprintln(console, "lsm")
|
||||||
|
switch {
|
||||||
|
case a.Create:
|
||||||
|
f, err := os.OpenFile(a.Archive, os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
return lsm.WriteLogFile(f, fileReaders(a.Files))
|
||||||
|
case a.Append:
|
||||||
|
f, err := os.OpenFile(a.Archive, os.O_RDWR, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
return lsm.AppendLogFile(f, fileReaders(a.Files))
|
||||||
|
case a.Read:
|
||||||
|
fmt.Fprintln(console, "reading", a.Archive)
|
||||||
|
|
||||||
|
f, err := os.Open(a.Archive)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
return readContent(f, console, a.Start, a.End)
|
||||||
|
case a.Serve:
|
||||||
|
fmt.Fprintln(console, "serving", a.Archive)
|
||||||
|
b, err := base64.RawStdEncoding.DecodeString(a.Archive)
|
||||||
|
now := time.Now()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.ServeContent(w, r, "", now, bytes.NewReader(b))
|
||||||
|
})
|
||||||
|
return http.ListenAndServe(":8080", nil)
|
||||||
|
case a.Client:
|
||||||
|
r, err := OpenHttpReader(context.Background(), a.Archive, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
defer func() {fmt.Println("bytes read", r.bytesRead)}()
|
||||||
|
return readContent(r, console, a.Start, a.End)
|
||||||
|
}
|
||||||
|
return errors.New("unknown command")
|
||||||
|
}
|
||||||
|
|
||||||
|
func readContent(r io.ReaderAt, console console, start, end int64) error {
|
||||||
|
lg, err := lsm.ReadLogFile(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for bi, rd := range lg.Iter(uint64(start)) {
|
||||||
|
if end > 0 && int64(bi.Index) >= end {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fmt.Fprintf(console, "=========================\n%+v:\n", bi)
|
||||||
|
wr := base64.NewEncoder(base64.RawStdEncoding, console)
|
||||||
|
io.Copy(wr, rd)
|
||||||
|
fmt.Fprintln(console, "\n=========================")
|
||||||
|
}
|
||||||
|
if lg.Err != nil {
|
||||||
|
return lg.Err
|
||||||
|
}
|
||||||
|
for bi, rd := range lg.Rev(lg.Count()) {
|
||||||
|
if end > 0 && int64(bi.Index) >= end {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fmt.Fprintf(console, "=========================\n%+v:\n", bi)
|
||||||
|
wr := base64.NewEncoder(base64.RawStdEncoding, console)
|
||||||
|
io.Copy(wr, rd)
|
||||||
|
fmt.Fprintln(console, "\n=========================")
|
||||||
|
}
|
||||||
|
|
||||||
|
return lg.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func fileReaders(names []string) iter.Seq[io.Reader] {
|
||||||
|
return iter.Seq[io.Reader](func(yield func(io.Reader) bool) {
|
||||||
|
for _, name := range names {
|
||||||
|
f, err := os.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !yield(f) {
|
||||||
|
f.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type HttpReader struct {
|
||||||
|
ctx context.Context
|
||||||
|
uri url.URL
|
||||||
|
tmpfile *os.File
|
||||||
|
pos int64
|
||||||
|
end int64
|
||||||
|
bytesRead int
|
||||||
|
}
|
||||||
|
|
||||||
|
func OpenHttpReader(ctx context.Context, uri string, end int64) (*HttpReader, error) {
|
||||||
|
u, err := url.Parse(uri)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &HttpReader{ctx: ctx, uri: *u, end: end}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HttpReader) Read(p []byte) (int, error) {
|
||||||
|
n, err := r.ReadAt(p, r.pos)
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
r.pos += int64(n)
|
||||||
|
r.bytesRead += n
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HttpReader) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
switch whence {
|
||||||
|
case io.SeekStart:
|
||||||
|
r.pos = offset
|
||||||
|
case io.SeekCurrent:
|
||||||
|
r.pos += offset
|
||||||
|
case io.SeekEnd:
|
||||||
|
r.pos = r.end + offset
|
||||||
|
}
|
||||||
|
return r.pos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HttpReader) Close() error {
|
||||||
|
r.ctx.Done()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadAt implements io.ReaderAt. It reads data from the internal buffer starting
|
||||||
|
// from the specified offset and writes it into the provided data slice. If the
|
||||||
|
// offset is negative, it returns an error. If the requested read extends beyond
|
||||||
|
// the buffer's length, it returns the data read so far along with an io.EOF error.
|
||||||
|
func (r *HttpReader) ReadAt(data []byte, offset int64) (int, error) {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset < 0 {
|
||||||
|
return 0, errors.New("negative offset")
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.end > 0 && offset > r.end {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
dlen := len(data) + int(offset)
|
||||||
|
|
||||||
|
if r.end > 0 && r.end+int64(dlen) > r.end {
|
||||||
|
dlen = int(r.end)
|
||||||
|
}
|
||||||
|
|
||||||
|
end := ""
|
||||||
|
if r.end > 0 {
|
||||||
|
end = fmt.Sprintf("/%d", r.end)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(r.ctx, "GET", r.uri.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d%s", offset, dlen, end))
|
||||||
|
|
||||||
|
fmt.Fprintln(Console.Stderr, req)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusRequestedRangeNotSatisfiable {
|
||||||
|
fmt.Fprintln(Console.Stderr, "requested range not satisfiable")
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
|
||||||
|
r.tmpfile, err = os.CreateTemp("", "httpReader")
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer os.Remove(r.tmpfile.Name())
|
||||||
|
n, err := io.Copy(r.tmpfile, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
r.bytesRead += int(n)
|
||||||
|
|
||||||
|
defer fmt.Fprintln(Console.Stderr, "wrote ", n, " bytes to ", r.tmpfile.Name())
|
||||||
|
resp.Body.Close()
|
||||||
|
r.tmpfile.Seek(offset, 0)
|
||||||
|
return io.ReadFull(r.tmpfile, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := io.ReadFull(resp.Body, data)
|
||||||
|
if n == 0 && err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
r.bytesRead += n
|
||||||
|
defer fmt.Fprintln(Console.Stderr, "read ", n, " bytes")
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
104
lsm/cli/main_test.go
Normal file
104
lsm/cli/main_test.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreate(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
wantOutput string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no input files",
|
||||||
|
args: args{
|
||||||
|
Create: true,
|
||||||
|
Archive: "test.txt",
|
||||||
|
Files: []string{},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
wantOutput: "creating test.txt from []\nwrote 0 files\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one input file",
|
||||||
|
args: args{
|
||||||
|
Create: true,
|
||||||
|
Archive: "test.txt",
|
||||||
|
Files: []string{"test_input.txt"},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
wantOutput: "creating test.txt from [test_input.txt]\nwrote 1 files\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple input files",
|
||||||
|
args: args{
|
||||||
|
Create: true,
|
||||||
|
Archive: "test.txt",
|
||||||
|
Files: []string{"test_input1.txt", "test_input2.txt"},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
wantOutput: "creating test.txt from [test_input1.txt test_input2.txt]\nwrote 2 files\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-existent input files",
|
||||||
|
args: args{
|
||||||
|
Create: true,
|
||||||
|
Archive: "test.txt",
|
||||||
|
Files: []string{"non_existent_file.txt"},
|
||||||
|
}, wantErr: false,
|
||||||
|
wantOutput: "creating test.txt from [non_existent_file.txt]\nwrote 0 files\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid command",
|
||||||
|
args: args{
|
||||||
|
Create: false,
|
||||||
|
Archive: "test.txt",
|
||||||
|
Files: []string{},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantOutput: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
// Create a temporary directory for the input files
|
||||||
|
tmpDir, err := os.MkdirTemp("", "lsm2-cli-test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
os.Chdir(tmpDir)
|
||||||
|
|
||||||
|
// Create the input files
|
||||||
|
for _, file := range tc.args.Files {
|
||||||
|
if file == "non_existent_file.txt" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(file, []byte(file), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a buffer to capture the output
|
||||||
|
var output bytes.Buffer
|
||||||
|
|
||||||
|
// Call the create function
|
||||||
|
err = run(console{Stdout: &output}, tc.args)
|
||||||
|
|
||||||
|
// Check the output
|
||||||
|
if output.String() != tc.wantOutput {
|
||||||
|
t.Errorf("run() output = %q, want %q", output.String(), tc.wantOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for errors
|
||||||
|
if tc.wantErr && err == nil {
|
||||||
|
t.Errorf("run() did not return an error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
634
lsm/sst.go
Normal file
634
lsm/sst.go
Normal file
@@ -0,0 +1,634 @@
|
|||||||
|
package lsm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
|
"io"
|
||||||
|
"iter"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// [Sour.is|size] [size|hash][data][hash|flag|size]... [prev|count|flag|size]
|
||||||
|
|
||||||
|
// Commit1: [magic>|<end]{10} ... [<count][<size][<flag]{3..30}
|
||||||
|
// +---------|--------------------------------> end = seek to end of file
|
||||||
|
// <---|-------------+ size = seek to magic header
|
||||||
|
// <---|-------------+10 size + 10 = seek to start of file
|
||||||
|
// <-----------------------------T+10----------------> 10 + size + trailer = full file size
|
||||||
|
|
||||||
|
// Commit2: [magic>|<end]{10} ... [<count][<size][<flag]{3..30} ... [<prev][<count][<size][<flag]{4..40}
|
||||||
|
// <---|---------+
|
||||||
|
// <-------------+T----------------->
|
||||||
|
// +--------|------------------------------------------------------------------------->
|
||||||
|
// <-------------------------------------|----------------+
|
||||||
|
// prev = seek to last commit <---|-+
|
||||||
|
// prev + trailer = size of commit <----T+--------------------------------->
|
||||||
|
|
||||||
|
// Block: [hash>|<end]{10} ... [<size][<flag]{2..20}
|
||||||
|
// +---------|------------------------> end = seek to end of block
|
||||||
|
// <---|-+ size = seek to end of header
|
||||||
|
// <-------------------|-+10 size + 10 = seek to start of block
|
||||||
|
// <---------------------T+10---------------> size + 10 + trailer = full block size
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeUnknown uint64 = iota
|
||||||
|
TypeSegment
|
||||||
|
TypeCommit
|
||||||
|
TypePrevCommit
|
||||||
|
|
||||||
|
headerSize = 10
|
||||||
|
|
||||||
|
maxCommitSize = 4 * binary.MaxVarintLen64
|
||||||
|
minCommitSize = 3
|
||||||
|
|
||||||
|
maxBlockSize = 2 * binary.MaxVarintLen64
|
||||||
|
minBlockSize = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Magic = [10]byte([]byte("Sour.is\x00\x00\x00"))
|
||||||
|
Version = uint8(1)
|
||||||
|
hash = fnv.New64a
|
||||||
|
|
||||||
|
ErrDecode = errors.New("decode")
|
||||||
|
)
|
||||||
|
|
||||||
|
type header struct {
|
||||||
|
end uint64
|
||||||
|
extra []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
|
||||||
|
// It decodes the input binary data into the header struct.
|
||||||
|
// The function expects the input data to be of a specific size (headerSize),
|
||||||
|
// otherwise it returns an error indicating bad data.
|
||||||
|
// It reads the 'end' field from the binary data, updates the 'extra' field,
|
||||||
|
// and reverses the byte order of 'extra' in place.
|
||||||
|
func (h *header) UnmarshalBinary(data []byte) error {
|
||||||
|
if len(data) != headerSize {
|
||||||
|
return fmt.Errorf("%w: bad data", ErrDecode)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.extra = make([]byte, headerSize)
|
||||||
|
copy(h.extra, data)
|
||||||
|
|
||||||
|
var bytesRead int
|
||||||
|
h.end, bytesRead = binary.Uvarint(h.extra)
|
||||||
|
reverse(h.extra)
|
||||||
|
h.extra = h.extra[:min(8,headerSize-bytesRead)]
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Commit struct {
|
||||||
|
flag uint64 // flag values
|
||||||
|
size uint64 // size of the trailer
|
||||||
|
count uint64 // number of entries
|
||||||
|
prev uint64 // previous commit
|
||||||
|
|
||||||
|
tsize int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append marshals the trailer into binary form and appends it to data.
|
||||||
|
// It returns the new slice.
|
||||||
|
func (h *Commit) AppendTrailer(data []byte) []byte {
|
||||||
|
h.flag |= TypeCommit
|
||||||
|
// if h.prev > 0 {
|
||||||
|
// h.flag |= TypePrevCommit
|
||||||
|
// }
|
||||||
|
|
||||||
|
size := len(data)
|
||||||
|
data = binary.AppendUvarint(data, h.size)
|
||||||
|
data = binary.AppendUvarint(data, h.flag)
|
||||||
|
data = binary.AppendUvarint(data, h.count)
|
||||||
|
// if h.prev > 0 {
|
||||||
|
// data = binary.AppendUvarint(data, h.prev)
|
||||||
|
// }
|
||||||
|
reverse(data[size:])
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
|
||||||
|
// It reads a trailer from binary data, and sets the fields
|
||||||
|
// of the receiver to the values found in the header.
|
||||||
|
func (h *Commit) UnmarshalBinary(data []byte) error {
|
||||||
|
if len(data) < minCommitSize {
|
||||||
|
return fmt.Errorf("%w: bad data", ErrDecode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var n int
|
||||||
|
h.size, n = binary.Uvarint(data)
|
||||||
|
data = data[n:]
|
||||||
|
h.tsize += n
|
||||||
|
|
||||||
|
h.flag, n = binary.Uvarint(data)
|
||||||
|
data = data[n:]
|
||||||
|
h.tsize += n
|
||||||
|
|
||||||
|
h.count, n = binary.Uvarint(data)
|
||||||
|
data = data[n:]
|
||||||
|
h.tsize += n
|
||||||
|
|
||||||
|
// h.prev = h.size
|
||||||
|
if h.flag&TypePrevCommit == TypePrevCommit {
|
||||||
|
h.prev, n = binary.Uvarint(data)
|
||||||
|
h.tsize += n
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Block struct {
|
||||||
|
header
|
||||||
|
|
||||||
|
size uint64
|
||||||
|
flag uint64
|
||||||
|
|
||||||
|
tsize int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Block) AppendHeader(data []byte) []byte {
|
||||||
|
size := len(data)
|
||||||
|
data = append(data, make([]byte, 10)...)
|
||||||
|
copy(data, h.extra)
|
||||||
|
if h.size == 0 {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
hdata := binary.AppendUvarint(make([]byte, 0, 10), h.end)
|
||||||
|
reverse(hdata)
|
||||||
|
copy(data[size+10-len(hdata):], hdata)
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendTrailer marshals the footer into binary form and appends it to data.
|
||||||
|
// It returns the new slice.
|
||||||
|
func (h *Block) AppendTrailer(data []byte) []byte {
|
||||||
|
size := len(data)
|
||||||
|
|
||||||
|
h.flag |= TypeSegment
|
||||||
|
data = binary.AppendUvarint(data, h.size)
|
||||||
|
data = binary.AppendUvarint(data, h.flag)
|
||||||
|
reverse(data[size:])
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
|
||||||
|
// It reads a footer from binary data, and sets the fields
|
||||||
|
// of the receiver to the values found in the footer.
|
||||||
|
func (h *Block) UnmarshalBinary(data []byte) error {
|
||||||
|
if len(data) < minBlockSize {
|
||||||
|
return fmt.Errorf("%w: bad data", ErrDecode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var n int
|
||||||
|
h.size, n = binary.Uvarint(data)
|
||||||
|
data = data[n:]
|
||||||
|
h.tsize += n
|
||||||
|
|
||||||
|
h.flag, n = binary.Uvarint(data)
|
||||||
|
h.tsize += n
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type logFile struct {
|
||||||
|
header
|
||||||
|
Commit
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *logFile) AppendMagic(data []byte) []byte {
|
||||||
|
size := len(data)
|
||||||
|
data = append(data, Magic[:]...)
|
||||||
|
if h.end == 0 {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
hdata := binary.AppendUvarint(make([]byte, 0, 10), h.end)
|
||||||
|
reverse(hdata)
|
||||||
|
copy(data[size+10-len(hdata):], hdata)
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteLogFile writes a log file to w, given a list of segments.
|
||||||
|
// The caller is responsible for calling WriteAt on the correct offset.
|
||||||
|
// The function will return an error if any of the segments fail to write.
|
||||||
|
// The offset is the initial offset of the first segment, and will be
|
||||||
|
// incremented by the length of the segment on each write.
|
||||||
|
//
|
||||||
|
// The log file is written with the following format:
|
||||||
|
// - A header with the magic, version, and flag (Dirty)
|
||||||
|
// - A series of segments, each with:
|
||||||
|
// - A footer with the length and hash of the segment
|
||||||
|
// - The contents of the segment
|
||||||
|
// - A header with the magic, version, flag (Clean), and end offset
|
||||||
|
func WriteLogFile(w io.WriterAt, segments iter.Seq[io.Reader]) error {
|
||||||
|
_, err := w.WriteAt(Magic[:], 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
lf := &LogWriter{
|
||||||
|
WriterAt: w,
|
||||||
|
}
|
||||||
|
|
||||||
|
return lf.writeIter(segments)
|
||||||
|
}
|
||||||
|
|
||||||
|
type rw interface {
|
||||||
|
io.ReaderAt
|
||||||
|
io.WriterAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func AppendLogFile(rw rw, segments iter.Seq[io.Reader]) error {
|
||||||
|
logFile, err := ReadLogFile(rw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
lf := &LogWriter{
|
||||||
|
WriterAt: rw,
|
||||||
|
logFile: logFile.logFile,
|
||||||
|
}
|
||||||
|
return lf.writeIter(segments)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lf *LogWriter) writeIter(segments iter.Seq[io.Reader]) error {
|
||||||
|
lf.size = 0
|
||||||
|
for s := range segments {
|
||||||
|
n, err := lf.writeBlock(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
lf.end += n
|
||||||
|
lf.size += n
|
||||||
|
lf.count++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the footer to the log file.
|
||||||
|
// The footer is written at the current end of file position.
|
||||||
|
n, err := lf.WriteAt(lf.AppendTrailer(make([]byte, 0, maxCommitSize)), int64(lf.end)+10)
|
||||||
|
if err != nil {
|
||||||
|
// If there is an error, return it.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
lf.end += uint64(n)
|
||||||
|
|
||||||
|
_, err = lf.WriteAt(lf.AppendMagic(make([]byte, 0, 10)), 0)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogWriter struct {
|
||||||
|
logFile
|
||||||
|
io.WriterAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeBlock writes a segment to the log file at the current end of file position.
|
||||||
|
// The segment is written in chunks of 1024 bytes, and the hash of the segment
|
||||||
|
func (lf *LogWriter) writeBlock(segment io.Reader) (uint64, error) {
|
||||||
|
h := hash()
|
||||||
|
block := Block{}
|
||||||
|
|
||||||
|
start := int64(lf.end) + 10
|
||||||
|
end := start
|
||||||
|
|
||||||
|
bytesWritten := 0
|
||||||
|
|
||||||
|
// Write the header to the log file.
|
||||||
|
// The footer is written at the current end of file position.
|
||||||
|
n, err := lf.WriteAt(make([]byte, headerSize), start)
|
||||||
|
bytesWritten += n
|
||||||
|
end += int64(n)
|
||||||
|
if err != nil {
|
||||||
|
// If there is an error, return it.
|
||||||
|
return uint64(bytesWritten), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the segment to the log file.
|
||||||
|
// The segment is written in chunks of 1024 bytes.
|
||||||
|
for {
|
||||||
|
// Read a chunk of the segment.
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
n, err := segment.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
// If the segment is empty, break the loop.
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// If there is an error, return it.
|
||||||
|
return uint64(bytesWritten), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the hash of the chunk.
|
||||||
|
h.Write(buf[:n])
|
||||||
|
|
||||||
|
// Write the chunk to the log file.
|
||||||
|
// The chunk is written at the current end of file position.
|
||||||
|
n, err = lf.WriteAt(buf[:n], end)
|
||||||
|
bytesWritten += n
|
||||||
|
if err != nil {
|
||||||
|
// If there is an error, return it.
|
||||||
|
return uint64(bytesWritten), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the length of the segment.
|
||||||
|
end += int64(n)
|
||||||
|
block.size += uint64(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
block.extra = h.Sum(nil)
|
||||||
|
block.end += block.size
|
||||||
|
|
||||||
|
// Write the footer to the log file.
|
||||||
|
// The footer is written at the current end of file position.
|
||||||
|
n, err = lf.WriteAt(block.AppendTrailer(make([]byte, 0, maxBlockSize)), end)
|
||||||
|
bytesWritten += n
|
||||||
|
if err != nil {
|
||||||
|
// If there is an error, return it.
|
||||||
|
return uint64(bytesWritten), err
|
||||||
|
}
|
||||||
|
end += int64(n)
|
||||||
|
block.end += uint64(n)
|
||||||
|
|
||||||
|
// Update header to the log file.
|
||||||
|
// The footer is written at the current end of file position.
|
||||||
|
_, err = lf.WriteAt(block.AppendHeader(make([]byte, 0, headerSize)), start)
|
||||||
|
if err != nil {
|
||||||
|
// If there is an error, return it.
|
||||||
|
return uint64(bytesWritten), err
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint64(bytesWritten), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// reverse reverses a slice in-place.
|
||||||
|
func reverse[T any](b []T) {
|
||||||
|
l := len(b)
|
||||||
|
for i := 0; i < l/2; i++ {
|
||||||
|
b[i], b[l-i-1] = b[l-i-1], b[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogReader struct {
|
||||||
|
logFile
|
||||||
|
io.ReaderAt
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadLogFile reads a log file from the given io.ReaderAt. It returns a pointer to a LogFile, or an error if the file
|
||||||
|
// could not be read.
|
||||||
|
func ReadLogFile(reader io.ReaderAt) (*LogReader, error) {
|
||||||
|
header := make([]byte, headerSize)
|
||||||
|
n, err := rsr(reader, 0, 10).ReadAt(header, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
header = header[:n]
|
||||||
|
|
||||||
|
logFile := &LogReader{ReaderAt: reader}
|
||||||
|
err = logFile.header.UnmarshalBinary(header)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if logFile.end == 0 {
|
||||||
|
return logFile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
commit := make([]byte, maxCommitSize)
|
||||||
|
n, err = rsr(reader, 10, int64(logFile.end)).ReadAt(commit, 0)
|
||||||
|
if n == 0 && err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
commit = commit[:n]
|
||||||
|
|
||||||
|
err = logFile.Commit.UnmarshalBinary(commit)
|
||||||
|
|
||||||
|
return logFile, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate reads the log file and calls the given function for each segment.
|
||||||
|
// It passes an io.Reader that reads from the current segment. It will stop
|
||||||
|
// calling the function if the function returns false.
|
||||||
|
func (lf *LogReader) Iter(begin uint64) iter.Seq2[blockInfo, io.Reader] {
|
||||||
|
var commits []*Commit
|
||||||
|
for commit := range lf.iterCommits() {
|
||||||
|
commits = append(commits, &commit)
|
||||||
|
}
|
||||||
|
if lf.Err != nil {
|
||||||
|
return func(yield func(blockInfo, io.Reader) bool) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
reverse(commits)
|
||||||
|
|
||||||
|
return func(yield func(blockInfo, io.Reader) bool) {
|
||||||
|
start := int64(10)
|
||||||
|
var adj uint64
|
||||||
|
for _, commit := range commits {
|
||||||
|
size := int64(commit.size)
|
||||||
|
it := iterBlocks(io.NewSectionReader(lf, start, size), size)
|
||||||
|
for bi, block := range it {
|
||||||
|
bi.Commit = *commit
|
||||||
|
bi.Index += adj
|
||||||
|
bi.Start += uint64(start)
|
||||||
|
if begin <= bi.Index {
|
||||||
|
if !yield(bi, block) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start += size + int64(commit.tsize)
|
||||||
|
adj = commit.count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type blockInfo struct{
|
||||||
|
Index uint64
|
||||||
|
Commit Commit
|
||||||
|
Start uint64
|
||||||
|
Size uint64
|
||||||
|
Hash []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func iterBlocks(r io.ReaderAt, end int64) iter.Seq2[blockInfo, io.Reader] {
|
||||||
|
var start int64
|
||||||
|
var i uint64
|
||||||
|
var bi blockInfo
|
||||||
|
|
||||||
|
return func(yield func(blockInfo, io.Reader) bool) {
|
||||||
|
buf := make([]byte, maxBlockSize)
|
||||||
|
for start < end {
|
||||||
|
block := &Block{}
|
||||||
|
buf = buf[:10]
|
||||||
|
n, err := rsr(r, int64(start), 10).ReadAt(buf, 0)
|
||||||
|
if n == 0 && err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
start += int64(n)
|
||||||
|
|
||||||
|
if err := block.header.UnmarshalBinary(buf); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf = buf[:maxBlockSize]
|
||||||
|
n, err = rsr(r, int64(start), int64(block.end)).ReadAt(buf, 0)
|
||||||
|
if n == 0 && err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf = buf[:n]
|
||||||
|
err = block.UnmarshalBinary(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bi.Index = i
|
||||||
|
bi.Start = uint64(start)
|
||||||
|
bi.Size = block.size
|
||||||
|
bi.Hash = block.extra
|
||||||
|
|
||||||
|
if !yield(bi, io.NewSectionReader(r, int64(start), int64(block.size))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
i++
|
||||||
|
start += int64(block.end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lf *LogReader) iterCommits() iter.Seq[Commit] {
|
||||||
|
if lf.end == 0 {
|
||||||
|
return slices.Values([]Commit(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := lf.end - lf.size - uint64(lf.tsize)
|
||||||
|
return func(yield func(Commit) bool) {
|
||||||
|
if !yield(lf.Commit) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, maxCommitSize)
|
||||||
|
|
||||||
|
for offset > 10 {
|
||||||
|
commit := Commit{}
|
||||||
|
buf = buf[:10]
|
||||||
|
n, err := rsr(lf, 10, int64(offset)).ReadAt(buf, 0)
|
||||||
|
if n == 0 && err != nil {
|
||||||
|
lf.Err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf = buf[:n]
|
||||||
|
err = commit.UnmarshalBinary(buf)
|
||||||
|
if err != nil {
|
||||||
|
lf.Err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !yield(commit) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
offset -= commit.size + uint64(commit.tsize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lf *LogReader) Rev(begin uint64) iter.Seq2[blockInfo, io.Reader] {
|
||||||
|
end := lf.end + 10
|
||||||
|
bi := blockInfo{}
|
||||||
|
bi.Index = lf.count-1
|
||||||
|
|
||||||
|
return func(yield func(blockInfo, io.Reader) bool) {
|
||||||
|
buf := make([]byte, maxBlockSize)
|
||||||
|
|
||||||
|
for commit := range lf.iterCommits() {
|
||||||
|
end -= uint64(commit.tsize)
|
||||||
|
start := end - commit.size
|
||||||
|
|
||||||
|
bi.Commit = commit
|
||||||
|
|
||||||
|
for start < end {
|
||||||
|
block := &Block{}
|
||||||
|
buf = buf[:maxBlockSize]
|
||||||
|
n, err := rsr(lf, int64(start), int64(commit.size)).ReadAt(buf, 0)
|
||||||
|
if n == 0 && err != nil {
|
||||||
|
lf.Err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf = buf[:n]
|
||||||
|
err = block.UnmarshalBinary(buf)
|
||||||
|
if err != nil {
|
||||||
|
lf.Err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if begin >= bi.Index {
|
||||||
|
|
||||||
|
bi.Start = uint64(end-block.size)-uint64(block.tsize)
|
||||||
|
bi.Size = block.size
|
||||||
|
|
||||||
|
buf = buf[:10]
|
||||||
|
_, err = rsr(lf, int64(bi.Start)-10, 10).ReadAt(buf, 0)
|
||||||
|
if err != nil {
|
||||||
|
lf.Err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = block.header.UnmarshalBinary(buf)
|
||||||
|
if err != nil {
|
||||||
|
lf.Err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bi.Hash = block.extra
|
||||||
|
|
||||||
|
if !yield(bi, io.NewSectionReader(lf, int64(bi.Start), int64(bi.Size))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end -= block.size + 10 + uint64(block.tsize)
|
||||||
|
bi.Index--
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lf *LogReader) Count() uint64 {
|
||||||
|
return lf.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lf *LogReader) Size() uint64 {
|
||||||
|
return lf.end + 10
|
||||||
|
}
|
||||||
|
|
||||||
|
func rsr(r io.ReaderAt, offset, size int64) *revSegmentReader {
|
||||||
|
r = io.NewSectionReader(r, offset, size)
|
||||||
|
return &revSegmentReader{r, size}
|
||||||
|
}
|
||||||
|
|
||||||
|
type revSegmentReader struct {
|
||||||
|
io.ReaderAt
|
||||||
|
size int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *revSegmentReader) ReadAt(data []byte, offset int64) (int, error) {
|
||||||
|
if offset < 0 {
|
||||||
|
return 0, errors.New("negative offset")
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset > int64(r.size) {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
o := r.size - int64(len(data)) - offset
|
||||||
|
d := int64(len(data))
|
||||||
|
if o < 0 {
|
||||||
|
d = max(0, d+o)
|
||||||
|
}
|
||||||
|
|
||||||
|
i, err := r.ReaderAt.ReadAt(data[:d], max(0, o))
|
||||||
|
reverse(data[:i])
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
300
lsm/sst_test.go
Normal file
300
lsm/sst_test.go
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
package lsm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"iter"
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docopt/docopt-go"
|
||||||
|
"github.com/matryer/is"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestWriteLogFile tests AppendLogFile and WriteLogFile against a set of test cases.
|
||||||
|
//
|
||||||
|
// Each test case contains a slice of slices of io.Readers, which are passed to
|
||||||
|
// AppendLogFile and WriteLogFile in order. The test case also contains the
|
||||||
|
// expected encoded output as a base64 string, as well as the expected output
|
||||||
|
// when the file is read back using ReadLogFile.
|
||||||
|
//
|
||||||
|
// The test case also contains the expected output when the file is read back in
|
||||||
|
// reverse order using ReadLogFile.Rev().
|
||||||
|
//
|
||||||
|
// The test cases are as follows:
|
||||||
|
//
|
||||||
|
// - nil reader: Passes a nil slice of io.Readers to WriteLogFile.
|
||||||
|
// - err reader: Passes a slice of io.Readers to WriteLogFile which returns an
|
||||||
|
// error when read.
|
||||||
|
// - single reader: Passes a single io.Reader to WriteLogFile.
|
||||||
|
// - multiple readers: Passes a slice of multiple io.Readers to WriteLogFile.
|
||||||
|
// - multiple commit: Passes multiple slices of io.Readers to AppendLogFile.
|
||||||
|
// - multiple commit 3x: Passes multiple slices of io.Readers to AppendLogFile
|
||||||
|
// three times.
|
||||||
|
//
|
||||||
|
// The test uses the is package from github.com/matryer/is to check that the
|
||||||
|
// output matches the expected output.
|
||||||
|
func TestWriteLogFile(t *testing.T) {
|
||||||
|
type test struct {
|
||||||
|
name string
|
||||||
|
in [][]io.Reader
|
||||||
|
enc string
|
||||||
|
out [][]byte
|
||||||
|
rev [][]byte
|
||||||
|
}
|
||||||
|
tests := []test{
|
||||||
|
{
|
||||||
|
name: "nil reader",
|
||||||
|
in: nil,
|
||||||
|
enc: "U291ci5pcwAAAwACAA",
|
||||||
|
out: [][]byte{},
|
||||||
|
rev: [][]byte{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "err reader",
|
||||||
|
in: nil,
|
||||||
|
enc: "U291ci5pcwAAAwACAA",
|
||||||
|
out: [][]byte{},
|
||||||
|
rev: [][]byte{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single reader",
|
||||||
|
in: [][]io.Reader{
|
||||||
|
{
|
||||||
|
bytes.NewBuffer([]byte{1, 2, 3, 4})}},
|
||||||
|
enc: "U291ci5pcwAAE756XndRZXhdAAYBAgMEAQQBAhA",
|
||||||
|
out: [][]byte{{1, 2, 3, 4}},
|
||||||
|
rev: [][]byte{{1, 2, 3, 4}}},
|
||||||
|
{
|
||||||
|
name: "multiple readers",
|
||||||
|
in: [][]io.Reader{
|
||||||
|
{
|
||||||
|
bytes.NewBuffer([]byte{1, 2, 3, 4}),
|
||||||
|
bytes.NewBuffer([]byte{5, 6, 7, 8})}},
|
||||||
|
enc: "U291ci5pcwAAI756XndRZXhdAAYBAgMEAQRhQyZWDDn5BQAGBQYHCAEEAgIg",
|
||||||
|
out: [][]byte{{1, 2, 3, 4}, {5, 6, 7, 8}},
|
||||||
|
rev: [][]byte{{5, 6, 7, 8}, {1, 2, 3, 4}}},
|
||||||
|
{
|
||||||
|
name: "multiple commit",
|
||||||
|
in: [][]io.Reader{
|
||||||
|
{
|
||||||
|
bytes.NewBuffer([]byte{1, 2, 3, 4})},
|
||||||
|
{
|
||||||
|
bytes.NewBuffer([]byte{5, 6, 7, 8})}},
|
||||||
|
enc: "U291ci5pcwAAJr56XndRZXhdAAYBAgMEAQQBAhBhQyZWDDn5BQAGBQYHCAEEAgIQ",
|
||||||
|
out: [][]byte{{1, 2, 3, 4}, {5, 6, 7, 8}},
|
||||||
|
rev: [][]byte{{5, 6, 7, 8}, {1, 2, 3, 4}}},
|
||||||
|
{
|
||||||
|
name: "multiple commit",
|
||||||
|
in: [][]io.Reader{
|
||||||
|
{
|
||||||
|
bytes.NewBuffer([]byte{1, 2, 3, 4}),
|
||||||
|
bytes.NewBuffer([]byte{5, 6, 7, 8})},
|
||||||
|
{
|
||||||
|
bytes.NewBuffer([]byte{9, 10, 11, 12})},
|
||||||
|
},
|
||||||
|
enc: "U291ci5pcwAANr56XndRZXhdAAYBAgMEAQRhQyZWDDn5BQAGBQYHCAEEAgIgA4Buuio8Ro0ABgkKCwwBBAMCEA",
|
||||||
|
out: [][]byte{{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}},
|
||||||
|
rev: [][]byte{{9, 10, 11, 12}, {5, 6, 7, 8}, {1, 2, 3, 4}}},
|
||||||
|
{
|
||||||
|
name: "multiple commit 3x",
|
||||||
|
in: [][]io.Reader{
|
||||||
|
{
|
||||||
|
bytes.NewBuffer([]byte{1, 2, 3}),
|
||||||
|
bytes.NewBuffer([]byte{4, 5, 6}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bytes.NewBuffer([]byte{7, 8, 9}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bytes.NewBuffer([]byte{10, 11, 12}),
|
||||||
|
bytes.NewBuffer([]byte{13, 14, 15}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enc: "U291ci5pcwAAVNCqYhhnLPWrAAUBAgMBA7axWhhYd+HsAAUEBQYBAwICHr9ryhhdbkEZAAUHCAkBAwMCDy/UIhidCwCqAAUKCwwBA/NCwhh6wXgXAAUNDg8BAwUCHg",
|
||||||
|
out: [][]byte{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10, 11, 12}, {13, 14, 15}},
|
||||||
|
rev: [][]byte{{13, 14, 15}, {10, 11, 12}, {7, 8, 9}, {4, 5, 6}, {1, 2, 3}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
buf := &buffer{}
|
||||||
|
|
||||||
|
buffers := 0
|
||||||
|
|
||||||
|
if len(test.in) == 0 {
|
||||||
|
err := WriteLogFile(buf, slices.Values([]io.Reader{}))
|
||||||
|
is.NoErr(err)
|
||||||
|
}
|
||||||
|
for i, in := range test.in {
|
||||||
|
buffers += len(in)
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
err := WriteLogFile(buf, slices.Values(in))
|
||||||
|
is.NoErr(err)
|
||||||
|
} else {
|
||||||
|
err := AppendLogFile(buf, slices.Values(in))
|
||||||
|
is.NoErr(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is.Equal(base64.RawStdEncoding.EncodeToString(buf.Bytes()), test.enc)
|
||||||
|
|
||||||
|
files, err := ReadLogFile(bytes.NewReader(buf.Bytes()))
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
is.Equal(files.Size(), uint64(len(buf.Bytes())))
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
for bi, fp := range files.Iter(0) {
|
||||||
|
buf, err := io.ReadAll(fp)
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
hash := hash()
|
||||||
|
hash.Write(buf)
|
||||||
|
is.Equal(bi.Hash, hash.Sum(nil)[:len(bi.Hash)])
|
||||||
|
|
||||||
|
is.True(len(test.out) > int(bi.Index))
|
||||||
|
is.Equal(buf, test.out[bi.Index])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
is.NoErr(files.Err)
|
||||||
|
is.Equal(i, buffers)
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
for bi, fp := range files.Rev(files.Count()) {
|
||||||
|
buf, err := io.ReadAll(fp)
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
hash := hash()
|
||||||
|
hash.Write(buf)
|
||||||
|
is.Equal(bi.Hash, hash.Sum(nil)[:len(bi.Hash)])
|
||||||
|
|
||||||
|
is.Equal(buf, test.rev[i])
|
||||||
|
is.Equal(buf, test.out[bi.Index])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
is.NoErr(files.Err)
|
||||||
|
is.Equal(i, buffers)
|
||||||
|
is.Equal(files.Count(), uint64(i))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestArgs tests that the CLI arguments are correctly parsed.
|
||||||
|
func TestArgs(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
usage := `Usage: lsm2 create <archive> <files>...`
|
||||||
|
|
||||||
|
arguments, err := docopt.ParseArgs(usage, []string{"create", "archive", "file1", "file2"}, "1.0")
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
var params struct {
|
||||||
|
Create bool `docopt:"create"`
|
||||||
|
Archive string `docopt:"<archive>"`
|
||||||
|
Files []string `docopt:"<files>"`
|
||||||
|
}
|
||||||
|
err = arguments.Bind(¶ms)
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
is.Equal(params.Create, true)
|
||||||
|
is.Equal(params.Archive, "archive")
|
||||||
|
is.Equal(params.Files, []string{"file1", "file2"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkIterate(b *testing.B) {
|
||||||
|
block := make([]byte, 1024)
|
||||||
|
buf := &buffer{}
|
||||||
|
|
||||||
|
|
||||||
|
b.Run("write", func(b *testing.B) {
|
||||||
|
WriteLogFile(buf, func(yield func(io.Reader) bool) {
|
||||||
|
for range (b.N) {
|
||||||
|
if !yield(bytes.NewBuffer(block)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("read", func(b *testing.B) {
|
||||||
|
lf, _ := ReadLogFile(buf)
|
||||||
|
b.Log(lf.Count())
|
||||||
|
for range (b.N) {
|
||||||
|
for _, fp := range lf.Iter(0) {
|
||||||
|
_, _ = io.Copy(io.Discard, fp)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("rev", func(b *testing.B) {
|
||||||
|
lf, _ := ReadLogFile(buf)
|
||||||
|
b.Log(lf.Count())
|
||||||
|
for range (b.N) {
|
||||||
|
for _, fp := range lf.Rev(lf.Count()) {
|
||||||
|
_, _ = io.Copy(io.Discard, fp)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type buffer struct {
|
||||||
|
buf []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns the underlying byte slice of the bufferWriterAt.
|
||||||
|
func (b *buffer) Bytes() []byte {
|
||||||
|
return b.buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteAt implements io.WriterAt. It appends data to the internal buffer
|
||||||
|
// if the offset is beyond the current length of the buffer. It will
|
||||||
|
// return an error if the offset is negative.
|
||||||
|
func (b *buffer) WriteAt(data []byte, offset int64) (written int, err error) {
|
||||||
|
if offset < 0 {
|
||||||
|
return 0, errors.New("negative offset")
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLength := int64(len(b.buf))
|
||||||
|
if currentLength < offset+int64(len(data)) {
|
||||||
|
b.buf = append(b.buf, make([]byte, offset+int64(len(data))-currentLength)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
written = copy(b.buf[offset:], data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadAt implements io.ReaderAt. It reads data from the internal buffer starting
|
||||||
|
// from the specified offset and writes it into the provided data slice. If the
|
||||||
|
// offset is negative, it returns an error. If the requested read extends beyond
|
||||||
|
// the buffer's length, it returns the data read so far along with an io.EOF error.
|
||||||
|
func (b *buffer) ReadAt(data []byte, offset int64) (int, error) {
|
||||||
|
if offset < 0 {
|
||||||
|
return 0, errors.New("negative offset")
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset > int64(len(b.buf)) || len(b.buf[offset:]) < len(data) {
|
||||||
|
return copy(data, b.buf[offset:]), io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
return copy(data, b.buf[offset:]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IterOne takes an iterator that yields values of type T along with a value of
|
||||||
|
// type I, and returns an iterator that yields only the values of type T. It
|
||||||
|
// discards the values of type I.
|
||||||
|
func IterOne[I, T any](it iter.Seq2[I, T]) iter.Seq[T] {
|
||||||
|
return func(yield func(T) bool) {
|
||||||
|
for i, v := range it {
|
||||||
|
_ = i
|
||||||
|
if !yield(v) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,9 +8,8 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"go.sour.is/pkg/mercury"
|
|
||||||
"go.sour.is/pkg/ident"
|
"go.sour.is/pkg/ident"
|
||||||
"go.sour.is/pkg/rsql"
|
"go.sour.is/pkg/mercury"
|
||||||
"go.sour.is/pkg/set"
|
"go.sour.is/pkg/set"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,6 +19,7 @@ const (
|
|||||||
mercuryHost = "mercury.host"
|
mercuryHost = "mercury.host"
|
||||||
appDotEnviron = "mercury.environ"
|
appDotEnviron = "mercury.environ"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
mercuryPolicy = func(id string) string { return "mercury.@" + id + ".policy" }
|
mercuryPolicy = func(id string) string { return "mercury.@" + id + ".policy" }
|
||||||
)
|
)
|
||||||
@@ -41,8 +41,13 @@ type mercuryEnviron struct {
|
|||||||
lookup func(context.Context, ident.Ident) (mercury.Rules, error)
|
lookup func(context.Context, ident.Ident) (mercury.Rules, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getSearch(spec mercury.Search) mercury.NamespaceSearch {
|
||||||
|
return spec.NamespaceSearch
|
||||||
|
}
|
||||||
|
|
||||||
// Index returns nil
|
// Index returns nil
|
||||||
func (app *mercuryEnviron) GetIndex(ctx context.Context, search mercury.NamespaceSearch, _ *rsql.Program) (lis mercury.Config, err error) {
|
func (app *mercuryEnviron) GetIndex(ctx context.Context, spec mercury.Search) (lis mercury.Config, err error) {
|
||||||
|
search := getSearch(spec)
|
||||||
|
|
||||||
if search.Match(mercurySource) {
|
if search.Match(mercurySource) {
|
||||||
for _, s := range app.cfg.ToArray() {
|
for _, s := range app.cfg.ToArray() {
|
||||||
@@ -74,7 +79,9 @@ func (app *mercuryEnviron) GetIndex(ctx context.Context, search mercury.Namespac
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Objects returns nil
|
// Objects returns nil
|
||||||
func (app *mercuryEnviron) GetConfig(ctx context.Context, search mercury.NamespaceSearch, _ *rsql.Program, _ []string) (lis mercury.Config, err error) {
|
func (app *mercuryEnviron) GetConfig(ctx context.Context, spec mercury.Search) (lis mercury.Config, err error) {
|
||||||
|
search := getSearch(spec)
|
||||||
|
|
||||||
if search.Match(mercurySource) {
|
if search.Match(mercurySource) {
|
||||||
for _, s := range app.cfg.ToArray() {
|
for _, s := range app.cfg.ToArray() {
|
||||||
if search.Match(s.Space) {
|
if search.Match(s.Space) {
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
package mercury_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/matryer/is"
|
|
||||||
"go.sour.is/pkg/mercury"
|
|
||||||
|
|
||||||
sq "github.com/Masterminds/squirrel"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNamespaceParse(t *testing.T) {
|
|
||||||
var tests = []struct {
|
|
||||||
in string
|
|
||||||
out string
|
|
||||||
args []any
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
in: "d42.bgp.kapha.*;trace:d42.bgp.kapha",
|
|
||||||
out: "(column LIKE ? OR ? LIKE column || '%')",
|
|
||||||
args: []any{"d42.bgp.kapha.%", "d42.bgp.kapha"},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
in: "d42.bgp.kapha.*,d42.bgp.kapha",
|
|
||||||
out: "(column LIKE ? OR column = ?)",
|
|
||||||
args: []any{"d42.bgp.kapha.%", "d42.bgp.kapha"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, tt := range tests {
|
|
||||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
|
||||||
is := is.New(t)
|
|
||||||
out := mercury.ParseNamespace(tt.in)
|
|
||||||
sql, args, err := getWhere(out).ToSql()
|
|
||||||
is.NoErr(err)
|
|
||||||
is.Equal(sql, tt.out)
|
|
||||||
is.Equal(args, tt.args)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getWhere(search mercury.NamespaceSearch) sq.Sqlizer {
|
|
||||||
var where sq.Or
|
|
||||||
space := "column"
|
|
||||||
for _, m := range search {
|
|
||||||
switch m.(type) {
|
|
||||||
case mercury.NamespaceNode:
|
|
||||||
where = append(where, sq.Eq{space: m.Value()})
|
|
||||||
case mercury.NamespaceStar:
|
|
||||||
where = append(where, sq.Like{space: m.Value()})
|
|
||||||
case mercury.NamespaceTrace:
|
|
||||||
e := sq.Expr(`? LIKE `+space+` || '%'`, m.Value())
|
|
||||||
where = append(where, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return where
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ package mercury
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -10,16 +11,15 @@ import (
|
|||||||
|
|
||||||
"go.sour.is/pkg/ident"
|
"go.sour.is/pkg/ident"
|
||||||
"go.sour.is/pkg/lg"
|
"go.sour.is/pkg/lg"
|
||||||
"go.sour.is/pkg/rsql"
|
|
||||||
"go.sour.is/pkg/set"
|
"go.sour.is/pkg/set"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GetIndex interface {
|
type GetIndex interface {
|
||||||
GetIndex(context.Context, NamespaceSearch, *rsql.Program) (Config, error)
|
GetIndex(context.Context, Search) (Config, error)
|
||||||
}
|
}
|
||||||
type GetConfig interface {
|
type GetConfig interface {
|
||||||
GetConfig(context.Context, NamespaceSearch, *rsql.Program, []string) (Config, error)
|
GetConfig(context.Context, Search) (Config, error)
|
||||||
}
|
}
|
||||||
type WriteConfig interface {
|
type WriteConfig interface {
|
||||||
WriteConfig(context.Context, Config) error
|
WriteConfig(context.Context, Config) error
|
||||||
@@ -60,7 +60,7 @@ func (reg *registry) accessFilter(rules Rules, lis Config) (out Config, err erro
|
|||||||
// HandlerItem a single handler matching
|
// HandlerItem a single handler matching
|
||||||
type matcher[T any] struct {
|
type matcher[T any] struct {
|
||||||
Name string
|
Name string
|
||||||
Match NamespaceSearch
|
Match Search
|
||||||
Priority int
|
Priority int
|
||||||
Handler T
|
Handler T
|
||||||
}
|
}
|
||||||
@@ -122,17 +122,23 @@ func (r *registry) Register(name string, h func(*Space) any) {
|
|||||||
func (r *registry) Configure(m SpaceMap) error {
|
func (r *registry) Configure(m SpaceMap) error {
|
||||||
r.resetMatchers()
|
r.resetMatchers()
|
||||||
for space, c := range m {
|
for space, c := range m {
|
||||||
|
log.Println("configure: ", space)
|
||||||
|
|
||||||
if strings.HasPrefix(space, "mercury.source.") {
|
if strings.HasPrefix(space, "mercury.source.") {
|
||||||
space = strings.TrimPrefix(space, "mercury.source.")
|
space = strings.TrimPrefix(space, "mercury.source.")
|
||||||
handler, name, _ := strings.Cut(space, ".")
|
handler, name, _ := strings.Cut(space, ".")
|
||||||
matches := c.FirstValue("match")
|
matches := c.FirstValue("match")
|
||||||
|
readonly := c.HasTag("readonly")
|
||||||
for _, match := range matches.Values {
|
for _, match := range matches.Values {
|
||||||
ps := strings.Fields(match)
|
ps := strings.Fields(match)
|
||||||
priority, err := strconv.Atoi(ps[0])
|
priority, err := strconv.Atoi(ps[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
r.add(name, handler, ps[1], priority, c)
|
err = r.add(name, handler, strings.Join(ps[1:],"|"), priority, c, readonly)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +152,10 @@ func (r *registry) Configure(m SpaceMap) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
r.add(name, handler, ps[1], priority, c)
|
err = r.add(name, handler, strings.Join(ps[1:],"|"), priority, c, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,8 +165,8 @@ func (r *registry) Configure(m SpaceMap) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Register add a handler to registry
|
// Register add a handler to registry
|
||||||
func (r *registry) add(name, handler, match string, priority int, cfg *Space) error {
|
func (r *registry) add(name, handler, match string, priority int, cfg *Space, readonly bool) error {
|
||||||
// log.Infos("mercury regster", "match", match, "pri", priority)
|
log.Println("mercury regster", "match", match, "pri", priority)
|
||||||
mkHandler, ok := r.handlers[handler]
|
mkHandler, ok := r.handlers[handler]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("handler not registered: %s", handler)
|
return fmt.Errorf("handler not registered: %s", handler)
|
||||||
@@ -173,61 +182,68 @@ func (r *registry) add(name, handler, match string, priority int, cfg *Space) er
|
|||||||
if hdlr, ok := hdlr.(GetIndex); ok {
|
if hdlr, ok := hdlr.(GetIndex); ok {
|
||||||
r.matchers.getIndex = append(
|
r.matchers.getIndex = append(
|
||||||
r.matchers.getIndex,
|
r.matchers.getIndex,
|
||||||
matcher[GetIndex]{Name: name, Match: ParseNamespace(match), Priority: priority, Handler: hdlr},
|
matcher[GetIndex]{Name: name, Match: ParseSearch(match), Priority: priority, Handler: hdlr},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if hdlr, ok := hdlr.(GetConfig); ok {
|
if hdlr, ok := hdlr.(GetConfig); ok {
|
||||||
r.matchers.getConfig = append(
|
r.matchers.getConfig = append(
|
||||||
r.matchers.getConfig,
|
r.matchers.getConfig,
|
||||||
matcher[GetConfig]{Name: name, Match: ParseNamespace(match), Priority: priority, Handler: hdlr},
|
matcher[GetConfig]{Name: name, Match: ParseSearch(match), Priority: priority, Handler: hdlr},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if hdlr, ok := hdlr.(WriteConfig); ok {
|
if hdlr, ok := hdlr.(WriteConfig); !readonly && ok {
|
||||||
|
|
||||||
r.matchers.writeConfig = append(
|
r.matchers.writeConfig = append(
|
||||||
r.matchers.writeConfig,
|
r.matchers.writeConfig,
|
||||||
matcher[WriteConfig]{Name: name, Match: ParseNamespace(match), Priority: priority, Handler: hdlr},
|
matcher[WriteConfig]{Name: name, Match: ParseSearch(match), Priority: priority, Handler: hdlr},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if hdlr, ok := hdlr.(GetRules); ok {
|
if hdlr, ok := hdlr.(GetRules); ok {
|
||||||
r.matchers.getRules = append(
|
r.matchers.getRules = append(
|
||||||
r.matchers.getRules,
|
r.matchers.getRules,
|
||||||
matcher[GetRules]{Name: name, Match: ParseNamespace(match), Priority: priority, Handler: hdlr},
|
matcher[GetRules]{Name: name, Match: ParseSearch(match), Priority: priority, Handler: hdlr},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if hdlr, ok := hdlr.(GetNotify); ok {
|
if hdlr, ok := hdlr.(GetNotify); ok {
|
||||||
r.matchers.getNotify = append(
|
r.matchers.getNotify = append(
|
||||||
r.matchers.getNotify,
|
r.matchers.getNotify,
|
||||||
matcher[GetNotify]{Name: name, Match: ParseNamespace(match), Priority: priority, Handler: hdlr},
|
matcher[GetNotify]{Name: name, Match: ParseSearch(match), Priority: priority, Handler: hdlr},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if hdlr, ok := hdlr.(SendNotify); ok {
|
if hdlr, ok := hdlr.(SendNotify); ok {
|
||||||
r.matchers.sendNotify = append(
|
r.matchers.sendNotify = append(
|
||||||
r.matchers.sendNotify,
|
r.matchers.sendNotify,
|
||||||
matcher[SendNotify]{Name: name, Match: ParseNamespace(match), Priority: priority, Handler: hdlr},
|
matcher[SendNotify]{Name: name, Match: ParseSearch(match), Priority: priority, Handler: hdlr},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getMatches(search Search, matchers matchers) []Search {
|
||||||
|
matches := make([]Search, len(matchers.getIndex))
|
||||||
|
|
||||||
|
for _, n := range search.NamespaceSearch {
|
||||||
|
for i, hdlr := range matchers.getIndex {
|
||||||
|
if hdlr.Match.Match(n.Raw()) {
|
||||||
|
matches[i].NamespaceSearch = append(matches[i].NamespaceSearch, n)
|
||||||
|
matches[i].Count = search.Count
|
||||||
|
matches[i].Cursor = search.Cursor // need to decode cursor for the match
|
||||||
|
matches[i].Fields = search.Fields
|
||||||
|
matches[i].Find = search.Find
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
|
||||||
// GetIndex query each handler that match namespace.
|
// GetIndex query each handler that match namespace.
|
||||||
func (r *registry) GetIndex(ctx context.Context, match, search string) (c Config, err error) {
|
func (r *registry) GetIndex(ctx context.Context, search Search) (c Config, err error) {
|
||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
spec := ParseNamespace(match)
|
matches := getMatches(search, r.matchers)
|
||||||
pgm := rsql.DefaultParse(search)
|
|
||||||
matches := make([]NamespaceSearch, len(r.matchers.getIndex))
|
|
||||||
|
|
||||||
for _, n := range spec {
|
|
||||||
for i, hdlr := range r.matchers.getIndex {
|
|
||||||
if hdlr.Match.Match(n.Raw()) {
|
|
||||||
matches[i] = append(matches[i], n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wg, ctx := errgroup.WithContext(ctx)
|
wg, ctx := errgroup.WithContext(ctx)
|
||||||
slots := make(chan Config, len(r.matchers.getConfig))
|
slots := make(chan Config, len(r.matchers.getConfig))
|
||||||
@@ -248,7 +264,7 @@ func (r *registry) GetIndex(ctx context.Context, match, search string) (c Config
|
|||||||
|
|
||||||
wg.Go(func() error {
|
wg.Go(func() error {
|
||||||
span.AddEvent(fmt.Sprintf("INDEX %s %s", hdlr.Name, hdlr.Match))
|
span.AddEvent(fmt.Sprintf("INDEX %s %s", hdlr.Name, hdlr.Match))
|
||||||
lis, err := hdlr.Handler.GetIndex(ctx, matches[i], pgm)
|
lis, err := hdlr.Handler.GetIndex(ctx, matches[i])
|
||||||
slots <- lis
|
slots <- lis
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
@@ -265,31 +281,19 @@ func (r *registry) GetIndex(ctx context.Context, match, search string) (c Config
|
|||||||
// Search query each handler with a key=value search
|
// Search query each handler with a key=value search
|
||||||
|
|
||||||
// GetConfig query each handler that match for fully qualified namespaces.
|
// GetConfig query each handler that match for fully qualified namespaces.
|
||||||
func (r *registry) GetConfig(ctx context.Context, match, search, fields string) (Config, error) {
|
func (r *registry) GetConfig(ctx context.Context, search Search) (Config, error) {
|
||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
spec := ParseNamespace(match)
|
matches := getMatches(search, r.matchers)
|
||||||
pgm := rsql.DefaultParse(search)
|
|
||||||
flds := strings.Split(fields, ",")
|
|
||||||
|
|
||||||
matches := make([]NamespaceSearch, len(r.matchers.getConfig))
|
|
||||||
|
|
||||||
for _, n := range spec {
|
|
||||||
for i, hdlr := range r.matchers.getConfig {
|
|
||||||
if hdlr.Match.Match(n.Raw()) {
|
|
||||||
matches[i] = append(matches[i], n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m := make(SpaceMap)
|
m := make(SpaceMap)
|
||||||
for i, hdlr := range r.matchers.getConfig {
|
for i, hdlr := range r.matchers.getConfig {
|
||||||
if len(matches[i]) == 0 {
|
if len(matches[i].NamespaceSearch) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
span.AddEvent(fmt.Sprintf("QUERY %s %s", hdlr.Name, hdlr.Match))
|
span.AddEvent(fmt.Sprintf("QUERY %s %s", hdlr.Name, hdlr.Match))
|
||||||
lis, err := hdlr.Handler.GetConfig(ctx, matches[i], pgm, flds)
|
lis, err := hdlr.Handler.GetConfig(ctx, matches[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,12 +70,12 @@ func (s *root) configV1(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Print("SPC: ", space)
|
log.Print("SPC: ", space)
|
||||||
ns := ParseNamespace(space)
|
ns := ParseSearch(space)
|
||||||
log.Print("PRE: ", ns)
|
log.Print("PRE: ", ns)
|
||||||
//ns = rules.ReduceSearch(ns)
|
//ns = rules.ReduceSearch(ns)
|
||||||
log.Print("POST: ", ns)
|
log.Print("POST: ", ns)
|
||||||
|
|
||||||
lis, err := Registry.GetConfig(ctx, ns.String(), "", "")
|
lis, err := Registry.GetConfig(ctx, ns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "ERR: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "ERR: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -250,11 +250,11 @@ func (s *root) indexV1(w http.ResponseWriter, r *http.Request) {
|
|||||||
space = "*"
|
space = "*"
|
||||||
}
|
}
|
||||||
|
|
||||||
ns := ParseNamespace(space)
|
ns := ParseSearch(space)
|
||||||
ns = rules.ReduceSearch(ns)
|
ns.NamespaceSearch = rules.ReduceSearch(ns.NamespaceSearch)
|
||||||
span.AddEvent(ns.String())
|
span.AddEvent(ns.String())
|
||||||
|
|
||||||
lis, err := Registry.GetIndex(ctx, ns.String(), "")
|
lis, err := Registry.GetIndex(ctx, ns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
span.RecordError(err)
|
span.RecordError(err)
|
||||||
http.Error(w, "ERR: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "ERR: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -1,11 +1,33 @@
|
|||||||
package mercury
|
package mercury
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NamespaceSpec implements a parsed namespace search
|
// Search implements a parsed namespace search
|
||||||
|
// It parses the input and generates an AST to inform the driver how to select values.
|
||||||
|
// * => all spaces
|
||||||
|
// mercury.* => all prefixed with `mercury.`
|
||||||
|
// mercury.config => only space `mercury.config`
|
||||||
|
// mercury.source.*#readonly => all prefixed with `mercury.source.` AND has tag `readonly`
|
||||||
|
// test.*|mercury.* => all prefixed with `test.` AND `mercury.`
|
||||||
|
// test.* find bin=eq=bar => all prefixed with `test.` AND has an attribute bin that equals bar
|
||||||
|
// test.* fields foo,bin => all prefixed with `test.` only show fields foo and bin
|
||||||
|
// - count 20 => start a cursor with 20 results
|
||||||
|
// - count 20 after <cursor> => continue after cursor for 20 results
|
||||||
|
// cursor encodes start points for each of the matched sources
|
||||||
|
type Search struct {
|
||||||
|
NamespaceSearch
|
||||||
|
Find []ops
|
||||||
|
Fields []string
|
||||||
|
Count uint64
|
||||||
|
Offset uint64
|
||||||
|
Cursor string
|
||||||
|
}
|
||||||
|
|
||||||
type NamespaceSpec interface {
|
type NamespaceSpec interface {
|
||||||
Value() string
|
Value() string
|
||||||
String() string
|
String() string
|
||||||
@@ -17,8 +39,10 @@ type NamespaceSpec interface {
|
|||||||
type NamespaceSearch []NamespaceSpec
|
type NamespaceSearch []NamespaceSpec
|
||||||
|
|
||||||
// ParseNamespace returns a list of parsed values
|
// ParseNamespace returns a list of parsed values
|
||||||
func ParseNamespace(ns string) (lis NamespaceSearch) {
|
func ParseSearch(text string) (search Search) {
|
||||||
for _, part := range strings.Split(ns, ";") {
|
ns, text, _ := strings.Cut(text, " ")
|
||||||
|
var lis NamespaceSearch
|
||||||
|
for _, part := range strings.Split(ns, "|") {
|
||||||
if strings.HasPrefix(part, "trace:") {
|
if strings.HasPrefix(part, "trace:") {
|
||||||
lis = append(lis, NamespaceTrace(part[6:]))
|
lis = append(lis, NamespaceTrace(part[6:]))
|
||||||
} else if strings.Contains(part, "*") {
|
} else if strings.Contains(part, "*") {
|
||||||
@@ -27,6 +51,40 @@ func ParseNamespace(ns string) (lis NamespaceSearch) {
|
|||||||
lis = append(lis, NamespaceNode(part))
|
lis = append(lis, NamespaceNode(part))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
search.NamespaceSearch = lis
|
||||||
|
|
||||||
|
field, text, next := strings.Cut(text, " ")
|
||||||
|
text = strings.TrimSpace(text)
|
||||||
|
for next {
|
||||||
|
switch strings.ToLower(field) {
|
||||||
|
case "find":
|
||||||
|
field, text, _ = strings.Cut(text, " ")
|
||||||
|
text = strings.TrimSpace(text)
|
||||||
|
search.Find = simpleParse(field)
|
||||||
|
|
||||||
|
case "fields":
|
||||||
|
field, text, _ = strings.Cut(text, " ")
|
||||||
|
text = strings.TrimSpace(text)
|
||||||
|
search.Fields = strings.Split(field, ",")
|
||||||
|
|
||||||
|
case "count":
|
||||||
|
field, text, _ = strings.Cut(text, " ")
|
||||||
|
text = strings.TrimSpace(text)
|
||||||
|
search.Count, _ = strconv.ParseUint(field, 10, 64)
|
||||||
|
|
||||||
|
case "offset":
|
||||||
|
field, text, _ = strings.Cut(text, " ")
|
||||||
|
text = strings.TrimSpace(text)
|
||||||
|
search.Offset, _ = strconv.ParseUint(field, 10, 64)
|
||||||
|
|
||||||
|
case "after":
|
||||||
|
field, text, _ = strings.Cut(text, " ")
|
||||||
|
text = strings.TrimSpace(text)
|
||||||
|
search.Cursor = field
|
||||||
|
}
|
||||||
|
field, text, next = strings.Cut(text, " ")
|
||||||
|
text = strings.TrimSpace(text)
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -117,3 +175,28 @@ func match(n NamespaceSpec, s string) bool {
|
|||||||
}
|
}
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ops struct {
|
||||||
|
Left string
|
||||||
|
Op string
|
||||||
|
Right string
|
||||||
|
}
|
||||||
|
|
||||||
|
func simpleParse(in string) (out []ops) {
|
||||||
|
items := strings.Split(in, ",")
|
||||||
|
for _, i := range items {
|
||||||
|
log.Println(i)
|
||||||
|
eq := strings.Split(i, "=")
|
||||||
|
switch len(eq) {
|
||||||
|
case 2:
|
||||||
|
out = append(out, ops{eq[0], "eq", eq[1]})
|
||||||
|
case 3:
|
||||||
|
if eq[1] == "" {
|
||||||
|
eq[1] = "eq"
|
||||||
|
}
|
||||||
|
out = append(out, ops{eq[0], eq[1], eq[2]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
109
mercury/spec_test.go
Normal file
109
mercury/spec_test.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package mercury_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/matryer/is"
|
||||||
|
"go.sour.is/pkg/mercury"
|
||||||
|
"go.sour.is/pkg/mercury/sql"
|
||||||
|
|
||||||
|
sq "github.com/Masterminds/squirrel"
|
||||||
|
)
|
||||||
|
|
||||||
|
var MAX_FILTER int = 40
|
||||||
|
|
||||||
|
func TestNamespaceParse(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
getWhere func(mercury.Search) sq.Sqlizer
|
||||||
|
in string
|
||||||
|
out string
|
||||||
|
args []any
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
getWhere: getWhere,
|
||||||
|
in: "d42.bgp.kapha.*|trace:d42.bgp.kapha",
|
||||||
|
out: "(column LIKE ? OR ? LIKE column || '%')",
|
||||||
|
args: []any{"d42.bgp.kapha.%", "d42.bgp.kapha"},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
getWhere: getWhere,
|
||||||
|
in: "d42.bgp.kapha.*|d42.bgp.kapha",
|
||||||
|
out: "(column LIKE ? OR column = ?)",
|
||||||
|
args: []any{"d42.bgp.kapha.%", "d42.bgp.kapha"},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
getWhere: mkWhere(t, sql.GetWhereSQ),
|
||||||
|
in: "d42.bgp.kapha.* find active=eq=true",
|
||||||
|
out: `SELECT * FROM spaces JOIN ( SELECT DISTINCT id FROM mercury_values mv, json_each(mv."values") vs WHERE (json_valid("values") AND name = ? AND vs.value = ?) ) r000 USING (id) WHERE (space LIKE ?)`,
|
||||||
|
args: []any{"active", "true", "d42.bgp.kapha.%"},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
getWhere: mkWhere(t, sql.GetWhereSQ),
|
||||||
|
in: "d42.bgp.kapha.* count 10 offset 5",
|
||||||
|
out: `SELECT * FROM spaces WHERE (space LIKE ?) LIMIT 10 OFFSET 5`,
|
||||||
|
args: []any{"d42.bgp.kapha.%"},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
getWhere: mkWhere(t, sql.GetWhereSQ),
|
||||||
|
in: "d42.bgp.kapha.* fields a,b,c",
|
||||||
|
out: `SELECT * FROM spaces WHERE (space LIKE ?)`,
|
||||||
|
args: []any{"d42.bgp.kapha.%"},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
getWhere: mkWhere(t, sql.GetWhereSQ),
|
||||||
|
in: "dn42.* find @type=in=[person,net]",
|
||||||
|
out: `SELECT `,
|
||||||
|
args: []any{"d42.bgp.kapha.%"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
//SELECT * FROM spaces JOIN ( SELECT DISTINCT id FROM mercury_values mv, json_valid("values") vs WHERE (json_valid("values") AND name = ? AND vs.value = ?) ) r000 USING (id) WHERE (space LIKE ?) !=
|
||||||
|
//SELECT * FROM spaces JOIN ( SELECT DISTINCT mv.id FROM mercury_values mv, json_each(mv."values") vs WHERE (json_valid("values") AND name = ? AND vs.value = ?) ) r000 USING (id) WHERE (space LIKE ?)
|
||||||
|
|
||||||
|
for i, tt := range tests {
|
||||||
|
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
out := mercury.ParseSearch(tt.in)
|
||||||
|
sql, args, err := tt.getWhere(out).ToSql()
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(sql, tt.out)
|
||||||
|
is.Equal(args, tt.args)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWhere(search mercury.Search) sq.Sqlizer {
|
||||||
|
var where sq.Or
|
||||||
|
space := "column"
|
||||||
|
for _, m := range search.NamespaceSearch {
|
||||||
|
switch m.(type) {
|
||||||
|
case mercury.NamespaceNode:
|
||||||
|
where = append(where, sq.Eq{space: m.Value()})
|
||||||
|
case mercury.NamespaceStar:
|
||||||
|
where = append(where, sq.Like{space: m.Value()})
|
||||||
|
case mercury.NamespaceTrace:
|
||||||
|
e := sq.Expr(`? LIKE `+space+` || '%'`, m.Value())
|
||||||
|
where = append(where, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return where
|
||||||
|
}
|
||||||
|
|
||||||
|
func mkWhere(t *testing.T, where func(search mercury.Search) (func(sq.SelectBuilder) sq.SelectBuilder, error)) func(search mercury.Search) sq.Sqlizer {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
return func(search mercury.Search) sq.Sqlizer {
|
||||||
|
w, err := where(search)
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
return w(sq.Select("*").From("spaces"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,9 +43,7 @@ func listScan(e *[]string, ends [2]rune) scanFn {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range splitComma(string(str)) {
|
*e = append(*e, splitComma(string(str))...)
|
||||||
*e = append(*e, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func (pgm *sqlHandler) GetNotify(ctx context.Context, event string) (lis mercury
|
|||||||
Where(squirrel.Eq{"event": event}).
|
Where(squirrel.Eq{"event": event}).
|
||||||
PlaceholderFormat(squirrel.Dollar).
|
PlaceholderFormat(squirrel.Dollar).
|
||||||
RunWith(pgm.db).
|
RunWith(pgm.db).
|
||||||
QueryContext(context.TODO())
|
QueryContext(ctx)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package sql
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"go.nhat.io/otelsql"
|
"go.nhat.io/otelsql"
|
||||||
semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
|
semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
|
||||||
@@ -9,12 +10,14 @@ import (
|
|||||||
|
|
||||||
func openDB(driver, dsn string) (*sql.DB, error) {
|
func openDB(driver, dsn string) (*sql.DB, error) {
|
||||||
system := semconv.DBSystemPostgreSQL
|
system := semconv.DBSystemPostgreSQL
|
||||||
if driver == "sqlite" {
|
if driver == "sqlite" || strings.HasPrefix(driver, "libsql") {
|
||||||
system = semconv.DBSystemSqlite
|
system = semconv.DBSystemSqlite
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if driver == "postgres" {
|
||||||
|
var err error
|
||||||
// Register the otelsql wrapper for the provided postgres driver.
|
// Register the otelsql wrapper for the provided postgres driver.
|
||||||
driverName, err := otelsql.Register(driver,
|
driver, err = otelsql.Register(driver,
|
||||||
otelsql.AllowRoot(),
|
otelsql.AllowRoot(),
|
||||||
otelsql.TraceQueryWithoutArgs(),
|
otelsql.TraceQueryWithoutArgs(),
|
||||||
otelsql.TraceRowsClose(),
|
otelsql.TraceRowsClose(),
|
||||||
@@ -25,9 +28,10 @@ func openDB(driver, dsn string) (*sql.DB, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Connect to a Postgres database using the postgres driver wrapper.
|
// Connect to a Postgres database using the postgres driver wrapper.
|
||||||
db, err := sql.Open(driverName, dsn)
|
db, err := sql.Open(driver, dsn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,21 +3,27 @@ package sql
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
sq "github.com/Masterminds/squirrel"
|
sq "github.com/Masterminds/squirrel"
|
||||||
"go.sour.is/pkg/lg"
|
"go.sour.is/pkg/lg"
|
||||||
"go.sour.is/pkg/mercury"
|
"go.sour.is/pkg/mercury"
|
||||||
"go.sour.is/pkg/rsql"
|
|
||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var MAX_FILTER int = 40
|
||||||
|
|
||||||
type sqlHandler struct {
|
type sqlHandler struct {
|
||||||
|
name string
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
paceholderFormat sq.PlaceholderFormat
|
paceholderFormat sq.PlaceholderFormat
|
||||||
listFormat [2]rune
|
listFormat [2]rune
|
||||||
|
readonly bool
|
||||||
|
getWhere func(search mercury.Search) (func(sq.SelectBuilder) sq.SelectBuilder, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -27,11 +33,13 @@ var (
|
|||||||
_ mercury.WriteConfig = (*sqlHandler)(nil)
|
_ mercury.WriteConfig = (*sqlHandler)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
func Register() {
|
func Register() func(context.Context) error {
|
||||||
|
var hdlrs []*sqlHandler
|
||||||
mercury.Registry.Register("sql", func(s *mercury.Space) any {
|
mercury.Registry.Register("sql", func(s *mercury.Space) any {
|
||||||
var dsn string
|
var dsn string
|
||||||
var opts strings.Builder
|
var opts strings.Builder
|
||||||
var dbtype string
|
var dbtype string
|
||||||
|
var readonly bool = slices.Contains(s.Tags, "readonly")
|
||||||
for _, c := range s.List {
|
for _, c := range s.List {
|
||||||
if c.Name == "match" {
|
if c.Name == "match" {
|
||||||
continue
|
continue
|
||||||
@@ -49,7 +57,6 @@ func Register() {
|
|||||||
if dsn == "" {
|
if dsn == "" {
|
||||||
dsn = opts.String()
|
dsn = opts.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := openDB(dbtype, dsn)
|
db, err := openDB(dbtype, dsn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -58,31 +65,47 @@ func Register() {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
switch dbtype {
|
switch dbtype {
|
||||||
case "sqlite":
|
case "sqlite", "libsql", "libsql+embed":
|
||||||
return &sqlHandler{db, sq.Dollar, [2]rune{'[', ']'}}
|
h := &sqlHandler{s.Space, db, sq.Question, [2]rune{'[', ']'}, readonly, GetWhereSQ}
|
||||||
|
hdlrs = append(hdlrs, h)
|
||||||
|
return h
|
||||||
case "postgres":
|
case "postgres":
|
||||||
return &sqlHandler{db, sq.Dollar, [2]rune{'{', '}'}}
|
h := &sqlHandler{s.Space, db, sq.Dollar, [2]rune{'{', '}'}, readonly, GetWherePG}
|
||||||
|
hdlrs = append(hdlrs, h)
|
||||||
|
return h
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported dbtype: %s", dbtype)
|
return fmt.Errorf("unsupported dbtype: %s", dbtype)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
var errs error
|
||||||
|
|
||||||
|
for _, h := range hdlrs {
|
||||||
|
// if err = ctx.Err(); err != nil {
|
||||||
|
// return errors.Join(errs, err)
|
||||||
|
// }
|
||||||
|
errs = errors.Join(errs, h.db.Close())
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Space struct {
|
type Space struct {
|
||||||
mercury.Space
|
mercury.Space
|
||||||
ID uint64
|
id uint64
|
||||||
}
|
}
|
||||||
type Value struct {
|
type Value struct {
|
||||||
mercury.Value
|
mercury.Value
|
||||||
ID uint64
|
id uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *sqlHandler) GetIndex(ctx context.Context, search mercury.NamespaceSearch, pgm *rsql.Program) (mercury.Config, error) {
|
func (p *sqlHandler) GetIndex(ctx context.Context, search mercury.Search) (mercury.Config, error) {
|
||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
cols := rsql.GetDbColumns(mercury.Space{})
|
where, err := p.getWhere(search)
|
||||||
where, err := getWhere(search, cols)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -100,28 +123,40 @@ func (p *sqlHandler) GetIndex(ctx context.Context, search mercury.NamespaceSearc
|
|||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *sqlHandler) GetConfig(ctx context.Context, search mercury.NamespaceSearch, pgm *rsql.Program, fields []string) (mercury.Config, error) {
|
func (p *sqlHandler) GetConfig(ctx context.Context, search mercury.Search) (config mercury.Config, err error) {
|
||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
idx, err := p.GetIndex(ctx, search, pgm)
|
where, err := p.getWhere(search)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
spaceMap := make(map[string]int, len(idx))
|
lis, err := p.listSpace(ctx, nil, where)
|
||||||
for u, s := range idx {
|
if err != nil {
|
||||||
spaceMap[s.Space] = u
|
log.Println(err)
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
where, err := getWhere(search, rsql.GetDbColumns(mercury.Value{}))
|
if len(lis) == 0 {
|
||||||
if err != nil {
|
return nil, nil
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
query := sq.Select(`"space"`, `"name"`, `"seq"`, `"notes"`, `"tags"`, `"values"`).
|
|
||||||
From("mercury_registry_vw").
|
spaceIDX := make([]uint64, len(lis))
|
||||||
Where(where).
|
spaceMap := make(map[uint64]int, len(lis))
|
||||||
OrderBy("space asc", "name asc").
|
config = make(mercury.Config, len(lis))
|
||||||
|
for i, s := range lis {
|
||||||
|
spaceIDX[i] = s.id
|
||||||
|
config[i] = &s.Space
|
||||||
|
spaceMap[s.id] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
query := sq.Select(`"id"`, `"name"`, `"seq"`, `"notes"`, `"tags"`, `"values"`).
|
||||||
|
From("mercury_values").
|
||||||
|
Where(sq.Eq{"id": spaceIDX}).
|
||||||
|
OrderBy("id asc", "seq asc").
|
||||||
PlaceholderFormat(p.paceholderFormat)
|
PlaceholderFormat(p.paceholderFormat)
|
||||||
|
|
||||||
|
span.AddEvent(p.name)
|
||||||
span.AddEvent(lg.LogQuery(query.ToSql()))
|
span.AddEvent(lg.LogQuery(query.ToSql()))
|
||||||
rows, err := query.RunWith(p.db).
|
rows, err := query.RunWith(p.db).
|
||||||
QueryContext(ctx)
|
QueryContext(ctx)
|
||||||
@@ -133,10 +168,10 @@ func (p *sqlHandler) GetConfig(ctx context.Context, search mercury.NamespaceSear
|
|||||||
|
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var s mercury.Value
|
var s Value
|
||||||
|
|
||||||
err = rows.Scan(
|
err = rows.Scan(
|
||||||
&s.Space,
|
&s.id,
|
||||||
&s.Name,
|
&s.Name,
|
||||||
&s.Seq,
|
&s.Seq,
|
||||||
listScan(&s.Notes, p.listFormat),
|
listScan(&s.Notes, p.listFormat),
|
||||||
@@ -146,19 +181,20 @@ func (p *sqlHandler) GetConfig(ctx context.Context, search mercury.NamespaceSear
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if u, ok := spaceMap[s.Space]; ok {
|
if u, ok := spaceMap[s.id]; ok {
|
||||||
idx[u].List = append(idx[u].List, s)
|
lis[u].List = append(lis[u].List, s.Value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = rows.Err()
|
err = rows.Err()
|
||||||
span.RecordError(err)
|
span.RecordError(err)
|
||||||
|
|
||||||
span.AddEvent(fmt.Sprint("read index ", len(idx)))
|
span.AddEvent(fmt.Sprint("read index ", len(lis)))
|
||||||
return idx, err
|
// log.Println(config.String())
|
||||||
|
return config, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *sqlHandler) listSpace(ctx context.Context, tx sq.BaseRunner, where sq.Sqlizer) ([]*Space, error) {
|
func (p *sqlHandler) listSpace(ctx context.Context, tx sq.BaseRunner, where func(sq.SelectBuilder) sq.SelectBuilder) ([]*Space, error) {
|
||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
@@ -168,9 +204,11 @@ func (p *sqlHandler) listSpace(ctx context.Context, tx sq.BaseRunner, where sq.S
|
|||||||
|
|
||||||
query := sq.Select(`"id"`, `"space"`, `"notes"`, `"tags"`, `"trailer"`).
|
query := sq.Select(`"id"`, `"space"`, `"notes"`, `"tags"`, `"trailer"`).
|
||||||
From("mercury_spaces").
|
From("mercury_spaces").
|
||||||
Where(where).
|
|
||||||
OrderBy("space asc").
|
OrderBy("space asc").
|
||||||
PlaceholderFormat(sq.Dollar)
|
PlaceholderFormat(p.paceholderFormat)
|
||||||
|
query = where(query)
|
||||||
|
|
||||||
|
span.AddEvent(p.name)
|
||||||
span.AddEvent(lg.LogQuery(query.ToSql()))
|
span.AddEvent(lg.LogQuery(query.ToSql()))
|
||||||
rows, err := query.RunWith(tx).
|
rows, err := query.RunWith(tx).
|
||||||
QueryContext(ctx)
|
QueryContext(ctx)
|
||||||
@@ -185,7 +223,7 @@ func (p *sqlHandler) listSpace(ctx context.Context, tx sq.BaseRunner, where sq.S
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var s Space
|
var s Space
|
||||||
err = rows.Scan(
|
err = rows.Scan(
|
||||||
&s.ID,
|
&s.id,
|
||||||
&s.Space.Space,
|
&s.Space.Space,
|
||||||
listScan(&s.Space.Notes, p.listFormat),
|
listScan(&s.Space.Notes, p.listFormat),
|
||||||
listScan(&s.Space.Tags, p.listFormat),
|
listScan(&s.Space.Tags, p.listFormat),
|
||||||
@@ -209,6 +247,10 @@ func (p *sqlHandler) WriteConfig(ctx context.Context, config mercury.Config) (er
|
|||||||
ctx, span := lg.Span(ctx)
|
ctx, span := lg.Span(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
|
if p.readonly {
|
||||||
|
return fmt.Errorf("readonly database")
|
||||||
|
}
|
||||||
|
|
||||||
// Delete spaces that are present in input but are empty.
|
// Delete spaces that are present in input but are empty.
|
||||||
deleteSpaces := make(map[string]struct{})
|
deleteSpaces := make(map[string]struct{})
|
||||||
|
|
||||||
@@ -233,7 +275,8 @@ func (p *sqlHandler) WriteConfig(ctx context.Context, config mercury.Config) (er
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// get current spaces
|
// get current spaces
|
||||||
lis, err := p.listSpace(ctx, tx, sq.Eq{"space": maps.Keys(names)})
|
where := func(qry sq.SelectBuilder) sq.SelectBuilder { return qry.Where(sq.Eq{"space": maps.Keys(names)}) }
|
||||||
|
lis, err := p.listSpace(ctx, tx, where)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -250,12 +293,12 @@ func (p *sqlHandler) WriteConfig(ctx context.Context, config mercury.Config) (er
|
|||||||
currentNames[spaceName] = struct{}{}
|
currentNames[spaceName] = struct{}{}
|
||||||
|
|
||||||
if _, ok := deleteSpaces[spaceName]; ok {
|
if _, ok := deleteSpaces[spaceName]; ok {
|
||||||
deleteIDs = append(deleteIDs, s.ID)
|
deleteIDs = append(deleteIDs, s.id)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSpaces = append(updateSpaces, config[names[spaceName]])
|
updateSpaces = append(updateSpaces, config[names[spaceName]])
|
||||||
updateIDs = append(updateIDs, s.ID)
|
updateIDs = append(updateIDs, s.id)
|
||||||
}
|
}
|
||||||
for _, s := range config {
|
for _, s := range config {
|
||||||
spaceName := s.Space
|
spaceName := s.Space
|
||||||
@@ -266,7 +309,7 @@ func (p *sqlHandler) WriteConfig(ctx context.Context, config mercury.Config) (er
|
|||||||
|
|
||||||
// delete spaces
|
// delete spaces
|
||||||
if ids := deleteIDs; len(ids) > 0 {
|
if ids := deleteIDs; len(ids) > 0 {
|
||||||
_, err = sq.Delete("mercury_spaces").Where(sq.Eq{"id": ids}).RunWith(tx).PlaceholderFormat(sq.Dollar).ExecContext(ctx)
|
_, err = sq.Delete("mercury_spaces").Where(sq.Eq{"id": ids}).RunWith(tx).PlaceholderFormat(p.paceholderFormat).ExecContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -274,7 +317,7 @@ func (p *sqlHandler) WriteConfig(ctx context.Context, config mercury.Config) (er
|
|||||||
|
|
||||||
// delete values
|
// delete values
|
||||||
if ids := append(updateIDs, deleteIDs...); len(ids) > 0 {
|
if ids := append(updateIDs, deleteIDs...); len(ids) > 0 {
|
||||||
_, err = sq.Delete("mercury_values").Where(sq.Eq{"id": ids}).RunWith(tx).PlaceholderFormat(sq.Dollar).ExecContext(ctx)
|
_, err = sq.Delete("mercury_values").Where(sq.Eq{"id": ids}).RunWith(tx).PlaceholderFormat(p.paceholderFormat).ExecContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -289,7 +332,8 @@ func (p *sqlHandler) WriteConfig(ctx context.Context, config mercury.Config) (er
|
|||||||
Set("tags", listValue(u.Tags, p.listFormat)).
|
Set("tags", listValue(u.Tags, p.listFormat)).
|
||||||
Set("notes", listValue(u.Notes, p.listFormat)).
|
Set("notes", listValue(u.Notes, p.listFormat)).
|
||||||
Set("trailer", listValue(u.Trailer, p.listFormat)).
|
Set("trailer", listValue(u.Trailer, p.listFormat)).
|
||||||
PlaceholderFormat(sq.Dollar)
|
PlaceholderFormat(p.paceholderFormat)
|
||||||
|
span.AddEvent(p.name)
|
||||||
span.AddEvent(lg.LogQuery(query.ToSql()))
|
span.AddEvent(lg.LogQuery(query.ToSql()))
|
||||||
_, err := query.RunWith(tx).ExecContext(ctx)
|
_, err := query.RunWith(tx).ExecContext(ctx)
|
||||||
|
|
||||||
@@ -298,7 +342,7 @@ func (p *sqlHandler) WriteConfig(ctx context.Context, config mercury.Config) (er
|
|||||||
}
|
}
|
||||||
// log.Debugf("UPDATED %d SPACES", len(updateSpaces))
|
// log.Debugf("UPDATED %d SPACES", len(updateSpaces))
|
||||||
for _, v := range u.List {
|
for _, v := range u.List {
|
||||||
newValues = append(newValues, &Value{Value: v, ID: updateIDs[i]})
|
newValues = append(newValues, &Value{Value: v, id: updateIDs[i]})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,7 +350,7 @@ func (p *sqlHandler) WriteConfig(ctx context.Context, config mercury.Config) (er
|
|||||||
for _, s := range insertSpaces {
|
for _, s := range insertSpaces {
|
||||||
var id uint64
|
var id uint64
|
||||||
query := sq.Insert("mercury_spaces").
|
query := sq.Insert("mercury_spaces").
|
||||||
PlaceholderFormat(sq.Dollar).
|
PlaceholderFormat(p.paceholderFormat).
|
||||||
Columns("space", "tags", "notes", "trailer").
|
Columns("space", "tags", "notes", "trailer").
|
||||||
Values(
|
Values(
|
||||||
s.Space,
|
s.Space,
|
||||||
@@ -315,6 +359,7 @@ func (p *sqlHandler) WriteConfig(ctx context.Context, config mercury.Config) (er
|
|||||||
listValue(s.Trailer, p.listFormat),
|
listValue(s.Trailer, p.listFormat),
|
||||||
).
|
).
|
||||||
Suffix("RETURNING \"id\"")
|
Suffix("RETURNING \"id\"")
|
||||||
|
span.AddEvent(p.name)
|
||||||
span.AddEvent(lg.LogQuery(query.ToSql()))
|
span.AddEvent(lg.LogQuery(query.ToSql()))
|
||||||
|
|
||||||
err := query.
|
err := query.
|
||||||
@@ -322,12 +367,13 @@ func (p *sqlHandler) WriteConfig(ctx context.Context, config mercury.Config) (er
|
|||||||
QueryRowContext(ctx).
|
QueryRowContext(ctx).
|
||||||
Scan(&id)
|
Scan(&id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
span.AddEvent(p.name)
|
||||||
s, v, _ := query.ToSql()
|
s, v, _ := query.ToSql()
|
||||||
log.Println(s, v, err)
|
log.Println(s, v, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, v := range s.List {
|
for _, v := range s.List {
|
||||||
newValues = append(newValues, &Value{Value: v, ID: id})
|
newValues = append(newValues, &Value{Value: v, id: id})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,7 +399,7 @@ func (p *sqlHandler) writeValues(ctx context.Context, tx sq.BaseRunner, lis []*V
|
|||||||
newInsert := func() sq.InsertBuilder {
|
newInsert := func() sq.InsertBuilder {
|
||||||
return sq.Insert("mercury_values").
|
return sq.Insert("mercury_values").
|
||||||
RunWith(tx).
|
RunWith(tx).
|
||||||
PlaceholderFormat(sq.Dollar).
|
PlaceholderFormat(p.paceholderFormat).
|
||||||
Columns(
|
Columns(
|
||||||
`"id"`,
|
`"id"`,
|
||||||
`"seq"`,
|
`"seq"`,
|
||||||
@@ -367,7 +413,7 @@ func (p *sqlHandler) writeValues(ctx context.Context, tx sq.BaseRunner, lis []*V
|
|||||||
insert := newInsert()
|
insert := newInsert()
|
||||||
for i, s := range lis {
|
for i, s := range lis {
|
||||||
insert = insert.Values(
|
insert = insert.Values(
|
||||||
s.ID,
|
s.id,
|
||||||
s.Seq,
|
s.Seq,
|
||||||
s.Name,
|
s.Name,
|
||||||
listValue(s.Values, p.listFormat),
|
listValue(s.Values, p.listFormat),
|
||||||
@@ -378,7 +424,7 @@ func (p *sqlHandler) writeValues(ctx context.Context, tx sq.BaseRunner, lis []*V
|
|||||||
|
|
||||||
if i > 0 && i%chunk == 0 {
|
if i > 0 && i%chunk == 0 {
|
||||||
// log.Debugf("inserting %v rows into %v", i%chunk, d.Table)
|
// log.Debugf("inserting %v rows into %v", i%chunk, d.Table)
|
||||||
// log.Debug(insert.ToSql())
|
span.AddEvent(p.name)
|
||||||
span.AddEvent(lg.LogQuery(insert.ToSql()))
|
span.AddEvent(lg.LogQuery(insert.ToSql()))
|
||||||
|
|
||||||
_, err = insert.ExecContext(ctx)
|
_, err = insert.ExecContext(ctx)
|
||||||
@@ -392,7 +438,7 @@ func (p *sqlHandler) writeValues(ctx context.Context, tx sq.BaseRunner, lis []*V
|
|||||||
}
|
}
|
||||||
if len(lis)%chunk > 0 {
|
if len(lis)%chunk > 0 {
|
||||||
// log.Debugf("inserting %v rows into %v", len(lis)%chunk, d.Table)
|
// log.Debugf("inserting %v rows into %v", len(lis)%chunk, d.Table)
|
||||||
// log.Debug(insert.ToSql())
|
span.AddEvent(p.name)
|
||||||
span.AddEvent(lg.LogQuery(insert.ToSql()))
|
span.AddEvent(lg.LogQuery(insert.ToSql()))
|
||||||
|
|
||||||
_, err = insert.ExecContext(ctx)
|
_, err = insert.ExecContext(ctx)
|
||||||
@@ -405,13 +451,11 @@ func (p *sqlHandler) writeValues(ctx context.Context, tx sq.BaseRunner, lis []*V
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func getWhere(search mercury.NamespaceSearch, d *rsql.DbColumns) (sq.Sqlizer, error) {
|
func GetWherePG(search mercury.Search) (func(sq.SelectBuilder) sq.SelectBuilder, error) {
|
||||||
var where sq.Or
|
var where sq.Or
|
||||||
space, err := d.Col("space")
|
space := "space"
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
for _, m := range search.NamespaceSearch {
|
||||||
}
|
|
||||||
for _, m := range search {
|
|
||||||
switch m.(type) {
|
switch m.(type) {
|
||||||
case mercury.NamespaceNode:
|
case mercury.NamespaceNode:
|
||||||
where = append(where, sq.Eq{space: m.Value()})
|
where = append(where, sq.Eq{space: m.Value()})
|
||||||
@@ -422,5 +466,129 @@ func getWhere(search mercury.NamespaceSearch, d *rsql.DbColumns) (sq.Sqlizer, er
|
|||||||
where = append(where, e)
|
where = append(where, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return where, nil
|
|
||||||
|
var joins []sq.SelectBuilder
|
||||||
|
for i, o := range search.Find {
|
||||||
|
log.Println(o)
|
||||||
|
if i > MAX_FILTER {
|
||||||
|
err := fmt.Errorf("too many filters [%d]", MAX_FILTER)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
q := sq.Select("DISTINCT id").From("mercury_values")
|
||||||
|
|
||||||
|
switch o.Op {
|
||||||
|
case "key":
|
||||||
|
q = q.Where(sq.Eq{"name": o.Left})
|
||||||
|
case "nkey":
|
||||||
|
q = q.Where(sq.NotEq{"name": o.Left})
|
||||||
|
case "eq":
|
||||||
|
q = q.Where("name = ? AND ? = any (values)", o.Left, o.Right)
|
||||||
|
case "neq":
|
||||||
|
q = q.Where("name = ? AND ? != any (values)", o.Left, o.Right)
|
||||||
|
|
||||||
|
case "gt":
|
||||||
|
q = q.Where("name = ? AND ? > any (values)", o.Left, o.Right)
|
||||||
|
case "lt":
|
||||||
|
q = q.Where("name = ? AND ? < any (values)", o.Left, o.Right)
|
||||||
|
case "ge":
|
||||||
|
q = q.Where("name = ? AND ? >= any (values)", o.Left, o.Right)
|
||||||
|
case "le":
|
||||||
|
q = q.Where("name = ? AND ? <= any (values)", o.Left, o.Right)
|
||||||
|
|
||||||
|
// case "like":
|
||||||
|
// q = q.Where("name = ? AND value LIKE ?", o.Left, o.Right)
|
||||||
|
// case "in":
|
||||||
|
// q = q.Where(sq.Eq{"name": o.Left, "value": strings.Split(o.Right, " ")})
|
||||||
|
}
|
||||||
|
joins = append(joins, q)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(s sq.SelectBuilder) sq.SelectBuilder {
|
||||||
|
for i, q := range joins {
|
||||||
|
s = s.JoinClause(q.Prefix("JOIN (").Suffix(fmt.Sprintf(`) r%03d USING (id)`, i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if search.Count > 0 {
|
||||||
|
s = s.Limit(search.Count)
|
||||||
|
}
|
||||||
|
return s.Where(where)
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetWhereSQ(search mercury.Search) (func(sq.SelectBuilder) sq.SelectBuilder, error) {
|
||||||
|
var where sq.Or
|
||||||
|
|
||||||
|
var errs error
|
||||||
|
id := "id"
|
||||||
|
space := "space"
|
||||||
|
name := "name"
|
||||||
|
values_each := `json_valid("values")`
|
||||||
|
values_valid := `json_valid("values")`
|
||||||
|
|
||||||
|
if errs != nil {
|
||||||
|
return nil, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range search.NamespaceSearch {
|
||||||
|
switch m.(type) {
|
||||||
|
case mercury.NamespaceNode:
|
||||||
|
where = append(where, sq.Eq{space: m.Value()})
|
||||||
|
case mercury.NamespaceStar:
|
||||||
|
where = append(where, sq.Like{space: m.Value()})
|
||||||
|
case mercury.NamespaceTrace:
|
||||||
|
e := sq.Expr(`? LIKE `+space+` || '%'`, m.Value())
|
||||||
|
where = append(where, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var joins []sq.SelectBuilder
|
||||||
|
for i, o := range search.Find {
|
||||||
|
log.Println(o)
|
||||||
|
if i > MAX_FILTER {
|
||||||
|
err := fmt.Errorf("too many filters [%d]", MAX_FILTER)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
q := sq.Select("DISTINCT " + id).From(`mercury_values mv, ` + values_each + ` vs`)
|
||||||
|
|
||||||
|
switch o.Op {
|
||||||
|
case "key":
|
||||||
|
q = q.Where(sq.Eq{name: o.Left})
|
||||||
|
case "nkey":
|
||||||
|
q = q.Where(sq.NotEq{name: o.Left})
|
||||||
|
case "eq":
|
||||||
|
q = q.Where(sq.And{sq.Expr(values_valid), sq.Eq{name: o.Left, `vs.value`: o.Right}})
|
||||||
|
case "neq":
|
||||||
|
q = q.Where(sq.And{sq.Expr(values_valid), sq.Eq{name: o.Left}, sq.NotEq{`vs.value`: o.Right}})
|
||||||
|
|
||||||
|
case "gt":
|
||||||
|
q = q.Where(sq.And{sq.Expr(values_valid), sq.Eq{name: o.Left}, sq.Gt{`vs.value`: o.Right}})
|
||||||
|
case "lt":
|
||||||
|
q = q.Where(sq.And{sq.Expr(values_valid), sq.Eq{name: o.Left}, sq.Lt{`vs.value`: o.Right}})
|
||||||
|
case "ge":
|
||||||
|
q = q.Where(sq.And{sq.Expr(values_valid), sq.Eq{name: o.Left}, sq.GtOrEq{`vs.value`: o.Right}})
|
||||||
|
case "le":
|
||||||
|
q = q.Where(sq.And{sq.Expr(values_valid), sq.Eq{name: o.Left}, sq.LtOrEq{`vs.value`: o.Right}})
|
||||||
|
case "like":
|
||||||
|
q = q.Where(sq.And{sq.Expr(values_valid), sq.Eq{name: o.Left}, sq.Like{`vs.value`: o.Right}})
|
||||||
|
case "in":
|
||||||
|
q = q.Where(sq.Eq{name: o.Left, "vs.value": strings.Split(o.Right, " ")})
|
||||||
|
}
|
||||||
|
joins = append(joins, q)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(s sq.SelectBuilder) sq.SelectBuilder {
|
||||||
|
for i, q := range joins {
|
||||||
|
s = s.JoinClause(q.Prefix("JOIN (").Suffix(fmt.Sprintf(`) r%03d USING (id)`, i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if search.Count > 0 {
|
||||||
|
s = s.Limit(search.Count)
|
||||||
|
}
|
||||||
|
|
||||||
|
if search.Offset > 0 {
|
||||||
|
s = s.Offset(search.Offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Where(where)
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,16 +40,16 @@ func GetDbColumns(o interface{}) *DbColumns {
|
|||||||
for i := 0; i < t.NumField(); i++ {
|
for i := 0; i < t.NumField(); i++ {
|
||||||
field := t.Field(i)
|
field := t.Field(i)
|
||||||
|
|
||||||
sp := append(strings.Split(field.Tag.Get("db"), ","), "")
|
tag, _, _ := strings.Cut(field.Tag.Get("db"), ",")
|
||||||
|
|
||||||
tag := sp[0]
|
|
||||||
|
|
||||||
json := field.Tag.Get("json")
|
json := field.Tag.Get("json")
|
||||||
|
json, _, _ = strings.Cut(json, ",")
|
||||||
if tag == "" {
|
if tag == "" {
|
||||||
tag = json
|
tag = json
|
||||||
}
|
}
|
||||||
|
|
||||||
graphql := field.Tag.Get("graphql")
|
graphql := field.Tag.Get("graphql")
|
||||||
|
graphql, _, _ = strings.Cut(graphql, ",")
|
||||||
if tag == "" {
|
if tag == "" {
|
||||||
tag = graphql
|
tag = graphql
|
||||||
}
|
}
|
||||||
@@ -88,3 +88,11 @@ func GetDbColumns(o interface{}) *DbColumns {
|
|||||||
}
|
}
|
||||||
return &d
|
return &d
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func QuoteCols(cols []string) []string {
|
||||||
|
lis := make([]string, len(cols))
|
||||||
|
for i := range cols {
|
||||||
|
lis[i] = `"` + cols[i] + `"`
|
||||||
|
}
|
||||||
|
return lis
|
||||||
|
}
|
||||||
|
|||||||
@@ -114,7 +114,6 @@ func (s *Harness) Run(ctx context.Context, appName, version string) error {
|
|||||||
err := g.Wait()
|
err := g.Wait()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Shutdown due to error: %s", err)
|
log.Printf("Shutdown due to error: %s", err)
|
||||||
|
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,3 +160,35 @@ func Align[T any](k []T, v []T, less func(T, T) bool) []Pair[*T, *T] {
|
|||||||
return lis
|
return lis
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Heapify[T any](arr []T, n, i int, less func(T, T) bool) {
|
||||||
|
largest := i
|
||||||
|
l := 2*i + 1
|
||||||
|
r := 2*i + 2
|
||||||
|
|
||||||
|
if l < n && less(arr[largest], arr[l]) {
|
||||||
|
largest = l
|
||||||
|
}
|
||||||
|
if r < n && less(arr[largest], arr[r]) {
|
||||||
|
largest = r
|
||||||
|
}
|
||||||
|
if largest != i {
|
||||||
|
arr[i], arr[largest] = arr[largest], arr[i]
|
||||||
|
Heapify(arr, n, largest, less)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildMaxHeap[T any](arr []T, less func(T, T) bool) {
|
||||||
|
n := len(arr)
|
||||||
|
for i := (n / 2) - 1; i >= 0; i-- {
|
||||||
|
Heapify(arr, n, i, less)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HeapSort[T any](arr []T, less func(T, T) bool) {
|
||||||
|
BuildMaxHeap(arr, less)
|
||||||
|
for i := len(arr) - 1; i > 0; i-- {
|
||||||
|
arr[0], arr[i] = arr[i], arr[0]
|
||||||
|
Heapify(arr, i, 0, less)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,3 +51,21 @@ func TestAlign(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ptr[T any](v T) *T { return &v }
|
func ptr[T any](v T) *T { return &v }
|
||||||
|
|
||||||
|
func TestHeapSort(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
arr := []int{9, 4, 3, 8, 10, 2, 5}
|
||||||
|
slice.HeapSort(arr, func(l, r int) bool { return l < r })
|
||||||
|
|
||||||
|
is.Equal(arr, []int{2, 3, 4, 5, 8, 9, 10})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildHeap(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
arr := []int{1, 3, 5, 4, 6, 13, 10, 9, 8, 15, 17}
|
||||||
|
slice.BuildMaxHeap(arr, func(l, r int) bool { return l < r })
|
||||||
|
|
||||||
|
is.Equal(arr, []int{17, 15, 13, 9, 6, 5, 10, 4, 8, 3, 1})
|
||||||
|
}
|
||||||
|
|||||||
11
xdg/xdg.go
11
xdg/xdg.go
@@ -39,6 +39,17 @@ func setENV(name, value string) string {
|
|||||||
func Get(base, suffix string) string {
|
func Get(base, suffix string) string {
|
||||||
return strings.Join(paths(base, suffix), string(os.PathListSeparator))
|
return strings.Join(paths(base, suffix), string(os.PathListSeparator))
|
||||||
}
|
}
|
||||||
|
func GetRoot(base, suffix string, perm os.FileMode) (*os.Root, error) {
|
||||||
|
fs, err := os.OpenRoot(base)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = fs.Mkdir(Get(base, suffix), perm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return os.OpenRoot(Get(base, suffix))
|
||||||
|
}
|
||||||
func paths(base, suffix string) []string {
|
func paths(base, suffix string) []string {
|
||||||
paths := strings.Split(os.ExpandEnv(base), string(os.PathListSeparator))
|
paths := strings.Split(os.ExpandEnv(base), string(os.PathListSeparator))
|
||||||
for i, path := range paths {
|
for i, path := range paths {
|
||||||
|
|||||||
Reference in New Issue
Block a user