diff --git a/Cargo.lock b/Cargo.lock index 6ff12d4620..72558d3e70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3440,7 +3440,7 @@ dependencies = [ [[package]] name = "clickhouse-user-query" -version = "25.4.2" +version = "25.5.1" dependencies = [ "clickhouse 0.12.2", "serde", diff --git a/Cargo.toml b/Cargo.toml index ad71bad626..823d16cebe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] resolver = "2" -members = ["packages/common/api-helper/build","packages/common/api-helper/macros","packages/common/cache/build","packages/common/cache/result","packages/common/chirp-workflow/core","packages/common/chirp-workflow/macros","packages/common/chirp/client","packages/common/chirp/metrics","packages/common/chirp/perf","packages/common/chirp/types","packages/common/chirp/worker","packages/common/chirp/worker-attributes","packages/common/claims","packages/common/clickhouse-inserter","packages/common/config","packages/common/connection","packages/common/convert","packages/common/deno-embed","packages/common/env","packages/common/fdb-util","packages/common/formatted-error","packages/common/global-error","packages/common/health-checks","packages/common/kv-str","packages/common/logs","packages/common/metrics","packages/common/migrate","packages/common/nomad-util","packages/common/operation/core","packages/common/operation/macros","packages/common/pools","packages/common/redis-util","packages/common/runtime","packages/common/s3-util","packages/common/schemac","packages/common/server-cli","packages/common/service-discovery","packages/common/service-manager","packages/common/smithy-output/api-auth/rust","packages/common/smithy-output/api-auth/rust-server","packages/common/smithy-output/api-cf-verification/rust","packages/common/smithy-output/api-cf-verification/rust-server","packages/common/smithy-output/api-cloud/rust","packages/common/smithy-output/api-cloud/rust-server","packages/common/smithy-output/api-group/rust","packages/common/smithy-output/api-group/rust-server","packages/common/smithy-output/api-identity/rust","packages/common/smithy-output/api-identity/rust-server","packages/common/smithy-output/api-job/rust","packages/common/smithy-output/api-job/rust-server","packages/common/smithy-output/api-kv/rust","packages/common/smithy-output/api-kv/rust-server","packages/common/smithy-output/api-matchmaker/rust","packages/common/smithy-output/api-matchmaker/rust-server","packages/common/smithy-output/api-party/rust","packages/common/smithy-output/api-party/rust-server","packages/common/smithy-output/api-portal/rust","packages/common/smithy-output/api-portal/rust-server","packages/common/smithy-output/api-status/rust","packages/common/smithy-output/api-status/rust-server","packages/common/smithy-output/api-traefik-provider/rust","packages/common/smithy-output/api-traefik-provider/rust-server","packages/common/test","packages/common/test-images","packages/common/types-proto/build","packages/common/types-proto/core","packages/common/util/core","packages/common/util/macros","packages/common/util/search","packages/core/api/actor","packages/core/api/auth","packages/core/api/cf-verification","packages/core/api/cloud","packages/core/api/games","packages/core/api/group","packages/core/api/identity","packages/core/api/intercom","packages/core/api/job","packages/core/api/matchmaker","packages/core/api/monolith-edge","packages/core/api/monolith-public","packages/core/api/portal","packages/core/api/provision","packages/core/api/status","packages/core/api/traefik-provider","packages/core/api/ui","packages/core/infra/legacy/job-runner","packages/core/infra/schema-generator","packages/core/infra/server","packages/core/services/build","packages/core/services/build/ops/create","packages/core/services/build/ops/get","packages/core/services/build/ops/list-for-env","packages/core/services/build/ops/list-for-game","packages/core/services/build/standalone/default-create","packages/core/services/build/util","packages/core/services/captcha/ops/hcaptcha-config-get","packages/core/services/captcha/ops/hcaptcha-verify","packages/core/services/captcha/ops/request","packages/core/services/captcha/ops/turnstile-config-get","packages/core/services/captcha/ops/turnstile-verify","packages/core/services/captcha/ops/verify","packages/core/services/captcha/util","packages/core/services/cdn/ops/namespace-auth-user-remove","packages/core/services/cdn/ops/namespace-auth-user-update","packages/core/services/cdn/ops/namespace-create","packages/core/services/cdn/ops/namespace-domain-create","packages/core/services/cdn/ops/namespace-domain-remove","packages/core/services/cdn/ops/namespace-get","packages/core/services/cdn/ops/namespace-resolve-domain","packages/core/services/cdn/ops/ns-auth-type-set","packages/core/services/cdn/ops/ns-enable-domain-public-auth-set","packages/core/services/cdn/ops/site-create","packages/core/services/cdn/ops/site-get","packages/core/services/cdn/ops/site-list-for-game","packages/core/services/cdn/ops/version-get","packages/core/services/cdn/ops/version-prepare","packages/core/services/cdn/ops/version-publish","packages/core/services/cdn/util","packages/core/services/cdn/worker","packages/core/services/cf-custom-hostname/ops/get","packages/core/services/cf-custom-hostname/ops/list-for-namespace-id","packages/core/services/cf-custom-hostname/ops/resolve-hostname","packages/core/services/cf-custom-hostname/worker","packages/core/services/cloud/ops/device-link-create","packages/core/services/cloud/ops/game-config-create","packages/core/services/cloud/ops/game-config-get","packages/core/services/cloud/ops/game-token-create","packages/core/services/cloud/ops/namespace-create","packages/core/services/cloud/ops/namespace-get","packages/core/services/cloud/ops/namespace-token-development-create","packages/core/services/cloud/ops/namespace-token-public-create","packages/core/services/cloud/ops/version-get","packages/core/services/cloud/ops/version-publish","packages/core/services/cloud/standalone/default-create","packages/core/services/cloud/worker","packages/core/services/cluster","packages/core/services/cluster/standalone/datacenter-tls-renew","packages/core/services/cluster/standalone/default-update","packages/core/services/cluster/standalone/gc","packages/core/services/cluster/standalone/metrics-publish","packages/core/services/custom-user-avatar/ops/list-for-game","packages/core/services/custom-user-avatar/ops/upload-complete","packages/core/services/debug/ops/email-res","packages/core/services/dynamic-config","packages/core/services/email-verification/ops/complete","packages/core/services/email-verification/ops/create","packages/core/services/email/ops/send","packages/core/services/external/ops/request-validate","packages/core/services/external/worker","packages/core/services/faker/ops/build","packages/core/services/faker/ops/cdn-site","packages/core/services/faker/ops/game","packages/core/services/faker/ops/game-namespace","packages/core/services/faker/ops/game-version","packages/core/services/faker/ops/job-run","packages/core/services/faker/ops/job-template","packages/core/services/faker/ops/mm-lobby","packages/core/services/faker/ops/mm-lobby-row","packages/core/services/faker/ops/mm-player","packages/core/services/faker/ops/region","packages/core/services/faker/ops/team","packages/core/services/faker/ops/user","packages/core/services/game/ops/banner-upload-complete","packages/core/services/game/ops/create","packages/core/services/game/ops/get","packages/core/services/game/ops/list-all","packages/core/services/game/ops/list-for-team","packages/core/services/game/ops/logo-upload-complete","packages/core/services/game/ops/namespace-create","packages/core/services/game/ops/namespace-get","packages/core/services/game/ops/namespace-list","packages/core/services/game/ops/namespace-resolve-name-id","packages/core/services/game/ops/namespace-resolve-url","packages/core/services/game/ops/namespace-validate","packages/core/services/game/ops/namespace-version-history-list","packages/core/services/game/ops/namespace-version-set","packages/core/services/game/ops/recommend","packages/core/services/game/ops/resolve-name-id","packages/core/services/game/ops/resolve-namespace-id","packages/core/services/game/ops/token-development-validate","packages/core/services/game/ops/validate","packages/core/services/game/ops/version-create","packages/core/services/game/ops/version-get","packages/core/services/game/ops/version-list","packages/core/services/game/ops/version-validate","packages/core/services/ip/ops/info","packages/core/services/job-log/ops/read","packages/core/services/job-log/worker","packages/core/services/job-run","packages/core/services/job/standalone/gc","packages/core/services/job/util","packages/core/services/linode","packages/core/services/linode/standalone/gc","packages/core/services/load-test/standalone/api-cloud","packages/core/services/load-test/standalone/mm","packages/core/services/load-test/standalone/mm-sustain","packages/core/services/load-test/standalone/sqlx","packages/core/services/load-test/standalone/watch-requests","packages/core/services/mm-config/ops/game-get","packages/core/services/mm-config/ops/game-upsert","packages/core/services/mm-config/ops/lobby-group-get","packages/core/services/mm-config/ops/lobby-group-resolve-name-id","packages/core/services/mm-config/ops/lobby-group-resolve-version","packages/core/services/mm-config/ops/namespace-config-set","packages/core/services/mm-config/ops/namespace-config-validate","packages/core/services/mm-config/ops/namespace-create","packages/core/services/mm-config/ops/namespace-get","packages/core/services/mm-config/ops/version-get","packages/core/services/mm-config/ops/version-prepare","packages/core/services/mm-config/ops/version-publish","packages/core/services/mm/ops/dev-player-token-create","packages/core/services/mm/ops/lobby-find-fail","packages/core/services/mm/ops/lobby-find-lobby-query-list","packages/core/services/mm/ops/lobby-find-try-complete","packages/core/services/mm/ops/lobby-for-run-id","packages/core/services/mm/ops/lobby-get","packages/core/services/mm/ops/lobby-history","packages/core/services/mm/ops/lobby-idle-update","packages/core/services/mm/ops/lobby-list-for-namespace","packages/core/services/mm/ops/lobby-list-for-user-id","packages/core/services/mm/ops/lobby-player-count","packages/core/services/mm/ops/lobby-runtime-aggregate","packages/core/services/mm/ops/lobby-state-get","packages/core/services/mm/ops/player-count-for-namespace","packages/core/services/mm/ops/player-get","packages/core/services/mm/standalone/gc","packages/core/services/mm/util","packages/core/services/mm/worker","packages/core/services/monolith/standalone/worker","packages/core/services/monolith/standalone/workflow-worker","packages/core/services/nomad/standalone/monitor","packages/core/services/region/ops/get","packages/core/services/region/ops/list","packages/core/services/region/ops/list-for-game","packages/core/services/region/ops/recommend","packages/core/services/region/ops/resolve","packages/core/services/region/ops/resolve-for-game","packages/core/services/route","packages/core/services/server-spec","packages/core/services/team-invite/ops/get","packages/core/services/team-invite/worker","packages/core/services/team/ops/avatar-upload-complete","packages/core/services/team/ops/get","packages/core/services/team/ops/join-request-list","packages/core/services/team/ops/member-count","packages/core/services/team/ops/member-get","packages/core/services/team/ops/member-list","packages/core/services/team/ops/member-relationship-get","packages/core/services/team/ops/profile-validate","packages/core/services/team/ops/recommend","packages/core/services/team/ops/resolve-display-name","packages/core/services/team/ops/user-ban-get","packages/core/services/team/ops/user-ban-list","packages/core/services/team/ops/validate","packages/core/services/team/util","packages/core/services/team/worker","packages/core/services/telemetry/standalone/beacon","packages/core/services/tier","packages/core/services/token/ops/create","packages/core/services/token/ops/exchange","packages/core/services/token/ops/get","packages/core/services/token/ops/revoke","packages/core/services/upload/ops/complete","packages/core/services/upload/ops/file-list","packages/core/services/upload/ops/get","packages/core/services/upload/ops/list-for-user","packages/core/services/upload/ops/prepare","packages/core/services/upload/worker","packages/core/services/user","packages/core/services/user-identity/ops/create","packages/core/services/user-identity/ops/delete","packages/core/services/user-identity/ops/get","packages/core/services/user/ops/avatar-upload-complete","packages/core/services/user/ops/get","packages/core/services/user/ops/pending-delete-toggle","packages/core/services/user/ops/profile-validate","packages/core/services/user/ops/resolve-email","packages/core/services/user/ops/team-list","packages/core/services/user/ops/token-create","packages/core/services/user/standalone/delete-pending","packages/core/services/user/worker","packages/edge/api/actor","packages/edge/api/intercom","packages/edge/api/monolith-edge","packages/edge/api/monolith-public","packages/edge/api/traefik-provider","packages/edge/infra/client/actor-kv","packages/edge/infra/client/config","packages/edge/infra/client/container-runner","packages/edge/infra/client/echo","packages/edge/infra/client/isolate-v8-runner","packages/edge/infra/client/manager","packages/edge/infra/edge-server","packages/edge/infra/guard/core","packages/edge/infra/guard/server","packages/edge/services/monolith/standalone/workflow-worker","packages/edge/services/pegboard","packages/edge/services/pegboard/standalone/usage-metrics-publish","packages/edge/services/pegboard/standalone/ws","packages/toolchain/cli","packages/toolchain/js-utils-embed","packages/toolchain/toolchain","sdks/api/full/rust"] +members = ["packages/common/api-helper/build","packages/common/api-helper/macros","packages/common/cache/build","packages/common/cache/result","packages/common/chirp-workflow/core","packages/common/chirp-workflow/macros","packages/common/chirp/client","packages/common/chirp/metrics","packages/common/chirp/perf","packages/common/chirp/types","packages/common/chirp/worker","packages/common/chirp/worker-attributes","packages/common/claims","packages/common/clickhouse-inserter","packages/common/clickhouse-user-query","packages/common/config","packages/common/connection","packages/common/convert","packages/common/deno-embed","packages/common/env","packages/common/fdb-util","packages/common/formatted-error","packages/common/global-error","packages/common/health-checks","packages/common/kv-str","packages/common/logs","packages/common/metrics","packages/common/migrate","packages/common/nomad-util","packages/common/operation/core","packages/common/operation/macros","packages/common/pools","packages/common/redis-util","packages/common/runtime","packages/common/s3-util","packages/common/schemac","packages/common/server-cli","packages/common/service-discovery","packages/common/service-manager","packages/common/smithy-output/api-auth/rust","packages/common/smithy-output/api-auth/rust-server","packages/common/smithy-output/api-cf-verification/rust","packages/common/smithy-output/api-cf-verification/rust-server","packages/common/smithy-output/api-cloud/rust","packages/common/smithy-output/api-cloud/rust-server","packages/common/smithy-output/api-group/rust","packages/common/smithy-output/api-group/rust-server","packages/common/smithy-output/api-identity/rust","packages/common/smithy-output/api-identity/rust-server","packages/common/smithy-output/api-job/rust","packages/common/smithy-output/api-job/rust-server","packages/common/smithy-output/api-kv/rust","packages/common/smithy-output/api-kv/rust-server","packages/common/smithy-output/api-matchmaker/rust","packages/common/smithy-output/api-matchmaker/rust-server","packages/common/smithy-output/api-party/rust","packages/common/smithy-output/api-party/rust-server","packages/common/smithy-output/api-portal/rust","packages/common/smithy-output/api-portal/rust-server","packages/common/smithy-output/api-status/rust","packages/common/smithy-output/api-status/rust-server","packages/common/smithy-output/api-traefik-provider/rust","packages/common/smithy-output/api-traefik-provider/rust-server","packages/common/test","packages/common/test-images","packages/common/types-proto/build","packages/common/types-proto/core","packages/common/util/core","packages/common/util/macros","packages/common/util/search","packages/core/api/actor","packages/core/api/auth","packages/core/api/cf-verification","packages/core/api/cloud","packages/core/api/games","packages/core/api/group","packages/core/api/identity","packages/core/api/intercom","packages/core/api/job","packages/core/api/matchmaker","packages/core/api/monolith-edge","packages/core/api/monolith-public","packages/core/api/portal","packages/core/api/provision","packages/core/api/status","packages/core/api/traefik-provider","packages/core/api/ui","packages/core/infra/legacy/job-runner","packages/core/infra/schema-generator","packages/core/infra/server","packages/core/services/build","packages/core/services/build/ops/create","packages/core/services/build/ops/get","packages/core/services/build/ops/list-for-env","packages/core/services/build/ops/list-for-game","packages/core/services/build/standalone/default-create","packages/core/services/build/util","packages/core/services/captcha/ops/hcaptcha-config-get","packages/core/services/captcha/ops/hcaptcha-verify","packages/core/services/captcha/ops/request","packages/core/services/captcha/ops/turnstile-config-get","packages/core/services/captcha/ops/turnstile-verify","packages/core/services/captcha/ops/verify","packages/core/services/captcha/util","packages/core/services/cdn/ops/namespace-auth-user-remove","packages/core/services/cdn/ops/namespace-auth-user-update","packages/core/services/cdn/ops/namespace-create","packages/core/services/cdn/ops/namespace-domain-create","packages/core/services/cdn/ops/namespace-domain-remove","packages/core/services/cdn/ops/namespace-get","packages/core/services/cdn/ops/namespace-resolve-domain","packages/core/services/cdn/ops/ns-auth-type-set","packages/core/services/cdn/ops/ns-enable-domain-public-auth-set","packages/core/services/cdn/ops/site-create","packages/core/services/cdn/ops/site-get","packages/core/services/cdn/ops/site-list-for-game","packages/core/services/cdn/ops/version-get","packages/core/services/cdn/ops/version-prepare","packages/core/services/cdn/ops/version-publish","packages/core/services/cdn/util","packages/core/services/cdn/worker","packages/core/services/cf-custom-hostname/ops/get","packages/core/services/cf-custom-hostname/ops/list-for-namespace-id","packages/core/services/cf-custom-hostname/ops/resolve-hostname","packages/core/services/cf-custom-hostname/worker","packages/core/services/cloud/ops/device-link-create","packages/core/services/cloud/ops/game-config-create","packages/core/services/cloud/ops/game-config-get","packages/core/services/cloud/ops/game-token-create","packages/core/services/cloud/ops/namespace-create","packages/core/services/cloud/ops/namespace-get","packages/core/services/cloud/ops/namespace-token-development-create","packages/core/services/cloud/ops/namespace-token-public-create","packages/core/services/cloud/ops/version-get","packages/core/services/cloud/ops/version-publish","packages/core/services/cloud/standalone/default-create","packages/core/services/cloud/worker","packages/core/services/cluster","packages/core/services/cluster/standalone/datacenter-tls-renew","packages/core/services/cluster/standalone/default-update","packages/core/services/cluster/standalone/gc","packages/core/services/cluster/standalone/metrics-publish","packages/core/services/custom-user-avatar/ops/list-for-game","packages/core/services/custom-user-avatar/ops/upload-complete","packages/core/services/debug/ops/email-res","packages/core/services/dynamic-config","packages/core/services/email-verification/ops/complete","packages/core/services/email-verification/ops/create","packages/core/services/email/ops/send","packages/core/services/external/ops/request-validate","packages/core/services/external/worker","packages/core/services/faker/ops/build","packages/core/services/faker/ops/cdn-site","packages/core/services/faker/ops/game","packages/core/services/faker/ops/game-namespace","packages/core/services/faker/ops/game-version","packages/core/services/faker/ops/job-run","packages/core/services/faker/ops/job-template","packages/core/services/faker/ops/mm-lobby","packages/core/services/faker/ops/mm-lobby-row","packages/core/services/faker/ops/mm-player","packages/core/services/faker/ops/region","packages/core/services/faker/ops/team","packages/core/services/faker/ops/user","packages/core/services/game/ops/banner-upload-complete","packages/core/services/game/ops/create","packages/core/services/game/ops/get","packages/core/services/game/ops/list-all","packages/core/services/game/ops/list-for-team","packages/core/services/game/ops/logo-upload-complete","packages/core/services/game/ops/namespace-create","packages/core/services/game/ops/namespace-get","packages/core/services/game/ops/namespace-list","packages/core/services/game/ops/namespace-resolve-name-id","packages/core/services/game/ops/namespace-resolve-url","packages/core/services/game/ops/namespace-validate","packages/core/services/game/ops/namespace-version-history-list","packages/core/services/game/ops/namespace-version-set","packages/core/services/game/ops/recommend","packages/core/services/game/ops/resolve-name-id","packages/core/services/game/ops/resolve-namespace-id","packages/core/services/game/ops/token-development-validate","packages/core/services/game/ops/validate","packages/core/services/game/ops/version-create","packages/core/services/game/ops/version-get","packages/core/services/game/ops/version-list","packages/core/services/game/ops/version-validate","packages/core/services/ip/ops/info","packages/core/services/job-log/ops/read","packages/core/services/job-log/worker","packages/core/services/job-run","packages/core/services/job/standalone/gc","packages/core/services/job/util","packages/core/services/linode","packages/core/services/linode/standalone/gc","packages/core/services/load-test/standalone/api-cloud","packages/core/services/load-test/standalone/mm","packages/core/services/load-test/standalone/mm-sustain","packages/core/services/load-test/standalone/sqlx","packages/core/services/load-test/standalone/watch-requests","packages/core/services/mm-config/ops/game-get","packages/core/services/mm-config/ops/game-upsert","packages/core/services/mm-config/ops/lobby-group-get","packages/core/services/mm-config/ops/lobby-group-resolve-name-id","packages/core/services/mm-config/ops/lobby-group-resolve-version","packages/core/services/mm-config/ops/namespace-config-set","packages/core/services/mm-config/ops/namespace-config-validate","packages/core/services/mm-config/ops/namespace-create","packages/core/services/mm-config/ops/namespace-get","packages/core/services/mm-config/ops/version-get","packages/core/services/mm-config/ops/version-prepare","packages/core/services/mm-config/ops/version-publish","packages/core/services/mm/ops/dev-player-token-create","packages/core/services/mm/ops/lobby-find-fail","packages/core/services/mm/ops/lobby-find-lobby-query-list","packages/core/services/mm/ops/lobby-find-try-complete","packages/core/services/mm/ops/lobby-for-run-id","packages/core/services/mm/ops/lobby-get","packages/core/services/mm/ops/lobby-history","packages/core/services/mm/ops/lobby-idle-update","packages/core/services/mm/ops/lobby-list-for-namespace","packages/core/services/mm/ops/lobby-list-for-user-id","packages/core/services/mm/ops/lobby-player-count","packages/core/services/mm/ops/lobby-runtime-aggregate","packages/core/services/mm/ops/lobby-state-get","packages/core/services/mm/ops/player-count-for-namespace","packages/core/services/mm/ops/player-get","packages/core/services/mm/standalone/gc","packages/core/services/mm/util","packages/core/services/mm/worker","packages/core/services/monolith/standalone/worker","packages/core/services/monolith/standalone/workflow-worker","packages/core/services/nomad/standalone/monitor","packages/core/services/region/ops/get","packages/core/services/region/ops/list","packages/core/services/region/ops/list-for-game","packages/core/services/region/ops/recommend","packages/core/services/region/ops/resolve","packages/core/services/region/ops/resolve-for-game","packages/core/services/route","packages/core/services/server-spec","packages/core/services/team-invite/ops/get","packages/core/services/team-invite/worker","packages/core/services/team/ops/avatar-upload-complete","packages/core/services/team/ops/get","packages/core/services/team/ops/join-request-list","packages/core/services/team/ops/member-count","packages/core/services/team/ops/member-get","packages/core/services/team/ops/member-list","packages/core/services/team/ops/member-relationship-get","packages/core/services/team/ops/profile-validate","packages/core/services/team/ops/recommend","packages/core/services/team/ops/resolve-display-name","packages/core/services/team/ops/user-ban-get","packages/core/services/team/ops/user-ban-list","packages/core/services/team/ops/validate","packages/core/services/team/util","packages/core/services/team/worker","packages/core/services/telemetry/standalone/beacon","packages/core/services/tier","packages/core/services/token/ops/create","packages/core/services/token/ops/exchange","packages/core/services/token/ops/get","packages/core/services/token/ops/revoke","packages/core/services/upload/ops/complete","packages/core/services/upload/ops/file-list","packages/core/services/upload/ops/get","packages/core/services/upload/ops/list-for-user","packages/core/services/upload/ops/prepare","packages/core/services/upload/worker","packages/core/services/user","packages/core/services/user-identity/ops/create","packages/core/services/user-identity/ops/delete","packages/core/services/user-identity/ops/get","packages/core/services/user/ops/avatar-upload-complete","packages/core/services/user/ops/get","packages/core/services/user/ops/pending-delete-toggle","packages/core/services/user/ops/profile-validate","packages/core/services/user/ops/resolve-email","packages/core/services/user/ops/team-list","packages/core/services/user/ops/token-create","packages/core/services/user/standalone/delete-pending","packages/core/services/user/worker","packages/edge/api/actor","packages/edge/api/intercom","packages/edge/api/monolith-edge","packages/edge/api/monolith-public","packages/edge/api/traefik-provider","packages/edge/infra/client/actor-kv","packages/edge/infra/client/config","packages/edge/infra/client/container-runner","packages/edge/infra/client/echo","packages/edge/infra/client/isolate-v8-runner","packages/edge/infra/client/manager","packages/edge/infra/edge-server","packages/edge/infra/guard/core","packages/edge/infra/guard/server","packages/edge/services/monolith/standalone/workflow-worker","packages/edge/services/pegboard","packages/edge/services/pegboard/standalone/usage-metrics-publish","packages/edge/services/pegboard/standalone/ws","packages/toolchain/cli","packages/toolchain/js-utils-embed","packages/toolchain/toolchain","sdks/api/full/rust"] [workspace.package] version = "25.5.2" diff --git a/packages/common/clickhouse-user-query/src/builder.rs b/packages/common/clickhouse-user-query/src/builder.rs index 8b2c854e58..f3608bddb0 100644 --- a/packages/common/clickhouse-user-query/src/builder.rs +++ b/packages/common/clickhouse-user-query/src/builder.rs @@ -8,200 +8,289 @@ use crate::schema::{PropertyType, Schema}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UserDefinedQueryBuilder { - where_clause: String, - bind_values: Vec, + where_clause: String, + bind_values: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] enum BindValue { - Bool(bool), - String(String), - Number(f64), - ArrayString(Vec), + Bool(bool), + String(String), + Number(f64), + ArrayString(Vec), } impl UserDefinedQueryBuilder { - pub fn new(schema: &Schema, expr: &QueryExpr) -> Result { - let mut builder = QueryBuilder::new(schema); - let where_clause = builder.build_where_clause(expr)?; - - if where_clause.trim().is_empty() { - return Err(UserQueryError::EmptyQuery); - } - - Ok(Self { - where_clause, - bind_values: builder.bind_values, - }) - } - - pub fn bind_to(&self, mut query: Query) -> Query { - for bind_value in &self.bind_values { - query = match bind_value { - BindValue::Bool(v) => query.bind(*v), - BindValue::String(v) => query.bind(v), - BindValue::Number(v) => query.bind(*v), - BindValue::ArrayString(v) => query.bind(v), - }; - } - query - } - - pub fn where_expr(&self) -> &str { - &self.where_clause - } + pub fn new(schema: &Schema, expr: &QueryExpr) -> Result { + let mut builder = QueryBuilder::new(schema); + let where_clause = builder.build_where_clause(expr)?; + + if where_clause.trim().is_empty() { + return Err(UserQueryError::EmptyQuery); + } + + Ok(Self { + where_clause, + bind_values: builder.bind_values, + }) + } + + pub fn bind_to(&self, mut query: Query) -> Query { + for bind_value in &self.bind_values { + query = match bind_value { + BindValue::Bool(v) => query.bind(*v), + BindValue::String(v) => query.bind(v), + BindValue::Number(v) => query.bind(*v), + BindValue::ArrayString(v) => query.bind(v), + }; + } + query + } + + pub fn where_expr(&self) -> &str { + &self.where_clause + } } struct QueryBuilder<'a> { - schema: &'a Schema, - bind_values: Vec, + schema: &'a Schema, + bind_values: Vec, } impl<'a> QueryBuilder<'a> { - fn new(schema: &'a Schema) -> Self { - Self { - schema, - bind_values: Vec::new(), - } - } - - fn build_where_clause(&mut self, expr: &QueryExpr) -> Result { - match expr { - QueryExpr::And { exprs } => { - if exprs.is_empty() { - return Err(UserQueryError::EmptyQuery); - } - let clauses: Result> = exprs - .iter() - .map(|e| self.build_where_clause(e)) - .collect(); - Ok(format!("({})", clauses?.join(" AND "))) - } - QueryExpr::Or { exprs } => { - if exprs.is_empty() { - return Err(UserQueryError::EmptyQuery); - } - let clauses: Result> = exprs - .iter() - .map(|e| self.build_where_clause(e)) - .collect(); - Ok(format!("({})", clauses?.join(" OR "))) - } - QueryExpr::BoolEqual { property, subproperty, value } => { - self.validate_property_access(property, subproperty, &PropertyType::Bool)?; - let column = self.build_column_reference(property, subproperty)?; - self.bind_values.push(BindValue::Bool(*value)); - Ok(format!("{} = ?", column)) - } - QueryExpr::BoolNotEqual { property, subproperty, value } => { - self.validate_property_access(property, subproperty, &PropertyType::Bool)?; - let column = self.build_column_reference(property, subproperty)?; - self.bind_values.push(BindValue::Bool(*value)); - Ok(format!("{} != ?", column)) - } - QueryExpr::StringEqual { property, subproperty, value } => { - self.validate_property_access(property, subproperty, &PropertyType::String)?; - let column = self.build_column_reference(property, subproperty)?; - self.bind_values.push(BindValue::String(value.clone())); - Ok(format!("{} = ?", column)) - } - QueryExpr::StringNotEqual { property, subproperty, value } => { - self.validate_property_access(property, subproperty, &PropertyType::String)?; - let column = self.build_column_reference(property, subproperty)?; - self.bind_values.push(BindValue::String(value.clone())); - Ok(format!("{} != ?", column)) - } - QueryExpr::ArrayContains { property, subproperty, values } => { - if values.is_empty() { - return Err(UserQueryError::EmptyArrayValues("ArrayContains".to_string())); - } - self.validate_property_access(property, subproperty, &PropertyType::ArrayString)?; - let column = self.build_column_reference(property, subproperty)?; - self.bind_values.push(BindValue::ArrayString(values.clone())); - Ok(format!("hasAny({}, ?)", column)) - } - QueryExpr::ArrayDoesNotContain { property, subproperty, values } => { - if values.is_empty() { - return Err(UserQueryError::EmptyArrayValues("ArrayDoesNotContain".to_string())); - } - self.validate_property_access(property, subproperty, &PropertyType::ArrayString)?; - let column = self.build_column_reference(property, subproperty)?; - self.bind_values.push(BindValue::ArrayString(values.clone())); - Ok(format!("NOT hasAny({}, ?)", column)) - } - QueryExpr::NumberEqual { property, subproperty, value } => { - self.validate_property_access(property, subproperty, &PropertyType::Number)?; - let column = self.build_column_reference(property, subproperty)?; - self.bind_values.push(BindValue::Number(*value)); - Ok(format!("{} = ?", column)) - } - QueryExpr::NumberNotEqual { property, subproperty, value } => { - self.validate_property_access(property, subproperty, &PropertyType::Number)?; - let column = self.build_column_reference(property, subproperty)?; - self.bind_values.push(BindValue::Number(*value)); - Ok(format!("{} != ?", column)) - } - QueryExpr::NumberLess { property, subproperty, value } => { - self.validate_property_access(property, subproperty, &PropertyType::Number)?; - let column = self.build_column_reference(property, subproperty)?; - self.bind_values.push(BindValue::Number(*value)); - Ok(format!("{} < ?", column)) - } - QueryExpr::NumberLessOrEqual { property, subproperty, value } => { - self.validate_property_access(property, subproperty, &PropertyType::Number)?; - let column = self.build_column_reference(property, subproperty)?; - self.bind_values.push(BindValue::Number(*value)); - Ok(format!("{} <= ?", column)) - } - QueryExpr::NumberGreater { property, subproperty, value } => { - self.validate_property_access(property, subproperty, &PropertyType::Number)?; - let column = self.build_column_reference(property, subproperty)?; - self.bind_values.push(BindValue::Number(*value)); - Ok(format!("{} > ?", column)) - } - QueryExpr::NumberGreaterOrEqual { property, subproperty, value } => { - self.validate_property_access(property, subproperty, &PropertyType::Number)?; - let column = self.build_column_reference(property, subproperty)?; - self.bind_values.push(BindValue::Number(*value)); - Ok(format!("{} >= ?", column)) - } - } - } - - fn validate_property_access( - &self, - property: &str, - subproperty: &Option, - expected_type: &PropertyType, - ) -> Result<()> { - let prop = self.schema.get_property(property) - .ok_or_else(|| UserQueryError::PropertyNotFound(property.to_string()))?; - - if subproperty.is_some() && !prop.supports_subproperties { - return Err(UserQueryError::SubpropertiesNotSupported(property.to_string())); - } - - if &prop.ty != expected_type { - return Err(UserQueryError::PropertyTypeMismatch( - property.to_string(), - expected_type.type_name().to_string(), - prop.ty.type_name().to_string(), - )); - } - - Ok(()) - } - - fn build_column_reference(&self, property: &str, subproperty: &Option) -> Result { - let property_ident = Identifier(property); - - match subproperty { - Some(subprop) => { - // For ClickHouse Map access, use string literal syntax - Ok(format!("{}[{}]", property_ident.0, format!("'{}'", subprop.replace("'", "\\'")))) - } - None => Ok(property_ident.0.to_string()), - } - } -} + fn new(schema: &'a Schema) -> Self { + Self { + schema, + bind_values: Vec::new(), + } + } + + fn build_where_clause(&mut self, expr: &QueryExpr) -> Result { + match expr { + QueryExpr::And { exprs } => { + if exprs.is_empty() { + return Err(UserQueryError::EmptyQuery); + } + let clauses: Result> = + exprs.iter().map(|e| self.build_where_clause(e)).collect(); + Ok(format!("({})", clauses?.join(" AND "))) + } + QueryExpr::Or { exprs } => { + if exprs.is_empty() { + return Err(UserQueryError::EmptyQuery); + } + let clauses: Result> = + exprs.iter().map(|e| self.build_where_clause(e)).collect(); + Ok(format!("({})", clauses?.join(" OR "))) + } + QueryExpr::BoolEqual { + property, + subproperty, + value, + } => { + self.validate_property_access(property, subproperty, &PropertyType::Bool)?; + let column = self.build_column_reference(property, subproperty)?; + self.bind_values.push(BindValue::Bool(*value)); + Ok(format!("{} = ?", column)) + } + QueryExpr::BoolNotEqual { + property, + subproperty, + value, + } => { + self.validate_property_access(property, subproperty, &PropertyType::Bool)?; + let column = self.build_column_reference(property, subproperty)?; + self.bind_values.push(BindValue::Bool(*value)); + Ok(format!("{} != ?", column)) + } + QueryExpr::StringEqual { + property, + subproperty, + value, + } => { + self.validate_property_access(property, subproperty, &PropertyType::String)?; + let column = self.build_column_reference(property, subproperty)?; + self.bind_values.push(BindValue::String(value.clone())); + Ok(format!("{} = ?", column)) + } + QueryExpr::StringNotEqual { + property, + subproperty, + value, + } => { + self.validate_property_access(property, subproperty, &PropertyType::String)?; + let column = self.build_column_reference(property, subproperty)?; + self.bind_values.push(BindValue::String(value.clone())); + Ok(format!("{} != ?", column)) + } + QueryExpr::ArrayContains { + property, + subproperty, + values, + } => { + if values.is_empty() { + return Err(UserQueryError::EmptyArrayValues( + "ArrayContains".to_string(), + )); + } + self.validate_property_access(property, subproperty, &PropertyType::ArrayString)?; + let column = self.build_column_reference(property, subproperty)?; + self.bind_values + .push(BindValue::ArrayString(values.clone())); + Ok(format!("hasAny({}, ?)", column)) + } + QueryExpr::ArrayDoesNotContain { + property, + subproperty, + values, + } => { + if values.is_empty() { + return Err(UserQueryError::EmptyArrayValues( + "ArrayDoesNotContain".to_string(), + )); + } + self.validate_property_access(property, subproperty, &PropertyType::ArrayString)?; + let column = self.build_column_reference(property, subproperty)?; + self.bind_values + .push(BindValue::ArrayString(values.clone())); + Ok(format!("NOT hasAny({}, ?)", column)) + } + QueryExpr::NumberEqual { + property, + subproperty, + value, + } => { + self.validate_property_access(property, subproperty, &PropertyType::Number)?; + let column = self.build_column_reference(property, subproperty)?; + self.bind_values.push(BindValue::Number(*value)); + Ok(format!("{} = ?", column)) + } + QueryExpr::NumberNotEqual { + property, + subproperty, + value, + } => { + self.validate_property_access(property, subproperty, &PropertyType::Number)?; + let column = self.build_column_reference(property, subproperty)?; + self.bind_values.push(BindValue::Number(*value)); + Ok(format!("{} != ?", column)) + } + QueryExpr::NumberLess { + property, + subproperty, + value, + } => { + self.validate_property_access(property, subproperty, &PropertyType::Number)?; + let column = self.build_column_reference(property, subproperty)?; + self.bind_values.push(BindValue::Number(*value)); + Ok(format!("{} < ?", column)) + } + QueryExpr::NumberLessOrEqual { + property, + subproperty, + value, + } => { + self.validate_property_access(property, subproperty, &PropertyType::Number)?; + let column = self.build_column_reference(property, subproperty)?; + self.bind_values.push(BindValue::Number(*value)); + Ok(format!("{} <= ?", column)) + } + QueryExpr::NumberGreater { + property, + subproperty, + value, + } => { + self.validate_property_access(property, subproperty, &PropertyType::Number)?; + let column = self.build_column_reference(property, subproperty)?; + self.bind_values.push(BindValue::Number(*value)); + Ok(format!("{} > ?", column)) + } + QueryExpr::NumberGreaterOrEqual { + property, + subproperty, + value, + } => { + self.validate_property_access(property, subproperty, &PropertyType::Number)?; + let column = self.build_column_reference(property, subproperty)?; + self.bind_values.push(BindValue::Number(*value)); + Ok(format!("{} >= ?", column)) + } + } + } + + fn validate_property_access( + &self, + property: &str, + subproperty: &Option, + expected_type: &PropertyType, + ) -> Result<()> { + let prop = self + .schema + .get_property(property) + .ok_or_else(|| UserQueryError::PropertyNotFound(property.to_string()))?; + + if subproperty.is_some() && !prop.supports_subproperties { + return Err(UserQueryError::SubpropertiesNotSupported( + property.to_string(), + )); + } + + if &prop.ty != expected_type { + return Err(UserQueryError::PropertyTypeMismatch( + property.to_string(), + expected_type.type_name().to_string(), + prop.ty.type_name().to_string(), + )); + } + Ok(()) + } + + fn build_column_reference( + &self, + property: &str, + subproperty: &Option, + ) -> Result { + let property_ident = Identifier(property); + + match subproperty { + Some(subprop) => { + // Validate subproperty name for safe charset + Self::validate_subproperty_name(subprop)?; + + // For ClickHouse Map access, use string literal syntax + Ok(format!( + "{}[{}]", + property_ident.0, + format!("'{}'", subprop.replace("'", "\\'")) + )) + } + None => Ok(property_ident.0.to_string()), + } + } + + /// Validates that a subproperty name only contains safe characters for database queries + fn validate_subproperty_name(name: &str) -> Result<()> { + // Check if empty + if name.is_empty() { + return Err(UserQueryError::InvalidSubpropertyName(name.to_string())); + } + + // Check length (reasonable limit for database identifiers) + if name.len() > 64 { + return Err(UserQueryError::InvalidSubpropertyName(name.to_string())); + } + + // Only allow alphanumeric characters and underscores (SQL-safe) + if !name.chars().all(|c| c.is_alphanumeric() || c == '_') { + return Err(UserQueryError::InvalidSubpropertyName(name.to_string())); + } + + // Must not start with a number (SQL identifier rule) + if name.chars().next().unwrap().is_numeric() { + return Err(UserQueryError::InvalidSubpropertyName(name.to_string())); + } + + Ok(()) + } +} diff --git a/packages/common/clickhouse-user-query/src/error.rs b/packages/common/clickhouse-user-query/src/error.rs index 5dbdc30d53..b582616169 100644 --- a/packages/common/clickhouse-user-query/src/error.rs +++ b/packages/common/clickhouse-user-query/src/error.rs @@ -2,23 +2,26 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum UserQueryError { - #[error("Property '{0}' not found in schema")] - PropertyNotFound(String), - - #[error("Property '{0}' does not support subproperties")] - SubpropertiesNotSupported(String), - - #[error("Property '{0}' type mismatch: expected {1}, got {2}")] - PropertyTypeMismatch(String, String, String), - - #[error("Invalid property or subproperty name '{0}': must contain only alphanumeric characters and underscores")] - InvalidPropertyName(String), - - #[error("Empty query expression")] - EmptyQuery, - - #[error("Empty array values in {0} operation")] - EmptyArrayValues(String), + #[error("Property '{0}' not found in schema")] + PropertyNotFound(String), + + #[error("Property '{0}' does not support subproperties")] + SubpropertiesNotSupported(String), + + #[error("Property '{0}' type mismatch: expected {1}, got {2}")] + PropertyTypeMismatch(String, String, String), + + #[error("Invalid property name '{0}': must contain only alphanumeric characters and underscores, and cannot start with a number")] + InvalidPropertyName(String), + + #[error("Invalid subproperty name '{0}': must contain only alphanumeric characters and underscores, and cannot start with a number")] + InvalidSubpropertyName(String), + + #[error("Empty query expression")] + EmptyQuery, + + #[error("Empty array values in {0} operation")] + EmptyArrayValues(String), } -pub type Result = std::result::Result; \ No newline at end of file +pub type Result = std::result::Result; diff --git a/packages/common/clickhouse-user-query/src/lib.rs b/packages/common/clickhouse-user-query/src/lib.rs index be7896fec7..09c602a4ae 100644 --- a/packages/common/clickhouse-user-query/src/lib.rs +++ b/packages/common/clickhouse-user-query/src/lib.rs @@ -57,4 +57,4 @@ pub use schema::{Property, PropertyType, Schema}; pub mod builder; pub mod error; pub mod query; -pub mod schema; \ No newline at end of file +pub mod schema; diff --git a/packages/common/clickhouse-user-query/src/query.rs b/packages/common/clickhouse-user-query/src/query.rs index c6970e6525..9fb5489e28 100644 --- a/packages/common/clickhouse-user-query/src/query.rs +++ b/packages/common/clickhouse-user-query/src/query.rs @@ -3,71 +3,70 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum QueryExpr { - And { - exprs: Vec, - }, - Or { - exprs: Vec, - }, - BoolEqual { - property: String, - subproperty: Option, - value: bool, - }, - BoolNotEqual { - property: String, - subproperty: Option, - value: bool, - }, - StringEqual { - property: String, - subproperty: Option, - value: String, - }, - StringNotEqual { - property: String, - subproperty: Option, - value: String, - }, - ArrayContains { - property: String, - subproperty: Option, - values: Vec, - }, - ArrayDoesNotContain { - property: String, - subproperty: Option, - values: Vec, - }, - NumberEqual { - property: String, - subproperty: Option, - value: f64, - }, - NumberNotEqual { - property: String, - subproperty: Option, - value: f64, - }, - NumberLess { - property: String, - subproperty: Option, - value: f64, - }, - NumberLessOrEqual { - property: String, - subproperty: Option, - value: f64, - }, - NumberGreater { - property: String, - subproperty: Option, - value: f64, - }, - NumberGreaterOrEqual { - property: String, - subproperty: Option, - value: f64, - }, + And { + exprs: Vec, + }, + Or { + exprs: Vec, + }, + BoolEqual { + property: String, + subproperty: Option, + value: bool, + }, + BoolNotEqual { + property: String, + subproperty: Option, + value: bool, + }, + StringEqual { + property: String, + subproperty: Option, + value: String, + }, + StringNotEqual { + property: String, + subproperty: Option, + value: String, + }, + ArrayContains { + property: String, + subproperty: Option, + values: Vec, + }, + ArrayDoesNotContain { + property: String, + subproperty: Option, + values: Vec, + }, + NumberEqual { + property: String, + subproperty: Option, + value: f64, + }, + NumberNotEqual { + property: String, + subproperty: Option, + value: f64, + }, + NumberLess { + property: String, + subproperty: Option, + value: f64, + }, + NumberLessOrEqual { + property: String, + subproperty: Option, + value: f64, + }, + NumberGreater { + property: String, + subproperty: Option, + value: f64, + }, + NumberGreaterOrEqual { + property: String, + subproperty: Option, + value: f64, + }, } - diff --git a/packages/common/clickhouse-user-query/src/schema.rs b/packages/common/clickhouse-user-query/src/schema.rs index 3f45dea3e7..8938402710 100644 --- a/packages/common/clickhouse-user-query/src/schema.rs +++ b/packages/common/clickhouse-user-query/src/schema.rs @@ -3,70 +3,69 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Schema { - pub properties: Vec, + pub properties: Vec, } impl Schema { - pub fn new(properties: Vec) -> Result { - // All property validation happens in Property::new() - Ok(Self { properties }) - } - - pub fn get_property(&self, name: &str) -> Option<&Property> { - self.properties.iter().find(|p| p.name == name) - } + pub fn new(properties: Vec) -> Result { + // All property validation happens in Property::new() + Ok(Self { properties }) + } + + pub fn get_property(&self, name: &str) -> Option<&Property> { + self.properties.iter().find(|p| p.name == name) + } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Property { - pub name: String, - pub supports_subproperties: bool, - pub ty: PropertyType, + pub name: String, + pub supports_subproperties: bool, + pub ty: PropertyType, } impl Property { - pub fn new(name: String, supports_subproperties: bool, ty: PropertyType) -> Result { - validate_property_name(&name)?; - Ok(Self { - name, - supports_subproperties, - ty, - }) - } + pub fn new(name: String, supports_subproperties: bool, ty: PropertyType) -> Result { + validate_property_name(&name)?; + Ok(Self { + name, + supports_subproperties, + ty, + }) + } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum PropertyType { - Bool, - String, - Number, - ArrayString, + Bool, + String, + Number, + ArrayString, } impl PropertyType { - pub fn type_name(&self) -> &'static str { - match self { - PropertyType::Bool => "bool", - PropertyType::String => "string", - PropertyType::Number => "number", - PropertyType::ArrayString => "array[string]", - } - } + pub fn type_name(&self) -> &'static str { + match self { + PropertyType::Bool => "bool", + PropertyType::String => "string", + PropertyType::Number => "number", + PropertyType::ArrayString => "array[string]", + } + } } fn validate_property_name(name: &str) -> Result<()> { - if name.is_empty() { - return Err(UserQueryError::InvalidPropertyName(name.to_string())); - } - - if !name.chars().all(|c| c.is_alphanumeric() || c == '_') { - return Err(UserQueryError::InvalidPropertyName(name.to_string())); - } - - if name.chars().next().unwrap().is_numeric() { - return Err(UserQueryError::InvalidPropertyName(name.to_string())); - } - - Ok(()) -} + if name.is_empty() { + return Err(UserQueryError::InvalidPropertyName(name.to_string())); + } + + if !name.chars().all(|c| c.is_alphanumeric() || c == '_') { + return Err(UserQueryError::InvalidPropertyName(name.to_string())); + } + if name.chars().next().unwrap().is_numeric() { + return Err(UserQueryError::InvalidPropertyName(name.to_string())); + } + + Ok(()) +} diff --git a/packages/common/clickhouse-user-query/tests/builder_tests.rs b/packages/common/clickhouse-user-query/tests/builder_tests.rs index 71db71ce2f..fad3c2f40a 100644 --- a/packages/common/clickhouse-user-query/tests/builder_tests.rs +++ b/packages/common/clickhouse-user-query/tests/builder_tests.rs @@ -1,211 +1,364 @@ use clickhouse_user_query::*; fn create_test_schema() -> Schema { - Schema::new(vec![ - Property::new("prop_a".to_string(), false, PropertyType::String).unwrap(), - Property::new("prop_b".to_string(), true, PropertyType::String).unwrap(), - Property::new("bool_prop".to_string(), false, PropertyType::Bool).unwrap(), - Property::new("number_prop".to_string(), false, PropertyType::Number).unwrap(), - Property::new("array_prop".to_string(), false, PropertyType::ArrayString).unwrap(), - ]).unwrap() + Schema::new(vec![ + Property::new("prop_a".to_string(), false, PropertyType::String).unwrap(), + Property::new("prop_b".to_string(), true, PropertyType::String).unwrap(), + Property::new("bool_prop".to_string(), false, PropertyType::Bool).unwrap(), + Property::new("number_prop".to_string(), false, PropertyType::Number).unwrap(), + Property::new("array_prop".to_string(), false, PropertyType::ArrayString).unwrap(), + ]) + .unwrap() } #[test] fn test_simple_string_equal() { - let schema = create_test_schema(); - let query = QueryExpr::StringEqual { - property: "prop_a".to_string(), - subproperty: None, - value: "foo".to_string(), - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); - assert_eq!(builder.where_expr(), "prop_a = ?"); + let schema = create_test_schema(); + let query = QueryExpr::StringEqual { + property: "prop_a".to_string(), + subproperty: None, + value: "foo".to_string(), + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); + assert_eq!(builder.where_expr(), "prop_a = ?"); } #[test] fn test_subproperty_access() { - let schema = create_test_schema(); - let query = QueryExpr::StringEqual { - property: "prop_b".to_string(), - subproperty: Some("sub".to_string()), - value: "bar".to_string(), - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); - assert_eq!(builder.where_expr(), "prop_b['sub'] = ?"); + let schema = create_test_schema(); + let query = QueryExpr::StringEqual { + property: "prop_b".to_string(), + subproperty: Some("sub".to_string()), + value: "bar".to_string(), + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); + assert_eq!(builder.where_expr(), "prop_b['sub'] = ?"); } #[test] fn test_and_query() { - let schema = create_test_schema(); - let query = QueryExpr::And { - exprs: vec![ - QueryExpr::StringEqual { - property: "prop_a".to_string(), - subproperty: None, - value: "foo".to_string(), - }, - QueryExpr::BoolEqual { - property: "bool_prop".to_string(), - subproperty: None, - value: true, - }, - ], - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); - assert_eq!(builder.where_expr(), "(prop_a = ? AND bool_prop = ?)"); + let schema = create_test_schema(); + let query = QueryExpr::And { + exprs: vec![ + QueryExpr::StringEqual { + property: "prop_a".to_string(), + subproperty: None, + value: "foo".to_string(), + }, + QueryExpr::BoolEqual { + property: "bool_prop".to_string(), + subproperty: None, + value: true, + }, + ], + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); + assert_eq!(builder.where_expr(), "(prop_a = ? AND bool_prop = ?)"); } #[test] fn test_array_contains() { - let schema = create_test_schema(); - let query = QueryExpr::ArrayContains { - property: "array_prop".to_string(), - subproperty: None, - values: vec!["val1".to_string(), "val2".to_string()], - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); - assert_eq!(builder.where_expr(), "hasAny(array_prop, ?)"); + let schema = create_test_schema(); + let query = QueryExpr::ArrayContains { + property: "array_prop".to_string(), + subproperty: None, + values: vec!["val1".to_string(), "val2".to_string()], + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); + assert_eq!(builder.where_expr(), "hasAny(array_prop, ?)"); } #[test] fn test_property_not_found() { - let schema = create_test_schema(); - let query = QueryExpr::StringEqual { - property: "nonexistent".to_string(), - subproperty: None, - value: "foo".to_string(), - }; - - let result = UserDefinedQueryBuilder::new(&schema, &query); - assert!(matches!(result, Err(UserQueryError::PropertyNotFound(_)))); + let schema = create_test_schema(); + let query = QueryExpr::StringEqual { + property: "nonexistent".to_string(), + subproperty: None, + value: "foo".to_string(), + }; + + let result = UserDefinedQueryBuilder::new(&schema, &query); + assert!(matches!(result, Err(UserQueryError::PropertyNotFound(_)))); } #[test] fn test_type_mismatch() { - let schema = create_test_schema(); - let query = QueryExpr::BoolEqual { - property: "prop_a".to_string(), // This is a string property - subproperty: None, - value: true, - }; - - let result = UserDefinedQueryBuilder::new(&schema, &query); - assert!(matches!(result, Err(UserQueryError::PropertyTypeMismatch(_, _, _)))); + let schema = create_test_schema(); + let query = QueryExpr::BoolEqual { + property: "prop_a".to_string(), // This is a string property + subproperty: None, + value: true, + }; + + let result = UserDefinedQueryBuilder::new(&schema, &query); + assert!(matches!( + result, + Err(UserQueryError::PropertyTypeMismatch(_, _, _)) + )); } #[test] fn test_subproperties_not_supported() { - let schema = create_test_schema(); - let query = QueryExpr::StringEqual { - property: "prop_a".to_string(), // This doesn't support subproperties - subproperty: Some("sub".to_string()), - value: "foo".to_string(), - }; - - let result = UserDefinedQueryBuilder::new(&schema, &query); - assert!(matches!(result, Err(UserQueryError::SubpropertiesNotSupported(_)))); + let schema = create_test_schema(); + let query = QueryExpr::StringEqual { + property: "prop_a".to_string(), // This doesn't support subproperties + subproperty: Some("sub".to_string()), + value: "foo".to_string(), + }; + + let result = UserDefinedQueryBuilder::new(&schema, &query); + assert!(matches!( + result, + Err(UserQueryError::SubpropertiesNotSupported(_)) + )); } #[test] fn test_invalid_property_name() { - let schema = create_test_schema(); - let query = QueryExpr::StringEqual { - property: "prop-with-dashes".to_string(), - subproperty: None, - value: "foo".to_string(), - }; - - // Invalid property names are now caught as "not found" since schema validation - // happens at schema creation time, not query time - let builder_result = UserDefinedQueryBuilder::new(&schema, &query); - assert!(matches!(builder_result, Err(UserQueryError::PropertyNotFound(_)))); -} - -#[test] -fn test_subproperty_with_special_chars() { - let schema = create_test_schema(); - let query = QueryExpr::StringEqual { - property: "prop_b".to_string(), // This supports subproperties - subproperty: Some("sub-with-dashes".to_string()), - value: "foo".to_string(), - }; - - // Subproperties with special characters should work fine with Identifier escaping - let builder_result = UserDefinedQueryBuilder::new(&schema, &query); - assert!(builder_result.is_ok()); - - let builder = builder_result.unwrap(); - assert_eq!(builder.where_expr(), "prop_b['sub-with-dashes'] = ?"); + let schema = create_test_schema(); + let query = QueryExpr::StringEqual { + property: "prop-with-dashes".to_string(), + subproperty: None, + value: "foo".to_string(), + }; + + // Invalid property names are now caught as "not found" since schema validation + // happens at schema creation time, not query time + let builder_result = UserDefinedQueryBuilder::new(&schema, &query); + assert!(matches!( + builder_result, + Err(UserQueryError::PropertyNotFound(_)) + )); +} + +#[test] +fn test_subproperty_with_safe_chars() { + let schema = create_test_schema(); + let query = QueryExpr::StringEqual { + property: "prop_b".to_string(), // This supports subproperties + subproperty: Some("sub_with_underscores123".to_string()), + value: "foo".to_string(), + }; + + // Subproperties with safe characters (alphanumeric + underscore) should work + let builder_result = UserDefinedQueryBuilder::new(&schema, &query); + assert!(builder_result.is_ok()); + + let builder = builder_result.unwrap(); + assert_eq!( + builder.where_expr(), + "prop_b['sub_with_underscores123'] = ?" + ); } #[test] fn test_empty_array_values() { - let schema = create_test_schema(); - let query = QueryExpr::ArrayContains { - property: "array_prop".to_string(), - subproperty: None, - values: vec![], - }; - - let result = UserDefinedQueryBuilder::new(&schema, &query); - assert!(matches!(result, Err(UserQueryError::EmptyArrayValues(_)))); + let schema = create_test_schema(); + let query = QueryExpr::ArrayContains { + property: "array_prop".to_string(), + subproperty: None, + values: vec![], + }; + + let result = UserDefinedQueryBuilder::new(&schema, &query); + assert!(matches!(result, Err(UserQueryError::EmptyArrayValues(_)))); } #[test] fn test_number_greater() { - let schema = create_test_schema(); - let query = QueryExpr::NumberGreater { - property: "number_prop".to_string(), - subproperty: None, - value: 42.5, - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); - assert_eq!(builder.where_expr(), "number_prop > ?"); + let schema = create_test_schema(); + let query = QueryExpr::NumberGreater { + property: "number_prop".to_string(), + subproperty: None, + value: 42.5, + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); + assert_eq!(builder.where_expr(), "number_prop > ?"); } #[test] fn test_number_less_or_equal() { - let schema = create_test_schema(); - let query = QueryExpr::NumberLessOrEqual { - property: "number_prop".to_string(), - subproperty: None, - value: 100.0, - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); - assert_eq!(builder.where_expr(), "number_prop <= ?"); + let schema = create_test_schema(); + let query = QueryExpr::NumberLessOrEqual { + property: "number_prop".to_string(), + subproperty: None, + value: 100.0, + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); + assert_eq!(builder.where_expr(), "number_prop <= ?"); } #[test] fn test_number_with_subproperty() { - let schema = Schema::new(vec![ - Property::new("metrics".to_string(), true, PropertyType::Number).unwrap(), - ]).unwrap(); - - let query = QueryExpr::NumberEqual { - property: "metrics".to_string(), - subproperty: Some("score".to_string()), - value: 95.5, - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); - assert_eq!(builder.where_expr(), "metrics['score'] = ?"); + let schema = Schema::new(vec![Property::new( + "metrics".to_string(), + true, + PropertyType::Number, + ) + .unwrap()]) + .unwrap(); + + let query = QueryExpr::NumberEqual { + property: "metrics".to_string(), + subproperty: Some("score".to_string()), + value: 95.5, + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query).unwrap(); + assert_eq!(builder.where_expr(), "metrics['score'] = ?"); } #[test] fn test_number_type_mismatch() { - let schema = create_test_schema(); - let query = QueryExpr::NumberGreater { - property: "prop_a".to_string(), // This is a String type, not Number - subproperty: None, - value: 42.0, - }; - - let result = UserDefinedQueryBuilder::new(&schema, &query); - assert!(matches!(result, Err(UserQueryError::PropertyTypeMismatch(_, _, _)))); -} \ No newline at end of file + let schema = create_test_schema(); + let query = QueryExpr::NumberGreater { + property: "prop_a".to_string(), // This is a String type, not Number + subproperty: None, + value: 42.0, + }; + + let result = UserDefinedQueryBuilder::new(&schema, &query); + assert!(matches!( + result, + Err(UserQueryError::PropertyTypeMismatch(_, _, _)) + )); +} + +#[test] +fn test_subproperty_validation_valid_names() { + let schema = create_test_schema(); + + // Valid subproperty names + let valid_names = vec![ + "valid_name", + "valid123", + "name_with_underscore", + "CamelCase", + "a", + "a1", + "test_case_123", + ]; + + for name in valid_names { + let query = QueryExpr::StringEqual { + property: "prop_b".to_string(), + subproperty: Some(name.to_string()), + value: "test".to_string(), + }; + + let result = UserDefinedQueryBuilder::new(&schema, &query); + assert!( + result.is_ok(), + "Valid subproperty name '{}' should be accepted", + name + ); + } +} + +#[test] +fn test_subproperty_validation_invalid_names() { + let schema = create_test_schema(); + + // Invalid subproperty names + let long_name = "a".repeat(65); + let invalid_names = vec![ + "", // Empty + "123invalid", // Starts with number + "invalid-name", // Contains dash + "invalid.name", // Contains dot + "invalid name", // Contains space + "invalid@name", // Contains special character + "invalid'name", // Contains quote + "invalid\"name", // Contains double quote + "invalid;name", // Contains semicolon + "invalid(name", // Contains parenthesis + "invalid)name", // Contains parenthesis + "invalid[name", // Contains bracket + "invalid]name", // Contains bracket + "invalid{name", // Contains brace + "invalid}name", // Contains brace + "invalid+name", // Contains plus + "invalid=name", // Contains equals + "invalid*name", // Contains asterisk + "invalid%name", // Contains percent + "invalid$name", // Contains dollar + "invalid#name", // Contains hash + "invalid!name", // Contains exclamation + "invalid|name", // Contains pipe + "invalid\\name", // Contains backslash + "invalid/name", // Contains forward slash + "invalidname", // Contains greater than + "invalid?name", // Contains question mark + "invalid~name", // Contains tilde + "invalid`name", // Contains backtick + &long_name, // Too long (over 64 characters) + ]; + + for name in invalid_names { + let query = QueryExpr::StringEqual { + property: "prop_b".to_string(), + subproperty: Some(name.to_string()), + value: "test".to_string(), + }; + + let result = UserDefinedQueryBuilder::new(&schema, &query); + assert!( + result.is_err(), + "Invalid subproperty name '{}' should be rejected", + name + ); + assert!( + matches!(result, Err(UserQueryError::InvalidSubpropertyName(_))), + "Invalid subproperty name '{}' should return InvalidSubpropertyName error", + name + ); + } +} + +#[test] +fn test_subproperty_validation_sql_injection_attempts() { + let schema = create_test_schema(); + + // SQL injection attempts + let injection_attempts = vec![ + "'; DROP TABLE users; --", + "' OR '1'='1", + "'; INSERT INTO users VALUES ('hacker'); --", + "' UNION SELECT * FROM passwords --", + "'; DELETE FROM users; --", + "'/**/OR/**/1=1", + "' OR 1=1 #", + "admin'--", + "' OR 'x'='x", + "1' OR '1'='1", + "'; EXEC xp_cmdshell('dir'); --", + "'; SHUTDOWN; --", + ]; + + for attempt in injection_attempts { + let query = QueryExpr::StringEqual { + property: "prop_b".to_string(), + subproperty: Some(attempt.to_string()), + value: "test".to_string(), + }; + + let result = UserDefinedQueryBuilder::new(&schema, &query); + assert!( + result.is_err(), + "SQL injection attempt '{}' should be rejected", + attempt + ); + assert!( + matches!(result, Err(UserQueryError::InvalidSubpropertyName(_))), + "SQL injection attempt '{}' should return InvalidSubpropertyName error", + attempt + ); + } +} diff --git a/packages/common/clickhouse-user-query/tests/integration_tests.rs b/packages/common/clickhouse-user-query/tests/integration_tests.rs index b612afc190..8be27e552e 100644 --- a/packages/common/clickhouse-user-query/tests/integration_tests.rs +++ b/packages/common/clickhouse-user-query/tests/integration_tests.rs @@ -2,60 +2,67 @@ use clickhouse::{Client, Row}; use clickhouse_user_query::*; use serde::Deserialize; use serde_json; -use testcontainers::{runners::AsyncRunner, ContainerAsync, GenericImage, core::ContainerPort}; +use testcontainers::{core::ContainerPort, runners::AsyncRunner, ContainerAsync, GenericImage}; #[derive(Row, Deserialize)] struct UserRow { - id: String, + id: String, } struct TestSetup { - client: Client, - _container: ContainerAsync, + client: Client, + _container: ContainerAsync, } impl TestSetup { - async fn new() -> Self { - let clickhouse_image = GenericImage::new("clickhouse/clickhouse-server", "23.8-alpine") - .with_exposed_port(ContainerPort::Tcp(8123)) - .with_exposed_port(ContainerPort::Tcp(9000)); - - let container = clickhouse_image.start().await.expect("Failed to start ClickHouse container"); - - let port = container.get_host_port_ipv4(8123).await.expect("Failed to get port"); - let client = Client::default() - .with_url(format!("http://localhost:{}", port)); - - // Wait for ClickHouse to be ready and create test table - let setup = Self { - client, - _container: container, - }; - - // Wait for ClickHouse to fully start up - tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; - - setup.setup_test_data().await; - setup - } - - async fn setup_test_data(&self) { - // Create test table with sample data - self.client - .query("CREATE TABLE IF NOT EXISTS test_users ( + async fn new() -> Self { + let clickhouse_image = GenericImage::new("clickhouse/clickhouse-server", "23.8-alpine") + .with_exposed_port(ContainerPort::Tcp(8123)) + .with_exposed_port(ContainerPort::Tcp(9000)); + + let container = clickhouse_image + .start() + .await + .expect("Failed to start ClickHouse container"); + + let port = container + .get_host_port_ipv4(8123) + .await + .expect("Failed to get port"); + let client = Client::default().with_url(format!("http://localhost:{}", port)); + + // Wait for ClickHouse to be ready and create test table + let setup = Self { + client, + _container: container, + }; + + // Wait for ClickHouse to fully start up + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + + setup.setup_test_data().await; + setup + } + + async fn setup_test_data(&self) { + // Create test table with sample data + self.client + .query( + "CREATE TABLE IF NOT EXISTS test_users ( id String, active Bool, metadata Map(String, String), tags Array(String), age UInt32, score Float64 - ) ENGINE = Memory") - .execute() - .await - .expect("Failed to create test table"); - - // Insert test data - self.client + ) ENGINE = Memory", + ) + .execute() + .await + .expect("Failed to create test table"); + + // Insert test data + self.client .query("INSERT INTO test_users VALUES ('user1', true, {'region': 'us-east', 'tier': 'premium'}, ['verified', 'premium'], 25, 95.5), ('user2', false, {'region': 'us-west', 'tier': 'basic'}, ['basic'], 30, 67.2), @@ -63,357 +70,416 @@ impl TestSetup { .execute() .await .expect("Failed to insert test data"); - } + } } #[tokio::test] async fn test_simple_query_execution() { - let setup = TestSetup::new().await; - - // Create schema - let schema = Schema::new(vec![ - Property::new("active".to_string(), false, PropertyType::Bool).unwrap(), - ]).unwrap(); - - // Create query - let query_expr = QueryExpr::BoolEqual { - property: "active".to_string(), - subproperty: None, - value: true, - }; - - // Build query - let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); - - // Execute query - let query = setup.client.query(&format!("SELECT id FROM test_users WHERE {}", builder.where_expr())); - let query = builder.bind_to(query); - - let result: Vec = query - .fetch_all::() - .await - .expect("Query execution failed") - .into_iter() - .map(|user| user.id) - .collect(); - - // Should return user1 and user3 (active users) - assert_eq!(result.len(), 2); - assert!(result.contains(&"user1".to_string())); - assert!(result.contains(&"user3".to_string())); + let setup = TestSetup::new().await; + + // Create schema + let schema = Schema::new(vec![Property::new( + "active".to_string(), + false, + PropertyType::Bool, + ) + .unwrap()]) + .unwrap(); + + // Create query + let query_expr = QueryExpr::BoolEqual { + property: "active".to_string(), + subproperty: None, + value: true, + }; + + // Build query + let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); + + // Execute query + let query = setup.client.query(&format!( + "SELECT id FROM test_users WHERE {}", + builder.where_expr() + )); + let query = builder.bind_to(query); + + let result: Vec = query + .fetch_all::() + .await + .expect("Query execution failed") + .into_iter() + .map(|user| user.id) + .collect(); + + // Should return user1 and user3 (active users) + assert_eq!(result.len(), 2); + assert!(result.contains(&"user1".to_string())); + assert!(result.contains(&"user3".to_string())); } #[tokio::test] async fn test_subproperty_query_execution() { - let setup = TestSetup::new().await; - - // Create schema with map support - let schema = Schema::new(vec![ - Property::new("metadata".to_string(), true, PropertyType::String).unwrap(), - ]).unwrap(); - - // Query for premium tier users - let query_expr = QueryExpr::StringEqual { - property: "metadata".to_string(), - subproperty: Some("tier".to_string()), - value: "premium".to_string(), - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); - - let query = setup.client.query(&format!("SELECT id FROM test_users WHERE {}", builder.where_expr())); - let query = builder.bind_to(query); - - let result: Vec = query - .fetch_all::() - .await - .expect("Query execution failed") - .into_iter() - .map(|user| user.id) - .collect(); - - // Should return user1 and user3 (premium tier) - assert_eq!(result.len(), 2); - assert!(result.contains(&"user1".to_string())); - assert!(result.contains(&"user3".to_string())); + let setup = TestSetup::new().await; + + // Create schema with map support + let schema = Schema::new(vec![Property::new( + "metadata".to_string(), + true, + PropertyType::String, + ) + .unwrap()]) + .unwrap(); + + // Query for premium tier users + let query_expr = QueryExpr::StringEqual { + property: "metadata".to_string(), + subproperty: Some("tier".to_string()), + value: "premium".to_string(), + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); + + let query = setup.client.query(&format!( + "SELECT id FROM test_users WHERE {}", + builder.where_expr() + )); + let query = builder.bind_to(query); + + let result: Vec = query + .fetch_all::() + .await + .expect("Query execution failed") + .into_iter() + .map(|user| user.id) + .collect(); + + // Should return user1 and user3 (premium tier) + assert_eq!(result.len(), 2); + assert!(result.contains(&"user1".to_string())); + assert!(result.contains(&"user3".to_string())); } #[tokio::test] async fn test_array_contains_query_execution() { - let setup = TestSetup::new().await; - - // Create schema with array support - let schema = Schema::new(vec![ - Property::new("tags".to_string(), false, PropertyType::ArrayString).unwrap(), - ]).unwrap(); - - // Query for users with specific tags - let query_expr = QueryExpr::ArrayContains { - property: "tags".to_string(), - subproperty: None, - values: vec!["verified".to_string(), "beta".to_string()], - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); - - let query = setup.client.query(&format!("SELECT id FROM test_users WHERE {}", builder.where_expr())); - let query = builder.bind_to(query); - - let result: Vec = query - .fetch_all::() - .await - .expect("Query execution failed") - .into_iter() - .map(|user| user.id) - .collect(); - - // Should return user1 and user3 (have verified) and user3 (has beta) - assert_eq!(result.len(), 2); - assert!(result.contains(&"user1".to_string())); - assert!(result.contains(&"user3".to_string())); + let setup = TestSetup::new().await; + + // Create schema with array support + let schema = Schema::new(vec![Property::new( + "tags".to_string(), + false, + PropertyType::ArrayString, + ) + .unwrap()]) + .unwrap(); + + // Query for users with specific tags + let query_expr = QueryExpr::ArrayContains { + property: "tags".to_string(), + subproperty: None, + values: vec!["verified".to_string(), "beta".to_string()], + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); + + let query = setup.client.query(&format!( + "SELECT id FROM test_users WHERE {}", + builder.where_expr() + )); + let query = builder.bind_to(query); + + let result: Vec = query + .fetch_all::() + .await + .expect("Query execution failed") + .into_iter() + .map(|user| user.id) + .collect(); + + // Should return user1 and user3 (have verified) and user3 (has beta) + assert_eq!(result.len(), 2); + assert!(result.contains(&"user1".to_string())); + assert!(result.contains(&"user3".to_string())); } #[tokio::test] async fn test_complex_and_or_query_execution() { - let setup = TestSetup::new().await; - - // Create comprehensive schema - let schema = Schema::new(vec![ - Property::new("active".to_string(), false, PropertyType::Bool).unwrap(), - Property::new("metadata".to_string(), true, PropertyType::String).unwrap(), - Property::new("tags".to_string(), false, PropertyType::ArrayString).unwrap(), - ]).unwrap(); - - // Complex query: (active = true AND metadata['tier'] = 'premium') OR tags contains 'beta' - let query_expr = QueryExpr::Or { - exprs: vec![ - QueryExpr::And { - exprs: vec![ - QueryExpr::BoolEqual { - property: "active".to_string(), - subproperty: None, - value: true, - }, - QueryExpr::StringEqual { - property: "metadata".to_string(), - subproperty: Some("tier".to_string()), - value: "premium".to_string(), - }, - ], - }, - QueryExpr::ArrayContains { - property: "tags".to_string(), - subproperty: None, - values: vec!["beta".to_string()], - }, - ], - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); - - let query = setup.client.query(&format!("SELECT id FROM test_users WHERE {}", builder.where_expr())); - let query = builder.bind_to(query); - - let result: Vec = query - .fetch_all::() - .await - .expect("Query execution failed") - .into_iter() - .map(|user| user.id) - .collect(); - - // Should return: - // - user1 (active=true AND tier=premium) - // - user3 (active=true AND tier=premium AND has beta tag) - assert_eq!(result.len(), 2); - assert!(result.contains(&"user1".to_string())); - assert!(result.contains(&"user3".to_string())); + let setup = TestSetup::new().await; + + // Create comprehensive schema + let schema = Schema::new(vec![ + Property::new("active".to_string(), false, PropertyType::Bool).unwrap(), + Property::new("metadata".to_string(), true, PropertyType::String).unwrap(), + Property::new("tags".to_string(), false, PropertyType::ArrayString).unwrap(), + ]) + .unwrap(); + + // Complex query: (active = true AND metadata['tier'] = 'premium') OR tags contains 'beta' + let query_expr = QueryExpr::Or { + exprs: vec![ + QueryExpr::And { + exprs: vec![ + QueryExpr::BoolEqual { + property: "active".to_string(), + subproperty: None, + value: true, + }, + QueryExpr::StringEqual { + property: "metadata".to_string(), + subproperty: Some("tier".to_string()), + value: "premium".to_string(), + }, + ], + }, + QueryExpr::ArrayContains { + property: "tags".to_string(), + subproperty: None, + values: vec!["beta".to_string()], + }, + ], + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); + + let query = setup.client.query(&format!( + "SELECT id FROM test_users WHERE {}", + builder.where_expr() + )); + let query = builder.bind_to(query); + + let result: Vec = query + .fetch_all::() + .await + .expect("Query execution failed") + .into_iter() + .map(|user| user.id) + .collect(); + + // Should return: + // - user1 (active=true AND tier=premium) + // - user3 (active=true AND tier=premium AND has beta tag) + assert_eq!(result.len(), 2); + assert!(result.contains(&"user1".to_string())); + assert!(result.contains(&"user3".to_string())); } #[tokio::test] async fn test_sql_injection_protection() { - let setup = TestSetup::new().await; - - // Create schema - let schema = Schema::new(vec![ - Property::new("metadata".to_string(), true, PropertyType::String).unwrap(), - ]).unwrap(); - - // Attempt SQL injection in subproperty - let query_expr = QueryExpr::StringEqual { - property: "metadata".to_string(), - subproperty: Some("'; DROP TABLE test_users; --".to_string()), - value: "malicious".to_string(), - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); - - // Verify the query builds safely with proper escaping - let where_clause = builder.where_expr(); - assert!(where_clause.contains("metadata['\\'; DROP TABLE test_users; --']")); - assert!(where_clause.contains("= ?")); - - // Execute the query - it should run safely and return no results - let query = setup.client.query(&format!("SELECT id FROM test_users WHERE {}", builder.where_expr())); - let query = builder.bind_to(query); - - let result: Vec = query - .fetch_all::() - .await - .expect("Query execution should succeed safely") - .into_iter() - .map(|user| user.id) - .collect(); - - // Should return no results (not drop the table) - assert_eq!(result.len(), 0); - - // Verify table still exists by running a simple query - let table_check: Vec = setup.client - .query("SELECT id FROM test_users LIMIT 1") - .fetch_all::() - .await - .expect("Table should still exist") - .into_iter() - .map(|user| user.id) - .collect(); - - assert!(!table_check.is_empty(), "Table should not have been dropped"); + let setup = TestSetup::new().await; + + // Create schema + let schema = Schema::new(vec![Property::new( + "metadata".to_string(), + true, + PropertyType::String, + ) + .unwrap()]) + .unwrap(); + + // Attempt SQL injection in subproperty + let query_expr = QueryExpr::StringEqual { + property: "metadata".to_string(), + subproperty: Some("'; DROP TABLE test_users; --".to_string()), + value: "malicious".to_string(), + }; + + // The builder should reject the SQL injection attempt + let builder_result = UserDefinedQueryBuilder::new(&schema, &query_expr); + assert!(builder_result.is_err()); + assert!(matches!( + builder_result, + Err(UserQueryError::InvalidSubpropertyName(_)) + )); + + // Test with a valid subproperty name to ensure normal operation works + let safe_query_expr = QueryExpr::StringEqual { + property: "metadata".to_string(), + subproperty: Some("safe_key".to_string()), + value: "test_value".to_string(), + }; + + let safe_builder = UserDefinedQueryBuilder::new(&schema, &safe_query_expr); + assert!(safe_builder.is_ok()); + + let safe_builder = safe_builder.unwrap(); + let where_clause = safe_builder.where_expr(); + assert_eq!(where_clause, "metadata['safe_key'] = ?"); + + // Execute a safe query to verify the table is still accessible + let query = setup.client.query(&format!( + "SELECT id FROM test_users WHERE {}", + safe_builder.where_expr() + )); + let query = safe_builder.bind_to(query); + + let result: Vec = query + .fetch_all::() + .await + .expect("Safe query execution should succeed") + .into_iter() + .map(|user| user.id) + .collect(); + + // Should return no results (since we're looking for a non-existent metadata key) + assert_eq!(result.len(), 0); } #[tokio::test] async fn test_json_serialization_roundtrip() { - let setup = TestSetup::new().await; - - // Create schema - let schema = Schema::new(vec![ - Property::new("active".to_string(), false, PropertyType::Bool).unwrap(), - Property::new("metadata".to_string(), true, PropertyType::String).unwrap(), - ]).unwrap(); - - // Create complex query - let original_query = QueryExpr::And { - exprs: vec![ - QueryExpr::BoolEqual { - property: "active".to_string(), - subproperty: None, - value: true, - }, - QueryExpr::StringEqual { - property: "metadata".to_string(), - subproperty: Some("tier".to_string()), - value: "premium".to_string(), - }, - ], - }; - - // Serialize to JSON - let json = serde_json::to_string(&original_query).unwrap(); - - // Deserialize from JSON - let deserialized_query: QueryExpr = serde_json::from_str(&json).unwrap(); - - // Build queries from both and verify they're identical - let original_builder = UserDefinedQueryBuilder::new(&schema, &original_query).unwrap(); - let deserialized_builder = UserDefinedQueryBuilder::new(&schema, &deserialized_query).unwrap(); - - assert_eq!(original_builder.where_expr(), deserialized_builder.where_expr()); - - // Execute both queries and verify results are the same - let query1 = setup.client.query(&format!("SELECT id FROM test_users WHERE {}", original_builder.where_expr())); - let query1 = original_builder.bind_to(query1); - - let query2 = setup.client.query(&format!("SELECT id FROM test_users WHERE {}", deserialized_builder.where_expr())); - let query2 = deserialized_builder.bind_to(query2); - - let result1: Vec = query1 - .fetch_all::() - .await - .unwrap() - .into_iter() - .map(|user| user.id) - .collect(); - - let result2: Vec = query2 - .fetch_all::() - .await - .unwrap() - .into_iter() - .map(|user| user.id) - .collect(); - - assert_eq!(result1, result2); - assert_eq!(result1.len(), 2); // user1 and user3 + let setup = TestSetup::new().await; + + // Create schema + let schema = Schema::new(vec![ + Property::new("active".to_string(), false, PropertyType::Bool).unwrap(), + Property::new("metadata".to_string(), true, PropertyType::String).unwrap(), + ]) + .unwrap(); + + // Create complex query + let original_query = QueryExpr::And { + exprs: vec![ + QueryExpr::BoolEqual { + property: "active".to_string(), + subproperty: None, + value: true, + }, + QueryExpr::StringEqual { + property: "metadata".to_string(), + subproperty: Some("tier".to_string()), + value: "premium".to_string(), + }, + ], + }; + + // Serialize to JSON + let json = serde_json::to_string(&original_query).unwrap(); + + // Deserialize from JSON + let deserialized_query: QueryExpr = serde_json::from_str(&json).unwrap(); + + // Build queries from both and verify they're identical + let original_builder = UserDefinedQueryBuilder::new(&schema, &original_query).unwrap(); + let deserialized_builder = UserDefinedQueryBuilder::new(&schema, &deserialized_query).unwrap(); + + assert_eq!( + original_builder.where_expr(), + deserialized_builder.where_expr() + ); + + // Execute both queries and verify results are the same + let query1 = setup.client.query(&format!( + "SELECT id FROM test_users WHERE {}", + original_builder.where_expr() + )); + let query1 = original_builder.bind_to(query1); + + let query2 = setup.client.query(&format!( + "SELECT id FROM test_users WHERE {}", + deserialized_builder.where_expr() + )); + let query2 = deserialized_builder.bind_to(query2); + + let result1: Vec = query1 + .fetch_all::() + .await + .unwrap() + .into_iter() + .map(|user| user.id) + .collect(); + + let result2: Vec = query2 + .fetch_all::() + .await + .unwrap() + .into_iter() + .map(|user| user.id) + .collect(); + + assert_eq!(result1, result2); + assert_eq!(result1.len(), 2); // user1 and user3 } #[tokio::test] async fn test_numeric_query_execution() { - let setup = TestSetup::new().await; - - // Create schema with number support - let schema = Schema::new(vec![ - Property::new("score".to_string(), false, PropertyType::Number).unwrap(), - ]).unwrap(); - - // Query for users with score greater than 80 - let query_expr = QueryExpr::NumberGreater { - property: "score".to_string(), - subproperty: None, - value: 80.0, - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); - - let query = setup.client.query(&format!("SELECT id FROM test_users WHERE {}", builder.where_expr())); - let query = builder.bind_to(query); - - let result: Vec = query - .fetch_all::() - .await - .expect("Query execution failed") - .into_iter() - .map(|user| user.id) - .collect(); - - // Should return user1 (95.5) and user3 (88.9), but not user2 (67.2) - assert_eq!(result.len(), 2); - assert!(result.contains(&"user1".to_string())); - assert!(result.contains(&"user3".to_string())); - assert!(!result.contains(&"user2".to_string())); + let setup = TestSetup::new().await; + + // Create schema with number support + let schema = Schema::new(vec![Property::new( + "score".to_string(), + false, + PropertyType::Number, + ) + .unwrap()]) + .unwrap(); + + // Query for users with score greater than 80 + let query_expr = QueryExpr::NumberGreater { + property: "score".to_string(), + subproperty: None, + value: 80.0, + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); + + let query = setup.client.query(&format!( + "SELECT id FROM test_users WHERE {}", + builder.where_expr() + )); + let query = builder.bind_to(query); + + let result: Vec = query + .fetch_all::() + .await + .expect("Query execution failed") + .into_iter() + .map(|user| user.id) + .collect(); + + // Should return user1 (95.5) and user3 (88.9), but not user2 (67.2) + assert_eq!(result.len(), 2); + assert!(result.contains(&"user1".to_string())); + assert!(result.contains(&"user3".to_string())); + assert!(!result.contains(&"user2".to_string())); } #[tokio::test] async fn test_numeric_less_or_equal_query() { - let setup = TestSetup::new().await; - - // Create schema with number support - let schema = Schema::new(vec![ - Property::new("score".to_string(), false, PropertyType::Number).unwrap(), - ]).unwrap(); - - // Query for users with score <= 90 - let query_expr = QueryExpr::NumberLessOrEqual { - property: "score".to_string(), - subproperty: None, - value: 90.0, - }; - - let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); - - let query = setup.client.query(&format!("SELECT id FROM test_users WHERE {}", builder.where_expr())); - let query = builder.bind_to(query); - - let result: Vec = query - .fetch_all::() - .await - .expect("Query execution failed") - .into_iter() - .map(|user| user.id) - .collect(); - - // Should return user2 (67.2) and user3 (88.9), but not user1 (95.5) - assert_eq!(result.len(), 2); - assert!(result.contains(&"user2".to_string())); - assert!(result.contains(&"user3".to_string())); - assert!(!result.contains(&"user1".to_string())); -} \ No newline at end of file + let setup = TestSetup::new().await; + + // Create schema with number support + let schema = Schema::new(vec![Property::new( + "score".to_string(), + false, + PropertyType::Number, + ) + .unwrap()]) + .unwrap(); + + // Query for users with score <= 90 + let query_expr = QueryExpr::NumberLessOrEqual { + property: "score".to_string(), + subproperty: None, + value: 90.0, + }; + + let builder = UserDefinedQueryBuilder::new(&schema, &query_expr).unwrap(); + + let query = setup.client.query(&format!( + "SELECT id FROM test_users WHERE {}", + builder.where_expr() + )); + let query = builder.bind_to(query); + + let result: Vec = query + .fetch_all::() + .await + .expect("Query execution failed") + .into_iter() + .map(|user| user.id) + .collect(); + + // Should return user2 (67.2) and user3 (88.9), but not user1 (95.5) + assert_eq!(result.len(), 2); + assert!(result.contains(&"user2".to_string())); + assert!(result.contains(&"user3".to_string())); + assert!(!result.contains(&"user1".to_string())); +} diff --git a/packages/common/clickhouse-user-query/tests/query_tests.rs b/packages/common/clickhouse-user-query/tests/query_tests.rs index a5834df7b5..ea162d4033 100644 --- a/packages/common/clickhouse-user-query/tests/query_tests.rs +++ b/packages/common/clickhouse-user-query/tests/query_tests.rs @@ -2,164 +2,178 @@ use clickhouse_user_query::*; #[test] fn test_query_expr_serde() { - let query = QueryExpr::StringEqual { - property: "user_id".to_string(), - subproperty: None, - value: "12345".to_string(), - }; - - // Test serialization - let json = serde_json::to_string_pretty(&query).unwrap(); - - assert!(json.contains(r#""string_equal""#)); - assert!(json.contains(r#""property": "user_id""#)); - assert!(json.contains(r#""value": "12345""#)); - - // Test deserialization - let deserialized: QueryExpr = serde_json::from_str(&json).unwrap(); - match deserialized { - QueryExpr::StringEqual { property, value, .. } => { - assert_eq!(property, "user_id"); - assert_eq!(value, "12345"); - } - _ => panic!("Expected StringEqual"), - } + let query = QueryExpr::StringEqual { + property: "user_id".to_string(), + subproperty: None, + value: "12345".to_string(), + }; + + // Test serialization + let json = serde_json::to_string_pretty(&query).unwrap(); + + assert!(json.contains(r#""string_equal""#)); + assert!(json.contains(r#""property": "user_id""#)); + assert!(json.contains(r#""value": "12345""#)); + + // Test deserialization + let deserialized: QueryExpr = serde_json::from_str(&json).unwrap(); + match deserialized { + QueryExpr::StringEqual { + property, value, .. + } => { + assert_eq!(property, "user_id"); + assert_eq!(value, "12345"); + } + _ => panic!("Expected StringEqual"), + } } #[test] fn test_complex_query_serde() { - let query = QueryExpr::And { - exprs: vec![ - QueryExpr::BoolEqual { - property: "active".to_string(), - subproperty: None, - value: true, - }, - QueryExpr::ArrayContains { - property: "tags".to_string(), - subproperty: Some("category".to_string()), - values: vec!["premium".to_string(), "verified".to_string()], - }, - ], - }; - - let json = serde_json::to_string_pretty(&query).unwrap(); - - assert!(json.contains(r#""and""#)); - assert!(json.contains(r#""bool_equal""#)); - assert!(json.contains(r#""array_contains""#)); - - let deserialized: QueryExpr = serde_json::from_str(&json).unwrap(); - match deserialized { - QueryExpr::And { exprs } => { - assert_eq!(exprs.len(), 2); - } - _ => panic!("Expected And expression"), - } + let query = QueryExpr::And { + exprs: vec![ + QueryExpr::BoolEqual { + property: "active".to_string(), + subproperty: None, + value: true, + }, + QueryExpr::ArrayContains { + property: "tags".to_string(), + subproperty: Some("category".to_string()), + values: vec!["premium".to_string(), "verified".to_string()], + }, + ], + }; + + let json = serde_json::to_string_pretty(&query).unwrap(); + + assert!(json.contains(r#""and""#)); + assert!(json.contains(r#""bool_equal""#)); + assert!(json.contains(r#""array_contains""#)); + + let deserialized: QueryExpr = serde_json::from_str(&json).unwrap(); + match deserialized { + QueryExpr::And { exprs } => { + assert_eq!(exprs.len(), 2); + } + _ => panic!("Expected And expression"), + } } #[test] fn test_query_expr_creation() { - let query = QueryExpr::And { - exprs: vec![ - QueryExpr::StringEqual { - property: "user_id".to_string(), - subproperty: None, - value: "12345".to_string(), - }, - QueryExpr::BoolEqual { - property: "active".to_string(), - subproperty: None, - value: true, - }, - ], - }; - - match query { - QueryExpr::And { exprs } => { - assert_eq!(exprs.len(), 2); - } - _ => panic!("Expected And expression"), - } + let query = QueryExpr::And { + exprs: vec![ + QueryExpr::StringEqual { + property: "user_id".to_string(), + subproperty: None, + value: "12345".to_string(), + }, + QueryExpr::BoolEqual { + property: "active".to_string(), + subproperty: None, + value: true, + }, + ], + }; + + match query { + QueryExpr::And { exprs } => { + assert_eq!(exprs.len(), 2); + } + _ => panic!("Expected And expression"), + } } #[test] fn test_subproperty_query() { - let query = QueryExpr::StringEqual { - property: "metadata".to_string(), - subproperty: Some("key".to_string()), - value: "value".to_string(), - }; - - match query { - QueryExpr::StringEqual { property, subproperty, value } => { - assert_eq!(property, "metadata"); - assert_eq!(subproperty, Some("key".to_string())); - assert_eq!(value, "value"); - } - _ => panic!("Expected StringEqual expression"), - } + let query = QueryExpr::StringEqual { + property: "metadata".to_string(), + subproperty: Some("key".to_string()), + value: "value".to_string(), + }; + + match query { + QueryExpr::StringEqual { + property, + subproperty, + value, + } => { + assert_eq!(property, "metadata"); + assert_eq!(subproperty, Some("key".to_string())); + assert_eq!(value, "value"); + } + _ => panic!("Expected StringEqual expression"), + } } #[test] fn test_array_query() { - let query = QueryExpr::ArrayContains { - property: "tags".to_string(), - subproperty: None, - values: vec!["premium".to_string(), "verified".to_string()], - }; - - match query { - QueryExpr::ArrayContains { property, values, .. } => { - assert_eq!(property, "tags"); - assert_eq!(values.len(), 2); - assert!(values.contains(&"premium".to_string())); - } - _ => panic!("Expected ArrayContains expression"), - } + let query = QueryExpr::ArrayContains { + property: "tags".to_string(), + subproperty: None, + values: vec!["premium".to_string(), "verified".to_string()], + }; + + match query { + QueryExpr::ArrayContains { + property, values, .. + } => { + assert_eq!(property, "tags"); + assert_eq!(values.len(), 2); + assert!(values.contains(&"premium".to_string())); + } + _ => panic!("Expected ArrayContains expression"), + } } #[test] fn test_numeric_query() { - let query = QueryExpr::NumberGreater { - property: "score".to_string(), - subproperty: None, - value: 85.5, - }; - - match query { - QueryExpr::NumberGreater { property, value, .. } => { - assert_eq!(property, "score"); - assert_eq!(value, 85.5); - } - _ => panic!("Expected NumberGreater expression"), - } + let query = QueryExpr::NumberGreater { + property: "score".to_string(), + subproperty: None, + value: 85.5, + }; + + match query { + QueryExpr::NumberGreater { + property, value, .. + } => { + assert_eq!(property, "score"); + assert_eq!(value, 85.5); + } + _ => panic!("Expected NumberGreater expression"), + } } #[test] fn test_numeric_query_serde() { - let query = QueryExpr::NumberLessOrEqual { - property: "metrics".to_string(), - subproperty: Some("latency".to_string()), - value: 100.0, - }; - - // Test serialization - let json = serde_json::to_string_pretty(&query).unwrap(); - - assert!(json.contains(r#""number_less_or_equal""#)); - assert!(json.contains(r#""property": "metrics""#)); - assert!(json.contains(r#""subproperty": "latency""#)); - assert!(json.contains(r#""value": 100.0"#)); - - // Test deserialization - let deserialized: QueryExpr = serde_json::from_str(&json).unwrap(); - match deserialized { - QueryExpr::NumberLessOrEqual { property, subproperty, value } => { - assert_eq!(property, "metrics"); - assert_eq!(subproperty, Some("latency".to_string())); - assert_eq!(value, 100.0); - } - _ => panic!("Expected NumberLessOrEqual"), - } -} \ No newline at end of file + let query = QueryExpr::NumberLessOrEqual { + property: "metrics".to_string(), + subproperty: Some("latency".to_string()), + value: 100.0, + }; + + // Test serialization + let json = serde_json::to_string_pretty(&query).unwrap(); + + assert!(json.contains(r#""number_less_or_equal""#)); + assert!(json.contains(r#""property": "metrics""#)); + assert!(json.contains(r#""subproperty": "latency""#)); + assert!(json.contains(r#""value": 100.0"#)); + + // Test deserialization + let deserialized: QueryExpr = serde_json::from_str(&json).unwrap(); + match deserialized { + QueryExpr::NumberLessOrEqual { + property, + subproperty, + value, + } => { + assert_eq!(property, "metrics"); + assert_eq!(subproperty, Some("latency".to_string())); + assert_eq!(value, 100.0); + } + _ => panic!("Expected NumberLessOrEqual"), + } +} diff --git a/packages/common/clickhouse-user-query/tests/schema_tests.rs b/packages/common/clickhouse-user-query/tests/schema_tests.rs index 16c4a5bbfb..7e14d310d4 100644 --- a/packages/common/clickhouse-user-query/tests/schema_tests.rs +++ b/packages/common/clickhouse-user-query/tests/schema_tests.rs @@ -2,26 +2,27 @@ use clickhouse_user_query::*; #[test] fn test_schema_creation() { - let schema = Schema::new(vec![ - Property::new("valid_name".to_string(), false, PropertyType::String).unwrap(), - Property::new("another_valid_123".to_string(), true, PropertyType::Bool).unwrap(), - ]).unwrap(); - - assert_eq!(schema.properties.len(), 2); - assert!(schema.get_property("valid_name").is_some()); - assert!(schema.get_property("nonexistent").is_none()); + let schema = Schema::new(vec![ + Property::new("valid_name".to_string(), false, PropertyType::String).unwrap(), + Property::new("another_valid_123".to_string(), true, PropertyType::Bool).unwrap(), + ]) + .unwrap(); + + assert_eq!(schema.properties.len(), 2); + assert!(schema.get_property("valid_name").is_some()); + assert!(schema.get_property("nonexistent").is_none()); } #[test] fn test_invalid_property_name() { - let result = Property::new("invalid-name".to_string(), false, PropertyType::String); - assert!(result.is_err()); + let result = Property::new("invalid-name".to_string(), false, PropertyType::String); + assert!(result.is_err()); } #[test] fn test_property_type_names() { - assert_eq!(PropertyType::Bool.type_name(), "bool"); - assert_eq!(PropertyType::String.type_name(), "string"); - assert_eq!(PropertyType::Number.type_name(), "number"); - assert_eq!(PropertyType::ArrayString.type_name(), "array[string]"); -} \ No newline at end of file + assert_eq!(PropertyType::Bool.type_name(), "bool"); + assert_eq!(PropertyType::String.type_name(), "string"); + assert_eq!(PropertyType::Number.type_name(), "number"); + assert_eq!(PropertyType::ArrayString.type_name(), "array[string]"); +}