Compare commits

..

1 Commits

Author SHA1 Message Date
Tushar Vats
9e318d77be fix(querier): decode JSON columns for scalar and time-series results
Share the raw reader's JSON scan target across all three readers, and unwrap
Dynamic columns from their chcol.Variant envelope.
2026-08-14 05:26:47 +05:30
14 changed files with 195 additions and 268 deletions

View File

@@ -88,7 +88,6 @@
"jest": "30.2.0",
"js-base64": "^3.7.2",
"lodash-es": "^4.17.21",
"monaco-editor": "0.55.1",
"motion": "12.4.13",
"nuqs": "2.8.8",
"overlayscrollbars": "^2.16.0",
@@ -238,4 +237,4 @@
"tmp@<0.2.6": ">=0.2.6 <0.3.0",
"yaml@>=1.0.0 <1.10.3": ">=1.10.3 <2"
}
}
}

View File

@@ -180,9 +180,6 @@ importers:
lodash-es:
specifier: ^4.17.21
version: 4.18.1
monaco-editor:
specifier: 0.55.1
version: 0.55.1
motion:
specifier: 12.4.13
version: 12.4.13(@emotion/is-prop-valid@1.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -1758,89 +1755,105 @@ packages:
resolution: {integrity: sha512-C0SqjoFKnszqa44EQ7xoaT48nnO0lOyXEULfXMWi8krrjOPGYkeK30Okzla6ATbBYsyZ0ySinK0FVkpv3DwzfQ==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.3.0':
resolution: {integrity: sha512-A8UpHoUDW4DwnXoV6+q3C1s7QLRAHtPDEjWuNZjwHMyoCNZnm0GeNN8ls9f/bsEYTRQRW96C/n34XJQHJ2fT7A==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.3.0':
resolution: {integrity: sha512-WOpkVxAjFd369iaIzEgNRreFD+gWdUMIGD5zplhNKNeqS6mm5dac3q2AFyCBmzYoAdouzZvRBgxy4z8QHZb4/A==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.3.0':
resolution: {integrity: sha512-DRWw0mOHusrCCuw2rqP87oLg6PGlkomVDFqw2hIwsSfwWpu4k3XLcBPaKKl6ct/GtL/cwNkgwjV/tc0Mqht3VA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.3.0':
resolution: {integrity: sha512-9APy+nFWhHS+kzLgWZfLcyrUd7YqnAQVa4BPOo4xkoHpdoktOAPG4cEr9+Jpl0TtqfVmcMJimNL5qNTyyOHZNA==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.3.0':
resolution: {integrity: sha512-y9RNUYDe2A1UAdhLyfeOodGRszQdaEoe4nfOpp/sNVPl2CWIcUyFaDoCh4vPLPxu19803j2naLqZup2WxDXCLA==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.3.0':
resolution: {integrity: sha512-cC1wkC0Mlucd0KSiGrLkJnB/ZqPvZCntc/Lk7ZnYO5ZSbF2euNek4Xvxafojq+wN1q/W0eprdpUIjUr/EV2PBg==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.3.0':
resolution: {integrity: sha512-LiYMhUZicB1QG//+RvmYZpXJO8fYRENfp+MZUCnG9aw+AKvGAy9gPaCnuwsPcBFs8EV66M0NNxj9VHcNklE8zw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.35.0':
resolution: {integrity: sha512-4+4XHLNT5wDT0roYlHTEmH9lDKt0acf9Tv+3hM3iceOirkxrR404/3WjAYZ9F9CkHrxeRcGLJXbi4vluMZ9O+A==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.35.0':
resolution: {integrity: sha512-VVlpEWwizEFIOom0zdoeKuO5nuTswzVE5uHcBNvHzmeHUpNFajY3HFfbQ+zIH4E2kVaZ/yVxmsShW56TtEy4uA==}
engines: {node: '>=20.9.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.35.0':
resolution: {integrity: sha512-N3hzbEpUTJC8pWpPVJvgzGxM+so/MAXc8O2s/53B0LL9ZGpfXpME7Wizkc5d/8fRBlBtkDjzoZGDCqqNDHqLEw==}
engines: {node: '>=20.9.0'}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.35.0':
resolution: {integrity: sha512-l6vmKVPnbS0RhVMbyxP5meAARsbhCnBN4fy31qz0+3a6Rv4jEqfzDrT89y6ZPkCi0AJGnwp2En528yXo401Hpw==}
engines: {node: '>=20.9.0'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.35.0':
resolution: {integrity: sha512-MYlMiPFiv/EKPAHnp3yNZ9AAWFsxga9c5Bkc6wkar6bqzHLlkGVJHRm0u1ei+VXnZxp3Mz9MG9ZIsI8vSOf3sQ==}
engines: {node: '>=20.9.0'}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.35.0':
resolution: {integrity: sha512-TYaItB5oj1ioXjhyn2xrR208vf+YuIIcHptQWRRaBmFhvIvL9D72DXN8w75xup0KXA8UdEAhQ9Qb2S49FD/9Cw==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.35.0':
resolution: {integrity: sha512-DSTb6ijQzqe6DdAaOBVqJ/SYf1vO8EW5bK6X6LRXufEBebf2722VCdvBUtZ3rtV0x2ApfPNDy/p7LrrjaWjiyQ==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.35.0':
resolution: {integrity: sha512-K7ykQ+26Rt6+4BTU80AuGgTPIYX86UxiAKT4rcXX/WNTo7k1ZxpKz+TguHnwVpCqQK3B5PK0vZ0ZBe6nz/ib1w==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.35.0':
resolution: {integrity: sha512-9woLIFORERCr+6cWu87dQ22J34EExkhc73U1kZW0c+RclQqWetoodByp4dWZ/hN8/KVmTRAx2HOnUwib8AwZdA==}
@@ -2221,48 +2234,56 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-arm64-musl@0.54.0':
resolution: {integrity: sha512-QrwJlBFFKnxOd95TAaszpMbZBLzMoYMpGaQTZF8oibacnF5rv8l12IhILhQRPmksWiBqg0YSe2Mnl7ayeJAHSA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxfmt/binding-linux-ppc64-gnu@0.54.0':
resolution: {integrity: sha512-WILatiol/TUHTlhod7R09+7Az/XlhKwmY1MHfLZNmewltPWNN/EwxP2rQSHahibZ/cB8gmckEBjBOByD+5bYsQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-riscv64-gnu@0.54.0':
resolution: {integrity: sha512-f05YMG4BH4G8S4ME6UM6fi1MnJ9094mrnvO5Pa4SJlMfWlUM+1/ZWMEF4NnjM7shZAvbHsHRuVYpUo0PHC4P9Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-riscv64-musl@0.54.0':
resolution: {integrity: sha512-UfL+2hj1ClNqcCRT9s8vBU4axDpjxgVxX96G+9DYAYjoc5b0u15CJtn2jgsi9iM+EbGNc5CW1HVRgwVu76UsSA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@oxfmt/binding-linux-s390x-gnu@0.54.0':
resolution: {integrity: sha512-3/XZe931Hka+J6NjnaqJzYpsWWxDTuRdUdwSQHnOuJEgbC+SehIMFJS8hsEjV7LBhVSL2OCnRLvbVW8O97XIyw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-x64-gnu@0.54.0':
resolution: {integrity: sha512-Ik93RlObtu43GbxApafayFjwYE06L6Xr08cSwpBPYbDrLp2ReZx0Jm1DqwRyYRnukUJy+rK2WaEvUQOxdytU9Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-x64-musl@0.54.0':
resolution: {integrity: sha512-yZcakmPlD86CNymknd7KfW+FH+qfbqJH+i0h69CYfV1+KMoVeM9UED+8+TDVoU4haxI0NxY7RPCvRLy3Sqd2Qg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxfmt/binding-openharmony-arm64@0.54.0':
resolution: {integrity: sha512-GiVBZNnEZnKu00f1jTg49nomv187d0GQX+O+ocykoLeiaALuEO+swoTehHn9TehTfi7V8H0i0e/yvUjCqnwk1w==}
@@ -2365,48 +2386,56 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-arm64-musl@1.69.0':
resolution: {integrity: sha512-xEPpNppTfN1l/nM7gYSf9iocscu/as+p/7vxkLeLEKnYU+09Dm+5V6IhDYDh+Uz6FajEupWwCLt5SOG0y1PCKg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxlint/binding-linux-ppc64-gnu@1.69.0':
resolution: {integrity: sha512-Ug0+eU7HJBlek+SjklYH62IlOMirEJsdxpihH0kSqX0XdrDD4NdHpQc10fK1JC35yn6KrrcN+uYzlHD38XAf8Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-riscv64-gnu@1.69.0':
resolution: {integrity: sha512-iEyI3GIg0l/s3G4qy2TlaaWKdzj4PJJStwtlocpDTC00PY9hZueotf6OKUj9+yfQh0lrpBW/pLMgTztbAHKJEg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-riscv64-musl@1.69.0':
resolution: {integrity: sha512-NjHjpiI4WIKSMwuoJSZi5VToPeoYOS1FR52HLIDG6lidMdqquusgtODb4iLk0+lb1q3Z0nv2/aPRcC/olmpQGg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@oxlint/binding-linux-s390x-gnu@1.69.0':
resolution: {integrity: sha512-Ai/prDewoItkDXbp38gwGZi41DycZbUTZJ3UidwoHgQC0/DaqC2TGdtBTQLJ6hSD+SAxASzh8+/eSBPmxfOacA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-x64-gnu@1.69.0':
resolution: {integrity: sha512-Gt3KHgp46mRKz4sJeaASmKvD8ayXookRw07RMf+NowhEztGGDZ7VrXpoW96XuKJLjFukWizOFVNjmYb/u7caNQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-x64-musl@1.69.0':
resolution: {integrity: sha512-7tQhJ2+p/oHv1zcfnjYI7YVzC/7iBaVOfIvFYtxdJ5F45mWgEdrCyXZXZGfiLey5t/5JhOhsaMnnv1kAzckd7g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxlint/binding-openharmony-arm64@1.69.0':
resolution: {integrity: sha512-vmWz6TKp/3hfA4lksR0zHBv/6xuX1jhym6eqOjdH2DXsDDHZWcp2f0KG0VCAnlVbIrjk29G4wAWMXb/Hn1YobA==}
@@ -2461,36 +2490,42 @@ packages:
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.1':
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.1':
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.1':
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.1':
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.1':
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@parcel/watcher-win32-arm64@2.5.1':
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
@@ -3107,24 +3142,28 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.53':
resolution: {integrity: sha512-bGe5EBB8FVjHBR1mOLOPEFg1Lp3//7geqWkU5NIhxe+yH0W8FVrQ6WRYOap4SUTKdklD/dC4qPLREkMMQ855FA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.53':
resolution: {integrity: sha512-qL+63WKVQs1CMvFedlPt0U9PiEKJOAL/bsHMKUDS6Vp2Q+YAv/QLPu8rcvkfIMvQ0FPU2WL0aX4eWwF6e/GAnA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.0.0-beta.53':
resolution: {integrity: sha512-VGl9JIGjoJh3H8Mb+7xnVqODajBmrdOOb9lxWXdcmxyI+zjB2sux69br0hZJDTyLJfvBoYm439zPACYbCjGRmw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@rolldown/binding-openharmony-arm64@1.0.0-beta.53':
resolution: {integrity: sha512-B4iIserJXuSnNzA5xBLFUIjTfhNy7d9sq4FUMQY3GhQWGVhS2RWWzzDnkSU6MUt7/aHUrep0CdQfXUJI9D3W7A==}
@@ -3785,41 +3824,49 @@ packages:
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
cpu: [x64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
@@ -6228,24 +6275,28 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.31.1:
resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.31.1:
resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.31.1:
resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.31.1:
resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==}

View File

@@ -15,8 +15,6 @@ import store from 'store';
import APIError from 'types/api/error';
import { installTranslationResilience } from 'translation-resilience';
import 'lib/monaco/setup';
import './ReactI18';
import 'styles.scss';

View File

@@ -1,21 +0,0 @@
import { loader } from '@monaco-editor/react';
import * as monaco from 'monaco-editor';
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
// Serve Monaco's workers from our own origin instead of letting @monaco-editor/loader
// fetch them from cdn.jsdelivr.net at runtime. The CDN default breaks the editor in
// air-gapped/on-prem installs, CDN-blocking corporate networks, and regions where
// jsdelivr is unreachable. Ref: engineering-pod#5871, SIGNOZ-UI-5G0.
self.MonacoEnvironment = {
getWorker(_workerId: string, label: string): Worker {
if (label === 'json') {
return new JsonWorker(); // JSON language service
}
// SigNoz editors use JSON + a hand-registered ClickHouse tokenizer (no worker),
// so the base editor worker covers everything else.
return new EditorWorker(); // base worker — sql, yaml, plaintext etc.
},
};
loader.config({ monaco });

View File

@@ -125,37 +125,6 @@ describe('NumberPanelRenderer', () => {
expect(queryByText('3.14159')).not.toBeInTheDocument();
});
// #7669: large scalars are unreadable as an undelimited digit run.
it('groups large values into thousands', () => {
const { getByText, queryByText } = renderPanel({
panel: panelWith({}),
data: dataWith('1234567'),
});
expect(getByText('1,234,567')).toBeInTheDocument();
expect(queryByText('1234567')).not.toBeInTheDocument();
});
it('groups the value while keeping its unit separate', () => {
const { getByText } = renderPanel({
panel: panelWith({ formatting: { unit: 'percent' } }),
data: dataWith('1234567'),
});
expect(getByText('1,234,567')).toBeInTheDocument();
expect(getByText('%')).toBeInTheDocument();
});
it('leaves a unit-scaled value ungrouped', () => {
const { getByText } = renderPanel({
panel: panelWith({ formatting: { unit: 'bytes' } }),
data: dataWith('1234567'),
});
expect(getByText('1.18')).toBeInTheDocument();
expect(getByText('MiB')).toBeInTheDocument();
});
it('renders No Data when the response has no scalar results', () => {
const { getByTestId } = renderPanel({ data: emptyData });

View File

@@ -93,15 +93,6 @@ describe('TablePanelRenderer', () => {
expect(getByText('cartservice')).toBeInTheDocument();
});
// Value cells share `formatPanelValue`, so they group like the Number panel.
it('groups large value cells into thousands', () => {
const { getByText } = renderPanel({
data: dataWith([['frontend', 1234567]]),
});
expect(getByText('1,234,567')).toBeInTheDocument();
});
it('renders No Data when the response has no scalar results', () => {
const { getByTestId } = renderPanel({ data: emptyData });

View File

@@ -30,14 +30,4 @@ describe('formatPanelValue', () => {
it('renders whole numbers without a trailing decimal', () => {
expect(formatPanelValue(5, undefined, 2)).toBe('5');
});
it('groups the integer part into thousands', () => {
expect(formatPanelValue(1234567, undefined, 2)).toBe('1,234,567');
expect(formatPanelValue(1234567, 'percent', 2)).toBe('1,234,567%');
expect(formatPanelValue(1234567.891, undefined, 2)).toBe('1,234,567.89');
});
it('leaves unit-scaled values ungrouped', () => {
expect(formatPanelValue(1234567, 'bytes', 2)).toBe('1.18 MiB');
});
});

View File

@@ -1,55 +0,0 @@
import { groupThousands } from '../groupThousands';
describe('groupThousands', () => {
it('groups the integer digits of a plain number', () => {
expect(groupThousands('1234567')).toBe('1,234,567');
expect(groupThousands('1000')).toBe('1,000');
expect(groupThousands('1000000000000000000000')).toBe(
'1,000,000,000,000,000,000,000',
);
});
it('leaves values below a thousand alone', () => {
expect(groupThousands('0')).toBe('0');
expect(groupThousands('999')).toBe('999');
expect(groupThousands('295.43')).toBe('295.43');
});
it('groups only the integer part', () => {
expect(groupThousands('1234567.891')).toBe('1,234,567.891');
expect(groupThousands('1234.0001234')).toBe('1,234.0001234');
});
it('keeps the sign outside the first group', () => {
expect(groupThousands('-1234567')).toBe('-1,234,567');
expect(groupThousands('-1234567.891')).toBe('-1,234,567.891');
});
it('preserves suffix and prefix unit decoration', () => {
expect(groupThousands('1234567 ms')).toBe('1,234,567 ms');
expect(groupThousands('1234567%')).toBe('1,234,567%');
expect(groupThousands('$ 1234567')).toBe('$ 1,234,567');
});
it('leaves formatter-scaled values untouched', () => {
expect(groupThousands('1.18 MiB')).toBe('1.18 MiB');
expect(groupThousands('1.23 Mil')).toBe('1.23 Mil');
expect(groupThousands('20.58 mins')).toBe('20.58 mins');
});
it('leaves exponent notation untouched', () => {
expect(groupThousands('1.234567e+21')).toBe('1.234567e+21');
expect(groupThousands('1234567e-8')).toBe('1234567e-8');
});
it('returns non-numeric output unchanged', () => {
expect(groupThousands('∞')).toBe('∞');
expect(groupThousands('-∞')).toBe('-∞');
expect(groupThousands('NaN')).toBe('NaN');
expect(groupThousands('')).toBe('');
});
it('is idempotent on already-grouped input', () => {
expect(groupThousands(groupThousands('1234567'))).toBe('1,234,567');
});
});

View File

@@ -1,60 +0,0 @@
import { parseFormattedValue } from '../parseFormattedValue';
describe('parseFormattedValue', () => {
it('splits a trailing unit label off the numeric core', () => {
expect(parseFormattedValue('295.43 ms')).toStrictEqual({
numericValue: '295.43',
prefixUnit: '',
suffixUnit: 'ms',
});
});
it('splits a leading currency symbol off the numeric core', () => {
expect(parseFormattedValue('$ 1.2K')).toStrictEqual({
numericValue: '1.2K',
prefixUnit: '$',
suffixUnit: '',
});
});
// Regression: the numeric core used to reject `,`, so a grouped value fell
// through to the whole-string fallback and lost its unit split.
it('keeps the unit split for grouped values', () => {
expect(parseFormattedValue('1,234,567 ms')).toStrictEqual({
numericValue: '1,234,567',
prefixUnit: '',
suffixUnit: 'ms',
});
expect(parseFormattedValue('1,234,567%')).toStrictEqual({
numericValue: '1,234,567',
prefixUnit: '',
suffixUnit: '%',
});
expect(parseFormattedValue('$ 1,234,567.89')).toStrictEqual({
numericValue: '1,234,567.89',
prefixUnit: '$',
suffixUnit: '',
});
});
it('treats a unitless value as the numeric core', () => {
expect(parseFormattedValue('1,234,567')).toStrictEqual({
numericValue: '1,234,567',
prefixUnit: '',
suffixUnit: '',
});
});
it('falls back to the whole string when nothing matches', () => {
expect(parseFormattedValue('∞')).toStrictEqual({
numericValue: '∞',
prefixUnit: '',
suffixUnit: '',
});
expect(parseFormattedValue('NaN')).toStrictEqual({
numericValue: 'NaN',
prefixUnit: '',
suffixUnit: '',
});
});
});

View File

@@ -1,21 +1,16 @@
import type { PrecisionOption } from 'components/Graph/types';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { groupThousands } from './groupThousands';
/**
* Formats a scalar for display in a V2 panel, honoring decimal precision and
* grouping the integer part into thousands. The single seam through which panels
* touch `getYAxisFormattedValue`. Unitless values format through the `'none'`
* unit, which still respects precision — so precision isn't silently dropped
* when no unit is set.
* Formats a scalar for display in a V2 panel, honoring decimal precision. The
* single seam through which panels touch `getYAxisFormattedValue`. Unitless
* values format through the `'none'` unit, which still respects precision — so
* precision isn't silently dropped when no unit is set.
*/
export function formatPanelValue(
value: number,
unit?: string,
precision?: PrecisionOption,
): string {
return groupThousands(
getYAxisFormattedValue(String(value), unit || 'none', precision),
);
return getYAxisFormattedValue(String(value), unit || 'none', precision);
}

View File

@@ -1,22 +0,0 @@
const THOUSANDS_BOUNDARY = /(\d)(?=(?:\d{3})+$)/g;
/** Leading number of a formatted value; unit decoration falls outside the match. */
const NUMERIC_TOKEN = /-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/;
/**
* Inserts thousand separators into the integer part of an already-formatted value,
* so large scalars read as `1,234,567`. Fractions, unit labels and formatter-scaled
* values (`1.18 MiB`) are left as-is, as is exponent notation.
*/
export function groupThousands(formatted: string): string {
return formatted.replace(NUMERIC_TOKEN, (token) => {
if (token.includes('e') || token.includes('E')) {
return token;
}
const [integerPart, fraction] = token.split('.');
const grouped = integerPart.replace(THOUSANDS_BOUNDARY, '$1,');
return fraction === undefined ? grouped : `${grouped}.${fraction}`;
});
}

View File

@@ -1,5 +1,5 @@
export interface ParsedFormattedValue {
/** The numeric portion (e.g. "295.43", "1,234,567", "1.2K"). */
/** The numeric portion (e.g. "295.43", "1.2K"). */
numericValue: string;
/** A leading unit symbol such as a currency prefix, if any. */
prefixUnit: string;
@@ -8,14 +8,13 @@ export interface ParsedFormattedValue {
}
/**
* Splits a formatted value (e.g. "$ 1.2K", "295.43 ms", "1,234,567") into its
* numeric core and prefix/suffix unit for independent styling. The core accepts
* thousand separators, so a grouped value keeps its unit split. Non-matching
* input falls back to the whole string as the numeric value.
* Splits a formatted value (e.g. "$ 1.2K", "295.43 ms") into its numeric core
* and prefix/suffix unit for independent styling. Non-matching input falls back
* to the whole string as the numeric value.
*/
export function parseFormattedValue(value: string): ParsedFormattedValue {
const matches = value.match(
/^([^\d.]*)?([\d.,]+(?:[eE][+-]?[\d]+)?[KMB]?)([^\d.]*)?$/,
/^([^\d.]*)?([\d.]+(?:[eE][+-]?[\d]+)?[KMB]?)([^\d.]*)?$/,
);
return {

View File

@@ -11,6 +11,7 @@ import (
"strings"
"time"
"github.com/ClickHouse/clickhouse-go/v2/lib/chcol"
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
"github.com/SigNoz/signoz/pkg/errors"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
@@ -40,6 +41,45 @@ func stripKeyAlias(name string) string {
return keyAliasRe.ReplaceAllString(name, "")
}
func isJSONColumn(colType driver.ColumnType) bool {
return strings.HasPrefix(strings.ToUpper(colType.DatabaseTypeName()), "JSON")
}
// scanTarget returns the scan destination for a column.
//
// A JSON column is read as raw bytes: its ScanType is chcol.JSON, whose Scan accepts only a
// JSON or a map[string]any and rejects the string the server sends us (we ask for JSON as
// strings via output_format_native_write_json_as_string). decodeColumnValue turns the bytes
// back into a map. Dynamic/Variant columns scan natively into a chcol.Variant envelope.
func scanTarget(colType driver.ColumnType) any {
if isJSONColumn(colType) {
var v []byte
return &v
}
return reflect.New(colType.ScanType()).Interface()
}
// decodeColumnValue turns a scanned value into the value carried in the payload: JSON bytes
// become a map, and a Dynamic/Variant envelope is unwrapped to the concrete value it holds.
func decodeColumnValue(colType driver.ColumnType, name string, val any) (any, error) {
if isJSONColumn(colType) {
b, ok := val.([]byte)
if !ok {
// already a structured type (map[string]any, []any, etc.)
return val, nil
}
var m map[string]any
if err := sonic.Unmarshal(b, &m); err != nil {
return nil, errors.WrapInternalf(err, CodeFailUnmarshalJSONColumn, "failed to unmarshal JSON column %s", name)
}
return m, nil
}
if v, ok := val.(chcol.Variant); ok {
return v.Any(), nil
}
return val, nil
}
// consume reads every row and shapes it into the payload expected for the
// given request type.
//
@@ -73,7 +113,7 @@ func readAsTimeSeries(rows driver.Rows, queryWindow *qbtypes.TimeRange, step qbt
slots := make([]any, len(colTypes))
numericColsCount := 0
for i, ct := range colTypes {
slots[i] = reflect.New(ct.ScanType()).Interface()
slots[i] = scanTarget(ct)
if isNumericKind(ct.ScanType()) {
numericColsCount++
}
@@ -332,7 +372,7 @@ func readAsScalar(rows driver.Rows, queryName string) (*qbtypes.ScalarData, erro
// Pre-allocate scan slots once
scan := make([]any, len(colTypes))
for i := range scan {
scan[i] = reflect.New(colTypes[i].ScanType()).Interface()
scan[i] = scanTarget(colTypes[i])
}
var data [][]any
@@ -345,7 +385,11 @@ func readAsScalar(rows driver.Rows, queryName string) (*qbtypes.ScalarData, erro
// 2. deref each slot into the output row
row := make([]any, len(scan))
for i, cell := range scan {
row[i] = derefValue(cell)
val, err := decodeColumnValue(colTypes[i], cd[i].Name, derefValue(cell))
if err != nil {
return nil, err
}
row[i] = val
}
data = append(data, row)
}
@@ -382,31 +426,13 @@ func readAsRaw(rows driver.Rows, queryName string) (*qbtypes.RawData, error) {
colTypes := rows.ColumnTypes()
colCnt := len(colNames)
// Helper that decides scan target per column based on DB type
makeScanTarget := func(i int) any {
dbt := strings.ToUpper(colTypes[i].DatabaseTypeName())
if strings.HasPrefix(dbt, "JSON") {
// Since the driver fails to decode JSON/Dynamic into native Go values, we read it as raw bytes
// TODO: check in future if fixed in the driver
var v []byte
return &v
}
return reflect.New(colTypes[i].ScanType()).Interface()
}
// Build a template slice of correctly-typed pointers once
scanTpl := make([]any, colCnt)
for i := range colTypes {
scanTpl[i] = makeScanTarget(i)
}
var outRows []*qbtypes.RawRow
for rows.Next() {
// fresh copy of the scan slice (otherwise the driver reuses pointers)
scan := make([]any, colCnt)
for i := range scanTpl {
scan[i] = makeScanTarget(i)
for i := range colTypes {
scan[i] = scanTarget(colTypes[i])
}
if err := rows.Scan(scan...); err != nil {
@@ -421,20 +447,9 @@ func readAsRaw(rows driver.Rows, queryName string) (*qbtypes.RawData, error) {
name := stripKeyAlias(colNames[i])
// de-reference the typed pointer to any
val := reflect.ValueOf(cellPtr).Elem().Interface()
// Post-process JSON columns: unmarshal bytes into map[string]any
if strings.HasPrefix(strings.ToUpper(colTypes[i].DatabaseTypeName()), "JSON") {
switch x := val.(type) {
case []byte:
var m map[string]any
err := sonic.Unmarshal(x, &m)
if err != nil {
return nil, errors.WrapInternalf(err, CodeFailUnmarshalJSONColumn, "failed to unmarshal JSON column %s", name)
}
val = m
default:
// already a structured type (map[string]any, []any, etc.)
}
val, err := decodeColumnValue(colTypes[i], name, reflect.ValueOf(cellPtr).Elem().Interface())
if err != nil {
return nil, err
}
// special-case: timestamp column

View File

@@ -3,8 +3,14 @@ package querier
import (
"reflect"
"testing"
"time"
"github.com/ClickHouse/clickhouse-go/v2/lib/chcol"
cmock "github.com/SigNoz/clickhouse-go-mock"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/spantypes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMergeSpanAttributeColumns_ParsesEventsAndLinks(t *testing.T) {
@@ -75,6 +81,78 @@ func TestMergeSpanAttributeColumns_ParsesEventsAndLinks(t *testing.T) {
}
}
// A ClickHouse query can put a JSON column in the result of any request type — e.g.
// `select * from signoz_logs.logs_v2` on a body_v2 stack, where `*` covers body_v2.
func TestConsume_JSONColumn(t *testing.T) {
ts := time.Date(2026, 8, 14, 10, 0, 0, 0, time.UTC)
body := `{"level":"error","attrs":{"code":500}}`
wantBody := map[string]any{
"level": "error",
"attrs": map[string]any{"code": float64(500)},
}
t.Run("scalar", func(t *testing.T) {
rows := cmock.NewRows([]cmock.ColumnType{
{Name: "body_v2", Type: "JSON"},
{Name: "__result_0", Type: "UInt64"},
}, [][]any{{body, uint64(3)}})
payload, err := consume(rows, qbtypes.RequestTypeScalar, nil, qbtypes.Step{}, "A")
require.NoError(t, err)
data := payload.(*qbtypes.ScalarData)
require.Len(t, data.Data, 1)
assert.Equal(t, wantBody, data.Data[0][0])
assert.Equal(t, uint64(3), data.Data[0][1])
})
t.Run("time series", func(t *testing.T) {
rows := cmock.NewRows([]cmock.ColumnType{
{Name: "ts", Type: "DateTime"},
{Name: "body_v2", Type: "JSON"},
{Name: "__result_0", Type: "UInt64"},
}, [][]any{{ts, body, uint64(3)}})
payload, err := consume(rows, qbtypes.RequestTypeTimeSeries, nil, qbtypes.Step{}, "A")
require.NoError(t, err)
data := payload.(*qbtypes.TimeSeriesData)
require.Len(t, data.Aggregations, 1)
require.Len(t, data.Aggregations[0].Series, 1)
require.Len(t, data.Aggregations[0].Series[0].Values, 1)
assert.Equal(t, float64(3), data.Aggregations[0].Series[0].Values[0].Value)
})
t.Run("raw", func(t *testing.T) {
rows := cmock.NewRows([]cmock.ColumnType{
{Name: "timestamp", Type: "DateTime"},
{Name: "body_v2", Type: "JSON"},
}, [][]any{{ts, body}})
payload, err := consume(rows, qbtypes.RequestTypeRaw, nil, qbtypes.Step{}, "A")
require.NoError(t, err)
data := payload.(*qbtypes.RawData)
require.Len(t, data.Rows, 1)
assert.Equal(t, ts, data.Rows[0].Timestamp.UTC())
assert.Equal(t, wantBody, data.Rows[0].Data["body_v2"])
})
}
// A JSON path (e.g. `body_v2.level`) comes back as a Dynamic column, which the driver scans
// into a chcol.Variant envelope rather than the value itself.
func TestDecodeColumnValue_Dynamic(t *testing.T) {
colType := cmock.NewColumnType("level", "Dynamic", false, reflect.TypeOf(chcol.Variant{}))
val, err := decodeColumnValue(colType, "level", chcol.NewDynamicWithType("error", "String"))
require.NoError(t, err)
assert.Equal(t, "error", val)
val, err = decodeColumnValue(colType, "level", chcol.Dynamic{})
require.NoError(t, err)
assert.Nil(t, val)
}
func TestMergeSpanAttributeColumns_EmptyEventsAndLinks(t *testing.T) {
data := map[string]any{
"events": []string{},