diff --git a/go.mod b/go.mod index 914fba66..bf75edd9 100755 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/testcontainers/testcontainers-go v0.42.0 github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 - github.com/yaacov/tree-search-language v0.0.0-20190923184055-1c2dad2e354b + github.com/yaacov/tree-search-language/v6 v6.0.10 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 @@ -59,7 +59,6 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/antlr/antlr4 v0.0.0-20190518164840-edae2a1c9b4b // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect diff --git a/go.sum b/go.sum index 60109ac4..d2eae4c6 100755 --- a/go.sum +++ b/go.sum @@ -1,7 +1,3 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= -cloud.google.com/go v0.39.0/go.mod h1:rVLT6fkc8chs9sfPtFc1SBH6em7n+ZoXaG+87tDISts= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= @@ -10,11 +6,8 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8af github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= -github.com/Masterminds/squirrel v1.1.0/go.mod h1:yaPeOnPG5ZRwL9oKdTsO/prlkPbXWZlRVMQ/gGlzIuA= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/MicahParks/jwkset v0.11.0 h1:yc0zG+jCvZpWgFDFmvs8/8jqqVBG9oyIbmBtmjOhoyQ= @@ -24,16 +17,8 @@ github.com/MicahParks/keyfunc/v3 v3.8.0/go.mod h1:z66bkCviwqfg2YUp+Jcc/xRE9IXLcM github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= -github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/antlr/antlr4 v0.0.0-20190518164840-edae2a1c9b4b h1:IyTcB1l64U991qSZ0ufqiJv9GVEOUBiSPwsObDm7+cc= -github.com/antlr/antlr4 v0.0.0-20190518164840-edae2a1c9b4b/go.mod h1:T7PbCXFs94rrTttyxjbyT5+/1V8T2TYDejxUfHJjw1Y= -github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= @@ -45,7 +30,6 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= @@ -62,26 +46,18 @@ github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfv github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/denisenkom/go-mssqldb v0.0.0-20190423183735-731ef375ac02/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= -github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= @@ -90,8 +66,6 @@ github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= github.com/go-gormigrate/gormigrate/v2 v2.1.6 h1:VtX+l1Stj2v5RGubVQk0LS/8EPGXR+ldcOyCmlmKoyg= github.com/go-gormigrate/gormigrate/v2 v2.1.6/go.mod h1:PZpedQc4tWaxn6kvXicwhinh3L0seLpMc5ReKRX5id4= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -111,61 +85,38 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8= github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc= -github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -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.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= -github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/graphql-go/graphql v0.7.8/go.mod h1:k6yrAYQaSP59DC5UVxbgxESlmVyojThKdORUqGDGmrI= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hokaccha/go-prettyjson v0.0.0-20180920040306-f579f869bbfe/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -176,30 +127,18 @@ github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jinzhu/gorm v1.9.8/go.mod h1:bdqTT3q6dhSph2K3pWxrHP6nqxuAp2yQ3KFtc3U3F84= -github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.0.0/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= @@ -208,8 +147,6 @@ github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhR github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= @@ -218,12 +155,8 @@ github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8S github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= @@ -252,7 +185,6 @@ github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs= github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= github.com/oapi-codegen/runtime v1.4.2 h1:GMxFVYLzoYLua+/KvzgSphkyK1lLTReQI9Vf4hvATKE= @@ -261,49 +193,32 @@ github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//J github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/ginkgo/v2 v2.22.1 h1:QW7tbJAUDyVDVOM5dFa7qaybo+CRfR7bemlQUN6Z8aM= +github.com/onsi/ginkgo/v2 v2.22.1/go.mod h1:S6aTpoRsSq2cZOd+pssHAlKW/Q/jZt6cPrPlnj4a1xM= github.com/onsi/gomega v1.42.0 h1:CJby8u36xb7v34W78F8WKvqTQP7PCMIPB78IVDB73l4= github.com/onsi/gomega v1.42.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/openshift-hyperfleet/hyperfleet-api-spec v1.0.21 h1:+Vlr1GOldX+pWJdVVlHxiCuVRQjTlr1Q3ez+ON+To/Q= -github.com/openshift-hyperfleet/hyperfleet-api-spec v1.0.21/go.mod h1:KITzIAd8HcMpH5lXdHFjgk45dvL6XLpP3wwz8iK+KCI= github.com/openshift-hyperfleet/hyperfleet-api-spec v1.0.23 h1:HyOWpJfEonjpLFnMmmhiqaAD3EJSVIvAuaA7y5fZ4kE= github.com/openshift-hyperfleet/hyperfleet-api-spec v1.0.23/go.mod h1:KITzIAd8HcMpH5lXdHFjgk45dvL6XLpP3wwz8iK+KCI= -github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= -github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -311,7 +226,6 @@ github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDc github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= @@ -329,8 +243,6 @@ github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -344,7 +256,6 @@ github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44Xt github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 h1:GCbb1ndrF7OTDiIvxXyItaDab4qkzTFJ48LKFdM7EIo= github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0/go.mod h1:IRPBaI8jXdrNfD0e4Zm7Fbcgaz5shKxOQv4axiL09xs= -github.com/tidwall/pretty v0.0.0-20190325153808-1166b9ac2b65/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= @@ -353,16 +264,10 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= -github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= -github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= -github.com/yaacov/tree-search-language v0.0.0-20190923184055-1c2dad2e354b h1:aWR0+NlUGQpFPxpjcYW7oXsN1GnYUVIdB5Act7I6jzc= -github.com/yaacov/tree-search-language v0.0.0-20190923184055-1c2dad2e354b/go.mod h1:uXZEzDS1siuQsBuHL1A4gy27xIsnnL06MhqrwvySsIk= +github.com/yaacov/tree-search-language/v6 v6.0.10 h1:DOI0agE6VmTKLq03guXnGrWRxnGYb0KxN9mhWyCBVD0= +github.com/yaacov/tree-search-language/v6 v6.0.10/go.mod h1:ZXSpcgyyUZswjeC/OOYBnrvUWhhENumaV4+Xeg59NFk= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.mongodb.org/mongo-driver v1.0.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= -go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= @@ -405,62 +310,14 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190509164839-32b2708ab171/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -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.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/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.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -468,63 +325,28 @@ golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -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-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190601110225-0abef6e9ecb8/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= -google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -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.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190508193815-b515fa19cec8/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -543,9 +365,5 @@ gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190531162725-42df64e2171a/go.mod h1:wtc9q0E9zm8PjdRMh29DPlTlCCHVzKDwnkT4GskQVzg= pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/pkg/db/sql_helpers.go b/pkg/db/sql_helpers.go index a559b55b..b1e0bdd4 100755 --- a/pkg/db/sql_helpers.go +++ b/pkg/db/sql_helpers.go @@ -5,13 +5,14 @@ import ( "math" "reflect" "regexp" + "slices" "strings" "time" sq "github.com/Masterminds/squirrel" "github.com/jinzhu/inflection" "github.com/openshift-hyperfleet/hyperfleet-api/pkg/errors" - "github.com/yaacov/tree-search-language/pkg/tsl" + "github.com/yaacov/tree-search-language/v6/pkg/tsl" "gorm.io/gorm" ) @@ -39,28 +40,6 @@ func validateLabelKey(key string) *errors.ServiceError { return validateFieldKey(key, "label key") } -// Check if a field name starts with properties. -func startsWithProperties(s string) bool { - return strings.HasPrefix(s, "properties.") -} - -// hasProperty return true if node has a property identifier on left hand side. -func hasProperty(n tsl.Node) bool { - // Get the left side operator. - l, ok := n.Left.(tsl.Node) - if !ok { - return false - } - - // If left side hand is not a `properties` identifier, return. - leftStr, ok := l.Left.(string) - if !ok || l.Func != tsl.IdentOp || !startsWithProperties(leftStr) { - return false - } - - return true -} - // Field mapping rules for user-friendly syntax to database columns var statusFieldMappings = map[string]string{ "status.conditions": "status_conditions", @@ -68,28 +47,34 @@ var statusFieldMappings = map[string]string{ // getField gets the sql field associated with a name. func getField(name string, disallowedFields map[string]string) (field string, err *errors.ServiceError) { - // We want to accept names with trailing and leading spaces trimmedName := strings.Trim(name, " ") - // Check for properties ->> '' - if strings.HasPrefix(trimmedName, "properties ->>") { - field = trimmedName + if strings.HasPrefix(trimmedName, "properties.") { + if _, disallowed := disallowedFields["properties"]; disallowed { + err = errors.BadRequest("%s is not a valid field name", name) + return + } + key := strings.TrimPrefix(trimmedName, "properties.") + if validationErr := validateFieldKey(key, "property key"); validationErr != nil { + err = validationErr + return + } + field = fmt.Sprintf("properties ->> '%s'", key) return } // Map user-friendly spec.xxx (and nested spec.xxx.yyy...) syntax to JSONB query. - // Paths are pre-encoded by PreprocessSpecSubfields: dots beyond the first key - // segment are replaced with __ so the TSL parser sees at most 2 segments. + // v6 gives us the full dotted path directly: // spec.region → spec->>'region' - // spec.release__channel → spec->'release'->>'channel' - // spec.a__b__c → spec->'a'->'b'->>'c' + // spec.release.channel → spec->'release'->>'channel' + // spec.a.b.c → spec->'a'->'b'->>'c' if strings.HasPrefix(trimmedName, "spec.") { if _, disallowed := disallowedFields["spec"]; disallowed { err = errors.BadRequest("%s is not a valid field name", name) return } - parts := strings.Split(strings.TrimPrefix(trimmedName, "spec."), "__") + parts := strings.Split(strings.TrimPrefix(trimmedName, "spec."), ".") for _, part := range parts { if validationErr := validateFieldKey(part, "spec field segment"); validationErr != nil { err = validationErr @@ -112,7 +97,6 @@ func getField(name string, disallowedFields map[string]string) (field string, er if strings.HasPrefix(trimmedName, "labels.") { key := strings.TrimPrefix(trimmedName, "labels.") - // Validate label key to prevent SQL injection if validationErr := validateLabelKey(key); validationErr != nil { err = validationErr return @@ -148,42 +132,6 @@ func getField(name string, disallowedFields map[string]string) (field string, er return } -// propertiesNodeConverter converts a node with a properties identifier -// to a properties node. -// -// For example, it will convert: -// ( properties. = ) to -// ( properties ->> = ) -func propertiesNodeConverter(n tsl.Node) tsl.Node { - - // Get the left side operator. - l, ok := n.Left.(tsl.Node) - if !ok { - return n - } - - // Get the property name. - leftStr, ok := l.Left.(string) - if !ok || len(leftStr) <= 11 { - return n - } - propertyName := leftStr[11:] - - // Build a new node that converts: - // ( properties. = ) to - // ( properties ->> = ) - propertyNode := tsl.Node{ - Func: n.Func, - Left: tsl.Node{ - Func: tsl.IdentOp, - Left: fmt.Sprintf("properties ->> '%s'", propertyName), - }, - Right: n.Right, - } - - return propertyNode -} - // Condition type validation pattern: PascalCase condition types (e.g., Reconciled, Available, Progressing) var conditionTypePattern = regexp.MustCompile(`^[A-Z][a-zA-Z0-9]*$`) @@ -194,128 +142,6 @@ var validConditionStatuses = map[string]bool{ "Unknown": true, } -// specDeepPathPattern matches spec paths with 2 or more key segments after "spec." -// (e.g. spec.release.channel, spec.a.b.c) so they can be encoded before TSL parsing. -// The TSL parser supports at most 3-part identifiers (database.table.column), so -// spec.release.channel (3 parts) is at the limit and spec.a.b.c (4 parts) would fail. -// PreprocessSpecSubfields collapses dots beyond the first key segment into __ so the -// TSL parser always sees exactly 2 parts (spec and the encoded key path). -var specDeepPathPattern = regexp.MustCompile( - `(^|[\s(])(spec\.[a-z0-9_]+(?:\.[a-z0-9_]+)+)`, -) - -// PreprocessSpecSubfields rewrites spec paths with 2+ key segments into 2-part paths -// before TSL parsing, by replacing dots beyond the first key segment with __. -// Only replaces in unquoted segments to avoid mutating string literals. -// -// Examples: -// -// spec.release.channel → spec.release__channel -// spec.a.b.c → spec.a__b__c -// spec.region (1 segment) → unchanged -func PreprocessSpecSubfields(search string) string { - var result strings.Builder - result.Grow(len(search)) - inQuote := false - quoteChar := byte(0) - segStart := 0 - - encode := func(s string) string { - return specDeepPathPattern.ReplaceAllStringFunc(s, func(match string) string { - idx := strings.Index(match, "spec.") - if idx < 0 { - return match - } - boundary := match[:idx] - // SplitN on "." with n=3 gives ["spec", "firstKey", "rest..."] - parts := strings.SplitN(match[idx:], ".", 3) - if len(parts) < 3 { - return match - } - // encode remaining dots in the tail as __ - return boundary + parts[0] + "." + parts[1] + "__" + strings.ReplaceAll(parts[2], ".", "__") - }) - } - - for i := 0; i < len(search); i++ { - ch := search[i] - if inQuote { - if ch == quoteChar { - result.WriteString(search[segStart : i+1]) - segStart = i + 1 - inQuote = false - } - continue - } - if ch == '\'' || ch == '"' { - result.WriteString(encode(search[segStart:i])) - segStart = i - inQuote = true - quoteChar = ch - } - } - if inQuote { - result.WriteString(search[segStart:]) - } else { - result.WriteString(encode(search[segStart:])) - } - return result.String() -} - -// conditionSubfieldPattern matches 4-part condition paths like status.conditions.Reconciled.last_updated_time -// and encodes them to 3-part paths (status.conditions.Reconciled__last_updated_time) so the TSL parser can handle them. -// The TSL grammar only supports up to 3-part identifiers (database.table.column). -// The (^|[\s(]) anchor ensures we don't match things like "xstatus.conditions.Reconciled.last_updated_time". -// Group 1 captures the leading boundary (start-of-string, whitespace, or opening paren) to preserve it. -var conditionSubfieldPattern = regexp.MustCompile( - `(^|[\s(])(status\.conditions\.[A-Z][a-zA-Z0-9]*)\.([a-z][a-z_]+)`, -) - -// PreprocessConditionSubfields rewrites 4-part condition paths into 3-part paths -// before TSL parsing. The TSL parser supports at most 3 dot-separated segments. -// Only replaces in unquoted segments to avoid mutating string literals. -// -// Example: status.conditions.Reconciled.last_updated_time < '2026-03-06T00:00:00Z' -// becomes: status.conditions.Reconciled__last_updated_time < '2026-03-06T00:00:00Z' -func PreprocessConditionSubfields(search string) string { - var result strings.Builder - result.Grow(len(search)) - inQuote := false - quoteChar := byte(0) - segStart := 0 - - for i := 0; i < len(search); i++ { - ch := search[i] - if inQuote { - if ch == quoteChar { - // Flush quoted segment as-is (no replacement) - result.WriteString(search[segStart : i+1]) - segStart = i + 1 - inQuote = false - } - continue - } - if ch == '\'' || ch == '"' { - // Flush unquoted segment with replacement applied - result.WriteString( - conditionSubfieldPattern.ReplaceAllString(search[segStart:i], "${1}${2}__${3}"), - ) - segStart = i - inQuote = true - quoteChar = ch - } - } - // Flush remaining segment — only apply replacement if outside a quote - if inQuote { - result.WriteString(search[segStart:]) - } else { - result.WriteString( - conditionSubfieldPattern.ReplaceAllString(search[segStart:], "${1}${2}__${3}"), - ) - } - return result.String() -} - // conditionTimeSubfields are condition subfields that store timestamps and require TIMESTAMPTZ casting. // Note: created_time is intentionally excluded — it reflects when the condition was first created // and is not useful for Sentinel polling or staleness queries. @@ -330,13 +156,13 @@ var conditionIntSubfields = map[string]bool{ } // comparisonOperators maps TSL operator constants to SQL operator strings -var comparisonOperators = map[string]string{ - tsl.EqOp: "=", - tsl.NotEqOp: "!=", - tsl.LtOp: "<", - tsl.LteOp: "<=", - tsl.GtOp: ">", - tsl.GteOp: ">=", +var comparisonOperators = map[tsl.Operator]string{ + tsl.OpEQ: "=", + tsl.OpNE: "!=", + tsl.OpLT: "<", + tsl.OpLE: "<=", + tsl.OpGT: ">", + tsl.OpGE: ">=", } // startsWithConditions checks if a field starts with status.conditions. @@ -345,138 +171,117 @@ func startsWithConditions(s string) bool { } // hasCondition returns true if node has a status.conditions. identifier on left hand side. -func hasCondition(n tsl.Node) bool { - // Get the left side operator. - l, ok := n.Left.(tsl.Node) - if !ok { +func hasCondition(n *tsl.Node) bool { + if n.Left == nil || n.Left.Kind != tsl.KindIdentifier { return false } - - // If left side is not a `status.conditions.` identifier, return. - leftStr, ok := l.Left.(string) - if !ok || l.Func != tsl.IdentOp || !startsWithConditions(leftStr) { + leftStr, ok := n.Left.Value.(string) + if !ok || !startsWithConditions(leftStr) { return false } - return true } // conditionsNodeConverter handles condition queries in two forms: // // 3-part path (status query): status.conditions.='' -// -// Transforms to: jsonb_path_query_first(status_conditions, '$[*] ? (@.type == "Reconciled")') ->> 'status' = 'True' -// -// 4-part path (subfield query): -// -// status.conditions.. '' -// Time subfields use CAST(... AS TIMESTAMPTZ) -// Integer subfields use CAST(... AS INTEGER) -func conditionsNodeConverter(n tsl.Node) (interface{}, *errors.ServiceError) { - // Get the left side operator. - l, ok := n.Left.(tsl.Node) - if !ok { +// 4-part path (subfield query): status.conditions.. '' +func conditionsNodeConverter(n *tsl.Node) (interface{}, *errors.ServiceError) { + if n.Left == nil || n.Left.Kind != tsl.KindIdentifier { return nil, errors.BadRequest("invalid condition query structure") } - // Extract the full field path - leftStr, ok := l.Left.(string) + leftStr, ok := n.Left.Value.(string) if !ok { return nil, errors.BadRequest("expected string for left side of condition") } - // After PreprocessConditionSubfields, the path is always 3 parts: - // - status.conditions.Reconciled (status query) - // - status.conditions.Reconciled__last_updated_time (subfield query, encoded with __) + // v6 gives us the full dotted path: 3 or 4 parts parts := strings.Split(leftStr, ".") - if len(parts) != 3 || parts[0] != "status" || parts[1] != "conditions" { + if len(parts) < 3 || len(parts) > 4 || parts[0] != "status" || parts[1] != "conditions" { return nil, errors.BadRequest("invalid condition field path: %s", leftStr) } - // Check if the 3rd part contains an encoded subfield (e.g., Reconciled__last_updated_time). - // The __ encoding is produced by PreprocessConditionSubfields, but users can also - // submit the encoded form directly — the same validation applies either way. - subfieldParts := strings.SplitN(parts[2], "__", 2) - conditionType := subfieldParts[0] + conditionType := parts[2] - // Validate condition type to prevent SQL injection if !conditionTypePattern.MatchString(conditionType) { return nil, errors.BadRequest( "condition type '%s' is invalid: must be PascalCase (e.g., Reconciled, Available)", conditionType, ) } - // Subfield query (e.g., Reconciled__last_updated_time -> conditionType=Reconciled, subfield=last_updated_time) - if len(subfieldParts) == 2 { - return conditionSubfieldConverter(n, conditionType, subfieldParts[1]) + // 4-part path: subfield query (e.g., status.conditions.Reconciled.last_updated_time) + if len(parts) == 4 { + return conditionSubfieldConverter(n, conditionType, parts[3]) } - // Status query (e.g., status.conditions.Reconciled='True') + // 3-part path: status query (e.g., status.conditions.Reconciled='True') return conditionStatusConverter(n, conditionType) } // conditionStatusConverter handles 3-part condition status queries: // status.conditions.='' -func conditionStatusConverter(n tsl.Node, conditionType string) (interface{}, *errors.ServiceError) { - // Get the right side value (the expected status) - r, ok := n.Right.(tsl.Node) - if !ok { +func conditionStatusConverter(n *tsl.Node, conditionType string) (interface{}, *errors.ServiceError) { + if n.Right == nil || n.Right.Kind != tsl.KindStringLiteral { return nil, errors.BadRequest("invalid condition query structure: missing right side") } - rightStr, ok := r.Left.(string) + rightStr, ok := n.Right.Value.(string) if !ok { return nil, errors.BadRequest("expected string for right side of condition") } - // Validate condition status if !validConditionStatuses[rightStr] { return nil, errors.BadRequest( "condition status '%s' is invalid: must be True, False, or Unknown", rightStr, ) } - // Only support equality operator for condition status queries - if n.Func != tsl.EqOp { + if n.Operator != tsl.OpEQ { return nil, errors.BadRequest("only equality operator (=) is supported for condition status queries") } - // Build query using jsonb_path_query_first. - // NOTE: Ideally the jsonpath literal would be inlined so PostgreSQL can match expression - // indexes, but Squirrel treats the '?' in jsonpath filter syntax ($[*] ? (...)) as a bind - // placeholder. HYPERFLEET-736 evaluates generated columns or table normalization as a - // proper fix to enable index usage. jsonPath := fmt.Sprintf(`$[*] ? (@.type == "%s")`, conditionType) return sq.Expr("jsonb_path_query_first(status_conditions, ?::jsonpath) ->> 'status' = ?", jsonPath, rightStr), nil } // conditionSubfieldConverter handles 4-part condition subfield queries: // status.conditions.. '' -func conditionSubfieldConverter(n tsl.Node, conditionType, subfield string) (interface{}, *errors.ServiceError) { - // Validate the operator is a supported comparison operator - sqlOp, ok := comparisonOperators[n.Func] +func conditionSubfieldConverter(n *tsl.Node, conditionType, subfield string) (interface{}, *errors.ServiceError) { + sqlOp, ok := comparisonOperators[n.Operator] if !ok { return nil, errors.BadRequest( - "operator '%s' is not supported for condition subfield queries; use =, !=, <, <=, >, or >=", n.Func, + "operator '%s' is not supported for condition subfield queries; use =, !=, <, <=, >, or >=", n.Operator, ) } - // Get the right side value - r, rOk := n.Right.(tsl.Node) - if !rOk { + if n.Right == nil { return nil, errors.BadRequest("invalid condition query structure: missing right side") } - // NOTE: Ideally the jsonpath and subfield literals would be inlined so PostgreSQL can - // match expression indexes, but Squirrel treats '?' in jsonpath filter syntax as a bind - // placeholder. HYPERFLEET-736 evaluates generated columns or table normalization as a - // proper fix to enable index usage. jsonPath := fmt.Sprintf(`$[*] ? (@.type == "%s")`, conditionType) - // Handle time subfields if conditionTimeSubfields[subfield] { - rightStr, strOk := r.Left.(string) - if !strOk { + var rightStr string + switch n.Right.Kind { + case tsl.KindStringLiteral: + s, ok := n.Right.Value.(string) + if !ok { + return nil, errors.BadRequest( + "expected string timestamp value for condition subfield '%s'", subfield, + ) + } + rightStr = s + case tsl.KindTimestampLiteral: + // v6 parses RFC3339 strings as time.Time — convert back for our SQL binding + t, ok := n.Right.Value.(time.Time) + if !ok { + return nil, errors.BadRequest( + "expected timestamp value for condition subfield '%s'", subfield, + ) + } + rightStr = t.Format(time.RFC3339Nano) + default: return nil, errors.BadRequest( "expected string timestamp value for condition subfield '%s'", subfield, ) @@ -494,9 +299,13 @@ func conditionSubfieldConverter(n tsl.Node, conditionType, subfield string) (int return sq.Expr(query, jsonPath, subfield, rightStr), nil } - // Handle integer subfields if conditionIntSubfields[subfield] { - rightVal, numOk := r.Left.(float64) + if n.Right.Kind != tsl.KindNumericLiteral { + return nil, errors.BadRequest( + "expected numeric value for condition subfield '%s'", subfield, + ) + } + rightVal, numOk := n.Right.Value.(float64) if !numOk { return nil, errors.BadRequest( "expected numeric value for condition subfield '%s'", subfield, @@ -526,55 +335,40 @@ func conditionSubfieldConverter(n tsl.Node, conditionType, subfield string) (int ) } -// ConditionExpression represents an extracted condition query as a Squirrel expression -type ConditionExpression struct { - Expr sq.Sqlizer -} - // ExtractConditionQueries walks the TSL tree and extracts condition queries, // returning the modified tree (with condition nodes replaced) and the extracted conditions. -// This is necessary because the TSL library doesn't support JSONB containment operators. -func ExtractConditionQueries(n tsl.Node, tableName string) (tsl.Node, []sq.Sqlizer, *errors.ServiceError) { +func ExtractConditionQueries(n *tsl.Node) (*tsl.Node, []sq.Sqlizer, *errors.ServiceError) { var conditions []sq.Sqlizer modifiedTree, err := extractConditionsWalk(n, &conditions) return modifiedTree, conditions, err } -// Returns true if any node in the subtree rooted at n is a condition query -func subtreeHasCondition(n tsl.Node) bool { +// subtreeHasCondition returns true if any node in the subtree is a condition query +func subtreeHasCondition(n *tsl.Node) bool { + if n == nil { + return false + } if hasCondition(n) { return true } - - l, ok := n.Left.(tsl.Node) - if ok && subtreeHasCondition(l) { + if subtreeHasCondition(n.Left) { return true } - - r, ok := n.Right.(tsl.Node) - if ok && subtreeHasCondition(r) { + if subtreeHasCondition(n.Right) { return true } - - rr, ok := n.Right.([]tsl.Node) - if ok { - for _, r := range rr { - if subtreeHasCondition(r) { - return true - } - } - } - return false + return slices.ContainsFunc(n.Children, subtreeHasCondition) } // extractConditionsWalk recursively walks the tree and extracts condition queries -func extractConditionsWalk(n tsl.Node, conditions *[]sq.Sqlizer) (tsl.Node, *errors.ServiceError) { - // Reject NOT applied to condition queries — the condition is extracted before - // NOT is applied, so the negation would be silently lost, producing wrong results. - // Scan the entire NOT subtree, not just the direct child, to catch conditions - // nested inside AND/OR under NOT (e.g., NOT (condA AND x)). - if n.Func == tsl.NotOp { - if child, ok := n.Left.(tsl.Node); ok && subtreeHasCondition(child) { +func extractConditionsWalk(n *tsl.Node, conditions *[]sq.Sqlizer) (*tsl.Node, *errors.ServiceError) { + if n == nil { + return nil, nil + } + + // NOT is unary in v6: child is in Right, Left is nil + if n.Kind == tsl.KindUnaryExpr && n.Operator == tsl.OpNot { + if subtreeHasCondition(n.Right) { return n, errors.BadRequest( "NOT operator is not supported with condition queries; " + "use the inverse condition instead (e.g., Reconciled='False')", @@ -582,7 +376,19 @@ func extractConditionsWalk(n tsl.Node, conditions *[]sq.Sqlizer) (tsl.Node, *err } } - // Check if this node is a condition query + // OR with condition queries is not supported: extracting conditions from + // an OR branch and replacing them with 1=1 changes the query semantics + // (e.g. "name='x' OR condition" becomes "(name='x' OR 1=1) AND condition", + // which collapses the OR to always-true). + if n.Kind == tsl.KindBinaryExpr && n.Operator == tsl.OpOr { + if subtreeHasCondition(n.Left) || subtreeHasCondition(n.Right) { + return n, errors.BadRequest( + "OR operator is not supported with condition queries (status.conditions.*); " + + "use separate requests or combine conditions with AND", + ) + } + } + if hasCondition(n) { expr, err := conditionsNodeConverter(n) if err != nil { @@ -595,217 +401,164 @@ func extractConditionsWalk(n tsl.Node, conditions *[]sq.Sqlizer) (tsl.Node, *err *conditions = append(*conditions, sqlizer) // Replace with a placeholder that always evaluates to true - // This allows the rest of the tree to be processed normally - return tsl.Node{ - Func: tsl.EqOp, - Left: tsl.Node{Func: tsl.NumberOp, Left: float64(1)}, - Right: tsl.Node{Func: tsl.NumberOp, Left: float64(1)}, + return &tsl.Node{ + Kind: tsl.KindBinaryExpr, + Operator: tsl.OpEQ, + Left: &tsl.Node{Kind: tsl.KindNumericLiteral, Value: float64(1)}, + Right: &tsl.Node{Kind: tsl.KindNumericLiteral, Value: float64(1)}, }, nil } // For non-condition nodes, recursively process children - var newLeft, newRight interface{} + var newLeft, newRight *tsl.Node if n.Left != nil { - switch v := n.Left.(type) { - case tsl.Node: - newLeftNode, err := extractConditionsWalk(v, conditions) - if err != nil { - return n, err - } - newLeft = newLeftNode - default: - newLeft = n.Left + var err *errors.ServiceError + newLeft, err = extractConditionsWalk(n.Left, conditions) + if err != nil { + return n, err } } if n.Right != nil { - switch v := n.Right.(type) { - case tsl.Node: - newRightNode, err := extractConditionsWalk(v, conditions) - if err != nil { - return n, err - } - newRight = newRightNode - case []tsl.Node: - var newRightNodes []tsl.Node - for _, rightNode := range v { - newRightNode, err := extractConditionsWalk(rightNode, conditions) - if err != nil { - return n, err - } - newRightNodes = append(newRightNodes, newRightNode) - } - newRight = newRightNodes - default: - newRight = n.Right + var err *errors.ServiceError + newRight, err = extractConditionsWalk(n.Right, conditions) + if err != nil { + return n, err } } - return tsl.Node{ - Func: n.Func, - Left: newLeft, - Right: newRight, - }, nil -} + var newChildren []*tsl.Node + for _, child := range n.Children { + newChild, childErr := extractConditionsWalk(child, conditions) + if childErr != nil { + return n, childErr + } + newChildren = append(newChildren, newChild) + } -// WrapSpecNumericCasts walks the TSL tree after field name mapping and wraps -// spec field identifiers in CAST(... AS numeric) when the right-hand side of a -// comparison is a number. This is necessary because JSONB ->> returns text, so -// without the cast, numeric ordering breaks for multi-digit values -// (e.g. '10' < '9' lexicographically). -func WrapSpecNumericCasts(n tsl.Node) tsl.Node { - return wrapSpecNumericCastsWalk(n) + return &tsl.Node{ + Kind: n.Kind, + Operator: n.Operator, + Value: n.Value, + Left: newLeft, + Right: newRight, + Children: newChildren, + Position: n.Position, + }, nil } -func wrapSpecNumericCastsWalk(n tsl.Node) tsl.Node { - // If this is a comparison with a spec field on the left and a number on the right, - // wrap the field in CAST(... AS numeric). - if _, isComparison := comparisonOperators[n.Func]; isComparison { - leftNode, leftOk := n.Left.(tsl.Node) - rightNode, rightOk := n.Right.(tsl.Node) - if leftOk && rightOk && leftNode.Func == tsl.IdentOp && rightNode.Func == tsl.NumberOp { - if field, ok := leftNode.Left.(string); ok && strings.HasPrefix(field, "spec->") { - return tsl.Node{ - Func: n.Func, - Left: tsl.Node{Func: tsl.IdentOp, Left: fmt.Sprintf("CAST(%s AS numeric)", field)}, - Right: n.Right, - } - } +// FieldNameWalk walks the filter tree, maps user-facing field names to SQL columns +// via getField, then wraps spec JSONB numeric comparisons in CAST. +func FieldNameWalk( + n *tsl.Node, + disallowedFields map[string]string, +) (*tsl.Node, *errors.ServiceError) { + mapped, err := IdentWalk(n, func(name string) (string, error) { + field, svcErr := getField(name, disallowedFields) + if svcErr != nil { + return "", svcErr + } + return field, nil + }) + if err != nil { + if svcErr, ok := err.(*errors.ServiceError); ok { + return n, svcErr } + return n, errors.BadRequest("%s", err.Error()) } + return wrapSpecNumericCasts(mapped), nil +} - // Recurse into children. - var newLeft, newRight interface{} - if n.Left != nil { - if leftNode, ok := n.Left.(tsl.Node); ok { - newLeft = wrapSpecNumericCastsWalk(leftNode) - } else { - newLeft = n.Left +// wrapSpecNumericCasts wraps spec JSONB fields in CAST(... AS numeric) when +// compared against a number, so multi-digit values sort correctly. +func wrapSpecNumericCasts(n *tsl.Node) *tsl.Node { + if n == nil { + return nil + } + if n.Kind == tsl.KindBinaryExpr { + l := wrapSpecNumericCasts(n.Left) + r := wrapSpecNumericCasts(n.Right) + if _, isCmp := comparisonOperators[n.Operator]; isCmp && + l != nil && r != nil && + l.Kind == tsl.KindIdentifier && r.Kind == tsl.KindNumericLiteral { + if field, ok := l.Value.(string); ok && strings.HasPrefix(field, "spec->") { + l = &tsl.Node{Kind: tsl.KindIdentifier, Value: fmt.Sprintf("CAST(%s AS numeric)", field)} + } } - } - if n.Right != nil { - switch v := n.Right.(type) { - case tsl.Node: - newRight = wrapSpecNumericCastsWalk(v) - case []tsl.Node: - nodes := make([]tsl.Node, len(v)) - for i, node := range v { - nodes[i] = wrapSpecNumericCastsWalk(node) + if _, isCmp := comparisonOperators[n.Operator]; isCmp && + l != nil && r != nil && + l.Kind == tsl.KindNumericLiteral && r.Kind == tsl.KindIdentifier { + if field, ok := r.Value.(string); ok && strings.HasPrefix(field, "spec->") { + r = &tsl.Node{Kind: tsl.KindIdentifier, Value: fmt.Sprintf("CAST(%s AS numeric)", field)} } - newRight = nodes - default: - newRight = v } + return &tsl.Node{Kind: n.Kind, Operator: n.Operator, Left: l, Right: r, Position: n.Position} } - return tsl.Node{Func: n.Func, Left: newLeft, Right: newRight} + if n.Kind == tsl.KindUnaryExpr { + return &tsl.Node{Kind: n.Kind, Operator: n.Operator, Right: wrapSpecNumericCasts(n.Right), Position: n.Position} + } + return n } -// FieldNameWalk walks on the filter tree and check/replace -// the search fields names: -// a. the the field name is valid. -// b. replace the field name with the SQL column name. -func FieldNameWalk( - n tsl.Node, - disallowedFields map[string]string) (newNode tsl.Node, err *errors.ServiceError) { - - var field string - var l, r tsl.Node - - // Check for properties. = nodes, and convert them to - // ( properties ->> = ) - // nodes. - if hasProperty(n) { - n = propertiesNodeConverter(n) +// IdentWalk walks the tree and replaces identifier values using the check function. +// Unlike v6's ident.Walk, this does NOT re-parse the return value — safe for JSONB expressions. +func IdentWalk(n *tsl.Node, check func(string) (string, error)) (*tsl.Node, error) { + if n == nil { + return nil, nil } - switch n.Func { - case tsl.IdentOp: - // If this is an Identifier, check field name is a string. - userFieldName, ok := n.Left.(string) + switch n.Kind { + case tsl.KindIdentifier: + s, ok := n.Value.(string) if !ok { - err = errors.BadRequest("Identifier name must be a string") - return + return nil, fmt.Errorf("identifier value is not a string") } - - // Check field name in the disallowedFields field names. - field, err = getField(userFieldName, disallowedFields) + v, err := check(s) if err != nil { - return + return nil, err } + return &tsl.Node{Kind: tsl.KindIdentifier, Value: v, Position: n.Position}, nil - // Replace identifier name. - newNode = tsl.Node{Func: tsl.IdentOp, Left: field} - case tsl.StringOp, tsl.NumberOp: - // This are leafs, just return. - newNode = tsl.Node{Func: n.Func, Left: n.Left} - default: - // o/w continue walking the tree. - if n.Left != nil { - leftNode, ok := n.Left.(tsl.Node) - if !ok { - err = errors.BadRequest("invalid node structure") - return - } - l, err = FieldNameWalk(leftNode, disallowedFields) - if err != nil { - return - } + case tsl.KindStringLiteral, tsl.KindNumericLiteral, tsl.KindDateLiteral, + tsl.KindTimestampLiteral, tsl.KindBooleanLiteral, tsl.KindNullLiteral: + return n, nil + + case tsl.KindBinaryExpr: + newLeft, err := IdentWalk(n.Left, check) + if err != nil { + return nil, err + } + newRight, err := IdentWalk(n.Right, check) + if err != nil { + return nil, err } + return &tsl.Node{Kind: n.Kind, Operator: n.Operator, Left: newLeft, Right: newRight, Position: n.Position}, nil - // Add right child(ren) if exist. - if n.Right != nil { - switch v := n.Right.(type) { - case tsl.Node: - // It's a regular node, just add it. - r, err = FieldNameWalk(v, disallowedFields) - if err != nil { - return - } - - newNode = tsl.Node{Func: n.Func, Left: l, Right: r} - case []tsl.Node: - // It's a list of nodes, some TSL operators have multiple RHS arguments - // for example `IN` and `BETWEEN`. If this operator has a list of arguments, - // loop over the list, and add all nodes. - var rr []tsl.Node - - // Add all nodes in the right side array. - for _, e := range v { - r, err = FieldNameWalk(e, disallowedFields) - if err != nil { - return - } - - rr = append(rr, r) - } - - newNode = tsl.Node{Func: n.Func, Left: l, Right: rr} - default: - // We only support `Node` and `[]Node` types for the right hand side, - // of TSL operators. If here than this is an unsupported right hand side - // type. - err = errors.BadRequest("unsupported right hand side type in search query") - } - } else { - // If here than `n.Right` is nil. This is a legit type of node, - // we just need to ignore the right hand side, and continue walking the - // tree. - newNode = tsl.Node{Func: n.Func, Left: l} + case tsl.KindUnaryExpr: + newRight, err := IdentWalk(n.Right, check) + if err != nil { + return nil, err } - } + return &tsl.Node{Kind: n.Kind, Operator: n.Operator, Right: newRight, Position: n.Position}, nil - return + case tsl.KindArrayLiteral: + // Array elements in IN [...] are values (literals), not column names. + // Don't run them through the check function — it would try to resolve + // them as field names and either mangle or reject valid values. + return n, nil + + default: + return n, nil + } } // cleanOrderBy takes the orderBy arg and cleans it. func cleanOrderBy(userArg string, disallowedFields map[string]string) (orderBy string, err *errors.ServiceError) { var orderField string - // We want to accept user params with trailing and leading spaces trimedName := strings.Trim(userArg, " ") - // Each OrderBy can be a "" or a " asc|desc" order := strings.Split(trimedName, " ") direction := "none valid" @@ -828,8 +581,8 @@ func cleanOrderBy(userArg string, disallowedFields map[string]string) (orderBy s // ArgsToOrderBy returns cleaned orderBy list. func ArgsToOrderBy( orderByArgs []string, - disallowedFields map[string]string) (orderBy []string, err *errors.ServiceError) { - + disallowedFields map[string]string, +) (orderBy []string, err *errors.ServiceError) { var order string if len(orderByArgs) != 0 { orderBy = []string{} @@ -839,7 +592,6 @@ func ArgsToOrderBy( return } - // If valid add the user entered order by, to the order by list orderBy = append(orderBy, order) } } diff --git a/pkg/db/sql_helpers_test.go b/pkg/db/sql_helpers_test.go index 92058115..0561cf36 100644 --- a/pkg/db/sql_helpers_test.go +++ b/pkg/db/sql_helpers_test.go @@ -2,9 +2,10 @@ package db import ( "testing" + "time" . "github.com/onsi/gomega" - "github.com/yaacov/tree-search-language/pkg/tsl" + "github.com/yaacov/tree-search-language/v6/pkg/tsl" ) func TestConditionsNodeConverterStatus(t *testing.T) { @@ -45,13 +46,6 @@ func TestConditionsNodeConverterStatus(t *testing.T) { expectedSQL: "jsonb_path_query_first(status_conditions, ?::jsonpath) ->> 'status' = ?", expectedArgs: []interface{}{`$[*] ? (@.type == "Available")`, "Unknown"}, }, - { - name: "Reconciled condition", - field: "status.conditions.Reconciled", - value: "True", - expectedSQL: "jsonb_path_query_first(status_conditions, ?::jsonpath) ->> 'status' = ?", - expectedArgs: []interface{}{`$[*] ? (@.type == "Reconciled")`, "True"}, - }, { name: "Invalid condition status", field: "status.conditions.Reconciled", @@ -79,16 +73,11 @@ func TestConditionsNodeConverterStatus(t *testing.T) { t.Run(tt.name, func(t *testing.T) { RegisterTestingT(t) - node := tsl.Node{ - Func: tsl.EqOp, - Left: tsl.Node{ - Func: tsl.IdentOp, - Left: tt.field, - }, - Right: tsl.Node{ - Func: tsl.StringOp, - Left: tt.value, - }, + node := &tsl.Node{ + Kind: tsl.KindBinaryExpr, + Operator: tsl.OpEQ, + Left: &tsl.Node{Kind: tsl.KindIdentifier, Value: tt.field}, + Right: &tsl.Node{Kind: tsl.KindStringLiteral, Value: tt.value}, } result, err := conditionsNodeConverter(node) @@ -119,20 +108,19 @@ func TestConditionsNodeConverterStatus(t *testing.T) { func TestConditionsNodeConverterSubfields(t *testing.T) { tests := []struct { - value interface{} name string field string - op string expectedSQL string errorContains string + value interface{} expectedArgs []interface{} + op tsl.Operator expectError bool }{ - // Time subfield: last_updated_time (encoded with __ after preprocessing) { name: "last_updated_time less than", - field: "status.conditions.Reconciled__last_updated_time", - op: tsl.LtOp, + field: "status.conditions.Reconciled.last_updated_time", + op: tsl.OpLT, value: "2026-03-06T00:00:00Z", expectedSQL: "CAST(jsonb_path_query_first(status_conditions, ?::jsonpath) ->> ? AS TIMESTAMPTZ) < ?::timestamptz", expectedArgs: []interface{}{ @@ -143,8 +131,8 @@ func TestConditionsNodeConverterSubfields(t *testing.T) { }, { name: "last_updated_time greater than", - field: "status.conditions.Reconciled__last_updated_time", - op: tsl.GtOp, + field: "status.conditions.Reconciled.last_updated_time", + op: tsl.OpGT, value: "2026-03-06T00:00:00Z", expectedSQL: "CAST(jsonb_path_query_first(status_conditions, ?::jsonpath) ->> ? AS TIMESTAMPTZ) > ?::timestamptz", expectedArgs: []interface{}{ @@ -155,8 +143,8 @@ func TestConditionsNodeConverterSubfields(t *testing.T) { }, { name: "last_updated_time less than or equal", - field: "status.conditions.Reconciled__last_updated_time", - op: tsl.LteOp, + field: "status.conditions.Reconciled.last_updated_time", + op: tsl.OpLE, value: "2026-03-06T00:00:00Z", expectedSQL: "CAST(jsonb_path_query_first(status_conditions, ?::jsonpath) ->> ? AS TIMESTAMPTZ) <= ?::timestamptz", expectedArgs: []interface{}{ @@ -167,8 +155,8 @@ func TestConditionsNodeConverterSubfields(t *testing.T) { }, { name: "last_updated_time greater than or equal", - field: "status.conditions.Reconciled__last_updated_time", - op: tsl.GteOp, + field: "status.conditions.Reconciled.last_updated_time", + op: tsl.OpGE, value: "2026-03-06T00:00:00Z", expectedSQL: "CAST(jsonb_path_query_first(status_conditions, ?::jsonpath) ->> ? AS TIMESTAMPTZ) >= ?::timestamptz", expectedArgs: []interface{}{ @@ -179,8 +167,8 @@ func TestConditionsNodeConverterSubfields(t *testing.T) { }, { name: "last_updated_time equal", - field: "status.conditions.Reconciled__last_updated_time", - op: tsl.EqOp, + field: "status.conditions.Reconciled.last_updated_time", + op: tsl.OpEQ, value: "2026-03-06T00:00:00Z", expectedSQL: "CAST(jsonb_path_query_first(status_conditions, ?::jsonpath) ->> ? AS TIMESTAMPTZ) = ?::timestamptz", expectedArgs: []interface{}{ @@ -191,8 +179,8 @@ func TestConditionsNodeConverterSubfields(t *testing.T) { }, { name: "last_updated_time not equal", - field: "status.conditions.Reconciled__last_updated_time", - op: tsl.NotEqOp, + field: "status.conditions.Reconciled.last_updated_time", + op: tsl.OpNE, value: "2026-03-06T00:00:00Z", expectedSQL: "CAST(jsonb_path_query_first(status_conditions, ?::jsonpath) ->> ? AS TIMESTAMPTZ) != ?::timestamptz", expectedArgs: []interface{}{ @@ -201,11 +189,10 @@ func TestConditionsNodeConverterSubfields(t *testing.T) { "2026-03-06T00:00:00Z", }, }, - // Time subfield: last_transition_time { name: "last_transition_time less than", - field: "status.conditions.Available__last_transition_time", - op: tsl.LtOp, + field: "status.conditions.Available.last_transition_time", + op: tsl.OpLT, value: "2026-03-06T00:00:00Z", expectedSQL: "CAST(jsonb_path_query_first(status_conditions, ?::jsonpath) ->> ? AS TIMESTAMPTZ) < ?::timestamptz", expectedArgs: []interface{}{ @@ -214,11 +201,10 @@ func TestConditionsNodeConverterSubfields(t *testing.T) { "2026-03-06T00:00:00Z", }, }, - // Integer subfield: observed_generation { name: "observed_generation less than", - field: "status.conditions.Reconciled__observed_generation", - op: tsl.LtOp, + field: "status.conditions.Reconciled.observed_generation", + op: tsl.OpLT, value: float64(5), expectedSQL: "CAST(jsonb_path_query_first(status_conditions, ?::jsonpath) ->> ? AS INTEGER) < ?", expectedArgs: []interface{}{ @@ -229,8 +215,8 @@ func TestConditionsNodeConverterSubfields(t *testing.T) { }, { name: "observed_generation equal", - field: "status.conditions.Reconciled__observed_generation", - op: tsl.EqOp, + field: "status.conditions.Reconciled.observed_generation", + op: tsl.OpEQ, value: float64(3), expectedSQL: "CAST(jsonb_path_query_first(status_conditions, ?::jsonpath) ->> ? AS INTEGER) = ?", expectedArgs: []interface{}{ @@ -239,51 +225,74 @@ func TestConditionsNodeConverterSubfields(t *testing.T) { 3, }, }, - // Error cases + { + name: "KindTimestampLiteral preserves fractional seconds", + field: "status.conditions.Reconciled.last_updated_time", + op: tsl.OpLT, + value: time.Date(2026, 3, 6, 12, 30, 45, 123456789, time.UTC), + expectedSQL: "CAST(jsonb_path_query_first(status_conditions, ?::jsonpath) ->> ? AS TIMESTAMPTZ) < ?::timestamptz", + expectedArgs: []interface{}{ + `$[*] ? (@.type == "Reconciled")`, + "last_updated_time", + "2026-03-06T12:30:45.123456789Z", + }, + }, + { + name: "KindTimestampLiteral without fractional seconds", + field: "status.conditions.Reconciled.last_updated_time", + op: tsl.OpGE, + value: time.Date(2026, 3, 6, 0, 0, 0, 0, time.UTC), + expectedSQL: "CAST(jsonb_path_query_first(status_conditions, ?::jsonpath) ->> ? AS TIMESTAMPTZ) >= ?::timestamptz", + expectedArgs: []interface{}{ + `$[*] ? (@.type == "Reconciled")`, + "last_updated_time", + "2026-03-06T00:00:00Z", + }, + }, { name: "Invalid subfield name", - field: "status.conditions.Reconciled__unknown_field", - op: tsl.LtOp, + field: "status.conditions.Reconciled.unknown_field", + op: tsl.OpLT, value: "2026-03-06T00:00:00Z", expectError: true, errorContains: "not supported", }, { name: "Invalid operator for subfield", - field: "status.conditions.Reconciled__last_updated_time", - op: tsl.LikeOp, + field: "status.conditions.Reconciled.last_updated_time", + op: tsl.OpLike, value: "2026%", expectError: true, errorContains: "not supported for condition subfield", }, { name: "Invalid condition type in subfield query", - field: "status.conditions.ready__last_updated_time", - op: tsl.LtOp, + field: "status.conditions.ready.last_updated_time", + op: tsl.OpLT, value: "2026-03-06T00:00:00Z", expectError: true, errorContains: "must be PascalCase", }, { name: "Invalid timestamp format", - field: "status.conditions.Reconciled__last_updated_time", - op: tsl.LtOp, + field: "status.conditions.Reconciled.last_updated_time", + op: tsl.OpLT, value: "not-a-timestamp", expectError: true, errorContains: "expected RFC3339 format", }, { name: "Float value for integer subfield", - field: "status.conditions.Reconciled__observed_generation", - op: tsl.LtOp, + field: "status.conditions.Reconciled.observed_generation", + op: tsl.OpLT, value: float64(3.5), expectError: true, errorContains: "expected integer value", }, { name: "Integer overflow for integer subfield", - field: "status.conditions.Reconciled__observed_generation", - op: tsl.LtOp, + field: "status.conditions.Reconciled.observed_generation", + op: tsl.OpLT, value: float64(3000000000), expectError: true, errorContains: "out of 32-bit integer range", @@ -294,21 +303,21 @@ func TestConditionsNodeConverterSubfields(t *testing.T) { t.Run(tt.name, func(t *testing.T) { RegisterTestingT(t) - var rightNode tsl.Node + var rightNode *tsl.Node switch v := tt.value.(type) { case string: - rightNode = tsl.Node{Func: tsl.StringOp, Left: v} + rightNode = &tsl.Node{Kind: tsl.KindStringLiteral, Value: v} case float64: - rightNode = tsl.Node{Func: tsl.NumberOp, Left: v} + rightNode = &tsl.Node{Kind: tsl.KindNumericLiteral, Value: v} + case time.Time: + rightNode = &tsl.Node{Kind: tsl.KindTimestampLiteral, Value: v} } - node := tsl.Node{ - Func: tt.op, - Left: tsl.Node{ - Func: tsl.IdentOp, - Left: tt.field, - }, - Right: rightNode, + node := &tsl.Node{ + Kind: tsl.KindBinaryExpr, + Operator: tt.op, + Left: &tsl.Node{Kind: tsl.KindIdentifier, Value: tt.field}, + Right: rightNode, } result, err := conditionsNodeConverter(node) @@ -337,73 +346,6 @@ func TestConditionsNodeConverterSubfields(t *testing.T) { } } -func TestPreprocessConditionSubfields(t *testing.T) { - tests := []struct { - name string - input string - expected string - }{ - { - name: "4-part path is encoded", - input: "status.conditions.Reconciled.last_updated_time < '2026-03-06T00:00:00Z'", - expected: "status.conditions.Reconciled__last_updated_time < '2026-03-06T00:00:00Z'", - }, - { - name: "3-part path is unchanged", - input: "status.conditions.Reconciled='True'", - expected: "status.conditions.Reconciled='True'", - }, - { - name: "Mixed 3-part and 4-part", - input: "status.conditions.Reconciled='False' AND " + - "status.conditions.Reconciled.last_updated_time < '2026-03-06T00:00:00Z'", - expected: "status.conditions.Reconciled='False' AND " + - "status.conditions.Reconciled__last_updated_time < '2026-03-06T00:00:00Z'", - }, - { - name: "Labels are unchanged", - input: "labels.environment='production'", - expected: "labels.environment='production'", - }, - { - name: "last_transition_time is encoded", - input: "status.conditions.Available.last_transition_time > '2026-01-01T00:00:00Z'", - expected: "status.conditions.Available__last_transition_time > '2026-01-01T00:00:00Z'", - }, - { - name: "observed_generation is encoded", - input: "status.conditions.Reconciled.observed_generation < 5", - expected: "status.conditions.Reconciled__observed_generation < 5", - }, - { - name: "Text inside single quotes is not encoded", - input: "name='status.conditions.Reconciled.last_updated_time'", - expected: "name='status.conditions.Reconciled.last_updated_time'", - }, - { - name: "Text inside double quotes is not encoded", - input: `name="status.conditions.Reconciled.last_updated_time"`, - expected: `name="status.conditions.Reconciled.last_updated_time"`, - }, - { - name: "Mixed quoted and unquoted segments", - input: "status.conditions.Reconciled.last_updated_time < '2026-03-06T00:00:00Z'" + - " AND name='status.conditions.Reconciled.last_updated_time'", - expected: "status.conditions.Reconciled__last_updated_time < '2026-03-06T00:00:00Z'" + - " AND name='status.conditions.Reconciled.last_updated_time'", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - RegisterTestingT(t) - - result := PreprocessConditionSubfields(tt.input) - Expect(result).To(Equal(tt.expected)) - }) - } -} - func TestHasConditionWithSubfields(t *testing.T) { tests := []struct { name string @@ -416,8 +358,8 @@ func TestHasConditionWithSubfields(t *testing.T) { expected: true, }, { - name: "Encoded subfield (after preprocessing)", - field: "status.conditions.Reconciled__last_updated_time", + name: "4-part subfield (v6 native)", + field: "status.conditions.Reconciled.last_updated_time", expected: true, }, { @@ -431,16 +373,11 @@ func TestHasConditionWithSubfields(t *testing.T) { t.Run(tt.name, func(t *testing.T) { RegisterTestingT(t) - node := tsl.Node{ - Func: tsl.EqOp, - Left: tsl.Node{ - Func: tsl.IdentOp, - Left: tt.field, - }, - Right: tsl.Node{ - Func: tsl.StringOp, - Left: "value", - }, + node := &tsl.Node{ + Kind: tsl.KindBinaryExpr, + Operator: tsl.OpEQ, + Left: &tsl.Node{Kind: tsl.KindIdentifier, Value: tt.field}, + Right: &tsl.Node{Kind: tsl.KindStringLiteral, Value: "value"}, } result := hasCondition(node) @@ -478,12 +415,12 @@ func TestExtractConditionQueriesWithSubfields(t *testing.T) { }, { name: "NOT operator on condition query returns error", - searchQuery: "NOT status.conditions.Reconciled='True'", + searchQuery: "NOT (status.conditions.Reconciled='True')", expectError: true, }, { name: "NOT operator on condition subfield query returns error", - searchQuery: "NOT status.conditions.Reconciled.last_updated_time < '2026-03-06T00:00:00Z'", + searchQuery: "NOT (status.conditions.Reconciled.last_updated_time < '2026-03-06T00:00:00Z')", expectError: true, }, { @@ -497,11 +434,11 @@ func TestExtractConditionQueriesWithSubfields(t *testing.T) { t.Run(tt.name, func(t *testing.T) { RegisterTestingT(t) - preprocessed := PreprocessConditionSubfields(tt.searchQuery) - tslTree, err := tsl.ParseTSL(preprocessed) + // v6 handles deep identifiers natively — no preprocessing needed + tslTreeWrapper, err := tsl.ParseTSL(tt.searchQuery) Expect(err).ToNot(HaveOccurred()) - _, conditions, serviceErr := ExtractConditionQueries(tslTree, "clusters") + _, conditions, serviceErr := ExtractConditionQueries(tslTreeWrapper.Node) if tt.expectError { Expect(serviceErr).ToNot(BeNil()) @@ -549,19 +486,45 @@ func TestExtractConditionQueries(t *testing.T) { searchQuery: "status.conditions.Reconciled='True' AND status.conditions.Available='True'", expectedConditions: 2, }, + { + name: "OR with condition on right side is rejected", + searchQuery: "name='test' OR status.conditions.Reconciled='True'", + expectError: true, + }, + { + name: "OR with condition on left side is rejected", + searchQuery: "status.conditions.Available='True' OR name='test'", + expectError: true, + }, + { + name: "OR with conditions on both sides is rejected", + searchQuery: "status.conditions.Reconciled='True' OR status.conditions.Available='True'", + expectError: true, + }, + { + name: "Nested OR with condition is rejected", + searchQuery: "name='a' AND (region='us' OR status.conditions.Reconciled='True')", + expectError: true, + }, + { + name: "OR without conditions is allowed", + searchQuery: "name='test' OR region='us'", + expectedConditions: 0, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { RegisterTestingT(t) - tslTree, err := tsl.ParseTSL(tt.searchQuery) + tslTreeWrapper, err := tsl.ParseTSL(tt.searchQuery) Expect(err).ToNot(HaveOccurred()) - _, conditions, serviceErr := ExtractConditionQueries(tslTree, "clusters") + _, conditions, serviceErr := ExtractConditionQueries(tslTreeWrapper.Node) if tt.expectError { Expect(serviceErr).ToNot(BeNil()) + Expect(serviceErr.Error()).To(ContainSubstring("OR operator is not supported with condition queries")) return } @@ -609,16 +572,11 @@ func TestHasCondition(t *testing.T) { t.Run(tt.name, func(t *testing.T) { RegisterTestingT(t) - node := tsl.Node{ - Func: tsl.EqOp, - Left: tsl.Node{ - Func: tsl.IdentOp, - Left: tt.field, - }, - Right: tsl.Node{ - Func: tsl.StringOp, - Left: "value", - }, + node := &tsl.Node{ + Kind: tsl.KindBinaryExpr, + Operator: tsl.OpEQ, + Left: &tsl.Node{Kind: tsl.KindIdentifier, Value: tt.field}, + Right: &tsl.Node{Kind: tsl.KindStringLiteral, Value: "value"}, } result := hasCondition(node) @@ -724,66 +682,46 @@ func TestGetField_SpecDisallowed(t *testing.T) { Expect(err.Reason).To(ContainSubstring("not a valid field name")) } -func TestPreprocessSpecSubfields(t *testing.T) { +func TestGetField_PropertiesDisallowed(t *testing.T) { + RegisterTestingT(t) + + disallowed := map[string]string{"properties": "properties"} + + _, err := getField("properties.foo", disallowed) + Expect(err).ToNot(BeNil()) + Expect(err.Reason).To(ContainSubstring("not a valid field name")) +} + +func TestGetField_SpecNested(t *testing.T) { tests := []struct { name string input string expected string }{ { - name: "1-level path is unchanged", - input: "spec.region = 'us-east-1'", - expected: "spec.region = 'us-east-1'", - }, - { - name: "2-level path is encoded", - input: "spec.release.channel = 'dev'", - expected: "spec.release__channel = 'dev'", - }, - { - name: "3-level path is encoded", - input: "spec.release.config.zone = 'us-east-1a'", - expected: "spec.release__config__zone = 'us-east-1a'", - }, - { - name: "non-spec path is unchanged", - input: "labels.environment = 'production'", - expected: "labels.environment = 'production'", - }, - { - name: "spec path inside single quotes is not encoded", - input: "name='spec.release.channel'", - expected: "name='spec.release.channel'", - }, - { - name: "spec path inside double quotes is not encoded", - input: `name="spec.release.channel"`, - expected: `name="spec.release.channel"`, - }, - { - name: "multiple occurrences all encoded", - input: "spec.release.channel = 'dev' AND spec.release.version > 9", - expected: "spec.release__channel = 'dev' AND spec.release__version > 9", + name: "1-level: spec.region", + input: "spec.region", + expected: "spec->>'region'", }, { - name: "mixed quoted and unquoted", - input: "spec.release.channel = 'dev' AND name = 'spec.release.channel'", - expected: "spec.release__channel = 'dev' AND name = 'spec.release.channel'", + name: "2-level: spec.release.channel", + input: "spec.release.channel", + expected: "spec->'release'->>'channel'", }, { - name: "adjacent paren before spec path", - input: "(spec.release.channel = 'dev')", - expected: "(spec.release__channel = 'dev')", + name: "3-level: spec.release.config.zone", + input: "spec.release.config.zone", + expected: "spec->'release'->'config'->>'zone'", }, { - name: "empty input", - input: "", - expected: "", + name: "2-level with underscore in key: spec.release.image_v2", + input: "spec.release.image_v2", + expected: "spec->'release'->>'image_v2'", }, { - name: "no spec paths", - input: "name = 'my-cluster'", - expected: "name = 'my-cluster'", + name: "leading/trailing spaces are trimmed", + input: " spec.region ", + expected: "spec->>'region'", }, } @@ -791,141 +729,84 @@ func TestPreprocessSpecSubfields(t *testing.T) { t.Run(tt.name, func(t *testing.T) { RegisterTestingT(t) - result := PreprocessSpecSubfields(tt.input) - Expect(result).To(Equal(tt.expected)) + field, err := getField(tt.input, map[string]string{}) + Expect(err).To(BeNil()) + Expect(field).To(Equal(tt.expected)) }) } } -func TestWrapSpecNumericCasts(t *testing.T) { - // helper: build a comparison node with an ident LHS and a value RHS - identNode := func(field string) tsl.Node { - return tsl.Node{Func: tsl.IdentOp, Left: field} - } - numNode := func(v float64) tsl.Node { - return tsl.Node{Func: tsl.NumberOp, Left: v} - } - strNode := func(v string) tsl.Node { - return tsl.Node{Func: tsl.StringOp, Left: v} - } - cmpNode := func(op string, left, right tsl.Node) tsl.Node { - return tsl.Node{Func: op, Left: left, Right: right} - } +// TestFieldNameWalk_NumericCast verifies that FieldNameWalk applies CAST(... AS numeric) +// to spec JSONB fields when compared against a number. This logic was previously in a +// separate WrapSpecNumericCasts tree walk and is now integrated into FieldNameWalk. +func TestFieldNameWalk_NumericCast(t *testing.T) { + noDisallowed := map[string]string{} - checkIdent := func(t *testing.T, result tsl.Node, expected string) { + parseAndWalk := func(t *testing.T, search string) *tsl.Node { t.Helper() - leftNode, ok := result.Left.(tsl.Node) - Expect(ok).To(BeTrue()) - ident, ok := leftNode.Left.(string) - Expect(ok).To(BeTrue()) - Expect(ident).To(Equal(expected)) + tree, err := tsl.ParseTSL(search) + Expect(err).ToNot(HaveOccurred()) + result, serviceErr := FieldNameWalk(tree.Node, noDisallowed) + Expect(serviceErr).To(BeNil()) + return result } t.Run("spec field with numeric RHS — CAST applied", func(t *testing.T) { RegisterTestingT(t) - checkIdent(t, - WrapSpecNumericCasts(cmpNode(tsl.GtOp, identNode("spec->>'replicas'"), numNode(9))), - "CAST(spec->>'replicas' AS numeric)", - ) + result := parseAndWalk(t, "spec.replicas > 9") + Expect(result.Left.Value).To(Equal("CAST(spec->>'replicas' AS numeric)")) }) t.Run("nested spec field with numeric RHS — CAST applied", func(t *testing.T) { RegisterTestingT(t) - checkIdent(t, - WrapSpecNumericCasts(cmpNode(tsl.GtOp, identNode("spec->'release'->>'version'"), numNode(9))), - "CAST(spec->'release'->>'version' AS numeric)", - ) + result := parseAndWalk(t, "spec.release.version > 9") + Expect(result.Left.Value).To(Equal("CAST(spec->'release'->>'version' AS numeric)")) }) t.Run("spec field with string RHS — no CAST", func(t *testing.T) { RegisterTestingT(t) - checkIdent(t, - WrapSpecNumericCasts(cmpNode(tsl.EqOp, identNode("spec->>'channel'"), strNode("dev"))), - "spec->>'channel'", - ) + result := parseAndWalk(t, "spec.channel = 'dev'") + Expect(result.Left.Value).To(Equal("spec->>'channel'")) }) t.Run("non-spec field with numeric RHS — no CAST", func(t *testing.T) { RegisterTestingT(t) - checkIdent(t, - WrapSpecNumericCasts(cmpNode(tsl.GtOp, identNode("generation"), numNode(1))), - "generation", - ) + result := parseAndWalk(t, "generation > 1") + Expect(result.Left.Value).To(Equal("generation")) }) - t.Run("AND tree: only spec+numeric nodes get CAST", func(t *testing.T) { + t.Run("numeric LHS with spec field RHS — CAST applied", func(t *testing.T) { RegisterTestingT(t) + result := parseAndWalk(t, "9 < spec.replicas") + Expect(result.Right.Value).To(Equal("CAST(spec->>'replicas' AS numeric)")) + }) - // spec->>'replicas' > 9 AND generation > 1 AND spec->>'channel' = 'dev' - tree := cmpNode(tsl.AndOp, - cmpNode(tsl.AndOp, - cmpNode(tsl.GtOp, identNode("spec->>'replicas'"), numNode(9)), - cmpNode(tsl.GtOp, identNode("generation"), numNode(1)), - ), - cmpNode(tsl.EqOp, identNode("spec->>'channel'"), strNode("dev")), - ) - - result := WrapSpecNumericCasts(tree) - - // left AND subtree - andLeft := result.Left.(tsl.Node) - specNode := andLeft.Left.(tsl.Node) - specIdent := specNode.Left.(tsl.Node).Left.(string) - Expect(specIdent).To(Equal("CAST(spec->>'replicas' AS numeric)")) - - genNode := andLeft.Right.(tsl.Node) - genIdent := genNode.Left.(tsl.Node).Left.(string) - Expect(genIdent).To(Equal("generation")) // unchanged + t.Run("numeric LHS with nested spec field RHS — CAST applied", func(t *testing.T) { + RegisterTestingT(t) + result := parseAndWalk(t, "9 < spec.release.version") + Expect(result.Right.Value).To(Equal("CAST(spec->'release'->>'version' AS numeric)")) + }) - // right: string RHS — no CAST - chanNode := result.Right.(tsl.Node) - chanIdent := chanNode.Left.(tsl.Node).Left.(string) - Expect(chanIdent).To(Equal("spec->>'channel'")) // unchanged + t.Run("numeric LHS with non-spec field RHS — no CAST", func(t *testing.T) { + RegisterTestingT(t) + result := parseAndWalk(t, "1 < generation") + Expect(result.Right.Value).To(Equal("generation")) }) -} -func TestGetField_SpecNestedEncoded(t *testing.T) { - tests := []struct { - name string - input string - expected string - }{ - { - name: "1-level: spec.region", - input: "spec.region", - expected: "spec->>'region'", - }, - { - name: "2-level encoded: spec.release__channel", - input: "spec.release__channel", - expected: "spec->'release'->>'channel'", - }, - { - name: "3-level encoded: spec.release__config__zone", - input: "spec.release__config__zone", - expected: "spec->'release'->'config'->>'zone'", - }, - { - name: "2-level encoded with underscore in key: spec.release__image_v2", - input: "spec.release__image_v2", - expected: "spec->'release'->>'image_v2'", - }, - { - name: "leading/trailing spaces are trimmed", - input: " spec.region ", - expected: "spec->>'region'", - }, - } + t.Run("AND tree: only spec+numeric nodes get CAST", func(t *testing.T) { + RegisterTestingT(t) + result := parseAndWalk(t, "spec.replicas > 9 AND generation > 1 AND spec.channel = 'dev'") - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - RegisterTestingT(t) + andLeft := result.Left + specIdent := andLeft.Left.Left.Value.(string) + Expect(specIdent).To(Equal("CAST(spec->>'replicas' AS numeric)")) - field, err := getField(tt.input, map[string]string{}) - Expect(err).To(BeNil()) - Expect(field).To(Equal(tt.expected)) - }) - } + genIdent := andLeft.Right.Left.Value.(string) + Expect(genIdent).To(Equal("generation")) + + chanIdent := result.Right.Left.Value.(string) + Expect(chanIdent).To(Equal("spec->>'channel'")) + }) } func TestConditionStatusValidation(t *testing.T) { diff --git a/pkg/services/generic.go b/pkg/services/generic.go index 6ca416d0..9e7ccfa5 100755 --- a/pkg/services/generic.go +++ b/pkg/services/generic.go @@ -10,9 +10,8 @@ import ( "gorm.io/gorm" "github.com/Masterminds/squirrel" - "github.com/yaacov/tree-search-language/pkg/tsl" - "github.com/yaacov/tree-search-language/pkg/walkers/ident" - sqlFilter "github.com/yaacov/tree-search-language/pkg/walkers/sql" + "github.com/yaacov/tree-search-language/v6/pkg/tsl" + sqlFilter "github.com/yaacov/tree-search-language/v6/pkg/walkers/sql" "github.com/openshift-hyperfleet/hyperfleet-api/pkg/api" "github.com/openshift-hyperfleet/hyperfleet-api/pkg/dao" @@ -169,35 +168,24 @@ func (s *sqlGenericService) buildSearchValues( ) } - // Pre-process before TSL parsing — the parser only supports 3-part identifiers. - // Condition subfields: status.conditions.Reconciled.last_updated_time → 3-part encoding - // Spec deep paths: spec.a.b.c → spec.a__b__c (2 parts, decoded in getField) - preprocessedSearch := db.PreprocessConditionSubfields(listCtx.args.Search) - preprocessedSearch = db.PreprocessSpecSubfields(preprocessedSearch) - - // create the TSL tree - tslTree, err := tsl.ParseTSL(preprocessedSearch) + tslTreeWrapper, err := tsl.ParseTSL(listCtx.args.Search) if err != nil { return "", nil, errors.BadRequest("Failed to parse search query: %s", listCtx.args.Search) } + tslTree := tslTreeWrapper.Node // Extract condition queries (status.conditions.xxx) before field name mapping - // These require special JSONB containment handling that TSL doesn't support - tableName := (*d).GetTableName() - tslTree, conditionExprs, serviceErr := db.ExtractConditionQueries(tslTree, tableName) + tslTree, conditionExprs, serviceErr := db.ExtractConditionQueries(tslTree) if serviceErr != nil { return "", nil, serviceErr } - // apply field name mapping first (status.xxx -> status_xxx, labels.xxx -> labels->>'xxx') - // this must happen before treeWalkForRelatedTables to prevent treating "status" and "labels" as related resources + // apply field name mapping (status.xxx -> status_xxx, labels.xxx -> labels->>'xxx', spec.xxx -> spec->>'xxx') + // also wraps spec JSONB fields in CAST(... AS numeric) when compared against a number tslTree, serviceErr = db.FieldNameWalk(tslTree, *listCtx.disallowedFields) if serviceErr != nil { return "", nil, serviceErr } - // wrap spec field identifiers in CAST(... AS numeric) when compared against a number, - // so that multi-digit values sort correctly instead of using text ordering - tslTree = db.WrapSpecNumericCasts(tslTree) // find all related tables tslTree, serviceErr = s.treeWalkForRelatedTables(listCtx, tslTree, d) if serviceErr != nil { @@ -208,8 +196,8 @@ func (s *sqlGenericService) buildSearchValues( if serviceErr != nil { return "", nil, serviceErr } - // convert to sqlizer - sqlizer, serviceErr := s.treeWalkForSqlizer(listCtx, tslTree) + // convert to sqlizer — wrap back to TSLNode for v6's sql.Walk + sqlizer, serviceErr := s.treeWalkForSqlizer(listCtx, &tsl.TSLNode{Node: tslTree}) if serviceErr != nil { return "", nil, serviceErr } @@ -229,7 +217,6 @@ func (s *sqlGenericService) buildSearchValues( if err != nil { return "", nil, errors.GeneralError("%s", err.Error()) } - // Append condition to the main SQL with AND if sql == "" { sql = condSQL } else { @@ -337,13 +324,19 @@ func zeroSlice(i interface{}, cap int64) *errors.ServiceError { // (1) look up the related table by its 1st part - creator // (2) replace it by table name - creator.username -> accounts.username func (s *sqlGenericService) treeWalkForRelatedTables( - listCtx *listContext, tslTree tsl.Node, genericDao *dao.GenericDao, -) (tsl.Node, *errors.ServiceError) { + listCtx *listContext, tslTree *tsl.Node, genericDao *dao.GenericDao, +) (*tsl.Node, *errors.ServiceError) { resourceTable := (*genericDao).GetTableName() if listCtx.joins == nil { listCtx.joins = map[string]dao.TableRelation{} } walkFn := func(field string) (string, error) { + // After FieldNameWalk, JSONB-mapped fields (e.g. labels->>'env', + // spec->'release'->>'channel') are no longer relation paths. + // Skip dot-splitting for any already-mapped JSONB expression. + if strings.Contains(field, "->") { + return field, nil + } fieldParts := strings.Split(field, ".") if len(fieldParts) > 1 && fieldParts[0] != resourceTable { fieldName := fieldParts[0] @@ -365,7 +358,7 @@ func (s *sqlGenericService) treeWalkForRelatedTables( return field, nil } - tslTree, err := ident.Walk(tslTree, walkFn) + tslTree, err := db.IdentWalk(tslTree, walkFn) if err != nil { return tslTree, errors.BadRequest("%s", err.Error()) } @@ -376,9 +369,9 @@ func (s *sqlGenericService) treeWalkForRelatedTables( // prepend table name to these "free" identifiers since they could cause "ambiguous" errors func (s *sqlGenericService) treeWalkForAddingTableName( _ *listContext, - tslTree tsl.Node, + tslTree *tsl.Node, dao *dao.GenericDao, -) (tsl.Node, *errors.ServiceError) { +) (*tsl.Node, *errors.ServiceError) { resourceTable := (*dao).GetTableName() walkFn := func(field string) (string, error) { @@ -392,7 +385,7 @@ func (s *sqlGenericService) treeWalkForAddingTableName( return field, nil } - tslTree, err := ident.Walk(tslTree, walkFn) + tslTree, err := db.IdentWalk(tslTree, walkFn) if err != nil { return tslTree, errors.BadRequest("%s", err.Error()) } @@ -402,12 +395,8 @@ func (s *sqlGenericService) treeWalkForAddingTableName( func (s *sqlGenericService) treeWalkForSqlizer( _ *listContext, - tslTree tsl.Node, + tslTree *tsl.TSLNode, ) (squirrel.Sqlizer, *errors.ServiceError) { - // Note: FieldNameWalk is now called earlier in buildSearchValues to ensure field mapping - // happens before related table detection. No need to call it again here. - - // Convert the search tree into SQL [Squirrel] filter sqlizer, err := sqlFilter.Walk(tslTree) if err != nil { return nil, errors.BadRequest("%s", err.Error()) diff --git a/pkg/services/generic_test.go b/pkg/services/generic_test.go index 1a6b5f50..398f6708 100755 --- a/pkg/services/generic_test.go +++ b/pkg/services/generic_test.go @@ -9,7 +9,7 @@ import ( dbmocks "github.com/openshift-hyperfleet/hyperfleet-api/pkg/db/mocks" "github.com/onsi/gomega/types" - "github.com/yaacov/tree-search-language/pkg/tsl" + "github.com/yaacov/tree-search-language/v6/pkg/tsl" "github.com/openshift-hyperfleet/hyperfleet-api/pkg/api" "github.com/openshift-hyperfleet/hyperfleet-api/pkg/errors" @@ -26,13 +26,15 @@ func TestSQLTranslation(t *testing.T) { genericService := sqlGenericService{genericDao: g} // ill-formatted search should be rejected - tests := []map[string]interface{}{ + // Note: v6 accepts bare identifiers like "garbage" as valid expressions, + // so we use a truly unparseable input instead. + errorTests := []map[string]interface{}{ { - "search": "garbage", - "error": errors.CodeBadRequest + ": Failed to parse search query: garbage", + "search": "= = =", + "error": errors.CodeBadRequest + ": Failed to parse search query: = = =", }, } - for _, test := range tests { + for _, test := range errorTests { var list []api.Cluster search := test["search"].(string) errorMsg := test["error"].(string) @@ -48,13 +50,12 @@ func TestSQLTranslation(t *testing.T) { } // tests for sql parsing - tests = []map[string]interface{}{ + sqlTests := []map[string]interface{}{ { - "search": "username in ('ooo.openshift')", + "search": "username in ['ooo.openshift']", "sql": "username IN (?)", "values": ConsistOf("ooo.openshift"), }, - // Test status.conditions field mapping (use status.conditions.='' syntax for condition queries) // Test labels.xxx field mapping { "search": "labels.environment = 'production'", @@ -104,7 +105,7 @@ func TestSQLTranslation(t *testing.T) { "values": ConsistOf("cls-123"), }, } - for _, test := range tests { + for _, test := range sqlTests { var list []api.Cluster search := test["search"].(string) sqlReal := test["sql"].(string) @@ -113,17 +114,61 @@ func TestSQLTranslation(t *testing.T) { context.Background(), &ListArguments{Search: search}, &list, ) Expect(serviceErr).ToNot(HaveOccurred()) - // Mirror the production pipeline: pre-process spec deep paths before TSL parsing - preprocessed := db.PreprocessSpecSubfields(search) - tslTree, err := tsl.ParseTSL(preprocessed) + // v6 handles deep identifiers natively — no preprocessing needed + tslTreeWrapper, err := tsl.ParseTSL(search) Expect(err).ToNot(HaveOccurred()) - // Apply field name mapping (status.xxx -> status_xxx, labels.xxx -> labels->>'xxx') - // This must happen before converting to sqlizer + tslTree := tslTreeWrapper.Node + // Apply field name mapping (includes numeric CAST for spec fields) tslTree, serviceErr = db.FieldNameWalk(tslTree, *listCtx.disallowedFields) Expect(serviceErr).ToNot(HaveOccurred()) - // Wrap spec fields in CAST(... AS numeric) when compared against a number - tslTree = db.WrapSpecNumericCasts(tslTree) - sqlizer, serviceErr := genericService.treeWalkForSqlizer(listCtx, tslTree) + sqlizer, serviceErr := genericService.treeWalkForSqlizer(listCtx, &tsl.TSLNode{Node: tslTree}) + Expect(serviceErr).ToNot(HaveOccurred()) + sql, values, err := sqlizer.ToSql() + Expect(err).ToNot(HaveOccurred()) + Expect(sql).To(Equal(sqlReal)) + Expect(values).To(valuesReal) + } + + // Verify JSONB-mapped fields survive treeWalkForRelatedTables without + // being misclassified as related-resource paths (the "->" substring + // signals an already-mapped JSONB expression that should be skipped). + jsonbRelatedTableTests := []map[string]interface{}{ + { + "search": "labels.environment = 'production'", + "sql": "labels->>'environment' = ?", + "values": ConsistOf("production"), + }, + { + "search": "spec.release.version = '2'", + "sql": "spec->'release'->>'version' = ?", + "values": ConsistOf("2"), + }, + { + "search": "properties.owner = 'team_a'", + "sql": "properties ->> 'owner' = ?", + "values": ConsistOf("team_a"), + }, + } + for _, test := range jsonbRelatedTableTests { + var list []api.Cluster + search := test["search"].(string) + sqlReal := test["sql"].(string) + valuesReal := test["values"].(types.GomegaMatcher) + listCtx, _, serviceErr := genericService.newListContext( + context.Background(), &ListArguments{Search: search}, &list, + ) + Expect(serviceErr).ToNot(HaveOccurred()) + tslTreeWrapper, err := tsl.ParseTSL(search) + Expect(err).ToNot(HaveOccurred()) + tslTree := tslTreeWrapper.Node + tslTree, serviceErr = db.FieldNameWalk(tslTree, *listCtx.disallowedFields) + Expect(serviceErr).ToNot(HaveOccurred()) + d := g.GetInstanceDao(context.Background(), &api.Cluster{}) + tslTree, serviceErr = genericService.treeWalkForRelatedTables(listCtx, tslTree, &d) + Expect(serviceErr).ToNot(HaveOccurred(), "JSONB field should not be misclassified as related table: %s", search) + tslTree, serviceErr = genericService.treeWalkForAddingTableName(listCtx, tslTree, &d) + Expect(serviceErr).ToNot(HaveOccurred()) + sqlizer, serviceErr := genericService.treeWalkForSqlizer(listCtx, &tsl.TSLNode{Node: tslTree}) Expect(serviceErr).ToNot(HaveOccurred()) sql, values, err := sqlizer.ToSql() Expect(err).ToNot(HaveOccurred()) diff --git a/test/integration/clusters_test.go b/test/integration/clusters_test.go index d1e3aa73..b6256d85 100644 --- a/test/integration/clusters_test.go +++ b/test/integration/clusters_test.go @@ -226,7 +226,7 @@ func TestClusterListSearch(t *testing.T) { clusters, err := h.Factories.NewClustersList("bronto", 20) Expect(err).NotTo(HaveOccurred(), "Error creating test clusters: %v", err) - searchStr := fmt.Sprintf("id in ('%s')", clusters[0].ID) + searchStr := fmt.Sprintf("id in ['%s']", clusters[0].ID) search := openapi.SearchParams(searchStr) params := &openapi.GetClustersParams{ Search: &search, diff --git a/test/integration/node_pools_test.go b/test/integration/node_pools_test.go index 313f75c8..222e5de8 100644 --- a/test/integration/node_pools_test.go +++ b/test/integration/node_pools_test.go @@ -266,7 +266,7 @@ func TestNodePoolListSearch(t *testing.T) { nodePools, err := h.Factories.NewNodePoolsList("bronto", 20) Expect(err).NotTo(HaveOccurred(), "Error creating test nodepools: %v", err) - searchStr := fmt.Sprintf("id in ('%s')", nodePools[0].ID) + searchStr := fmt.Sprintf("id in ['%s']", nodePools[0].ID) search := openapi.SearchParams(searchStr) params := &openapi.GetNodePoolsParams{ Search: &search,