[
  {
    "path": ".config/.cprc.json",
    "content": "{\n  \"version\": \"6.4.4\",\n  \"features\": {}\n}\n"
  },
  {
    "path": ".config/.prettierrc.js",
    "content": "/*\n * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️\n *\n * In order to extend the configuration follow the steps in .config/README.md\n */\n\nmodule.exports = {\n  endOfLine: 'auto',\n  printWidth: 120,\n  trailingComma: 'es5',\n  semi: true,\n  jsxSingleQuote: false,\n  singleQuote: true,\n  useTabs: false,\n  tabWidth: 2,\n};\n"
  },
  {
    "path": ".config/Dockerfile",
    "content": "ARG grafana_version=latest\nARG grafana_image=grafana-enterprise\n\nFROM grafana/${grafana_image}:${grafana_version}\n\nARG anonymous_auth_enabled=true\nARG development=false\nARG TARGETARCH\n\nARG GO_VERSION=1.21.6\nARG GO_ARCH=${TARGETARCH:-amd64}\n\nENV DEV \"${development}\"\n\n# Make it as simple as possible to access the grafana instance for development purposes\n# Do NOT enable these settings in a public facing / production grafana instance\nENV GF_AUTH_ANONYMOUS_ORG_ROLE \"Admin\"\nENV GF_AUTH_ANONYMOUS_ENABLED \"${anonymous_auth_enabled}\"\nENV GF_AUTH_BASIC_ENABLED \"false\"\n# Set development mode so plugins can be loaded without the need to sign\nENV GF_DEFAULT_APP_MODE \"development\"\n\n\nLABEL maintainer=\"Grafana Labs <hello@grafana.com>\"\n\nENV GF_PATHS_HOME=\"/usr/share/grafana\"\nWORKDIR $GF_PATHS_HOME\n\nUSER root\n\n# Installing supervisor and inotify-tools\nRUN if [ \"${development}\" = \"true\" ]; then \\\n    if grep -i -q alpine /etc/issue; then \\\n    apk add supervisor inotify-tools git; \\\n    elif grep -i -q ubuntu /etc/issue; then \\\n    DEBIAN_FRONTEND=noninteractive && \\\n    apt-get update && \\\n    apt-get install -y supervisor inotify-tools git && \\\n    rm -rf /var/lib/apt/lists/*; \\\n    else \\\n    echo 'ERROR: Unsupported base image' && /bin/false; \\\n    fi \\\n    fi\n\nCOPY supervisord/supervisord.conf /etc/supervisor.d/supervisord.ini\nCOPY supervisord/supervisord.conf /etc/supervisor/conf.d/supervisord.conf\n\n\n# Installing Go\nRUN if [ \"${development}\" = \"true\" ]; then \\\n    curl -O -L https://golang.org/dl/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz && \\\n    rm -rf /usr/local/go && \\\n    tar -C /usr/local -xzf go${GO_VERSION}.linux-${GO_ARCH}.tar.gz && \\\n    echo \"export PATH=$PATH:/usr/local/go/bin:~/go/bin\" >> ~/.bashrc && \\\n    rm -f go${GO_VERSION}.linux-${GO_ARCH}.tar.gz; \\\n    fi\n\n# Installing delve for debugging\nRUN if [ \"${development}\" = \"true\" ]; then \\\n    /usr/local/go/bin/go install github.com/go-delve/delve/cmd/dlv@latest; \\\n    fi\n\n# Installing mage for plugin (re)building\nRUN if [ \"${development}\" = \"true\" ]; then \\\n    git clone https://github.com/magefile/mage; \\\n    cd mage; \\\n    export PATH=$PATH:/usr/local/go/bin; \\\n    go run bootstrap.go; \\\n    fi\n\n# Inject livereload script into grafana index.html\nRUN sed -i 's|</body>|<script src=\"http://localhost:35729/livereload.js\"></script></body>|g' /usr/share/grafana/public/views/index.html\n\n\nCOPY entrypoint.sh /entrypoint.sh\nRUN chmod +x /entrypoint.sh\nENTRYPOINT [\"/entrypoint.sh\"]\n"
  },
  {
    "path": ".config/README.md",
    "content": "# Default build configuration by Grafana\n\n**This is an auto-generated directory and is not intended to be changed! ⚠️**\n\nThe `.config/` directory holds basic configuration for the different tools\nthat are used to develop, test and build the project. In order to make it updates easier we ask you to\nnot edit files in this folder to extend configuration.\n\n## How to extend the basic configs?\n\nBear in mind that you are doing it at your own risk, and that extending any of the basic configuration can lead\nto issues around working with the project.\n\n### Extending the ESLint config\n\nEdit the `.eslintrc` file in the project root in order to extend the ESLint configuration.\n\n**Example:**\n\n```json\n{\n  \"extends\": \"./.config/.eslintrc\",\n  \"rules\": {\n    \"react/prop-types\": \"off\"\n  }\n}\n```\n\n---\n\n### Extending the Prettier config\n\nEdit the `.prettierrc.js` file in the project root in order to extend the Prettier configuration.\n\n**Example:**\n\n```javascript\nmodule.exports = {\n  // Prettier configuration provided by Grafana scaffolding\n  ...require('./.config/.prettierrc.js'),\n\n  semi: false,\n};\n```\n\n---\n\n### Extending the Jest config\n\nThere are two configuration in the project root that belong to Jest: `jest-setup.js` and `jest.config.js`.\n\n**`jest-setup.js`:** A file that is run before each test file in the suite is executed. We are using it to\nset up the Jest DOM for the testing library and to apply some polyfills. ([link to Jest docs](https://jestjs.io/docs/configuration#setupfilesafterenv-array))\n\n**`jest.config.js`:** The main Jest configuration file that extends the Grafana recommended setup. ([link to Jest docs](https://jestjs.io/docs/configuration))\n\n#### ESM errors with Jest\n\nA common issue with the current jest config involves importing an npm package that only offers an ESM build. These packages cause jest to error with `SyntaxError: Cannot use import statement outside a module`. To work around this, we provide a list of known packages to pass to the `[transformIgnorePatterns](https://jestjs.io/docs/configuration#transformignorepatterns-arraystring)` jest configuration property. If need be, this can be extended in the following way:\n\n```javascript\nprocess.env.TZ = 'UTC';\nconst { grafanaESModules, nodeModulesToTransform } = require('./config/jest/utils');\n\nmodule.exports = {\n  // Jest configuration provided by Grafana\n  ...require('./.config/jest.config'),\n  // Inform jest to only transform specific node_module packages.\n  transformIgnorePatterns: [nodeModulesToTransform([...grafanaESModules, 'packageName'])],\n};\n```\n\n---\n\n### Extending the TypeScript config\n\nEdit the `tsconfig.json` file in the project root in order to extend the TypeScript configuration.\n\n**Example:**\n\n```json\n{\n  \"extends\": \"./.config/tsconfig.json\",\n  \"compilerOptions\": {\n    \"preserveConstEnums\": true\n  }\n}\n```\n\n---\n\n### Extending the Webpack config\n\nFollow these steps to extend the basic Webpack configuration that lives under `.config/`:\n\n#### 1. Create a new Webpack configuration file\n\nCreate a new config file that is going to extend the basic one provided by Grafana.\nIt can live in the project root, e.g. `webpack.config.ts`.\n\n#### 2. Merge the basic config provided by Grafana and your custom setup\n\nWe are going to use [`webpack-merge`](https://github.com/survivejs/webpack-merge) for this.\n\n```typescript\n// webpack.config.ts\nimport type { Configuration } from 'webpack';\nimport { merge } from 'webpack-merge';\nimport grafanaConfig, { type Env } from './.config/webpack/webpack.config';\n\nconst config = async (env: Env): Promise<Configuration> => {\n  const baseConfig = await grafanaConfig(env);\n\n  return merge(baseConfig, {\n    // Add custom config here...\n    output: {\n      asyncChunks: true,\n    },\n  });\n};\n\nexport default config;\n```\n\n#### 3. Update the `package.json` to use the new Webpack config\n\nWe need to update the `scripts` in the `package.json` to use the extended Webpack configuration.\n\n**Update for `build`:**\n\n```diff\n-\"build\": \"webpack -c ./.config/webpack/webpack.config.ts --env production\",\n+\"build\": \"webpack -c ./webpack.config.ts --env production\",\n```\n\n**Update for `dev`:**\n\n```diff\n-\"dev\": \"webpack -w -c ./.config/webpack/webpack.config.ts --env development\",\n+\"dev\": \"webpack -w -c ./webpack.config.ts --env development\",\n```\n\n### Configure grafana image to use when running docker\n\nBy default, `grafana-enterprise` will be used as the docker image for all docker related commands. If you want to override this behavior, simply alter the `docker-compose.yaml` by adding the following build arg `grafana_image`.\n\n**Example:**\n\n```yaml\nversion: '3.7'\n\nservices:\n  grafana:\n    extends:\n      file: .config/docker-compose-base.yaml\n      service: grafana\n    build:\n      args:\n        grafana_version: ${GRAFANA_VERSION:-9.1.2}\n        grafana_image: ${GRAFANA_IMAGE:-grafana}\n```\n\nIn this example, we assign the environment variable `GRAFANA_IMAGE` to the build arg `grafana_image` with a default value of `grafana`. This will allow you to set the value while running the docker compose commands, which might be convenient in some scenarios.\n\n---\n"
  },
  {
    "path": ".config/bundler/externals.ts",
    "content": "import type { Configuration, ExternalItemFunctionData } from 'webpack';\n\ntype ExternalsType = Configuration['externals'];\n\nexport const externals: ExternalsType = [\n  // Required for dynamic publicPath resolution\n  { 'amd-module': 'module' },\n  'lodash',\n  'jquery',\n  'moment',\n  'slate',\n  'emotion',\n  '@emotion/react',\n  '@emotion/css',\n  'prismjs',\n  'slate-plain-serializer',\n  '@grafana/slate-react',\n  'react',\n  'react-dom',\n  'react-redux',\n  'redux',\n  'rxjs',\n  'i18next',\n  'react-router',\n  'react-router-dom',\n  'd3',\n  'angular',\n  /^@grafana\\/ui/i,\n  /^@grafana\\/runtime/i,\n  /^@grafana\\/data/i,\n\n  // Mark legacy SDK imports as external if their name starts with the \"grafana/\" prefix\n  ({ request }: ExternalItemFunctionData, callback: (error?: Error, result?: string) => void) => {\n    const prefix = 'grafana/';\n    const hasPrefix = (request: string) => request.indexOf(prefix) === 0;\n    const stripPrefix = (request: string) => request.slice(prefix.length);\n\n    if (request && hasPrefix(request)) {\n      return callback(undefined, stripPrefix(request));\n    }\n\n    callback();\n  },\n];\n"
  },
  {
    "path": ".config/docker-compose-base.yaml",
    "content": "services:\n  grafana:\n    user: root\n    container_name: 'grafana-clickhouse-datasource'\n\n    build:\n      context: .\n      args:\n        grafana_image: ${GRAFANA_IMAGE:-grafana-enterprise}\n        grafana_version: ${GRAFANA_VERSION:-12.2.0}\n        development: ${DEVELOPMENT:-false}\n        anonymous_auth_enabled: ${ANONYMOUS_AUTH_ENABLED:-true}\n    ports:\n      - 3000:3000/tcp\n      - 2345:2345/tcp # delve\n    security_opt:\n      - 'apparmor:unconfined'\n      - 'seccomp:unconfined'\n    cap_add:\n      - SYS_PTRACE\n    volumes:\n      - ../dist:/var/lib/grafana/plugins/grafana-clickhouse-datasource\n      - ../provisioning:/etc/grafana/provisioning\n      - ..:/root/grafana-clickhouse-datasource\n\n    environment:\n      NODE_ENV: development\n      GF_LOG_FILTERS: plugin.grafana-clickhouse-datasource:debug\n      GF_LOG_LEVEL: debug\n      GF_DATAPROXY_LOGGING: 1\n      GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS: grafana-clickhouse-datasource\n"
  },
  {
    "path": ".config/entrypoint.sh",
    "content": "#!/bin/sh\n\nif [ \"${DEV}\" = \"false\" ]; then\n    echo \"Starting test mode\"\n    exec /run.sh\nfi\n\necho \"Starting development mode\"\n\nif grep -i -q alpine /etc/issue; then\n    exec /usr/bin/supervisord -c /etc/supervisord.conf\nelif grep -i -q ubuntu /etc/issue; then\n    exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf\nelse\n    echo 'ERROR: Unsupported base image'\n    exit 1\nfi\n\n"
  },
  {
    "path": ".config/eslint.config.mjs",
    "content": "import { defineConfig } from 'eslint/config';\nimport grafanaConfig from '@grafana/eslint-config/flat.js';\n\nexport default defineConfig([\n  ...grafanaConfig,\n  {\n    rules: {\n      'react/prop-types': 'off',\n    },\n  },\n  {\n    files: ['src/**/*.{ts,tsx}'],\n\n    languageOptions: {\n      parserOptions: {\n        project: './tsconfig.json',\n      },\n    },\n\n    rules: {\n      '@typescript-eslint/no-deprecated': 'warn',\n    },\n  },\n  {\n    files: ['./tests/**/*'],\n\n    rules: {\n      'react-hooks/rules-of-hooks': 'off',\n    },\n  },\n]);\n"
  },
  {
    "path": ".config/jest/mocks/react-inlinesvg.tsx",
    "content": "// Due to the grafana/ui Icon component making fetch requests to\n// `/public/img/icon/<icon_name>.svg` we need to mock react-inlinesvg to prevent\n// the failed fetch requests from displaying errors in console.\n\nimport React from 'react';\n\ntype Callback = (...args: any[]) => void;\n\nexport interface StorageItem {\n  content: string;\n  queue: Callback[];\n  status: string;\n}\n\nexport const cacheStore: { [key: string]: StorageItem } = Object.create(null);\n\nconst SVG_FILE_NAME_REGEX = /(.+)\\/(.+)\\.svg$/;\n\nconst InlineSVG = ({ src }: { src: string }) => {\n  // testId will be the file name without extension (e.g. `public/img/icons/angle-double-down.svg` -> `angle-double-down`)\n  const testId = src.replace(SVG_FILE_NAME_REGEX, '$2');\n  return <svg xmlns=\"http://www.w3.org/2000/svg\" data-testid={testId} viewBox=\"0 0 24 24\" />;\n};\n\nexport default InlineSVG;\n"
  },
  {
    "path": ".config/jest/utils.js",
    "content": "/*\n * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️\n *\n * In order to extend the configuration follow the steps in .config/README.md\n */\n\n/*\n * This utility function is useful in combination with jest `transformIgnorePatterns` config\n * to transform specific packages (e.g.ES modules) in a projects node_modules folder.\n */\nconst nodeModulesToTransform = (moduleNames) => `node_modules\\/(?!.*(${moduleNames.join('|')})\\/.*)`;\n\n// Array of known nested grafana package dependencies that only bundle an ESM version\nconst grafanaESModules = [\n  '.pnpm', // Support using pnpm symlinked packages\n  '@grafana/schema',\n  '@wojtekmaj/date-utils',\n  'd3',\n  'd3-color',\n  'd3-force',\n  'd3-interpolate',\n  'd3-scale-chromatic',\n  'get-user-locale',\n  'marked',\n  'memoize',\n  'mimic-function',\n  'ol',\n  'react-calendar',\n  'react-colorful',\n  'rxjs',\n  'uuid',\n];\n\nmodule.exports = {\n  nodeModulesToTransform,\n  grafanaESModules,\n};\n"
  },
  {
    "path": ".config/jest-setup.js",
    "content": "/*\n * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️\n *\n * In order to extend the configuration follow the steps in\n * https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-jest-config\n */\n\nimport '@testing-library/jest-dom';\nimport { TextEncoder, TextDecoder } from 'util';\n\nObject.assign(global, { TextDecoder, TextEncoder });\n\n// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom\nObject.defineProperty(global, 'matchMedia', {\n  writable: true,\n  value: (query) => ({\n    matches: false,\n    media: query,\n    onchange: null,\n    addListener: jest.fn(), // deprecated\n    removeListener: jest.fn(), // deprecated\n    addEventListener: jest.fn(),\n    removeEventListener: jest.fn(),\n    dispatchEvent: jest.fn(),\n  }),\n});\n\nHTMLCanvasElement.prototype.getContext = () => {};\n"
  },
  {
    "path": ".config/jest.config.js",
    "content": "/*\n * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️\n *\n * In order to extend the configuration follow the steps in\n * https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-jest-config\n */\n\nconst path = require('path');\nconst { grafanaESModules, nodeModulesToTransform } = require('./jest/utils');\n\nmodule.exports = {\n  moduleNameMapper: {\n    '\\\\.(css|scss|sass)$': 'identity-obj-proxy',\n    'react-inlinesvg': path.resolve(__dirname, 'jest', 'mocks', 'react-inlinesvg.tsx'),\n  },\n  modulePaths: ['<rootDir>/src'],\n  setupFilesAfterEnv: ['<rootDir>/jest-setup.js'],\n  testEnvironment: 'jest-environment-jsdom',\n  testMatch: [\n    '<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',\n    '<rootDir>/src/**/*.{spec,test,jest}.{js,jsx,ts,tsx}',\n    '<rootDir>/src/**/*.{spec,test,jest}.{js,jsx,ts,tsx}',\n  ],\n  transform: {\n    '^.+\\\\.(t|j)sx?$': [\n      '@swc/jest',\n      {\n        sourceMaps: 'inline',\n        jsc: {\n          parser: {\n            syntax: 'typescript',\n            tsx: true,\n            decorators: false,\n            dynamicImport: true,\n          },\n        },\n      },\n    ],\n  },\n  // Jest will throw `Cannot use import statement outside module` if it tries to load an\n  // ES module without it being transformed first. ./config/README.md#esm-errors-with-jest\n  transformIgnorePatterns: [nodeModulesToTransform(grafanaESModules)],\n  watchPathIgnorePatterns: ['<rootDir>/node_modules', '<rootDir>/dist'],\n};\n"
  },
  {
    "path": ".config/supervisord/supervisord.conf",
    "content": "[supervisord]\nnodaemon=true\nuser=root\n\n[program:grafana]\nuser=root\ndirectory=/var/lib/grafana\ncommand=bash -c 'while [ ! -f /root/grafana-clickhouse-datasource/dist/gpx_clickhouse* ]; do sleep 1; done; /run.sh'\nstdout_logfile=/dev/fd/1\nstdout_logfile_maxbytes=0\nredirect_stderr=true\nkillasgroup=true\nstopasgroup=true\nautostart=true\n\n[program:delve]\nuser=root\ncommand=/bin/bash -c 'pid=\"\"; while [ -z \"$pid\" ]; do pid=$(pgrep -f gpx_clickhouse); done; /root/go/bin/dlv attach --api-version=2 --headless --continue --accept-multiclient --listen=:2345 $pid'\nstdout_logfile=/dev/fd/1\nstdout_logfile_maxbytes=0\nredirect_stderr=true\nkillasgroup=false\nstopasgroup=false\nautostart=true\nautorestart=true\n\n[program:build-watcher]\nuser=root\ncommand=/bin/bash -c 'while inotifywait -e modify,create,delete -r /var/lib/grafana/plugins/grafana-clickhouse-datasource; do echo \"Change detected, restarting delve...\";supervisorctl restart delve; done'\nstdout_logfile=/dev/fd/1\nstdout_logfile_maxbytes=0\nredirect_stderr=true\nkillasgroup=true\nstopasgroup=true\nautostart=true\n\n[program:mage-watcher]\nuser=root\nenvironment=PATH=\"/usr/local/go/bin:/root/go/bin:%(ENV_PATH)s\"\ndirectory=/root/grafana-clickhouse-datasource\ncommand=/bin/bash -c 'git config --global --add safe.directory /root/grafana-clickhouse-datasource && mage -v watch'\nstdout_logfile=/dev/fd/1\nstdout_logfile_maxbytes=0\nredirect_stderr=true\nkillasgroup=true\nstopasgroup=true\nautostart=true\n"
  },
  {
    "path": ".config/tsconfig.json",
    "content": "/*\n * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️\n *\n * In order to extend the configuration follow the steps in\n * https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-typescript-config\n */\n{\n  \"compilerOptions\": {\n    \"alwaysStrict\": true,\n    \"declaration\": false,\n    \"rootDir\": \"../src\",\n    \"baseUrl\": \"../src\",\n    \"typeRoots\": [\"../node_modules/@types\"],\n    \"resolveJsonModule\": true\n  },\n  \"ts-node\": {\n    \"compilerOptions\": {\n      \"module\": \"commonjs\",\n      \"target\": \"es5\",\n      \"esModuleInterop\": true\n    },\n    \"transpileOnly\": true\n  },\n  \"include\": [\"../src\", \"./types\"],\n  \"extends\": \"@grafana/tsconfig\"\n}\n"
  },
  {
    "path": ".config/types/bundler-rules.d.ts",
    "content": "// Image declarations\ndeclare module '*.gif' {\n  const src: string;\n  export default src;\n}\n\ndeclare module '*.jpg' {\n  const src: string;\n  export default src;\n}\n\ndeclare module '*.jpeg' {\n  const src: string;\n  export default src;\n}\n\ndeclare module '*.png' {\n  const src: string;\n  export default src;\n}\n\ndeclare module '*.webp' {\n  const src: string;\n  export default src;\n}\n\ndeclare module '*.svg' {\n  const src: string;\n  export default src;\n}\n\n// Font declarations\ndeclare module '*.woff';\ndeclare module '*.woff2';\ndeclare module '*.eot';\ndeclare module '*.ttf';\ndeclare module '*.otf';\n"
  },
  {
    "path": ".config/types/custom.d.ts",
    "content": "// Image declarations\ndeclare module '*.gif' {\n  const src: string;\n  export default src;\n}\n\ndeclare module '*.jpg' {\n  const src: string;\n  export default src;\n}\n\ndeclare module '*.jpeg' {\n  const src: string;\n  export default src;\n}\n\ndeclare module '*.png' {\n  const src: string;\n  export default src;\n}\n\ndeclare module '*.webp' {\n  const src: string;\n  export default src;\n}\n\ndeclare module '*.svg' {\n  const content: string;\n  export default content;\n}\n\n// Font declarations\ndeclare module '*.woff';\ndeclare module '*.woff2';\ndeclare module '*.eot';\ndeclare module '*.ttf';\ndeclare module '*.otf';\n"
  },
  {
    "path": ".config/types/setupTests.d.ts",
    "content": "import '@testing-library/jest-dom';\n"
  },
  {
    "path": ".config/types/webpack-plugins.d.ts",
    "content": "declare module 'replace-in-file-webpack-plugin' {\n  import { Compiler, Plugin } from 'webpack';\n\n  interface ReplaceRule {\n    search: string | RegExp;\n    replace: string | ((match: string) => string);\n  }\n\n  interface ReplaceOption {\n    dir?: string;\n    files?: string[];\n    test?: RegExp | RegExp[];\n    rules: ReplaceRule[];\n  }\n\n  class ReplaceInFilePlugin extends Plugin {\n    constructor(options?: ReplaceOption[]);\n    options: ReplaceOption[];\n    apply(compiler: Compiler): void;\n  }\n\n  export = ReplaceInFilePlugin;\n}\n\ndeclare module 'webpack-livereload-plugin' {\n  import { ServerOptions } from 'https';\n  import { Compiler, Plugin, Stats, Compilation } from 'webpack';\n\n  interface Options extends Pick<ServerOptions, 'cert' | 'key' | 'pfx'> {\n    /**\n     * protocol for livereload `<script>` src attribute value\n     * @default protocol of the page, either `http` or `https`\n     */\n    protocol?: string | undefined;\n    /**\n     * The desired port for the livereload server.\n     * If you define port 0, an available port will be searched for, starting from 35729.\n     * @default 35729\n     */\n    port?: number | undefined;\n    /**\n     * he desired hostname for the appended `<script>` (if present) to point to\n     * @default hostname of the page, like `localhost` or 10.0.2.2\n     */\n    hostname?: string | undefined;\n    /**\n     * livereload `<script>` automatically to `<head>`.\n     * @default false\n     */\n    appendScriptTag?: boolean | undefined;\n    /**\n     * RegExp of files to ignore. Null value means ignore nothing.\n     * It is also possible to define an array and use multiple anymatch patterns\n     */\n    ignore?: RegExp | RegExp[] | null | undefined;\n    /**\n     * amount of milliseconds by which to delay the live reload (in case build takes longer)\n     * @default 0\n     */\n    delay?: number | undefined;\n    /**\n     * create hash for each file source and only notify livereload if hash has changed\n     * @default false\n     */\n    useSourceHash?: boolean | undefined;\n  }\n\n  class LiveReloadPlugin extends Plugin {\n    readonly isRunning: boolean;\n    constructor(options?: Options);\n\n    apply(compiler: Compiler): void;\n\n    start(watching: any, cb: () => void): void;\n    done(stats: Stats): void;\n    failed(): void;\n    autoloadJs(): string;\n    scriptTag(source: string): string;\n    applyCompilation(compilation: Compilation): void;\n  }\n\n  export = LiveReloadPlugin;\n}\n"
  },
  {
    "path": ".config/webpack/BuildModeWebpackPlugin.ts",
    "content": "import webpack, { type Compiler } from 'webpack';\n\nconst PLUGIN_NAME = 'BuildModeWebpack';\n\nexport class BuildModeWebpackPlugin {\n  apply(compiler: webpack.Compiler) {\n    compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {\n      compilation.hooks.processAssets.tap(\n        {\n          name: PLUGIN_NAME,\n          stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,\n        },\n        async () => {\n          const assets = compilation.getAssets();\n          for (const asset of assets) {\n            if (asset.name.endsWith('plugin.json')) {\n              const pluginJsonString = asset.source.source().toString();\n              const pluginJsonWithBuildMode = JSON.stringify(\n                {\n                  ...JSON.parse(pluginJsonString),\n                  buildMode: compilation.options.mode,\n                },\n                null,\n                4\n              );\n              compilation.updateAsset(asset.name, new webpack.sources.RawSource(pluginJsonWithBuildMode));\n            }\n          }\n        }\n      );\n    });\n  }\n}\n"
  },
  {
    "path": ".config/webpack/constants.ts",
    "content": "export const SOURCE_DIR = 'src';\nexport const DIST_DIR = 'dist';\n"
  },
  {
    "path": ".config/webpack/utils.ts",
    "content": "import fs from 'fs';\nimport process from 'process';\nimport os from 'os';\nimport path from 'path';\nimport { glob } from 'glob';\nimport { SOURCE_DIR } from './constants.ts';\n\nexport function isWSL() {\n  if (process.platform !== 'linux') {\n    return false;\n  }\n\n  if (os.release().toLowerCase().includes('microsoft')) {\n    return true;\n  }\n\n  try {\n    return fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft');\n  } catch {\n    return false;\n  }\n}\n\nfunction loadJson(path: string) {\n  const rawJson = fs.readFileSync(path, 'utf8');\n  return JSON.parse(rawJson);\n}\n\nexport function getPackageJson() {\n  return loadJson(path.resolve(process.cwd(), 'package.json'));\n}\n\nexport function getPluginJson() {\n  return loadJson(path.resolve(process.cwd(), `${SOURCE_DIR}/plugin.json`));\n}\n\nexport function getCPConfigVersion() {\n  const cprcJson = path.resolve(process.cwd(), './.config', '.cprc.json');\n  return fs.existsSync(cprcJson) ? loadJson(cprcJson).version : { version: 'unknown' };\n}\n\nexport function hasReadme() {\n  return fs.existsSync(path.resolve(process.cwd(), SOURCE_DIR, 'README.md'));\n}\n\n// Support bundling nested plugins by finding all plugin.json files in src directory\n// then checking for a sibling module.[jt]sx? file.\nexport async function getEntries() {\n  const pluginsJson = await glob('**/src/**/plugin.json', { absolute: true });\n\n  const plugins = await Promise.all(\n    pluginsJson.map((pluginJson) => {\n      const folder = path.dirname(pluginJson);\n      return glob(`${folder}/module.{ts,tsx,js,jsx}`, { absolute: true });\n    })\n  );\n\n  return plugins.reduce<Record<string, string>>((result, modules) => {\n    return modules.reduce((innerResult, module) => {\n      const pluginPath = path.dirname(module);\n      const pluginName = path.relative(process.cwd(), pluginPath).replace(/src\\/?/i, '');\n      const entryName = pluginName === '' ? 'module' : `${pluginName}/module`;\n\n      innerResult[entryName] = module;\n      return innerResult;\n    }, result);\n  }, {});\n}\n"
  },
  {
    "path": ".config/webpack/webpack.config.ts",
    "content": "/*\n * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️\n *\n * In order to extend the configuration follow the steps in\n * https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-webpack-config\n */\n\nimport CopyWebpackPlugin from 'copy-webpack-plugin';\nimport ESLintPlugin from 'eslint-webpack-plugin';\nimport ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';\nimport path from 'path';\nimport ReplaceInFileWebpackPlugin from 'replace-in-file-webpack-plugin';\nimport TerserPlugin from 'terser-webpack-plugin';\nimport { SubresourceIntegrityPlugin } from 'webpack-subresource-integrity';\nimport webpack, { type Configuration } from 'webpack';\nimport LiveReloadPlugin from 'webpack-livereload-plugin';\nimport VirtualModulesPlugin from 'webpack-virtual-modules';\n\nimport { BuildModeWebpackPlugin } from './BuildModeWebpackPlugin.ts';\nimport { DIST_DIR, SOURCE_DIR } from './constants.ts';\nimport { getCPConfigVersion, getEntries, getPackageJson, getPluginJson, hasReadme, isWSL } from './utils.ts';\nimport { externals } from '../bundler/externals.ts';\n\nconst pluginJson = getPluginJson();\nconst cpVersion = getCPConfigVersion();\nconst pluginVersion = getPackageJson().version;\n\nconst virtualPublicPath = new VirtualModulesPlugin({\n  'node_modules/grafana-public-path.js': `\nimport amdMetaModule from 'amd-module';\n\n__webpack_public_path__ =\n  amdMetaModule && amdMetaModule.uri\n    ? amdMetaModule.uri.slice(0, amdMetaModule.uri.lastIndexOf('/') + 1)\n    : 'public/plugins/${pluginJson.id}/';\n`,\n});\n\nexport type Env = {\n  [key: string]: true | string | Env;\n};\n\nconst config = async (env: Env): Promise<Configuration> => {\n  const baseConfig: Configuration = {\n    cache: {\n      type: 'filesystem',\n      buildDependencies: {\n        // __filename doesn't work in Node 24\n        config: [path.resolve(process.cwd(), '.config', 'webpack', 'webpack.config.ts')],\n      },\n    },\n\n    context: path.join(process.cwd(), SOURCE_DIR),\n\n    devtool: env.production ? 'source-map' : 'eval-source-map',\n\n    entry: await getEntries(),\n\n    externals,\n\n    // Support WebAssembly according to latest spec - makes WebAssembly module async\n    experiments: {\n      asyncWebAssembly: true,\n    },\n\n    mode: env.production ? 'production' : 'development',\n\n    module: {\n      rules: [\n        // This must come first in the rules array otherwise it breaks sourcemaps.\n        {\n          test: /src\\/(?:.*\\/)?module\\.tsx?$/,\n          use: [\n            {\n              loader: 'imports-loader',\n              options: {\n                imports: `side-effects grafana-public-path`,\n              },\n            },\n          ],\n        },\n        {\n          exclude: /(node_modules)/,\n          test: /\\.[tj]sx?$/,\n          use: {\n            loader: 'swc-loader',\n            options: {\n              jsc: {\n                baseUrl: path.resolve(process.cwd(), SOURCE_DIR),\n                target: 'es2015',\n                loose: false,\n                parser: {\n                  syntax: 'typescript',\n                  tsx: true,\n                  decorators: false,\n                  dynamicImport: true,\n                },\n              },\n            },\n          },\n        },\n        {\n          test: /\\.css$/,\n          use: ['style-loader', 'css-loader'],\n        },\n        {\n          test: /\\.s[ac]ss$/,\n          use: ['style-loader', 'css-loader', 'sass-loader'],\n        },\n        {\n          test: /\\.(png|jpe?g|gif|svg)$/,\n          type: 'asset/resource',\n          generator: {\n            filename: Boolean(env.production) ? '[hash][ext]' : '[file]',\n          },\n        },\n        {\n          test: /\\.(woff|woff2|eot|ttf|otf)(\\?v=\\d+\\.\\d+\\.\\d+)?$/,\n          type: 'asset/resource',\n          generator: {\n            filename: Boolean(env.production) ? '[hash][ext]' : '[file]',\n          },\n        },\n      ],\n    },\n\n    optimization: {\n      minimize: Boolean(env.production),\n      minimizer: [\n        new TerserPlugin({\n          terserOptions: {\n            format: {\n              comments: (_, { type, value }) => type === 'comment2' && value.trim().startsWith('[create-plugin]'),\n            },\n            compress: {\n              drop_console: ['log', 'info'],\n            },\n          },\n        }),\n      ],\n    },\n\n    output: {\n      clean: {\n        keep: new RegExp(`(.*?_(amd64|arm(64)?)(.exe)?|go_plugin_build_manifest)`),\n      },\n      filename: '[name].js',\n      chunkFilename: env.production ? '[name].js?_cache=[contenthash]' : '[name].js',\n      library: {\n        type: 'amd',\n      },\n      path: path.resolve(process.cwd(), DIST_DIR),\n      publicPath: `public/plugins/${pluginJson.id}/`,\n      uniqueName: pluginJson.id,\n      crossOriginLoading: 'anonymous',\n    },\n\n    plugins: [\n      new BuildModeWebpackPlugin(),\n      virtualPublicPath,\n      // Insert create plugin version information into the bundle\n      new webpack.BannerPlugin({\n        banner: `/* [create-plugin] version: ${cpVersion} */\n          /* [create-plugin] plugin: ${pluginJson.id}@${pluginVersion} */`,\n        raw: true,\n        entryOnly: true,\n      }),\n      new CopyWebpackPlugin({\n        patterns: [\n          // If src/README.md exists use it; otherwise the root README\n          // To `compiler.options.output`\n          { from: hasReadme() ? 'README.md' : '../README.md', to: '.', force: true },\n          { from: 'plugin.json', to: '.' },\n          { from: '../LICENSE', to: '.' },\n          { from: '../CHANGELOG.md', to: '.', force: true },\n          { from: '**/*.json', to: '.' },\n          { from: '**/*.svg', to: '.', noErrorOnMissing: true },\n          { from: '**/*.png', to: '.', noErrorOnMissing: true },\n          { from: '**/*.html', to: '.', noErrorOnMissing: true },\n          { from: 'img/**/*', to: '.', noErrorOnMissing: true },\n          { from: 'libs/**/*', to: '.', noErrorOnMissing: true },\n          { from: 'static/**/*', to: '.', noErrorOnMissing: true },\n          { from: '**/query_help.md', to: '.', noErrorOnMissing: true },\n        ],\n      }),\n      // Replace certain template-variables in the README and plugin.json\n      new ReplaceInFileWebpackPlugin([\n        {\n          dir: DIST_DIR,\n          test: [/(^|\\/)plugin\\.json$/, /(^|\\/)README\\.md$/],\n\n          rules: [\n            {\n              search: /\\%VERSION\\%/g,\n              replace: pluginVersion,\n            },\n            {\n              search: /\\%TODAY\\%/g,\n              replace: new Date().toISOString().substring(0, 10),\n            },\n            {\n              search: /\\%PLUGIN_ID\\%/g,\n              replace: pluginJson.id,\n            },\n          ],\n        },\n      ]),\n      new SubresourceIntegrityPlugin({\n        hashFuncNames: ['sha256'],\n      }),\n      ...(env.development\n        ? [\n            new LiveReloadPlugin(),\n            new ForkTsCheckerWebpackPlugin({\n              async: Boolean(env.development),\n              issue: {\n                include: [{ file: '**/*.{ts,tsx}' }],\n              },\n              typescript: { configFile: path.join(process.cwd(), 'tsconfig.json') },\n            }),\n            new ESLintPlugin({\n              extensions: ['.ts', '.tsx'],\n              lintDirtyModulesOnly: Boolean(env.development), // don't lint on start, only lint changed files\n              failOnError: Boolean(env.production),\n            }),\n          ]\n        : []),\n    ],\n\n    resolve: {\n      extensions: ['.js', '.jsx', '.ts', '.tsx'],\n      // handle resolving \"rootDir\" paths\n      modules: [path.resolve(process.cwd(), 'src'), 'node_modules'],\n      unsafeCache: true,\n    },\n  };\n\n  if (isWSL()) {\n    baseConfig.watchOptions = {\n      poll: 3000,\n      ignored: /node_modules/,\n    };\n  }\n\n  return baseConfig;\n};\n\nexport default config;\n"
  },
  {
    "path": ".cprc.json",
    "content": "{\n  \"features\": {\n    \"bundleGrafanaUI\": false,\n    \"useReactRouterV6\": false,\n    \"useExperimentalRspack\": false\n  }\n}\n"
  },
  {
    "path": ".cursor/rules/word-list.mdc",
    "content": "---\ndescription: Project word list and preferred spelling\nalwaysApply: true\n---\n\n# Word list\n\nUse these spellings in this project (docs and code):\n\n- **drop-down** (not \"dropdown\") — e.g. \"the filter drop-down\", \"drop-downs and filters\"\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "# Lines starting with '#' are comments.\n# Each line is a file pattern followed by one or more owners.\n\n# More details are here: https://help.github.com/articles/about-codeowners/\n\n# The '*' pattern is global owners.\n\n# Order is important. The last matching pattern has the most precedence.\n# The folders are ordered as follows:\n\n# In each subsection folders are ordered first by depth, then alphabetically.\n# This should make it easy to add new rules without breaking existing ones.\n\n* @grafana/data-sources-plugins\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/1-bug_report.md",
    "content": "---\nname: Bug report\nabout: Report a bug you found when using this plugin\nlabels: ['datasource/ClickHouse', 'type/bug']\n---\n\n<!--\nPlease use this template to create your bug report. By providing as much info as possible you help us understand the issue, reproduce it and resolve it for you quicker. Therefore, take a couple of extra minutes to make sure you have provided all info needed.\n\nPROTIP: record your screen and attach it as a gif to showcase the issue.\n\n- Use query inspector to troubleshoot issues: https://bit.ly/2XNF6YS\n- How to record and attach gif: https://bit.ly/2Mi8T6K\n-->\n\n**What happened**:\n\n**What you expected to happen**:\n\n**How to reproduce it (as minimally and precisely as possible)**:\n\n<!--\nExample:\n\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n-->\n\n**Screenshots**\n\n<!--\nIf applicable, add screenshots to help explain your problem.\n-->\n\n**Anything else we need to know?**:\n\n**Environment**:\n\n- Grafana version:\n- Plugin version:\n- OS Grafana is installed on:\n- User OS & Browser:\n- Others:\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Feature Request\n    url: https://github.com/grafana/clickhouse-datasource/discussions/new\n    about: Discuss ideas for new features or changes\n  - name: Questions & Help\n    url: https://community.grafana.com\n    about: Please ask and answer questions here\n"
  },
  {
    "path": ".github/issue_commands.json",
    "content": "[\n  {\n    \"type\": \"label\",\n    \"name\": \"datasource/ClickHouse\",\n    \"action\": \"addToProject\",\n    \"addToProject\": {\n      \"url\": \"https://github.com/orgs/grafana/projects/190\"\n    }\n  },\n  {\n    \"type\": \"label\",\n    \"name\": \"datasource/ClickHouse\",\n    \"action\": \"removeFromProject\",\n    \"addToProject\": {\n      \"url\": \"https://github.com/orgs/grafana/projects/190\"\n    }\n  },\n  {\n    \"type\": \"label\",\n    \"name\": \"type/docs\",\n    \"action\": \"addToProject\",\n    \"addToProject\": {\n      \"url\": \"https://github.com/orgs/grafana/projects/69\"\n    }\n  },\n  {\n    \"type\": \"label\",\n    \"name\": \"type/docs\",\n    \"action\": \"removeFromProject\",\n    \"addToProject\": {\n      \"url\": \"https://github.com/orgs/grafana/projects/69\"\n    }\n  }\n]\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "<!-- Please take time to fill out the below template appropriately, deleting sections where necessary. -->\n<!-- Doing so will aid our reviewers in providing timely feedback and allow us to reach an outcome faster. Thanks! -->\n<!-- Please delete all sections that do not apply. -->\n<!-- To surface this PR in the changelog add the label: changelog -->\n<!-- If this PR is going in the changelog please make sure the title of the PR explains the feature in a user-centric way: -->\n<!-- Bad: fix state bug in hooks -->\n<!-- Good: Fix crash when switching from Query Builder -->\n\n## Type of Change\n\n_Please check the relevant option._\n\n- [ ] 🚀 Feature\n- [ ] 🐛 Bug Fix\n- [ ] 📝 Documentation\n- [ ] 🧹 Refactor / Chore\n\n---\n\n## Feature\n\n_If this is a feature, please complete this section. Otherwise, delete it._\n\n### What is this feature?\n\n_Provide a clear and concise description of the feature._\n\n### Why is this feature needed?\n\n_Explain the problem this feature solves or the value it provides._\n\n### Who is this feature for?\n\n_Describe the target users or use cases for this feature._\n\n### How to test this feature\n\n_Provide step-by-step instructions for reviewers to test the feature._\n\n1.\n2.\n3.\n\n---\n\n## Bug Fix\n\n_If this is a bug fix, please complete this section. Otherwise, delete it._\n\n### What is the bug?\n\n_Provide a clear and concise description of the bug._\n\n### How to reproduce\n\n_Provide step-by-step instructions to reproduce the bug._\n\n1.\n2.\n3.\n\n### Related Issues\n\n_Link to any open issues this PR will close. Use \"Closes #123\" or \"Fixes #123\" syntax._\n\nCloses #\n\n### Environment (if no related issue)\n\n_If there is no open issue, please specify the versions where the bug occurs._\n\n- **ClickHouse version**:\n- **Grafana version**:\n- **Plugin version**:\n\n---\n\n## Screenshots / Videos\n\n_Please provide screenshots or videos demonstrating the bug or the feature working. This helps reviewers understand the change visually and speeds up the review process._\n\n| Before | After |\n| ------ | ----- |\n|        |       |\n\n---\n\n## Please check that:\n\n- [ ] Tests for this change have been added/updated.\n- [ ] Documentation has been added/updated (where applicable).\n\n## Special notes for your reviewer\n\n_Use this section for any additional information that may help the reviewer, such as implementation decisions, areas of concern, or context that doesn't fit elsewhere. If you'd like to request review or feedback on a specific part of your changes, please note that here._\n"
  },
  {
    "path": ".github/release.yml",
    "content": "changelog:\n  categories:\n    - title: Copy the following lines for the CHANGELOG\n      labels:\n        - changelog\n    - title: Hidden\n      exclude:\n        labels:\n          - '*'\n"
  },
  {
    "path": ".github/renovate.json",
    "content": "{\n    \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n    \"extends\": [\n        \"github>grafana/grafana-renovate-config//presets/automerge\"\n    ],\n    \"platformAutomerge\": true,\n    \"packageRules\": [\n        {\n            \"description\": \"Disable minimum release age for Grafana owned npm packages (@grafana/*)\",\n            \"matchDatasources\": [\n                \"npm\"\n            ],\n            \"matchPackageNames\": [\n                \"@grafana/*\"\n            ],\n            \"minimumReleaseAge\": null\n        },\n        {\n            \"description\": \"Disable minimum release age for Grafana owned Go modules (github.com/grafana/*)\",\n            \"matchManagers\": [\n                \"gomod\"\n            ],\n            \"matchPackageNames\": [\n                \"github.com/grafana/*\"\n            ],\n            \"minimumReleaseAge\": null\n        },\n        {\n            \"description\": \"Disable minimum release age for Grafana owned GitHub Actions (grafana/*)\",\n            \"matchManagers\": [\n                \"github-actions\"\n            ],\n            \"matchPackageNames\": [\n                \"grafana/*\"\n            ],\n            \"minimumReleaseAge\": null\n        },\n        {\n            \"description\": \"Ignore major updates for dependencies that need to be kept in sync with Grafana\",\n            \"matchDatasources\": [\n                \"npm\"\n            ],\n            \"matchPackageNames\": [\n                \"react\",\n                \"react-dom\",\n                \"react-router-dom\",\n                \"react-router-dom-v5-compat\"\n            ],\n            \"matchUpdateTypes\": [\n                \"major\"\n            ],\n            \"enabled\": false\n        },\n        {\n            \"description\": \"Ignore major updates for dependencies that need to be kept in sync with the Node version defined in .nvmrc\",\n            \"matchDatasources\": [\n                \"npm\"\n            ],\n            \"matchPackageNames\": [\n                \"@types/node\"\n            ],\n            \"matchUpdateTypes\": [\n                \"major\"\n            ],\n            \"enabled\": false\n        },\n        {\n            \"description\": \"Keep rxjs in sync with the version used by @grafana/* packages\",\n            \"matchDatasources\": [\n                \"npm\"\n            ],\n            \"matchPackageNames\": [\n                \"rxjs\"\n            ],\n            \"enabled\": false\n        },\n        {\n            \"description\": \"Automerge minor dependencies\",\n            \"matchUpdateTypes\": [\n                \"minor\"\n            ],\n            \"automerge\": true,\n            \"labels\": [\n                \"automerge-minor\"\n            ]\n        }\n    ]\n}"
  },
  {
    "path": ".github/workflows/cron.yml",
    "content": "name: Scheduled Cloud E2E tests\n\non:\n  # Run nightly against the shared Cloud instance\n  schedule:\n    - cron: '0 9 * * *' # Daily at 09:00 UTC\n\n  # Allow engineers to run Cloud E2E on-demand (useful for debugging)\n  workflow_dispatch:\n\njobs:\n  playwright-cloud:\n    uses: grafana/data-sources-ci-workflows/.github/workflows/playwright-cloud.yml@main\n    secrets: inherit\n    with:\n      pdc-network-name: datasources-pdc-network-aws-datasourcese2e\n      repo-secrets: |\n        DS_INSTANCE_HOST=ds-instance:host\n        DS_INSTANCE_PASSWORD=ds-instance:password\n        DS_INSTANCE_PORT=ds-instance:port\n        DS_INSTANCE_USERNAME=ds-instance:username\n"
  },
  {
    "path": ".github/workflows/detect-breaking-changes.yml",
    "content": "name: Compatibility check\non: [push, pull_request]\n\njobs:\n  compatibilitycheck:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          persist-credentials: false\n\n      - uses: actions/setup-node@v6\n        with:\n          node-version-file: '.nvmrc'\n      - name: Install dependencies\n        run: npm install\n      - name: Build plugin\n        run: npm run build\n      - name: Compatibility check\n        uses: grafana/plugin-actions/is-compatible@4698961fa64137fcac207108397ebe8a738a33d1\n        with:\n          module: './src/module.ts'\n          comment-pr: 'yes'\n          skip-comment-if-compatible: 'yes'\n          fail-if-incompatible: 'no'\n          targets: '@grafana/data,@grafana/ui,@grafana/runtime,@grafana/e2e-selectors'\n"
  },
  {
    "path": ".github/workflows/integration.yml",
    "content": "name: Integration tests\n\non:\n  push:\n    branches:\n      - v1\n      - main\n  pull_request:\n    branches:\n      - v1\n      - main\n  schedule:\n    - cron: '0 9 1 * *'\n\njobs:\n  run:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: true\n      matrix:\n        clickhouse:\n          - 24.8\n          - 25.3\n          - 25.5\n          - 25.6\n          - 25.7\n          - latest\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          persist-credentials: false\n\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: 'go.mod'\n\n      - name: Build backend\n        uses: magefile/mage-action@07f03e200d4d168576899f4d31ffebfa8fb195ff\n        with:\n          args: buildAll\n          version: latest\n\n      - name: Run integration tests\n        run: CLICKHOUSE_VERSION=${{ matrix.clickhouse }} go test -v -tags=integration ./...\n"
  },
  {
    "path": ".github/workflows/issue_commands.yml",
    "content": "name: Run commands when issues are labeled\non:\n  issues:\n    types: [labeled]\njobs:\n  main:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout Actions\n        uses: actions/checkout@v6\n        with:\n          repository: 'grafana/grafana-github-actions'\n          path: ./actions\n          ref: main\n          persist-credentials: false\n      - name: Install Actions\n        run: npm install --production --prefix ./actions\n      - name: Run Commands\n        uses: ./actions/commands\n        with:\n          token: ${{secrets.ISSUE_COMMANDS_TOKEN}}\n          configPath: issue_commands\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Plugins - CD\nrun-name: Deploy ${{ inputs.branch }} to ${{ inputs.environment }} by @${{ github.actor }}\n\non:\n  workflow_dispatch:\n    inputs:\n      branch:\n        description: Branch to publish from. Can be used to deploy PRs to dev\n        default: main\n      environment:\n        description: Environment will always publish to all waves (dev + ops + prod). Cloud will publish scoped only to Grafana Cloud, On Prem will publish with Universal scope. Please use Cloud unless emergency fix needed for On Prem customer.\n        required: true\n        type: choice\n        default: \"cloud (recommended)\"\n        options:\n          - \"cloud (recommended)\"\n          - \"on-prem (for emergencies fix to On Prem customers)\"\n      docs-only:\n        description: Only publish docs, do not publish the plugin\n        default: false\n        type: boolean\n\npermissions: {}\n\njobs:\n  cd:\n    strategy:\n      fail-fast: false\n      matrix:\n        environment: [dev, ops, prod]\n    name: CD\n    uses: grafana/plugin-ci-workflows/.github/workflows/cd.yml@ci-cd-workflows/v7.3.1\n    permissions:\n      contents: write\n      id-token: write\n      attestations: write\n      pull-requests: read\n    with:\n      branch: ${{ github.event.inputs.branch }}\n      environment: ${{ matrix.environment }}\n      docs-only: ${{ fromJSON(github.event.inputs.docs-only) }}\n      scopes: ${{ github.event.inputs.environment == 'cloud (recommended)' && 'grafana_cloud' || github.event.inputs.environment == 'on-prem (for emergencies fix to On Prem customers)' && 'universal' }}\n      run-playwright: false\n      github-draft-release: false\n"
  },
  {
    "path": ".github/workflows/push.yml",
    "content": "name: Plugins - CI\n\non:\n  # Run CI on all PRs\n  pull_request:\n\n  # Also run on pushes to main (used for publish + downstream automation)\n  push:\n    branches: [main]\n\n  # Allow manual re-runs from the Actions UI (useful for debugging failures)\n  workflow_dispatch:\n\n# Minimal top-level permissions; jobs can extend as needed\npermissions:\n  contents: read\n  id-token: write # Required for OIDC (Vault / shared workflows)\n\n# Prevent duplicate runs on the same ref.\n# For PRs: cancel older in-progress runs when new commits are pushed.\n# For main: do NOT cancel (publishing should complete once started).\nconcurrency:\n  group: plugins-ci-${{ github.ref }}\n  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}\n\njobs:\n  ci:\n    name: CI\n\n    uses: grafana/plugin-ci-workflows/.github/workflows/ci.yml@ci-cd-workflows/v7.3.1\n\n    # Only run CI job for PRs / non-main refs.\n    # Main publishing is handled by the CD workflow below.\n    if: github.ref != 'refs/heads/main'\n\n    # Required for checkout + OIDC in shared workflows\n    permissions:\n      contents: read\n      id-token: write\n\n    with:\n      # Ensure PR builds produce unique plugin versions.\n      # For PR events, suffix with the head SHA; otherwise leave empty.\n      plugin-version-suffix: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }}\n\n  publish-and-deploy:\n    name: Publish to Dev Catalog and Deploy\n\n    # Main-only: publish the latest build and trigger Argo deployment\n    if: github.ref == 'refs/heads/main'\n\n    uses: grafana/data-sources-ci-workflows/.github/workflows/cd-dev.yml@main\n\n    permissions:\n      attestations: write\n      contents: write\n      id-token: write\n      pull-requests: read\n\n    with:\n      go-version: \"1.26.0\"\n      golangci-lint-version: \"2.10.1\"\n"
  },
  {
    "path": ".github/workflows/stale.yml",
    "content": "name: 'Close stale issues and PRs'\n\non:\n  schedule:\n    - cron: '30 1 * * *'\n  workflow_dispatch:\n\npermissions:\n  contents: write\n  issues: write\n  pull-requests: write\n\njobs:\n  stale:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/stale@v10\n        with:\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n          operations-per-run: 750\n          # start from the oldest issues/PRs when performing stale operations\n          ascending: true\n          days-before-issue-stale: 365\n          days-before-issue-close: 30\n          stale-issue-label: stale\n          exempt-issue-labels: no stalebot,type/epic\n          stale-issue-message: >\n            This issue has been automatically marked as stale because it has not had\n            activity in the last year. It will be closed in 30 days if no further activity occurs. Please\n            feel free to leave a comment if you believe the issue is still relevant.\n            Thank you for your contributions!\n          close-issue-message: >\n            This issue has been automatically closed because it has not had any further\n            activity in the last 30 days. Thank you for your contributions!\n          days-before-pr-stale: 30\n          days-before-pr-close: 14\n          stale-pr-label: stale\n          exempt-pr-labels: no stalebot\n          stale-pr-message: >\n            This pull request has been automatically marked as stale because it has not had\n            activity in the last 30 days. It will be closed in 2 weeks if no further activity occurs. Please\n            feel free to give a status update or ping for review. Thank you for your contributions!\n          close-pr-message: >\n            This pull request has been automatically closed because it has not had any further\n            activity in the last 2 weeks. Thank you for your contributions!\n          # Remove the PR head branch when the stale workflow closes the PR (requires contents: write)\n          delete-branch: true\n"
  },
  {
    "path": ".github/zizmor.yml",
    "content": "# This is also used as the default configuration for the Zizmor reusable\n# workflow.\n\nrules:\n  unpinned-uses:\n    config:\n      policies:\n        actions/*: any # trust GitHub\n        grafana/*: any # trust Grafana"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\nnode_modules/\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\ndist/\nartifacts/\nwork/\nci/\ne2e-results/\ntest_summary.json\n\n# Editor\n.idea\n\npkg/__debug_bin\n\n**/.DS_Store\n.eslintcache\n/test-results/\n/playwright-report/\n/blob-report/\n/playwright/.cache/\n/playwright/.auth/\n"
  },
  {
    "path": ".nvmrc",
    "content": "22\n"
  },
  {
    "path": ".prettierrc.js",
    "content": "module.exports = {\n  // Prettier configuration provided by Grafana scaffolding\n  ...require('./.config/.prettierrc.js'),\n};\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"name\": \"Run standalone plugin\",\n      \"type\": \"go\",\n      \"request\": \"launch\",\n      \"mode\": \"auto\",\n      \"program\": \"${workspaceFolder}/pkg/\",\n      \"env\": {},\n      \"args\": [\"--standalone=true\"]\n    },\n    {\n      \"name\": \"Debug in Container\",\n      \"type\": \"go\",\n      \"request\": \"attach\",\n      \"mode\": \"remote\",\n      \"remotePath\": \"/var/lib/grafana/plugins/grafana-k6-app/\",\n      \"port\": 2345,\n      \"host\": \"127.0.0.1\",\n      \"apiVersion\": 1,\n      \"trace\": \"verbose\"\n    },\n    {\n      \"name\": \"Debug Jest test\",\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"runtimeExecutable\": \"npm\",\n      \"runtimeArgs\": [\"run\", \"jest\", \"--runInBand\", \"${file}\"],\n      \"console\": \"integratedTerminal\",\n      \"internalConsoleOptions\": \"neverOpen\",\n      \"port\": 9229\n    }\n  ]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"gopls\": {\n    \"buildFlags\": [\"-tags=integration\"]\n  },\n  \"specstory.cloudSync.enabled\": \"never\"\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## 4.17.1\n\n### Fixes\n\n- Dependency updates\n\n## 4.17.0\n\n### Features\n\n- Allow `$__adhocFilters` macro to work with multiple tables (#1757)\n\n### Performance\n\n- Hold a single `*sql.DB` on `SchemaProvider` and dispose it with the instance (#1819)\n\n### Fixes\n\n- Dedupe autocomplete suggestions and dispose Monaco providers on unmount (#1820)\n- Map adhoc regex operators to `REGEXP` instead of `ILIKE` (#1794)\n- Close `*sql.DB` after each schema introspection query (#1818)\n- Dependency updates\n\n## 4.16.0\n\n### Features\n\n- Explain column roles in logs/traces/timeseries query builders (#1790)\n- Cache system.tables/columns schema introspection with single flight (#1787)\n\n### Fixes\n\n- Drop LIMIT from single-trace span query (#1795)\n- Forward X-Grafana-User header to ClickHouse when enabled (#1797)\n- Fix slow traceId lookup (#1705)\n- Replace `js-sql-parser` with existing ClickHouse lexer for SQL validation (#1778)\n- Match column suggestions case-insensitively in autocomplete (#1779)\n- Dependency updates\n\n## 4.15.0\n\n### Features\n\n- Add LogsSample supplemental query, remove deprecated getDataProvider (#1744)\n- Add Grafana query metadata support (#1743)\n- Allow disabling logs and trace links (#1754)\n- Improve query modification (#1738)\n- Add initial support for SQL abstractions (dsAbstraction) (#1756)\n- Add OpenTelemetry instrumentation (#1734)\n- Add field config to trace search results for better default display (#1733)\n- Add filterQuery method to datasource (#1732)\n- Support grouping orderBy columns by their column hint values (#1695)\n- Bump Grafana minimum version to v11.6.0 (#1753)\n\n### Fixes\n\n- Fix log context field returning labels (#1737)\n- Fix additional settings config bug (#1630)\n- Dependency updates\n\n## 4.14.1\n\n### Features\n\n- Extract `Map` value type for `mapKey` filter in `getFilters` (#1694)\n\n### Fixes\n\n- Dependency updates\n  \n## 4.14.0\n\n### Features\n\n- Add FilterTime hint to enable multi-timestamp log filtering/sorting (#1642)\n- Skip OTel trace time range optimization when trace timestamp table does not exist (#1663)\n\n### Fixes\n\n- Dependency updates\n- Add separate columns for Resource/Scope/Log Attributes (#1560)\n- Fix panic when configuring datasource with numeric timeout values (#1559)\n\n## 4.13.0\n\n### Features\n\n- Support for hiding table names in ad-hoc filters (#1493)\n- Allow manual placement of ad-hoc filters (#1488)\n- Add support for Nullable(Enum8/16) column types (#1523)\n- Add dashboard variable to control AdHoc filter syntax (#1464)\n\n### Fixes\n\n- Fix generating query with column names containing colons (#1538)\n- Update config HTTPS language (#1537)\n- Dependency updates\n\n## 4.12.0\n\n### Features\n\n- Support log volume queries for the SQL editor. Note that this will only work for Grafana versions >= 12.4.0 (#1475)\n- Support columns with `.` in ad-hoc filters (#1481)\n\n### Fixes\n\n- Dependency updates\n\n## 4.11.4\n\n## Fixes\n\n- Fix view logs link in Explore and Dashboards (#1462)\n- Fix filter for map type LogLabels (#1456)\n- Temporarily disable slow JSON suggestions function (#1468)\n- Dependency updates\n\n## 4.11.3\n\n### Fixes\n\n- Fix config UI bugs (#1409) and update design (#1422)\n- Dependency updates\n\n## 4.11.2\n\n### Features\n\n- Second part of redesigned ClickHouse config page (behind newClickhouseConfigPageDesign) (#1387)\n\n### Fixes\n\n- Improved error classification to mark all ClickHouse errors as downstream errors, including errors wrapped in HTTP response bodies and multi-error chains (#1405)\n- Dependency updates\n\n## 4.11.1\n\n### Fixes\n\n- All Clickhouse errors are marked as downstream errors for Grafana (#1378)\n\n## 4.11.0\n\n### Features\n\n- Merge OpenTelemetry resource/scope/log attributes into a unified Labels column in Logs (#1369)\n- First part of redesigned ClickHouse config page with sidebar navigation and collapsible sections (behind newClickhouseConfigPageDesign) (#1370)\n\n### Fixes\n\n- Fix ad-hoc filter application with templated target tables (#1241)\n- Fix column sorting by formatting bytes in Grafana (#1352)\n- Fix events and links not displaying correctly for table view queries (#1345)\n- Dependency updates\n\n## 4.10.2\n\n### Fixes\n\n- Fix Ad-Hoc filters for variable datasources #1330\n- Fix switching between SQL Editor and Query Builder #1337\n- Fix large JSON objects + complex JSON types #1326\n- Configuration fixes related to row limit implementation #1294\n- Fix bug where switched to logs query type errored #1341\n- Dependency updates\n\n## 4.10.1\n\n### Fixes\n\n- Bump grafana/plugin-actions from ff169fa386880e34ca85a49414e5a0ff84c3f7ad to b788be6746403ff9bae26d5e800794f2a5620b4c (#1286)\n- Bump cspell from 9.0.2 to 9.1.1 (#1278)\n\n## 4.10.0\n\n### Features\n\n- Ad-hoc queries: Allow to filter by values inside the map (#1265)\n\n### Fixes\n\n- Fix ad-hoc filter application with templated target tables (#1241)\n- Dependency updates\n\n## 4.9.1\n\n### Fixes\n\n- Error logging fix\n\n## 4.9.0\n\n### Features\n\n- Add support for the Grafana `row_limit` [configuration setting](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#row_limit).\n- Add support for kind, status, instrumentation library, links, events and state data for traces (#1043, #1208)\n- Cancel JSON paths query after 10s (#1206)\n- SQL Editor now suggests database, table, column, and function names while typing (#1204)\n- Add SQL Formatter button + shortcut for making long queries more readable in the editor (#1205)\n\n### Fixes\n\n- Fixed \"run query\" shortcut from running stale query (#1205)\n- Dependency updates\n\n## 4.8.2\n\n### Fixes\n\n- Dependency updates\n\n## 4.8.1\n\n### Fixes\n\n- Dependency updates\n\n## 4.8.0\n\n### Features\n\n- Enable CtrlCmd + Enter keyboard shortcut to Run Query (#1158)\n\n### Fixes\n\n- Refactor `MutateResponse` function and PDC dialler creation (#1155)\n- Refactor `clickhouse.Connect` to improve context cancellation handling (#1154)\n- Prevent usage of failed connections and improve connection management (#1156). Please note that following this change, the following limits will be set. Although we believe these limits are reasonable, you can adjust them in the datasource settings if needed:\n  - `MaxOpenConns` to 50.\n  - `MaxIdleConns` to 25.\n  - `ConnMaxLifetime` to 5 minutes.\n- Dependency updates\n\n## 4.7.0\n\n### Features\n\n- Add JSON column sub-paths to column selection in query builder\n- Added events support in trace detail view.(https://github.com/grafana/clickhouse-datasource/pull/1128)\n\n## 4.6.0\n\n### Features\n\n- Add support for new Variant, Dynamic, and JSON types (https://github.com/grafana/clickhouse-datasource/pull/1108)\n\n### Fixes\n\n- Optimized performance for log volumes processing using ClickHouse `multiSearchAny`\n\n## 4.5.1\n\n### Fixes\n\n- Dependency updates\n\n## 4.5.0\n\n### Features\n\n- Implemented log context for log queries\n- Added configuration options for log context columns\n- Queries parsed from the SQL editor will now attempt to re-map columns into their correct fields for Log and Trace queries.\n- Added support for IN operator in adhoc filters\n\n### Fixes\n\n- Fixed and enhanced the logic for parsing a query back into the query builder.\n\n## 4.4.0\n\n### Features\n\n- Added \"Labels\" column selector to the log query builder\n- Datasource OTel configuration will now set default table names for logs and traces.\n\n### Fixes\n\n- Added warning for when `uid` is missing in provisioned datasources.\n- Map filters in the query builder now correctly show the key instead of the column name\n- Updated and fixed missing `system.dashboards` dashboard in list of dashboards\n- Updated the duration value in example traces dashboard to provide useful information\n- Fix to display status codes from spans in trace queries (#950)\n\n## 4.3.2\n\n### Fixes\n\n- Optimized performance for types dependent on the JSON converter\n- Dependency updates\n\n## 4.3.1\n\n### Features\n\n- Added preset dashboard from `system.dashboards` table\n\n### Fixes\n\n- Fix trace start times in trace ID mode (#900)\n- Fixed OTel dashboard that waa failing to import (#908)\n\n## 4.3.0\n\n### Features\n\n- Added OpenTelemetry dashboard (#884)\n\n### Fixes\n\n- Fix support for LowCardinality strings (#857)\n- Update trace queries to better handle time fields (#890)\n- Dependency bumps\n\n## 4.2.0\n\n### Features\n\n- Added `$__dateTimeFilter()` macro for conveniently filtering a PRIMARY KEY composed of Date and DateTime columns.\n\n## 4.1.0\n\n### Features\n\n- Added the ability to define column alias tables in the config, which simplifies query syntax for tables with a known schema.\n\n## 4.0.8\n\n### Fixes\n\n- Fixed `IN` operator escaping the entire string (specifically with `Nullable(String)`), also added `FixedString(N)` (#830)\n- Fixed query builder filter editor on alert rules page (#828)\n\n## 4.0.7\n\n- Upgrade dependencies\n\n## 4.0.6\n\n### Fixes\n\n- Add support for configuring proxy options from context rather than environment variables (supported by updating `sqlds`) (#799)\n\n## 4.0.5\n\n### Fixes\n\n- Fixed converter regex for `Nullable(IP)` and `Nullable(String)`. It won't match to `Array(Nullable(IP))` or `Array(Nullable(String))` any more. (#783)\n- Updated `grafana-plugin-sdk-go` to fix a PDC issue. More details [here](https://github.com/grafana/grafana-plugin-sdk-go/releases/tag/v0.217.0) (#790)\n\n## 4.0.4\n\n### Fixes\n\n- Changed trace timestamp table from the constant `otel_traces_trace_id_ts` to a suffix `_trace_id_ts` applied to the current table name.\n\n## 4.0.3\n\n### Features\n\n- Added `$__fromTime_ms` macro that represents the dashboard \"from\" time in milliseconds using a `DateTime64(3)`\n- Added `$__toTime_ms` macro that represents the dashboard \"to\" time in milliseconds using a `DateTime64(3)`\n- Added `$__timeFilter_ms` macro that uses `DateTime64(3)` for millisecond precision time filtering\n- Re-added query type selector in dashboard view. This was only visible in explore view, but somehow it affects dashboard view, and so it has been re-added. (#730)\n- When OTel is enabled, Trace ID queries now use a skip index to optimize exact ID lookups on large trace datasets (#724)\n\n### Fixes\n\n- Fixed performance issues caused by `$__timeFilter` using a `DateTime64(3)` instead of `DateTime` (#699)\n- Fixed trace queries from rounding span durations under 1ms to `0` (#720)\n- Fixed AST error when including Grafana macros/variables in SQL (#714)\n- Fixed empty builder options when switching from SQL Editor back to Query Editor\n- Fix SQL Generator including \"undefined\" in `FROM` when database isn't defined\n- Allow adding spaces in multi filters (such as `WHERE .. IN`)\n- Fixed missing `AND` keyword when adding a filter to a Trace ID query\n\n## 4.0.2\n\n### Fixes\n\n- Fixed migration script not running when opening an existing v3 config\n\n## 4.0.1\n\n### Fixes\n\n- Set `protocol` to `native` by default in config view. Fixes the \"default port\" description.\n\n## 4.0.0\n\n### Features\n\nVersion 4.0.0 contains major revisions to the query builder and datasource configuration settings.\n\n#### Query Builder\n\n- Completely rebuilt query builder to have specialized editors for Table, Logs, Time Series, and Traces.\n- Completely rebuilt SQL generator to support more complicated and dynamic queries.\n- Updated query builder options structure to be clearer and support more complex queries.\n- Updated database/table selector to be in a more convenient location. Database and table options are automatically selected on initial load.\n- Upgraded query builder state management so queries stay consistent when saving/editing/sharing queries.\n- Separated Table and Time Series query builders. Table view operates as a catch-all for queries that don't fit the other query types.\n- Combined \"format\" into the query type switcher for simplicity. The query tab now changes the builder view and the display format when on the Explore page. This includes the raw SQL editor.\n- Added an OTEL switch for logs and trace views. This will allow for quicker query building for those using the OTEL exporter for ClickHouse.\n- Updated Time Series query builder with dedicated Time column. Default filters are added on-load.\n- Added an `IS ANYTHING` filter that acts as a placeholder for easily editing later (useful for query templates/bookmarks on the Explore page.)\n- Added better support for Map types on the Filter editor.\n- LIMIT editor can now be set to 0 to be excluded from the query.\n- Table and Time Series views now have a simple / aggregate mode, depending on the query complexity.\n- Updated the logs histogram query to use the new query builder options and column hints.\n- Added Logs query builder with dedicated Time, Level, and Message columns. Includes OTEL switch for automatically loading OTEL schema columns. Default filters are added on-load.\n- Added Trace query builder with dedicated trace columns. Includes OTEL switch for automatically loading OTEL schema columns. Default filters are added on-load.\n- Updated data panel filtering to append filters with column hints. Visible in logs view when filtering by a specific level. Instead of referencing a column by name, it will use its hint.\n- Order By now lists aggregates by their full name + alias.\n- Order By column allows for custom value to be typed in.\n- Aggregate column name allows for custom value to be typed in.\n- Filter editor allows for custom column names to be typed in.\n- Increased width of filter value text input.\n- Columns with the `Map*` type now show a `[]` at the end to indicate they are complex types. For example, `SpanAttributes[]`.\n- Filter editor now has a dedicated field for map key. You can now select a map column and its key separately. For example, `SpanAttributes['key']`.\n- Map types now load a sample of options when editing the `key` for the map. This doesn't include all unique values, but for most datasets it should be a convenience.\n- Added column hints, which offers better linking across query components when working with columns and filters. For example, a filter can be added for the `Time` column, even without knowing what the time column name is yet. This enables better SQL generation that is \"aware\" of a column's intended use.\n\n### Plugin Backend\n\n- Added migration logic for `v3` configs going to `v4+`. This is applied when the config is loaded when building a database connection.\n- `$__timeFilter`, `$__fromTime`, and `$__toTime` macros now convert to `DateTime64(3)` for better server-side type conversion. Also enables millisecond precision time range filtering.\n\n#### Datasource Configuration\n\n- Added migration script for `v3.x` configurations to `v4+`. This runs automatically when opening/saving the datasource configuration.\n- Renamed config value `server` to `host`.\n- Renamed config value `timeout` to the more specific `dial_timeout`.\n- Updated labeling for port selection. The default port will now change depending on native/http and secure/unsecure setting.\n- Rearranged fields and sections to flow better for initial setup of a new datasource.\n- Added plugin version to config data for easier config version migrations in the future.\n- Added fields for setting default values for database/table.\n- Added section for setting default log database/table/columns. Includes OTEL. These are used when using the log query builder.\n- Added section for setting default trace database/table/columns. Includes OTEL. These are used when using the trace query builder.\n- Added OTEL switches for logs/traces for quicker query building. OTEL defaults to the latest version, and will auto update if kept on this setting.\n- Increased width of inputs for typically long values (server URL, path, etc.)\n- Allow adding custom HTTP headers with either plain text or secure credentials. [#633](https://github.com/grafana/clickhouse-datasource/pull/633)\n- Add `path` setting to specify an additional URL path when using the HTTP protocol. [#512](https://github.com/grafana/clickhouse-datasource/pull/512)\n\n### Fixes\n\n- Queries will now remain consistent when reloading/editing a previously saved query.\n- Fixed default Ad-Hoc filters. [#650](https://github.com/grafana/clickhouse-datasource/pull/650)\n- Fixed Ad-Hoc filters parsing numeric fields. [#629](https://github.com/grafana/clickhouse-datasource/pull/629)\n- Fixed majority of usability quirks with redesigned query builder.\n\n### Upgrades\n\n- Updated all dependencies to latest compatible versions (Includes Dependabot PRs)\n\n## 3.3.0\n\n### Features\n\n- Support Point geo data type.\n\n### Fixes\n\n- Fix timeInterval_ms macro.\n- Fix Table summary and Parts over time panels in Data Analysis dashboard.\n\n### Upgrades\n\n- Upgrade [grafana-plugin-sdk-go](https://github.com/grafana/grafana-plugin-sdk-go).\n\n## 3.2.0\n\n### Features\n\n- Add `timeInterval_ms` macro to allow higher precision queries on DateTime64 columns. [#462](https://github.com/grafana/clickhouse-datasource/pull/462).\n\n### Fixes\n\n- Ensure databases, tables, and columns are escaped correctly. [#460](https://github.com/grafana/clickhouse-datasource/pull/460).\n- Fix conditionAll handling. [#459](https://github.com/grafana/clickhouse-datasource/pull/459).\n- Fix support for ad-hoc regexp filters: `=~`, `!~` [#414](https://github.com/grafana/clickhouse-datasource/pull/414).\n- Do not create malformed adhoc filters [#451](https://github.com/grafana/clickhouse-datasource/pull/451). invalid values will be ignored.\n- Fix auto formatting by reverting to table correctly. [#469](https://github.com/grafana/clickhouse-datasource/pull/469).\n- Fix parsing of numeric configuration values in `yaml` file. [#456](https://github.com/grafana/clickhouse-datasource/pull/456).\n\n## 3.1.0\n\n- Stable release of v3.0.4-beta\n\n## 3.0.4-beta\n\n- Update Grafana dependencies to >=v9.0.0\n- **Feature** - [Add support for the secure socks proxy](https://github.com/grafana/clickhouse-datasource/pull/389)\n\n## 3.0.3-beta\n\n- Update ClickHouse driver to v2.9.2\n\n## 3.0.2-beta\n\n- Custom ClickHouse settings can be set in data source settings. [Allow passing custom ClickHouse settings in datasource](https://github.com/grafana/clickhouse-datasource/pull/366)\n- Histogram UI fixes [Histogram UI fixes](https://github.com/grafana/clickhouse-datasource/pull/363)\n  - Support filter/filter out logs view actions\n  - Fix undefined database name by default\n  - Reset level and time field properly on table/database change\n  - Make it possible to clear the level field (so the histogram will render without grouping by level)\n  - Fix filter value that gets stuck in the UI\n- Tracing dashboard added to default dashboards. [Tracing dashboard ](https://github.com/grafana/clickhouse-datasource/pull/336)\n\n## 3.0.1-beta\n\n- Users on v8.x of Grafana are encouraged to continue to use v2.2.0 of the plugin.\n- Users of Grafana v9.x can use v3 however it is beta and may contain bugs.\n\n## 3.0.0\n\n- **Feature** - [Logs volume histogram support](https://github.com/grafana/clickhouse-datasource/pull/352)\n- **Chore** - Update clickhouse-go to v2.8.1\n\n## 2.2.1\n\n- **Chore** - Backend binaries compiled with latest go version 1.20.4\n- Custom ClickHouse settings can be set in data source settings. Allow passing custom [ClickHouse settings in datasource](https://github.com/grafana/clickhouse-datasource/pull/371)\n- Standard Golang HTTP proxy environment variables support (`HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY`). See [FromEnvironment](https://pkg.go.dev/golang.org/x/net/http/httpproxy#FromEnvironment) for more information. If the Grafana instance is started with one of these env variables, the driver will automatically load them now.\n\n## 2.2.0\n\n- **Feature** - [Support format dropdown and support for rendering traces](https://github.com/grafana/clickhouse-datasource/pull/329)\n\n## 2.1.1\n\n- **Fix** - [Date and Date32 type normalization with user's timezone](https://github.com/grafana/clickhouse-datasource/pull/314)\n\n## 2.1.0\n\n- **Fix** - Quote table names with dots by @slvrtrn in https://github.com/grafana/clickhouse-datasource/pull/298\n- Add a predefined TimeRange filter if there is at least one DateTime\\* column by @slvrtrn in https://github.com/grafana/clickhouse-datasource/pull/304\n\n## 2.0.7\n\n- **Fix** - Empty template variables used with the conditionalAll macro work the same as selecting All. [Allow empty Inputs for $\\_\\_conditionalAll](https://github.com/grafana/clickhouse-datasource/issues/262)\n- **Fix** - Intervals are limited to 1 second. [limit $\\_\\_interval_s to at least 1 second](https://github.com/grafana/clickhouse-datasource/pull/270)\n- **Chore** - Bump ClickHouse go API to v2.5.1 [Bump github.com/ClickHouse/clickhouse-go/v2 from 2.4.3 to 2.5.1](https://github.com/grafana/clickhouse-datasource/pull/283)\n\n## 2.0.6\n\n- **Chore** - Backend binaries compiled with latest go version 1.19.4\n- **Chore** - Backend grafana dependencies updated to latest version\n- **Chore** - Clickhouse-go client updated to [v2.4.3](https://github.com/ClickHouse/clickhouse-go/blob/main/CHANGELOG.md#243-2022-11-30)\n\n## 2.0.5\n\n- **Chore** - Update sqlds to 2.3.17 which fixes complex macro queries\n- **Chore** - Backend grafana dependency updated\n- **Fix** - Allow default protocol toggle value when saving in settings\n\n## 2.0.4\n\n- **Fix** - Query builder: allow custom filter values for fields with [`Map`](https://clickhouse.com/docs/en/sql-reference/data-types/map/) type\n\n## 2.0.3\n\n- **Chore** - Backend binaries compiled with latest go version 1.19.3\n- **Chore** - Backend grafana dependencies updated\n\n## 2.0.2\n\n- **Feature** - Update sqlds to 2.3.13 which fixes some macro queries\n\n## 2.0.1\n\n- **Bug** - Now works with Safari. Safari does not support regex look aheads\n\n## 2.0.0\n\n- **Feature** - Upgrade driver to support HTTP\n- **Feature** - Changed how ad hoc filters work with a settings option provided in CH 22.7\n- **Feature** - Conditional alls are now handled with a conditional all function. The function checks if the second parameter is a template var set to all, if it then replaces the function with 1=1, and if not set the function to the first parameter.\n- **Bug** - Visual query builder can use any date type for time field\n- **Fix** - 'any' is now an aggregation type in the visual query builder\n- **Fix** - Time filter macros can be used in the adhoc query\n- **Bug** - Time interval macro cannot have an interval of 0\n- **Fix** - Update drive to v2.1.0\n- **Bug** - Expand query button works with grafana 8.0+\n- **Fix** - Added adhoc columns macro\n\n## 1.1.2\n\n- **Bug** - Add timerange to metricFindQuery\n\n## 1.1.1\n\n- **Bug** - Add timeout\n\n## 1.1.0\n\n- **Feature** - Add convention for showing logs panel in Explore\n\n## 1.0.0\n\n- Official release\n\n## 0.12.7\n\n- **Fix** - Ignore template vars when validating sql\n\n## 0.12.6\n\n- **Fix** - Time series builder - use time alias when grouping/ordering\n\n## 0.12.5\n\n- **Chore** - Dashboards\n\n## 0.12.4\n\n- **Fix** - timeseries where clause. make default db the default in visual editor\n\n## 0.12.3\n\n- **Fix** - When removing conditional all, check scoped vars (support repeating panels)\n\n## 0.12.2\n\n- **Fix** - When removing conditional all, only remove lines with variables\n\n## 0.12.1\n\n- **Fix** - Handle large decimals properly\n\n## 0.12.0\n\n- **Feature** - Time series builder: use $\\_\\_timeInterval macro on time field so buckets can be adjusted from query options.\n\n## 0.11.0\n\n- **Feature** - Time series: Hide fields, use group by in select, use time field in group by\n\n## 0.10.0\n\n- **Feature** - Ad-Hoc sourced by database or table\n\n## 0.9.13\n\n- **Fix** - Update sdk to show streaming errors\n\n## 0.9.12\n\n- **Fix** - Format check after ast change\n\n## 0.9.11\n\n- **Feature** - $**timeInterval(column) and $**interval_s macros\n\n## 0.9.10\n\n- **Fix** - Set format when using the new Run Query button.\n\n## 0.9.9\n\n- **Feature** - Query Builder.\n\n## 0.9.8\n\n- **Fix** - Detect Multi-line time series. Handle cases with functions.\n\n## 0.9.7\n\n- **Feature** - Multi-line time series.\n\n## 0.9.6\n\n- **Bug** - Change time template variable names.\n\n## 0.9.5\n\n- **Bug** - Fix global template variables.\n\n## 0.9.4\n\n- **Bug** - Fix query type variables.\n\n## 0.9.3\n\n- **Bug** - Support Array data types.\n\n## 0.9.2\n\n- **Bug** - Fix TLS model.\n\n## 0.9.1\n\n- **Feature** - Add secure toggle to config editor.\n\n## 0.9.0\n\n- Initial Beta release.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to ClickHouse Datasource\n\nThank you for your interest in contributing to this repository. We are glad you want to help us to improve the project and join our community. Feel free to [browse the open issues](https://github.com/grafana/clickhouse-datasource/issues). If you want more straightforward tasks to complete, [we have some](https://github.com/grafana/clickhouse-datasource/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). For more details about how you can help, please take a look at [Grafana’s Contributing Guide](https://github.com/grafana/grafana/blob/main/CONTRIBUTING.md).\n\n## Development setup\n\n### Getting started\n\nClone this repository into your local environment. The frontend code lives in the `src` folder, alongside the [plugin.json file](https://grafana.com/docs/grafana/latest/developers/plugins/metadata/). The backend Go code is in the `pkg` folder. To build this plugin refer to [Build a plugin](https://grafana.com/docs/grafana/latest/developers/plugins/)\n\n### Running the development version\n\nBefore you can set up the plugin, you need to set up your environment by following [Set up your environment](https://grafana.com/tutorials/build-a-data-source-backend-plugin/#set-up-your-environment).\n\n#### Compiling the backend\n\nYou can use [mage](https://github.com/magefile/mage) to compile and test the Go backend.\n\n```sh\nmage test # run all Go test cases\nmage build:backend && mage reloadPlugin # builds and reloads the plugin in Grafana\n```\n\n#### Compiling the frontend\n\nYou can build and test the frontend by using `npm`:\n\n```sh\nnpm run test # run all test cases\nnpm run dev # builds and puts the output at ./dist\n```\n\nYou can also have `npm` watch for changes and automatically recompile them:\n\n```sh\nnpm run watch\n```\n\n#### Running E2E tests locally\n\n1. Install [K6](https://k6.io/docs/get-started/installation/)\n2. Run `npm run test:e2e:local`\n\n## Create a pull request\n\nRun `npm run lint` and `npm run prettier:check` to check for any style errors. Any PRs that have linter or `prettier` errors will not pass pull request CI checks. Run `npm run lint:fix && npm run prettier:write` to automatically fix linter or prettier errors.\n\nOnce you are ready to make a pull request, please read and follow [Create a pull request](https://github.com/grafana/grafana/blob/master/contribute/create-pull-request.md).\n\n## Build a release for the ClickHouse data source plugin\n\nYou need to have commit rights to the GitHub repository to publish a release.\n\n1. Update the version number in the `package.json` file.\n2. Update the `CHANGELOG.md` by copy and pasting the relevant PRs\n   from [GitHub's Release drafter interface](https://github.com/grafana/clickhouse-datasource/releases/new) or by\n   running `npm run generate-release-notes`.\n3. PR the changes.\n4. Once merged, follow the Drone release process that you can find [here](https://github.com/grafana/integrations-team/wiki/Plugin-Release-Process#drone-release-proces\n"
  },
  {
    "path": "DEV_GUIDE.md",
    "content": "# Guide to get Clickhouse running\n\n## Add a directory where the database files will mount to from your docker container\n\nmkdir $HOME/workspace/clickhouse/db/db\n\n## run clickhouse - expose ports and add a volume to your folder above\n\ndocker run -d -p 8123:8123 -p 9000:9000 --name grafana-clickhouse-server --ulimit nofile=262144:262144 --volume=$HOME/workspace/clickhouse/db/db:/var/lib/clickhouse clickhouse/clickhouse-server\n\n## clickhouse client (optional, if you want to query from the command line)\n\ndocker run -it --rm --network=container:grafana-clickhouse-server --entrypoint clickhouse-client clickhouse/clickhouse-server\n\n## Data loading - MGBench test data\n\n### Timeseries data - Brown benchmark using docker clickhouse client\n\nhttps://clickhouse.com/docs/en/getting-started/example-datasets/brown-benchmark/\n\n### Download and unpack the csv files from the above link\n\n### Create database and tables using commands from above link (can use a SQL Editor like DBeaver: https://dbeaver.io/)\n\n### Load the tables from the downloaded csv files\n\nsudo cat $HOME/workspace/clickhouse/mgbench/mgbench1.csv | docker run -i --rm --network=container:grafana-clickhouse-server --entrypoint clickhouse-client clickhouse/clickhouse-server -m --query=\"INSERT INTO mgbench.logs1 FORMAT CSVWithNames\"\n\nsudo cat $HOME/workspace/clickhouse/mgbench/mgbench2.csv | docker run -i --rm --network=container:grafana-clickhouse-server --entrypoint clickhouse-client clickhouse/clickhouse-server -m --query=\"INSERT INTO mgbench.logs2 FORMAT CSVWithNames\"\n\nsudo cat $HOME/workspace/clickhouse/mgbench/mgbench3.csv | docker run -i --rm --network=container:grafana-clickhouse-server --entrypoint clickhouse-client clickhouse/clickhouse-server -m --query=\"INSERT INTO mgbench.logs3 FORMAT CSVWithNames\"\n\n## Connect from the Plugin (minimum requirements)\n\nserver address: localhost\nserver port: 9000\n\n## With custom config\n\ndocker run -d -p 8443:8443 -p 9440:9440 --name secure-clickhouse-server --ulimit nofile=262144:262144 -v $PWD/config:/etc/clickhouse-server clickhouse/clickhouse-server\n\n## With secure config - for testing TLS scenarios\n\n### First setup the certificates\n\n1. Create the CA cert\n\n```\n./scripts/ca.sh\n```\n\n2. Create the Server cert from the CA\n\n```\n./scripts/ca-cert.sh\n```\n\n3. The Common/SAN name is \"foo\". Add an entry to your hosts file on the host.\n\n```\n127.0.0.1  foo\n```\n\n### Now start the container using the config-secure settings\n\ndocker run -d -p 8443:8443 -p 9440:9440 -p 9000:9000 -p 8123:8123 --name secure-clickhouse-server --ulimit nofile=262144:262144 -v $PWD/config-secure:/etc/clickhouse-server clickhouse/clickhouse-server\n\n### Login to the container and add the ca cert to trusted certs\n\ndocker exec -it secure-clickhouse-server bash\ncp /etc/clickhouse-server/my-own-ca.crt /usr/local/share/ca-certificates/root.ca.crt\nupdate-ca-certificates\n\n# Code Structure / Notes\n\n## Column Hints\n\nColumn hints are used within the query builder and SQL generator to enable flexible and dynamic queries.\n\nHere's an example of some column hints:\n\n```js\nColumnHint.Time;\nColumnHint.LogMessage;\nColumnHint.LogLevel;\nColumnHint.TraceId;\n```\n\nThe easiest example is the time hint (`ColumnHint.Time`). When building a Logs query, we need to know what the primary log time column is:\n\n```ts\nconst logTimeColumn: SelectedColumn = { name: 'my_time_column_on_my_table', hint: ColumnHint.Time, alias: 'logTime' };\n```\n\nUsing the column hint, we can add an `ORDER BY` statement to the query without having to know the actual column name:\n\n```ts\nconst logsOrderBy: OrderBy = { name: '', hint: ColumnHint.Time, dir: OrderByDirection.ASC };\n```\n\nNotice how `name` can be left empty, this is because the SQL generator knows to find the final column/alias by the time hint:\n\n```ts\n// Input options\nconst queryBuilderOptions: QueryBuilderOptions = {\n  table: 'logs',\n  columns: [logTimeColumn],\n  orderBy: [logsOrderBy],\n  . . .\n};\n```\n\n```sql\n-- Final output from SQL generator\nSELECT my_time_column_on_my_table as logTime FROM logs ORDER BY logTime ASC\n```\n\nBy adding a simple hint, we can apply filters, orderBys, and other behaviors to the SQL generator without having to reference specific columns. This simplifies the UI logic and user experience by reducing the number of places where a column name needs to be updated.\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"{}\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2022-2023 Grafana Labs\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "Magefile.go",
    "content": "//+build mage\n\npackage main\n\nimport (\n\t\"fmt\"\n\t// mage:import\n\tbuild \"github.com/grafana/grafana-plugin-sdk-go/build\"\n)\n\n// Hello prints a message (shows that you can define custom Mage targets).\nfunc Hello() {\n\tfmt.Println(\"hello plugin developer!\")\n}\n\n// Default configures the default target.\nvar Default = build.BuildAll\n"
  },
  {
    "path": "README.md",
    "content": "### ClickHouse Support\n\nGrafana supports ClickHouse through a plugin. You can perform a variety of simple or complex ClickHouse queries to visualize logs or metrics stored in ClickHouse. You can also annotate your graphs with events stored in ClickHouse.\n\nRead more about it here:  \n[Grafana ClickHouse Documentation](https://grafana.com/docs/plugins/grafana-clickhouse-datasource/latest/)\n"
  },
  {
    "path": "config/admin.xml",
    "content": "<clickhouse>\n    <!-- Profiles of settings. -->\n    <profiles>\n        <!-- Default settings. -->\n        <default>\n            <!-- Allows us to create replicated databases. -->\n            <allow_experimental_database_replicated>1</allow_experimental_database_replicated>\n        </default>\n    </profiles>\n    <users>\n        <default>\n            <access_management>1</access_management>\n        </default>\n    </users>\n</clickhouse>\n"
  },
  {
    "path": "config/config-preprocessed.xml",
    "content": "<!-- This file was generated automatically.\n     Do not edit it: it is likely to be discarded and generated again before it's read next time.\n     Files used to generate this file:\n       /etc/clickhouse-server/config.xml      -->\n\n<yandex>\n    <https_port>8443</https_port>\n    <tcp_port_secure>9440</tcp_port_secure>\n    <!--\n    <http_port>8123</http_port>\n    <tcp_port>9000</tcp_port>\n    <interserver_http_port>9009</interserver_http_port> \n    -->\n    <listen_host>0.0.0.0</listen_host>\n\n    <openSSL>\n      <server>\n        <!-- Used for https server AND secure tcp port -->\n        <certificateFile>/etc/clickhouse-server/server.crt</certificateFile>\n        <privateKeyFile>/etc/clickhouse-server/server.key</privateKeyFile>\n        <!-- <dhParamsFile>/etc/clickhouse-server/dhparam.pem</dhParamsFile> -->\n        <verificationMode>none</verificationMode>\n        <loadDefaultCAFile>true</loadDefaultCAFile>\n        <cacheSessions>true</cacheSessions> \n        <disableProtocols>sslv2,sslv3</disableProtocols> \n        <preferServerCiphers>true</preferServerCiphers>\n      </server>\n    </openSSL>\n</yandex>"
  },
  {
    "path": "config/config.xml",
    "content": "<?xml version=\"1.0\"?>\n<!--\n  NOTE: User and query level settings are set up in \"users.xml\" file.\n  If you have accidentally specified user-level settings here, server won't start.\n  You can either move the settings to the right place inside \"users.xml\" file\n   or add <skip_check_for_incorrect_settings>1</skip_check_for_incorrect_settings> here.\n-->\n<clickhouse>\n    <logger>\n        <!-- Possible levels [1]:\n          - none (turns off logging)\n          - fatal\n          - critical\n          - error\n          - warning\n          - notice\n          - information\n          - debug\n          - trace\n          - test (not for production usage)\n            [1]: https://github.com/pocoproject/poco/blob/poco-1.9.4-release/Foundation/include/Poco/Logger.h#L105-L114\n        -->\n        <level>trace</level>\n        <log>/etc/clickhouse-server/clickhouse-server.log</log>\n        <errorlog>/etc/clickhouse-server/clickhouse-server.err.log</errorlog>\n        <!-- Rotation policy\n             See https://github.com/pocoproject/poco/blob/poco-1.9.4-release/Foundation/include/Poco/FileChannel.h#L54-L85\n          -->\n        <size>1000M</size>\n        <count>10</count>\n        <!-- <console>1</console> --> <!-- Default behavior is autodetection (log to console if not daemon mode and is tty) -->\n\n        <!-- Per level overrides (legacy):\n        For example to suppress logging of the ConfigReloader you can use:\n        NOTE: levels.logger is reserved, see below.\n        -->\n        <!--\n        <levels>\n          <ConfigReloader>none</ConfigReloader>\n        </levels>\n        -->\n\n        <!-- Per level overrides:\n        For example to suppress logging of the RBAC for default user you can use:\n        (But please note that the logger name maybe changed from version to version, even after minor upgrade)\n        -->\n        <!--\n        <levels>\n          <logger>\n            <name>ContextAccess (default)</name>\n            <level>none</level>\n          </logger>\n          <logger>\n            <name>DatabaseOrdinary (test)</name>\n            <level>none</level>\n          </logger>\n        </levels>\n        -->\n    </logger>\n\n    <!-- Add headers to response in options request. OPTIONS method is used in CORS preflight requests. -->\n    <!-- It is off by default. Next headers are obligate for CORS.-->\n    <!-- http_options_response>\n        <header>\n            <name>Access-Control-Allow-Origin</name>\n            <value>*</value>\n        </header>\n        <header>\n            <name>Access-Control-Allow-Headers</name>\n            <value>origin, x-requested-with</value>\n        </header>\n        <header>\n            <name>Access-Control-Allow-Methods</name>\n            <value>POST, GET, OPTIONS</value>\n        </header>\n        <header>\n            <name>Access-Control-Max-Age</name>\n            <value>86400</value>\n        </header>\n    </http_options_response -->\n\n    <!-- It is the name that will be shown in the clickhouse-client.\n         By default, anything with \"production\" will be highlighted in red in query prompt.\n    -->\n    <!--display_name>production</display_name-->\n\n    <!-- Port for HTTP API. See also 'https_port' for secure connections.\n         This interface is also used by ODBC and JDBC drivers (DataGrip, Dbeaver, ...)\n         and by most of web interfaces (embedded UI, Grafana, Redash, ...).\n      -->\n    <http_port>8123</http_port>\n\n    <!-- Port for interaction by native protocol with:\n         - clickhouse-client and other native ClickHouse tools (clickhouse-benchmark, clickhouse-copier);\n         - clickhouse-server with other clickhouse-servers for distributed query processing;\n         - ClickHouse drivers and applications supporting native protocol\n         (this protocol is also informally called as \"the TCP protocol\");\n         See also 'tcp_port_secure' for secure connections.\n    -->\n    <tcp_port>9000</tcp_port>\n\n    <!-- Compatibility with MySQL protocol.\n         ClickHouse will pretend to be MySQL for applications connecting to this port.\n    -->\n    <mysql_port>9004</mysql_port>\n\n    <!-- Compatibility with PostgreSQL protocol.\n         ClickHouse will pretend to be PostgreSQL for applications connecting to this port.\n    -->\n    <postgresql_port>9005</postgresql_port>\n\n    <!-- HTTP API with TLS (HTTPS).\n         You have to configure certificate to enable this interface.\n         See the openSSL section below.\n    -->\n    <https_port>8443</https_port>\n\n    <!-- Native interface with TLS.\n         You have to configure certificate to enable this interface.\n         See the openSSL section below.\n    -->\n    <tcp_port_secure>9440</tcp_port_secure>\n\n    <!-- Native interface wrapped with PROXYv1 protocol\n         PROXYv1 header sent for every connection.\n         ClickHouse will extract information about proxy-forwarded client address from the header.\n    -->\n    <!-- <tcp_with_proxy_port>9011</tcp_with_proxy_port> -->\n\n    <!-- Port for communication between replicas. Used for data exchange.\n         It provides low-level data access between servers.\n         This port should not be accessible from untrusted networks.\n         See also 'interserver_http_credentials'.\n         Data transferred over connections to this port should not go through untrusted networks.\n         See also 'interserver_https_port'.\n      -->\n    <interserver_http_port>9009</interserver_http_port>\n\n    <!-- Port for communication between replicas with TLS.\n         You have to configure certificate to enable this interface.\n         See the openSSL section below.\n         See also 'interserver_http_credentials'.\n      -->\n    <!-- <interserver_https_port>9010</interserver_https_port> -->\n\n    <!-- Hostname that is used by other replicas to request this server.\n         If not specified, than it is determined analogous to 'hostname -f' command.\n         This setting could be used to switch replication to another network interface\n         (the server may be connected to multiple networks via multiple addresses)\n      -->\n    <!--\n    <interserver_http_host>example.yandex.ru</interserver_http_host>\n    -->\n\n    <!-- You can specify credentials for authenthication between replicas.\n         This is required when interserver_https_port is accessible from untrusted networks,\n         and also recommended to avoid SSRF attacks from possibly compromised services in your network.\n      -->\n    <!--<interserver_http_credentials>\n        <user>interserver</user>\n        <password></password>\n    </interserver_http_credentials>-->\n\n    <!-- Listen specified address.\n         Use :: (wildcard IPv6 address), if you want to accept connections both with IPv4 and IPv6 from everywhere.\n         Notes:\n         If you open connections from wildcard address, make sure that at least one of the following measures applied:\n         - server is protected by firewall and not accessible from untrusted networks;\n         - all users are restricted to subset of network addresses (see users.xml);\n         - all users have strong passwords, only secure (TLS) interfaces are accessible, or connections are only made via TLS interfaces.\n         - users without password have readonly access.\n         See also: https://www.shodan.io/search?query=clickhouse\n      -->\n    <!-- <listen_host>::</listen_host> -->\n\n    <!-- Same for hosts without support for IPv6: -->\n    <!-- <listen_host>0.0.0.0</listen_host> -->\n\n    <!-- Default values - try listen localhost on IPv4 and IPv6. -->\n    <!--\n    <listen_host>::1</listen_host>\n    <listen_host>127.0.0.1</listen_host>\n    -->\n\n    <listen_host>::</listen_host>\n    <listen_host>0.0.0.0</listen_host>\n    <listen_try>1</listen_try>\n\n    <!-- Don't exit if IPv6 or IPv4 networks are unavailable while trying to listen. -->\n    <!-- <listen_try>0</listen_try> -->\n\n    <!-- Allow multiple servers to listen on the same address:port. This is not recommended.\n      -->\n    <!-- <listen_reuse_port>0</listen_reuse_port> -->\n\n    <!-- <listen_backlog>4096</listen_backlog> -->\n\n    <max_connections>4096</max_connections>\n\n    <!-- For 'Connection: keep-alive' in HTTP 1.1 -->\n    <keep_alive_timeout>3</keep_alive_timeout>\n\n    <!-- gRPC protocol (see src/Server/grpc_protos/clickhouse_grpc.proto for the API) -->\n    <!-- <grpc_port>9100</grpc_port> -->\n    <grpc>\n        <enable_ssl>false</enable_ssl>\n\n        <!-- The following two files are used only if enable_ssl=1 -->\n        <ssl_cert_file>/path/to/ssl_cert_file</ssl_cert_file>\n        <ssl_key_file>/path/to/ssl_key_file</ssl_key_file>\n\n        <!-- Whether server will request client for a certificate -->\n        <ssl_require_client_auth>false</ssl_require_client_auth>\n\n        <!-- The following file is used only if ssl_require_client_auth=1 -->\n        <ssl_ca_cert_file>/path/to/ssl_ca_cert_file</ssl_ca_cert_file>\n\n        <!-- Default compression algorithm (applied if client doesn't specify another algorithm, see result_compression in QueryInfo).\n             Supported algorithms: none, deflate, gzip, stream_gzip -->\n        <compression>deflate</compression>\n\n        <!-- Default compression level (applied if client doesn't specify another level, see result_compression in QueryInfo).\n             Supported levels: none, low, medium, high -->\n        <compression_level>medium</compression_level>\n\n        <!-- Send/receive message size limits in bytes. -1 means unlimited -->\n        <max_send_message_size>-1</max_send_message_size>\n        <max_receive_message_size>-1</max_receive_message_size>\n\n        <!-- Enable if you want very detailed logs -->\n        <verbose_logs>false</verbose_logs>\n    </grpc>\n\n    <!-- Used with https_port and tcp_port_secure. Full ssl options list: https://github.com/ClickHouse-Extras/poco/blob/master/NetSSL_OpenSSL/include/Poco/Net/SSLManager.h#L71 -->\n    <openSSL>\n        <server> <!-- Used for https server AND secure tcp port -->\n            <!-- openssl req -subj \"/CN=localhost\" -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /etc/clickhouse-server/server.key -out /etc/clickhouse-server/server.crt -->\n            <certificateFile>/etc/clickhouse-server/server.crt</certificateFile>\n            <privateKeyFile>/etc/clickhouse-server/server.key</privateKeyFile>\n            <!-- dhparams are optional. You can delete the <dhParamsFile> element.\n                 To generate dhparams, use the following command:\n                  openssl dhparam -out /etc/clickhouse-server/dhparam.pem 4096\n                 Only file format with BEGIN DH PARAMETERS is supported.\n              -->\n            <!-- <dhParamsFile>/etc/clickhouse-server/dhparam.pem</dhParamsFile> -->\n            <verificationMode>none</verificationMode>\n            <loadDefaultCAFile>true</loadDefaultCAFile>\n            <cacheSessions>true</cacheSessions>\n            <disableProtocols>sslv2,sslv3</disableProtocols>\n            <preferServerCiphers>true</preferServerCiphers>\n        </server>\n\n        <client> <!-- Used for connecting to https dictionary source and secured Zookeeper communication -->\n            <loadDefaultCAFile>true</loadDefaultCAFile>\n            <cacheSessions>true</cacheSessions>\n            <disableProtocols>sslv2,sslv3</disableProtocols>\n            <preferServerCiphers>true</preferServerCiphers>\n            <!-- Use for self-signed: <verificationMode>none</verificationMode> -->\n            <invalidCertificateHandler>\n                <!-- Use for self-signed: <name>AcceptCertificateHandler</name> -->\n                <name>RejectCertificateHandler</name>\n            </invalidCertificateHandler>\n        </client>\n    </openSSL>\n\n    <!-- Default root page on http[s] server. For example load UI from https://tabix.io/ when opening http://localhost:8123 -->\n    <!--\n    <http_server_default_response><![CDATA[<html ng-app=\"SMI2\"><head><base href=\"http://ui.tabix.io/\"></head><body><div ui-view=\"\" class=\"content-ui\"></div><script src=\"http://loader.tabix.io/master.js\"></script></body></html>]]></http_server_default_response>\n    -->\n\n    <!-- Maximum number of concurrent queries. -->\n    <max_concurrent_queries>100</max_concurrent_queries>\n\n    <!-- Maximum memory usage (resident set size) for server process.\n         Zero value or unset means default. Default is \"max_server_memory_usage_to_ram_ratio\" of available physical RAM.\n         If the value is larger than \"max_server_memory_usage_to_ram_ratio\" of available physical RAM, it will be cut down.\n         The constraint is checked on query execution time.\n         If a query tries to allocate memory and the current memory usage plus allocation is greater\n          than specified threshold, exception will be thrown.\n         It is not practical to set this constraint to small values like just a few gigabytes,\n          because memory allocator will keep this amount of memory in caches and the server will deny service of queries.\n      -->\n    <max_server_memory_usage>0</max_server_memory_usage>\n\n    <!-- Maximum number of threads in the Global thread pool.\n    This will default to a maximum of 10000 threads if not specified.\n    This setting will be useful in scenarios where there are a large number\n    of distributed queries that are running concurrently but are idling most\n    of the time, in which case a higher number of threads might be required.\n    -->\n\n    <max_thread_pool_size>10000</max_thread_pool_size>\n\n    <!-- On memory constrained environments you may have to set this to value larger than 1.\n      -->\n    <max_server_memory_usage_to_ram_ratio>0.9</max_server_memory_usage_to_ram_ratio>\n\n    <!-- Simple server-wide memory profiler. Collect a stack trace at every peak allocation step (in bytes).\n         Data will be stored in system.trace_log table with query_id = empty string.\n         Zero means disabled.\n      -->\n    <total_memory_profiler_step>4194304</total_memory_profiler_step>\n\n    <!-- Collect random allocations and deallocations and write them into system.trace_log with 'MemorySample' trace_type.\n         The probability is for every alloc/free regardless to the size of the allocation.\n         Note that sampling happens only when the amount of untracked memory exceeds the untracked memory limit,\n          which is 4 MiB by default but can be lowered if 'total_memory_profiler_step' is lowered.\n         You may want to set 'total_memory_profiler_step' to 1 for extra fine grained sampling.\n      -->\n    <total_memory_tracker_sample_probability>0</total_memory_tracker_sample_probability>\n\n    <!-- Set limit on number of open files (default: maximum). This setting makes sense on Mac OS X because getrlimit() fails to retrieve\n         correct maximum value. -->\n    <!-- <max_open_files>262144</max_open_files> -->\n\n    <!-- Size of cache of uncompressed blocks of data, used in tables of MergeTree family.\n         In bytes. Cache is single for server. Memory is allocated only on demand.\n         Cache is used when 'use_uncompressed_cache' user setting turned on (off by default).\n         Uncompressed cache is advantageous only for very short queries and in rare cases.\n         Note: uncompressed cache can be pointless for lz4, because memory bandwidth\n         is slower than multi-core decompression on some server configurations.\n         Enabling it can sometimes paradoxically make queries slower.\n      -->\n    <uncompressed_cache_size>8589934592</uncompressed_cache_size>\n\n    <!-- Approximate size of mark cache, used in tables of MergeTree family.\n         In bytes. Cache is single for server. Memory is allocated only on demand.\n         You should not lower this value.\n      -->\n    <mark_cache_size>5368709120</mark_cache_size>\n\n\n    <!-- If you enable the `min_bytes_to_use_mmap_io` setting,\n         the data in MergeTree tables can be read with mmap to avoid copying from kernel to userspace.\n         It makes sense only for large files and helps only if data reside in page cache.\n         To avoid frequent open/mmap/munmap/close calls (which are very expensive due to consequent page faults)\n         and to reuse mappings from several threads and queries,\n         the cache of mapped files is maintained. Its size is the number of mapped regions (usually equal to the number of mapped files).\n         The amount of data in mapped files can be monitored\n         in system.metrics, system.metric_log by the MMappedFiles, MMappedFileBytes metrics\n         and in system.asynchronous_metrics, system.asynchronous_metrics_log by the MMapCacheCells metric,\n         and also in system.events, system.processes, system.query_log, system.query_thread_log, system.query_views_log by the\n         CreatedReadBufferMMap, CreatedReadBufferMMapFailed, MMappedFileCacheHits, MMappedFileCacheMisses events.\n         Note that the amount of data in mapped files does not consume memory directly and is not accounted\n         in query or server memory usage - because this memory can be discarded similar to OS page cache.\n         The cache is dropped (the files are closed) automatically on removal of old parts in MergeTree,\n         also it can be dropped manually by the SYSTEM DROP MMAP CACHE query.\n      -->\n    <mmap_cache_size>1000</mmap_cache_size>\n\n    <!-- Cache size in bytes for compiled expressions.-->\n    <compiled_expression_cache_size>134217728</compiled_expression_cache_size>\n\n    <!-- Cache size in elements for compiled expressions.-->\n    <compiled_expression_cache_elements_size>10000</compiled_expression_cache_elements_size>\n\n    <!-- Path to data directory, with trailing slash. -->\n    <path>/var/lib/clickhouse/</path>\n\n    <!-- Path to temporary data for processing hard queries. -->\n    <tmp_path>/var/lib/clickhouse/tmp/</tmp_path>\n\n    <!-- Policy from the <storage_configuration> for the temporary files.\n         If not set <tmp_path> is used, otherwise <tmp_path> is ignored.\n         Notes:\n         - move_factor              is ignored\n         - keep_free_space_bytes    is ignored\n         - max_data_part_size_bytes is ignored\n         - you must have exactly one volume in that policy\n    -->\n    <!-- <tmp_policy>tmp</tmp_policy> -->\n\n    <!-- Directory with user provided files that are accessible by 'file' table function. -->\n    <user_files_path>/var/lib/clickhouse/user_files/</user_files_path>\n\n    <!-- LDAP server definitions. -->\n    <ldap_servers>\n        <!-- List LDAP servers with their connection parameters here to later 1) use them as authenticators for dedicated local users,\n              who have 'ldap' authentication mechanism specified instead of 'password', or to 2) use them as remote user directories.\n             Parameters:\n                host - LDAP server hostname or IP, this parameter is mandatory and cannot be empty.\n                port - LDAP server port, default is 636 if enable_tls is set to true, 389 otherwise.\n                bind_dn - template used to construct the DN to bind to.\n                        The resulting DN will be constructed by replacing all '{user_name}' substrings of the template with the actual\n                         user name during each authentication attempt.\n                user_dn_detection - section with LDAP search parameters for detecting the actual user DN of the bound user.\n                        This is mainly used in search filters for further role mapping when the server is Active Directory. The\n                         resulting user DN will be used when replacing '{user_dn}' substrings wherever they are allowed. By default,\n                         user DN is set equal to bind DN, but once search is performed, it will be updated with to the actual detected\n                         user DN value.\n                    base_dn - template used to construct the base DN for the LDAP search.\n                            The resulting DN will be constructed by replacing all '{user_name}' and '{bind_dn}' substrings\n                             of the template with the actual user name and bind DN during the LDAP search.\n                    scope - scope of the LDAP search.\n                            Accepted values are: 'base', 'one_level', 'children', 'subtree' (the default).\n                    search_filter - template used to construct the search filter for the LDAP search.\n                            The resulting filter will be constructed by replacing all '{user_name}', '{bind_dn}', and '{base_dn}'\n                             substrings of the template with the actual user name, bind DN, and base DN during the LDAP search.\n                            Note, that the special characters must be escaped properly in XML.\n                verification_cooldown - a period of time, in seconds, after a successful bind attempt, during which a user will be assumed\n                         to be successfully authenticated for all consecutive requests without contacting the LDAP server.\n                        Specify 0 (the default) to disable caching and force contacting the LDAP server for each authentication request.\n                enable_tls - flag to trigger use of secure connection to the LDAP server.\n                        Specify 'no' for plain text (ldap://) protocol (not recommended).\n                        Specify 'yes' for LDAP over SSL/TLS (ldaps://) protocol (recommended, the default).\n                        Specify 'starttls' for legacy StartTLS protocol (plain text (ldap://) protocol, upgraded to TLS).\n                tls_minimum_protocol_version - the minimum protocol version of SSL/TLS.\n                        Accepted values are: 'ssl2', 'ssl3', 'tls1.0', 'tls1.1', 'tls1.2' (the default).\n                tls_require_cert - SSL/TLS peer certificate verification behavior.\n                        Accepted values are: 'never', 'allow', 'try', 'demand' (the default).\n                tls_cert_file - path to certificate file.\n                tls_key_file - path to certificate key file.\n                tls_ca_cert_file - path to CA certificate file.\n                tls_ca_cert_dir - path to the directory containing CA certificates.\n                tls_cipher_suite - allowed cipher suite (in OpenSSL notation).\n             Example:\n                <my_ldap_server>\n                    <host>localhost</host>\n                    <port>636</port>\n                    <bind_dn>uid={user_name},ou=users,dc=example,dc=com</bind_dn>\n                    <verification_cooldown>300</verification_cooldown>\n                    <enable_tls>yes</enable_tls>\n                    <tls_minimum_protocol_version>tls1.2</tls_minimum_protocol_version>\n                    <tls_require_cert>demand</tls_require_cert>\n                    <tls_cert_file>/path/to/tls_cert_file</tls_cert_file>\n                    <tls_key_file>/path/to/tls_key_file</tls_key_file>\n                    <tls_ca_cert_file>/path/to/tls_ca_cert_file</tls_ca_cert_file>\n                    <tls_ca_cert_dir>/path/to/tls_ca_cert_dir</tls_ca_cert_dir>\n                    <tls_cipher_suite>ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:AES256-GCM-SHA384</tls_cipher_suite>\n                </my_ldap_server>\n             Example (typical Active Directory with configured user DN detection for further role mapping):\n                <my_ad_server>\n                    <host>localhost</host>\n                    <port>389</port>\n                    <bind_dn>EXAMPLE\\{user_name}</bind_dn>\n                    <user_dn_detection>\n                        <base_dn>CN=Users,DC=example,DC=com</base_dn>\n                        <search_filter>(&amp;(objectClass=user)(sAMAccountName={user_name}))</search_filter>\n                    </user_dn_detection>\n                    <enable_tls>no</enable_tls>\n                </my_ad_server>\n        -->\n    </ldap_servers>\n\n    <!-- To enable Kerberos authentication support for HTTP requests (GSS-SPNEGO), for those users who are explicitly configured\n          to authenticate via Kerberos, define a single 'kerberos' section here.\n         Parameters:\n            principal - canonical service principal name, that will be acquired and used when accepting security contexts.\n                    This parameter is optional, if omitted, the default principal will be used.\n                    This parameter cannot be specified together with 'realm' parameter.\n            realm - a realm, that will be used to restrict authentication to only those requests whose initiator's realm matches it.\n                    This parameter is optional, if omitted, no additional filtering by realm will be applied.\n                    This parameter cannot be specified together with 'principal' parameter.\n         Example:\n            <kerberos />\n         Example:\n            <kerberos>\n                <principal>HTTP/clickhouse.example.com@EXAMPLE.COM</principal>\n            </kerberos>\n         Example:\n            <kerberos>\n                <realm>EXAMPLE.COM</realm>\n            </kerberos>\n    -->\n\n    <!-- Sources to read users, roles, access rights, profiles of settings, quotas. -->\n    <user_directories>\n        <users_xml>\n            <!-- Path to configuration file with predefined users. -->\n            <path>/etc/clickhouse-server/users.xml</path>\n        </users_xml>\n        <local_directory>\n            <!-- Path to folder where users created by SQL commands are stored. -->\n            <path>/var/lib/clickhouse/access/</path>\n        </local_directory>\n\n        <!-- To add an LDAP server as a remote user directory of users that are not defined locally, define a single 'ldap' section\n              with the following parameters:\n                server - one of LDAP server names defined in 'ldap_servers' config section above.\n                        This parameter is mandatory and cannot be empty.\n                roles - section with a list of locally defined roles that will be assigned to each user retrieved from the LDAP server.\n                        If no roles are specified here or assigned during role mapping (below), user will not be able to perform any\n                         actions after authentication.\n                role_mapping - section with LDAP search parameters and mapping rules.\n                        When a user authenticates, while still bound to LDAP, an LDAP search is performed using search_filter and the\n                         name of the logged in user. For each entry found during that search, the value of the specified attribute is\n                         extracted. For each attribute value that has the specified prefix, the prefix is removed, and the rest of the\n                         value becomes the name of a local role defined in ClickHouse, which is expected to be created beforehand by\n                         CREATE ROLE command.\n                        There can be multiple 'role_mapping' sections defined inside the same 'ldap' section. All of them will be\n                         applied.\n                    base_dn - template used to construct the base DN for the LDAP search.\n                            The resulting DN will be constructed by replacing all '{user_name}', '{bind_dn}', and '{user_dn}'\n                             substrings of the template with the actual user name, bind DN, and user DN during each LDAP search.\n                    scope - scope of the LDAP search.\n                            Accepted values are: 'base', 'one_level', 'children', 'subtree' (the default).\n                    search_filter - template used to construct the search filter for the LDAP search.\n                            The resulting filter will be constructed by replacing all '{user_name}', '{bind_dn}', '{user_dn}', and\n                             '{base_dn}' substrings of the template with the actual user name, bind DN, user DN, and base DN during\n                             each LDAP search.\n                            Note, that the special characters must be escaped properly in XML.\n                    attribute - attribute name whose values will be returned by the LDAP search. 'cn', by default.\n                    prefix - prefix, that will be expected to be in front of each string in the original list of strings returned by\n                             the LDAP search. Prefix will be removed from the original strings and resulting strings will be treated\n                             as local role names. Empty, by default.\n             Example:\n                <ldap>\n                    <server>my_ldap_server</server>\n                    <roles>\n                        <my_local_role1 />\n                        <my_local_role2 />\n                    </roles>\n                    <role_mapping>\n                        <base_dn>ou=groups,dc=example,dc=com</base_dn>\n                        <scope>subtree</scope>\n                        <search_filter>(&amp;(objectClass=groupOfNames)(member={bind_dn}))</search_filter>\n                        <attribute>cn</attribute>\n                        <prefix>clickhouse_</prefix>\n                    </role_mapping>\n                </ldap>\n             Example (typical Active Directory with role mapping that relies on the detected user DN):\n                <ldap>\n                    <server>my_ad_server</server>\n                    <role_mapping>\n                        <base_dn>CN=Users,DC=example,DC=com</base_dn>\n                        <attribute>CN</attribute>\n                        <scope>subtree</scope>\n                        <search_filter>(&amp;(objectClass=group)(member={user_dn}))</search_filter>\n                        <prefix>clickhouse_</prefix>\n                    </role_mapping>\n                </ldap>\n        -->\n    </user_directories>\n\n    <!-- Default profile of settings. -->\n    <default_profile>default</default_profile>\n\n    <!-- Comma-separated list of prefixes for user-defined settings. -->\n    <custom_settings_prefixes></custom_settings_prefixes>\n\n    <!-- System profile of settings. This settings are used by internal processes (Distributed DDL worker and so on). -->\n    <!-- <system_profile>default</system_profile> -->\n\n    <!-- Buffer profile of settings.\n         This settings are used by Buffer storage to flush data to the underlying table.\n         Default: used from system_profile directive.\n    -->\n    <!-- <buffer_profile>default</buffer_profile> -->\n\n    <!-- Default database. -->\n    <default_database>default</default_database>\n\n    <!-- Server time zone could be set here.\n         Time zone is used when converting between String and DateTime types,\n          when printing DateTime in text formats and parsing DateTime from text,\n          it is used in date and time related functions, if specific time zone was not passed as an argument.\n         Time zone is specified as identifier from IANA time zone database, like UTC or Africa/Abidjan.\n         If not specified, system time zone at server startup is used.\n         Please note, that server could display time zone alias instead of specified name.\n         Example: W-SU is an alias for Europe/Moscow and Zulu is an alias for UTC.\n    -->\n    <!-- <timezone>Europe/Moscow</timezone> -->\n\n    <!-- You can specify umask here (see \"man umask\"). Server will apply it on startup.\n         Number is always parsed as octal. Default umask is 027 (other users cannot read logs, data files, etc; group can only read).\n    -->\n    <!-- <umask>022</umask> -->\n\n    <!-- Perform mlockall after startup to lower first queries latency\n          and to prevent clickhouse executable from being paged out under high IO load.\n         Enabling this option is recommended but will lead to increased startup time for up to a few seconds.\n    -->\n    <mlock_executable>true</mlock_executable>\n\n    <!-- Reallocate memory for machine code (\"text\") using huge pages. Highly experimental. -->\n    <remap_executable>false</remap_executable>\n\n    <![CDATA[\n         Uncomment below in order to use JDBC table engine and function.\n         To install and run JDBC bridge in background:\n         * [Debian/Ubuntu]\n           export MVN_URL=https://repo1.maven.org/maven2/ru/yandex/clickhouse/clickhouse-jdbc-bridge\n           export PKG_VER=$(curl -sL $MVN_URL/maven-metadata.xml | grep '<release>' | sed -e 's|.*>\\(.*\\)<.*|\\1|')\n           wget https://github.com/ClickHouse/clickhouse-jdbc-bridge/releases/download/v$PKG_VER/clickhouse-jdbc-bridge_$PKG_VER-1_all.deb\n           apt install --no-install-recommends -f ./clickhouse-jdbc-bridge_$PKG_VER-1_all.deb\n           clickhouse-jdbc-bridge &\n         * [CentOS/RHEL]\n           export MVN_URL=https://repo1.maven.org/maven2/ru/yandex/clickhouse/clickhouse-jdbc-bridge\n           export PKG_VER=$(curl -sL $MVN_URL/maven-metadata.xml | grep '<release>' | sed -e 's|.*>\\(.*\\)<.*|\\1|')\n           wget https://github.com/ClickHouse/clickhouse-jdbc-bridge/releases/download/v$PKG_VER/clickhouse-jdbc-bridge-$PKG_VER-1.noarch.rpm\n           yum localinstall -y clickhouse-jdbc-bridge-$PKG_VER-1.noarch.rpm\n           clickhouse-jdbc-bridge &\n         Please refer to https://github.com/ClickHouse/clickhouse-jdbc-bridge#usage for more information.\n    ]]>\n    <!--\n    <jdbc_bridge>\n        <host>127.0.0.1</host>\n        <port>9019</port>\n    </jdbc_bridge>\n    -->\n\n    <!-- Configuration of clusters that could be used in Distributed tables.\n         https://clickhouse.com/docs/en/operations/table_engines/distributed/\n      -->\n    <remote_servers>\n        <!-- Test only shard config for testing distributed storage -->\n        <test_shard_localhost>\n            <!-- Inter-server per-cluster secret for Distributed queries\n                 default: no secret (no authentication will be performed)\n                 If set, then Distributed queries will be validated on shards, so at least:\n                 - such cluster should exist on the shard,\n                 - such cluster should have the same secret.\n                 And also (and which is more important), the initial_user will\n                 be used as current user for the query.\n                 Right now the protocol is pretty simple and it only takes into account:\n                 - cluster name\n                 - query\n                 Also it will be nice if the following will be implemented:\n                 - source hostname (see interserver_http_host), but then it will depends from DNS,\n                   it can use IP address instead, but then the you need to get correct on the initiator node.\n                 - target hostname / ip address (same notes as for source hostname)\n                 - time-based security tokens\n            -->\n            <!-- <secret></secret> -->\n\n            <shard>\n                <!-- Optional. Whether to write data to just one of the replicas. Default: false (write data to all replicas). -->\n                <!-- <internal_replication>false</internal_replication> -->\n                <!-- Optional. Shard weight when writing data. Default: 1. -->\n                <!-- <weight>1</weight> -->\n                <replica>\n                    <host>localhost</host>\n                    <port>9000</port>\n                    <!-- Optional. Priority of the replica for load_balancing. Default: 1 (less value has more priority). -->\n                    <!-- <priority>1</priority> -->\n                </replica>\n            </shard>\n        </test_shard_localhost>\n        <test_cluster_one_shard_three_replicas_localhost>\n            <shard>\n                <internal_replication>false</internal_replication>\n                <replica>\n                    <host>127.0.0.1</host>\n                    <port>9000</port>\n                </replica>\n                <replica>\n                    <host>127.0.0.2</host>\n                    <port>9000</port>\n                </replica>\n                <replica>\n                    <host>127.0.0.3</host>\n                    <port>9000</port>\n                </replica>\n            </shard>\n            <!--shard>\n                <internal_replication>false</internal_replication>\n                <replica>\n                    <host>127.0.0.1</host>\n                    <port>9000</port>\n                </replica>\n                <replica>\n                    <host>127.0.0.2</host>\n                    <port>9000</port>\n                </replica>\n                <replica>\n                    <host>127.0.0.3</host>\n                    <port>9000</port>\n                </replica>\n            </shard-->\n        </test_cluster_one_shard_three_replicas_localhost>\n        <test_cluster_two_shards_localhost>\n             <shard>\n                 <replica>\n                     <host>localhost</host>\n                     <port>9000</port>\n                 </replica>\n             </shard>\n             <shard>\n                 <replica>\n                     <host>localhost</host>\n                     <port>9000</port>\n                 </replica>\n             </shard>\n        </test_cluster_two_shards_localhost>\n        <test_cluster_two_shards>\n            <shard>\n                <replica>\n                    <host>127.0.0.1</host>\n                    <port>9000</port>\n                </replica>\n            </shard>\n            <shard>\n                <replica>\n                    <host>127.0.0.2</host>\n                    <port>9000</port>\n                </replica>\n            </shard>\n        </test_cluster_two_shards>\n        <test_cluster_two_shards_internal_replication>\n            <shard>\n                <internal_replication>true</internal_replication>\n                <replica>\n                    <host>127.0.0.1</host>\n                    <port>9000</port>\n                </replica>\n            </shard>\n            <shard>\n                <internal_replication>true</internal_replication>\n                <replica>\n                    <host>127.0.0.2</host>\n                    <port>9000</port>\n                </replica>\n            </shard>\n        </test_cluster_two_shards_internal_replication>\n        <test_shard_localhost_secure>\n            <shard>\n                <replica>\n                    <host>localhost</host>\n                    <port>9440</port>\n                    <secure>1</secure>\n                </replica>\n            </shard>\n        </test_shard_localhost_secure>\n        <test_unavailable_shard>\n            <shard>\n                <replica>\n                    <host>localhost</host>\n                    <port>9000</port>\n                </replica>\n            </shard>\n            <shard>\n                <replica>\n                    <host>localhost</host>\n                    <port>1</port>\n                </replica>\n            </shard>\n        </test_unavailable_shard>\n    </remote_servers>\n\n    <!-- The list of hosts allowed to use in URL-related storage engines and table functions.\n        If this section is not present in configuration, all hosts are allowed.\n    -->\n    <!--<remote_url_allow_hosts>-->\n        <!-- Host should be specified exactly as in URL. The name is checked before DNS resolution.\n            Example: \"yandex.ru\", \"yandex.ru.\" and \"www.yandex.ru\" are different hosts.\n                    If port is explicitly specified in URL, the host:port is checked as a whole.\n                    If host specified here without port, any port with this host allowed.\n                    \"yandex.ru\" -> \"yandex.ru:443\", \"yandex.ru:80\" etc. is allowed, but \"yandex.ru:80\" -> only \"yandex.ru:80\" is allowed.\n            If the host is specified as IP address, it is checked as specified in URL. Example: \"[2a02:6b8:a::a]\".\n            If there are redirects and support for redirects is enabled, every redirect (the Location field) is checked.\n            Host should be specified using the host xml tag:\n                    <host>yandex.ru</host>\n        -->\n\n        <!-- Regular expression can be specified. RE2 engine is used for regexps.\n            Regexps are not aligned: don't forget to add ^ and $. Also don't forget to escape dot (.) metacharacter\n            (forgetting to do so is a common source of error).\n        -->\n    <!--</remote_url_allow_hosts>-->\n\n    <!-- If element has 'incl' attribute, then for it's value will be used corresponding substitution from another file.\n         By default, path to file with substitutions is /etc/metrika.xml. It could be changed in config in 'include_from' element.\n         Values for substitutions are specified in /clickhouse/name_of_substitution elements in that file.\n      -->\n\n    <!-- ZooKeeper is used to store metadata about replicas, when using Replicated tables.\n         Optional. If you don't use replicated tables, you could omit that.\n         See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/\n      -->\n\n    <!--\n    <zookeeper>\n        <node>\n            <host>example1</host>\n            <port>2181</port>\n        </node>\n        <node>\n            <host>example2</host>\n            <port>2181</port>\n        </node>\n        <node>\n            <host>example3</host>\n            <port>2181</port>\n        </node>\n    </zookeeper>\n    -->\n\n    <!-- Substitutions for parameters of replicated tables.\n          Optional. If you don't use replicated tables, you could omit that.\n         See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/#creating-replicated-tables\n      -->\n    <!--\n    <macros>\n        <shard>01</shard>\n        <replica>example01-01-1</replica>\n    </macros>\n    -->\n\n\n    <!-- Reloading interval for embedded dictionaries, in seconds. Default: 3600. -->\n    <builtin_dictionaries_reload_interval>3600</builtin_dictionaries_reload_interval>\n\n\n    <!-- Maximum session timeout, in seconds. Default: 3600. -->\n    <max_session_timeout>3600</max_session_timeout>\n\n    <!-- Default session timeout, in seconds. Default: 60. -->\n    <default_session_timeout>60</default_session_timeout>\n\n    <!-- Sending data to Graphite for monitoring. Several sections can be defined. -->\n    <!--\n        interval - send every X second\n        root_path - prefix for keys\n        hostname_in_path - append hostname to root_path (default = true)\n        metrics - send data from table system.metrics\n        events - send data from table system.events\n        asynchronous_metrics - send data from table system.asynchronous_metrics\n    -->\n    <!--\n    <graphite>\n        <host>localhost</host>\n        <port>42000</port>\n        <timeout>0.1</timeout>\n        <interval>60</interval>\n        <root_path>one_min</root_path>\n        <hostname_in_path>true</hostname_in_path>\n        <metrics>true</metrics>\n        <events>true</events>\n        <events_cumulative>false</events_cumulative>\n        <asynchronous_metrics>true</asynchronous_metrics>\n    </graphite>\n    <graphite>\n        <host>localhost</host>\n        <port>42000</port>\n        <timeout>0.1</timeout>\n        <interval>1</interval>\n        <root_path>one_sec</root_path>\n        <metrics>true</metrics>\n        <events>true</events>\n        <events_cumulative>false</events_cumulative>\n        <asynchronous_metrics>false</asynchronous_metrics>\n    </graphite>\n    -->\n\n    <!-- Serve endpoint for Prometheus monitoring. -->\n    <!--\n        endpoint - mertics path (relative to root, statring with \"/\")\n        port - port to setup server. If not defined or 0 than http_port used\n        metrics - send data from table system.metrics\n        events - send data from table system.events\n        asynchronous_metrics - send data from table system.asynchronous_metrics\n        status_info - send data from different component from CH, ex: Dictionaries status\n    -->\n    <!--\n    <prometheus>\n        <endpoint>/metrics</endpoint>\n        <port>9363</port>\n        <metrics>true</metrics>\n        <events>true</events>\n        <asynchronous_metrics>true</asynchronous_metrics>\n        <status_info>true</status_info>\n    </prometheus>\n    -->\n\n    <!-- Query log. Used only for queries with setting log_queries = 1. -->\n    <query_log>\n        <!-- What table to insert data. If table is not exist, it will be created.\n             When query log structure is changed after system update,\n              then old table will be renamed and new table will be created automatically.\n        -->\n        <database>system</database>\n        <table>query_log</table>\n        <!--\n            PARTITION BY expr: https://clickhouse.com/docs/en/table_engines/mergetree-family/custom_partitioning_key/\n            Example:\n                event_date\n                toMonday(event_date)\n                toYYYYMM(event_date)\n                toStartOfHour(event_time)\n        -->\n        <partition_by>toYYYYMM(event_date)</partition_by>\n        <!--\n            Table TTL specification: https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/mergetree/#mergetree-table-ttl\n            Example:\n                event_date + INTERVAL 1 WEEK\n                event_date + INTERVAL 7 DAY DELETE\n                event_date + INTERVAL 2 WEEK TO DISK 'bbb'\n        <ttl>event_date + INTERVAL 30 DAY DELETE</ttl>\n        -->\n\n        <!-- Instead of partition_by, you can provide full engine expression (starting with ENGINE = ) with parameters,\n             Example: <engine>ENGINE = MergeTree PARTITION BY toYYYYMM(event_date) ORDER BY (event_date, event_time) SETTINGS index_granularity = 1024</engine>\n          -->\n\n        <!-- Interval of flushing data. -->\n        <flush_interval_milliseconds>7500</flush_interval_milliseconds>\n    </query_log>\n\n    <!-- Trace log. Stores stack traces collected by query profilers.\n         See query_profiler_real_time_period_ns and query_profiler_cpu_time_period_ns settings. -->\n    <trace_log>\n        <database>system</database>\n        <table>trace_log</table>\n\n        <partition_by>toYYYYMM(event_date)</partition_by>\n        <flush_interval_milliseconds>7500</flush_interval_milliseconds>\n    </trace_log>\n\n    <!-- Query thread log. Has information about all threads participated in query execution.\n         Used only for queries with setting log_query_threads = 1. -->\n    <query_thread_log>\n        <database>system</database>\n        <table>query_thread_log</table>\n        <partition_by>toYYYYMM(event_date)</partition_by>\n        <flush_interval_milliseconds>7500</flush_interval_milliseconds>\n    </query_thread_log>\n\n    <!-- Query views log. Has information about all dependent views associated with a query.\n         Used only for queries with setting log_query_views = 1. -->\n    <query_views_log>\n        <database>system</database>\n        <table>query_views_log</table>\n        <partition_by>toYYYYMM(event_date)</partition_by>\n        <flush_interval_milliseconds>7500</flush_interval_milliseconds>\n    </query_views_log>\n\n    <!-- Uncomment if use part log.\n         Part log contains information about all actions with parts in MergeTree tables (creation, deletion, merges, downloads).-->\n    <part_log>\n        <database>system</database>\n        <table>part_log</table>\n        <partition_by>toYYYYMM(event_date)</partition_by>\n        <flush_interval_milliseconds>7500</flush_interval_milliseconds>\n    </part_log>\n\n    <!-- Uncomment to write text log into table.\n         Text log contains all information from usual server log but stores it in structured and efficient way.\n         The level of the messages that goes to the table can be limited (<level>), if not specified all messages will go to the table.\n    <text_log>\n        <database>system</database>\n        <table>text_log</table>\n        <flush_interval_milliseconds>7500</flush_interval_milliseconds>\n        <level></level>\n    </text_log>\n    -->\n\n    <!-- Metric log contains rows with current values of ProfileEvents, CurrentMetrics collected with \"collect_interval_milliseconds\" interval. -->\n    <metric_log>\n        <database>system</database>\n        <table>metric_log</table>\n        <flush_interval_milliseconds>7500</flush_interval_milliseconds>\n        <collect_interval_milliseconds>1000</collect_interval_milliseconds>\n    </metric_log>\n\n    <!--\n        Asynchronous metric log contains values of metrics from\n        system.asynchronous_metrics.\n    -->\n    <asynchronous_metric_log>\n        <database>system</database>\n        <table>asynchronous_metric_log</table>\n        <!--\n            Asynchronous metrics are updated once a minute, so there is\n            no need to flush more often.\n        -->\n        <flush_interval_milliseconds>7000</flush_interval_milliseconds>\n    </asynchronous_metric_log>\n\n    <!--\n        OpenTelemetry log contains OpenTelemetry trace spans.\n    -->\n    <opentelemetry_span_log>\n        <!--\n            The default table creation code is insufficient, this <engine> spec\n            is a workaround. There is no 'event_time' for this log, but two times,\n            start and finish. It is sorted by finish time, to avoid inserting\n            data too far away in the past (probably we can sometimes insert a span\n            that is seconds earlier than the last span in the table, due to a race\n            between several spans inserted in parallel). This gives the spans a\n            global order that we can use to e.g. retry insertion into some external\n            system.\n        -->\n        <engine>\n            engine MergeTree\n            partition by toYYYYMM(finish_date)\n            order by (finish_date, finish_time_us, trace_id)\n        </engine>\n        <database>system</database>\n        <table>opentelemetry_span_log</table>\n        <flush_interval_milliseconds>7500</flush_interval_milliseconds>\n    </opentelemetry_span_log>\n\n\n    <!-- Crash log. Stores stack traces for fatal errors.\n         This table is normally empty. -->\n    <crash_log>\n        <database>system</database>\n        <table>crash_log</table>\n\n        <partition_by />\n        <flush_interval_milliseconds>1000</flush_interval_milliseconds>\n    </crash_log>\n\n    <!-- Session log. Stores user log in (successful or not) and log out events. -->\n    <session_log>\n        <database>system</database>\n        <table>session_log</table>\n\n        <partition_by>toYYYYMM(event_date)</partition_by>\n        <flush_interval_milliseconds>7500</flush_interval_milliseconds>\n    </session_log>\n\n    <!-- Parameters for embedded dictionaries, used in Yandex.Metrica.\n         See https://clickhouse.com/docs/en/dicts/internal_dicts/\n    -->\n\n    <!-- Path to file with region hierarchy. -->\n    <!-- <path_to_regions_hierarchy_file>/opt/geo/regions_hierarchy.txt</path_to_regions_hierarchy_file> -->\n\n    <!-- Path to directory with files containing names of regions -->\n    <!-- <path_to_regions_names_files>/opt/geo/</path_to_regions_names_files> -->\n\n\n    <!-- <top_level_domains_path>/var/lib/clickhouse/top_level_domains/</top_level_domains_path> -->\n    <!-- Custom TLD lists.\n         Format: <name>/path/to/file</name>\n         Changes will not be applied w/o server restart.\n         Path to the list is under top_level_domains_path (see above).\n    -->\n    <top_level_domains_lists>\n        <!--\n        <public_suffix_list>/path/to/public_suffix_list.dat</public_suffix_list>\n        -->\n    </top_level_domains_lists>\n\n    <!-- Configuration of external dictionaries. See:\n         https://clickhouse.com/docs/en/sql-reference/dictionaries/external-dictionaries/external-dicts\n    -->\n    <dictionaries_config>*_dictionary.xml</dictionaries_config>\n\n    <!-- Configuration of user defined executable functions -->\n    <user_defined_executable_functions_config>*_function.xml</user_defined_executable_functions_config>\n\n    <!-- Uncomment if you want data to be compressed 30-100% better.\n         Don't do that if you just started using ClickHouse.\n      -->\n    <!--\n    <compression>\n        <!- - Set of variants. Checked in order. Last matching case wins. If nothing matches, lz4 will be used. - ->\n        <case>\n            <!- - Conditions. All must be satisfied. Some conditions may be omitted. - ->\n            <min_part_size>10000000000</min_part_size>        <!- - Min part size in bytes. - ->\n            <min_part_size_ratio>0.01</min_part_size_ratio>   <!- - Min size of part relative to whole table size. - ->\n            <!- - What compression method to use. - ->\n            <method>zstd</method>\n        </case>\n    </compression>\n    -->\n\n    <!-- Configuration of encryption. The server executes a command to\n         obtain an encryption key at startup if such a command is\n         defined, or encryption codecs will be disabled otherwise. The\n         command is executed through /bin/sh and is expected to write\n         a Base64-encoded key to the stdout. -->\n    <encryption_codecs>\n        <!-- aes_128_gcm_siv -->\n            <!-- Example of getting hex key from env -->\n            <!-- the code should use this key and throw an exception if its length is not 16 bytes -->\n            <!--key_hex from_env=\"...\"></key_hex -->\n\n            <!-- Example of multiple hex keys. They can be imported from env or be written down in config-->\n            <!-- the code should use these keys and throw an exception if their length is not 16 bytes -->\n            <!-- key_hex id=\"0\">...</key_hex -->\n            <!-- key_hex id=\"1\" from_env=\"..\"></key_hex -->\n            <!-- key_hex id=\"2\">...</key_hex -->\n            <!-- current_key_id>2</current_key_id -->\n\n            <!-- Example of getting hex key from config -->\n            <!-- the code should use this key and throw an exception if its length is not 16 bytes -->\n            <!-- key>...</key -->\n\n            <!-- example of adding nonce -->\n            <!-- nonce>...</nonce -->\n\n        <!-- /aes_128_gcm_siv -->\n    </encryption_codecs>\n\n    <!-- Allow to execute distributed DDL queries (CREATE, DROP, ALTER, RENAME) on cluster.\n         Works only if ZooKeeper is enabled. Comment it if such functionality isn't required. -->\n    <distributed_ddl>\n        <!-- Path in ZooKeeper to queue with DDL queries -->\n        <path>/clickhouse/task_queue/ddl</path>\n\n        <!-- Settings from this profile will be used to execute DDL queries -->\n        <!-- <profile>default</profile> -->\n\n        <!-- Controls how much ON CLUSTER queries can be run simultaneously. -->\n        <!-- <pool_size>1</pool_size> -->\n\n        <!--\n             Cleanup settings (active tasks will not be removed)\n        -->\n\n        <!-- Controls task TTL (default 1 week) -->\n        <!-- <task_max_lifetime>604800</task_max_lifetime> -->\n\n        <!-- Controls how often cleanup should be performed (in seconds) -->\n        <!-- <cleanup_delay_period>60</cleanup_delay_period> -->\n\n        <!-- Controls how many tasks could be in the queue -->\n        <!-- <max_tasks_in_queue>1000</max_tasks_in_queue> -->\n    </distributed_ddl>\n\n    <!-- Settings to fine tune MergeTree tables. See documentation in source code, in MergeTreeSettings.h -->\n    <!--\n    <merge_tree>\n        <max_suspicious_broken_parts>5</max_suspicious_broken_parts>\n    </merge_tree>\n    -->\n\n    <!-- Protection from accidental DROP.\n         If size of a MergeTree table is greater than max_table_size_to_drop (in bytes) than table could not be dropped with any DROP query.\n         If you want do delete one table and don't want to change clickhouse-server config, you could create special file <clickhouse-path>/flags/force_drop_table and make DROP once.\n         By default max_table_size_to_drop is 50GB; max_table_size_to_drop=0 allows to DROP any tables.\n         The same for max_partition_size_to_drop.\n         Uncomment to disable protection.\n    -->\n    <!-- <max_table_size_to_drop>0</max_table_size_to_drop> -->\n    <!-- <max_partition_size_to_drop>0</max_partition_size_to_drop> -->\n\n    <!-- Example of parameters for GraphiteMergeTree table engine -->\n    <graphite_rollup_example>\n        <pattern>\n            <regexp>click_cost</regexp>\n            <function>any</function>\n            <retention>\n                <age>0</age>\n                <precision>3600</precision>\n            </retention>\n            <retention>\n                <age>86400</age>\n                <precision>60</precision>\n            </retention>\n        </pattern>\n        <default>\n            <function>max</function>\n            <retention>\n                <age>0</age>\n                <precision>60</precision>\n            </retention>\n            <retention>\n                <age>3600</age>\n                <precision>300</precision>\n            </retention>\n            <retention>\n                <age>86400</age>\n                <precision>3600</precision>\n            </retention>\n        </default>\n    </graphite_rollup_example>\n\n    <!-- Directory in <clickhouse-path> containing schema files for various input formats.\n         The directory will be created if it doesn't exist.\n      -->\n    <format_schema_path>/var/lib/clickhouse/format_schemas/</format_schema_path>\n\n    <!-- Default query masking rules, matching lines would be replaced with something else in the logs\n        (both text logs and system.query_log).\n        name - name for the rule (optional)\n        regexp - RE2 compatible regular expression (mandatory)\n        replace - substitution string for sensitive data (optional, by default - six asterisks)\n    -->\n    <query_masking_rules>\n        <rule>\n            <name>hide encrypt/decrypt arguments</name>\n            <regexp>((?:aes_)?(?:encrypt|decrypt)(?:_mysql)?)\\s*\\(\\s*(?:'(?:\\\\'|.)+'|.*?)\\s*\\)</regexp>\n            <!-- or more secure, but also more invasive:\n                (aes_\\w+)\\s*\\(.*\\)\n            -->\n            <replace>\\1(???)</replace>\n        </rule>\n    </query_masking_rules>\n\n    <!-- Uncomment to use custom http handlers.\n        rules are checked from top to bottom, first match runs the handler\n            url - to match request URL, you can use 'regex:' prefix to use regex match(optional)\n            methods - to match request method, you can use commas to separate multiple method matches(optional)\n            headers - to match request headers, match each child element(child element name is header name), you can use 'regex:' prefix to use regex match(optional)\n        handler is request handler\n            type - supported types: static, dynamic_query_handler, predefined_query_handler\n            query - use with predefined_query_handler type, executes query when the handler is called\n            query_param_name - use with dynamic_query_handler type, extracts and executes the value corresponding to the <query_param_name> value in HTTP request params\n            status - use with static type, response status code\n            content_type - use with static type, response content-type\n            response_content - use with static type, Response content sent to client, when using the prefix 'file://' or 'config://', find the content from the file or configuration send to client.\n    <http_handlers>\n        <rule>\n            <url>/</url>\n            <methods>POST,GET</methods>\n            <headers><pragma>no-cache</pragma></headers>\n            <handler>\n                <type>dynamic_query_handler</type>\n                <query_param_name>query</query_param_name>\n            </handler>\n        </rule>\n        <rule>\n            <url>/predefined_query</url>\n            <methods>POST,GET</methods>\n            <handler>\n                <type>predefined_query_handler</type>\n                <query>SELECT * FROM system.settings</query>\n            </handler>\n        </rule>\n        <rule>\n            <handler>\n                <type>static</type>\n                <status>200</status>\n                <content_type>text/plain; charset=UTF-8</content_type>\n                <response_content>config://http_server_default_response</response_content>\n            </handler>\n        </rule>\n    </http_handlers>\n    -->\n\n    <send_crash_reports>\n        <!-- Changing <enabled> to true allows sending crash reports to -->\n        <!-- the ClickHouse core developers team via Sentry https://sentry.io -->\n        <!-- Doing so at least in pre-production environments is highly appreciated -->\n        <enabled>false</enabled>\n        <!-- Change <anonymize> to true if you don't feel comfortable attaching the server hostname to the crash report -->\n        <anonymize>false</anonymize>\n        <!-- Default endpoint should be changed to different Sentry DSN only if you have -->\n        <!-- some in-house engineers or hired consultants who're going to debug ClickHouse issues for you -->\n        <endpoint>https://6f33034cfe684dd7a3ab9875e57b1c8d@o388870.ingest.sentry.io/5226277</endpoint>\n    </send_crash_reports>\n\n    <!-- Uncomment to disable ClickHouse internal DNS caching. -->\n    <!-- <disable_internal_dns_cache>1</disable_internal_dns_cache> -->\n\n    <!-- You can also configure rocksdb like this: -->\n    <!--\n    <rocksdb>\n        <options>\n            <max_background_jobs>8</max_background_jobs>\n        </options>\n        <column_family_options>\n            <num_levels>2</num_levels>\n        </column_family_options>\n        <tables>\n            <table>\n                <name>TABLE</name>\n                <options>\n                    <max_background_jobs>8</max_background_jobs>\n                </options>\n                <column_family_options>\n                    <num_levels>2</num_levels>\n                </column_family_options>\n            </table>\n        </tables>\n    </rocksdb>\n    -->\n</clickhouse>\n"
  },
  {
    "path": "config/custom.xml",
    "content": "<?xml version=\"1.0\" ?>\n<clickhouse>\n    <listen_host>::</listen_host>\n    <listen_host>0.0.0.0</listen_host>\n    <listen_try>1</listen_try>\n    <logger>\n        <console>1</console>\n    </logger>\n    <timezone>UTC</timezone>\n\n</clickhouse>\n"
  },
  {
    "path": "config/server.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIICqjCCAZICCQCmreUKLiQrrzANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxt\neS5ob3N0Lm5hbWUwHhcNMjExMjE1MjAzMTU2WhcNMjIxMjE1MjAzMTU2WjAXMRUw\nEwYDVQQDDAxteS5ob3N0Lm5hbWUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\nAoIBAQDBt3kbY3SwS89oNQhllMyK+AumX9d01bbU1ZFcLJcn9wyLK+wSFJTe6V8f\nhiHPZosnDcOWkspw/SBAVDCyvAx0QgjgkVj4JgCjxdXl25R9Ouz582hhTqqjpBCW\nn1ZKRhkau6ASHz4JF0MyemIdYB8DgnUKRfYnWtNrXS91RE/8H0U6s8kO3Msbj8GG\nubTBHOPWEJt6VAnWzVXXAGCNopNLF3Z7UUmJ7IMiTm923eGP0zZzvj5kMFLBiTm3\nsHtSN39PJ6evJ7oeO+l+JOtBkuaA3yYuPcAHNHpK+SffPulZpx7VxmY2ot7696xM\nkq2wl9uswOLCrn87hum2M+Bhy673AgMBAAEwDQYJKoZIhvcNAQELBQADggEBACA/\nVcLExf/XOmbueWvNg7IkBaUWL5waZf+MIlAuwaYjAsKSM/hYSY/g480XXFjN9Urm\nLJgIJ2T3EhGzqK+wdOITbEE7O1MM28TUex8giMKrl53XSbz2ni8dClehUJFch99D\n+tBVjihLJC+GP+M/jfCiWV45+MOc+Mqkz92MVNmltfhklH00IBz/a0RdD0XqdyqB\nAr/U19CgbZ7D/UAyjXnYjU1ucUBpMOmvfwdIABneeyLCzWQtH0vMaTrNmLsn6QwB\nC2nTdEeDIim9SZkRk5Fm1N4Ya6gWdP5PV6+yXdYSw/DtYsjwbNUfQMqiJVdQKQDM\ncuG+FV1/eq4dnDOYw3U=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "config/server.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDBt3kbY3SwS89o\nNQhllMyK+AumX9d01bbU1ZFcLJcn9wyLK+wSFJTe6V8fhiHPZosnDcOWkspw/SBA\nVDCyvAx0QgjgkVj4JgCjxdXl25R9Ouz582hhTqqjpBCWn1ZKRhkau6ASHz4JF0My\nemIdYB8DgnUKRfYnWtNrXS91RE/8H0U6s8kO3Msbj8GGubTBHOPWEJt6VAnWzVXX\nAGCNopNLF3Z7UUmJ7IMiTm923eGP0zZzvj5kMFLBiTm3sHtSN39PJ6evJ7oeO+l+\nJOtBkuaA3yYuPcAHNHpK+SffPulZpx7VxmY2ot7696xMkq2wl9uswOLCrn87hum2\nM+Bhy673AgMBAAECggEAOOWbs5itoE5T9+aDtdpTjYm3WkGSNeXDkpW74RfTudBN\nJd9bsh/LbgGbh9XMvm7+9hSL2wD4ZuFiBKL1vrmO6uKuWs82E4SN8Yxc++tXnMSe\n7/c3NEV3xyKcILFiFeSq4Pg01r3IacEkYoIhqUEfOtepasALwZliuYkgNFBBMeq1\njQWYs5k5h49etBc64ut+J++gFv77uJcmfetacKe4VxabX2iExW5SLmJQXLROC3o+\n9+r4bIt/jgufvODWZem0EeHWPSoL8CnT8/YCJCiX/cynyqt/LAEZGnqfHRKW3spI\nC/igOqzYM+86XLDG9HnV2jFGVU91euSQyLbAmLYYOQKBgQD7uQBAWCwRM6vb/6tq\ntga+wUhaHtynrvuLIg0lw9tUGzSxPIp9/FIGfVKhOuPBaNxRIIrInazorBwQxtFd\ncvCE+xTT2wcYRos5WxuSYiszIl4ltXWmzQZDnmcUubCcrJ9qfI3Q+dTFb0C0HYTz\n01WQAeqsEh3dALPVruEshHKcIwKBgQDFAiUWglhL/TyxcOZgyISbzJUvb0t1lA05\nyE2DsgKaIX8gFHIumCl15G2tqfDb+BfwWRfs/2VX187VYy97MoRNM9Lh4tUyWddV\noyZrE8t/T2MfoMF6QMKX7MU6y9eUdgIsklilJyOu2QFLu06hvF9S43bBxFgMLm3L\nW2UJz/x1HQKBgBLLLC6hpqCeJ/2j6AtujbBeQ+WemkDWuqcXor2oEs8DvPpil8By\nPzmGz82D1Q9SoehYsqPpycgRWYMTJPyCIVz8VgC/QJdaZPiiSbuzIqCNt1O/aYpL\nkmUoBXAxsPLxnHFZ3Ui17mHTPZR1A8EkjSXUTs4MCDjA3axdgyhMtzXbAoGBAI4e\nHv0e6G1g8GCcrkSRQkBWFCTU552ZQPU3Dtv7FS91DIzq0vfT4szeDVTjLBKy5SoI\nS183WjdFQjrjQ0RfS9uZj/5NsTiSYOmxOSyzafCcJ0iQoiH8B6SrNBhXJlw9yRG4\nPORe2LnwZ6PnKjE4f5d+6ZOcfVvEPoYdl0S92kPtAoGAMrWc/n/0dKAV9CSmLXS0\nAAqCtXw4OJNQBjCKks4DncWoopWvGHinVJAQYPrdnDO8iKcDYm6Kg+fVo0MWCha9\nopWIu4hgbcsH+wzTrgvUD4VlUdqAXqbDet6iZEu1IkSuGQqathAJk0ePa1swdl10\nN4SFUzUUwkhTFWW0BuMH0YU=\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "config/users.xml",
    "content": "<?xml version=\"1.0\"?>\n<clickhouse>\n    <!-- See also the files in users.d directory where the settings can be overridden. -->\n\n    <!-- Profiles of settings. -->\n    <profiles>\n        <!-- Default settings. -->\n        <default>\n            <!-- Maximum memory usage for processing single query, in bytes. -->\n            <max_memory_usage>10000000000</max_memory_usage>\n\n            <!-- How to choose between replicas during distributed query processing.\n                 random - choose random replica from set of replicas with minimum number of errors\n                 nearest_hostname - from set of replicas with minimum number of errors, choose replica\n                  with minimum number of different symbols between replica's hostname and local hostname\n                  (Hamming distance).\n                 in_order - first live replica is chosen in specified order.\n                 first_or_random - if first replica one has higher number of errors, pick a random one from replicas with minimum number of errors.\n            -->\n            <load_balancing>random</load_balancing>\n        </default>\n\n        <!-- Profile that allows only read queries. -->\n        <readonly>\n            <readonly>1</readonly>\n        </readonly>\n    </profiles>\n\n    <!-- Users and ACL. -->\n    <users>\n        <!-- If user name was not specified, 'default' user is used. -->\n        <default>\n            <!-- See also the files in users.d directory where the password can be overridden.\n                 Password could be specified in plaintext or in SHA256 (in hex format).\n                 If you want to specify password in plaintext (not recommended), place it in 'password' element.\n                 Example: <password>qwerty</password>.\n                 Password could be empty.\n                 If you want to specify SHA256, place it in 'password_sha256_hex' element.\n                 Example: <password_sha256_hex>65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5</password_sha256_hex>\n                 Restrictions of SHA256: impossibility to connect to ClickHouse using MySQL JS client (as of July 2019).\n                 If you want to specify double SHA1, place it in 'password_double_sha1_hex' element.\n                 Example: <password_double_sha1_hex>e395796d6546b1b65db9d665cd43f0e858dd4303</password_double_sha1_hex>\n                 If you want to specify a previously defined LDAP server (see 'ldap_servers' in the main config) for authentication,\n                  place its name in 'server' element inside 'ldap' element.\n                 Example: <ldap><server>my_ldap_server</server></ldap>\n                 If you want to authenticate the user via Kerberos (assuming Kerberos is enabled, see 'kerberos' in the main config),\n                  place 'kerberos' element instead of 'password' (and similar) elements.\n                 The name part of the canonical principal name of the initiator must match the user name for authentication to succeed.\n                 You can also place 'realm' element inside 'kerberos' element to further restrict authentication to only those requests\n                  whose initiator's realm matches it.\n                 Example: <kerberos />\n                 Example: <kerberos><realm>EXAMPLE.COM</realm></kerberos>\n                 How to generate decent password:\n                 Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo \"$PASSWORD\"; echo -n \"$PASSWORD\" | sha256sum | tr -d '-'\n                 In first line will be password and in second - corresponding SHA256.\n                 How to generate double SHA1:\n                 Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo \"$PASSWORD\"; echo -n \"$PASSWORD\" | sha1sum | tr -d '-' | xxd -r -p | sha1sum | tr -d '-'\n                 In first line will be password and in second - corresponding double SHA1.\n            -->\n            <password></password>\n\n            <!-- List of networks with open access.\n                 To open access from everywhere, specify:\n                    <ip>::/0</ip>\n                 To open access only from localhost, specify:\n                    <ip>::1</ip>\n                    <ip>127.0.0.1</ip>\n                 Each element of list has one of the following forms:\n                 <ip> IP-address or network mask. Examples: 213.180.204.3 or 10.0.0.1/8 or 10.0.0.1/255.255.255.0\n                     2a02:6b8::3 or 2a02:6b8::3/64 or 2a02:6b8::3/ffff:ffff:ffff:ffff::.\n                 <host> Hostname. Example: server01.yandex.ru.\n                     To check access, DNS query is performed, and all received addresses compared to peer address.\n                 <host_regexp> Regular expression for host names. Example, ^server\\d\\d-\\d\\d-\\d\\.yandex\\.ru$\n                     To check access, DNS PTR query is performed for peer address and then regexp is applied.\n                     Then, for result of PTR query, another DNS query is performed and all received addresses compared to peer address.\n                     Strongly recommended that regexp is ends with $\n                 All results of DNS requests are cached till server restart.\n            -->\n            <networks>\n                <ip>::/0</ip>\n            </networks>\n\n            <!-- Settings profile for user. -->\n            <profile>default</profile>\n\n            <!-- Quota for user. -->\n            <quota>default</quota>\n\n            <!-- User can create other users and grant rights to them. -->\n            <!-- <access_management>1</access_management> -->\n        </default>\n    </users>\n\n    <!-- Quotas. -->\n    <quotas>\n        <!-- Name of quota. -->\n        <default>\n            <!-- Limits for time interval. You could specify many intervals with different limits. -->\n            <interval>\n                <!-- Length of interval. -->\n                <duration>3600</duration>\n\n                <!-- No limits. Just calculate resource usage for time interval. -->\n                <queries>0</queries>\n                <errors>0</errors>\n                <result_rows>0</result_rows>\n                <read_rows>0</read_rows>\n                <execution_time>0</execution_time>\n            </interval>\n        </default>\n    </quotas>\n</clickhouse>"
  },
  {
    "path": "config-secure/config.xml",
    "content": "<?xml version=\"1.0\"?>\n<!--\n  NOTE: User and query level settings are set up in \"users.xml\" file.\n  If you have accidentally specified user-level settings here, server won't start.\n  You can either move the settings to the right place inside \"users.xml\" file\n   or add <skip_check_for_incorrect_settings>1</skip_check_for_incorrect_settings> here.\n-->\n<clickhouse>\n    <logger>\n        <!-- Possible levels [1]:\n          - none (turns off logging)\n          - fatal\n          - critical\n          - error\n          - warning\n          - notice\n          - information\n          - debug\n          - trace\n          - test (not for production usage)\n            [1]: https://github.com/pocoproject/poco/blob/poco-1.9.4-release/Foundation/include/Poco/Logger.h#L105-L114\n        -->\n        <level>trace</level>\n        <log>/etc/clickhouse-server/clickhouse-server.log</log>\n        <errorlog>/etc/clickhouse-server/clickhouse-server.err.log</errorlog>\n        <!-- Rotation policy\n             See https://github.com/pocoproject/poco/blob/poco-1.9.4-release/Foundation/include/Poco/FileChannel.h#L54-L85\n          -->\n        <size>1000M</size>\n        <count>10</count>\n        <!-- <console>1</console> --> <!-- Default behavior is autodetection (log to console if not daemon mode and is tty) -->\n\n        <!-- Per level overrides (legacy):\n        For example to suppress logging of the ConfigReloader you can use:\n        NOTE: levels.logger is reserved, see below.\n        -->\n        <!--\n        <levels>\n          <ConfigReloader>none</ConfigReloader>\n        </levels>\n        -->\n\n        <!-- Per level overrides:\n        For example to suppress logging of the RBAC for default user you can use:\n        (But please note that the logger name maybe changed from version to version, even after minor upgrade)\n        -->\n        <!--\n        <levels>\n          <logger>\n            <name>ContextAccess (default)</name>\n            <level>none</level>\n          </logger>\n          <logger>\n            <name>DatabaseOrdinary (test)</name>\n            <level>none</level>\n          </logger>\n        </levels>\n        -->\n    </logger>\n\n    <!-- Add headers to response in options request. OPTIONS method is used in CORS preflight requests. -->\n    <!-- It is off by default. Next headers are obligate for CORS.-->\n    <!-- http_options_response>\n        <header>\n            <name>Access-Control-Allow-Origin</name>\n            <value>*</value>\n        </header>\n        <header>\n            <name>Access-Control-Allow-Headers</name>\n            <value>origin, x-requested-with</value>\n        </header>\n        <header>\n            <name>Access-Control-Allow-Methods</name>\n            <value>POST, GET, OPTIONS</value>\n        </header>\n        <header>\n            <name>Access-Control-Max-Age</name>\n            <value>86400</value>\n        </header>\n    </http_options_response -->\n\n    <!-- It is the name that will be shown in the clickhouse-client.\n         By default, anything with \"production\" will be highlighted in red in query prompt.\n    -->\n    <!--display_name>production</display_name-->\n\n    <!-- Port for HTTP API. See also 'https_port' for secure connections.\n         This interface is also used by ODBC and JDBC drivers (DataGrip, Dbeaver, ...)\n         and by most of web interfaces (embedded UI, Grafana, Redash, ...).\n      -->\n    <http_port>8123</http_port>\n\n    <!-- Port for interaction by native protocol with:\n         - clickhouse-client and other native ClickHouse tools (clickhouse-benchmark, clickhouse-copier);\n         - clickhouse-server with other clickhouse-servers for distributed query processing;\n         - ClickHouse drivers and applications supporting native protocol\n         (this protocol is also informally called as \"the TCP protocol\");\n         See also 'tcp_port_secure' for secure connections.\n    -->\n    <tcp_port>9000</tcp_port>\n\n    <!-- Compatibility with MySQL protocol.\n         ClickHouse will pretend to be MySQL for applications connecting to this port.\n    -->\n    <mysql_port>9004</mysql_port>\n\n    <!-- Compatibility with PostgreSQL protocol.\n         ClickHouse will pretend to be PostgreSQL for applications connecting to this port.\n    -->\n    <postgresql_port>9005</postgresql_port>\n\n    <!-- HTTP API with TLS (HTTPS).\n         You have to configure certificate to enable this interface.\n         See the openSSL section below.\n    -->\n    <https_port>8443</https_port>\n\n    <!-- Native interface with TLS.\n         You have to configure certificate to enable this interface.\n         See the openSSL section below.\n    -->\n    <tcp_port_secure>9440</tcp_port_secure>\n\n    <!-- Native interface wrapped with PROXYv1 protocol\n         PROXYv1 header sent for every connection.\n         ClickHouse will extract information about proxy-forwarded client address from the header.\n    -->\n    <!-- <tcp_with_proxy_port>9011</tcp_with_proxy_port> -->\n\n    <!-- Port for communication between replicas. Used for data exchange.\n         It provides low-level data access between servers.\n         This port should not be accessible from untrusted networks.\n         See also 'interserver_http_credentials'.\n         Data transferred over connections to this port should not go through untrusted networks.\n         See also 'interserver_https_port'.\n      -->\n    <interserver_http_port>9009</interserver_http_port>\n\n    <!-- Port for communication between replicas with TLS.\n         You have to configure certificate to enable this interface.\n         See the openSSL section below.\n         See also 'interserver_http_credentials'.\n      -->\n    <!-- <interserver_https_port>9010</interserver_https_port> -->\n\n    <!-- Hostname that is used by other replicas to request this server.\n         If not specified, than it is determined analogous to 'hostname -f' command.\n         This setting could be used to switch replication to another network interface\n         (the server may be connected to multiple networks via multiple addresses)\n      -->\n    <!--\n    <interserver_http_host>example.yandex.ru</interserver_http_host>\n    -->\n\n    <!-- You can specify credentials for authenthication between replicas.\n         This is required when interserver_https_port is accessible from untrusted networks,\n         and also recommended to avoid SSRF attacks from possibly compromised services in your network.\n      -->\n    <!--<interserver_http_credentials>\n        <user>interserver</user>\n        <password></password>\n    </interserver_http_credentials>-->\n\n    <!-- Listen specified address.\n         Use :: (wildcard IPv6 address), if you want to accept connections both with IPv4 and IPv6 from everywhere.\n         Notes:\n         If you open connections from wildcard address, make sure that at least one of the following measures applied:\n         - server is protected by firewall and not accessible from untrusted networks;\n         - all users are restricted to subset of network addresses (see users.xml);\n         - all users have strong passwords, only secure (TLS) interfaces are accessible, or connections are only made via TLS interfaces.\n         - users without password have readonly access.\n         See also: https://www.shodan.io/search?query=clickhouse\n      -->\n    <!-- <listen_host>::</listen_host> -->\n\n    <!-- Same for hosts without support for IPv6: -->\n    <!-- <listen_host>0.0.0.0</listen_host> -->\n\n    <!-- Default values - try listen localhost on IPv4 and IPv6. -->\n    <!--\n    <listen_host>::1</listen_host>\n    <listen_host>127.0.0.1</listen_host>\n    -->\n\n    <listen_host>::</listen_host>\n    <listen_host>0.0.0.0</listen_host>\n    <listen_try>1</listen_try>\n\n    <!-- Don't exit if IPv6 or IPv4 networks are unavailable while trying to listen. -->\n    <!-- <listen_try>0</listen_try> -->\n\n    <!-- Allow multiple servers to listen on the same address:port. This is not recommended.\n      -->\n    <!-- <listen_reuse_port>0</listen_reuse_port> -->\n\n    <!-- <listen_backlog>4096</listen_backlog> -->\n\n    <max_connections>4096</max_connections>\n\n    <!-- For 'Connection: keep-alive' in HTTP 1.1 -->\n    <keep_alive_timeout>3</keep_alive_timeout>\n\n    <!-- gRPC protocol (see src/Server/grpc_protos/clickhouse_grpc.proto for the API) -->\n    <!-- <grpc_port>9100</grpc_port> -->\n    <grpc>\n        <enable_ssl>false</enable_ssl>\n\n        <!-- The following two files are used only if enable_ssl=1 -->\n        <ssl_cert_file>/path/to/ssl_cert_file</ssl_cert_file>\n        <ssl_key_file>/path/to/ssl_key_file</ssl_key_file>\n\n        <!-- Whether server will request client for a certificate -->\n        <ssl_require_client_auth>false</ssl_require_client_auth>\n\n        <!-- The following file is used only if ssl_require_client_auth=1 -->\n        <ssl_ca_cert_file>/path/to/ssl_ca_cert_file</ssl_ca_cert_file>\n\n        <!-- Default compression algorithm (applied if client doesn't specify another algorithm, see result_compression in QueryInfo).\n             Supported algorithms: none, deflate, gzip, stream_gzip -->\n        <compression>deflate</compression>\n\n        <!-- Default compression level (applied if client doesn't specify another level, see result_compression in QueryInfo).\n             Supported levels: none, low, medium, high -->\n        <compression_level>medium</compression_level>\n\n        <!-- Send/receive message size limits in bytes. -1 means unlimited -->\n        <max_send_message_size>-1</max_send_message_size>\n        <max_receive_message_size>-1</max_receive_message_size>\n\n        <!-- Enable if you want very detailed logs -->\n        <verbose_logs>false</verbose_logs>\n    </grpc>\n\n    <!-- Used with https_port and tcp_port_secure. Full ssl options list: https://github.com/ClickHouse-Extras/poco/blob/master/NetSSL_OpenSSL/include/Poco/Net/SSLManager.h#L71 -->\n    <openSSL>\n        <server> <!-- Used for https server AND secure tcp port -->\n            <!-- openssl req -subj \"/CN=localhost\" -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /etc/clickhouse-server/server.key -out /etc/clickhouse-server/server.crt -->\n            <certificateFile>/etc/clickhouse-server/server.crt</certificateFile>\n            <privateKeyFile>/etc/clickhouse-server/server.key</privateKeyFile>\n            <!-- dhparams are optional. You can delete the <dhParamsFile> element.\n                 To generate dhparams, use the following command:\n                  openssl dhparam -out /etc/clickhouse-server/dhparam.pem 4096\n                 Only file format with BEGIN DH PARAMETERS is supported.\n              -->\n            <!-- <dhParamsFile>/etc/clickhouse-server/dhparam.pem</dhParamsFile> -->\n            <verificationMode>none</verificationMode>\n            <loadDefaultCAFile>true</loadDefaultCAFile>\n            <cacheSessions>true</cacheSessions>\n            <disableProtocols>sslv2,sslv3</disableProtocols>\n            <preferServerCiphers>true</preferServerCiphers>\n        </server>\n\n        <client> <!-- Used for connecting to https dictionary source and secured Zookeeper communication -->\n            <loadDefaultCAFile>true</loadDefaultCAFile>\n            <cacheSessions>true</cacheSessions>\n            <disableProtocols>sslv2,sslv3</disableProtocols>\n            <preferServerCiphers>true</preferServerCiphers>\n            <!-- Use for self-signed: <verificationMode>none</verificationMode> -->\n            <invalidCertificateHandler>\n                <!-- Use for self-signed: <name>AcceptCertificateHandler</name> -->\n                <name>RejectCertificateHandler</name>\n            </invalidCertificateHandler>\n        </client>\n    </openSSL>\n\n    <!-- Default root page on http[s] server. For example load UI from https://tabix.io/ when opening http://localhost:8123 -->\n    <!--\n    <http_server_default_response><![CDATA[<html ng-app=\"SMI2\"><head><base href=\"http://ui.tabix.io/\"></head><body><div ui-view=\"\" class=\"content-ui\"></div><script src=\"http://loader.tabix.io/master.js\"></script></body></html>]]></http_server_default_response>\n    -->\n\n    <!-- Maximum number of concurrent queries. -->\n    <max_concurrent_queries>100</max_concurrent_queries>\n\n    <!-- Maximum memory usage (resident set size) for server process.\n         Zero value or unset means default. Default is \"max_server_memory_usage_to_ram_ratio\" of available physical RAM.\n         If the value is larger than \"max_server_memory_usage_to_ram_ratio\" of available physical RAM, it will be cut down.\n         The constraint is checked on query execution time.\n         If a query tries to allocate memory and the current memory usage plus allocation is greater\n          than specified threshold, exception will be thrown.\n         It is not practical to set this constraint to small values like just a few gigabytes,\n          because memory allocator will keep this amount of memory in caches and the server will deny service of queries.\n      -->\n    <max_server_memory_usage>0</max_server_memory_usage>\n\n    <!-- Maximum number of threads in the Global thread pool.\n    This will default to a maximum of 10000 threads if not specified.\n    This setting will be useful in scenarios where there are a large number\n    of distributed queries that are running concurrently but are idling most\n    of the time, in which case a higher number of threads might be required.\n    -->\n\n    <max_thread_pool_size>10000</max_thread_pool_size>\n\n    <!-- On memory constrained environments you may have to set this to value larger than 1.\n      -->\n    <max_server_memory_usage_to_ram_ratio>0.9</max_server_memory_usage_to_ram_ratio>\n\n    <!-- Simple server-wide memory profiler. Collect a stack trace at every peak allocation step (in bytes).\n         Data will be stored in system.trace_log table with query_id = empty string.\n         Zero means disabled.\n      -->\n    <total_memory_profiler_step>4194304</total_memory_profiler_step>\n\n    <!-- Collect random allocations and deallocations and write them into system.trace_log with 'MemorySample' trace_type.\n         The probability is for every alloc/free regardless to the size of the allocation.\n         Note that sampling happens only when the amount of untracked memory exceeds the untracked memory limit,\n          which is 4 MiB by default but can be lowered if 'total_memory_profiler_step' is lowered.\n         You may want to set 'total_memory_profiler_step' to 1 for extra fine grained sampling.\n      -->\n    <total_memory_tracker_sample_probability>0</total_memory_tracker_sample_probability>\n\n    <!-- Set limit on number of open files (default: maximum). This setting makes sense on Mac OS X because getrlimit() fails to retrieve\n         correct maximum value. -->\n    <!-- <max_open_files>262144</max_open_files> -->\n\n    <!-- Size of cache of uncompressed blocks of data, used in tables of MergeTree family.\n         In bytes. Cache is single for server. Memory is allocated only on demand.\n         Cache is used when 'use_uncompressed_cache' user setting turned on (off by default).\n         Uncompressed cache is advantageous only for very short queries and in rare cases.\n         Note: uncompressed cache can be pointless for lz4, because memory bandwidth\n         is slower than multi-core decompression on some server configurations.\n         Enabling it can sometimes paradoxically make queries slower.\n      -->\n    <uncompressed_cache_size>8589934592</uncompressed_cache_size>\n\n    <!-- Approximate size of mark cache, used in tables of MergeTree family.\n         In bytes. Cache is single for server. Memory is allocated only on demand.\n         You should not lower this value.\n      -->\n    <mark_cache_size>5368709120</mark_cache_size>\n\n\n    <!-- If you enable the `min_bytes_to_use_mmap_io` setting,\n         the data in MergeTree tables can be read with mmap to avoid copying from kernel to userspace.\n         It makes sense only for large files and helps only if data reside in page cache.\n         To avoid frequent open/mmap/munmap/close calls (which are very expensive due to consequent page faults)\n         and to reuse mappings from several threads and queries,\n         the cache of mapped files is maintained. Its size is the number of mapped regions (usually equal to the number of mapped files).\n         The amount of data in mapped files can be monitored\n         in system.metrics, system.metric_log by the MMappedFiles, MMappedFileBytes metrics\n         and in system.asynchronous_metrics, system.asynchronous_metrics_log by the MMapCacheCells metric,\n         and also in system.events, system.processes, system.query_log, system.query_thread_log, system.query_views_log by the\n         CreatedReadBufferMMap, CreatedReadBufferMMapFailed, MMappedFileCacheHits, MMappedFileCacheMisses events.\n         Note that the amount of data in mapped files does not consume memory directly and is not accounted\n         in query or server memory usage - because this memory can be discarded similar to OS page cache.\n         The cache is dropped (the files are closed) automatically on removal of old parts in MergeTree,\n         also it can be dropped manually by the SYSTEM DROP MMAP CACHE query.\n      -->\n    <mmap_cache_size>1000</mmap_cache_size>\n\n    <!-- Cache size in bytes for compiled expressions.-->\n    <compiled_expression_cache_size>134217728</compiled_expression_cache_size>\n\n    <!-- Cache size in elements for compiled expressions.-->\n    <compiled_expression_cache_elements_size>10000</compiled_expression_cache_elements_size>\n\n    <!-- Path to data directory, with trailing slash. -->\n    <path>/var/lib/clickhouse/</path>\n\n    <!-- Path to temporary data for processing hard queries. -->\n    <tmp_path>/var/lib/clickhouse/tmp/</tmp_path>\n\n    <!-- Policy from the <storage_configuration> for the temporary files.\n         If not set <tmp_path> is used, otherwise <tmp_path> is ignored.\n         Notes:\n         - move_factor              is ignored\n         - keep_free_space_bytes    is ignored\n         - max_data_part_size_bytes is ignored\n         - you must have exactly one volume in that policy\n    -->\n    <!-- <tmp_policy>tmp</tmp_policy> -->\n\n    <!-- Directory with user provided files that are accessible by 'file' table function. -->\n    <user_files_path>/var/lib/clickhouse/user_files/</user_files_path>\n\n    <!-- LDAP server definitions. -->\n    <ldap_servers>\n        <!-- List LDAP servers with their connection parameters here to later 1) use them as authenticators for dedicated local users,\n              who have 'ldap' authentication mechanism specified instead of 'password', or to 2) use them as remote user directories.\n             Parameters:\n                host - LDAP server hostname or IP, this parameter is mandatory and cannot be empty.\n                port - LDAP server port, default is 636 if enable_tls is set to true, 389 otherwise.\n                bind_dn - template used to construct the DN to bind to.\n                        The resulting DN will be constructed by replacing all '{user_name}' substrings of the template with the actual\n                         user name during each authentication attempt.\n                user_dn_detection - section with LDAP search parameters for detecting the actual user DN of the bound user.\n                        This is mainly used in search filters for further role mapping when the server is Active Directory. The\n                         resulting user DN will be used when replacing '{user_dn}' substrings wherever they are allowed. By default,\n                         user DN is set equal to bind DN, but once search is performed, it will be updated with to the actual detected\n                         user DN value.\n                    base_dn - template used to construct the base DN for the LDAP search.\n                            The resulting DN will be constructed by replacing all '{user_name}' and '{bind_dn}' substrings\n                             of the template with the actual user name and bind DN during the LDAP search.\n                    scope - scope of the LDAP search.\n                            Accepted values are: 'base', 'one_level', 'children', 'subtree' (the default).\n                    search_filter - template used to construct the search filter for the LDAP search.\n                            The resulting filter will be constructed by replacing all '{user_name}', '{bind_dn}', and '{base_dn}'\n                             substrings of the template with the actual user name, bind DN, and base DN during the LDAP search.\n                            Note, that the special characters must be escaped properly in XML.\n                verification_cooldown - a period of time, in seconds, after a successful bind attempt, during which a user will be assumed\n                         to be successfully authenticated for all consecutive requests without contacting the LDAP server.\n                        Specify 0 (the default) to disable caching and force contacting the LDAP server for each authentication request.\n                enable_tls - flag to trigger use of secure connection to the LDAP server.\n                        Specify 'no' for plain text (ldap://) protocol (not recommended).\n                        Specify 'yes' for LDAP over SSL/TLS (ldaps://) protocol (recommended, the default).\n                        Specify 'starttls' for legacy StartTLS protocol (plain text (ldap://) protocol, upgraded to TLS).\n                tls_minimum_protocol_version - the minimum protocol version of SSL/TLS.\n                        Accepted values are: 'ssl2', 'ssl3', 'tls1.0', 'tls1.1', 'tls1.2' (the default).\n                tls_require_cert - SSL/TLS peer certificate verification behavior.\n                        Accepted values are: 'never', 'allow', 'try', 'demand' (the default).\n                tls_cert_file - path to certificate file.\n                tls_key_file - path to certificate key file.\n                tls_ca_cert_file - path to CA certificate file.\n                tls_ca_cert_dir - path to the directory containing CA certificates.\n                tls_cipher_suite - allowed cipher suite (in OpenSSL notation).\n             Example:\n                <my_ldap_server>\n                    <host>localhost</host>\n                    <port>636</port>\n                    <bind_dn>uid={user_name},ou=users,dc=example,dc=com</bind_dn>\n                    <verification_cooldown>300</verification_cooldown>\n                    <enable_tls>yes</enable_tls>\n                    <tls_minimum_protocol_version>tls1.2</tls_minimum_protocol_version>\n                    <tls_require_cert>demand</tls_require_cert>\n                    <tls_cert_file>/path/to/tls_cert_file</tls_cert_file>\n                    <tls_key_file>/path/to/tls_key_file</tls_key_file>\n                    <tls_ca_cert_file>/path/to/tls_ca_cert_file</tls_ca_cert_file>\n                    <tls_ca_cert_dir>/path/to/tls_ca_cert_dir</tls_ca_cert_dir>\n                    <tls_cipher_suite>ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:AES256-GCM-SHA384</tls_cipher_suite>\n                </my_ldap_server>\n             Example (typical Active Directory with configured user DN detection for further role mapping):\n                <my_ad_server>\n                    <host>localhost</host>\n                    <port>389</port>\n                    <bind_dn>EXAMPLE\\{user_name}</bind_dn>\n                    <user_dn_detection>\n                        <base_dn>CN=Users,DC=example,DC=com</base_dn>\n                        <search_filter>(&amp;(objectClass=user)(sAMAccountName={user_name}))</search_filter>\n                    </user_dn_detection>\n                    <enable_tls>no</enable_tls>\n                </my_ad_server>\n        -->\n    </ldap_servers>\n\n    <!-- To enable Kerberos authentication support for HTTP requests (GSS-SPNEGO), for those users who are explicitly configured\n          to authenticate via Kerberos, define a single 'kerberos' section here.\n         Parameters:\n            principal - canonical service principal name, that will be acquired and used when accepting security contexts.\n                    This parameter is optional, if omitted, the default principal will be used.\n                    This parameter cannot be specified together with 'realm' parameter.\n            realm - a realm, that will be used to restrict authentication to only those requests whose initiator's realm matches it.\n                    This parameter is optional, if omitted, no additional filtering by realm will be applied.\n                    This parameter cannot be specified together with 'principal' parameter.\n         Example:\n            <kerberos />\n         Example:\n            <kerberos>\n                <principal>HTTP/clickhouse.example.com@EXAMPLE.COM</principal>\n            </kerberos>\n         Example:\n            <kerberos>\n                <realm>EXAMPLE.COM</realm>\n            </kerberos>\n    -->\n\n    <!-- Sources to read users, roles, access rights, profiles of settings, quotas. -->\n    <user_directories>\n        <users_xml>\n            <!-- Path to configuration file with predefined users. -->\n            <path>/etc/clickhouse-server/users.xml</path>\n        </users_xml>\n        <local_directory>\n            <!-- Path to folder where users created by SQL commands are stored. -->\n            <path>/var/lib/clickhouse/access/</path>\n        </local_directory>\n\n        <!-- To add an LDAP server as a remote user directory of users that are not defined locally, define a single 'ldap' section\n              with the following parameters:\n                server - one of LDAP server names defined in 'ldap_servers' config section above.\n                        This parameter is mandatory and cannot be empty.\n                roles - section with a list of locally defined roles that will be assigned to each user retrieved from the LDAP server.\n                        If no roles are specified here or assigned during role mapping (below), user will not be able to perform any\n                         actions after authentication.\n                role_mapping - section with LDAP search parameters and mapping rules.\n                        When a user authenticates, while still bound to LDAP, an LDAP search is performed using search_filter and the\n                         name of the logged in user. For each entry found during that search, the value of the specified attribute is\n                         extracted. For each attribute value that has the specified prefix, the prefix is removed, and the rest of the\n                         value becomes the name of a local role defined in ClickHouse, which is expected to be created beforehand by\n                         CREATE ROLE command.\n                        There can be multiple 'role_mapping' sections defined inside the same 'ldap' section. All of them will be\n                         applied.\n                    base_dn - template used to construct the base DN for the LDAP search.\n                            The resulting DN will be constructed by replacing all '{user_name}', '{bind_dn}', and '{user_dn}'\n                             substrings of the template with the actual user name, bind DN, and user DN during each LDAP search.\n                    scope - scope of the LDAP search.\n                            Accepted values are: 'base', 'one_level', 'children', 'subtree' (the default).\n                    search_filter - template used to construct the search filter for the LDAP search.\n                            The resulting filter will be constructed by replacing all '{user_name}', '{bind_dn}', '{user_dn}', and\n                             '{base_dn}' substrings of the template with the actual user name, bind DN, user DN, and base DN during\n                             each LDAP search.\n                            Note, that the special characters must be escaped properly in XML.\n                    attribute - attribute name whose values will be returned by the LDAP search. 'cn', by default.\n                    prefix - prefix, that will be expected to be in front of each string in the original list of strings returned by\n                             the LDAP search. Prefix will be removed from the original strings and resulting strings will be treated\n                             as local role names. Empty, by default.\n             Example:\n                <ldap>\n                    <server>my_ldap_server</server>\n                    <roles>\n                        <my_local_role1 />\n                        <my_local_role2 />\n                    </roles>\n                    <role_mapping>\n                        <base_dn>ou=groups,dc=example,dc=com</base_dn>\n                        <scope>subtree</scope>\n                        <search_filter>(&amp;(objectClass=groupOfNames)(member={bind_dn}))</search_filter>\n                        <attribute>cn</attribute>\n                        <prefix>clickhouse_</prefix>\n                    </role_mapping>\n                </ldap>\n             Example (typical Active Directory with role mapping that relies on the detected user DN):\n                <ldap>\n                    <server>my_ad_server</server>\n                    <role_mapping>\n                        <base_dn>CN=Users,DC=example,DC=com</base_dn>\n                        <attribute>CN</attribute>\n                        <scope>subtree</scope>\n                        <search_filter>(&amp;(objectClass=group)(member={user_dn}))</search_filter>\n                        <prefix>clickhouse_</prefix>\n                    </role_mapping>\n                </ldap>\n        -->\n    </user_directories>\n\n    <!-- Default profile of settings. -->\n    <default_profile>default</default_profile>\n\n    <!-- Comma-separated list of prefixes for user-defined settings. -->\n    <custom_settings_prefixes></custom_settings_prefixes>\n\n    <!-- System profile of settings. This settings are used by internal processes (Distributed DDL worker and so on). -->\n    <!-- <system_profile>default</system_profile> -->\n\n    <!-- Buffer profile of settings.\n         This settings are used by Buffer storage to flush data to the underlying table.\n         Default: used from system_profile directive.\n    -->\n    <!-- <buffer_profile>default</buffer_profile> -->\n\n    <!-- Default database. -->\n    <default_database>default</default_database>\n\n    <!-- Server time zone could be set here.\n         Time zone is used when converting between String and DateTime types,\n          when printing DateTime in text formats and parsing DateTime from text,\n          it is used in date and time related functions, if specific time zone was not passed as an argument.\n         Time zone is specified as identifier from IANA time zone database, like UTC or Africa/Abidjan.\n         If not specified, system time zone at server startup is used.\n         Please note, that server could display time zone alias instead of specified name.\n         Example: W-SU is an alias for Europe/Moscow and Zulu is an alias for UTC.\n    -->\n    <!-- <timezone>Europe/Moscow</timezone> -->\n\n    <!-- You can specify umask here (see \"man umask\"). Server will apply it on startup.\n         Number is always parsed as octal. Default umask is 027 (other users cannot read logs, data files, etc; group can only read).\n    -->\n    <!-- <umask>022</umask> -->\n\n    <!-- Perform mlockall after startup to lower first queries latency\n          and to prevent clickhouse executable from being paged out under high IO load.\n         Enabling this option is recommended but will lead to increased startup time for up to a few seconds.\n    -->\n    <mlock_executable>true</mlock_executable>\n\n    <!-- Reallocate memory for machine code (\"text\") using huge pages. Highly experimental. -->\n    <remap_executable>false</remap_executable>\n\n    <![CDATA[\n         Uncomment below in order to use JDBC table engine and function.\n         To install and run JDBC bridge in background:\n         * [Debian/Ubuntu]\n           export MVN_URL=https://repo1.maven.org/maven2/ru/yandex/clickhouse/clickhouse-jdbc-bridge\n           export PKG_VER=$(curl -sL $MVN_URL/maven-metadata.xml | grep '<release>' | sed -e 's|.*>\\(.*\\)<.*|\\1|')\n           wget https://github.com/ClickHouse/clickhouse-jdbc-bridge/releases/download/v$PKG_VER/clickhouse-jdbc-bridge_$PKG_VER-1_all.deb\n           apt install --no-install-recommends -f ./clickhouse-jdbc-bridge_$PKG_VER-1_all.deb\n           clickhouse-jdbc-bridge &\n         * [CentOS/RHEL]\n           export MVN_URL=https://repo1.maven.org/maven2/ru/yandex/clickhouse/clickhouse-jdbc-bridge\n           export PKG_VER=$(curl -sL $MVN_URL/maven-metadata.xml | grep '<release>' | sed -e 's|.*>\\(.*\\)<.*|\\1|')\n           wget https://github.com/ClickHouse/clickhouse-jdbc-bridge/releases/download/v$PKG_VER/clickhouse-jdbc-bridge-$PKG_VER-1.noarch.rpm\n           yum localinstall -y clickhouse-jdbc-bridge-$PKG_VER-1.noarch.rpm\n           clickhouse-jdbc-bridge &\n         Please refer to https://github.com/ClickHouse/clickhouse-jdbc-bridge#usage for more information.\n    ]]>\n    <!--\n    <jdbc_bridge>\n        <host>127.0.0.1</host>\n        <port>9019</port>\n    </jdbc_bridge>\n    -->\n\n    <!-- Configuration of clusters that could be used in Distributed tables.\n         https://clickhouse.com/docs/en/operations/table_engines/distributed/\n      -->\n    <remote_servers>\n        <!-- Test only shard config for testing distributed storage -->\n        <test_shard_localhost>\n            <!-- Inter-server per-cluster secret for Distributed queries\n                 default: no secret (no authentication will be performed)\n                 If set, then Distributed queries will be validated on shards, so at least:\n                 - such cluster should exist on the shard,\n                 - such cluster should have the same secret.\n                 And also (and which is more important), the initial_user will\n                 be used as current user for the query.\n                 Right now the protocol is pretty simple and it only takes into account:\n                 - cluster name\n                 - query\n                 Also it will be nice if the following will be implemented:\n                 - source hostname (see interserver_http_host), but then it will depends from DNS,\n                   it can use IP address instead, but then the you need to get correct on the initiator node.\n                 - target hostname / ip address (same notes as for source hostname)\n                 - time-based security tokens\n            -->\n            <!-- <secret></secret> -->\n\n            <shard>\n                <!-- Optional. Whether to write data to just one of the replicas. Default: false (write data to all replicas). -->\n                <!-- <internal_replication>false</internal_replication> -->\n                <!-- Optional. Shard weight when writing data. Default: 1. -->\n                <!-- <weight>1</weight> -->\n                <replica>\n                    <host>localhost</host>\n                    <port>9000</port>\n                    <!-- Optional. Priority of the replica for load_balancing. Default: 1 (less value has more priority). -->\n                    <!-- <priority>1</priority> -->\n                </replica>\n            </shard>\n        </test_shard_localhost>\n        <test_cluster_one_shard_three_replicas_localhost>\n            <shard>\n                <internal_replication>false</internal_replication>\n                <replica>\n                    <host>127.0.0.1</host>\n                    <port>9000</port>\n                </replica>\n                <replica>\n                    <host>127.0.0.2</host>\n                    <port>9000</port>\n                </replica>\n                <replica>\n                    <host>127.0.0.3</host>\n                    <port>9000</port>\n                </replica>\n            </shard>\n            <!--shard>\n                <internal_replication>false</internal_replication>\n                <replica>\n                    <host>127.0.0.1</host>\n                    <port>9000</port>\n                </replica>\n                <replica>\n                    <host>127.0.0.2</host>\n                    <port>9000</port>\n                </replica>\n                <replica>\n                    <host>127.0.0.3</host>\n                    <port>9000</port>\n                </replica>\n            </shard-->\n        </test_cluster_one_shard_three_replicas_localhost>\n        <test_cluster_two_shards_localhost>\n             <shard>\n                 <replica>\n                     <host>localhost</host>\n                     <port>9000</port>\n                 </replica>\n             </shard>\n             <shard>\n                 <replica>\n                     <host>localhost</host>\n                     <port>9000</port>\n                 </replica>\n             </shard>\n        </test_cluster_two_shards_localhost>\n        <test_cluster_two_shards>\n            <shard>\n                <replica>\n                    <host>127.0.0.1</host>\n                    <port>9000</port>\n                </replica>\n            </shard>\n            <shard>\n                <replica>\n                    <host>127.0.0.2</host>\n                    <port>9000</port>\n                </replica>\n            </shard>\n        </test_cluster_two_shards>\n        <test_cluster_two_shards_internal_replication>\n            <shard>\n                <internal_replication>true</internal_replication>\n                <replica>\n                    <host>127.0.0.1</host>\n                    <port>9000</port>\n                </replica>\n            </shard>\n            <shard>\n                <internal_replication>true</internal_replication>\n                <replica>\n                    <host>127.0.0.2</host>\n                    <port>9000</port>\n                </replica>\n            </shard>\n        </test_cluster_two_shards_internal_replication>\n        <test_shard_localhost_secure>\n            <shard>\n                <replica>\n                    <host>localhost</host>\n                    <port>9440</port>\n                    <secure>1</secure>\n                </replica>\n            </shard>\n        </test_shard_localhost_secure>\n        <test_unavailable_shard>\n            <shard>\n                <replica>\n                    <host>localhost</host>\n                    <port>9000</port>\n                </replica>\n            </shard>\n            <shard>\n                <replica>\n                    <host>localhost</host>\n                    <port>1</port>\n                </replica>\n            </shard>\n        </test_unavailable_shard>\n    </remote_servers>\n\n    <!-- The list of hosts allowed to use in URL-related storage engines and table functions.\n        If this section is not present in configuration, all hosts are allowed.\n    -->\n    <!--<remote_url_allow_hosts>-->\n        <!-- Host should be specified exactly as in URL. The name is checked before DNS resolution.\n            Example: \"yandex.ru\", \"yandex.ru.\" and \"www.yandex.ru\" are different hosts.\n                    If port is explicitly specified in URL, the host:port is checked as a whole.\n                    If host specified here without port, any port with this host allowed.\n                    \"yandex.ru\" -> \"yandex.ru:443\", \"yandex.ru:80\" etc. is allowed, but \"yandex.ru:80\" -> only \"yandex.ru:80\" is allowed.\n            If the host is specified as IP address, it is checked as specified in URL. Example: \"[2a02:6b8:a::a]\".\n            If there are redirects and support for redirects is enabled, every redirect (the Location field) is checked.\n            Host should be specified using the host xml tag:\n                    <host>yandex.ru</host>\n        -->\n\n        <!-- Regular expression can be specified. RE2 engine is used for regexps.\n            Regexps are not aligned: don't forget to add ^ and $. Also don't forget to escape dot (.) metacharacter\n            (forgetting to do so is a common source of error).\n        -->\n    <!--</remote_url_allow_hosts>-->\n\n    <!-- If element has 'incl' attribute, then for it's value will be used corresponding substitution from another file.\n         By default, path to file with substitutions is /etc/metrika.xml. It could be changed in config in 'include_from' element.\n         Values for substitutions are specified in /clickhouse/name_of_substitution elements in that file.\n      -->\n\n    <!-- ZooKeeper is used to store metadata about replicas, when using Replicated tables.\n         Optional. If you don't use replicated tables, you could omit that.\n         See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/\n      -->\n\n    <!--\n    <zookeeper>\n        <node>\n            <host>example1</host>\n            <port>2181</port>\n        </node>\n        <node>\n            <host>example2</host>\n            <port>2181</port>\n        </node>\n        <node>\n            <host>example3</host>\n            <port>2181</port>\n        </node>\n    </zookeeper>\n    -->\n\n    <!-- Substitutions for parameters of replicated tables.\n          Optional. If you don't use replicated tables, you could omit that.\n         See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/#creating-replicated-tables\n      -->\n    <!--\n    <macros>\n        <shard>01</shard>\n        <replica>example01-01-1</replica>\n    </macros>\n    -->\n\n\n    <!-- Reloading interval for embedded dictionaries, in seconds. Default: 3600. -->\n    <builtin_dictionaries_reload_interval>3600</builtin_dictionaries_reload_interval>\n\n\n    <!-- Maximum session timeout, in seconds. Default: 3600. -->\n    <max_session_timeout>3600</max_session_timeout>\n\n    <!-- Default session timeout, in seconds. Default: 60. -->\n    <default_session_timeout>60</default_session_timeout>\n\n    <!-- Sending data to Graphite for monitoring. Several sections can be defined. -->\n    <!--\n        interval - send every X second\n        root_path - prefix for keys\n        hostname_in_path - append hostname to root_path (default = true)\n        metrics - send data from table system.metrics\n        events - send data from table system.events\n        asynchronous_metrics - send data from table system.asynchronous_metrics\n    -->\n    <!--\n    <graphite>\n        <host>localhost</host>\n        <port>42000</port>\n        <timeout>0.1</timeout>\n        <interval>60</interval>\n        <root_path>one_min</root_path>\n        <hostname_in_path>true</hostname_in_path>\n        <metrics>true</metrics>\n        <events>true</events>\n        <events_cumulative>false</events_cumulative>\n        <asynchronous_metrics>true</asynchronous_metrics>\n    </graphite>\n    <graphite>\n        <host>localhost</host>\n        <port>42000</port>\n        <timeout>0.1</timeout>\n        <interval>1</interval>\n        <root_path>one_sec</root_path>\n        <metrics>true</metrics>\n        <events>true</events>\n        <events_cumulative>false</events_cumulative>\n        <asynchronous_metrics>false</asynchronous_metrics>\n    </graphite>\n    -->\n\n    <!-- Serve endpoint for Prometheus monitoring. -->\n    <!--\n        endpoint - mertics path (relative to root, statring with \"/\")\n        port - port to setup server. If not defined or 0 than http_port used\n        metrics - send data from table system.metrics\n        events - send data from table system.events\n        asynchronous_metrics - send data from table system.asynchronous_metrics\n        status_info - send data from different component from CH, ex: Dictionaries status\n    -->\n    <!--\n    <prometheus>\n        <endpoint>/metrics</endpoint>\n        <port>9363</port>\n        <metrics>true</metrics>\n        <events>true</events>\n        <asynchronous_metrics>true</asynchronous_metrics>\n        <status_info>true</status_info>\n    </prometheus>\n    -->\n\n    <!-- Query log. Used only for queries with setting log_queries = 1. -->\n    <query_log>\n        <!-- What table to insert data. If table is not exist, it will be created.\n             When query log structure is changed after system update,\n              then old table will be renamed and new table will be created automatically.\n        -->\n        <database>system</database>\n        <table>query_log</table>\n        <!--\n            PARTITION BY expr: https://clickhouse.com/docs/en/table_engines/mergetree-family/custom_partitioning_key/\n            Example:\n                event_date\n                toMonday(event_date)\n                toYYYYMM(event_date)\n                toStartOfHour(event_time)\n        -->\n        <partition_by>toYYYYMM(event_date)</partition_by>\n        <!--\n            Table TTL specification: https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/mergetree/#mergetree-table-ttl\n            Example:\n                event_date + INTERVAL 1 WEEK\n                event_date + INTERVAL 7 DAY DELETE\n                event_date + INTERVAL 2 WEEK TO DISK 'bbb'\n        <ttl>event_date + INTERVAL 30 DAY DELETE</ttl>\n        -->\n\n        <!-- Instead of partition_by, you can provide full engine expression (starting with ENGINE = ) with parameters,\n             Example: <engine>ENGINE = MergeTree PARTITION BY toYYYYMM(event_date) ORDER BY (event_date, event_time) SETTINGS index_granularity = 1024</engine>\n          -->\n\n        <!-- Interval of flushing data. -->\n        <flush_interval_milliseconds>7500</flush_interval_milliseconds>\n    </query_log>\n\n    <!-- Trace log. Stores stack traces collected by query profilers.\n         See query_profiler_real_time_period_ns and query_profiler_cpu_time_period_ns settings. -->\n    <trace_log>\n        <database>system</database>\n        <table>trace_log</table>\n\n        <partition_by>toYYYYMM(event_date)</partition_by>\n        <flush_interval_milliseconds>7500</flush_interval_milliseconds>\n    </trace_log>\n\n    <!-- Query thread log. Has information about all threads participated in query execution.\n         Used only for queries with setting log_query_threads = 1. -->\n    <query_thread_log>\n        <database>system</database>\n        <table>query_thread_log</table>\n        <partition_by>toYYYYMM(event_date)</partition_by>\n        <flush_interval_milliseconds>7500</flush_interval_milliseconds>\n    </query_thread_log>\n\n    <!-- Query views log. Has information about all dependent views associated with a query.\n         Used only for queries with setting log_query_views = 1. -->\n    <query_views_log>\n        <database>system</database>\n        <table>query_views_log</table>\n        <partition_by>toYYYYMM(event_date)</partition_by>\n        <flush_interval_milliseconds>7500</flush_interval_milliseconds>\n    </query_views_log>\n\n    <!-- Uncomment if use part log.\n         Part log contains information about all actions with parts in MergeTree tables (creation, deletion, merges, downloads).-->\n    <part_log>\n        <database>system</database>\n        <table>part_log</table>\n        <partition_by>toYYYYMM(event_date)</partition_by>\n        <flush_interval_milliseconds>7500</flush_interval_milliseconds>\n    </part_log>\n\n    <!-- Uncomment to write text log into table.\n         Text log contains all information from usual server log but stores it in structured and efficient way.\n         The level of the messages that goes to the table can be limited (<level>), if not specified all messages will go to the table.\n    <text_log>\n        <database>system</database>\n        <table>text_log</table>\n        <flush_interval_milliseconds>7500</flush_interval_milliseconds>\n        <level></level>\n    </text_log>\n    -->\n\n    <!-- Metric log contains rows with current values of ProfileEvents, CurrentMetrics collected with \"collect_interval_milliseconds\" interval. -->\n    <metric_log>\n        <database>system</database>\n        <table>metric_log</table>\n        <flush_interval_milliseconds>7500</flush_interval_milliseconds>\n        <collect_interval_milliseconds>1000</collect_interval_milliseconds>\n    </metric_log>\n\n    <!--\n        Asynchronous metric log contains values of metrics from\n        system.asynchronous_metrics.\n    -->\n    <asynchronous_metric_log>\n        <database>system</database>\n        <table>asynchronous_metric_log</table>\n        <!--\n            Asynchronous metrics are updated once a minute, so there is\n            no need to flush more often.\n        -->\n        <flush_interval_milliseconds>7000</flush_interval_milliseconds>\n    </asynchronous_metric_log>\n\n    <!--\n        OpenTelemetry log contains OpenTelemetry trace spans.\n    -->\n    <opentelemetry_span_log>\n        <!--\n            The default table creation code is insufficient, this <engine> spec\n            is a workaround. There is no 'event_time' for this log, but two times,\n            start and finish. It is sorted by finish time, to avoid inserting\n            data too far away in the past (probably we can sometimes insert a span\n            that is seconds earlier than the last span in the table, due to a race\n            between several spans inserted in parallel). This gives the spans a\n            global order that we can use to e.g. retry insertion into some external\n            system.\n        -->\n        <engine>\n            engine MergeTree\n            partition by toYYYYMM(finish_date)\n            order by (finish_date, finish_time_us, trace_id)\n        </engine>\n        <database>system</database>\n        <table>opentelemetry_span_log</table>\n        <flush_interval_milliseconds>7500</flush_interval_milliseconds>\n    </opentelemetry_span_log>\n\n\n    <!-- Crash log. Stores stack traces for fatal errors.\n         This table is normally empty. -->\n    <crash_log>\n        <database>system</database>\n        <table>crash_log</table>\n\n        <partition_by />\n        <flush_interval_milliseconds>1000</flush_interval_milliseconds>\n    </crash_log>\n\n    <!-- Session log. Stores user log in (successful or not) and log out events. -->\n    <session_log>\n        <database>system</database>\n        <table>session_log</table>\n\n        <partition_by>toYYYYMM(event_date)</partition_by>\n        <flush_interval_milliseconds>7500</flush_interval_milliseconds>\n    </session_log>\n\n    <!-- Parameters for embedded dictionaries, used in Yandex.Metrica.\n         See https://clickhouse.com/docs/en/dicts/internal_dicts/\n    -->\n\n    <!-- Path to file with region hierarchy. -->\n    <!-- <path_to_regions_hierarchy_file>/opt/geo/regions_hierarchy.txt</path_to_regions_hierarchy_file> -->\n\n    <!-- Path to directory with files containing names of regions -->\n    <!-- <path_to_regions_names_files>/opt/geo/</path_to_regions_names_files> -->\n\n\n    <!-- <top_level_domains_path>/var/lib/clickhouse/top_level_domains/</top_level_domains_path> -->\n    <!-- Custom TLD lists.\n         Format: <name>/path/to/file</name>\n         Changes will not be applied w/o server restart.\n         Path to the list is under top_level_domains_path (see above).\n    -->\n    <top_level_domains_lists>\n        <!--\n        <public_suffix_list>/path/to/public_suffix_list.dat</public_suffix_list>\n        -->\n    </top_level_domains_lists>\n\n    <!-- Configuration of external dictionaries. See:\n         https://clickhouse.com/docs/en/sql-reference/dictionaries/external-dictionaries/external-dicts\n    -->\n    <dictionaries_config>*_dictionary.xml</dictionaries_config>\n\n    <!-- Configuration of user defined executable functions -->\n    <user_defined_executable_functions_config>*_function.xml</user_defined_executable_functions_config>\n\n    <!-- Uncomment if you want data to be compressed 30-100% better.\n         Don't do that if you just started using ClickHouse.\n      -->\n    <!--\n    <compression>\n        <!- - Set of variants. Checked in order. Last matching case wins. If nothing matches, lz4 will be used. - ->\n        <case>\n            <!- - Conditions. All must be satisfied. Some conditions may be omitted. - ->\n            <min_part_size>10000000000</min_part_size>        <!- - Min part size in bytes. - ->\n            <min_part_size_ratio>0.01</min_part_size_ratio>   <!- - Min size of part relative to whole table size. - ->\n            <!- - What compression method to use. - ->\n            <method>zstd</method>\n        </case>\n    </compression>\n    -->\n\n    <!-- Configuration of encryption. The server executes a command to\n         obtain an encryption key at startup if such a command is\n         defined, or encryption codecs will be disabled otherwise. The\n         command is executed through /bin/sh and is expected to write\n         a Base64-encoded key to the stdout. -->\n    <encryption_codecs>\n        <!-- aes_128_gcm_siv -->\n            <!-- Example of getting hex key from env -->\n            <!-- the code should use this key and throw an exception if its length is not 16 bytes -->\n            <!--key_hex from_env=\"...\"></key_hex -->\n\n            <!-- Example of multiple hex keys. They can be imported from env or be written down in config-->\n            <!-- the code should use these keys and throw an exception if their length is not 16 bytes -->\n            <!-- key_hex id=\"0\">...</key_hex -->\n            <!-- key_hex id=\"1\" from_env=\"..\"></key_hex -->\n            <!-- key_hex id=\"2\">...</key_hex -->\n            <!-- current_key_id>2</current_key_id -->\n\n            <!-- Example of getting hex key from config -->\n            <!-- the code should use this key and throw an exception if its length is not 16 bytes -->\n            <!-- key>...</key -->\n\n            <!-- example of adding nonce -->\n            <!-- nonce>...</nonce -->\n\n        <!-- /aes_128_gcm_siv -->\n    </encryption_codecs>\n\n    <!-- Allow to execute distributed DDL queries (CREATE, DROP, ALTER, RENAME) on cluster.\n         Works only if ZooKeeper is enabled. Comment it if such functionality isn't required. -->\n    <distributed_ddl>\n        <!-- Path in ZooKeeper to queue with DDL queries -->\n        <path>/clickhouse/task_queue/ddl</path>\n\n        <!-- Settings from this profile will be used to execute DDL queries -->\n        <!-- <profile>default</profile> -->\n\n        <!-- Controls how much ON CLUSTER queries can be run simultaneously. -->\n        <!-- <pool_size>1</pool_size> -->\n\n        <!--\n             Cleanup settings (active tasks will not be removed)\n        -->\n\n        <!-- Controls task TTL (default 1 week) -->\n        <!-- <task_max_lifetime>604800</task_max_lifetime> -->\n\n        <!-- Controls how often cleanup should be performed (in seconds) -->\n        <!-- <cleanup_delay_period>60</cleanup_delay_period> -->\n\n        <!-- Controls how many tasks could be in the queue -->\n        <!-- <max_tasks_in_queue>1000</max_tasks_in_queue> -->\n    </distributed_ddl>\n\n    <!-- Settings to fine tune MergeTree tables. See documentation in source code, in MergeTreeSettings.h -->\n    <!--\n    <merge_tree>\n        <max_suspicious_broken_parts>5</max_suspicious_broken_parts>\n    </merge_tree>\n    -->\n\n    <!-- Protection from accidental DROP.\n         If size of a MergeTree table is greater than max_table_size_to_drop (in bytes) than table could not be dropped with any DROP query.\n         If you want do delete one table and don't want to change clickhouse-server config, you could create special file <clickhouse-path>/flags/force_drop_table and make DROP once.\n         By default max_table_size_to_drop is 50GB; max_table_size_to_drop=0 allows to DROP any tables.\n         The same for max_partition_size_to_drop.\n         Uncomment to disable protection.\n    -->\n    <!-- <max_table_size_to_drop>0</max_table_size_to_drop> -->\n    <!-- <max_partition_size_to_drop>0</max_partition_size_to_drop> -->\n\n    <!-- Example of parameters for GraphiteMergeTree table engine -->\n    <graphite_rollup_example>\n        <pattern>\n            <regexp>click_cost</regexp>\n            <function>any</function>\n            <retention>\n                <age>0</age>\n                <precision>3600</precision>\n            </retention>\n            <retention>\n                <age>86400</age>\n                <precision>60</precision>\n            </retention>\n        </pattern>\n        <default>\n            <function>max</function>\n            <retention>\n                <age>0</age>\n                <precision>60</precision>\n            </retention>\n            <retention>\n                <age>3600</age>\n                <precision>300</precision>\n            </retention>\n            <retention>\n                <age>86400</age>\n                <precision>3600</precision>\n            </retention>\n        </default>\n    </graphite_rollup_example>\n\n    <!-- Directory in <clickhouse-path> containing schema files for various input formats.\n         The directory will be created if it doesn't exist.\n      -->\n    <format_schema_path>/var/lib/clickhouse/format_schemas/</format_schema_path>\n\n    <!-- Default query masking rules, matching lines would be replaced with something else in the logs\n        (both text logs and system.query_log).\n        name - name for the rule (optional)\n        regexp - RE2 compatible regular expression (mandatory)\n        replace - substitution string for sensitive data (optional, by default - six asterisks)\n    -->\n    <query_masking_rules>\n        <rule>\n            <name>hide encrypt/decrypt arguments</name>\n            <regexp>((?:aes_)?(?:encrypt|decrypt)(?:_mysql)?)\\s*\\(\\s*(?:'(?:\\\\'|.)+'|.*?)\\s*\\)</regexp>\n            <!-- or more secure, but also more invasive:\n                (aes_\\w+)\\s*\\(.*\\)\n            -->\n            <replace>\\1(???)</replace>\n        </rule>\n    </query_masking_rules>\n\n    <!-- Uncomment to use custom http handlers.\n        rules are checked from top to bottom, first match runs the handler\n            url - to match request URL, you can use 'regex:' prefix to use regex match(optional)\n            methods - to match request method, you can use commas to separate multiple method matches(optional)\n            headers - to match request headers, match each child element(child element name is header name), you can use 'regex:' prefix to use regex match(optional)\n        handler is request handler\n            type - supported types: static, dynamic_query_handler, predefined_query_handler\n            query - use with predefined_query_handler type, executes query when the handler is called\n            query_param_name - use with dynamic_query_handler type, extracts and executes the value corresponding to the <query_param_name> value in HTTP request params\n            status - use with static type, response status code\n            content_type - use with static type, response content-type\n            response_content - use with static type, Response content sent to client, when using the prefix 'file://' or 'config://', find the content from the file or configuration send to client.\n    <http_handlers>\n        <rule>\n            <url>/</url>\n            <methods>POST,GET</methods>\n            <headers><pragma>no-cache</pragma></headers>\n            <handler>\n                <type>dynamic_query_handler</type>\n                <query_param_name>query</query_param_name>\n            </handler>\n        </rule>\n        <rule>\n            <url>/predefined_query</url>\n            <methods>POST,GET</methods>\n            <handler>\n                <type>predefined_query_handler</type>\n                <query>SELECT * FROM system.settings</query>\n            </handler>\n        </rule>\n        <rule>\n            <handler>\n                <type>static</type>\n                <status>200</status>\n                <content_type>text/plain; charset=UTF-8</content_type>\n                <response_content>config://http_server_default_response</response_content>\n            </handler>\n        </rule>\n    </http_handlers>\n    -->\n\n    <send_crash_reports>\n        <!-- Changing <enabled> to true allows sending crash reports to -->\n        <!-- the ClickHouse core developers team via Sentry https://sentry.io -->\n        <!-- Doing so at least in pre-production environments is highly appreciated -->\n        <enabled>false</enabled>\n        <!-- Change <anonymize> to true if you don't feel comfortable attaching the server hostname to the crash report -->\n        <anonymize>false</anonymize>\n        <!-- Default endpoint should be changed to different Sentry DSN only if you have -->\n        <!-- some in-house engineers or hired consultants who're going to debug ClickHouse issues for you -->\n        <endpoint>https://6f33034cfe684dd7a3ab9875e57b1c8d@o388870.ingest.sentry.io/5226277</endpoint>\n    </send_crash_reports>\n\n    <!-- Uncomment to disable ClickHouse internal DNS caching. -->\n    <!-- <disable_internal_dns_cache>1</disable_internal_dns_cache> -->\n\n    <!-- You can also configure rocksdb like this: -->\n    <!--\n    <rocksdb>\n        <options>\n            <max_background_jobs>8</max_background_jobs>\n        </options>\n        <column_family_options>\n            <num_levels>2</num_levels>\n        </column_family_options>\n        <tables>\n            <table>\n                <name>TABLE</name>\n                <options>\n                    <max_background_jobs>8</max_background_jobs>\n                </options>\n                <column_family_options>\n                    <num_levels>2</num_levels>\n                </column_family_options>\n            </table>\n        </tables>\n    </rocksdb>\n    -->\n</clickhouse>\n"
  },
  {
    "path": "config-secure/my-own-ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDEDCCAfigAwIBAgIUeyhiP/kbQCZacNPgv+BdoUBN6PcwDQYJKoZIhvcNAQEL\nBQAwDzENMAsGA1UEAwwEcm9vdDAeFw0yMTEyMTYyMDU5MjVaFw0zMTEyMTQyMDU5\nMjVaMA8xDTALBgNVBAMMBHJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\nAoIBAQDNU3zvxLjeTDnRi2Vlqd3UaO8ViV/jjQwj6Gtx7QceVe034URS7Zq8WlsV\nHjn8a2e6ygebjy02Ri+fpG/NbHWOpS58VJnFAXT+iSrvyr6PHPFNx7ncNiNrwvHs\n9YuD0njOWrj0JpAa8oxGm1NOpqCUg0ytwHwF4YJ9Lk9f2F6heRhlvFECWf8k58rR\nUI/N0eOrjp3IDpYGupfa07Sug+EExpg8/CV1mV2HHnvvcLWRMsn4yJD6gUu64anA\nj1LklYEFyBUUnv7L7EFCH9wOx4QZSQOWrOHVh5l60Ib/6kUm4NYW8f1MY64Cd5WZ\nY5+JWGoU2wEk1kKXUfx0RGFRcHSrAgMBAAGjZDBiMB0GA1UdDgQWBBTIAlr/Acx3\nlsu9BqF2+GALhGlJDTAfBgNVHSMEGDAWgBTIAlr/Acx3lsu9BqF2+GALhGlJDTAP\nBgNVHRMBAf8EBTADAQH/MA8GA1UdEQQIMAaCBHJvb3QwDQYJKoZIhvcNAQELBQAD\nggEBAMmpw+w0k3U0faH0ldyDiIZiyP+4u6VS9CEwaeB3hX5dGQr/Ya0ibqCZRKCG\nhp4tnMNwPPULV/P4uyM08yLi9oIP+Dm457xe7DCe7Eg87l+EZeVZY7oG3HgXMU29\nt9cN3N7OVqKh4XpsZw4YmaXVB5KQx+TNYjS/4+pJEXNSP+ntDckAAuXbLoj7NSEu\nV/uJGOMTLYgk5R1tL4+HM80lrHlGrmwsc0RiATGVFPxIZQZ5X61RSjbA+VcHncw5\np+HrrnmPCAxET+B/lL6NhaIKUQmRnbdAnT+4dyKRfBYiwy+pk9N3pjqOoQAM6/Cz\nkdPUedu68KypMieaM23RTg0posg=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "config-secure/my-own-ca.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDNU3zvxLjeTDnR\ni2Vlqd3UaO8ViV/jjQwj6Gtx7QceVe034URS7Zq8WlsVHjn8a2e6ygebjy02Ri+f\npG/NbHWOpS58VJnFAXT+iSrvyr6PHPFNx7ncNiNrwvHs9YuD0njOWrj0JpAa8oxG\nm1NOpqCUg0ytwHwF4YJ9Lk9f2F6heRhlvFECWf8k58rRUI/N0eOrjp3IDpYGupfa\n07Sug+EExpg8/CV1mV2HHnvvcLWRMsn4yJD6gUu64anAj1LklYEFyBUUnv7L7EFC\nH9wOx4QZSQOWrOHVh5l60Ib/6kUm4NYW8f1MY64Cd5WZY5+JWGoU2wEk1kKXUfx0\nRGFRcHSrAgMBAAECggEAIYapVskrWnjd0/5L3y6+XumHaF/W/WPRgKd5q8+FIwnq\nwv4QVu4fHvQt/SPDWhj7hf9pAJh/TGZnbky+SK+V/mWwUnLJ7OYRAWLKVP8o4Ftc\nd1POYEuiuvzI3eU2E58xRJiBrLQDQbMq/nhsQOJqQ/WwdoqAdcFduizunvrIcNr3\nIB4uUpwhvQSt8ve7BET+/rmFypHP9Ck7wvUq4QkVSKgvKrLbGwQi6gmJlf1UnDGw\nEHIwSfhSmKgoney4wlX3G3tk6KdxFfSA+RBTcPGRfKGqa/zWYPQ8eabQGXSkcIxS\noXwJ9f4UE1vHMXveth7tPAxN33Zfx1RB6IVRPt4jSQKBgQDrYkmoT7EbCGa8GkWV\nm+0ijBdcwQx7ohDYi5G3smN17pDUDvaHPh/bOoEdB7cxJJ0f2UHqah6Ha7pQt55q\n+sfo1Tjfs53JyFFTQLmCa3HAzarggB5cPTfRDRb5CKWiMPxBkvYjVjvV/r9iseZy\nojY3SGg5ezhadUuLWYi0wbE1vQKBgQDfT0CmPSObkr7Z8waHLFG3odVzT7IdsfpM\nQmOOXC2p/I2p5CWt9goZ60kA2exQvOkyjYSTX7xYTB8QWthkdH3G2k4UrM7tWu8X\nsI6NeQPXGU3Th0yzb7k5qKoI0/bFC9YBuok/5Y1Y8vP4HoKmKzQDN2UrbVMwgc/e\nJpfwTph2hwKBgEyhewloqGf8nDWw9+Z1FQaiRRjVYJL/eCyHg7EiSm8ic9QV6vys\npQJiUZZ55JIDMYQk3ujKE5ZS5B1TKif57QtIH3P0rfH7XT6VW8+x2x7B1lewXjH5\nXCqa8FezEPl0qStQBQIMGP7aKMSg1j2LwcrNr+DG1NneRfHf/DmctWyhAoGAeUrl\n1aXNynnJmj5rpE5JUJHhi5GVMJX0WymQQ8oDr5oTJF1crgG++NcYvxKfTjdd/uxp\nP1c3yUoHcW22rdGsY689y/MVLk0/IsHunB9IG7SN1kBeQ/SCSjQ3rzXaiqrkIeo9\nFGzN+qt0IqgH1NQQm1KibBUko2tPCd4ylv9Jxs8CgYAG6XKpW30J5m1BiJVL8lx4\nDTfhOvbtXiq/n5xHgqu81ven8b9MURqOkOlCnMrNatySaZgMYfCzAL+EARWgVOeI\npGmXLNEDGhPnv4IrZbIiVXIzTntufQZybaJRPSYQ+r8GBDSZtNVlwPHe/up+906q\nH6PEd7NnDbL2TOr57Hfsow==\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "config-secure/my-own-ca.srl",
    "content": "F1882B6FB9749F92\n"
  },
  {
    "path": "config-secure/server.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDFTCCAf2gAwIBAgIUT3+4BMdujNm5CYsgrvGHHq+1CtIwDQYJKoZIhvcNAQEL\nBQAwDzENMAsGA1UEAwwEcm9vdDAeFw0yMTEyMTYyMjE0MTJaFw0yNDAzMjAyMjE0\nMTJaMA4xDDAKBgNVBAMMA2ZvbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\nggEBAOVt8A/EdMGRPAIB+jEqAeLlXJFgDgGr5cyfDMX2Em8B1WGkz8rTrQmJJdGr\nyjKICQxAjcNoyB4XfRuoPtdJV+z+VzlvOYn5NB6Tiq+2ZYbpXMexX6lAwsYZuqvb\nFR0Y5BOGLKhUz7BzyZiC+GMNesPKwKBscA+82LKS446d+XrWxC5oirTnho7jj/y9\nSIBP7OPz5tewEMZnS2Tw8a/nhoVl0jXsbV9PwmfPtwepDN5wpSZuN56qHYBpnmz/\nP6XUmIs/BeymY8tlqQ6n1PaXt4G0UVnqM8GY9WUxZ3oYbNH1kyoey4OjuwzJCCV1\nldm+2maGTYJ8xRgyFC/b0gM/pvcCAwEAAaNqMGgwHwYDVR0jBBgwFoAUyAJa/wHM\nd5bLvQahdvhgC4RpSQ0wCQYDVR0TBAIwADALBgNVHQ8EBAMCBPAwDgYDVR0RBAcw\nBYIDZm9vMB0GA1UdDgQWBBR5qSjNuOQvBcSf09EE3V8WHCcnuDANBgkqhkiG9w0B\nAQsFAAOCAQEAHVAXj2a+s2o7mfPm26heUp/EXJfD9dNDIjBxxb8JCk8MHRMk+0tP\n0PkpfCreg1TU5aUIjnIKPw5GmTK6QiDvtuwgz2pyMVqHQ2LZiKRc7sZWbhY42H21\n4qfJmsheoohUyyib+hwRpektNNyuMJEDHTAzZ/5/H5kvgY4WR8mNLOHepG8JZure\n+P8ACffg6xx0zPqkH58TOWnBi9gwjLjSOteUnamd8XdP7mCMLYCnuV7lIaxtTspv\nTyCqZMgNme5rf/oP/F9O4IZPgjdPAmYDDwsJtCAnlB72FFvO4+7lOw2nl2t1DP/K\nMUBEB1R3Z6mmh7HqMqu1V4yLov2vJL7+aQ==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "config-secure/server.csr",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIICUzCCATsCAQAwDjEMMAoGA1UEAwwDZm9vMIIBIjANBgkqhkiG9w0BAQEFAAOC\nAQ8AMIIBCgKCAQEA5W3wD8R0wZE8AgH6MSoB4uVckWAOAavlzJ8MxfYSbwHVYaTP\nytOtCYkl0avKMogJDECNw2jIHhd9G6g+10lX7P5XOW85ifk0HpOKr7Zlhulcx7Ff\nqUDCxhm6q9sVHRjkE4YsqFTPsHPJmIL4Yw16w8rAoGxwD7zYspLjjp35etbELmiK\ntOeGjuOP/L1IgE/s4/Pm17AQxmdLZPDxr+eGhWXSNextX0/CZ8+3B6kM3nClJm43\nnqodgGmebP8/pdSYiz8F7KZjy2WpDqfU9pe3gbRRWeozwZj1ZTFnehhs0fWTKh7L\ng6O7DMkIJXWV2b7aZoZNgnzFGDIUL9vSAz+m9wIDAQABoAAwDQYJKoZIhvcNAQEL\nBQADggEBALMPoIT9ppQ8bXa1Dke3V1BbEXz5oI483NR6KIdKspqOewure2GddnmQ\n5ej5mo+X7Jqo8iYrA/UwxZ6Nu26GkfS7YxPy7dJa1QNwaSTnq4eQtFzzg8crcGcX\n8GSsEY0JVv2ZgJ921ov0INxpkas9WA/Whs2G1+yhj8NrGSySxpxRtn8xJyuSqJLI\nNrshzQkaqyLHT5PtjlWRk4zWFZx9dXQY18zu7Tia4JXfH4xq+eAFYhaZv8rhKDef\nibhexj+lkIOVh4EpUdhqrsfRhjYi9mv+9K88C6rVuYkG8Ln/JVG2TrvXfLgBWqtT\nuT1cXxOTN4ilH7Sckf/YHPTEd5nn8go=\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "config-secure/server.ext",
    "content": "authorityKeyIdentifier=keyid,issuer\nbasicConstraints=CA:FALSE\nkeyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment\nsubjectAltName = @alt_names\n\n[alt_names]\nDNS.1 = foo"
  },
  {
    "path": "config-secure/server.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDlbfAPxHTBkTwC\nAfoxKgHi5VyRYA4Bq+XMnwzF9hJvAdVhpM/K060JiSXRq8oyiAkMQI3DaMgeF30b\nqD7XSVfs/lc5bzmJ+TQek4qvtmWG6VzHsV+pQMLGGbqr2xUdGOQThiyoVM+wc8mY\ngvhjDXrDysCgbHAPvNiykuOOnfl61sQuaIq054aO44/8vUiAT+zj8+bXsBDGZ0tk\n8PGv54aFZdI17G1fT8Jnz7cHqQzecKUmbjeeqh2AaZ5s/z+l1JiLPwXspmPLZakO\np9T2l7eBtFFZ6jPBmPVlMWd6GGzR9ZMqHsuDo7sMyQgldZXZvtpmhk2CfMUYMhQv\n29IDP6b3AgMBAAECggEABzYgnp7fKz99rrjH4aNYCZqdxl4E+0133z2LNKmUcyKQ\nRc3c95Gf9bhFhinBrrF33moRL5YahldOvjXEuvjts8c0N1vT59OFaHJS26+XH+hu\n5S5JNjQkt3Smu91L1vIa/599xcdbwFJN2Zy3CejHrBz3Q713SkMJ/NIGABDekr3H\nopXiz8XPnMnxprCMlDSLWZV7NXILCdOnUnvZStzxr9/g3L/vA2ja9wvtYJQYf44Y\n3hASo6wAd4mCIW/s/yJXEp6Hzxs0AKeAP3rB/Q4NCvPf77SASPxYySACDRCJYTS1\nf+38MhinS2wUOYtd37Byx051ip7Udym3KQ2cLQrY3QKBgQDnvCeDvmBhx/XDUXYS\n9WrcZnL/BDVxPEc4ZMMfLlXVHtPNae+BBQusXVsAaCz/J3EzXq7T6b8we5tyLy8Y\nmxjrL6CEMFRPDN9G78clDhMoruEe912BDPXT2zfm21s2oX2n6UwIyXg+JOQBIFZ/\n/MTBAr0yODxwMfcb+nKPtUYIZQKBgQD9c/tOzqUB9Bwh7a2FNEKnthCBmIXKb2Hq\njmic11YxYLdyys8P4/4N12SChJeXAm4RUaCu19I4l3xDzY7OjlmlnqSHVbncPvKP\nzuH1L4uOxUBI+76REmyGzomXPQ7rV2OAvOEkSpwKkajHegc0zO3wS4lzBvt8xyOq\njTwPyFRmKwKBgFiUMFqIe9kEkSmuyr5mdwl2U8CtACyfiO3Cfl892+tSFE3xj242\n2oZxTOaz63dAwWGMcLFqKP3EUd/sr0jtiDHmC6pbuu5YkkRQRUQhxCsJ5d1rWp+I\nr7LimdSxxoT0Z862O60kLcU7Xrgbf1T+7sqEXIOEwX11a+qS6hWKihGNAoGBAOYA\nZ3GHw2Q3c4QynUIBP+/UH7yLffZMB66El1ilbZmXrEJm22sPOlCzQ4nR64LleJ8M\n1WV1g1dJ2UHqe4rk0WOjyKjr2aOOGC76zkDjaaEhTYotsi0SbBwVt/TgOvbEsg50\n2VdGwb4xmtmS2pFG2zIySkRxdK0yRiKS0ot7/2NLAoGAJ4wqdfPVGH9aNd1JP5Kh\nMUH+UB8IJYehrfSeYxVp/wmcaSyZNZALueFyFn0rdKaLseOxc/OjOg4xr9tM5z5+\nhFstZwRHjt0Yb2xAyCzWBhYDpP3RZ81S3HAUtb7GL6RAvcpgXBoOgV7r6yiZMYK5\n4ePsQTp/Xv9Vg2KvwQpJyCU=\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "config-secure/users.xml",
    "content": "<?xml version=\"1.0\"?>\n<clickhouse>\n    <!-- See also the files in users.d directory where the settings can be overridden. -->\n\n    <!-- Profiles of settings. -->\n    <profiles>\n        <!-- Default settings. -->\n        <default>\n            <!-- Maximum memory usage for processing single query, in bytes. -->\n            <max_memory_usage>10000000000</max_memory_usage>\n\n            <!-- How to choose between replicas during distributed query processing.\n                 random - choose random replica from set of replicas with minimum number of errors\n                 nearest_hostname - from set of replicas with minimum number of errors, choose replica\n                  with minimum number of different symbols between replica's hostname and local hostname\n                  (Hamming distance).\n                 in_order - first live replica is chosen in specified order.\n                 first_or_random - if first replica one has higher number of errors, pick a random one from replicas with minimum number of errors.\n            -->\n            <load_balancing>random</load_balancing>\n        </default>\n\n        <!-- Profile that allows only read queries. -->\n        <readonly>\n            <readonly>1</readonly>\n        </readonly>\n    </profiles>\n\n    <!-- Users and ACL. -->\n    <users>\n        <!-- If user name was not specified, 'default' user is used. -->\n        <default>\n            <!-- See also the files in users.d directory where the password can be overridden.\n                 Password could be specified in plaintext or in SHA256 (in hex format).\n                 If you want to specify password in plaintext (not recommended), place it in 'password' element.\n                 Example: <password>qwerty</password>.\n                 Password could be empty.\n                 If you want to specify SHA256, place it in 'password_sha256_hex' element.\n                 Example: <password_sha256_hex>65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5</password_sha256_hex>\n                 Restrictions of SHA256: impossibility to connect to ClickHouse using MySQL JS client (as of July 2019).\n                 If you want to specify double SHA1, place it in 'password_double_sha1_hex' element.\n                 Example: <password_double_sha1_hex>e395796d6546b1b65db9d665cd43f0e858dd4303</password_double_sha1_hex>\n                 If you want to specify a previously defined LDAP server (see 'ldap_servers' in the main config) for authentication,\n                  place its name in 'server' element inside 'ldap' element.\n                 Example: <ldap><server>my_ldap_server</server></ldap>\n                 If you want to authenticate the user via Kerberos (assuming Kerberos is enabled, see 'kerberos' in the main config),\n                  place 'kerberos' element instead of 'password' (and similar) elements.\n                 The name part of the canonical principal name of the initiator must match the user name for authentication to succeed.\n                 You can also place 'realm' element inside 'kerberos' element to further restrict authentication to only those requests\n                  whose initiator's realm matches it.\n                 Example: <kerberos />\n                 Example: <kerberos><realm>EXAMPLE.COM</realm></kerberos>\n                 How to generate decent password:\n                 Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo \"$PASSWORD\"; echo -n \"$PASSWORD\" | sha256sum | tr -d '-'\n                 In first line will be password and in second - corresponding SHA256.\n                 How to generate double SHA1:\n                 Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo \"$PASSWORD\"; echo -n \"$PASSWORD\" | sha1sum | tr -d '-' | xxd -r -p | sha1sum | tr -d '-'\n                 In first line will be password and in second - corresponding double SHA1.\n            -->\n            <password></password>\n\n            <!-- List of networks with open access.\n                 To open access from everywhere, specify:\n                    <ip>::/0</ip>\n                 To open access only from localhost, specify:\n                    <ip>::1</ip>\n                    <ip>127.0.0.1</ip>\n                 Each element of list has one of the following forms:\n                 <ip> IP-address or network mask. Examples: 213.180.204.3 or 10.0.0.1/8 or 10.0.0.1/255.255.255.0\n                     2a02:6b8::3 or 2a02:6b8::3/64 or 2a02:6b8::3/ffff:ffff:ffff:ffff::.\n                 <host> Hostname. Example: server01.yandex.ru.\n                     To check access, DNS query is performed, and all received addresses compared to peer address.\n                 <host_regexp> Regular expression for host names. Example, ^server\\d\\d-\\d\\d-\\d\\.yandex\\.ru$\n                     To check access, DNS PTR query is performed for peer address and then regexp is applied.\n                     Then, for result of PTR query, another DNS query is performed and all received addresses compared to peer address.\n                     Strongly recommended that regexp is ends with $\n                 All results of DNS requests are cached till server restart.\n            -->\n            <networks>\n                <ip>::/0</ip>\n            </networks>\n\n            <!-- Settings profile for user. -->\n            <profile>default</profile>\n\n            <!-- Quota for user. -->\n            <quota>default</quota>\n\n            <!-- User can create other users and grant rights to them. -->\n            <!-- <access_management>1</access_management> -->\n        </default>\n    </users>\n\n    <!-- Quotas. -->\n    <quotas>\n        <!-- Name of quota. -->\n        <default>\n            <!-- Limits for time interval. You could specify many intervals with different limits. -->\n            <interval>\n                <!-- Length of interval. -->\n                <duration>3600</duration>\n\n                <!-- No limits. Just calculate resource usage for time interval. -->\n                <queries>0</queries>\n                <errors>0</errors>\n                <result_rows>0</result_rows>\n                <read_rows>0</read_rows>\n                <execution_time>0</execution_time>\n            </interval>\n        </default>\n    </quotas>\n</clickhouse>"
  },
  {
    "path": "cspell.config.json",
    "content": "{\n  \"ignorePaths\": [\n    \"node_modules/**\",\n    \"coverage/**\",\n    \"provisioning/**\",\n    \"src/dashboards/**\",\n    \"dist/**\",\n    \"yarn.lock\",\n    \"package-lock.json\",\n    \"go.sum\",\n    \"mage_output_file.go\",\n    \"test-results/**\",\n    \"test_summary.json\",\n    \"playwright-report/**\"\n  ],\n  \"words\": [\n    \"aggregatable\",\n    \"aheads\",\n    \"ASOF\",\n    \"apikey\",\n    \"buildinfo\",\n    \"CHSQL\",\n    \"ClickHouse\",\n    \"closedate\",\n    \"codefile\",\n    \"combobox\",\n    \"commitish\",\n    \"concats\",\n    \"createddate\",\n    \"dataframe\",\n    \"dataproxy\",\n    \"DataSource\",\n    \"datasourcese\",\n    \"DataSources\",\n    \"Drilldown\",\n    \"DateTime\",\n    \"dbug\",\n    \"dedup\",\n    \"elazarl\",\n    \"emerg\",\n    \"endregion\",\n    \"eror\",\n    \"errorsource\",\n    \"fixedstring\",\n    \"fromtime\",\n    \"godoc\",\n    \"goproxy\",\n    \"Grafana\",\n    \"grafanalabs\",\n    \"groupable\",\n    \"healthcheck\",\n    \"ILIKE\",\n    \"instancemgmt\",\n    \"jsonencode\",\n    \"lowcardinality\",\n    \"magefile\",\n    \"multiquery\",\n    \"mgbench\",\n    \"Milli\",\n    \"millis\",\n    \"moby\",\n    \"mydb\",\n    \"networkidle\",\n    \"nofile\",\n    \"noreferrer\",\n    \"nolint\",\n    \"operationname\",\n    \"Nonproxy\",\n    \"openfeature\",\n    \"oper\",\n    \"OFREP\",\n    \"otel\",\n    \"semconv\",\n    \"OUTFILE\",\n    \"paulmach\",\n    \"pgsql\",\n    \"picklist\",\n    \"picomatch\",\n    \"PREWHERE\",\n    \"proxying\",\n    \"regexes\",\n    \"schemacache\",\n    \"schemads\",\n    \"sdkconfig\",\n    \"sdkproxy\",\n    \"servicename\",\n    \"shopspring\",\n    \"singlequote\",\n    \"slvrtrn\",\n    \"sqlds\",\n    \"sqlutil\",\n    \"stagename\",\n    \"stretchr\",\n    \"subquery\",\n    \"subqueries\",\n    \"subresource\",\n    \"sugg\",\n    \"supress\",\n    \"templating\",\n    \"testcontainers\",\n    \"testid\",\n    \"timefilter\",\n    \"timepicker's\",\n    \"timerange\",\n    \"timeseries\",\n    \"timespan\",\n    \"TLSCA\",\n    \"TLSSSL\",\n    \"toggleTip\",\n    \"Toggletip\",\n    \"totime\",\n    \"traceid\",\n    \"typecheck\",\n    \"Ulimits\",\n    \"uuidv\",\n    \"uvcf\",\n    \"vectorator\",\n    \"WorkDir\",\n    \"varname\"\n  ]\n}\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n  grafana:\n    extends:\n      file: .config/docker-compose-base.yaml\n      service: grafana\n    depends_on:\n      clickhouse-server:\n        condition: service_healthy\n      e2e-data-loader:\n        condition: service_completed_successfully\n    networks:\n      - grafana\n    environment:\n      # splashScreen is disabled at runtime for E2E tests via @grafana/plugin-e2e's\n      # OFREP interception (>=3.5.0), but we also disable it at the server level so\n      # that manual `docker-compose up` + browsing doesn't trigger the onboarding modal.\n      - GF_FEATURE_TOGGLES_splashScreen=false\n      - GF_FEATURE_TOGGLES_dashboardNewLayouts=false\n      - GF_FEATURE_TOGGLES_newClickhouseConfigPageDesign=true\n      - GF_FEATURE_TOGGLES_clickHouseConfigValidation=true\n\n  clickhouse-server:\n    image: clickhouse/clickhouse-server:${CLICKHOUSE_VERSION:-latest-alpine}\n    container_name: clickhouse-server\n    ports:\n      - 8123:8123\n      - 9000:9000\n    ulimits:\n      nofile:\n        soft: 262144\n        hard: 262144\n    healthcheck:\n      test: [\"CMD\", \"clickhouse-client\", \"--host\", \"clickhouse-server\", \"--query\", \"SELECT 1\"]\n      interval: 1m30s\n      timeout: 30s\n      retries: 5\n      start_period: 30s\n    environment:\n      - CLICKHOUSE_SKIP_USER_SETUP=1\n    networks:\n      - grafana\n\n  e2e-data-loader:\n    image: clickhouse/clickhouse-server:${CLICKHOUSE_VERSION:-latest-alpine}\n    container_name: e2e-data-loader\n    entrypoint:\n      - sh\n      - -c\n      - |\n        set -e\n        # Load every *.sql fixture under /data in lexicographic order so new\n        # feature-specific fixtures can live alongside seed.sql without\n        # creating merge conflicts when multiple PRs add fixtures at once.\n        # $$f (not $f) so docker-compose passes a literal $ to the shell.\n        for f in /data/*.sql; do\n          echo \"Loading fixture: $$f\"\n          clickhouse-client --host clickhouse-server --multiquery < \"$$f\"\n        done\n    depends_on:\n      clickhouse-server:\n        condition: service_healthy\n    restart: no\n    volumes:\n      - ./tests/e2e/fixtures:/data:ro\n    networks:\n      - grafana\n\nnetworks:\n  grafana: \n"
  },
  {
    "path": "docs/Makefile",
    "content": ".ONESHELL:\n.DELETE_ON_ERROR:\nexport SHELL := bash\nexport SHELLOPTS := pipefail:errexit\nMAKEFLAGS += --warn-undefined-variables\nMAKEFLAGS += --no-builtin-rule\n\ninclude docs.mk\n"
  },
  {
    "path": "docs/docs.mk",
    "content": "# The source of this file is https://raw.githubusercontent.com/grafana/writers-toolkit/main/docs/docs.mk.\n# A changelog is included in the head of the `make-docs` script.\ninclude variables.mk\n-include variables.mk.local\n\n.ONESHELL:\n.DELETE_ON_ERROR:\nexport SHELL     := bash\nexport SHELLOPTS := pipefail:errexit\nMAKEFLAGS += --warn-undefined-variables\nMAKEFLAGS += --no-builtin-rule\n\n.DEFAULT_GOAL: help\n\n# Adapted from https://www.thapaliya.com/en/writings/well-documented-makefiles/\n.PHONY: help\nhelp: ## Display this help.\nhelp:\n\t@awk 'BEGIN { \\\n\t\tFS = \": ##\"; \\\n\t\tprintf \"Usage:\\n  make <target>\\n\\nTargets:\\n\" \\\n\t} \\\n\t/^[a-zA-Z0-9_\\.\\-\\/%]+: ##/ { printf \"  %-15s %s\\n\", $$1, $$2 }' \\\n\t$(MAKEFILE_LIST)\n\nGIT_ROOT := $(shell git rev-parse --show-toplevel)\n\nPODMAN := $(shell if command -v podman >/dev/null 2>&1; then echo podman; else echo docker; fi)\n\nifeq ($(PROJECTS),)\n$(error \"PROJECTS variable must be defined in variables.mk\")\nendif\n\n# Host port to publish container port to.\nifeq ($(origin DOCS_HOST_PORT), undefined)\nexport DOCS_HOST_PORT := 3002\nendif\n\n# Container image used to perform Hugo build.\nifeq ($(origin DOCS_IMAGE), undefined)\nexport DOCS_IMAGE := grafana/docs-base:latest\nendif\n\n# Container image used for Vale linting.\nifeq ($(origin VALE_IMAGE), undefined)\nexport VALE_IMAGE := grafana/vale:latest\nendif\n\n# PATH-like list of directories within which to find projects.\n# If all projects are checked out into the same directory, ~/repos/ for example, then the default should work.\nifeq ($(origin REPOS_PATH), undefined)\nexport REPOS_PATH := $(realpath $(GIT_ROOT)/..)\nendif\n\n# How to treat Hugo relref errors.\nifeq ($(origin HUGO_REFLINKSERRORLEVEL), undefined)\nexport HUGO_REFLINKSERRORLEVEL := WARNING\nendif\n\n# Whether to pull the latest container image before running the container.\nifeq ($(origin PULL), undefined)\nexport PULL := true\nendif\n\n.PHONY: docs-rm\ndocs-rm: ## Remove the docs container.\n\t$(PODMAN) rm -f $(DOCS_CONTAINER)\n\n.PHONY: docs-pull\ndocs-pull: ## Pull documentation base image.\n\t$(PODMAN) pull -q $(DOCS_IMAGE)\n\nmake-docs: ## Fetch the latest make-docs script.\nmake-docs:\n\tif [[ ! -f \"$(CURDIR)/make-docs\" ]]; then\n\t\techo 'WARN: No make-docs script found in the working directory. Run `make update` to download it.' >&2\n\t\texit 1\n\tfi\n\n.PHONY: docs\ndocs: ## Serve documentation locally, which includes pulling the latest `DOCS_IMAGE` (default: `grafana/docs-base:latest`) container image. To not pull the image, set `PULL=false`.\nifeq ($(PULL), true)\ndocs: docs-pull make-docs\nelse\ndocs: make-docs\nendif\n\t$(CURDIR)/make-docs $(PROJECTS)\n\n.PHONY: docs-debug\ndocs-debug: ## Run Hugo web server with debugging enabled. TODO: support all SERVER_FLAGS defined in website Makefile.\ndocs-debug: make-docs\n\tWEBSITE_EXEC='hugo server --bind 0.0.0.0 --port 3002 --logLevel debug' $(CURDIR)/make-docs $(PROJECTS)\n\n.PHONY: vale\nvale: ## Run vale on the entire docs folder which includes pulling the latest `VALE_IMAGE` (default: `grafana/vale:latest`) container image. To not pull the image, set `PULL=false`.\nvale: make-docs\nifeq ($(PULL), true)\n\t$(PODMAN) pull -q $(VALE_IMAGE)\nendif\n\tDOCS_IMAGE=$(VALE_IMAGE) $(CURDIR)/make-docs $(PROJECTS)\n\n.PHONY: update\nupdate: ## Fetch the latest version of this Makefile and the `make-docs` script from Writers' Toolkit.\n\tcurl -s -LO https://raw.githubusercontent.com/grafana/writers-toolkit/main/docs/docs.mk\n\tcurl -s -LO https://raw.githubusercontent.com/grafana/writers-toolkit/main/docs/make-docs\n\tchmod +x make-docs\n\n# ls static/templates/ | sed 's/-template\\.md//' | xargs\nTOPIC_TYPES := concept multiple-tasks reference section task tutorial visualization\n.PHONY: $(patsubst %,topic/%,$(TOPIC_TYPES))\ntopic/%: ## Create a topic from the Writers' Toolkit template. Specify the topic type as the target, for example, `make topic/task TOPIC_PATH=sources/my-new-topic.md`.\n$(patsubst %,topic/%,$(TOPIC_TYPES)):\n\t$(if $(TOPIC_PATH),,$(error \"You must set the TOPIC_PATH variable to the path where the $(@F) topic will be created. For example: make $(@) TOPIC_PATH=sources/my-new-topic.md\"))\n\tmkdir -p $(dir $(TOPIC_PATH))\n\tcurl -s -o $(TOPIC_PATH) https://raw.githubusercontent.com/grafana/writers-toolkit/refs/heads/main/docs/static/templates/$(@F)-template.md\n"
  },
  {
    "path": "docs/make-docs",
    "content": "#!/bin/sh\n# shellcheck disable=SC2034\n#\n# The source of this file is https://raw.githubusercontent.com/grafana/writers-toolkit/main/docs/make-docs.\n# # `make-docs` procedure changelog\n#\n# Updates should conform to the guidelines in https://keepachangelog.com/en/1.1.0/.\n# [Semantic versioning](https://semver.org/) is used to help the reader identify the significance of changes.\n# Changes are relevant to this script and the support docs.mk GNU Make interface.\n#\n# ## 10.1.1 (2026-03-17)\n#\n# ### Fixed\n#\n# - Run Vale non-interactively to enable Claude Code (and similar) to run the tool.\n#\n# ## 10.1.0 (2025-11-11)\n#\n# ### Fixed\n#\n# - Extend readiness probes to prevent confusing output.\n#\n#   Before, the probes could fail too soon and long an error message which contradicted the service starting up.\n#\n# ## 10.0.0 (2025-10-13)\n#\n# ### Changed\n#\n# - Hugo no longer supports the `--debug` option, use `--logLevel debug` instead.\n#\n#   Thank you to @karlskewes for their contribution!\n#\n# ## 9.0.0 (2025-04-05)\n#\n# ### Removed\n#\n# - doc-validator target and associated scripts.\n#\n#   Most useful rules have been migrated to Vale and the others are often false positives.\n#\n# ## 8.5.2 (2025-02-28)\n#\n# ### Fixed\n#\n# - topic/<KIND> targets are no longer no-ops as a result of 8.5.1.\n#\n# ## 8.5.1 (2025-02-18)\n#\n# ### Fixed\n#\n# - PHONY declaration for topic/<KIND> targets.\n#\n# ## 8.5.0 (2025-02-13)\n#\n# ### Added\n#\n# - make topic/<KIND> TOPIC_PATH=<PATH> target to create a new topic from the Writers' Toolkit templates.\n#\n# ## 8.4.0 (2025-01-27)\n#\n# ### Fixed\n#\n# - Correct mount for the /docs/grafana-cloud/send-data/fleet-management/ project.\n#\n# ## 8.3.0 (2024-12-27)\n#\n# ### Added\n#\n# - Debug output of the final command when DEBUG=true.\n#\n#   Useful to inspect if the script is correctly constructing the final command.\n#\n# ## 8.2.0 (2024-12-22)\n#\n# ### Removed\n#\n# - Special cases for Oracle and Datadog plugins now that they exist in the plugins monorepo.\n#\n# ## 8.1.0 (2024-08-22)\n#\n# ### Added\n#\n# - Additional website mounts for projects that use the website repository.\n#\n#   Mounts are required for `make docs` to work in the website repository or with the website project.\n#   The Makefile is also mounted for convenient development of the procedure in that repository.\n#\n# ## 8.0.1 (2024-07-01)\n#\n# ### Fixed\n#\n# - Update log suppression to catch new format of website /docs/ homepage REF_NOT_FOUND warnings.\n#\n#   These warnings are related to missing some pages during the build that are required for the /docs/ homepage.\n#   They were previously suppressed but the log format changed and without this change they reappear in the latest builds.\n#\n# ## 8.0.0 (2024-05-28)\n#\n# ### Changed\n#\n# - Add environment variable `OUTPUT_FORMAT` to control the output of commands.\n#\n#   The default value is `human` and means the output format is human readable.\n#   The value `json` is also supported and outputs JSON.\n#\n#   Note that the `json` format isn't supported by `make docs`, only `make doc-validator` and `make vale`.\n#\n# ## 7.0.0 (2024-05-03)\n#\n# ### Changed\n#\n# - Pull images for all recipes that use containers by default.\n#\n#   Use the `PULL=false` variable to disable this behavior.\n#\n# ### Removed\n#\n# - The `docs-no-pull` target as it's redundant with the new `PULL=false` variable.\n#\n# ## 6.1.0 (2024-04-22)\n#\n# ### Changed\n#\n# - Mount volumes with SELinux labels.\n#\n#   https://docs.docker.com/storage/bind-mounts/#configure-the-selinux-label\n#\n# ### Added\n#\n# - Pseudo project for including only website resources and no website content.\n#\n#   Facilitates testing shortcodes and layout changes with a small documentation set instead of Grafana Cloud or the entire website.\n#\n# ## 6.0.1 (2024-02-28)\n#\n# ### Added\n#\n# - Suppress new errors relating to absent content introduced in https://github.com/grafana/website/pull/17561.\n#\n# ## 6.0.0 (2024-02-16)\n#\n# ### Changed\n#\n# - Require `jq` for human readable `make doc-validator` output.\n#\n# ## 5.4.0 (2024-02-12)\n#\n# ### Changed\n#\n# - Set `WEBSITE_MOUNTS=true` when a user includes the `website` project.\n#\n#   Ensures consistent behavior across repositories.\n#   To disable website mounts, add `export WEBSITE_MOUNTS := false` to your `variables.mk` or `variables.mk.local` file.\n# - Use website mounts and container volumes also when a user includes the `grafana-cloud` project.\n#\n# ## 5.3.0 (2024-02-08)\n#\n# ### Changed\n#\n# - Updated support for plugins monorepo now that multiple projects have been moved into it.\n# - Use `printf` instead of `echo` for better portability of output.\n#\n#   https://www.in-ulm.de/~mascheck/various/echo+printf/\n#\n# ## 5.2.0 (2024-01-18)\n#\n# ### Changed\n#\n# - Updated `make vale` to use latest Vale style and configuration.\n# - Updated `make vale` to use platform appropriate image.\n#\n# ## 5.1.2 (2023-11-08)\n#\n# ### Added\n#\n# - Hide manual_mount warning messages from non-debug output.\n#   Set the DEBUG environment variable to see all hidden messages.\n#\n# ## 5.1.1 (2023-10-30)\n#\n# ### Added\n#\n# - Support for Datadog and Oracle data source plugins repositories.\n#\n# ## 5.1.0 (2023-10-20)\n#\n# ### Added\n#\n# - Support for the plugins monorepo.\n#\n# ## 5.0.0 (2023-10-18)\n#\n# ### Added\n#\n# - Improved support for website repository.\n#\n#   Mount more content and provide some feedback to users that the build can take time.\n#\n# - Ability to enter the `grafana/docs-base` container with a shell using the `ENTER` environment variable.\n#\n# ### Fixed\n#\n# - Correct key combination for interrupting the process.\n#\n#   Keyboards use capital letters so this more accurately reflects the exact key combination users are expected to press.\n#\n# ### Removed\n#\n# - Imperfect implementation of container name.\n#\n#   Facilitates running `make vale` and `make docs` at once.\n#   Container names are convenient for recognition in `docker ps` but the current implementation has more downsides than upsides.\n#\n# - Forced platform specification now that multiple architecture images exist.\n#\n#  Significantly speeds up build times on larger repositories.\n#\n# ## 4.2.2 (2023-10-05)\n\n# - Added support for Jira data source and MongoDB data source plugins repositories.\n#\n# ## 4.2.1 (2023-09-13)\n\n# ## Fixed\n#\n# - Improved consistency of the webserver request loop by polling the Hugo port rather than the proxy port.\n#\n# ## 4.2.0 (2023-09-01)\n#\n# ### Added\n#\n# - Retry the initial webserver request up to ten times to allow for the process to start.\n#   If it is still failing after ten seconds, an error message is logged.\n#\n# ## 4.1.1 (2023-07-20)\n#\n# ### Fixed\n#\n# - Replaced use of `realpath` with POSIX compatible alternative to determine default value for REPOS_PATH.\n#\n# ## 4.1.0 (2023-06-16)\n#\n# ### Added\n#\n# - Mounts of `layouts` and `config` directories for the `website` project.\n#   Ensures that local changes to mounts or shortcodes are reflected in the development server.\n#\n# ### Fixed\n#\n# - Version inference for versioned docs pages.\n#   Pages in versioned projects now have the `versioned: true` front matter set to ensure that \"version\" in $.Page.Scratch is set on builds.\n#\n# ## 4.0.0 (2023-06-06)\n#\n# ### Removed\n#\n# - `doc-validator/%` target.\n#   The behavior of the target was not as described.\n#   Instead, to limit `doc-validator` to only specific files, refer to https://grafana.com/docs/writers-toolkit/writing-guide/tooling-and-workflows/validate-technical-documentation/#run-on-specific-files.\n#\n# ## 3.0.0 (2023-05-18)\n#\n# ### Fixed\n#\n# - Compatibility with the updated Make targets in the `website` repository.\n#   `docs` now runs this script itself, `server-docs` builds the site with the `docs` Hugo environment.\n#\n# ## 2.0.0 (2023-05-18)\n#\n# ### Added\n#\n# - Support for the grafana-cloud/frontend-observability/faro-web-sdk project.\n# - Use of `doc-validator` v2.0.x which includes breaking changes to command line options.\n#\n# ### Fixed\n#\n# - Source grafana-cloud project from website repository.\n#\n# ### Added\n#\n# - Support for running the Vale linter with `make vale`.\n#\n# ## 1.2.1 (2023-05-05)\n#\n# ### Fixed\n#\n# - Use `latest` tag of `grafana/vale` image by default instead of hardcoded older version.\n# - Fix mounting multiple projects broken by the changes in 1.0.1\n#\n# ## 1.2.0 (2023-05-05)\n#\n# ### Added\n#\n# - Support for running the Vale linter with `make vale`.\n#\n# ### Fixed\n#\n# ## 1.1.0 (2023-05-05)\n#\n# ### Added\n#\n# - Rewrite error output so it can be followed by text editors.\n#\n# ### Fixed\n#\n# - Fix `docs-debug` container process port.\n#\n# ## 1.0.1 (2023-05-04)\n#\n# ### Fixed\n#\n# - Ensure complete section hierarchy so that all projects have a visible menu.\n#\n# ## 1.0.0 (2023-05-04)\n#\n# ### Added\n#\n# - Build multiple projects simultaneously if all projects are checked out locally.\n# - Run [`doc-validator`](https://github.com/grafana/technical-documentation/tree/main/tools/cmd/doc-validator) over projects.\n# - Redirect project root to mounted version.\n#   For example redirect `/docs/grafana/` to `/docs/grafana/latest/`.\n# - Support for Podman or Docker containers with `PODMAN` environment variable.\n# - Support for projects:\n#   - agent\n#   - enterprise-logs\n#   - enterprise-metrics\n#   - enterprise-traces\n#   - grafana\n#   - grafana-cloud\n#   - grafana-cloud/machine-learning\n#   - helm-charts/mimir-distributed\n#   - helm-charts/tempo-distributed\n#   - incident\n#   - loki\n#   - mimir\n#   - oncall\n#   - opentelemetry\n#   - phlare\n#   - plugins\n#   - slo\n#   - tempo\n#   - writers-toolkit\n\n\nset -ef\n\nreadonly DOCS_HOST_PORT=\"${DOCS_HOST_PORT:-3002}\"\nreadonly DOCS_IMAGE=\"${DOCS_IMAGE:-grafana/docs-base:latest}\"\n\nreadonly DOC_VALIDATOR_INCLUDE=\"${DOC_VALIDATOR_INCLUDE:-.+\\.md$}\"\nreadonly DOC_VALIDATOR_SKIP_CHECKS=\"${DOC_VALIDATOR_SKIP_CHECKS:-^image-}\"\n\nreadonly HUGO_REFLINKSERRORLEVEL=\"${HUGO_REFLINKSERRORLEVEL:-WARNING}\"\nreadonly VALE_MINALERTLEVEL=\"${VALE_MINALERTLEVEL:-error}\"\nreadonly WEBSITE_EXEC=\"${WEBSITE_EXEC:-make server-docs}\"\n\nreadonly OUTPUT_FORMAT=\"${OUTPUT_FORMAT:-human}\"\n\nPODMAN=\"$(if command -v podman >/dev/null 2>&1; then echo podman; else echo docker; fi)\"\n\nif ! command -v curl >/dev/null 2>&1; then\n  if ! command -v wget >/dev/null 2>&1; then\n    # shellcheck disable=SC2016\n    errr 'either `curl` or `wget` must be installed for this script to work.'\n\n    exit 1\n  fi\nfi\n\nif ! command -v \"${PODMAN}\" >/dev/null 2>&1; then\n  # shellcheck disable=SC2016\n  errr 'either `podman` or `docker` must be installed for this script to work.'\n\n  exit 1\nfi\n\n\nabout() {\n  cat <<EOF\nTest documentation locally with multiple source repositories.\n\nThe REPOS_PATH environment variable is a colon (:) separated list of paths in which to look for project repositories.\nEOF\n}\n\nusage() {\n  cat <<EOF\nUsage:\n  REPOS_PATH=<PATH[:<PATH>...]> $0 [<PROJECT>[:<VERSION>[:<REPO>[:<DIR>]]]...]\n\nExamples:\n  REPOS_PATH=~/ext/grafana/ $0 writers-toolkit tempo:latest helm-charts/mimir-distributed:latest:mimir:docs/sources/mimir-distributed\nEOF\n}\n\nif [ $# -lt 1 ]; then\n  cat <<EOF >&2\nERRR: arguments required but not supplied.\n\n$(about)\n\n$(usage)\nEOF\n  exit 1\nfi\n\nreadonly REPOS_PATH=\"${REPOS_PATH:-$(cd \"$(git rev-parse --show-toplevel)/..\" && echo \"${PWD}\")}\"\n\nif [ -z \"${REPOS_PATH}\" ]; then\n  cat <<EOF >&2\nERRR: REPOS_PATH environment variable is required but has not been provided.\n\n$(usage)\nEOF\n  exit 1\nfi\n\n# The following variables comprise a pseudo associative array of project names to source repositories.\n# You only need to set a SOURCES variable if the project name does not match the source repository name.\n# You can get a key identifier using the `identifier` function.\n# To look up the value of any pseudo associative array, use the `aget` function.\nSOURCES_as_code='as-code-docs'\nSOURCES_enterprise_metrics='backend-enterprise'\nSOURCES_enterprise_metrics_='backend-enterprise'\nSOURCES_grafana_cloud='website'\nSOURCES_grafana_cloud_alerting_and_irm_machine_learning='machine-learning'\nSOURCES_grafana_cloud_alerting_and_irm_slo='slo'\nSOURCES_grafana_cloud_k6='k6-docs'\nSOURCES_grafana_cloud_data_configuration_integrations='cloud-onboarding'\nSOURCES_grafana_cloud_frontend_observability_faro_web_sdk='faro-web-sdk'\nSOURCES_grafana_cloud_send_data_fleet_management='fleet-management'\nSOURCES_helm_charts_mimir_distributed='mimir'\nSOURCES_helm_charts_tempo_distributed='tempo'\nSOURCES_opentelemetry='opentelemetry-docs'\nSOURCES_resources='website'\n\n# The following variables comprise a pseudo associative array of project names to versions.\n# You only need to set a VERSIONS variable if it is not the default of 'latest'.\n# You can get a key identifier using the `identifier` function.\n# To look up the value of any pseudo associative array, use the `aget` function.\nVERSIONS_as_code='UNVERSIONED'\nVERSIONS_grafana_cloud='UNVERSIONED'\nVERSIONS_grafana_cloud_alerting_and_irm_machine_learning='UNVERSIONED'\nVERSIONS_grafana_cloud_alerting_and_irm_slo='UNVERSIONED'\nVERSIONS_grafana_cloud_k6='UNVERSIONED'\nVERSIONS_grafana_cloud_data_configuration_integrations='UNVERSIONED'\nVERSIONS_grafana_cloud_frontend_observability_faro_web_sdk='UNVERSIONED'\nVERSIONS_grafana_cloud_send_data_fleet_management='UNVERSIONED'\nVERSIONS_opentelemetry='UNVERSIONED'\nVERSIONS_resources='UNVERSIONED'\nVERSIONS_technical_documentation='UNVERSIONED'\nVERSIONS_website='UNVERSIONED'\nVERSIONS_writers_toolkit='UNVERSIONED'\n\n# The following variables comprise a pseudo associative array of project names to source repository paths.\n# You only need to set a PATHS variable if it is not the default of 'docs/sources'.\n# You can get a key identifier using the `identifier` function.\n# To look up the value of any pseudo associative array, use the `aget` function.\nPATHS_grafana_cloud='content/docs/grafana-cloud'\nPATHS_helm_charts_mimir_distributed='docs/sources/helm-charts/mimir-distributed'\nPATHS_helm_charts_tempo_distributed='docs/sources/helm-charts/tempo-distributed'\nPATHS_mimir='docs/sources/mimir'\nPATHS_resources='content'\nPATHS_tempo='docs/sources/tempo'\nPATHS_website='content'\n\n# identifier STR\n# Replace characters that are not valid in an identifier with underscores.\nidentifier() {\n  echo \"$1\" | tr -C '[:alnum:]_\\n' '_'\n}\n\n# aget ARRAY KEY\n# Get the value of KEY from associative array ARRAY.\n# Characters that are not valid in an identifier are replaced with underscores.\naget() {\n  eval echo '$'\"$(identifier \"$1\")_$(identifier \"$2\")\"\n}\n\n# src returns the project source repository name for a project.\nsrc() {\n  _project=\"$1\"\n\n  case \"${_project}\" in\n    plugins/*)\n      if [ -z \"$(aget SOURCES \"${_project}\")\" ]; then\n        echo plugins-private\n      else\n        aget SOURCES \"${_project}\"\n      fi\n      ;;\n    *)\n      if [ -z \"$(aget SOURCES \"${_project}\")\" ]; then\n        echo \"${_project}\"\n      else\n        aget SOURCES \"${_project}\"\n      fi\n      ;;\n  esac\n\n  unset _project\n}\n\n# path returns the relative path within the repository that contain the docs for a project.\npath() {\n  _project=\"$1\"\n\n  case \"${_project}\" in\n    plugins/*)\n      if [ -z \"$(aget PATHS \"${_project}\")\" ]; then\n        echo \"${_project}/docs/sources\"\n      else\n        aget PATHS \"${_project}\"\n      fi\n      ;;\n    *)\n      if [ -z \"$(aget PATHS \"${_project}\")\" ]; then\n        echo \"docs/sources\"\n      else\n        aget PATHS \"${_project}\"\n      fi\n  esac\n\n  unset _project\n}\n\n# version returns the version for a project. Unversioned projects return the special value 'UNVERSIONED'.\nversion() {\n  _project=\"$1\"\n\n  case \"${_project}\" in\n    plugins/*)\n      if [ -z \"$(aget VERSIONS \"${_project}\")\" ]; then\n        echo \"UNVERSIONED\"\n      else\n        aget VERSIONS \"${_project}\"\n      fi\n      ;;\n    *)\n    if [ -z \"$(aget VERSIONS \"${_project}\")\" ]; then\n      echo latest\n    else\n      aget VERSIONS \"${_project}\"\n    fi\n  esac\n\n  unset _project\n}\n\n\n# new_proj populates a new project structure.\nnew_proj() {\n  _project=\"$1\"\n  _version=\"$2\"\n  _repo=\"$3\"\n  _path=\"$4\"\n\n  # If version is not set, use the script mapping of project to default versions if it exists.\n  # Fallback to 'latest'.\n  if [ -z \"${_version}\" ]; then\n    _version=\"$(version \"${_project}\")\"\n  fi\n\n  # If repo is not set, use the script mapping of project to repo name if it exists.\n  # Fallback to using the project name.\n  if [ -z \"${_repo}\" ]; then\n    _repo=\"$(src \"${_project}\")\"\n  fi\n\n  # If path is not set, use the script mapping of project to docs sources path if it exists.\n  # Fallback to using 'docs/sources'.\n  if [ -z \"${_path}\" ]; then\n    _path=\"$(path \"${_project}\")\"\n  fi\n\n  echo \"${_project}:${_version}:${_repo}:${_path}\"\n  unset _project _version _repo _path\n}\n\n# proj_url returns the webserver URL for a project.\n# It expects a complete project structure as input.\nproj_url() {\n  IFS=: read -r _project _version _ _ <<POSIX_HERESTRING\n$1\nPOSIX_HERESTRING\n\n  if [ \"${_project}\" = website ]; then\n    echo \"http://localhost:${DOCS_HOST_PORT}/docs/\"\n\n    unset _project _version\n    return\n  fi\n\n  if [ -z \"${_version}\" ] || [ \"${_version}\" = 'UNVERSIONED' ]; then\n    echo \"http://localhost:${DOCS_HOST_PORT}/docs/${_project}/\"\n  else\n    echo \"http://localhost:${DOCS_HOST_PORT}/docs/${_project}/${_version}/\"\n  fi\n\n  unset _project _version\n}\n\n# proj_ver returns the version for a project.\n# It expects a complete project structure as input.\nproj_ver() {\n  IFS=: read -r _ _ver _ _ <<POSIX_HERESTRING\n$1\nPOSIX_HERESTRING\n\n  echo \"${_ver}\"\n  unset _ver\n}\n\n# proj_dst returns the container path to content source for a project.\n# It expects a complete project structure as input.\nproj_dst() {\n  IFS=: read -r _project _version _ _ <<POSIX_HERESTRING\n$1\nPOSIX_HERESTRING\n\n  if [ \"${_project}\" = website ]; then\n    echo '/hugo/content'\n\n    unset _project _version\n    return\n  fi\n\n  if [ -z \"${_version}\" ] || [ \"${_version}\" = 'UNVERSIONED' ]; then\n    echo \"/hugo/content/docs/${_project}\"\n  else\n    echo \"/hugo/content/docs/${_project}/${_version}\"\n  fi\n\n  unset _project _version\n}\n\n# repo_path returns the host path to the project repository.\n# It looks for the provided repository name in each of the paths specified in the REPOS_PATH environment variable.\nrepo_path() {\n  _repo=\"$1\"\n  IFS=:\n  for lookup in ${REPOS_PATH}; do\n    if [ -d \"${lookup}/${_repo}\" ]; then\n      echo \"${lookup}/${_repo}\"\n      unset _path _repo\n      return\n    fi\n  done\n  unset IFS\n\n  errr \"could not find project '${_repo}' in any of the paths in REPOS_PATH '${REPOS_PATH}'.\"\n  note \"you must have a checkout of the project '${_repo}' at '${REPOS_PATH##:*}/${_repo}'.\"\n  note \"if you have cloned the repository into a directory with a different name, consider changing it to ${_repo}.\"\n\n  unset _repo\n  exit 1\n}\n\n# proj_src returns the host path to content source for a project.\n# It expects a complete project structure as input.\n# It looks for the provided repository name in each of the paths specified in the REPOS_PATH environment variable.\nproj_src() {\n  IFS=: read -r _ _ _repo _path <<POSIX_HERESTRING\n$1\nPOSIX_HERESTRING\n\n  _repo=\"$(repo_path \"${_repo}\")\"\n  echo \"${_repo}/${_path}\"\n\n  unset _path _repo\n}\n\n# proj_canonical returns the canonical absolute path partial URI for a project.\n# It expects a complete project structure as input.\nproj_canonical() {\n  IFS=: read -r _project _version _ _ <<POSIX_HERESTRING\n$1\nPOSIX_HERESTRING\n\n  if [ \"${_project}\" = website ]; then\n    echo '/docs'\n\n    unset _project _version\n    return\n  fi\n\n  if [ -z \"${_version}\" ] || [ \"${_version}\" = 'UNVERSIONED' ]; then\n    echo \"/docs/${_project}\"\n  else\n    echo \"/docs/${_project}/${_version}\"\n  fi\n\n  unset _project _version\n}\n\nproj_to_url_src_dst_ver() {\n  _url=\"$(proj_url \"$1\")\"\n  _src=\"$(proj_src \"$1\")\"\n  _dst=\"$(proj_dst \"$1\")\"\n  _ver=\"$(proj_ver \"$1\")\"\n\n  echo \"${_url}^${_src}^${_dst}^${_ver}\"\n  unset _url _src _dst _ver\n}\n\nurl_src_dst_vers() {\n  for arg in \"$@\"; do\n    IFS=: read -r _project _version _repo _path <<POSIX_HERESTRING\n$arg\nPOSIX_HERESTRING\n\n    case \"${_project}\" in\n      # Workaround for arbitrary mounts where the version field is expected to be the local directory\n     # and the repo field is expected to be the container directory.\n      arbitrary)\n        echo \"${_project}^${_version}^${_repo}^\" # TODO\n        ;;\n      logs)\n        proj_to_url_src_dst_ver \"$(new_proj loki \"${_version}\")\"\n        proj_to_url_src_dst_ver \"$(new_proj enterprise-logs \"${_version}\")\"\n        ;;\n      metrics)\n        proj_to_url_src_dst_ver \"$(new_proj mimir \"${_version}\")\"\n        proj_to_url_src_dst_ver \"$(new_proj helm-charts/mimir-distributed \"${_version}\")\"\n        proj_to_url_src_dst_ver \"$(new_proj enterprise-metrics \"${_version}\")\"\n        ;;\n      resources)\n        _repo=\"$(repo_path website)\"\n        echo \"arbitrary^${_repo}/config^/hugo/config\" \"arbitrary^${_repo}/layouts^/hugo/layouts\" \"arbitrary^${_repo}/scripts^/hugo/scripts\"\n        unset _repo\n        ;;\n      traces)\n        proj_to_url_src_dst_ver \"$(new_proj tempo \"${_version}\")\"\n        proj_to_url_src_dst_ver \"$(new_proj enterprise-traces \"${_version}\")\"\n        ;;\n      *)\n        proj_to_url_src_dst_ver \"$(new_proj \"${_project}\" \"${_version}\" \"${_repo}\" \"${_path}\")\"\n        ;;\n    esac\n  done\n\n  unset _project _version _repo _path\n}\n\nawait_build() {\n  url=\"$1\"\n  req=\"$(if command -v curl >/dev/null 2>&1; then echo 'curl -s -o /dev/null'; else echo 'wget -q'; fi)\"\n\n  # Initial delay to allow container to start before beginning healthchecks\n  sleep 3\n\n  # Fast retries for initial startup (10 attempts, 1 second apart)\n  i=1\n  max=10\n  while [ \"${i}\" -ne \"${max}\" ]\n  do\n    sleep 1\n    debg \"Retrying request to web server assuming the process is still starting up.\"\n    i=$((i + 1))\n\n    if ${req} \"${url}\"; then\n      printf '\\r\\nView documentation locally:\\r\\n'\n      for x in ${url_src_dst_vers}; do\n        IFS='^' read -r url _ _ <<POSIX_HERESTRING\n$x\nPOSIX_HERESTRING\n\n        if [ -n \"${url}\" ]; then\n          if [ \"${url}\" != arbitrary ]; then\n            printf '\\r  %s\\r\\n' \"${url}\"\n          fi\n        fi\n      done\n      printf '\\r\\nPress Ctrl+C to stop the server\\r\\n'\n\n      unset i max req url\n      return\n    fi\n  done\n\n  # Continue checking with longer intervals for slower builds\n  # This prevents false positives for large builds that take longer to start\n  i=1\n  max=20\n  while [ \"${i}\" -ne \"${max}\" ]\n  do\n    sleep 2\n    debg \"Continuing to check web server (build may be taking longer than expected).\"\n    i=$((i + 1))\n\n    if ${req} \"${url}\"; then\n      printf '\\r\\nView documentation locally:\\r\\n'\n      for x in ${url_src_dst_vers}; do\n        IFS='^' read -r url _ _ <<POSIX_HERESTRING\n$x\nPOSIX_HERESTRING\n\n        if [ -n \"${url}\" ]; then\n          if [ \"${url}\" != arbitrary ]; then\n            printf '\\r  %s\\r\\n' \"${url}\"\n          fi\n        fi\n      done\n      printf '\\r\\nPress Ctrl+C to stop the server\\r\\n'\n\n      unset i max req url\n      return\n    fi\n  done\n\n  # Only log error after extended checking period (total ~60 seconds)\n  printf '\\r\\n'\n  errr 'The build was interrupted or a build error occurred, check the previous logs for possible causes.'\n  note 'You might need to use Ctrl+C to end the process.'\n\n  unset i max req url\n}\n\ndebg() {\n  if [ -n \"${DEBUG}\" ]; then\n    printf 'DEBG: %s\\r\\n' \"$1\" >&2\n  fi\n}\n\nerrr() {\n  printf 'ERRR: %s\\r\\n' \"$1\" >&2\n}\n\nnote() {\n  printf 'NOTE: %s\\r\\n' \"$1\" >&2\n}\n\nurl_src_dst_vers=\"$(url_src_dst_vers \"$@\")\"\n\nvolumes=\"\"\nredirects=\"\"\n\nfor arg in \"$@\"; do\n  IFS=: read -r _project _ _repo _ <<POSIX_HERESTRING\n${arg}\nPOSIX_HERESTRING\n  if [ \"${_project}\" = website ] || [ \"${_project}\" = grafana-cloud ]; then\n    note \"Please be patient, building the website can take some time.\"\n\n      # If set, the docs-base image will run a prebuild script that sets up Hugo mounts.\n    if [ \"${WEBSITE_MOUNTS}\" = false ]; then\n      unset WEBSITE_MOUNTS\n    else\n      readonly WEBSITE_MOUNTS=true\n    fi\n\n    _repo=\"$(repo_path website)\"\n    volumes=\"--volume=${_repo}/config:/hugo/config:z\"\n    volumes=\"${volumes} --volume=${_repo}/content/guides:/hugo/content/guides:z\"\n    volumes=\"${volumes} --volume=${_repo}/content/whats-new:/hugo/content/whats-new:z\"\n    volumes=\"${volumes} --volume=${_repo}/Makefile:/hugo/Makefile:z\"\n    volumes=\"${volumes} --volume=${_repo}/layouts:/hugo/layouts:z\"\n    volumes=\"${volumes} --volume=${_repo}/scripts:/hugo/scripts:z\"\n  fi\n  unset _project _repo\ndone\n\nfor x in ${url_src_dst_vers}; do\n  IFS='^' read -r _url _src _dst _ver <<POSIX_HERESTRING\n$x\nPOSIX_HERESTRING\n\n  if [ \"${_url}\" != arbitrary ]; then\n    if [ ! -f \"${_src}/_index.md\" ]; then\n      errr \"Index file '${_src}/_index.md' does not exist.\"\n      note \"Is '${_src}' the correct source directory?\"\n      exit 1\n    fi\n  fi\n\n  debg \"Mounting '${_src}' at container path '${_dst}'\"\n\n  if [ -z \"${volumes}\" ]; then\n    volumes=\"--volume=${_src}:${_dst}:z\"\n  else\n    volumes=\"${volumes} --volume=${_src}:${_dst}:z\"\n  fi\n\n  if [ -n \"${_ver}\" ] && [ \"${_ver}\" != 'UNVERSIONED' ]; then\n    if [ -z \"${redirects}\" ]; then\n      redirects=\"${_dst}^${_ver}\"\n    else\n      redirects=\"${redirects} ${_dst}^${_ver}\"\n    fi\n  fi\n  unset _url _src _dst _ver\ndone\n\nIFS=':' read -r image _ <<POSIX_HERESTRING\n${DOCS_IMAGE}\nPOSIX_HERESTRING\n\ncase \"${image}\" in\n  'grafana/vale')\n    proj=\"$(new_proj \"$1\")\"\n    printf '\\r\\n'\n    IFS='' read -r cmd <<EOF\n    ${PODMAN} run \\\n                --init \\\n                --rm \\\n                --workdir /etc/vale \\\n                --tty \\\n                ${volumes} \\\n                ${DOCS_IMAGE} \\\n                --minAlertLevel=${VALE_MINALERTLEVEL} \\\n                --glob=*.md \\\n                /hugo/content/docs\nEOF\n\n    if [ -n \"${DEBUG}\" ]; then\n      debg \"${cmd}\"\n    fi\n\n    case \"${OUTPUT_FORMAT}\" in\n      human)\n        ${cmd} --output=line \\\n        | sed \"s#$(proj_dst \"${proj}\")#sources#\"\n      ;;\n      json)\n        ${cmd} --output=/etc/vale/rdjsonl.tmpl \\\n        | sed \"s#$(proj_dst \"${proj}\")#sources#\"\n      ;;\n      *)\n        errr \"Invalid output format '${OUTPUT_FORMAT}'\"\n    esac\n\n    ;;\n  *)\n    tempfile=\"$(mktemp -t make-docs.XXX)\"\n    cat <<EOF >\"${tempfile}\"\n#!/usr/bin/env bash\n\ntc() {\n  set \\${*,,}\n  echo \\${*^}\n}\n\nfor redirect in ${redirects}; do\n  IFS='^' read -r path ver <<<\"\\${redirect}\"\n  echo -e \"---\\\\nredirectURL: \\\"\\${path/\\/hugo\\/content/}\\\"\\\\ntype: redirect\\\\nversioned: true\\\\n---\\\\n\" > \"\\${path/\\${ver}/_index.md}\"\ndone\n\nfor x in \"${url_src_dst_vers}\"; do\n  IFS='^' read -r _ _ dst _ <<<\"\\${x}\"\n\n  title=\"\\${dst%/*}\"\n  title=\"\\$(tc \\${title##*/})\"\n  while [[ -n \"\\${dst}\" ]]; do\n    if [[ ! -f \"\\${dst}/_index.md\" ]]; then\n        echo -e \"---title: \\${title}\\\\n---\\\\n\\\\n# \\${title}\\\\n\\\\n{{< section >}}\" > \"\\${dst}/_index.md\"\n    fi\n    dst=\"\\${dst%/*}\"\n  done\ndone\n\nif [[ -n \"${WEBSITE_MOUNTS}\" ]]; then\n  unset WEBSITE_SKIP_MOUNTS\nfi\n\n${WEBSITE_EXEC}\nEOF\n    chmod +x \"${tempfile}\"\n    volumes=\"${volumes} --volume=${tempfile}:/entrypoint:z\"\n    readonly volumes\n\n    IFS='' read -r cmd <<EOF\n${PODMAN} run \\\n  --env=HUGO_REFLINKSERRORLEVEL=${HUGO_REFLINKSERRORLEVEL} \\\n  --init \\\n  --interactive \\\n  --publish=${DOCS_HOST_PORT}:3002 \\\n  --publish=3003:3003 \\\n  --rm \\\n  --tty \\\n  ${volumes} \\\n  ${DOCS_IMAGE}\nEOF\n\n    if [ -n \"${ENTER}\" ]; then\n      ${cmd} /bin/bash\n    elif [ -n \"${DEBUG}\" ]; then\n      await_build http://localhost:3003 &\n\n      debg \"${cmd} /entrypoint\"\n      ${cmd} /entrypoint\n    else\n      await_build http://localhost:3003 &\n\n      ${cmd} /entrypoint  2>&1\\\n        | sed -u \\\n              -e '/Web Server is available at http:\\/\\/localhost:3003\\/ (bind address 0.0.0.0)/ d' \\\n              -e '/^hugo server/ d' \\\n              -e '/fatal: not a git repository (or any parent up to mount point \\/)/ d' \\\n              -e '/Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set)./ d' \\\n              -e \"/Makefile:[0-9]*: warning: overriding recipe for target 'docs'/ d\" \\\n              -e \"/docs.mk:[0-9]*: warning: ignoring old recipe for target 'docs'/ d\" \\\n              -e '/\\/usr\\/bin\\/make -j 2 proxy hserver-docs HUGO_PORT=3003/ d' \\\n              -e '/website-proxy/ d' \\\n              -e '/rm -rf dist*/ d' \\\n              -e '/Press Ctrl+C to stop/ d' \\\n              -e '/make/ d' \\\n              -e '/WARNING: The manual_mount source directory/ d' \\\n              -e '/\"docs\\/_index.md\" not found/d'\n    fi\n    ;;\nesac\n"
  },
  {
    "path": "docs/sources/_index.md",
    "content": "---\ndescription: This document introduces the ClickHouse data source\nlabels:\nproducts:\n  - Grafana Cloud\n  - Grafana OSS\n  - Grafana Enterprise\nkeywords:\n  - data source\nmenuTitle: ClickHouse data source\ntitle: ClickHouse data source\nweight: 10\nversion: 0.1\nlast_reviewed: 2026-04-27\n---\n\n# ClickHouse data source\n\nThe ClickHouse data source plugin allows you to query and visualize ClickHouse data in Grafana and create alerts based on your ClickHouse queries. Use the SQL editor or the visual query builder to build dashboards with time series, tables, logs, and traces.\n\n## Supported features\n\n| Feature | Supported |\n|---------|-----------|\n| Metrics | Yes |\n| Logs | Yes |\n| Traces | Yes |\n| Alerting and recording rules | Yes |\n| Annotations | Yes |\n| Template variables | Yes |\n| Ad hoc filters | Yes (ClickHouse 22.7+) |\n| Private Data Connect (PDC) | Yes (Grafana Cloud) |\n\n## Requirements\n\n| Grafana version | Plugin version |\n|-----------------|----------------|\n| 11.6.0 and later | v4.15+ |\n| 9.x – 11.5.x | v4.0 – v4.14 |\n\n{{< admonition type=\"note\" >}}\n- Ad hoc filters require ClickHouse 22.7 or later (from plugin v2.0 onward).\n- Log volume queries in the SQL editor require Grafana 12.4.0 or later.\n{{< /admonition >}}\n\n## Get started\n\nStart by configuring a connection to your ClickHouse server, then use the query editor to build queries for dashboards and alerts.\n\n1. [Configure the ClickHouse data source](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/configure/) — Set up the connection, authentication, TLS, and optional features like logs and traces.\n2. [ClickHouse query editor](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/query-editor/) — Write SQL queries or use the visual query builder. Includes macros, query types, and examples.\n3. [ClickHouse template variables](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/template-variables/) — Create dynamic dashboards with variables and ad hoc filters.\n4. [ClickHouse annotations](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/annotations/) — Overlay event markers on dashboard panels from ClickHouse data.\n5. [ClickHouse alerting](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/alerting/) — Create alert rules and recording rules from ClickHouse queries.\n6. [Troubleshoot ClickHouse data source issues](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/troubleshooting/) — Solutions for common connection, query, and configuration errors.\n\n## Additional features\n\nAfter configuring the data source, you can:\n\n- Use [Explore](https://grafana.com/docs/grafana/latest/explore/) to query data without building a dashboard\n- Add [Transformations](https://grafana.com/docs/grafana/latest/panels/transformations/) to manipulate query results\n- Set up [Alerting](https://grafana.com/docs/grafana/latest/alerting/) rules\n\n## Pre-built dashboards\n\nThe plugin includes the following pre-built dashboards:\n\n- **ClickHouse - Query Analysis** — Query performance, time distribution, top users, and memory usage.\n- **ClickHouse - Data Analysis** — Disk usage, table and database summary, parts over time, and dictionaries.\n- **ClickHouse - Cluster Analysis** — Cluster overview, merges, mutations, and replicated table delay.\n- **Simple ClickHouse OTel Dashboard** — Traces, logs, and service performance for OpenTelemetry data in ClickHouse.\n- **Advanced ClickHouse Monitoring Dashboard** — System metrics (CPU, queries/sec, IO, memory) similar to ClickHouse built-in monitoring.\n\nTo import a pre-built dashboard:\n\n1. Go to **Connections** > **Data sources**.\n2. Select your ClickHouse data source.\n3. Click the **Dashboards** tab.\n4. Click **Import** next to the dashboard you want to use.\n\n## Plugin updates\n\nAlways ensure that your plugin version is up-to-date so you have access to all current features and improvements. Navigate to **Plugins and data** > **Plugins** to check for updates. Grafana recommends upgrading to the latest Grafana version, and this applies to plugins as well.\n\n{{< admonition type=\"note\" >}}\nPlugins are automatically updated in Grafana Cloud.\n{{< /admonition >}}\n\n## Related resources\n\n- [ClickHouse documentation](https://clickhouse.com/docs)\n- [grafana-clickhouse-datasource on GitHub](https://github.com/grafana/clickhouse-datasource) — Source code, issues, and changelog.\n- [Grafana community forum](https://community.grafana.com/)\n"
  },
  {
    "path": "docs/sources/alerting.md",
    "content": "---\ndescription: Create alert rules from ClickHouse queries\nlabels:\nproducts:\n  - Grafana Cloud\n  - Grafana OSS\n  - Grafana Enterprise\nkeywords:\n  - data source\n  - alerting\nmenuTitle: Alerting\ntitle: ClickHouse alerting\nweight: 60\nversion: 0.1\nlast_reviewed: 2026-04-27\n---\n\n# ClickHouse alerting\n\nThe ClickHouse data source supports [Grafana Alerting](https://grafana.com/docs/grafana/latest/alerting/) and [Grafana-managed recording rules](https://grafana.com/docs/grafana/latest/alerting/configure-alert-rules/create-recording-rules/). You can create alert rules that run ClickHouse SQL queries and fire when the result meets a condition (for example, a value is above a threshold or no data is returned).\n\nAlert rules run as background processes. Grafana executes your ClickHouse query on a schedule, then evaluates the result using expressions such as **Reduce** and **Threshold**. Your query must return numeric data that Grafana can evaluate.\n\nFor an overview of alerting in Grafana, see [Alert rules](https://grafana.com/docs/grafana/latest/alerting/configure-alert-rules/) and [Create a Grafana-managed alert rule](https://grafana.com/docs/grafana/latest/alerting/configure-alert-rules/create-grafana-managed-rule/).\n\n## Before you begin\n\n- [Configure the ClickHouse data source](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/configure/).\n- Ensure your ClickHouse user has read access to the tables used in your alert query.\n- Familiarize yourself with [Grafana Alerting concepts](https://grafana.com/docs/grafana/latest/alerting/).\n\n## Query requirements for alerting\n\nAlert rules need numeric values to evaluate. Your ClickHouse query should return data that Grafana can use in a **Reduce** expression and then compare in a **Threshold** expression.\n\n| Query result | Use case |\n|--------------|----------|\n| **Time series** (time column + numeric column) | Threshold alerts on a metric (e.g. average CPU, error count per interval). Use **Reduce** (Last, Max, Mean, etc.) to get a single value from the series. |\n| **Single row, numeric value** | Threshold alerts on an aggregate (e.g. `SELECT count() FROM errors WHERE ...`). Use **Reduce** > **Last** to use the value. |\n\nQueries that return only text or non-numeric data cannot be used directly for threshold evaluation. Use `count()`, `avg()`, `sum()`, or similar in your SQL so the result is numeric.\n\nUse the **$__timeFilter(column)** macro in your WHERE clause so the query respects the alert rule’s evaluation interval and time range. See the [ClickHouse query editor](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/query-editor/) Macros section.\n\n## Create an alert rule\n\nYou can write alert queries using either the **SQL editor** or the **query builder**. Both are fully supported in alert rules.\n\nTo create an alert rule using ClickHouse data:\n\n1. Go to **Alerting** > **Alert rules**.\n2. Click **New alert rule**.\n3. Enter a **Name** for the rule.\n4. In **Define query and alert condition**:\n   - Select your **ClickHouse** data source.\n   - Write a query that returns a time column and a numeric column (or a single numeric value). Use **Format** > **Time series** if your query returns time + value; use **Table** if it returns a single row.\n   - Add a **Reduce** expression to aggregate the query result (e.g. **Last** to use the latest value, **Max** for the highest, **Mean** for the average).\n   - Add a **Threshold** expression to define when the alert fires (e.g. **Is above** 80, **Is below** 3).\n5. Configure **Set evaluation behavior**: choose a folder and evaluation group, set the evaluation interval, and set the pending period.\n6. Add **Labels** and **Annotations** for notifications.\n7. Click **Save rule**.\n\nIf your alert rule contains multiple queries (multiple refIds), you can hide a query by clicking the eye icon next to it. Hidden queries are excluded from evaluation.\n\nFor detailed steps, see [Create a Grafana-managed alert rule](https://grafana.com/docs/grafana/latest/alerting/configure-alert-rules/create-grafana-managed-rule/).\n\n## Example: Metric threshold alert\n\nThis example fires when the average value of a metric exceeds 80. Replace the table and column names with your own.\n\n**Query (Time series format):**\n\n```sql\nSELECT\n  $__timeInterval(timestamp) AS time,\n  avg(value) AS value\nFROM my_app.metrics\nWHERE $__timeFilter(timestamp)\n  AND metric_name = 'cpu_usage'\nGROUP BY time\nORDER BY time\n```\n\nIn the alert rule, add **Reduce** > **Last** (or **Max**) and **Threshold** > **Is above** 80.\n\n## Example: Error count alert\n\nThis example fires when the number of error rows in the last 5 minutes exceeds 10.\n\n**Query (Table format; single row):**\n\n```sql\nSELECT count() AS value\nFROM my_app.events\nWHERE $__timeFilter(timestamp)\n  AND level = 'error'\n```\n\nIn the alert rule, set the query **Format** to **Table**, add **Reduce** > **Last**, and **Threshold** > **Is above** 10.\n\n## Example: No data alert\n\nYou can alert when a query returns no rows (for example, a health check that should always return at least one row). Write a query that returns a row when things are healthy, then in the alert rule configure **Configure no data and error handling** to **Alerting** when there is no data.\n\n## Limitations\n\n### Ad hoc filters\n\n[Ad hoc filters](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/template-variables/#ad-hoc-filters) are a dashboard-scoped feature. When Grafana evaluates an alert rule, queries run through the backend and ad hoc filters are **not** applied. If you reuse a dashboard query that relies on ad hoc filters, the alert query will run without those filters and may return unexpected results.\n\nTo apply equivalent filtering in an alert rule, add the filter conditions directly in your SQL `WHERE` clause.\n\n### Row limit\n\nIf **Enable row limit** is turned on in the data source [configuration](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/configure/), ClickHouse applies a `limit` setting to every query, including alert queries. This can silently truncate results, causing alerts to evaluate against incomplete data. Disable the row limit or ensure it is set high enough to capture the full result set for your alert queries.\n\n## Best practices\n\n1. **Use an appropriate evaluation interval** — Set the alert evaluation interval to match how often your data is written. Avoid intervals shorter than your data resolution to prevent flapping or missed data.\n2. **Reduce multiple series** — If your query returns multiple time series (e.g. one per host), use **Reduce** to aggregate: **Last**, **Max**, **Mean**, or **Sum** so Grafana can evaluate a single value.\n3. **Restrict the time range** — Use **$__timeFilter(column)** in your WHERE clause so the query only reads data in the evaluation window. Avoid full table scans.\n4. **Handle no data** — In **Configure no data and error handling**, choose whether no data should keep the alert as-is, fire the alert, or resolve it. Use **Alerting** when no data indicates a problem (e.g. a heartbeat query).\n5. **Test the query first** — Run the query in **Explore** with the ClickHouse data source and confirm it returns the expected numeric data before saving the alert rule.\n\n## Recording rules\n\nThe ClickHouse data source supports [Grafana-managed recording rules](https://grafana.com/docs/grafana/latest/alerting/configure-alert-rules/create-recording-rules/), which evaluate a ClickHouse query on a schedule and write the result as a Prometheus metric. This is useful for pre-aggregating expensive queries so dashboards load faster.\n\nThe same query requirements and best practices that apply to alert rules also apply to recording rules. Pay particular attention to **data ingestion latency** — see the section below.\n\n### Account for data ingestion latency\n\nClickHouse data may not be available at the exact moment a rule evaluates. This is especially common when ClickHouse is fed by an asynchronous pipeline (for example, Kafka, Segment, or an OTel collector) where data arrives with a delay of seconds to minutes.\n\nIf a recording rule or alert rule returns missing data points or incomplete results:\n\n1. **Widen the relative time range.** Instead of querying only the last evaluation interval (for example, the last 1 minute), shift the query window back to account for ingestion lag. For example, query the last 5 minutes even if the rule evaluates every 1 minute.\n2. **Avoid filtering on `now()` tightly.** A `WHERE timestamp > now() - INTERVAL 1 MINUTE` filter will miss data that hasn't been flushed to ClickHouse yet. Use a wider window like `now() - INTERVAL 5 MINUTE` and rely on **Reduce** > **Last** to pick up the most recent value.\n3. **Test the query lag in Explore.** Run the query manually with different time offsets to determine how much delay your pipeline typically introduces, then build that buffer into the rule's time range.\n\n## Query metadata in ClickHouse\n\nWhen an alert rule evaluates, the ClickHouse plugin injects metadata into the ClickHouse [`ClientInfo` comment](https://clickhouse.com/docs/en/operations/system-tables/query_log), including:\n\n- `grafana_rule:<rule-UID>` — the Grafana alert rule UID\n- `grafana_user:<login>` — the Grafana user (when available)\n\nThis metadata appears in the ClickHouse `system.query_log` table and helps database administrators identify which alert rules are generating specific queries. Use it to debug slow or resource-heavy alert evaluations on the ClickHouse side.\n\n## Troubleshooting\n\nIf alerts or recording rules do not fire or evaluate as expected:\n\n- **Query returns no numeric data** — Confirm the query returns a time column and a numeric column (or a single numeric value). Test in **Explore** and check the result format.\n- **Missing data points** — Check for data ingestion latency. See [Account for data ingestion latency](#account-for-data-ingestion-latency) above.\n- **Evaluation interval** — Ensure the evaluation interval is long enough for data to be available. Avoid intervals shorter than your data resolution.\n- **No data handling** — In **Configure no data and error handling**, choose whether no data should fire the alert, resolve it, or keep the current state.\n- **Alerting times out via PDC** — If dashboards work but alert rules time out when using Private Data Connect, see [Alerting Times Out via PDC While Dashboards Work](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/troubleshooting/#alerting-times-out-via-pdc-while-dashboards-work) in the troubleshooting guide.\n\nFor connection errors, timeouts, or other data source issues, see [Troubleshoot ClickHouse data source issues](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/troubleshooting/).\n\n## Next steps\n\n- [ClickHouse query editor](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/query-editor/) — Macros such as `$__timeFilter` and `$__timeInterval`.\n- [Grafana Alerting](https://grafana.com/docs/grafana/latest/alerting/) — Alert rules, contact points, and notification policies.\n- [Troubleshoot ClickHouse data source issues](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/troubleshooting/) — Common errors and solutions.\n"
  },
  {
    "path": "docs/sources/annotations.md",
    "content": "---\ndescription: Use ClickHouse queries to create annotations on dashboards\nlabels:\nproducts:\n  - Grafana Cloud\n  - Grafana OSS\n  - Grafana Enterprise\nkeywords:\n  - data source\n  - annotations\nmenuTitle: Annotations\ntitle: ClickHouse annotations\nweight: 50\nversion: 0.1\nlast_reviewed: 2026-04-27\n---\n\n# ClickHouse annotations\n\nAnnotations overlay event markers on your dashboard panels. You can use ClickHouse SQL queries to create annotations that mark deployments, alerts, errors, or other events from your data.\n\nThe plugin uses Grafana’s default annotation support: you write a ClickHouse SQL query that returns a time column and a text column. Grafana positions each row as an annotation on the time axis and shows the text when you hover or click.\n\nAnnotation queries use Grafana’s built-in annotation query editor (a SQL text field with column mappings), not the full ClickHouse query builder available in panels.\n\nFor an overview of annotations in Grafana, see [Annotate visualizations](https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/annotate-visualizations/).\n\n## Before you begin\n\n- [Configure the ClickHouse data source](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/configure/).\n- Ensure your ClickHouse user has read access to the tables you use in your annotation query.\n\n## Create an annotation query\n\nTo add a ClickHouse annotation to a dashboard:\n\n1. Open the dashboard where you want to add annotations.\n2. Click **Dashboard settings** (gear icon) in the top navigation.\n3. Select **Annotations** in the left menu.\n4. Click **Add annotation query**.\n5. Enter a **Name** for the annotation (for example, \"Deployments\", \"Errors\").\n6. In the **Data source** drop-down, select your ClickHouse data source.\n7. In the **Query** field, enter a ClickHouse SQL query that returns the required columns (see [Query requirements](#query-requirements)).\n8. Use the **Column mappings** section to map your query columns to **Time**, **Text**, and optionally **Tags** (if your column names differ from Grafana’s defaults).\n9. Click **Apply** to save.\n\n## Query requirements\n\nYour SQL query must return at least a time column and a text column. Grafana uses these to place and label each annotation.\n\n| Column | Required | Description |\n|--------|----------|-------------|\n| **Time** | Yes | The timestamp for the annotation. Grafana uses this to position the marker on the time axis. Use a DateTime or DateTime64 column, or an expression that Grafana can interpret as time. |\n| **TimeEnd** | Optional | A second timestamp column. When present, Grafana draws a **region annotation** (a shaded range) from Time to TimeEnd instead of a single vertical line. |\n| **Text** | Yes | The annotation text shown when you hover over or click the marker. |\n| **Tags** | Optional | Additional columns become annotation tags. Use them to filter or group annotations. |\n\nAlways restrict the query to the dashboard time range so annotations load quickly. Use the **$__timeFilter(column)** macro in your WHERE clause. If your time column is `DateTime64` and you need sub-second precision, use **$__timeFilter_ms(column)** instead. See the [ClickHouse query editor](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/query-editor/) Macros section for the full list of available macros.\n\n## Annotation query examples\n\nThe following examples show common patterns. Replace the table and column names with your own.\n\n**Application events (e.g. deployments or status changes):**\n\n```sql\nSELECT\n  event_time AS time,\n  message AS text,\n  environment AS tag\nFROM my_app.events\nWHERE $__timeFilter(event_time)\n  AND event_type IN ('deployment', 'status_change')\nORDER BY event_time DESC\nLIMIT 100\n```\n\n**Query log events (e.g. long-running or failed queries from ClickHouse system tables):**\n\n```sql\nSELECT\n  event_time AS time,\n  concat(type, ': ', substring(query, 1, 80)) AS text,\n  initial_user AS tag\nFROM system.query_log\nWHERE $__timeFilter(event_time)\n  AND type IN ('QueryFinish', 'ExceptionWhileProcessing')\nORDER BY event_time DESC\nLIMIT 100\n```\n\n**Errors or alerts from a custom table:**\n\n```sql\nSELECT\n  timestamp AS time,\n  concat(severity, ' - ', message) AS text,\n  service AS tag\nFROM my_app.alerts\nWHERE $__timeFilter(timestamp)\nORDER BY timestamp DESC\nLIMIT 100\n```\n\n**Multiple tags (filter annotations by environment, service, or region):**\n\n```sql\nSELECT\n  timestamp AS time,\n  message AS text,\n  environment AS tag1,\n  service AS tag2,\n  region AS tag3\nFROM my_app.incidents\nWHERE $__timeFilter(timestamp)\nORDER BY timestamp DESC\nLIMIT 100\n```\n\nMap each tag column in the **Column mappings** section. In the dashboard, users can filter visible annotations by any combination of these tags.\n\n**Region annotation (maintenance windows or time ranges):**\n\n```sql\nSELECT\n  start_time AS time,\n  end_time AS timeEnd,\n  concat(window_type, ': ', description) AS text,\n  team AS tag\nFROM my_app.maintenance_windows\nWHERE $__timeFilter(start_time)\nORDER BY start_time DESC\nLIMIT 100\n```\n\nMap the `timeEnd` column in the **Column mappings** section. Grafana draws a shaded region between `time` and `timeEnd` instead of a single vertical line.\n\n## Ad hoc filter interaction\n\nIf the dashboard has [ad hoc filters](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/template-variables/#ad-hoc-filters) enabled for the ClickHouse data source, those filters are also applied to annotation queries. This means annotation results change as users adjust ad hoc filter values.\n\nIf this is not the desired behavior and you want the annotation to always show all events regardless of ad hoc filters, place the `$__adHocFilters` macro in a `SETTINGS` clause that targets a different table, or use a separate ClickHouse data source instance without ad hoc filters configured for your annotation queries.\n\n## Best practices\n\n1. **Use a time filter** — Include `$__timeFilter(your_time_column)` in the WHERE clause so the query only returns data in the dashboard time range.\n2. **Limit results** — Use `LIMIT` (for example, 100) so the query stays fast and the dashboard does not show too many markers.\n3. **Meaningful text** — Use `concat()` or similar so the text column is clear (e.g. event type plus a short description).\n4. **Use tags** — Return one or more tag columns (e.g. environment, service, user) so users can filter annotations in the dashboard.\n5. **Descriptive names** — Give the annotation a clear name (e.g. \"Production deployments\", \"Query errors\") so dashboard users know what it represents.\n\n## Next steps\n\n- [ClickHouse query editor](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/query-editor/) — Macros such as `$__timeFilter` and building queries.\n- [Annotate visualizations](https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/annotate-visualizations/) — Grafana annotation options (colors, which panels show annotations, filters).\n- [Troubleshoot ClickHouse data source issues](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/troubleshooting/) — Common errors and solutions.\n"
  },
  {
    "path": "docs/sources/configure.md",
    "content": "---\ndescription: Configure the ClickHouse data source for Grafana, including connection, TLS, logs, traces, and provisioning\nlabels:\nproducts:\n  - Grafana Cloud\n  - Grafana OSS\n  - Grafana Enterprise\nkeywords:\n  - data source\nmenuTitle: Configure\ntitle: Configure the ClickHouse data source\nweight: 20\nversion: 0.1\nlast_reviewed: 2026-04-24\n---\n\n# Configure the ClickHouse data source\n\nThis page explains how to configure the ClickHouse data source, including connection settings, TLS, logs and traces column mappings, and provisioning.\n\n## Before you begin\n\nBefore configuring the data source, ensure you have:\n\n- **Grafana permissions:** Organization administrator role.\n- **Plugin:** The ClickHouse data source plugin installed. For Grafana version compatibility, see [Requirements](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/#requirements).\n- **ClickHouse:** A running ClickHouse server and a user with read-only access (or the permissions described below).\n- **Network access:** The Grafana server can reach the ClickHouse server on the intended port (HTTP: 8123 or 8443 with TLS; Native: 9000 or 9440 with TLS).\n\n{{< admonition type=\"note\" >}}\n**Grafana Cloud users:** If your ClickHouse server is behind a firewall, you must allowlist the Grafana Cloud outbound IP addresses so that queries can reach your database. For the current list of IPs, refer to [Allow Grafana Cloud outbound traffic](https://grafana.com/docs/grafana-cloud/account-management/allow-traffic/).\n\nThe published list covers standard outbound IPs but may not include every address used by your specific Grafana Cloud stack. If connections are still blocked after allowlisting the documented IPs, check your firewall or ClickHouse server logs for the rejected source addresses and [open a support ticket](https://grafana.com/profile/org#support) so the Grafana team can confirm the full set of IPs for your stack.\n{{< /admonition >}}\n\n## ClickHouse user and permissions\n\nGrafana executes queries exactly as written and does not validate or restrict SQL. Use a **read-only ClickHouse user** for this data source to avoid accidental or destructive operations (such as modifying or deleting tables) while still allowing dashboards and queries to run.\n\nIf your ClickHouse administrator has already given you a read-only user and connection details, you can skip to [Add the data source](#add-the-data-source).\n\n### Recommended permissions\n\nCreate a ClickHouse user with:\n\n- **readonly** permission enabled\n- Access limited to the databases and tables you intend to query\n- Permission to modify the **max_execution_time** setting (required by the plugin’s client)\n\n{{< admonition type=\"warning\" >}}\nGrafana does not prevent execution of non-read queries. If the ClickHouse user has sufficient privileges, statements such as `DROP TABLE` or `ALTER TABLE` will be executed by ClickHouse.\n{{< /admonition >}}\n\n### Configure a read-only user\n\nTo configure a suitable read-only user:\n\n1. Create a user or profile using [Creating users and roles in ClickHouse](https://clickhouse.com/docs/en/operations/access-rights).\n1. Set `readonly = 1` for the user or profile. For details, see [Permissions for queries (readonly)](https://clickhouse.com/docs/en/operations/settings/permissions-for-queries#readonly).\n1. Allow modification of the **max_execution_time** setting, which is required by the [clickhouse-go](https://github.com/ClickHouse/clickhouse-go/) client so the plugin can enforce query timeouts.\n\n#### Required SETTINGS permissions\n\nThe plugin's underlying client ([clickhouse-go](https://github.com/ClickHouse/clickhouse-go/)) sets certain ClickHouse `SETTINGS` on each query. If the ClickHouse user does not have permission to modify these settings, queries will fail at runtime even though the **Save & test** check may pass.\n\nAt a minimum the user must be allowed to change the following settings:\n\n| Setting | Why the plugin needs it |\n|---------|------------------------|\n| **max_execution_time** | Enforces the query timeout configured in the data source. |\n\nWhen `readonly = 1` is set, ClickHouse blocks all setting changes by default. To allow the required settings without disabling read-only mode:\n\n1. Create a [settings profile or constraint](https://clickhouse.com/docs/en/operations/settings/constraints-on-settings) for the Grafana user.\n1. Set the constraint type for each required setting to **changeable_in_readonly**.\n\nExample (SQL):\n\n```sql\n-- Allow the grafana_reader profile to modify max_execution_time while remaining read-only\nALTER SETTINGS PROFILE grafana_reader\n  SETTINGS readonly = 1,\n  SETTINGS max_execution_time CHANGEABLE_IN_READONLY;\n```\n\nIf you see errors such as `DB::Exception: Cannot modify 'max_execution_time': Setting is locked` at query time, the user is missing this permission. Refer to [Troubleshoot ClickHouse data source issues](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/troubleshooting/) for more details.\n\n{{< admonition type=\"note\" >}}\nIf you use a **public ClickHouse instance**, do not set `readonly = 2`. Keep `readonly = 1` and use the `changeable_in_readonly` constraint described above.\n{{< /admonition >}}\n\n## ClickHouse protocol support\n\nThe data source supports two transport protocols: **Native** (default) and **HTTP**. Both support the same query capabilities. The Native protocol uses ClickHouse's binary TCP interface for better performance. HTTP uses the ClickHouse HTTP interface, which is useful when your network requires HTTP-based connectivity (for example, through a reverse proxy or load balancer).\n\n### Default ports\n\n| Protocol | TLS  | Port |\n|----------|------|------|\n| HTTP     | No   | 8123 |\n| HTTP     | Yes  | 8443 |\n| Native   | No   | 9000 |\n| Native   | Yes  | 9440 |\n\nWhen you enable **Secure connection** in Grafana, you must also set the port to a TLS-enabled port. Grafana does not change the port automatically when TLS is toggled on.\n\n## Add the data source\n\nTo add the data source:\n\n1. Click **Connections** in the left-side menu.\n1. Click **Add new connection**.\n1. Type **ClickHouse** in the search bar.\n1. Select **ClickHouse**.\n1. Click **Add new data source**.\n\n## Configure settings\n\nAfter adding the data source, configure the following settings.\n\n### Server settings\n\n| Setting | Description |\n|---------|-------------|\n| **Name** | The name used to refer to the data source in panels and queries. |\n| **Default** | Toggle to make this the default data source for new panels. |\n| **Server** | The ClickHouse server host (for example, `localhost`). |\n| **Protocol** | **Native** or **HTTP**. |\n| **Port** | Port number; depends on protocol and whether TLS is enabled (see default ports above). |\n| **Secure connection** | Enable when your ClickHouse server uses TLS. When enabled, update the **Port** to a TLS-enabled port and configure [TLS settings](#tls-settings) below. |\n| **Username** | ClickHouse user name. Use a [read-only user](#clickhouse-user-and-permissions). |\n| **Password** | ClickHouse user password. |\n| **Default database** | The database the query builder uses when no database is selected. If left blank, the plugin defaults to `default`. |\n| **Default table** | The default table used by the query builder. |\n\n### Default database guidance\n\nThe **Default database** setting controls which database the query builder and ad hoc filters use when no database is explicitly specified.\n\n- **Self-hosted ClickHouse:** Set this to the database you query most often so that the query builder pre-selects it.\n- **ClickHouse Cloud:** Leave this field **blank**. ClickHouse Cloud connections already route to the correct default database for your service. Setting an explicit value can cause `Unknown database` errors if the name does not match the service's configured database.\n\nIf you are unsure which database to use, leave the field blank and select a database per query in the query builder.\n\n### HTTP settings\n\nThe following settings appear only when **Protocol** is set to **HTTP**:\n\n| Setting | Description |\n|---------|-------------|\n| **HTTP URL Path** | Additional URL path appended to HTTP requests (for example, `/clickhouse`). Defaults to `/`. |\n| **Custom HTTP headers** | Static headers sent with every request. Each header has a name, value, and an optional **Secure** toggle that stores the value in encrypted storage. |\n| **Forward Grafana HTTP headers** | When enabled, forwards Grafana request headers (such as authentication headers) to ClickHouse. Enables multi-connection mode so each unique set of forwarded headers gets its own connection. |\n\n### TLS settings\n\nWhen **Secure connection** is enabled, the following TLS settings become available:\n\n| Setting | Description |\n|---------|-------------|\n| **Skip TLS Verify** | Skip server certificate verification. Use only for testing; not recommended for production. |\n| **TLS Client Auth** | Enable mutual TLS (mTLS) by providing a client certificate and key. |\n| **With CA Cert** | Provide a custom CA certificate for verifying the ClickHouse server's TLS certificate (required for self-signed certificates). |\n| **CA Cert** | PEM-encoded CA certificate. |\n| **Client Cert** | PEM-encoded client certificate (required when TLS Client Auth is enabled). |\n| **Client Key** | PEM-encoded client private key (required when TLS Client Auth is enabled). |\n\n### Additional settings\n\n| Setting | Description |\n|---------|-------------|\n| **Dial Timeout** | Timeout in seconds for establishing a connection. Default: `10`. |\n| **Query Timeout** | Timeout in seconds for read queries. Default: `60`. |\n| **Validate SQL** | When enabled, validates SQL syntax in the query editor. |\n| **Enable row limit** | When enabled, applies the Grafana row limit setting to query results. |\n\n### Custom ClickHouse settings\n\nYou can pass arbitrary ClickHouse `SETTINGS` with every query by adding key-value pairs in the **Custom Settings** section. For example, you can set `max_block_size` or `max_threads` to tune query performance.\n\nThese settings are appended to each query's `SETTINGS` clause. They do not replace any settings that the plugin sets internally (such as `max_execution_time`).\n\n### Logs configuration\n\nThe data source includes a dedicated configuration section for log queries. These settings control the default column mappings used by the [logs query builder](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/query-editor/#logs-query-builder):\n\n| Setting | Description |\n|---------|-------------|\n| **Default log database** | The default database for log queries. |\n| **Default log table** | The default table for log queries. |\n| **Use OTel** | When enabled, pre-fills column mappings for [OpenTelemetry ClickHouse exporter](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/clickhouseexporter) tables. Select the OTel schema version that matches your exporter. |\n| **Time column** | The high-precision timestamp column for sorting log rows. |\n| **Filter Time column** | A lower-precision time column for fast partition-based filtering. |\n| **Log Level column** | The column containing the log severity level. |\n| **Log Message column** | The column containing the log message body. |\n| **Context columns** | Comma-separated list of columns included alongside log messages for additional context. |\n\n### Traces configuration\n\nThe data source includes a dedicated configuration section for trace queries. These settings control the default column mappings used by the [traces query builder](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/query-editor/#traces-query-builder):\n\n| Setting | Description |\n|---------|-------------|\n| **Default trace database** | The default database for trace queries. |\n| **Default trace table** | The default table for trace queries. |\n| **Use OTel** | When enabled, pre-fills column mappings for OpenTelemetry tables. Select the OTel schema version that matches your exporter. |\n| **Duration unit** | The unit for the duration column (`seconds`, `milliseconds`, `microseconds`, or `nanoseconds`). |\n| **Flatten nested** | Enable if your traces table was created with `flatten_nested=1`. |\n\nWhen **Use OTel** is disabled, you can manually configure columns for Trace ID, Span ID, Parent Span ID, Service Name, Operation Name, Start Time, Duration, Tags, Service Tags, Kind, Status Code, Status Message, State, and Instrumentation Library.\n\n### Private data source connect\n\n{{< admonition type=\"note\" >}}\nOnly available for Grafana Cloud users.\n{{< /admonition >}}\n\nPrivate data source connect (PDC) allows you to establish a private, secured connection between a Grafana Cloud instance (or stack) and data sources secured within a private network. Select the drop-down to locate the URL for PDC. For more information, refer to [Private data source connect](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/).\n\nClick **Manage private data source connect** to go to your PDC connection page, where you can find your PDC configuration details.\n\n## Verify the connection\n\nOnce you have configured your ClickHouse connection settings, click **Save & test** to verify the connection. When the connection test succeeds, you see **Data source is working**. A successful test confirms that Grafana can reach ClickHouse and that the credentials are valid.\n\nIf the test fails, refer to [Troubleshoot ClickHouse data source issues](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/troubleshooting/) for common configuration errors and solutions.\n\n## Forward Grafana HTTP headers\n\nWhen you use the **HTTP** protocol, you can propagate Grafana's per-request HTTP headers end-to-end to ClickHouse. This attaches context about the originating Grafana user, dashboard, and panel to every query so you can drive query-log attribution, quotas, and row policies from ClickHouse.\n\nTo enable it, expand **Optional HTTP settings** on the data source and turn on **Forward Grafana HTTP headers to data source**.\n\n{{< admonition type=\"note\" >}}\nThis setting is only available on the HTTP protocol. The native protocol does not carry HTTP headers.\n{{< /admonition >}}\n\nWhen the toggle is on, the following headers are forwarded on each ClickHouse connection:\n\n| Header | Source |\n| ------ | ------ |\n| `X-Grafana-User` | The logged-in Grafana user's login. Populated from the plugin's request context, so you do not need to enable the Grafana `dataproxy.send_user_header` setting for this plugin. |\n| `X-Dashboard-Uid`, `X-Panel-Id`, `X-Panel-Plugin-Id`, `X-Dashboard-Title`, `X-Panel-Title` | Identifiers set by Grafana when the query originates from a dashboard panel. |\n| `X-Grafana-Org-Id`, `X-Query-Group-Id`, `X-Grafana-From-Expr`, `X-Datasource-Uid` | Request-context headers set by Grafana core. |\n\n### Use cases\n\n- **Query-log attribution** — ClickHouse records the forwarded headers in `system.query_log.http_user_agent` and related fields, so operators can correlate queries back to the Grafana user and dashboard that triggered them.\n- **Row policies and quotas** — ClickHouse [row policies](https://clickhouse.com/docs/en/operations/access-rights/#row-policies) and [quotas](https://clickhouse.com/docs/en/operations/quotas) can key on the `X-Grafana-User` header, so a single shared ClickHouse account can still enforce per-viewer access rules.\n\n### Connection pool implications\n\nWith header forwarding enabled, connections are keyed by the forwarded header set, which means each distinct Grafana user opens their own ClickHouse connection. Expect the connection count to scale with concurrent unique users and size `max_connections` on your ClickHouse server accordingly.\n\n### Custom headers\n\nTo forward headers other than the Grafana-set ones — for example, bearer tokens or tenant identifiers — add them as **Custom HTTP headers** in the same **Optional HTTP settings** panel. Custom headers are sent on every query regardless of the **Forward Grafana HTTP headers** toggle.\n\n## Provision the data source\n\nYou can define the data source in YAML files as part of the Grafana provisioning system. For more information, refer to [Provisioning Grafana data sources](https://grafana.com/docs/grafana/latest/administration/provisioning/#data-sources).\n\nExample ClickHouse data source configuration with basic authentication:\n\n```yaml\napiVersion: 1\ndatasources:\n  - name: ClickHouse\n    type: grafana-clickhouse-datasource\n    jsonData:\n      host: localhost\n      port: 9000\n      protocol: native\n      username: grafana_reader\n      # defaultDatabase: <string>\n      # defaultTable: <string>\n      # secure: <bool>\n      # tlsSkipVerify: <bool>\n      # tlsAuth: <bool>\n      # tlsAuthWithCACert: <bool>\n      # dialTimeout: <seconds>\n      # queryTimeout: <seconds>\n      # validateSql: <bool>\n      # enableRowLimit: <bool>\n      # forwardGrafanaHeaders: <bool>\n      # path: <string>  # HTTP URL path (HTTP protocol only)\n      # httpHeaders:     # HTTP protocol only\n      #   - name: X-Example-Header\n      #     secure: false\n      #     value: <string>\n      # customSettings:\n      #   - setting: max_block_size\n      #     value: \"65505\"\n    secureJsonData:\n      password: password\n      # tlsCACert: <string>\n      # tlsClientCert: <string>\n      # tlsClientKey: <string>\n```\n\n## Provision with Terraform\n\nYou can provision the ClickHouse data source using the [Grafana Terraform provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs). Example with basic authentication:\n\n```hcl\nresource \"grafana_data_source\" \"clickhouse\" {\n  type = \"grafana-clickhouse-datasource\"\n  name = \"ClickHouse\"\n\n  json_data_encoded = jsonencode({\n    host             = \"localhost\"\n    port             = 9000\n    protocol         = \"native\"\n    username         = \"grafana_reader\"\n    tlsSkipVerify    = false\n    # defaultDatabase = \"mydb\"\n    # dialTimeout     = \"10\"\n    # queryTimeout    = \"60\"\n    # validateSql     = true\n    # enableRowLimit  = true\n  })\n\n  secure_json_data_encoded = jsonencode({\n    password = var.clickhouse_password\n  })\n}\n```\n\nFor more options and authentication methods, refer to the [Grafana Terraform provider documentation](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/data_source).\n\n## Next steps\n\nAfter configuring the data source:\n\n- [ClickHouse query editor](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/query-editor/) — Build queries with the SQL editor or query builder.\n- [ClickHouse template variables](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/template-variables/) — Use variables in dashboards and queries.\n- [ClickHouse data source](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/) — Overview, supported features, and pre-built dashboards.\n"
  },
  {
    "path": "docs/sources/query-editor.md",
    "content": "---\ndescription: This document describes the ClickHouse query editor\nlabels:\nproducts:\n  - Grafana Cloud\n  - Grafana OSS\n  - Grafana Enterprise\nkeywords:\n  - data source\nmenuTitle: Query editor\ntitle: ClickHouse query editor\nweight: 30\nversion: 0.1\nlast_reviewed: 2026-04-24\n---\n\n# ClickHouse query editor\n\nThis document explains how to use the ClickHouse query editor to build and run queries. You can access the query editor from [Explore](https://grafana.com/docs/grafana/latest/visualizations/explore/) to run ad hoc queries, or when you add or edit a panel and select the ClickHouse data source.\n\n## Before you begin\n\n- [Configure the ClickHouse data source](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/configure/).\n- Ensure your ClickHouse user has read access to the databases and tables you want to query.\n\n## Query editor elements\n\nThe query editor appears in [Explore](https://grafana.com/docs/grafana/latest/visualizations/explore/) when you select the ClickHouse data source, or when you add or edit a panel and select ClickHouse. It includes the following elements.\n\n| Element         | Description                                                                                                                                                                                |\n| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| **Editor type** | Switch between **SQL** (write raw SQL) and **Query builder** (build queries with drop-downs and filters).                                                                                  |\n| **Run Query**   | Runs the current query and refreshes the panel. In the SQL editor you can also use **Ctrl+Enter** (Windows/Linux) or **Cmd+Enter** (macOS).                                                |\n| **Query type**  | Choose the result format: **Table**, **Logs**, **Time series**, or **Traces**. This sets how Grafana interprets and visualizes the results. Available in both SQL and Query builder modes. |\n\n**In SQL mode:**\n\n- **SQL editor** — A code editor where you write ClickHouse SQL. It provides schema suggestions (databases, tables, columns) as you type. If SQL validation is enabled in the data source settings, the editor marks invalid syntax.\n- **Format code** — Use the editor toolbar to format your SQL.\n- **Query type** — Select **Table**, **Logs**, **Time series**, or **Traces** so the panel uses the correct visualization.\n\n**In Query builder mode:**\n\n- **Database** and **Table** — Select the database and table to query from the drop-downs.\n- **Query type** — Select **Table**, **Logs**, **Time series**, or **Traces**. The builder shows options that match the type (for example, time column and value columns for time series; columns, filters, group by, and order by for tables).\n- **Type-specific options** — Configure columns, filters, grouping, sorting, limit (max rows), and (for traces) trace ID. The builder generates the SQL for you.\n- **SQL preview** — At the bottom of the builder, you can see the generated SQL. You can switch to SQL mode to edit it manually.\n\n## Build queries\n\nYou can build queries using the **SQL editor** (raw SQL) or the **Query builder**. Queries can include macros for dynamic parts such as time range filters.\n\n### Query builder modes\n\nWhen using the query builder, the available options depend on the selected **query type**:\n\n| Query type | Builder modes | Description |\n|------------|---------------|-------------|\n| **Table** | **List** or **Aggregate** | List returns raw rows. Aggregate lets you add functions like `count()`, `avg()`, and `GROUP BY`. |\n| **Time series** | **Trend** | Automatically groups by time using `$__timeInterval()` and applies aggregate functions. Select a time column, one or more value columns, and optional grouping columns. |\n| **Logs** | **List** or **Aggregate** | List returns log rows. Aggregate supports grouping for log volume histograms. |\n| **Traces** | **Trace ID** or **Trace search** | Trace ID fetches a single trace by ID. Trace search finds traces matching filters (service name, duration, time range). |\n\n### Logs query builder\n\nThe logs query builder provides structured fields for common log exploration patterns. When **Use OTel** is enabled, the builder pre-fills column mappings for [OpenTelemetry ClickHouse exporter](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/clickhouseexporter) tables.\n\n| Field | Description |\n|-------|-------------|\n| **Time column** | The high-precision timestamp column used for sorting log rows. |\n| **Filter Time column** | A lower-precision column for fast time range filtering (for example, a `Date` or `DateTime` column indexed for partition pruning). Using a separate filter column can significantly improve query performance. |\n| **Log Level column** | The column that contains the log severity level. |\n| **Log Message column** | The column that contains the log message body. |\n\nYou can also filter by log message text using the search field, and add column filters for resource, scope, or log attributes.\n\n### Traces query builder\n\nThe traces query builder supports two modes:\n\n- **Trace ID mode** — Enter a trace ID (or use a template variable like `$traceId`) to fetch all spans for that trace.\n- **Trace search mode** — Filter traces by service name, span name, duration range, and time range.\n\nWhen **Use OTel** is enabled, column mappings are pre-filled for the OpenTelemetry schema. You can also configure:\n\n| Field | Description |\n|-------|-------------|\n| **Duration unit** | The unit for the duration column (`seconds`, `milliseconds`, `microseconds`, or `nanoseconds`). |\n| **Flatten nested** | Enable if your traces table was created with `flatten_nested=1`. |\n| **Events prefix** | Prefix for event columns (for example, `Events.Timestamp`, `Events.Name`). |\n| **Links prefix** | Prefix for link columns (for example, `Links.TraceId`, `Links.SpanId`). |\n\n### Log volume and logs sample\n\nWhen using the **query builder** in Explore with the **Logs** query type, Grafana automatically shows a **log volume** histogram above the log results and can display a **logs sample** panel. These supplementary queries are generated by the plugin and run alongside your main query.\n\n{{< admonition type=\"note\" >}}\nLog volume and logs sample are only available when all queries in the panel use the **query builder**. If any query uses the **SQL editor**, supplementary queries are disabled.\n{{< /admonition >}}\n\n## Time series\n\nFor time series visualizations, your query must include a `datetime` column. Use an alias of `time` for the timestamp column. Grafana treats timestamp rows without an explicit time zone as UTC. Any column other than `time` is treated as a value column.\n\n## Multi-line time series\n\nTo create multi-line time series, the query must return at least 3 columns in this order:\n\n1. A `datetime` column with an alias of `time`\n1. A column to group by (for example, category or label)\n1. One or more metric columns\n\nExample (replace `mgbench.logs1` with your database and table):\n\n```sql\nSELECT log_time AS time, machine_group, avg(disk_free) AS avg_disk_free\nFROM mgbench.logs1\nGROUP BY machine_group, log_time\nORDER BY log_time\n```\n\n## Tables\n\nTable visualizations are available for any valid ClickHouse query. Select **Table** in the panel visualization options to view results in tabular form.\n\n## Visualize logs with the Logs panel\n\nTo use the Logs panel, your query must return a timestamp and string values. To default to the logs visualization in Explore, set the timestamp column alias to `log_time`.\n\nExample (replace `logs1` with your database and table, for example `mydb.logs`):\n\n```sql\nSELECT log_time AS log_time, machine_group, toString(avg(disk_free)) AS avg_disk_free\nFROM logs1\nGROUP BY machine_group, log_time\nORDER BY log_time\n```\n\nWhen you don't have a `log_time` column, set **Format** to **Logs** to force logs rendering (available from plugin version 2.2.0).\n\n## Visualize traces with the Traces panel\n\nTo use the Traces panel, your data must meet the [requirements of the traces panel](https://grafana.com/docs/grafana/latest/explore/trace-integration/#data-api). Set **Format** to **Trace** when building the query (available from plugin version 2.2.0).\n\nIf you use the [OpenTelemetry Collector and ClickHouse exporter](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/clickhouseexporter), the following query returns the required column names (case sensitive). Replace the trace ID in the WHERE clause with your trace ID or a template variable (for example `$traceId`):\n\n```sql\nSELECT\n  TraceId AS traceID,\n  SpanId AS spanID,\n  SpanName AS operationName,\n  ParentSpanId AS parentSpanID,\n  ServiceName AS serviceName,\n  Duration / 1000000 AS duration,\n  Timestamp AS startTime,\n  arrayMap(key -> map('key', key, 'value', SpanAttributes[key]), mapKeys(SpanAttributes)) AS tags,\n  arrayMap(key -> map('key', key, 'value', ResourceAttributes[key]), mapKeys(ResourceAttributes)) AS serviceTags,\n  if(StatusCode IN ('Error', 'STATUS_CODE_ERROR'), 2, 0) AS statusCode\nFROM otel.otel_traces\nWHERE TraceId = '61d489320c01243966700e172ab37081'\nORDER BY startTime ASC\n```\n\n## Column roles\n\nWhen you use the Query builder with the **Logs**, **Time series**, or **Traces** query type, each built-in column slot is mapped to a *semantic role*. The builder renames your columns to the fixed aliases Grafana's panels expect, so the same panel can visualize data from any ClickHouse schema once you tell it which columns play which roles.\n\nFor example, choosing your `EventTime` column as the **Time** role for a Logs query produces `SELECT EventTime AS timestamp, ...`; the Logs panel then sorts on `timestamp` without needing to know your real column name.\n\n### Logs query type\n\n| Role          | SQL alias   | OTel column    | Common non-OTel names                                 |\n| ------------- | ----------- | -------------- | ----------------------------------------------------- |\n| **Time**      | `timestamp` | `Timestamp`    | `timestamp`, `event_time`, `@timestamp`, `created_at` |\n| **Message**   | `body`      | `Body`         | `message`, `msg`, `log_message`                       |\n| **Log Level** | `level`     | `SeverityText` | `level`, `severity`, `severity_text`, `log_level`     |\n\nOptional additional columns (OTel mode only): `TraceId` → `traceID`, plus the attribute maps `ResourceAttributes`, `ScopeAttributes`, and `LogAttributes`.\n\n### Time series query type\n\n| Role     | Description                                                                                                                                     |\n| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |\n| **Time** | Timestamp used to order and bucket the series. Must be a `DateTime` or `DateTime64` column. The panel time range filter applies to this column. |\n\nAny other selected columns become value series. In **Aggregate** mode the builder produces `GROUP BY` on the time column and the aggregated columns.\n\n### Traces query type\n\nTrace-panel column aliases are fixed; choose the table columns that play each role.\n\n| Role               | SQL alias       | OTel column          | Common non-OTel names                      |\n| ------------------ | --------------- | -------------------- | ------------------------------------------ |\n| **Trace ID**       | `traceID`       | `TraceId`            | `trace_id`, `traceId`                      |\n| **Span ID**        | `spanID`        | `SpanId`             | `span_id`, `spanId`                        |\n| **Parent Span ID** | `parentSpanID`  | `ParentSpanId`       | `parent_span_id`                           |\n| **Service Name**   | `serviceName`   | `ServiceName`        | `service`, `service_name`                  |\n| **Operation Name** | `operationName` | `SpanName`           | `operation`, `operation_name`, `span_name` |\n| **Start Time**     | `startTime`     | `Timestamp`          | `start_time`, `timestamp`                  |\n| **Duration Time**  | `duration`      | `Duration`           | `duration`, `duration_ns`, `duration_ms`   |\n| **Tags**           | `tags`          | `SpanAttributes`     | `tags`, `attributes`                       |\n| **Service Tags**   | `serviceTags`   | `ResourceAttributes` | `resource`, `resource_attributes`          |\n| **Kind**           | `kind`          | `SpanKind`           | `kind`, `span_kind`                        |\n| **Status Code**    | `statusCode`    | `StatusCode`         | `status`, `status_code`                    |\n| **Status Message** | `statusMessage` | `StatusMessage`      | `status_message`                           |\n\nSet the **Duration Unit** to match the units your column stores (OTel uses nanoseconds; other schemas often use milliseconds or seconds). The builder converts durations to milliseconds for the trace panel.\n\nTo avoid re-mapping roles for every query, configure defaults under **Data source settings → Logs** and **Data source settings → Traces**. Enabling **OTel** mode populates every role with the OTel-conventional column name automatically.\n\n## Macros\n\nMacros simplify query syntax and add dynamic parts such as dashboard time range filters. The plugin replaces macros with the corresponding SQL before the query is sent to ClickHouse.\n\nExample query using a time filter macro (replace `test_data` and `date_time` with your table and timestamp column):\n\n```sql\nSELECT date_time, data_stuff\nFROM test_data\nWHERE $__timeFilter(date_time)\n```\n\n| Macro                                        | Description                                                                                      | Output example                                                                                        |\n| -------------------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |\n| `$__dateFilter(columnName)`                  | Filters by the panel date range using the given column.                                          | `date >= toDate('2022-10-21') AND date <= toDate('2022-10-23')`                                       |\n| `$__timeFilter(columnName)`                  | Filters by the panel time range (seconds).                                                       | `time >= toDateTime(1415792726) AND time <= toDateTime(1447328726)`                                   |\n| `$__timeFilter_ms(columnName)`               | Filters by the panel time range (milliseconds).                                                  | `time >= fromUnixTimestamp64Milli(1415792726123) AND time <= fromUnixTimestamp64Milli(1447328726456)` |\n| `$__dateTimeFilter(dateColumn, timeColumn)`  | Combines date and time filters for separate Date and DateTime columns.                           | `date >= toDate('2022-10-21') AND date <= toDate('2022-10-23') AND time >= toDateTime(1415792726) AND time <= toDateTime(1447328726)` |\n| `$__dt(dateColumn, timeColumn)`              | Shorthand alias for `$__dateTimeFilter`.                                                         | Same as `$__dateTimeFilter`.                                                                          |\n| `$__fromTime`                                | Start of the panel time range as `DateTime`.                                                     | `toDateTime(1415792726)`                                                                              |\n| `$__toTime`                                  | End of the panel time range as `DateTime`.                                                       | `toDateTime(1447328726)`                                                                              |\n| `$__fromTime_ms`                             | Start of the panel time range as `DateTime64(3)`.                                                | `fromUnixTimestamp64Milli(1415792726123)`                                                             |\n| `$__toTime_ms`                               | End of the panel time range as `DateTime64(3)`.                                                  | `fromUnixTimestamp64Milli(1447328726456)`                                                             |\n| `$__interval_s`                              | Panel interval in seconds.                                                                       | `20`                                                                                                  |\n| `$__timeInterval(columnName)`                | Interval from panel time range (seconds), for grouping.                                          | `toStartOfInterval(toDateTime(column), INTERVAL 20 second)`                                           |\n| `$__timeInterval_ms(columnName)`             | Interval from panel time range (milliseconds), for grouping.                                     | `toStartOfInterval(toDateTime64(column, 3), INTERVAL 20 millisecond)`                                 |\n| `$__conditionalAll(condition, $templateVar)` | Uses the first parameter when the template variable does not select all values; otherwise `1=1`. | `condition` or `1=1`                                                                                  |\n\nYou can also use brace notation `{}` when the macro parameter must contain a query or other expression.\n\n## Next steps\n\n- [ClickHouse template variables](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/template-variables/) — Use variables in dashboards and queries.\n- [Configure the ClickHouse data source](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/configure/) — Connection and authentication options.\n"
  },
  {
    "path": "docs/sources/template-variables.md",
    "content": "---\ndescription: Use template variables with the ClickHouse data source to build dynamic dashboards\nlabels:\nproducts:\n  - Grafana Cloud\n  - Grafana OSS\n  - Grafana Enterprise\nkeywords:\n  - data source\n  - variables\nmenuTitle: Template variables\ntitle: ClickHouse template variables\nweight: 40\nversion: 0.1\nlast_reviewed: 2026-04-24\n---\n\n# ClickHouse template variables\n\nTemplate variables let you parameterize your dashboards so you can change databases, tables, environments, or other values from a drop-down without editing each query. This makes dashboards more interactive, reusable, and easier to maintain.\n\nFor an introduction to templating and variable types, see [Templating](https://grafana.com/docs/grafana/latest/variables/) and [Add variables](https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/).\n\n## Before you begin\n\n- [Configure the ClickHouse data source](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/configure/).\n\n## Create a query variable\n\nTo create a template variable that gets its values from ClickHouse:\n\n1. Open the dashboard where you want to add the variable.\n2. Click **Dashboard settings** (gear icon) in the top navigation.\n3. Select **Variables** in the left menu.\n4. Click **Add variable**.\n5. Enter a **Name** for your variable (for example, `database`, `table`, or `environment`). Use a name you can reference in queries (for example, `$database`).\n6. In the **Type** drop-down, select **Query**.\n7. In the **Data source** drop-down, select your ClickHouse data source.\n8. In the **Query** field, enter a ClickHouse SQL query that returns the values for the variable. The query can return one column (same label and value) or two columns (value and label). See [How query results become variable options](#how-query-results-become-variable-options) and [Query examples](#query-examples).\n9. Click **Run query** to preview the variable options.\n10. Set **Refresh** to control when the variable options update (see [Variable refresh options](#variable-refresh-options)).\n11. Configure **Multi-value** or **Include All option** if needed.\n12. Click **Apply** to save the variable.\n\n## How query results become variable options\n\nThe plugin uses the query result to build the variable’s drop-down options:\n\n- **Single column:** Each row becomes one option. Both the displayed label and the value used in queries are that column’s value.\n- **Two columns:** The first column is used as the **value** (for example, an id or key). The second column is used as the **text** (the label shown in the drop-down).\n\n**Example — single column (database names as label and value):**\n\n```sql\nSELECT name FROM system.databases WHERE name NOT IN ('INFORMATION_SCHEMA', 'information_schema')\n```\n\nYou can omit the `WHERE` clause if your ClickHouse instance does not have those databases (for example, a standalone ClickHouse server typically only has `default` and `system`).\n\n**Example — two columns (id as value, name as label):**\n\n```sql\nSELECT id, name FROM my_app.environments\n```\n\nHere, the drop-down shows `name`, and queries receive `id` when the variable is used.\n\n## Variable syntax in queries\n\nUse variables in your ClickHouse queries by referencing them with `$varname` or `${varname}`. Grafana replaces the variable with the selected value (or values) before the query is sent to ClickHouse.\n\nFor full syntax and options, see [Variable syntax](https://grafana.com/docs/grafana/latest/variables/syntax/).\n\n## Format options for safe SQL\n\nTo avoid SQL syntax or injection issues, use a **format** when the variable is used inside a string or list:\n\n- **singlequote** — Wraps each value in single quotes and escapes single quotes inside the value. Use this for string literals and `IN` lists in ClickHouse.\n\n**Example — filter by one database:**\n\n```sql\nSELECT * FROM system.tables WHERE database = ${database:singlequote}\n```\n\n**Example — filter by multiple databases (multi-value variable):**\n\n```sql\nSELECT * FROM system.tables WHERE database IN (${database:singlequote})\n```\n\nWithout `:singlequote`, multi-value variables are comma-separated and can produce invalid SQL. Other formats (for example, **regex** or **pipe**) are described in [Variable syntax](https://grafana.com/docs/grafana/latest/variables/syntax/).\n\n## Cascading (dependent) variables\n\nYou can make one variable depend on another by using the first variable in the second variable’s query. When the user changes the first variable, the second variable’s options update automatically.\n\n**Example: database → table**\n\n1. Create a variable named `database` with query:\n\n   ```sql\n   SELECT name FROM system.databases WHERE name NOT IN ('INFORMATION_SCHEMA', 'information_schema')\n   ```\n\n2. Create a variable named `table` with query:\n\n   ```sql\n   SELECT name FROM system.tables WHERE database = ${database:singlequote}\n   ```\n\nWhen you change the selected database, the table drop-down refreshes with tables from that database.\n\n## Using the \"All\" option with `$__conditionalAll`\n\nIf you enable **Include All option** for a variable, selecting **All** sets the variable value to `$__all`. A condition like `WHERE database IN (${database:singlequote})` may not behave as intended when **All** is selected.\n\nUse the **$\\_\\_conditionalAll(condition, $variable)** macro so that:\n\n- When the variable is **not** \"All\", the macro is replaced by the condition (for example, `database IN ('db1', 'db2')`).\n- When the variable **is** \"All\", the macro is replaced by `1=1` (no filter).\n\n**Example:**\n\n```sql\nSELECT count() FROM system.tables\nWHERE $__conditionalAll(database IN (${database:singlequote}), $database)\n```\n\nWhen the user selects one or more databases, the condition filters by those databases. When the user selects **All**, the condition becomes `1=1` and all databases are included for optimization.\n\n{{< admonition type=\"note\" >}}\nThe second argument to `$__conditionalAll` must be a plain variable reference (`$database` or `${database}`). Do not use format specifiers like `${database:singlequote}` in the second argument — the macro will not detect the \"All\" selection correctly. Format specifiers should only be used in the **first** argument (the condition).\n{{< /admonition >}}\n\nSee the [ClickHouse query editor](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/query-editor/) Macros section for the full list of macros.\n\n## Query examples\n\n| Use case                                     | Query                                                                                                                                             |\n| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| List databases                               | `SELECT name FROM system.databases` (optionally add `WHERE name NOT IN ('INFORMATION_SCHEMA', 'information_schema')` to exclude those if present) |\n| List tables (for chosen database)            | `SELECT name FROM system.tables WHERE database = ${database:singlequote}`                                                                         |\n| List columns (for chosen database and table) | `SELECT name FROM system.columns WHERE database = ${database:singlequote} AND table = ${table:singlequote}`                                       |\n| Distinct values for a column                 | `SELECT DISTINCT environment FROM my_app.events ORDER BY environment`                                                                             |\n\nReplace `my_app.events` and column names with your own database, table, and columns.\n\n## Variable refresh options\n\nSet **Refresh** to control when the variable’s query runs and the options update:\n\n| Option                   | Behavior                                                                                                                 |\n| ------------------------ | ------------------------------------------------------------------------------------------------------------------------ |\n| **On dashboard load**    | Options refresh when the dashboard loads. Use for data that changes infrequently (for example, database or table lists). |\n| **On time range change** | Options refresh when the dashboard time range changes. Use only if your variable query depends on the time range.        |\n\nFor dashboards with many variables or heavy variable queries, **On dashboard load** is usually sufficient and avoids unnecessary load.\n\n## Multi-value variables\n\nWhen **Multi-value** is enabled, users can select more than one value. The selected values are typically comma-separated when substituted into the query. Use the **singlequote** format so each value is correctly quoted in SQL:\n\n```sql\nWHERE database IN (${database:singlequote})\n```\n\nWhen one variable’s query uses another variable (cascading variables) and that other variable is multi-value, Grafana often substitutes only the first selected value. Ensure that the first value alone still gives a valid and useful list for the dependent variable.\n\n## Ad hoc filters\n\nAd hoc filters let you add key/value filters that are applied to queries that use the ClickHouse data source. You choose filter values from a drop-down in the dashboard without editing the query. Ad hoc filters are supported only with **ClickHouse 22.7 or later**. For an overview, see [Grafana ad hoc filters](https://grafana.com/docs/grafana/latest/variables/variable-types/add-ad-hoc-filters/).\n\nBy default, the ad hoc filter drop-down lists all tables and columns from the data source. If you set a default database in the data source settings, only tables from that database are used. To limit which tables or columns appear (for example, to avoid slow loads), add a dashboard variable of type **Constant** named `clickhouse_adhoc_query`. Set its value to one of:\n\n- A single database name (for example, `my_database`) — shows tables and columns from that database only.\n- `database.table` (for example, `my_database.my_table`) — shows only columns for that specific table.\n- A `SELECT` query — uses the query results to populate filter keys. See [Use a query to populate ad hoc filters](#use-a-query-to-populate-ad-hoc-filters).\n\nYou can hide this variable from the dashboard; it is only used to scope the ad hoc filter options.\n\n{{< admonition type=\"note\" >}}\nAd hoc filter values loaded from the schema are limited to **1000 distinct values** per column. If a column has more than 1000 distinct values, only the first 1000 are shown in the filter drop-down.\n{{< /admonition >}}\n\n## Use a query to populate ad hoc filters\n\nYou can set `clickhouse_adhoc_query` to a **ClickHouse query** instead of a database or table name. The query results are used to populate the ad hoc filter’s selectable values. For example, set the variable value to:\n\n```sql\nSELECT DISTINCT machine_name FROM mgbench.logs1\n```\n\nThen the dashboard filter drop-down lists distinct `machine_name` values, and you can filter queries by the selected machine.\n\n### Dynamic column values with `$__adhoc_column`\n\nWhen using a `SELECT` query in `clickhouse_adhoc_query`, you can use the `$__adhoc_column` placeholder to make the query dynamic. The plugin replaces `$__adhoc_column` with the column name that the user selected as the filter key.\n\nThis is useful when you want the filter **values** drop-down to be populated by a query rather than by scanning the schema. For example, set `clickhouse_adhoc_query` to:\n\n```sql\nSELECT DISTINCT $__adhoc_column FROM my_database.my_table\n```\n\nWhen the user selects a column (for example, `status`) in the ad hoc filter key drop-down, the plugin runs `SELECT DISTINCT status FROM my_database.my_table` to populate the value drop-down. The filter keys themselves are derived from the table's columns in `system.columns`.\n\n## Supported ad hoc filter operators\n\nAd hoc filters support the following operators:\n\n| Operator | ClickHouse equivalent | Description |\n|----------|----------------------|-------------|\n| `=` | `=` | Equals |\n| `!=` | `!=` | Not equals |\n| `<` | `<` | Less than |\n| `>` | `>` | Greater than |\n| `=~` | `ILIKE` | Case-insensitive pattern match (use `%` as wildcard) |\n| `!~` | `NOT ILIKE` | Negated case-insensitive pattern match |\n| `IN` | `IN (...)` | Matches any value in a list |\n\n## Hide table name in ad hoc filter keys\n\nBy default, ad hoc filter keys are shown as `table.column`. To show only the column name (without the table prefix), enable the **Hide table name in ad hoc filters** option in the data source settings. This makes the filter drop-down cleaner when all queries target the same table.\n\n## Map and JSON types (OpenTelemetry)\n\nAd hoc filters work with Map and JSON types for OpenTelemetry data. **Map** is the default and turns merged labels into a filter. To use **JSON** syntax for the filter logic, add a dashboard variable of type **Constant** named `clickhouse_adhoc_use_json`. The variable’s value is ignored; it only needs to exist.\n\n## Apply ad hoc filters manually with `$__adHocFilters`\n\nBy default, ad hoc filters are applied automatically by detecting the target table from your SQL. For queries that use CTEs, subqueries, or ClickHouse-specific syntax (for example `INTERVAL` or parameterized aggregate functions), automatic detection can fail. In those cases, use the `$__adHocFilters('table_name')` macro to specify where to apply the filters.\n\nThe macro expands to the ClickHouse `additional_table_filters` setting with the currently active ad hoc filter conditions. Place it in the **SETTINGS** clause of your query.\n\n{{< admonition type=\"note\" >}}\nWhen `$__adHocFilters` is present in a query, the plugin skips automatic ad hoc filter injection for that query. Use either the macro **or** automatic injection for a given query, not both.\n{{< /admonition >}}\n\nExample:\n\n```sql\nSELECT *\nFROM (\n  SELECT * FROM my_complex_table\n  WHERE complicated_condition\n) AS result\nSETTINGS $__adHocFilters('my_complex_table')\n```\n\nWhen ad hoc filters are active (for example, `status = 'active'` and `region = 'us-west'`), the macro expands to:\n\n```sql\nSETTINGS additional_table_filters={'my_complex_table': 'status = \\'active\\' AND region = \\'us-west\\''}\n```\n\n## Next steps\n\n- [ClickHouse query editor](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/query-editor/) — Macros (including `$__timeFilter`, `$__conditionalAll`) and building queries.\n- [Configure the ClickHouse data source](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/configure/) — Connection and authentication options.\n- [Troubleshoot ClickHouse data source issues](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/troubleshooting/) — Common errors and solutions.\n"
  },
  {
    "path": "docs/sources/troubleshooting.md",
    "content": "---\ndescription: Solutions for common errors when using the ClickHouse data source\nlabels:\nproducts:\n  - Grafana Cloud\n  - Grafana OSS\n  - Grafana Enterprise\nkeywords:\n  - data source\n  - troubleshooting\nmenuTitle: Troubleshooting\ntitle: Troubleshoot ClickHouse data source issues\nweight: 70\nversion: 0.1\nlast_reviewed: 2026-04-27\n---\n\n## Troubleshoot ClickHouse data source issues\n\nThis guide provides solutions for common errors you may encounter when configuring or using the ClickHouse data source for Grafana.\n\n### Connection Errors\n\nInvalid Server Host\n\n**Error message:** \"invalid server host. Either empty or not set\"\n\n**Cause:** The server host field is empty or was not configured in the data source settings.\n\n**Solution:**\n\n1. Open the data source configuration in Grafana.\n2. Verify that the **Server** field contains a valid hostname or IP address.\n3. Ensure there are no leading or trailing spaces in the host value.\n\n---\n\nInvalid Port\n\n**Error message:** \"invalid port\"\n\n**Cause:** The port number is missing, empty, or contains an invalid value.\n\n**Solution:**\n\n1. Open the data source configuration in Grafana.\n2. Verify that the **Port** field contains a valid port number.\n3. Use the default port `9000` for native protocol or `8123` for HTTP protocol.\n4. Ensure the port value is a number without any special characters.\n\n---\n\nFailed to Create ClickHouse Client\n\n**Error message:** \"failed to create ClickHouse client\" or \"failed to create data source\"\n\n**Cause:** The plugin was unable to establish a connection to the ClickHouse server. This is the most commonly reported ClickHouse error. The error message is intentionally generic — the actual root cause can be any of the issues listed below.\n\n**Solution:**\n\nWork through the following checks in order. Most cases are resolved by one of the first four items.\n\n1. **Clear the Default database field.** This is one of the most common fixes. If you are connecting to **ClickHouse Cloud**, leave it blank — setting an explicit database name that does not match the service's configured database causes this error. For details, see [Default database guidance](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/configure/#default-database-guidance).\n2. **Verify credentials.** Confirm that the username and password are correct for the ClickHouse server. A typo or stale password is a frequent cause.\n3. **Check ClickHouse user permissions.** The connection test may pass, but queries can still fail if the user lacks permission to modify settings such as `max_execution_time`. See [Required SETTINGS permissions](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/configure/#required-settings-permissions).\n4. **Confirm network connectivity.** Verify that the ClickHouse server is running and reachable from the Grafana server on the configured port. Ensure no firewall rules or security groups are blocking the connection.\n5. **Review the hostname and port.** Check for leading/trailing spaces and confirm the port matches the protocol (Native: 9000/9440, HTTP: 8123/8443).\n6. **Verify TLS configuration.** If using TLS/SSL, confirm that certificates are correctly configured and the port supports TLS.\n7. **Check Grafana Cloud data source quotas.** On Grafana Cloud, verify that the data source is being added to the correct stack and that you have not reached the data source quota for your plan.\n8. **Test from the command line.** Run `clickhouse-client` from the Grafana server to isolate whether the problem is network/ClickHouse-side or Grafana-side.\n9. **Check Grafana server logs.** The server logs often contain a more detailed error message that narrows down the root cause.\n\n---\n\nConnection Timeout\n\n**Error message:** \"connection timeout\" or \"the operation was cancelled before starting: context deadline exceeded\"\n\n**Cause:** The connection to the ClickHouse server timed out before it could be established.\n\n**Solution:**\n\n1. Verify that the ClickHouse server is reachable from the Grafana server.\n2. Increase the **Dial Timeout** value in the data source settings (default is 10 seconds).\n3. Check for network latency or connectivity issues between Grafana and ClickHouse.\n4. Ensure no firewall or security group is blocking the connection.\n\n---\n\nOperation Cancelled During Execution\n\n**Error message:** \"the operation was cancelled during execution: context deadline exceeded\"\n\n**Cause:** The query or connection operation exceeded the configured timeout while running.\n\n**Solution:**\n\n1. Increase the **Query Timeout** value in the data source settings (default is 60 seconds).\n2. Optimize your query to reduce execution time.\n3. Check if the ClickHouse server is under heavy load.\n4. Consider adding appropriate indexes to your ClickHouse tables.\n\n---\n\nPlugin Not Found After Installation (Grafana Cloud)\n\n**Error message:** 404 error when adding the ClickHouse data source, or the plugin does not appear in the data source list after installation.\n\n**Cause:** New Grafana Cloud instances on the **Fast** release channel may not yet have the ClickHouse plugin available. The Fast channel receives Grafana updates earlier, but plugin availability can lag behind.\n\n**Solution:**\n\n1. [Open a support ticket](https://grafana.com/profile/org#support) and request that your instance be moved to the **Steady** release channel.\n2. After the channel change takes effect, reinstall or re-add the ClickHouse data source.\n3. Once the plugin is working, you can discuss with support whether switching back to the Fast channel is safe for your use case.\n\n---\n\n### Authentication Errors\n\nInvalid Username\n\n**Error message:** \"username is either empty or not set\"\n\n**Cause:** The username field is empty or was not configured in the data source settings.\n\n**Solution:**\n\n1. Open the data source configuration in Grafana.\n2. Enter a valid ClickHouse username in the **Username** field.\n3. Verify that the user exists in ClickHouse and has appropriate permissions.\n\n---\n\nInvalid Password\n\n**Error message:** \"password is either empty or not set\"\n\n**Cause:** The password field is empty or was not configured when authentication requires a password.\n\n**Solution:**\n\n1. Open the data source configuration in Grafana.\n2. Enter the correct password in the **Password** field.\n3. Verify that the password matches the one configured in ClickHouse for the specified user.\n\n---\n\n### TLS/SSL Certificate Errors\n\nInvalid CA Certificate\n\n**Error message:** \"failed to parse TLS CA PEM certificate\"\n\n**Cause:** The CA certificate provided is not in valid PEM format or is corrupted.\n\n**Solution:**\n\n1. Verify that the CA certificate is in PEM format (begins with `-----BEGIN CERTIFICATE-----`).\n2. Ensure the entire certificate content is copied, including the BEGIN and END markers.\n3. Check that there are no extra spaces or line breaks in the certificate.\n4. Regenerate the CA certificate if it may be corrupted.\n\n---\n\nInvalid Client Certificate\n\n**Error message:** \"tls: failed to find any PEM data in certificate input\"\n\n**Cause:** The client certificate or key provided is not in valid PEM format or is empty.\n\n**Solution:**\n\n1. Verify that both the client certificate and client key are in PEM format.\n2. Ensure the client certificate begins with `-----BEGIN CERTIFICATE-----`.\n3. Ensure the client key begins with `-----BEGIN PRIVATE KEY-----` or `-----BEGIN RSA PRIVATE KEY-----`.\n4. Check that the certificate and key match (were generated together).\n5. Verify that the entire content is copied without truncation.\n\n---\n\n### Protocol Errors\n\nInvalid Protocol\n\n**Error message:** \"protocol is invalid, use native or http\"\n\n**Cause:** An unsupported protocol was specified in the data source configuration.\n\n**Solution:**\n\n1. Open the data source configuration in Grafana.\n2. Set the **Protocol** to either `native` or `http`.\n3. Use `native` (port 9000) for better performance or `http` (port 8123) for HTTP-based connectivity.\n\n---\n\n### Configuration Parsing Errors\n\nInvalid JSON Configuration\n\n**Error message:** \"could not parse json\"\n\n**Cause:** The data source configuration contains invalid JSON syntax.\n\n**Solution:**\n\n1. If you are provisioning the data source via YAML/JSON files, validate the JSON syntax.\n2. Use a JSON validator to check for syntax errors.\n3. Ensure all string values are properly quoted and special characters are escaped.\n4. Re-save the data source configuration through the Grafana UI.\n\n---\n\n## Could Not Parse Configuration Values\n\n**Error messages:**\n\n- \"could not parse port value\"\n- \"could not parse secure value\"\n- \"could not parse tlsSkipVerify value\"\n- \"could not parse tlsAuth value\"\n- \"could not parse tlsAuthWithCACert value\"\n- \"could not parse forwardGrafanaHeaders value\"\n\n**Cause:** A configuration value could not be converted to the expected type (boolean or number).\n\n**Solution:**\n\n1. Verify that boolean values are set to `true` or `false` (without quotes in JSON, or as strings `\"true\"`/`\"false\"`).\n2. Verify that numeric values like port are valid integers.\n3. Re-configure the data source through the Grafana UI to ensure proper value types.\n\n---\n\nInvalid Timeout Values\n\n**Error messages:**\n\n- \"invalid timeout: [value]\"\n- \"invalid query timeout: [value]\"\n\n**Cause:** The dial timeout or query timeout value is not a valid integer.\n\n**Solution:**\n\n1. Open the data source configuration in Grafana.\n2. Ensure the **Dial Timeout** and **Query Timeout** fields contain valid integer values (in seconds).\n3. Remove any non-numeric characters from the timeout fields.\n\n---\n\n### Permission and Settings Errors\n\nSetting Is Locked (readonly)\n\n**Error message:** \"DB::Exception: Cannot modify 'max_execution_time': Setting is locked (in readonly mode)\"\n\n**Cause:** The ClickHouse user has `readonly = 1` but does not have permission to modify the `max_execution_time` setting, which the plugin's client needs to enforce query timeouts.\n\n**Solution:**\n\n1. Create a settings profile or constraint that allows `max_execution_time` to be changed in read-only mode:\n   ```sql\n   ALTER SETTINGS PROFILE grafana_reader\n     SETTINGS readonly = 1,\n     SETTINGS max_execution_time CHANGEABLE_IN_READONLY;\n   ```\n2. Assign this profile to the Grafana ClickHouse user.\n3. Verify the fix by running `SELECT 1 SETTINGS max_execution_time = 30` as the Grafana user in `clickhouse-client`.\n\nFor more details on configuring permissions, refer to [ClickHouse user and permissions](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/configure/#clickhouse-user-and-permissions).\n\n---\n\n### Query Builder Issues\n\nEmpty Database, Table, or Column Dropdowns\n\n**Symptoms:** The database, table, or column dropdowns in the query builder show no options, or they briefly attempt to load and then appear empty.\n\n**Cause:** The plugin queries ClickHouse system tables (`system.databases`, `system.columns`) to populate these dropdowns. If the ClickHouse user lacks permission to read those system tables, or if a network or timeout issue prevents the query from completing, the dropdowns will be empty without a visible error message in the panel — the error is logged only to the browser console.\n\n**Solution:**\n\n1. Open the browser developer console (**F12** > **Console**) and look for errors from the ClickHouse data source.\n2. Verify the ClickHouse user has `SELECT` permission on `system.databases`, `system.tables`, and `system.columns`.\n3. If the ClickHouse server is slow or under heavy load, the schema query may time out. Wait and try again, or optimize the server's load.\n4. Test the data source connection with **Save & test** to confirm basic connectivity.\n\n---\n\nColumn Value Suggestions Time Out on Large Tables\n\n**Symptoms:** Autocomplete suggestions for column values in the query builder are slow or return no results.\n\n**Cause:** The plugin runs `SELECT DISTINCT` queries for each column with `SETTINGS max_execution_time=10`. On tables with millions of rows or high-cardinality columns, this query can time out before completing.\n\n**Solution:**\n\n1. Use simpler filter values that you type manually rather than relying on autocomplete.\n2. Create a materialized view or dictionary with pre-aggregated distinct values for frequently filtered columns.\n3. Add a `clickhouse_adhoc_query` variable with a targeted `SELECT` query instead of relying on schema-driven suggestions.\n\n---\n\n### Query Errors\n\nClickHouse Database Exception\n\n**Error message:** \"DB::Exception: [error details]\"\n\n**Cause:** ClickHouse returned an error while executing your query. Common causes include syntax errors, missing tables, or permission issues.\n\n**Solution:**\n\n1. Review the error message for specific details about the issue.\n2. Verify your SQL syntax is correct for ClickHouse.\n3. Check that referenced tables and columns exist.\n4. Ensure the configured user has permission to access the requested data.\n5. Test the query directly in `clickhouse-client` to isolate the issue.\n\n---\n\nMacro Argument Count Error\n\n**Error messages:**\n\n- \"$\\_\\_timeFilter: expected 1 argument, received [n]\"\n- \"$\\_\\_timeFilter_ms: expected 1 argument, received [n]\"\n- \"$\\_\\_dateFilter: expected 1 argument, received [n]\"\n- \"$\\_\\_dateTimeFilter: expected 2 arguments, received [n]\"\n- \"$\\_\\_timeInterval: expected 1 argument, received [n]\"\n- \"$\\_\\_timeInterval_ms: expected 1 argument, received [n]\"\n\n**Cause:** A Grafana macro was used with the wrong number of arguments.\n\n**Solution:**\n\n1. Check the macro syntax in your query:\n   - `$__timeFilter(column)` - requires 1 argument (the time column)\n   - `$__timeFilter_ms(column)` - requires 1 argument (the time column for millisecond precision)\n   - `$__dateFilter(column)` - requires 1 argument (the date column)\n   - `$__dateTimeFilter(dateColumn, timeColumn)` - requires 2 arguments\n   - `$__timeInterval(column)` - requires 1 argument (the time column)\n   - `$__timeInterval_ms(column)` - requires 1 argument (the time column)\n2. Ensure arguments are separated by commas if multiple are required.\n\n---\n\nSQL Parse Error\n\n**Error message:** Parse error with line and column information\n\n**Cause:** The SQL query contains syntax errors that could not be parsed.\n\n**Solution:**\n\n1. Review the error message for the specific line and column where the error occurred.\n2. Check for common SQL syntax issues like missing commas, unmatched parentheses, or incorrect keywords.\n3. Verify that ClickHouse-specific syntax is being used correctly.\n4. Use the Query Builder mode to construct queries if you're unfamiliar with ClickHouse SQL.\n\n---\n\n### Ad Hoc Filter Errors\n\nUnable to Apply Ad Hoc Filters\n\n**Error message:** \"Unable to apply ad hoc filters. Upgrade ClickHouse to >=22.7 or remove ad hoc filters for the dashboard.\"\n\n**Cause:** Ad hoc filters require ClickHouse version 22.7 or higher, which introduced the `additional_table_filters` setting.\n\n**Solution:**\n\n1. Upgrade your ClickHouse server to version 22.7 or higher.\n2. Alternatively, remove the ad hoc filter variable from your dashboard.\n3. Use regular template variables as a workaround if upgrading is not possible.\n\n---\n\nFailed to Get Table from Ad Hoc Query\n\n**Error message:** \"Failed to get table from adhoc query.\"\n\n**Cause:** The plugin could not determine which table to apply ad hoc filters to from the query.\n\n**Solution:**\n\n1. Ensure your query contains a valid `FROM` clause with a table name.\n2. If using a complex query with subqueries or CTEs, consider using a simpler query structure.\n3. Explicitly specify the table in the ad hoc filter configuration variable.\n\n---\n\nAd Hoc Filters Produce Incorrect or Missing Results\n\n**Symptoms:**\n\n- Filters on column names that contain dots (for example, `raw.log.CONTEXT.subscriber`) are truncated to just the first segment (for example, `raw`), producing invalid or overly broad filters.\n- Filters silently stop being applied when the query contains a complex multi-condition `WHERE` clause. The `SETTINGS additional_table_filters` injection stops working entirely, and no error is shown.\n\n**Solution:**\n\n1. **Upgrade to plugin v4.12.0 or later.** Both issues were fixed in v4.12.0:\n   - Column names with dots are now handled correctly ([#1481](https://github.com/grafana/clickhouse-datasource/pull/1481)).\n   - You can now manually control where ad hoc filters are placed in a query, which prevents silent injection failures in complex `WHERE` clauses ([#1488](https://github.com/grafana/clickhouse-datasource/pull/1488)).\n2. **Manual filter placement (v4.12.0+):** If you have a complex query where automatic filter injection fails, use the `$__adHocFilters('table_name')` macro to explicitly specify where filters are applied. Place it in the `SETTINGS` clause. For details and examples, see [Apply ad hoc filters manually](/docs/plugins/grafana-clickhouse-datasource/<CLICKHOUSE_PLUGIN_VERSION>/template-variables/#apply-ad-hoc-filters-manually-with-__adHocFilters).\n3. **Older plugin versions:** If you cannot upgrade, avoid column names with dots in ad hoc filters (use a ClickHouse [alias](https://clickhouse.com/docs/en/sql-reference/statements/create/table#alias) to flatten nested paths), and simplify complex `WHERE` clauses or move them into a subquery.\n\n---\n\nInvalid Ad Hoc Filter\n\n**Error message:** \"Invalid adhoc filter will be ignored: [filter details]\"\n\n**Cause:** An ad hoc filter is missing required fields (key, operator, or value).\n\n**Solution:**\n\n1. Verify that all ad hoc filters have a key (column name), operator, and value specified.\n2. Check the filter configuration in your dashboard variables.\n3. Remove any incomplete filter definitions.\n\n---\n\n### Log Context Errors\n\nMissing Query for Log Context\n\n**Error message:** \"Missing query for log context\"\n\n**Cause:** The log context feature was invoked without a valid query.\n\n**Solution:**\n\n1. Ensure you're using the log context feature with a valid logs query.\n2. Verify that the query is using the Builder editor mode.\n\n---\n\nMissing Log Context Options\n\n**Error message:** \"Missing log context options for query\"\n\n**Cause:** Required options for log context (direction or limit) are missing.\n\n**Solution:**\n\n1. This is typically an internal error. Try refreshing the page.\n2. If the issue persists, report it as a bug.\n\n---\n\nLog Context Only Works for Builder Queries\n\n**Error message:** \"Log context feature only works for builder queries\"\n\n**Cause:** The log context feature was invoked on a SQL editor query instead of a Builder query.\n\n**Solution:**\n\n1. Switch your query from **SQL Editor** mode to **Builder** mode.\n2. Configure your logs query using the Builder interface.\n\n---\n\nMissing Time Column for Log Context\n\n**Error message:** \"Missing time column for log context\"\n\n**Cause:** The query doesn't have a time column configured, which is required for log context.\n\n**Solution:**\n\n1. In the Query Builder, ensure you've selected a column with the **Time** hint.\n2. Verify that your logs table has a timestamp column and it's properly configured.\n\n---\n\nUnable to Match Log Context Columns\n\n**Error message:** \"Unable to match any log context columns\"\n\n**Cause:** None of the configured context columns could be matched from the current log row's data frame.\n\n**Solution:**\n\n1. Verify that the **Context Columns** are configured in the data source settings under Logs configuration.\n2. Ensure the configured context column names match the actual column names in your query results.\n3. Check that the context columns are included in your SELECT statement.\n\n---\n\n### Proxy and Private Data Connect (PDC) Errors\n\nUnable to Cast SOCKS Proxy Dialer\n\n**Error message:** \"unable to cast SOCKS proxy dialer to context proxy dialer\"\n\n**Cause:** There was an issue initializing the secure SOCKS proxy connection for Private Data Connect (PDC).\n\n**Solution:**\n\n1. Verify your PDC configuration is correct.\n2. Check that the SOCKS proxy is properly configured and accessible.\n3. Review Grafana server logs for more detailed error information.\n4. Ensure your Grafana version supports the PDC feature.\n\n---\n\nPDC Connection Fails with No Agent Logs\n\n**Error message:** \"check PDC agent logs\" (but no relevant logs appear in the PDC agent pod)\n\n**Cause:** The PDC agent accepted the connection request but could not forward it to the target database. This commonly happens when the agent's authentication token has expired or become stale, or when the agent pod was restarted without refreshing credentials.\n\n**Solution:**\n\n1. Restart the PDC agent pod to force a fresh token handshake.\n2. If using Kubernetes, delete the pod and let the deployment recreate it:\n   ```bash\n   kubectl delete pod <pdc-agent-pod-name> -n <namespace>\n   ```\n3. Regenerate or refresh the PDC agent token in the Grafana Cloud portal, then redeploy the agent with the new token.\n4. After restarting, verify the agent logs show a successful registration message before retrying the data source connection.\n5. Confirm that the PDC agent can reach the ClickHouse server on the required port from within its network.\n\n---\n\nAlerting Times Out via PDC While Dashboards Work\n\n**Error message:** \"i/o timeout\", \"datasourceError\", or alert rule evaluations fail while dashboard queries using the same data source succeed.\n\n**Cause:** Alert rule evaluation uses a different backend code path than dashboard queries. In some configurations, the PDC connection is available for interactive queries but the alerting backend cannot establish or maintain the tunneled connection, causing timeouts specifically for alert evaluations.\n\n**Solution:**\n\n1. Ensure you are using the **Grafana ClickHouse plugin** (`grafana-clickhouse-datasource`), not a community or third-party ClickHouse plugin. The Grafana plugin has better PDC compatibility for backend operations like alerting.\n2. Upgrade to the latest plugin version. PDC-related alerting fixes have been included in recent releases.\n3. Verify the PDC agent is healthy and not silently disconnected (see [PDC Connection Fails with No Agent Logs](#pdc-connection-fails-with-no-agent-logs) above).\n4. Check the Grafana alerting logs for detailed timeout or connection errors by filtering for the data source name or UID.\n\n---\n\nStale PDC Token — Metrics Suddenly Lost\n\n**Symptoms:** ClickHouse dashboards that were previously working stop loading data. The PDC agent pod appears to be running, but all queries return errors or empty results. No obvious error is shown in Grafana.\n\n**Cause:** The PDC agent's authentication token has expired. The agent pod continues running but can no longer relay connections to Grafana Cloud. This can happen silently, with no alerts from the agent itself.\n\n**Solution:**\n\n1. Restart the PDC agent pod to trigger a fresh token handshake:\n   ```bash\n   kubectl delete pod <pdc-agent-pod-name> -n <namespace>\n   ```\n2. If restarting doesn't resolve it, regenerate the PDC agent token in the Grafana Cloud portal and redeploy the agent with the new token.\n3. After the agent is back, click **Save & test** on the data source to confirm connectivity.\n4. To prevent recurrence, set up monitoring on the PDC agent pod — for example, a liveness probe or periodic health check that verifies the agent can relay a test query.\n\n---\n\n### Header Parsing Errors\n\nCouldn't Parse Message as Args\n\n**Error message:** \"Couldn't parse message as args\"\n\n**Cause:** The plugin could not parse the forwarded headers message.\n\n**Solution:**\n\n1. This is typically an internal error related to header forwarding.\n2. Check if **Forward Grafana Headers** is enabled and configured correctly.\n3. Review Grafana server logs for more details.\n\n---\n\nCouldn't Parse Grafana HTTP Headers\n\n**Error message:** \"Couldn't parse grafana HTTP headers\"\n\n**Cause:** The Grafana HTTP headers could not be parsed from the request.\n\n**Solution:**\n\n1. Verify the header forwarding configuration.\n2. Check that custom HTTP headers are properly formatted.\n3. Review the data source configuration for any malformed header entries.\n\n---\n\n### Data Display Issues\n\nTimestamp Millisecond Precision Lost\n\n**Symptoms:** ClickHouse `DateTime64` timestamps with millisecond (or higher) precision display in Grafana with only second-level granularity. Milliseconds are truncated or shown as `.000` in both Explore and dashboard panels.\n\n**Cause:** This is a known Grafana platform limitation — Grafana's default time formatter displays timestamps at second precision regardless of the underlying data. The ClickHouse plugin returns the full-precision value, but Grafana's display layer truncates it.\n\n**Workaround for dashboards:**\n\n1. Add a [Convert field type](https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/transform-data/#convert-field-type) transformation to the panel.\n2. Set the time column's format to a pattern that includes milliseconds, for example `YYYY-MM-DD HH:mm:ss.SSS`.\n\n**Workaround for Explore:**\n\nThere is currently no transformation support in Explore. As an alternative, you can cast the timestamp to a string with millisecond precision directly in the SQL query:\n\n```sql\nSELECT\n  formatDateTime(timestamp, '%Y-%m-%d %H:%i:%S') || '.' || toString(toUnixTimestamp64Milli(timestamp) % 1000) AS timestamp_ms,\n  message\nFROM my_table\nWHERE $__timeFilter(timestamp)\n```\n\n{{< admonition type=\"note\" >}}\nThis is a Grafana platform limitation, not specific to the ClickHouse plugin. A feature request for native sub-second display has been filed with the Grafana team.\n{{< /admonition >}}\n\n---\n\n### Upgrade and Compatibility Issues\n\nv3 to v4 Migration Problems\n\n**Symptoms:** After upgrading from plugin v3 to v4, data source settings appear to be missing (blank host, no timeout), or saved dashboard queries no longer load in the query editor.\n\n**Cause:** Plugin v4 renamed several configuration fields (`server` to `host`, `timeout` to `dialTimeout`) and restructured the query model (the `queryType` field changed from `sql`/`builder` to the new `editorType` format). The plugin includes automatic migration logic, but in some cases — especially with provisioned data sources — the migration may not run until the configuration page is opened.\n\n**Solution:**\n\n1. Open the data source configuration page in Grafana, then click **Save & test**. This triggers the frontend migration that copies v3 field values to v4 fields.\n2. If provisioning via YAML or Terraform, update the field names manually:\n   - `server` → `host`\n   - `timeout` → `dialTimeout`\n3. For dashboard queries that fail to load, open the dashboard and re-save it. The plugin automatically migrates v3 query formats to v4 when the query editor loads.\n4. If individual panels still show errors, switch to the **SQL Editor** tab, verify the query, and re-save the panel.\n\n---\n\nLog Volume Not Showing in SQL Editor\n\n**Symptoms:** The log volume histogram does not appear above log results when using the SQL editor in Explore. It works in the query builder.\n\n**Cause:** Log volume support for SQL editor queries requires **Grafana 12.4.0 or later**. On older Grafana versions, log volume is only available for queries built with the query builder.\n\n**Solution:**\n\n1. Upgrade Grafana to version 12.4.0 or later.\n2. Alternatively, switch the query to the **Builder** editor mode, which supports log volume on older Grafana versions.\n\n---\n\nConnection Pool Saturation (Sudden Slowness)\n\n**Symptoms:** Queries become progressively slower or time out under concurrent load, even though individual queries run quickly when tested in isolation. The ClickHouse server itself is not overloaded.\n\n**Cause:** The plugin uses a connection pool with default limits: **50 max open connections**, **25 max idle connections**, and **5-minute connection lifetime**. When many dashboard panels, alert rules, or concurrent users exhaust the pool, new queries queue until a connection becomes available.\n\n**Solution:**\n\n1. Reduce the number of concurrent queries by consolidating dashboard panels or staggering alert evaluation groups.\n2. Increase the connection pool limits by adding custom settings in the data source provisioning configuration:\n   ```yaml\n   jsonData:\n     maxOpenConns: \"100\"\n     maxIdleConns: \"50\"\n     connMaxLifetime: \"10\"\n   ```\n3. Monitor the ClickHouse server's `system.metrics` table (`CurrentMetric_TCPConnection`) to see whether the connection count from Grafana is approaching the server-side limit.\n4. If using HTTP protocol, check whether a reverse proxy between Grafana and ClickHouse has its own connection limits.\n\n---\n\n### Getting More Help\n\nIf you continue to experience issues after trying the solutions in this guide:\n\n1. Check the [ClickHouse documentation](https://clickhouse.com/docs) for database-specific issues.\n2. Review the Grafana server logs for more detailed error messages.\n3. Search or create issues in the [grafana-clickhouse-datasource GitHub repository](https://github.com/grafana/clickhouse-datasource).\n4. Visit the [Grafana Community forums](https://community.grafana.com/) for community support.\n"
  },
  {
    "path": "docs/variables.mk",
    "content": "# List of projects to provide to the make-docs script.\n# Format is PROJECT[:[VERSION][:[REPOSITORY][:[DIRECTORY]]]]\n# The following PROJECTS value mounts content for the \"clickhouse-datasource\" project, at the \"latest\" version, which is the default if not explicitly set.\n# This results in the content being served at /docs/clickhouse-datasource/latest/.\n# The source of the content is the current repository which is determined by the name of the parent directory of the git root.\n# This overrides the default behavior of assuming the repository directory is the same as the project name.\nPROJECTS := grafana-clickhouse-datasource::$(notdir $(basename $(shell git rev-parse --show-toplevel)))\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import { defineConfig } from 'eslint/config';\nimport baseConfig from './.config/eslint.config.mjs';\n\nexport default defineConfig([\n  {\n    ignores: [\n      '**/logs',\n      '**/*.log',\n      '**/npm-debug.log*',\n      '**/yarn-debug.log*',\n      '**/yarn-error.log*',\n      '**/node_modules/',\n      '**/pids',\n      '**/*.pid',\n      '**/*.seed',\n      '**/*.pid.lock',\n      '**/lib-cov',\n      '**/coverage',\n      '**/dist/',\n      '**/artifacts/',\n      '**/work/',\n      '**/ci/',\n      '**/e2e-results/',\n      '**/test_summary.json',\n      '**/.idea',\n      'pkg/__debug_bin',\n      '**/.DS_Store',\n      '**/.eslintcache',\n      'test-results/',\n      'playwright-report/',\n      'blob-report/',\n      'playwright/.cache/',\n      'playwright/.auth/',\n    ],\n  },\n  ...baseConfig,\n]);\n"
  },
  {
    "path": "gen-db-dashboards.js",
    "content": "/**\n * This script will code-gen a Grafana dashboard using the \"system.dashboards\" table in a locally running ClickHouse instance.\n */\n\nconst fs = require('fs/promises');\nconst pluginVersion = require('./package.json').version;\n\nconst OUTPUT_FILE = './src/dashboards/system-dashboards.json';\nconst CLICKHOUSE_ADDRESS = 'localhost:8123';\n\nasync function fetchDashboardsFromClickHouse() {\n  const query = 'SELECT * FROM system.dashboards FORMAT JSON';\n  const url = `http://${CLICKHOUSE_ADDRESS}/?query=${query}`;\n\n  let response;\n  try {\n    response = await fetch(url, {\n      method: 'GET',\n      headers: {\n        Accept: 'application/json',\n      },\n    });\n  } catch (err) {\n    throw new Error('failed to fetch dashboards from ClickHouse HTTP: ' + err);\n  }\n\n  const queryResponse = await response.json();\n  return queryResponse.data;\n}\n\nfunction generatePanels(clickHouseDashboards) {\n  const panelWidth = 12;\n  const panelHeight = 8;\n\n  let lastRowName = '';\n  const panels = [];\n  for (let i = 0; i < clickHouseDashboards.length; i++) {\n    const id = i + 1;\n    const dashboard = clickHouseDashboards[i];\n    const { dashboard: dashboardTitle, title: name, query } = dashboard;\n    const positionX = i % 2 === 0 ? 0 : panelWidth;\n    const positionY = panelHeight * Math.floor(i / 2);\n    const panel = generatePanel(id, name, query, panelWidth, panelHeight, positionX, positionY);\n\n    if (lastRowName !== dashboardTitle) {\n      const rowPanel = generateRowPanel(dashboardTitle, positionY);\n      panels.push(rowPanel);\n      lastRowName = dashboardTitle;\n    }\n\n    panels.push(panel);\n  }\n\n  return panels;\n}\n\nfunction generateDashboard(panels) {\n  return {\n    __inputs: [\n      {\n        name: 'the_datasource',\n        label: 'The Datasource',\n        description: '',\n        type: 'datasource',\n        pluginId: 'grafana-clickhouse-datasource',\n        pluginName: 'ClickHouse',\n      },\n    ],\n    __elements: {},\n    __requires: [\n      {\n        type: 'grafana',\n        id: 'grafana',\n        name: 'Grafana',\n        version: '11.2.0-pre',\n      },\n      {\n        type: 'datasource',\n        id: 'grafana-clickhouse-datasource',\n        name: 'ClickHouse',\n        version: pluginVersion,\n      },\n      {\n        type: 'panel',\n        id: 'timeseries',\n        name: 'Time series',\n        version: '',\n      },\n    ],\n    annotations: {\n      list: [\n        {\n          builtIn: 1,\n          datasource: {\n            type: 'grafana',\n            uid: '-- Grafana --',\n          },\n          enable: true,\n          hide: true,\n          iconColor: 'rgba(0, 211, 255, 1)',\n          name: 'Annotations & Alerts',\n          type: 'dashboard',\n        },\n      ],\n    },\n    description: 'Similar to the monitoring dashboard that is built in to ClickHouse.',\n    editable: true,\n    fiscalYearStartMonth: 0,\n    graphTooltip: 0,\n    id: null,\n    links: [],\n    panels,\n    schemaVersion: 39,\n    tags: [],\n    templating: {\n      list: [],\n    },\n    time: {\n      from: 'now-6h',\n      to: 'now',\n    },\n    timepicker: {},\n    timezone: 'browser',\n    title: 'Advanced ClickHouse Monitoring Dashboard',\n    uid: null,\n    version: 1,\n    weekStart: '',\n  };\n}\n\n// Transform query to fit Grafana variables\nfunction preprocessQuery(rawQuery) {\n  rawQuery = rawQuery.replaceAll(\n    'event_date >= toDate(now() - {seconds:UInt32}) AND event_time >= now() - {seconds:UInt32}',\n    '$__dateFilter(event_date) AND $__timeFilter(event_time)'\n  );\n  rawQuery = rawQuery.replaceAll(\n    'event_date >= toDate(now() - {seconds:UInt32})\\n    AND event_time >= now() - {seconds:UInt32}',\n    '$__dateFilter(event_date) AND $__timeFilter(event_time)'\n  );\n  rawQuery = rawQuery.replaceAll('{rounding:UInt32}', '$__interval_s');\n  rawQuery = rawQuery.replaceAll('::INT AS t', ' AS t');\n  return rawQuery;\n}\n\nfunction generatePanel(id, name, rawQuery, width, height, x, y) {\n  const rawSql = preprocessQuery(rawQuery);\n\n  return {\n    datasource: {\n      type: 'grafana-clickhouse-datasource',\n      uid: '${the_datasource}',\n    },\n    fieldConfig: {\n      defaults: {\n        color: {\n          mode: 'palette-classic',\n        },\n        custom: {\n          axisBorderShow: false,\n          axisCenteredZero: false,\n          axisColorMode: 'text',\n          axisLabel: '',\n          axisPlacement: 'auto',\n          barAlignment: 0,\n          drawStyle: 'line',\n          fillOpacity: 0,\n          gradientMode: 'none',\n          hideFrom: {\n            legend: false,\n            tooltip: false,\n            viz: false,\n          },\n          insertNulls: false,\n          lineInterpolation: 'linear',\n          lineWidth: 1,\n          pointSize: 5,\n          scaleDistribution: {\n            type: 'linear',\n          },\n          showPoints: 'auto',\n          spanNulls: false,\n          stacking: {\n            group: 'A',\n            mode: 'none',\n          },\n          thresholdsStyle: {\n            mode: 'off',\n          },\n        },\n        mappings: [],\n        thresholds: {\n          mode: 'absolute',\n          steps: [\n            {\n              color: 'green',\n              value: null,\n            },\n            {\n              color: 'red',\n              value: 80,\n            },\n          ],\n        },\n      },\n      overrides: [],\n    },\n    gridPos: {\n      h: height,\n      w: width,\n      x: x,\n      y: y,\n    },\n    id,\n    options: {\n      legend: {\n        calcs: [],\n        displayMode: 'list',\n        placement: 'bottom',\n        showLegend: true,\n      },\n      tooltip: {\n        mode: 'single',\n        sort: 'none',\n      },\n    },\n    targets: [\n      {\n        datasource: {\n          type: 'grafana-clickhouse-datasource',\n          uid: '${the_datasource}',\n        },\n        editorType: 'sql',\n        format: 0,\n        meta: {\n          builderOptions: {\n            columns: [],\n            database: '',\n            limit: 1000,\n            mode: 'list',\n            queryType: 'table',\n            table: '',\n          },\n        },\n        pluginVersion,\n        queryType: 'timeseries',\n        rawSql,\n        refId: 'A',\n      },\n    ],\n    title: name,\n    type: 'timeseries',\n  };\n}\n\nfunction generateRowPanel(rowName, positionY) {\n  return {\n    collapsed: false,\n    gridPos: {\n      h: 1,\n      w: 24,\n      x: 0,\n      y: positionY,\n    },\n    id: null,\n    panels: [],\n    title: rowName,\n    type: 'row',\n  };\n}\n\nasync function main() {\n  const clickHouseDashboards = await fetchDashboardsFromClickHouse();\n  const panels = generatePanels(clickHouseDashboards);\n  const dashboard = generateDashboard(panels);\n  let fileData = JSON.stringify(dashboard, null, '\\t');\n  fileData += '\\n';\n\n  try {\n    await fs.writeFile(OUTPUT_FILE, fileData, 'utf-8');\n  } catch (err) {\n    throw new Error('failed to write dashboard to file: ' + err);\n  }\n\n  console.log(`Saved dashboard to ${OUTPUT_FILE}`);\n}\nmain().catch(console.error);\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/grafana/clickhouse-datasource\n\ngo 1.26.0\n\nrequire (\n\tgithub.com/ClickHouse/clickhouse-go/v2 v2.45.0\n\tgithub.com/docker/go-units v0.5.0\n\tgithub.com/grafana/grafana-plugin-sdk-go v0.292.0\n\tgithub.com/grafana/sqlds/v5 v5.1.1\n\tgithub.com/moby/moby/api v1.54.2\n\tgithub.com/paulmach/orb v0.13.0\n\tgithub.com/pkg/errors v0.9.1\n\tgithub.com/shopspring/decimal v1.4.0\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/testcontainers/testcontainers-go v0.42.0\n\tgolang.org/x/net v0.53.0\n\tgolang.org/x/sync v0.20.0\n)\n\nrequire (\n\tgithub.com/apache/arrow-go/v18 v18.5.2 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/clipperhouse/displaywidth v0.10.0 // indirect\n\tgithub.com/clipperhouse/uax29/v2 v2.6.0 // indirect\n\tgithub.com/containerd/errdefs v1.0.0 // indirect\n\tgithub.com/containerd/errdefs/pkg v0.3.0 // indirect\n\tgithub.com/containerd/log v0.1.0 // indirect\n\tgithub.com/containerd/platforms v0.2.1 // indirect\n\tgithub.com/distribution/reference v0.6.0 // indirect\n\tgithub.com/ebitengine/purego v0.10.0 // indirect\n\tgithub.com/emicklei/go-restful/v3 v3.11.0 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/fsnotify/fsnotify v1.6.0 // indirect\n\tgithub.com/go-openapi/jsonpointer v0.22.5 // indirect\n\tgithub.com/go-openapi/jsonreference v0.21.5 // indirect\n\tgithub.com/go-openapi/swag v0.23.0 // indirect\n\tgithub.com/go-openapi/swag/jsonname v0.25.5 // indirect\n\tgithub.com/goccy/go-json v0.10.5 // indirect\n\tgithub.com/gogo/googleapis v1.4.1 // indirect\n\tgithub.com/google/gnostic-models v0.7.0 // indirect\n\tgithub.com/grafana/dataplane/sdata v0.0.9 // indirect\n\tgithub.com/grafana/otel-profiling-go v0.5.1 // indirect\n\tgithub.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect\n\tgithub.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect\n\tgithub.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect\n\tgithub.com/huandu/go-clone v1.7.3 // indirect\n\tgithub.com/huandu/go-sqlbuilder v1.39.1 // indirect\n\tgithub.com/huandu/xstrings v1.4.0 // indirect\n\tgithub.com/jaegertracing/jaeger-idl v0.6.0 // indirect\n\tgithub.com/josharian/intern v1.0.0 // indirect\n\tgithub.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.3.0 // indirect\n\tgithub.com/mailru/easyjson v0.7.7 // indirect\n\tgithub.com/moby/docker-image-spec v1.3.1 // indirect\n\tgithub.com/moby/go-archive v0.2.0 // indirect\n\tgithub.com/moby/moby/client v0.4.0 // indirect\n\tgithub.com/moby/sys/user v0.4.0 // indirect\n\tgithub.com/moby/sys/userns v0.1.0 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect\n\tgithub.com/olekukonko/errors v1.2.0 // indirect\n\tgithub.com/olekukonko/ll v0.1.6 // indirect\n\tgithub.com/patrickmn/go-cache v2.1.0+incompatible // indirect\n\tgithub.com/shirou/gopsutil/v4 v4.26.3 // indirect\n\tgithub.com/zeebo/xxh3 v1.1.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect\n\tgo.opentelemetry.io/contrib/samplers/jaegerremote v0.36.0 // indirect\n\tgo.yaml.in/yaml/v2 v2.4.3 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect\n\tk8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 // indirect\n\tsigs.k8s.io/randfill v1.0.0 // indirect\n)\n\nrequire (\n\tdario.cat/mergo v1.0.2 // indirect\n\tgithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect\n\tgithub.com/BurntSushi/toml v1.5.0 // indirect\n\tgithub.com/ClickHouse/ch-go v0.71.0 // indirect\n\tgithub.com/Microsoft/go-winio v0.6.2 // indirect\n\tgithub.com/andybalholm/brotli v1.2.0 // indirect\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/cenkalti/backoff/v4 v4.3.0 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/cheekybits/genny v1.0.0 // indirect\n\tgithub.com/cpuguy83/dockercfg v0.3.2 // indirect\n\tgithub.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/docker/go-connections v0.6.0 // indirect\n\tgithub.com/elazarl/goproxy v1.8.3\n\tgithub.com/fatih/color v1.18.0 // indirect\n\tgithub.com/go-faster/city v1.0.1 // indirect\n\tgithub.com/go-faster/errors v0.7.1 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-ole/go-ole v1.2.6 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgithub.com/google/flatbuffers v25.12.19+incompatible // indirect\n\tgithub.com/google/go-cmp v0.7.0 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/grafana/schemads v0.0.6\n\tgithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect\n\tgithub.com/hashicorp/go-hclog v1.6.3 // indirect\n\tgithub.com/hashicorp/go-plugin v1.7.0 // indirect\n\tgithub.com/hashicorp/yamux v0.1.2 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect\n\tgithub.com/magefile/mage v1.17.1 // indirect\n\tgithub.com/magiconair/properties v1.8.10 // indirect\n\tgithub.com/mattetti/filebuffer v1.0.1 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.19 // indirect\n\tgithub.com/mitchellh/go-homedir v1.1.0 // indirect\n\tgithub.com/mithrandie/csvq v1.18.1 // indirect\n\tgithub.com/mithrandie/csvq-driver v1.7.0 // indirect\n\tgithub.com/mithrandie/go-file/v2 v2.1.0 // indirect\n\tgithub.com/mithrandie/go-text v1.6.0 // indirect\n\tgithub.com/mithrandie/ternary v1.1.1 // indirect\n\tgithub.com/moby/patternmatcher v0.6.1 // indirect\n\tgithub.com/moby/sys/sequential v0.6.0 // indirect\n\tgithub.com/moby/term v0.5.2 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/oklog/run v1.1.0 // indirect\n\tgithub.com/olekukonko/tablewriter v1.1.4 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.1.1 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.25 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect\n\tgithub.com/prometheus/client_golang v1.23.2 // indirect\n\tgithub.com/prometheus/client_model v0.6.2 // indirect\n\tgithub.com/prometheus/common v0.67.5 // indirect\n\tgithub.com/prometheus/procfs v0.16.1 // indirect\n\tgithub.com/russross/blackfriday/v2 v2.1.0 // indirect\n\tgithub.com/segmentio/asm v1.2.1 // indirect\n\tgithub.com/sirupsen/logrus v1.9.4 // indirect\n\tgithub.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a // indirect\n\tgithub.com/tklauser/go-sysconf v0.3.16 // indirect\n\tgithub.com/tklauser/numcpus v0.11.0 // indirect\n\tgithub.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 // indirect\n\tgithub.com/unknwon/com v1.0.1 // indirect\n\tgithub.com/unknwon/log v0.0.0-20200308114134-929b1006e34a // indirect\n\tgithub.com/urfave/cli v1.22.17 // indirect\n\tgithub.com/yusufpapurcu/wmi v1.2.4 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.67.0 // indirect\n\tgo.opentelemetry.io/contrib/propagators/jaeger v1.42.0 // indirect\n\tgo.opentelemetry.io/otel v1.43.0\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.43.0 // indirect\n\tgo.opentelemetry.io/otel/sdk v1.43.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.43.0\n\tgo.opentelemetry.io/proto/otlp v1.10.0 // indirect\n\tgolang.org/x/crypto v0.50.0 // indirect\n\tgolang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect\n\tgolang.org/x/mod v0.34.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/term v0.42.0 // indirect\n\tgolang.org/x/text v0.36.0 // indirect\n\tgolang.org/x/tools v0.43.0 // indirect\n\tgolang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect\n\tgoogle.golang.org/grpc v1.80.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.11 // indirect\n\tgopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=\nfilippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=\nfilippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=\ngithub.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=\ngithub.com/ClickHouse/ch-go v0.71.0 h1:bUdZ/EZj/LcVHsMqaRUP2holqygrPWQKeMjc6nZoyRM=\ngithub.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw=\ngithub.com/ClickHouse/clickhouse-go/v2 v2.45.0 h1:iHt15nA4iYhfde5bDQAcLAat9BAh7B5ksPRNRa4UI7s=\ngithub.com/ClickHouse/clickhouse-go/v2 v2.45.0/go.mod h1:giJfUVlMkcfUEPVfRpt51zZaGEx9i17gCos8gBl392c=\ngithub.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=\ngithub.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=\ngithub.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=\ngithub.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/apache/arrow-go/v18 v18.5.2 h1:3uoHjoaEie5eVsxx/Bt64hKwZx4STb+beAkqKOlq/lY=\ngithub.com/apache/arrow-go/v18 v18.5.2/go.mod h1:yNoizNTT4peTciJ7V01d2EgOkE1d0fQ1vZcFOsVtFsw=\ngithub.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc=\ngithub.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=\ngithub.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=\ngithub.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=\ngithub.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=\ngithub.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=\ngithub.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=\ngithub.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=\ngithub.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos=\ngithub.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=\ngithub.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=\ngithub.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=\ngithub.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=\ngithub.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=\ngithub.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=\ngithub.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=\ngithub.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=\ngithub.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=\ngithub.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=\ngithub.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=\ngithub.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=\ngithub.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=\ngithub.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=\ngithub.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=\ngithub.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=\ngithub.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=\ngithub.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=\ngithub.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=\ngithub.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=\ngithub.com/elazarl/goproxy v1.8.3 h1:XhiZpzW0NvsGOqSv/F3v4+1F29842yYaJNN+In5Fnuc=\ngithub.com/elazarl/goproxy v1.8.3/go.mod h1:b5xm6W48AUHNpRTCvlnd0YVh+JafCCtsLsJZvvNTz+E=\ngithub.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=\ngithub.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=\ngithub.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=\ngithub.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=\ngithub.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=\ngithub.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=\ngithub.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=\ngithub.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=\ngithub.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=\ngithub.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=\ngithub.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=\ngithub.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=\ngithub.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=\ngithub.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=\ngithub.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=\ngithub.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=\ngithub.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=\ngithub.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=\ngithub.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM=\ngithub.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=\ngithub.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=\ngithub.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=\ngithub.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=\ngithub.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=\ngithub.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0=\ngithub.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=\ngithub.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=\ngithub.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=\ngithub.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=\ngithub.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=\ngithub.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6kE/MWfg7s=\ngithub.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU=\ngithub.com/grafana/grafana-plugin-sdk-go v0.292.0 h1:HaFIbBmPX9K+BVsVemid+poDEbja+HJ8VE+6tVnZKLU=\ngithub.com/grafana/grafana-plugin-sdk-go v0.292.0/go.mod h1:lnWyfzENuIU+N2EzivEe9YJob8AAqPl7HBMXMbPyv3k=\ngithub.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=\ngithub.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls=\ngithub.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=\ngithub.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=\ngithub.com/grafana/schemads v0.0.6 h1:Pz0fClDO0wfzb0THzgM8pyxVycbEa7b0ARS7YxfZ4RU=\ngithub.com/grafana/schemads v0.0.6/go.mod h1:4BnVcbwJT6xqyge9nB0GFbfzc6Aw3RzXD5TaZIOnPqY=\ngithub.com/grafana/sqlds/v5 v5.1.1 h1:QEZayyzZZZjDnfMX/f5k7oLehOBH7X1MMnpgn+yDkkI=\ngithub.com/grafana/sqlds/v5 v5.1.1/go.mod h1:fUt31TRrHrBZ6lyaHZyDqSfeouuSXV6PeBgrNFuZVmo=\ngithub.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=\ngithub.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=\ngithub.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns=\ngithub.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=\ngithub.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=\ngithub.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=\ngithub.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=\ngithub.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=\ngithub.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=\ngithub.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=\ngithub.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U=\ngithub.com/huandu/go-assert v1.1.6 h1:oaAfYxq9KNDi9qswn/6aE0EydfxSa+tWZC1KabNitYs=\ngithub.com/huandu/go-assert v1.1.6/go.mod h1:JuIfbmYG9ykwvuxoJ3V8TB5QP+3+ajIA54Y44TmkMxs=\ngithub.com/huandu/go-clone v1.7.3 h1:rtQODA+ABThEn6J5LBTppJfKmZy/FwfpMUWa8d01TTQ=\ngithub.com/huandu/go-clone v1.7.3/go.mod h1:ReGivhG6op3GYr+UY3lS6mxjKp7MIGTknuU5TbTVaXE=\ngithub.com/huandu/go-sqlbuilder v1.39.1 h1:uUaj41yLNTQBe7ojNF6Im1RPbHCN4zCjMRySTEC2ooI=\ngithub.com/huandu/go-sqlbuilder v1.39.1/go.mod h1:zdONH67liL+/TvoUMwnZP/sUYGSSvHh9psLe/HpXn8E=\ngithub.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=\ngithub.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=\ngithub.com/jaegertracing/jaeger-idl v0.6.0 h1:LOVQfVby9ywdMPI9n3hMwKbyLVV3BL1XH2QqsP5KTMk=\ngithub.com/jaegertracing/jaeger-idl v0.6.0/go.mod h1:mpW0lZfG907/+o5w5OlnNnig7nHJGT3SfKmRqC42HGQ=\ngithub.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=\ngithub.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=\ngithub.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 h1:SwcnSwBR7X/5EHJQlXBockkJVIMRVt5yKaesBPMtyZQ=\ngithub.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6/go.mod h1:WrYiIuiXUMIvTDAQw97C+9l0CnBmCcvosPjN3XDqS/o=\ngithub.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=\ngithub.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=\ngithub.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4=\ngithub.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=\ngithub.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=\ngithub.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=\ngithub.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=\ngithub.com/magefile/mage v1.17.1 h1:F1d2lnLSlbQDM0Plq6Ac4NtaHxkxTK8t5nrMY9SkoNA=\ngithub.com/magefile/mage v1.17.1/go.mod h1:Yj51kqllmsgFpvvSzgrZPK9WtluG3kUhFaBUVLo4feA=\ngithub.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=\ngithub.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=\ngithub.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=\ngithub.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\ngithub.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM=\ngithub.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs=\ngithub.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=\ngithub.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=\ngithub.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs=\ngithub.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=\ngithub.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI=\ngithub.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=\ngithub.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=\ngithub.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=\ngithub.com/mithrandie/csvq v1.18.1 h1:f7NB2scbb7xx2ffPduJ2VtZ85RpWXfvanYskAkGlCBU=\ngithub.com/mithrandie/csvq v1.18.1/go.mod h1:MRJj7AtcXfk7jhNGxLuJGP3LORmh4lpiPWxQ7VyCRn8=\ngithub.com/mithrandie/csvq-driver v1.7.0 h1:ejiavXNWwTPMyr3fJFnhcqd1L1cYudA0foQy9cZrqhw=\ngithub.com/mithrandie/csvq-driver v1.7.0/go.mod h1:HcN3xL9UCJnBYA/AIQOOB/KlyfXAiYr5yxDmiwrGk5o=\ngithub.com/mithrandie/go-file/v2 v2.1.0 h1:XA5Tl+73GXMDvgwSE3Sg0uC5FkLr3hnXs8SpUas0hyg=\ngithub.com/mithrandie/go-file/v2 v2.1.0/go.mod h1:9YtTF3Xo59GqC1Pxw6KyGVcM/qubAMlxVsqI/u9r++c=\ngithub.com/mithrandie/go-text v1.6.0 h1:8gOXTMPbMY8DJbKMTv8kHhADcJlDWXqS/YQH4SyWO6s=\ngithub.com/mithrandie/go-text v1.6.0/go.mod h1:xCgj1xiNbI/d4xA9sLVvXkjh5B2tNx2ZT2/3rpmh8to=\ngithub.com/mithrandie/ternary v1.1.1 h1:k/joD6UGVYxHixYmSR8EGgDFNONBMqyD373xT4QRdC4=\ngithub.com/mithrandie/ternary v1.1.1/go.mod h1:0D9Ba3+09K2TdSZO7/bFCC0GjSXetCvYuYq0u8FY/1g=\ngithub.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=\ngithub.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=\ngithub.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=\ngithub.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=\ngithub.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=\ngithub.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=\ngithub.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw=\ngithub.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g=\ngithub.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=\ngithub.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=\ngithub.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=\ngithub.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=\ngithub.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=\ngithub.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=\ngithub.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=\ngithub.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=\ngithub.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=\ngithub.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=\ngithub.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=\ngithub.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=\ngithub.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=\ngithub.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=\ngithub.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=\ngithub.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA=\ngithub.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88=\ngithub.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=\ngithub.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=\ngithub.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=\ngithub.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=\ngithub.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=\ngithub.com/paulmach/orb v0.13.0 h1:r7n7mQGGF+cj/CbcivEj9J3HGK+XR+yXnvzRdq9saIw=\ngithub.com/paulmach/orb v0.13.0/go.mod h1:6scRWINywA2Jf05dcjOfLfxrUIMECvTSG2MVbRLxu/k=\ngithub.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=\ngithub.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=\ngithub.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=\ngithub.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=\ngithub.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=\ngithub.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=\ngithub.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=\ngithub.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=\ngithub.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=\ngithub.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=\ngithub.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=\ngithub.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=\ngithub.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=\ngithub.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=\ngithub.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=\ngithub.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY=\ngithub.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=\ngithub.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=\ngithub.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs=\ngithub.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=\ngithub.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY=\ngithub.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30=\ngithub.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=\ngithub.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=\ngithub.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=\ngithub.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=\ngithub.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 h1:aVGB3YnaS/JNfOW3tiHIlmNmTDg618va+eT0mVomgyI=\ngithub.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8/go.mod h1:fVle4kNr08ydeohzYafr20oZzbAkhQT39gKK/pFQ5M4=\ngithub.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs=\ngithub.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=\ngithub.com/unknwon/log v0.0.0-20150304194804-e617c87089d3/go.mod h1:1xEUf2abjfP92w2GZTV+GgaRxXErwRXcClbUwrNJffU=\ngithub.com/unknwon/log v0.0.0-20200308114134-929b1006e34a h1:vcrhXnj9g9PIE+cmZgaPSwOyJ8MAQTRmsgGrB0x5rF4=\ngithub.com/unknwon/log v0.0.0-20200308114134-929b1006e34a/go.mod h1:1xEUf2abjfP92w2GZTV+GgaRxXErwRXcClbUwrNJffU=\ngithub.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=\ngithub.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=\ngithub.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=\ngithub.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=\ngithub.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=\ngithub.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=\ngithub.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=\ngithub.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=\ngithub.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=\ngithub.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=\ngo.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.67.0 h1:c9r/G1CSw4dPI1jaNNG9RnQP+q4SvZnHciDQJVIvchU=\ngo.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.67.0/go.mod h1:gO9smoZe9KnZcJCqcB0lMmQ4Z5VEifYmjMTpnwtTSuQ=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=\ngo.opentelemetry.io/contrib/propagators/jaeger v1.42.0 h1:jP8unWI6q5kcb3gpGLjKDGaUa+JW+nHKWvpS/q+YuWA=\ngo.opentelemetry.io/contrib/propagators/jaeger v1.42.0/go.mod h1:xd89e/pUyPatUP1C4z1UknD9jHptESO99tWyvd4mWD4=\ngo.opentelemetry.io/contrib/samplers/jaegerremote v0.36.0 h1:h8kHGv9+VIiJbQ2Qx6BbORZwcvVnd0le/SFK8Vom0bA=\ngo.opentelemetry.io/contrib/samplers/jaegerremote v0.36.0/go.mod h1:tjrgaYHDx+1CmTk5YzNAUCbLX1ZrjrsogXBQHaVf7rI=\ngo.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=\ngo.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=\ngo.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk=\ngo.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=\ngo.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=\ngo.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=\ngo.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=\ngo.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=\ngo.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=\ngo.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=\ngo.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=\ngo.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=\ngo.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=\ngo.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=\ngo.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=\ngo.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=\ngo.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=\ngolang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=\ngolang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191020152052-9984515f0562/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c h1:6a8FdnNk6bTXBjR4AGKFgUKuo+7GnR3FX5L7CbveeZc=\ngolang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw=\ngolang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=\ngolang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=\ngolang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=\ngolang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=\ngonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=\ngonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=\ngoogle.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=\ngoogle.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/fsnotify/fsnotify.v1 v1.4.7 h1:XNNYLJHt73EyYiCZi6+xjupS9CpvmiDgjPTAjrBlQbo=\ngopkg.in/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=\ngotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=\nk8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 h1:HhDfevmPS+OalTjQRKbTHppRIz01AWi8s45TMXStgYY=\nk8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=\nk8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=\nk8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=\npgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=\npgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=\nsigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=\nsigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=\nsigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=\nsigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=\n"
  },
  {
    "path": "jest-runner-serial.js",
    "content": "const JestRunner = require('jest-runner');\n\nclass SerialJestRunner extends JestRunner {\n  constructor(...args) {\n    super(...args);\n    this.isSerial = true;\n    // this.maxConcurrency = 1\n  }\n}\n\nmodule.exports = SerialJestRunner;\n"
  },
  {
    "path": "jest-setup.js",
    "content": "// Jest setup provided by Grafana scaffolding\nimport './.config/jest-setup';\nimport { TextEncoder, TextDecoder } from 'util';\n\nglobal.TextEncoder = TextEncoder;\nglobal.TextDecoder = TextDecoder;\n\nconst mockIntersectionObserver = jest.fn().mockImplementation((arg) => ({\n  observe: jest.fn().mockImplementation((elem) => {\n    arg([{ target: elem, isIntersecting: true }]);\n  }),\n  unobserve: jest.fn(),\n  disconnect: jest.fn(),\n}));\n\nglobal.IntersectionObserver = mockIntersectionObserver;\n"
  },
  {
    "path": "jest.config.js",
    "content": "// force timezone to UTC to allow tests to work regardless of local timezone\n// generally used by snapshots, but can affect specific tests\nprocess.env.TZ = 'UTC';\n\nmodule.exports = {\n  // Jest configuration provided by Grafana scaffolding\n  ...require('./.config/jest.config'),\n};\n"
  },
  {
    "path": "otel-semconv.yaml",
    "content": "# otel-semconv.yaml — generated by otel-scout on 2026-03-19\n\nplugin_id: grafana-clickhouse-datasource\nrepo: grafana/clickhouse-datasource\ndomain: db\nspans:\n    - name: query_data\n      kind: client\n      description: Created when the plugin receives a QueryData request and dispatches queries to ClickHouse via the sqlds framework\n      required_attributes:\n        - name: db.system\n          type: string\n          semconv_ref: https://opentelemetry.io/docs/specs/semconv/attributes-registry/db/#db-system\n          description: The database management system being queried\n          example: clickhouse\n        - name: grafana.plugin.id\n          type: string\n          semconv_ref: https://opentelemetry.io/docs/specs/semconv/attributes-registry/\n          description: The Grafana plugin identifier\n          example: grafana-clickhouse-datasource\n      optional_attributes:\n        - name: db.statement\n          type: string\n          semconv_ref: https://opentelemetry.io/docs/specs/semconv/attributes-registry/db/#db-statement\n          description: The raw SQL query string sent to ClickHouse after macro interpolation\n          example: SELECT timestamp, value FROM metrics WHERE timestamp > '2024-01-01'\n        - name: db.operation\n          type: string\n          semconv_ref: https://opentelemetry.io/docs/specs/semconv/attributes-registry/db/#db-operation\n          description: The type of database operation being performed\n          example: SELECT\n        - name: datasource.uid\n          type: string\n          semconv_ref: https://opentelemetry.io/docs/specs/semconv/attributes-registry/\n          description: The unique identifier of the Grafana datasource instance\n          example: abc123\n        - name: request.query_count\n          type: int\n          semconv_ref: https://opentelemetry.io/docs/specs/semconv/attributes-registry/\n          description: The number of queries in the QueryData request\n          example: \"3\"\n        - name: status\n          type: string\n          semconv_ref: https://opentelemetry.io/docs/specs/semconv/attributes-registry/\n          description: The outcome status of the request (ok, error, cancelled)\n          example: ok\n        - name: error.source\n          type: string\n          semconv_ref: https://opentelemetry.io/docs/specs/semconv/attributes-registry/\n          description: The source of an error (plugin or downstream)\n          example: downstream\n    - name: db.query\n      kind: client\n      description: Created for each individual SQL query execution against the ClickHouse database via database/sql\n      required_attributes:\n        - name: db.system\n          type: string\n          semconv_ref: https://opentelemetry.io/docs/specs/semconv/attributes-registry/db/#db-system\n          description: The database management system being queried\n          example: clickhouse\n      optional_attributes:\n        - name: db.statement\n          type: string\n          semconv_ref: https://opentelemetry.io/docs/specs/semconv/attributes-registry/db/#db-statement\n          description: The SQL query string executed against ClickHouse\n          example: SELECT timestamp, value FROM metrics WHERE timestamp > ?\n        - name: db.operation\n          type: string\n          semconv_ref: https://opentelemetry.io/docs/specs/semconv/attributes-registry/db/#db-operation\n          description: The type of database operation (SELECT, INSERT, etc.)\n          example: SELECT\n        - name: db.query.ref_id\n          type: string\n          semconv_ref: https://opentelemetry.io/docs/specs/semconv/attributes-registry/\n          description: The Grafana query reference ID associated with this database query\n          example: A\n        - name: db.query.type\n          type: string\n          semconv_ref: https://opentelemetry.io/docs/specs/semconv/attributes-registry/\n          description: The type of the query (e.g., table, time series, logs)\n          example: time_series\n        - name: db.row_count\n          type: int\n          semconv_ref: https://opentelemetry.io/docs/specs/semconv/attributes-registry/\n          description: The number of rows returned by the query\n          example: \"1500\"\n        - name: db.query.duration_ms\n          type: float64\n          semconv_ref: https://opentelemetry.io/docs/specs/semconv/attributes-registry/\n          description: The duration of the query execution in milliseconds\n          example: \"245.3\"\n    - name: db.connect\n      kind: client\n      description: Created when a new database connection is established to the ClickHouse server, including reconnection attempts\n      required_attributes:\n        - name: db.system\n          type: string\n          semconv_ref: https://opentelemetry.io/docs/specs/semconv/attributes-registry/db/#db-system\n          description: The database management system being connected to\n          example: clickhouse\n      optional_attributes:\n        - name: server.address\n          type: string\n          semconv_ref: https://opentelemetry.io/docs/specs/semconv/attributes-registry/server/\n          description: The hostname or IP address of the ClickHouse server\n          example: clickhouse.example.com\n        - name: server.port\n          type: int\n          semconv_ref: https://opentelemetry.io/docs/specs/semconv/attributes-registry/server/\n          description: The port number of the ClickHouse server\n          example: \"9000\"\n        - name: db.name\n          type: string\n          semconv_ref: https://opentelemetry.io/docs/specs/semconv/attributes-registry/db/#db-name\n          description: The name of the database being connected to\n          example: default\n        - name: db.connection.is_reconnect\n          type: bool\n          semconv_ref: https://opentelemetry.io/docs/specs/semconv/attributes-registry/\n          description: Whether this connection is a reconnection attempt after a failed query\n          example: \"true\"\n        - name: network.transport\n          type: string\n          semconv_ref: https://opentelemetry.io/docs/specs/semconv/attributes-registry/network/\n          description: The transport protocol used (tcp, http)\n          example: tcp\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"clickhouse-datasource\",\n  \"version\": \"4.17.1\",\n  \"description\": \"Clickhouse Datasource\",\n  \"engines\": {\n    \"node\": \">=20\"\n  },\n  \"scripts\": {\n    \"build\": \"webpack -c ./.config/webpack/webpack.config.ts --env production\",\n    \"dev\": \"webpack -w -c ./.config/webpack/webpack.config.ts --env development\",\n    \"e2e:report\": \"npx playwright show-report\",\n    \"e2e:ui\": \"npx playwright test --ui\",\n    \"e2e\": \"playwright test\",\n    \"gen-dashboards\": \"node ./gen-db-dashboards\",\n    \"lint:fix\": \"npm run lint -- --fix && prettier --write --list-different .\",\n    \"lint\": \"eslint --cache .\",\n    \"prettier:check\": \"npx prettier --check --ignore-path .prettierignore --list-different=false --log-level=warn \\\"**/*.{ts,tsx,scss,md,mdx,json,js,cjs}\\\"\",\n    \"prettier:write\": \"npx prettier --ignore-path .prettierignore --list-different \\\"**/*.{js,ts,tsx,scss,md,mdx,json,cjs}\\\" --write\",\n    \"server\": \"docker compose up --build\",\n    \"sign\": \"npx --yes @grafana/sign-plugin@latest\",\n    \"spellcheck\": \"cspell -c cspell.config.json \\\"**/*.{ts,tsx,js,go,md,mdx,yml,yaml,json,scss,css}\\\"\",\n    \"test:ci\": \"jest --passWithNoTests --maxWorkers 4\",\n    \"test\": \"jest --watch --onlyChanged\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"author\": \"Grafana Labs\",\n  \"license\": \"Apache-2.0\",\n  \"devDependencies\": {\n    \"@axe-core/playwright\": \"^4.11.1\",\n    \"@babel/core\": \"^7.29.0\",\n    \"@grafana/eslint-config\": \"^8.2.0\",\n    \"@grafana/plugin-e2e\": \"^3.6.0\",\n    \"@grafana/tsconfig\": \"^2.0.0\",\n    \"@playwright/test\": \"^1.57.0\",\n    \"@stylistic/eslint-plugin-ts\": \"^4.4.1\",\n    \"@swc/core\": \"^1.13.5\",\n    \"@swc/helpers\": \"^0.5.17\",\n    \"@swc/jest\": \"^0.2.39\",\n    \"@testing-library/dom\": \"^10.4.1\",\n    \"@testing-library/jest-dom\": \"6.9.1\",\n    \"@testing-library/react\": \"^16.3.0\",\n    \"@testing-library/user-event\": \"^14.6.1\",\n    \"@types/glob\": \"^8.1.0\",\n    \"@types/jest\": \"^29.5.13\",\n    \"@types/lodash\": \"^4.17.23\",\n    \"@types/node\": \"^24.8.0\",\n    \"@types/react\": \"18.3.28\",\n    \"@types/react-dom\": \"18.3.7\",\n    \"@types/react-router-dom\": \"^5.3.3\",\n    \"@types/semver\": \"^7.7.1\",\n    \"@types/webpack-livereload-plugin\": \"^2.3.6\",\n    \"@typescript-eslint/eslint-plugin\": \"^8.57.0\",\n    \"@typescript-eslint/parser\": \"^8.57.0\",\n    \"copy-webpack-plugin\": \"^14.0.0\",\n    \"cspell\": \"^10.0.0\",\n    \"css-loader\": \"^7.1.2\",\n    \"eslint\": \"^9.39.3\",\n    \"eslint-config-prettier\": \"^10.0.0\",\n    \"eslint-plugin-jsdoc\": \"^62.8.0\",\n    \"eslint-plugin-react\": \"^7.37.5\",\n    \"eslint-plugin-react-hooks\": \"^5.2.0\",\n    \"eslint-webpack-plugin\": \"^6.0.0\",\n    \"fork-ts-checker-webpack-plugin\": \"^9.0.2\",\n    \"glob\": \"^13.0.3\",\n    \"identity-obj-proxy\": \"3.0.0\",\n    \"imports-loader\": \"^5.0.0\",\n    \"jest\": \"^29.7.0\",\n    \"jest-environment-jsdom\": \"^29.7.0\",\n    \"prettier\": \"^3.8.1\",\n    \"replace-in-file-webpack-plugin\": \"^1.0.6\",\n    \"sass\": \"1.99.0\",\n    \"sass-loader\": \"16.0.7\",\n    \"style-loader\": \"4.0.0\",\n    \"swc-loader\": \"^0.2.6\",\n    \"terser-webpack-plugin\": \"^5.4.0\",\n    \"ts-node\": \"^10.9.2\",\n    \"tsconfig-paths\": \"^4.2.0\",\n    \"typescript\": \"5.9.3\",\n    \"webpack\": \"^5.105.0\",\n    \"webpack-cli\": \"^7.0.1\",\n    \"webpack-livereload-plugin\": \"^3.0.2\",\n    \"webpack-subresource-integrity\": \"^5.1.0\",\n    \"webpack-virtual-modules\": \"^0.6.2\"\n  },\n  \"overrides\": {\n    \"rxjs\": \"^7.5.6\",\n    \"picomatch\": \"4.0.4\",\n    \"brace-expansion@1.x\": \"1.1.14\",\n    \"brace-expansion@5.x\": \"5.0.5\",\n    \"minimatch@3.x\": \"3.1.5\",\n    \"minimatch@10.x\": \"10.2.5\",\n    \"ajv@8.x\": \"8.18.0\",\n    \"react-router\": \"6.30.3\",\n    \"react-router-dom-v5-compat\": \"6.30.3\",\n    \"@remix-run/router\": \"1.23.2\",\n    \"immutable\": \"5.1.5\"\n  },\n  \"dependencies\": {\n    \"@emotion/css\": \"11.13.5\",\n    \"@grafana/data\": \"12.4.2\",\n    \"@grafana/runtime\": \"12.4.2\",\n    \"@grafana/schema\": \"12.4.2\",\n    \"@grafana/ui\": \"12.4.2\",\n    \"@openfeature/web-sdk\": \"^1.7.3\",\n    \"es-toolkit\": \"^1.45.1\",\n    \"pgsql-ast-parser\": \"^12.0.1\",\n    \"react\": \"18.3.1\",\n    \"react-dom\": \"18.3.1\",\n    \"react-router-dom\": \"5.3.4\",\n    \"semver\": \"^7.7.3\",\n    \"sql-formatter\": \"^15.6.12\",\n    \"tslib\": \"2.8.1\"\n  },\n  \"packageManager\": \"yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e\"\n}\n"
  },
  {
    "path": "pkg/converters/converters.go",
    "content": "package converters\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"net\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/grafana/grafana-plugin-sdk-go/data\"\n\t\"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil\"\n\t\"github.com/paulmach/orb\"\n\t\"github.com/shopspring/decimal\"\n)\n\ntype Converter struct {\n\tname       string\n\tconvert    func(in interface{}) (interface{}, error)\n\tfieldType  data.FieldType\n\tmatchRegex *regexp.Regexp\n\tscanType   reflect.Type\n}\n\n// matchRegexes is a mapping of regular expressions.\n// When adding entries, try to prevent overlap in regular expressions.\n// For example `^Date` and `^DateTime` could conflict when matching `DateTime64`\nvar matchRegexes = map[string]*regexp.Regexp{\n\t// for complex Arrays e.g. Array(Tuple)\n\t\"Array()\":                   regexp.MustCompile(`^Array\\(.*\\)`),\n\t\"Date\":                      regexp.MustCompile(`^Date\\(?`),\n\t\"Decimal\":                   regexp.MustCompile(`^Decimal`),\n\t\"FixedString()\":             regexp.MustCompile(`^Nullable\\(FixedString\\(.*\\)\\)`),\n\t\"IP\":                        regexp.MustCompile(`^IPv[4,6]`),\n\t\"LowCardinality()\":          regexp.MustCompile(`^LowCardinality\\(([^)]*)\\)`),\n\t\"LowCardinality(Nullable)\":  regexp.MustCompile(`^LowCardinality\\(Nullable([^)]*)\\)`),\n\t\"Map()\":                     regexp.MustCompile(`^Map\\(.*\\)`),\n\t\"Nested()\":                  regexp.MustCompile(`^Nested\\(.*\\)`),\n\t\"Nullable(Date)\":            regexp.MustCompile(`^Nullable\\(Date\\(?`),\n\t\"Nullable(Decimal)\":         regexp.MustCompile(`^Nullable\\(Decimal`),\n\t\"Nullable(IP)\":              regexp.MustCompile(`^Nullable\\(IP`),\n\t\"Nullable(String)\":          regexp.MustCompile(`^Nullable\\(String`),\n\t\"Point\":                     regexp.MustCompile(`^Point`),\n\t\"SimpleAggregateFunction()\": regexp.MustCompile(`^SimpleAggregateFunction\\(.*\\)`),\n\t\"Tuple()\":                   regexp.MustCompile(`^Tuple\\(.*\\)`),\n\t\"Variant\":                   regexp.MustCompile(`^Variant`),\n\t\"Dynamic\":                   regexp.MustCompile(`^Dynamic`),\n\t\"JSON\":                      regexp.MustCompile(`^JSON`),\n\t\"Nullable(JSON)\":            regexp.MustCompile(`^Nullable\\(JSON`),\n\t\"Enum\":                      regexp.MustCompile(`^Enum(8|16)\\(.*\\)`),\n\t\"Nullable(Enum)\":            regexp.MustCompile(`^Nullable\\(Enum(8|16)\\(.*\\)\\)`),\n}\n\n// Converters defines a list of type converters.\n// When a converter is looked up by name or regex, it will be in the order they are defined below.\n// This is important for regular expressions that may overlap or conflict.\nvar Converters = []Converter{\n\t{\n\t\tname:      \"String\",\n\t\tfieldType: data.FieldTypeString,\n\t\tscanType:  reflect.PointerTo(reflect.TypeOf(\"\")),\n\t},\n\t{\n\t\tname:       \"Enum\",\n\t\tfieldType:  data.FieldTypeString,\n\t\tmatchRegex: matchRegexes[\"Enum\"],\n\t\tscanType:   reflect.PointerTo(reflect.TypeOf(\"\")),\n\t},\n\t{\n\t\tname:       \"Nullable(Enum)\",\n\t\tfieldType:  data.FieldTypeNullableString,\n\t\tmatchRegex: matchRegexes[\"Nullable(Enum)\"],\n\t\tscanType:   reflect.PointerTo(reflect.PointerTo(reflect.TypeOf(\"\"))),\n\t},\n\t{\n\t\tname:      \"Bool\",\n\t\tfieldType: data.FieldTypeBool,\n\t\tscanType:  reflect.PointerTo(reflect.TypeOf(true)),\n\t},\n\t{\n\t\tname:      \"Nullable(Bool)\",\n\t\tfieldType: data.FieldTypeNullableBool,\n\t\tscanType:  reflect.PointerTo(reflect.PointerTo(reflect.TypeOf(true))),\n\t},\n\t{\n\t\tname:      \"Float64\",\n\t\tfieldType: data.FieldTypeFloat64,\n\t\tscanType:  reflect.PointerTo(reflect.TypeOf(float64(0))),\n\t},\n\t{\n\t\tname:      \"Float32\",\n\t\tfieldType: data.FieldTypeFloat32,\n\t\tscanType:  reflect.PointerTo(reflect.TypeOf(float32(0))),\n\t},\n\t{\n\t\tname:      \"Nullable(Float32)\",\n\t\tfieldType: data.FieldTypeNullableFloat32,\n\t\tscanType:  reflect.PointerTo(reflect.PointerTo(reflect.TypeOf(float32(0)))),\n\t},\n\t{\n\t\tname:      \"Nullable(Float64)\",\n\t\tfieldType: data.FieldTypeNullableFloat64,\n\t\tscanType:  reflect.PointerTo(reflect.PointerTo(reflect.TypeOf(float64(0)))),\n\t},\n\t{\n\t\tname:      \"Int64\",\n\t\tfieldType: data.FieldTypeInt64,\n\t\tscanType:  reflect.PointerTo(reflect.TypeOf(int64(0))),\n\t},\n\t{\n\t\tname:      \"Int32\",\n\t\tfieldType: data.FieldTypeInt32,\n\t\tscanType:  reflect.PointerTo(reflect.TypeOf(int32(0))),\n\t},\n\t{\n\t\tname:      \"Int16\",\n\t\tfieldType: data.FieldTypeInt16,\n\t\tscanType:  reflect.PointerTo(reflect.TypeOf(int16(0))),\n\t},\n\t{\n\t\tname:      \"Int8\",\n\t\tfieldType: data.FieldTypeInt8,\n\t\tscanType:  reflect.PointerTo(reflect.TypeOf(int8(0))),\n\t},\n\t{\n\t\tname:      \"UInt64\",\n\t\tfieldType: data.FieldTypeUint64,\n\t\tscanType:  reflect.PointerTo(reflect.TypeOf(uint64(0))),\n\t},\n\t{\n\t\tname:      \"UInt32\",\n\t\tfieldType: data.FieldTypeUint32,\n\t\tscanType:  reflect.PointerTo(reflect.TypeOf(uint32(0))),\n\t},\n\t{\n\t\tname:      \"UInt16\",\n\t\tfieldType: data.FieldTypeUint16,\n\t\tscanType:  reflect.PointerTo(reflect.TypeOf(uint16(0))),\n\t},\n\t{\n\t\tname:      \"UInt8\",\n\t\tfieldType: data.FieldTypeUint8,\n\t\tscanType:  reflect.PointerTo(reflect.TypeOf(uint8(0))),\n\t},\n\t{\n\t\tname:      \"Nullable(UInt64)\",\n\t\tfieldType: data.FieldTypeNullableUint64,\n\t\tscanType:  reflect.PointerTo(reflect.PointerTo(reflect.TypeOf(uint64(0)))),\n\t},\n\t{\n\t\tname:      \"Nullable(UInt32)\",\n\t\tfieldType: data.FieldTypeNullableUint32,\n\t\tscanType:  reflect.PointerTo(reflect.PointerTo(reflect.TypeOf(uint32(0)))),\n\t},\n\t{\n\t\tname:      \"Nullable(UInt16)\",\n\t\tfieldType: data.FieldTypeNullableUint16,\n\t\tscanType:  reflect.PointerTo(reflect.PointerTo(reflect.TypeOf(uint16(0)))),\n\t},\n\t{\n\t\tname:      \"Nullable(UInt8)\",\n\t\tfieldType: data.FieldTypeNullableUint8,\n\t\tscanType:  reflect.PointerTo(reflect.PointerTo(reflect.TypeOf(uint8(0)))),\n\t},\n\t{\n\t\tname:      \"Nullable(Int64)\",\n\t\tfieldType: data.FieldTypeNullableInt64,\n\t\tscanType:  reflect.PointerTo(reflect.PointerTo(reflect.TypeOf(int64(0)))),\n\t},\n\t{\n\t\tname:      \"Nullable(Int32)\",\n\t\tfieldType: data.FieldTypeNullableInt32,\n\t\tscanType:  reflect.PointerTo(reflect.PointerTo(reflect.TypeOf(int32(0)))),\n\t},\n\t{\n\t\tname:      \"Nullable(Int16)\",\n\t\tfieldType: data.FieldTypeNullableInt16,\n\t\tscanType:  reflect.PointerTo(reflect.PointerTo(reflect.TypeOf(int16(0)))),\n\t},\n\t{\n\t\tname:      \"Nullable(Int8)\",\n\t\tfieldType: data.FieldTypeNullableInt8,\n\t\tscanType:  reflect.PointerTo(reflect.PointerTo(reflect.TypeOf(int8(0)))),\n\t},\n\t{\n\t\tname:      \"Int128\",\n\t\tconvert:   bigIntConvert,\n\t\tfieldType: data.FieldTypeFloat64,\n\t\tscanType:  reflect.PointerTo(reflect.TypeOf(big.NewInt(0))),\n\t},\n\t{\n\t\tname:      \"Nullable(Int128)\",\n\t\tconvert:   bigIntNullableConvert,\n\t\tfieldType: data.FieldTypeNullableFloat64,\n\t\tscanType:  reflect.PointerTo(reflect.PointerTo(reflect.TypeOf(big.NewInt(0)))),\n\t},\n\t{\n\t\tname:      \"Int256\",\n\t\tconvert:   bigIntConvert,\n\t\tfieldType: data.FieldTypeFloat64,\n\t\tscanType:  reflect.PointerTo(reflect.TypeOf(big.NewInt(0))),\n\t},\n\t{\n\t\tname:      \"Nullable(Int256)\",\n\t\tconvert:   bigIntNullableConvert,\n\t\tfieldType: data.FieldTypeNullableFloat64,\n\t\tscanType:  reflect.PointerTo(reflect.PointerTo(reflect.TypeOf(big.NewInt(0)))),\n\t},\n\t{\n\t\tname:      \"UInt128\",\n\t\tconvert:   bigIntConvert,\n\t\tfieldType: data.FieldTypeFloat64,\n\t\tscanType:  reflect.PointerTo(reflect.TypeOf(big.NewInt(0))),\n\t},\n\t{\n\t\tname:      \"Nullable(UInt128)\",\n\t\tconvert:   bigIntNullableConvert,\n\t\tfieldType: data.FieldTypeNullableFloat64,\n\t\tscanType:  reflect.PointerTo(reflect.PointerTo(reflect.TypeOf(big.NewInt(0)))),\n\t},\n\t{\n\t\tname:      \"UInt256\",\n\t\tconvert:   bigIntConvert,\n\t\tfieldType: data.FieldTypeFloat64,\n\t\tscanType:  reflect.PointerTo(reflect.TypeOf(big.NewInt(0))),\n\t},\n\t{\n\t\tname:      \"Nullable(UInt256)\",\n\t\tconvert:   bigIntNullableConvert,\n\t\tfieldType: data.FieldTypeNullableFloat64,\n\t\tscanType:  reflect.PointerTo(reflect.PointerTo(reflect.TypeOf(big.NewInt(0)))),\n\t},\n\t{\n\t\tname:       \"Date\",\n\t\tfieldType:  data.FieldTypeTime,\n\t\tmatchRegex: matchRegexes[\"Date\"],\n\t\tscanType:   reflect.PointerTo(reflect.TypeOf(time.Time{})),\n\t},\n\t{\n\t\tname:       \"Nullable(Date)\",\n\t\tfieldType:  data.FieldTypeNullableTime,\n\t\tmatchRegex: matchRegexes[\"Nullable(Date)\"],\n\t\tscanType:   reflect.PointerTo(reflect.PointerTo(reflect.TypeOf(time.Time{}))),\n\t},\n\t{\n\t\tname:       \"Nullable(String)\",\n\t\tfieldType:  data.FieldTypeNullableString,\n\t\tmatchRegex: matchRegexes[\"Nullable(String)\"],\n\t\tscanType:   reflect.PointerTo(reflect.PointerTo(reflect.TypeOf(\"\"))),\n\t},\n\t{\n\t\tname:       \"Decimal\",\n\t\tconvert:    decimalConvert,\n\t\tfieldType:  data.FieldTypeFloat64,\n\t\tmatchRegex: matchRegexes[\"Decimal\"],\n\t\tscanType:   reflect.PointerTo(reflect.TypeOf(decimal.Decimal{})),\n\t},\n\t{\n\t\tname:       \"Nullable(Decimal)\",\n\t\tconvert:    decimalNullConvert,\n\t\tfieldType:  data.FieldTypeNullableFloat64,\n\t\tmatchRegex: matchRegexes[\"Nullable(Decimal)\"],\n\t\tscanType:   reflect.PointerTo(reflect.PointerTo(reflect.TypeOf(decimal.Decimal{}))),\n\t},\n\t{\n\t\tname:       \"Tuple()\",\n\t\tconvert:    jsonConverter,\n\t\tfieldType:  data.FieldTypeJSON,\n\t\tmatchRegex: matchRegexes[\"Tuple()\"],\n\t\tscanType:   reflect.TypeOf((*interface{})(nil)).Elem(),\n\t},\n\t{\n\t\tname:       \"Variant\",\n\t\tconvert:    jsonConverter,\n\t\tfieldType:  data.FieldTypeJSON,\n\t\tmatchRegex: matchRegexes[\"Variant\"],\n\t\tscanType:   reflect.TypeOf((*interface{})(nil)).Elem(),\n\t},\n\t{\n\t\tname:       \"Dynamic\",\n\t\tconvert:    jsonConverter,\n\t\tfieldType:  data.FieldTypeJSON,\n\t\tmatchRegex: matchRegexes[\"Dynamic\"],\n\t\tscanType:   reflect.TypeOf((*interface{})(nil)).Elem(),\n\t},\n\t{\n\t\tname:       \"JSON\",\n\t\tconvert:    jsonConverter,\n\t\tfieldType:  data.FieldTypeJSON,\n\t\tmatchRegex: matchRegexes[\"JSON\"],\n\t\tscanType:   reflect.TypeOf((*interface{})(nil)).Elem(),\n\t},\n\t{\n\t\tname:       \"Nullable(JSON)\",\n\t\tconvert:    jsonConverter,\n\t\tfieldType:  data.FieldTypeJSON,\n\t\tmatchRegex: matchRegexes[\"Nullable(JSON)\"],\n\t\tscanType:   reflect.TypeOf((*interface{})(nil)).Elem(),\n\t},\n\t{\n\t\tname:       \"Nested()\",\n\t\tconvert:    jsonConverter,\n\t\tfieldType:  data.FieldTypeJSON,\n\t\tmatchRegex: matchRegexes[\"Nested()\"],\n\t\tscanType:   reflect.TypeOf([]map[string]interface{}{}),\n\t},\n\t{\n\t\tname:       \"Array()\",\n\t\tconvert:    jsonConverter,\n\t\tfieldType:  data.FieldTypeJSON,\n\t\tmatchRegex: matchRegexes[\"Array()\"],\n\t\tscanType:   reflect.TypeOf((*interface{})(nil)).Elem(),\n\t},\n\t{\n\t\tname:       \"Map()\",\n\t\tconvert:    jsonConverter,\n\t\tfieldType:  data.FieldTypeJSON,\n\t\tmatchRegex: matchRegexes[\"Map()\"],\n\t\tscanType:   reflect.TypeOf((*interface{})(nil)).Elem(),\n\t},\n\t{\n\t\tname:       \"FixedString()\",\n\t\tfieldType:  data.FieldTypeNullableString,\n\t\tmatchRegex: matchRegexes[\"FixedString()\"],\n\t\tscanType:   reflect.PointerTo(reflect.PointerTo(reflect.TypeOf(\"\"))),\n\t},\n\t{\n\t\tname:       \"IP\",\n\t\tconvert:    ipConverter,\n\t\tfieldType:  data.FieldTypeString,\n\t\tmatchRegex: matchRegexes[\"IP\"],\n\t\tscanType:   reflect.PointerTo(reflect.TypeOf(net.IP{})),\n\t},\n\t{\n\t\tname:       \"Nullable(IP)\",\n\t\tconvert:    ipNullConverter,\n\t\tfieldType:  data.FieldTypeNullableString,\n\t\tmatchRegex: matchRegexes[\"Nullable(IP)\"],\n\t\tscanType:   reflect.PointerTo(reflect.PointerTo(reflect.TypeOf(net.IP{}))),\n\t},\n\t{\n\t\tname:       \"SimpleAggregateFunction()\",\n\t\tconvert:    jsonConverter,\n\t\tfieldType:  data.FieldTypeJSON,\n\t\tmatchRegex: matchRegexes[\"SimpleAggregateFunction()\"],\n\t\tscanType:   reflect.TypeOf((*interface{})(nil)).Elem(),\n\t},\n\t{\n\t\tname:       \"Point\",\n\t\tconvert:    pointConverter,\n\t\tfieldType:  data.FieldTypeJSON,\n\t\tmatchRegex: matchRegexes[\"Point\"],\n\t\tscanType:   reflect.TypeOf((*interface{})(nil)).Elem(),\n\t},\n\t{\n\t\tname:      \"LowCardinality(String)\",\n\t\tfieldType: data.FieldTypeString,\n\t\tscanType:  reflect.PointerTo(reflect.TypeOf(\"\")),\n\t},\n\t{\n\t\tname:       \"LowCardinality(Nullable(String))\",\n\t\tfieldType:  data.FieldTypeNullableString,\n\t\tmatchRegex: matchRegexes[\"LowCardinality(Nullable)\"],\n\t\tscanType:   reflect.PointerTo(reflect.PointerTo(reflect.TypeOf(\"\"))),\n\t},\n}\n\nvar ClickhouseConverters = ClickHouseConverters()\n\nfunc ClickHouseConverters() []sqlutil.Converter {\n\tvar list []sqlutil.Converter\n\tfor _, converter := range Converters {\n\t\tlist = append(list, createConverter(converter))\n\t}\n\treturn list\n}\n\n// GetConverter returns a sqlutil.Converter for the given column type.\nfunc GetConverter(columnType string) sqlutil.Converter {\n\t// check for 'LowCardinality()' type first and get the converter for the inner type\n\tif innerType, ok := extractLowCardinalityType(columnType); ok {\n\t\treturn GetConverter(innerType)\n\t}\n\n\t// direct match by name\n\tfor _, converter := range Converters {\n\t\tif converter.name == columnType {\n\t\t\treturn createConverter(converter)\n\t\t}\n\t}\n\n\t// regex-based search through `Converters` map\n\treturn findConverterWithRegex(columnType)\n}\n\nconst (\n\tlowCardinalityPrefix = \"LowCardinality(\"\n\tlowCardinalitySuffix = \")\"\n)\n\n// extractLowCardinalityType checks if the column type is a `LowCardinality()` type and returns the inner type.\nfunc extractLowCardinalityType(columnType string) (string, bool) {\n\tif strings.HasPrefix(columnType, lowCardinalityPrefix) && strings.HasSuffix(columnType, lowCardinalitySuffix) {\n\t\treturn columnType[len(lowCardinalityPrefix) : len(columnType)-len(lowCardinalitySuffix)], true\n\t}\n\n\treturn \"\", false\n}\n\n// findConverterWithRegex searches through the `Converters` map using regex matching.\nfunc findConverterWithRegex(columnType string) sqlutil.Converter {\n\tfor _, converter := range Converters {\n\t\tif converter.matchRegex != nil && converter.matchRegex.MatchString(columnType) {\n\t\t\treturn createConverter(converter)\n\t\t}\n\t}\n\n\treturn sqlutil.Converter{}\n}\n\nfunc createConverter(converter Converter) sqlutil.Converter {\n\tconvert := defaultConvert\n\tif converter.convert != nil {\n\t\tconvert = converter.convert\n\t}\n\treturn sqlutil.Converter{\n\t\tName:           converter.name,\n\t\tInputScanType:  converter.scanType,\n\t\tInputTypeRegex: converter.matchRegex,\n\t\tInputTypeName:  converter.name,\n\t\tFrameConverter: sqlutil.FrameConverter{\n\t\t\tFieldType:     converter.fieldType,\n\t\t\tConverterFunc: convert,\n\t\t},\n\t}\n}\n\nfunc jsonConverter(in any) (any, error) {\n\t// Unwrap `*any` to be `any`\n\tif anyPtr, ok := in.(*any); ok {\n\t\tin = *anyPtr\n\t}\n\n\tswitch v := in.(type) {\n\tcase nil:\n\t\treturn (json.RawMessage)(nil), nil\n\tcase string:\n\t\treturn json.RawMessage(v), nil\n\tcase *string:\n\t\treturn json.RawMessage(*v), nil\n\tcase []byte:\n\t\treturn json.RawMessage(v), nil\n\tcase *[]byte:\n\t\treturn json.RawMessage(*v), nil\n\tdefault:\n\t}\n\n\tjBytes, err := json.Marshal(in)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn json.RawMessage(jBytes), nil\n}\n\nfunc defaultConvert(in interface{}) (interface{}, error) {\n\tif in == nil {\n\t\treturn reflect.Zero(reflect.TypeOf(in)).Interface(), nil\n\t}\n\n\t// check the type of the input and handle strings separately because they cannot be dereferenced\n\tval := reflect.ValueOf(in)\n\tif val.Kind() == reflect.String {\n\t\treturn in, nil\n\t}\n\n\t// handle pointers and dereference if possible\n\tif val.Kind() == reflect.Ptr {\n\t\tif val.IsNil() {\n\t\t\treturn nil, errors.New(\"nil pointer cannot be dereferenced in defaultConvert\")\n\t\t}\n\t\treturn val.Elem().Interface(), nil\n\t}\n\n\treturn in, nil\n}\n\nfunc decimalConvert(in interface{}) (interface{}, error) {\n\tif in == nil {\n\t\treturn float64(0), nil\n\t}\n\tv, ok := in.(*decimal.Decimal)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"invalid decimal - %v\", in)\n\t}\n\tf, _ := (*v).Float64()\n\treturn f, nil\n}\n\nfunc decimalNullConvert(in interface{}) (interface{}, error) {\n\tif in == nil {\n\t\treturn float64(0), nil\n\t}\n\tv, ok := in.(**decimal.Decimal)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"invalid decimal - %v\", in)\n\t}\n\tif *v == nil {\n\t\treturn (*float64)(nil), nil\n\t}\n\tf, _ := (*v).Float64()\n\treturn &f, nil\n}\n\nfunc bigIntConvert(in interface{}) (interface{}, error) {\n\tif in == nil {\n\t\treturn float64(0), nil\n\t}\n\tv, ok := in.(**big.Int)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"invalid big int - %v\", in)\n\t}\n\tf, _ := new(big.Float).SetInt(*v).Float64()\n\treturn f, nil\n}\n\nfunc bigIntNullableConvert(in interface{}) (interface{}, error) {\n\tif in == nil {\n\t\treturn (*float64)(nil), nil\n\t}\n\tv, ok := in.(***big.Int)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"invalid big int - %v\", in)\n\t}\n\tif *v == nil || **v == nil {\n\t\treturn (*float64)(nil), nil\n\t}\n\tf, _ := new(big.Float).SetInt(**v).Float64()\n\treturn &f, nil\n}\n\nfunc ipConverter(in interface{}) (interface{}, error) {\n\tif in == nil {\n\t\treturn nil, nil\n\t}\n\tv, ok := in.(*net.IP)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"invalid ip - %v\", in)\n\t}\n\tif v == nil {\n\t\treturn nil, nil\n\t}\n\tsIP := v.String()\n\treturn sIP, nil\n}\n\nfunc ipNullConverter(in interface{}) (interface{}, error) {\n\tif in == nil {\n\t\treturn nil, nil\n\t}\n\tv, ok := in.(**net.IP)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"invalid ip - %v\", in)\n\t}\n\tif *v == nil {\n\t\treturn nil, nil\n\t}\n\tsIP := (*v).String()\n\treturn &sIP, nil\n}\n\nfunc pointConverter(in interface{}) (interface{}, error) {\n\tif in == nil {\n\t\treturn nil, nil\n\t}\n\tv, ok := (*(in.(*interface{}))).(orb.Point)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"invalid point - %v\", in)\n\t}\n\treturn jsonConverter(v)\n}\n"
  },
  {
    "path": "pkg/converters/converters_test.go",
    "content": "package converters\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"math/big\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/ClickHouse/clickhouse-go/v2\"\n\t\"github.com/paulmach/orb\"\n\t\"github.com/shopspring/decimal\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDate(t *testing.T) {\n\tlayout := \"2006-01-02T15:04:05.000Z\"\n\tstr := \"2014-11-12T11:45:26.371Z\"\n\td, _ := time.Parse(layout, str)\n\tsut := GetConverter(\"Date\")\n\tv, err := sut.FrameConverter.ConverterFunc(&d)\n\tassert.Nil(t, err)\n\tactual := v.(time.Time)\n\tassert.Equal(t, d, actual)\n}\n\nfunc TestNullableDate(t *testing.T) {\n\tlayout := \"2006-01-02T15:04:05.000Z\"\n\tstr := \"2014-11-12T11:45:26.371Z\"\n\td, _ := time.Parse(layout, str)\n\tval := &d\n\tsut := GetConverter(\"Nullable(Date)\")\n\tv, err := sut.FrameConverter.ConverterFunc(&val)\n\tassert.Nil(t, err)\n\tactual := v.(*time.Time)\n\tassert.Equal(t, val, actual)\n}\n\nfunc TestNullableDateShouldBeNil(t *testing.T) {\n\tsut := GetConverter(\"Nullable(Date)\")\n\tvar d *time.Time\n\tv, err := sut.FrameConverter.ConverterFunc(&d)\n\tassert.Nil(t, err)\n\tactual := v.(*time.Time)\n\tassert.Equal(t, (*time.Time)(nil), actual)\n}\n\nfunc TestNullableDecimal(t *testing.T) {\n\tval := decimal.New(25, 4)\n\tvalue := &val\n\tnullableDecimal := GetConverter(\"Nullable(Decimal(15,2))\")\n\tv, err := nullableDecimal.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(*float64)\n\tf, _ := val.Float64()\n\tassert.Equal(t, f, *actual)\n}\n\nfunc TestNullableDecimalShouldBeNull(t *testing.T) {\n\tnullableDecimal := GetConverter(\"Nullable(Decimal(15,2))\")\n\tvar value *decimal.Decimal\n\tv, err := nullableDecimal.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(*float64)\n\tassert.Equal(t, (*float64)(nil), actual)\n}\n\nfunc TestDecimal(t *testing.T) {\n\tval := decimal.New(25, 4)\n\tnullableDecimal := GetConverter(\"Decimal(15,2)\")\n\tv, err := nullableDecimal.FrameConverter.ConverterFunc(&val)\n\tassert.Nil(t, err)\n\tactual := v.(float64)\n\tf, _ := val.Float64()\n\tassert.Equal(t, f, actual)\n}\n\nfunc TestNullableString(t *testing.T) {\n\tvar value *string\n\tsut := GetConverter(\"Nullable(String)\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(*string)\n\tassert.Equal(t, value, actual)\n}\n\nfunc TestEnum8(t *testing.T) {\n\tvalue := \"CONST\"\n\tsut := GetConverter(\"Enum8('WRITABLE' = 0, 'CONST' = 1, 'CHANGEABLE_IN_READONLY' = 2)\")\n\trequire.NotNil(t, sut.InputScanType, \"Enum8 converter should be found\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(string)\n\tassert.Equal(t, value, actual)\n}\n\nfunc TestEnum16(t *testing.T) {\n\tvalue := \"option1\"\n\tsut := GetConverter(\"Enum16('option1' = 1000, 'option2' = 2000)\")\n\trequire.NotNil(t, sut.InputScanType, \"Enum16 converter should be found\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(string)\n\tassert.Equal(t, value, actual)\n}\n\nfunc TestNullableEnum8(t *testing.T) {\n\tvalue := \"CONST\"\n\tvaluePtr := &value\n\tsut := GetConverter(\"Nullable(Enum8('WRITABLE' = 0, 'CONST' = 1, 'CHANGEABLE_IN_READONLY' = 2))\")\n\trequire.NotNil(t, sut.InputScanType, \"Nullable(Enum8) converter should be found\")\n\tv, err := sut.FrameConverter.ConverterFunc(&valuePtr)\n\tassert.Nil(t, err)\n\tactual := v.(*string)\n\tassert.Equal(t, valuePtr, actual)\n}\n\nfunc TestNullableEnum8ShouldBeNil(t *testing.T) {\n\tvar value *string\n\tsut := GetConverter(\"Nullable(Enum8('WRITABLE' = 0, 'CONST' = 1, 'CHANGEABLE_IN_READONLY' = 2))\")\n\trequire.NotNil(t, sut.InputScanType, \"Nullable(Enum8) converter should be found\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(*string)\n\tassert.Equal(t, (*string)(nil), actual)\n}\n\nfunc TestNullableEnum16(t *testing.T) {\n\tvalue := \"option1\"\n\tvaluePtr := &value\n\tsut := GetConverter(\"Nullable(Enum16('option1' = 1000, 'option2' = 2000))\")\n\trequire.NotNil(t, sut.InputScanType, \"Nullable(Enum16) converter should be found\")\n\tv, err := sut.FrameConverter.ConverterFunc(&valuePtr)\n\tassert.Nil(t, err)\n\tactual := v.(*string)\n\tassert.Equal(t, valuePtr, actual)\n}\n\nfunc TestNullableEnum16ShouldBeNil(t *testing.T) {\n\tvar value *string\n\tsut := GetConverter(\"Nullable(Enum16('option1' = 1000, 'option2' = 2000))\")\n\trequire.NotNil(t, sut.InputScanType, \"Nullable(Enum16) converter should be found\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(*string)\n\tassert.Equal(t, (*string)(nil), actual)\n}\n\nfunc TestBool(t *testing.T) {\n\tvalue := true\n\tsut := GetConverter(\"Bool\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(bool)\n\tassert.True(t, actual)\n}\n\nfunc TestNullableBool(t *testing.T) {\n\tvar value *bool\n\tsut := GetConverter(\"Nullable(Bool)\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(*bool)\n\tassert.Equal(t, value, actual)\n}\n\nfunc TestFloat64(t *testing.T) {\n\tvalue := 1.1\n\tsut := GetConverter(\"Float64\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(float64)\n\tassert.Equal(t, value, actual)\n}\n\nfunc TestNullableFloat64(t *testing.T) {\n\tvar value *float64\n\tsut := GetConverter(\"Nullable(Float64)\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(*float64)\n\tassert.Equal(t, value, actual)\n}\n\nfunc TestFloat32(t *testing.T) {\n\tvalue := 1.1\n\tsut := GetConverter(\"Float32\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(float64)\n\tassert.Equal(t, value, actual)\n}\n\nfunc TestInt64(t *testing.T) {\n\tvalue := int64(1)\n\tsut := GetConverter(\"Int64\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(int64)\n\tassert.Equal(t, value, actual)\n}\n\nfunc TestNullableInt64(t *testing.T) {\n\tvar value *int64\n\tsut := GetConverter(\"Nullable(Int64)\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(*int64)\n\tassert.Equal(t, value, actual)\n}\n\nfunc TestInt32(t *testing.T) {\n\tvalue := int32(1)\n\tsut := GetConverter(\"Int32\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(int32)\n\tassert.Equal(t, value, actual)\n}\n\nfunc TestNullableInt32(t *testing.T) {\n\tvar value *int32\n\tsut := GetConverter(\"Nullable(Int32)\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(*int32)\n\tassert.Equal(t, value, actual)\n}\n\nfunc TestInt8(t *testing.T) {\n\tvalue := int8(1)\n\tsut := GetConverter(\"Int8\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(int8)\n\tassert.Equal(t, value, actual)\n}\n\nfunc TestNullableInt8(t *testing.T) {\n\tvar value *int8\n\tsut := GetConverter(\"Nullable(Int8)\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(*int8)\n\tassert.Equal(t, value, actual)\n}\n\nfunc TestInt16(t *testing.T) {\n\tvalue := int16(1)\n\tsut := GetConverter(\"Int16\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(int16)\n\tassert.Equal(t, value, actual)\n}\n\nfunc TestNullableInt16(t *testing.T) {\n\tvar value *int16\n\tsut := GetConverter(\"Nullable(Int16)\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(*int16)\n\tassert.Equal(t, value, actual)\n}\n\nfunc TestUInt8(t *testing.T) {\n\tvalue := uint8(1)\n\tsut := GetConverter(\"UInt8\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(uint8)\n\tassert.Equal(t, value, actual)\n}\n\nfunc TestNullableUInt8(t *testing.T) {\n\tvalue := uint8(100)\n\tval := &value\n\tsut := GetConverter(\"Nullable(UInt8)\")\n\tv, err := sut.FrameConverter.ConverterFunc(&val)\n\tassert.Nil(t, err)\n\tactual := v.(*uint8)\n\tassert.Equal(t, value, *actual)\n}\n\nfunc TestNullableUInt8ShouldBeNil(t *testing.T) {\n\tvar value *uint8\n\tval := &value\n\tsut := GetConverter(\"Nullable(UInt8)\")\n\tv, err := sut.FrameConverter.ConverterFunc(val)\n\tassert.Nil(t, err)\n\tactual := v.(*uint8)\n\tassert.Equal(t, value, actual)\n}\n\nfunc TestUInt16(t *testing.T) {\n\tvalue := uint16(100)\n\tval := &value\n\tsut := GetConverter(\"UInt16\")\n\tv, err := sut.FrameConverter.ConverterFunc(&val)\n\tassert.Nil(t, err)\n\tactual := v.(*uint16)\n\tassert.Equal(t, value, *actual)\n}\n\nfunc TestNullableUInt16(t *testing.T) {\n\tvalue := uint16(100)\n\tval := &value\n\tsut := GetConverter(\"Nullable(UInt16)\")\n\tv, err := sut.FrameConverter.ConverterFunc(&val)\n\tassert.Nil(t, err)\n\tactual := v.(*uint16)\n\tassert.Equal(t, value, *actual)\n}\n\nfunc TestNullableUInt16ShouldBeNil(t *testing.T) {\n\tvar value *uint16\n\tval := &value\n\tsut := GetConverter(\"Nullable(UInt16)\")\n\tv, err := sut.FrameConverter.ConverterFunc(val)\n\tassert.Nil(t, err)\n\tactual := v.(*uint16)\n\tassert.Equal(t, value, actual)\n}\n\nfunc TestUInt32(t *testing.T) {\n\tvalue := uint32(100)\n\tval := &value\n\tsut := GetConverter(\"UInt32\")\n\tv, err := sut.FrameConverter.ConverterFunc(&val)\n\tassert.Nil(t, err)\n\tactual := v.(*uint32)\n\tassert.Equal(t, value, *actual)\n}\n\nfunc TestNullableUInt32(t *testing.T) {\n\tvalue := uint32(100)\n\tval := &value\n\tsut := GetConverter(\"Nullable(UInt32)\")\n\tv, err := sut.FrameConverter.ConverterFunc(&val)\n\tassert.Nil(t, err)\n\tactual := v.(*uint32)\n\tassert.Equal(t, value, *actual)\n}\n\nfunc TestNullableUInt32ShouldBeNil(t *testing.T) {\n\tvar value *uint32\n\tval := &value\n\tsut := GetConverter(\"Nullable(UInt32)\")\n\tv, err := sut.FrameConverter.ConverterFunc(val)\n\tassert.Nil(t, err)\n\tactual := v.(*uint32)\n\tassert.Equal(t, value, actual)\n}\n\nfunc TestUInt64(t *testing.T) {\n\tvalue := uint64(100)\n\tval := &value\n\tsut := GetConverter(\"UInt64\")\n\tv, err := sut.FrameConverter.ConverterFunc(&val)\n\tassert.Nil(t, err)\n\tactual := v.(*uint64)\n\tassert.Equal(t, value, *actual)\n}\n\nfunc TestNullableUInt64(t *testing.T) {\n\tvalue := uint64(100)\n\tval := &value\n\tsut := GetConverter(\"Nullable(UInt64)\")\n\tv, err := sut.FrameConverter.ConverterFunc(&val)\n\tassert.Nil(t, err)\n\tactual := v.(*uint64)\n\tassert.Equal(t, value, *actual)\n}\n\nfunc TestNullableUInt64ShouldBeNil(t *testing.T) {\n\tvar value *uint64\n\tval := &value\n\tsut := GetConverter(\"Nullable(UInt64)\")\n\tv, err := sut.FrameConverter.ConverterFunc(val)\n\tassert.Nil(t, err)\n\tactual := v.(*uint64)\n\tassert.Equal(t, value, actual)\n}\n\nfunc TestInt128(t *testing.T) {\n\tvalue := big.NewInt(128)\n\tsut := GetConverter(\"Int128\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(float64)\n\texpected, _ := new(big.Float).SetInt(value).Float64()\n\tassert.Equal(t, expected, actual)\n}\n\nfunc TestNullableInt128(t *testing.T) {\n\tvalue := big.NewInt(128)\n\tval := &value\n\tsut := GetConverter(\"Nullable(Int128)\")\n\tv, err := sut.FrameConverter.ConverterFunc(&val)\n\tassert.Nil(t, err)\n\tactual := v.(*float64)\n\texpected, _ := new(big.Float).SetInt(value).Float64()\n\tassert.Equal(t, &expected, actual)\n}\n\nfunc TestNullableInt128ShouldBeNil(t *testing.T) {\n\tvar value *big.Int\n\tval := &value\n\tsut := GetConverter(\"Nullable(Int128)\")\n\tv, err := sut.FrameConverter.ConverterFunc(&val)\n\tassert.Nil(t, err)\n\tactual := v.(*float64)\n\tassert.Equal(t, (*float64)(nil), actual)\n}\n\nfunc TestInt256(t *testing.T) {\n\tvalue := big.NewInt(128)\n\tsut := GetConverter(\"Int256\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(float64)\n\texpected, _ := new(big.Float).SetInt(value).Float64()\n\tassert.Equal(t, expected, actual)\n}\n\nfunc TestNullableInt256(t *testing.T) {\n\tvalue := big.NewInt(128)\n\tval := &value\n\tsut := GetConverter(\"Nullable(Int256)\")\n\tv, err := sut.FrameConverter.ConverterFunc(&val)\n\tassert.Nil(t, err)\n\tactual := v.(*float64)\n\texpected, _ := new(big.Float).SetInt(value).Float64()\n\tassert.Equal(t, &expected, actual)\n}\n\nfunc TestNullableInt256ShouldBeNil(t *testing.T) {\n\tvar value *big.Int\n\tval := &value\n\tsut := GetConverter(\"Nullable(Int256)\")\n\tv, err := sut.FrameConverter.ConverterFunc(&val)\n\tassert.Nil(t, err)\n\tactual := v.(*float64)\n\tassert.Equal(t, (*float64)(nil), actual)\n}\n\nfunc TestUInt128(t *testing.T) {\n\tvalue := big.NewInt(128)\n\tsut := GetConverter(\"UInt128\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(float64)\n\texpected, _ := new(big.Float).SetInt(value).Float64()\n\tassert.Equal(t, expected, actual)\n}\n\nfunc TestNullableUInt128(t *testing.T) {\n\tvalue := big.NewInt(128)\n\tval := &value\n\tsut := GetConverter(\"Nullable(UInt128)\")\n\tv, err := sut.FrameConverter.ConverterFunc(&val)\n\tassert.Nil(t, err)\n\tactual := v.(*float64)\n\texpected, _ := new(big.Float).SetInt(value).Float64()\n\tassert.Equal(t, &expected, actual)\n}\n\nfunc TestNullableUInt128ShouldBeNil(t *testing.T) {\n\tvar value *big.Int\n\tval := &value\n\tsut := GetConverter(\"Nullable(UInt128)\")\n\tv, err := sut.FrameConverter.ConverterFunc(&val)\n\tassert.Nil(t, err)\n\tactual := v.(*float64)\n\tassert.Equal(t, (*float64)(nil), actual)\n}\n\nfunc TestUInt256(t *testing.T) {\n\tvalue := big.NewInt(128)\n\tsut := GetConverter(\"UInt256\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tactual := v.(float64)\n\texpected, _ := new(big.Float).SetInt(value).Float64()\n\tassert.Equal(t, expected, actual)\n}\n\nfunc TestNullableUInt256(t *testing.T) {\n\tvalue := big.NewInt(128)\n\tval := &value\n\tsut := GetConverter(\"Nullable(UInt256)\")\n\tv, err := sut.FrameConverter.ConverterFunc(&val)\n\tassert.Nil(t, err)\n\tactual := v.(*float64)\n\texpected, _ := new(big.Float).SetInt(value).Float64()\n\tassert.Equal(t, &expected, actual)\n}\n\nfunc TestNullableUInt256ShouldBeNil(t *testing.T) {\n\tvar value *big.Int\n\tval := &value\n\tsut := GetConverter(\"Nullable(UInt256)\")\n\tv, err := sut.FrameConverter.ConverterFunc(&val)\n\tassert.Nil(t, err)\n\tactual := v.(*float64)\n\tassert.Equal(t, (*float64)(nil), actual)\n}\n\nfunc toJson(obj interface{}) (json.RawMessage, error) {\n\tbytes, err := json.Marshal(obj)\n\tif err != nil {\n\t\treturn nil, errors.New(\"unable to marshal\")\n\t}\n\tvar rawJSON json.RawMessage\n\terr = json.Unmarshal(bytes, &rawJSON)\n\tif err != nil {\n\t\treturn nil, errors.New(\"unable to unmarshal\")\n\t}\n\treturn rawJSON, nil\n}\n\nfunc TestTuple(t *testing.T) {\n\tvalue := map[string]interface{}{\n\t\t\"1\": uint16(1),\n\t\t\"2\": uint16(2),\n\t\t\"3\": uint16(3),\n\t\t\"4\": uint16(4),\n\t}\n\tsut := GetConverter(\"Tuple(name String, id Uint16)\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tmsg, err := toJson(value)\n\tassert.Nil(t, err)\n\tassert.Equal(t, msg, v.(json.RawMessage))\n}\n\nfunc TestNested(t *testing.T) {\n\tvalue := []map[string]interface{}{\n\t\t{\n\t\t\t\"1\": uint16(1),\n\t\t\t\"2\": uint16(2),\n\t\t\t\"3\": uint16(3),\n\t\t\t\"4\": uint16(4),\n\t\t},\n\t}\n\tsut := GetConverter(\"Nested(name String, id Uint16)\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tmsg, err := toJson(value)\n\tassert.Nil(t, err)\n\tassert.Equal(t, msg, v.(json.RawMessage))\n}\n\nfunc TestMap(t *testing.T) {\n\tvalue := map[string]interface{}{\n\t\t\"1\": uint16(1),\n\t\t\"2\": uint16(2),\n\t\t\"3\": uint16(3),\n\t\t\"4\": uint16(4),\n\t}\n\tsut := GetConverter(\"Map(String, Uint16)\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tmsg, err := toJson(value)\n\tassert.Nil(t, err)\n\tassert.Equal(t, msg, v.(json.RawMessage))\n}\n\nfunc TestJSONObject(t *testing.T) {\n\tvalue := clickhouse.NewJSON()\n\tvalue.SetValueAtPath(\"x\", \"1234\")\n\n\tsut := GetConverter(\"JSON\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tmsg, err := toJson(value)\n\tassert.Nil(t, err)\n\tassert.Equal(t, msg, v.(json.RawMessage))\n}\n\nfunc TestJSONString(t *testing.T) {\n\tjsonStr := `{\"x\":\"1234\"}`\n\tvalue := []byte(jsonStr)\n\tsut := GetConverter(\"JSON\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tassert.Nil(t, err)\n\tassert.Equal(t, json.RawMessage(jsonStr), v.(json.RawMessage))\n}\n\nfunc TestNullableFixedString(t *testing.T) {\n\tvalue := \"2\"\n\tsut := GetConverter(\"Nullable(FixedString(2))\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tassert.Equal(t, value, v.(string))\n}\n\nfunc TestArray(t *testing.T) {\n\tvalue := []string{\"1\", \"2\", \"3\"}\n\tipConverter := GetConverter(\"Array(String)\")\n\tv, err := ipConverter.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tmsg, err := toJson(value)\n\tassert.Nil(t, err)\n\tassert.Equal(t, msg, v.(json.RawMessage))\n}\n\nfunc TestIPv4(t *testing.T) {\n\tvalue := net.ParseIP(\"127.0.0.1\")\n\tipConverter := GetConverter(\"IPv4\")\n\tv, err := ipConverter.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tassert.Equal(t, value.String(), v)\n}\n\nfunc TestIPv6(t *testing.T) {\n\tvalue := net.ParseIP(\"2001:44c8:129:2632:33:0:252:2\")\n\tipConverter := GetConverter(\"IPv6\")\n\tv, err := ipConverter.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tassert.Equal(t, value.String(), v)\n}\n\nfunc TestNullableIPv4(t *testing.T) {\n\tvalue := net.ParseIP(\"127.0.0.1\")\n\tval := &value\n\tipConverter := GetConverter(\"Nullable(IPv4)\")\n\tv, err := ipConverter.FrameConverter.ConverterFunc(&val)\n\tassert.Nil(t, err)\n\tactual := v.(*string)\n\tassert.Equal(t, value.String(), *actual)\n}\n\nfunc TestNullableIPv4ShouldBeNull(t *testing.T) {\n\tvar value *net.IP\n\tipConverter := GetConverter(\"Nullable(IPv4)\")\n\tv, err := ipConverter.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\trequire.Nil(t, v)\n}\n\nfunc TestNullableIPv6(t *testing.T) {\n\tvalue := net.ParseIP(\"2001:44c8:129:2632:33:0:252:2\")\n\tval := &value\n\tipConverter := GetConverter(\"Nullable(IPv6)\")\n\tv, err := ipConverter.FrameConverter.ConverterFunc(&val)\n\tassert.Nil(t, err)\n\tactual := v.(*string)\n\tassert.Equal(t, value.String(), *actual)\n}\n\nfunc TestNullableIPv6ShouldBeNull(t *testing.T) {\n\tvar value *net.IP\n\tipConverter := GetConverter(\"Nullable(IPv6)\")\n\tv, err := ipConverter.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\trequire.Nil(t, v)\n}\n\nfunc TestSimpleAggregateFunction(t *testing.T) {\n\tvalue := [][]int{{1, 2, 3}, {1, 2, 3}}\n\taggConverter := GetConverter(\"SimpleAggregateFunction()\")\n\tv, err := aggConverter.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tmsg, err := toJson(value)\n\tassert.Nil(t, err)\n\tassert.Equal(t, msg, v.(json.RawMessage))\n}\n\nfunc TestPoint(t *testing.T) {\n\tvalue := interface{}(interface{}(orb.Point{10, 10}))\n\tsut := GetConverter(\"Point\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tmsg, err := toJson(value)\n\tassert.Nil(t, err)\n\tassert.Equal(t, msg, v.(json.RawMessage))\n}\n\nfunc TestLowCardinality(t *testing.T) {\n\tvalue := \"value\"\n\tsut := GetConverter(\"LowCardinality(String)\")\n\tv, err := sut.FrameConverter.ConverterFunc(value)\n\tassert.Nil(t, err)\n\tassert.Equal(t, value, v)\n}\n\nfunc TestLowCardinalityNullable(t *testing.T) {\n\tvalue := \"value\"\n\tsut := GetConverter(\"LowCardinality(Nullable(String))\")\n\tv, err := sut.FrameConverter.ConverterFunc(&value)\n\tassert.Nil(t, err)\n\tassert.Equal(t, value, v)\n}\n\nfunc TestExtractLowCardinality(t *testing.T) {\n\tcases := []struct {\n\t\tinputType    string\n\t\texpectedType string\n\t\texpectedOk   bool\n\t}{\n\t\t{\n\t\t\tinputType:    \"Nullable(LowCardinality(String))\",\n\t\t\texpectedType: \"\",\n\t\t\texpectedOk:   false,\n\t\t},\n\t\t{\n\t\t\tinputType:    \"String\",\n\t\t\texpectedType: \"\",\n\t\t\texpectedOk:   false,\n\t\t},\n\t\t{\n\t\t\tinputType:    \"LowCardinality(String)\",\n\t\t\texpectedType: \"String\",\n\t\t\texpectedOk:   true,\n\t\t},\n\t\t{\n\t\t\tinputType:    \"LowCardinality(Nullable(String))\",\n\t\t\texpectedType: \"Nullable(String)\",\n\t\t\texpectedOk:   true,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.inputType, func(t *testing.T) {\n\t\t\tactualType, actualOk := extractLowCardinalityType(c.inputType)\n\t\t\tassert.Equal(t, c.expectedOk, actualOk)\n\t\t\tassert.Equal(t, c.expectedType, actualType)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/macros/macros.go",
    "content": "package macros\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/grafana/sqlds/v5\"\n\n\t\"github.com/grafana/grafana-plugin-sdk-go/backend\"\n\t\"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil\"\n)\n\n// Converts a time.Time to a Date\nfunc timeToDate(t time.Time) string {\n\treturn fmt.Sprintf(\"toDate('%s')\", t.Format(\"2006-01-02\"))\n}\n\n// Converts a time.Time to a UTC DateTime with seconds precision\nfunc timeToDateTime(t time.Time) string {\n\treturn fmt.Sprintf(\"toDateTime(%d)\", t.Unix())\n}\n\n// Converts a time.Time to a UTC DateTime64 with milliseconds precision\nfunc timeToDateTime64(t time.Time) string {\n\treturn fmt.Sprintf(\"fromUnixTimestamp64Milli(%d)\", t.UnixMilli())\n}\n\n// FromTimeFilter returns a time filter expression based on grafana's timepicker's \"from\" time in seconds\nfunc FromTimeFilter(query *sqlutil.Query, args []string) (string, error) {\n\treturn timeToDateTime(query.TimeRange.From), nil\n}\n\n// ToTimeFilter returns a time filter expression based on grafana's timepicker's \"to\" time in seconds\nfunc ToTimeFilter(query *sqlutil.Query, args []string) (string, error) {\n\treturn timeToDateTime(query.TimeRange.To), nil\n}\n\n// FromTimeFilterMs returns a time filter expression based on grafana's timepicker's \"from\" time in milliseconds\nfunc FromTimeFilterMs(query *sqlutil.Query, args []string) (string, error) {\n\treturn timeToDateTime64(query.TimeRange.From), nil\n}\n\n// ToTimeFilterMs returns a time filter expression based on grafana's timepicker's \"to\" time in milliseconds\nfunc ToTimeFilterMs(query *sqlutil.Query, args []string) (string, error) {\n\treturn timeToDateTime64(query.TimeRange.To), nil\n}\n\nfunc TimeFilter(query *sqlutil.Query, args []string) (string, error) {\n\tif len(args) != 1 {\n\t\treturn \"\", backend.DownstreamError(fmt.Errorf(\"%w: expected 1 argument, received %d\", sqlutil.ErrorBadArgumentCount, len(args)))\n\t}\n\n\tvar (\n\t\tcolumn = args[0]\n\t\tfrom   = query.TimeRange.From\n\t\tto     = query.TimeRange.To\n\t)\n\n\treturn fmt.Sprintf(\"%s >= %s AND %s <= %s\", column, timeToDateTime(from), column, timeToDateTime(to)), nil\n}\n\nfunc TimeFilterMs(query *sqlutil.Query, args []string) (string, error) {\n\tif len(args) != 1 {\n\t\treturn \"\", backend.DownstreamError(fmt.Errorf(\"%w: expected 1 argument, received %d\", sqlutil.ErrorBadArgumentCount, len(args)))\n\t}\n\n\tvar (\n\t\tcolumn = args[0]\n\t\tfrom   = query.TimeRange.From\n\t\tto     = query.TimeRange.To\n\t)\n\n\treturn fmt.Sprintf(\"%s >= %s AND %s <= %s\", column, timeToDateTime64(from), column, timeToDateTime64(to)), nil\n}\n\nfunc DateFilter(query *sqlutil.Query, args []string) (string, error) {\n\tif len(args) != 1 {\n\t\treturn \"\", backend.DownstreamError(fmt.Errorf(\"%w: expected 1 argument, received %d\", sqlutil.ErrorBadArgumentCount, len(args)))\n\t}\n\tvar (\n\t\tcolumn = args[0]\n\t\tfrom   = query.TimeRange.From\n\t\tto     = query.TimeRange.To\n\t)\n\n\treturn fmt.Sprintf(\"%s >= %s AND %s <= %s\", column, timeToDate(from), column, timeToDate(to)), nil\n}\n\nfunc DateTimeFilter(query *sqlutil.Query, args []string) (string, error) {\n\tif len(args) != 2 {\n\t\treturn \"\", backend.DownstreamError(fmt.Errorf(\"%w: expected 2 arguments, received %d\", sqlutil.ErrorBadArgumentCount, len(args)))\n\t}\n\tvar (\n\t\tdateColumn = args[0]\n\t\ttimeColumn = args[1]\n\t\tfrom       = query.TimeRange.From\n\t\tto         = query.TimeRange.To\n\t)\n\n\tdateFilter := fmt.Sprintf(\"(%s >= %s AND %s <= %s)\", dateColumn, timeToDate(from), dateColumn, timeToDate(to))\n\ttimeFilter := fmt.Sprintf(\"(%s >= %s AND %s <= %s)\", timeColumn, timeToDateTime(from), timeColumn, timeToDateTime(to))\n\treturn fmt.Sprintf(\"%s AND %s\", dateFilter, timeFilter), nil\n}\n\nfunc TimeInterval(query *sqlutil.Query, args []string) (string, error) {\n\tif len(args) != 1 {\n\t\treturn \"\", backend.DownstreamError(fmt.Errorf(\"%w: expected 1 argument, received %d\", sqlutil.ErrorBadArgumentCount, len(args)))\n\t}\n\n\tseconds := math.Max(query.Interval.Seconds(), 1)\n\treturn fmt.Sprintf(\"toStartOfInterval(toDateTime(%s), INTERVAL %d second)\", args[0], int(seconds)), nil\n}\n\nfunc TimeIntervalMs(query *sqlutil.Query, args []string) (string, error) {\n\tif len(args) != 1 {\n\t\treturn \"\", backend.DownstreamError(fmt.Errorf(\"%w: expected 1 argument, received %d\", sqlutil.ErrorBadArgumentCount, len(args)))\n\t}\n\n\tmilliseconds := math.Max(float64(query.Interval.Milliseconds()), 1)\n\treturn fmt.Sprintf(\"toStartOfInterval(toDateTime64(%s, 3), INTERVAL %d millisecond)\", args[0], int(milliseconds)), nil\n}\n\nfunc IntervalSeconds(query *sqlutil.Query, args []string) (string, error) {\n\tseconds := math.Max(query.Interval.Seconds(), 1)\n\treturn fmt.Sprintf(\"%d\", int(seconds)), nil\n}\n\n// RemoveQuotesInArgs remove all quotes from macro arguments and return\nfunc RemoveQuotesInArgs(args []string) []string {\n\tupdatedArgs := []string{}\n\tfor _, arg := range args {\n\t\treplacer := strings.NewReplacer(\n\t\t\t\"\\\"\", \"\",\n\t\t\t\"'\", \"\",\n\t\t)\n\t\tupdatedArgs = append(updatedArgs, replacer.Replace(arg))\n\t}\n\treturn updatedArgs\n}\n\n// IsValidComparisonPredicates checks for a string and return true if it is a valid SQL comparison predicate\nfunc IsValidComparisonPredicates(comparison_predicates string) bool {\n\tswitch comparison_predicates {\n\tcase \"=\", \"!=\", \"<>\", \"<\", \"<=\", \">\", \">=\":\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Macros is a map of all macro functions\nvar Macros = map[string]sqlds.MacroFunc{\n\t\"fromTime\":        FromTimeFilter,\n\t\"toTime\":          ToTimeFilter,\n\t\"fromTime_ms\":     FromTimeFilterMs,\n\t\"toTime_ms\":       ToTimeFilterMs,\n\t\"timeFilter\":      TimeFilter,\n\t\"timeFilter_ms\":   TimeFilterMs,\n\t\"dateFilter\":      DateFilter,\n\t\"dateTimeFilter\":  DateTimeFilter,\n\t\"dt\":              DateTimeFilter,\n\t\"timeInterval\":    TimeInterval,\n\t\"timeInterval_ms\": TimeIntervalMs,\n\t\"interval_s\":      IntervalSeconds,\n}\n"
  },
  {
    "path": "pkg/macros/macros_test.go",
    "content": "package macros\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/grafana/grafana-plugin-sdk-go/backend\"\n\t\"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil\"\n\t\"github.com/grafana/sqlds/v5\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype ClickhouseDriver struct {\n\tsqlds.Driver\n}\n\ntype MockDB struct {\n\tClickhouseDriver\n}\n\nfunc (h *ClickhouseDriver) Macros() sqlds.Macros {\n\treturn Macros\n}\n\nfunc TestTimeToDate(t *testing.T) {\n\td, _ := time.Parse(\"2006-01-02T15:04:05.000Z\", \"2014-11-12T11:45:26.371Z\")\n\n\texpected := \"toDate('2014-11-12')\"\n\tresult := timeToDate(d)\n\n\tif expected != result {\n\t\tt.Errorf(\"unexpected output. expected: %s got: %s\", expected, result)\n\t}\n}\n\nfunc TestTimeToDateTime(t *testing.T) {\n\tdt := time.Unix(1708430068, 0)\n\n\texpected := \"toDateTime(1708430068)\"\n\tresult := timeToDateTime(dt)\n\n\tif expected != result {\n\t\tt.Errorf(\"unexpected output. expected: %s got: %s\", expected, result)\n\t}\n}\n\nfunc TestTimeToDateTime64(t *testing.T) {\n\tdt := time.UnixMilli(1708430068123)\n\n\texpected := \"fromUnixTimestamp64Milli(1708430068123)\"\n\tresult := timeToDateTime64(dt)\n\n\tif expected != result {\n\t\tt.Errorf(\"unexpected output. expected: %s got: %s\", expected, result)\n\t}\n}\n\nfunc TestMacroFromTimeFilter(t *testing.T) {\n\tfrom, _ := time.Parse(\"2006-01-02T15:04:05.000Z\", \"2014-11-12T11:45:26.371Z\")\n\tto, _ := time.Parse(\"2006-01-02T15:04:05.000Z\", \"2015-11-12T11:45:26.371Z\")\n\tquery := sqlutil.Query{\n\t\tTimeRange: backend.TimeRange{\n\t\t\tFrom: from,\n\t\t\tTo:   to,\n\t\t},\n\t\tRawSQL: \"select foo from foo where bar > $__fromTime\",\n\t}\n\ttests := []struct {\n\t\twant    string\n\t\twantErr bool\n\t\tname    string\n\t}{\n\t\t{\n\t\t\tname: \"should return timeFilter\",\n\t\t\twant: \"toDateTime(1415792726)\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := FromTimeFilter(&query, []string{})\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"macroFromTimeFilter() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestMacroToTimeFilter(t *testing.T) {\n\tfrom, _ := time.Parse(\"2006-01-02T15:04:05.000Z\", \"2014-11-12T11:45:26.371Z\")\n\tto, _ := time.Parse(\"2006-01-02T15:04:05.000Z\", \"2015-11-12T11:45:26.371Z\")\n\tquery := sqlutil.Query{\n\t\tTimeRange: backend.TimeRange{\n\t\t\tFrom: from,\n\t\t\tTo:   to,\n\t\t},\n\t\tRawSQL: \"select foo from foo where bar > $__toTime\",\n\t}\n\ttests := []struct {\n\t\twant    string\n\t\twantErr bool\n\t\tname    string\n\t}{\n\t\t{\n\t\t\tname: \"should return timeFilter\",\n\t\t\twant: \"toDateTime(1447328726)\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := ToTimeFilter(&query, []string{})\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"macroToTimeFilter() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestMacroFromTimeFilterMs(t *testing.T) {\n\tfrom, _ := time.Parse(\"2006-01-02T15:04:05.000Z\", \"2014-11-12T11:45:26.371Z\")\n\tto, _ := time.Parse(\"2006-01-02T15:04:05.000Z\", \"2015-11-12T11:45:26.371Z\")\n\tquery := sqlutil.Query{\n\t\tTimeRange: backend.TimeRange{\n\t\t\tFrom: from,\n\t\t\tTo:   to,\n\t\t},\n\t\tRawSQL: \"select foo from foo where bar > $__fromTime\",\n\t}\n\ttests := []struct {\n\t\twant    string\n\t\twantErr bool\n\t\tname    string\n\t}{\n\t\t{\n\t\t\tname: \"should return timeFilter_ms\",\n\t\t\twant: \"fromUnixTimestamp64Milli(1415792726371)\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := FromTimeFilterMs(&query, []string{})\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"macroFromTimeFilterMs() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestMacroToTimeFilterMs(t *testing.T) {\n\tfrom, _ := time.Parse(\"2006-01-02T15:04:05.000Z\", \"2014-11-12T11:45:26.371Z\")\n\tto, _ := time.Parse(\"2006-01-02T15:04:05.000Z\", \"2015-11-12T11:45:26.371Z\")\n\tquery := sqlutil.Query{\n\t\tTimeRange: backend.TimeRange{\n\t\t\tFrom: from,\n\t\t\tTo:   to,\n\t\t},\n\t\tRawSQL: \"select foo from foo where bar > $__toTime\",\n\t}\n\ttests := []struct {\n\t\twant    string\n\t\twantErr bool\n\t\tname    string\n\t}{\n\t\t{\n\t\t\tname: \"should return timeFilter_ms\",\n\t\t\twant: \"fromUnixTimestamp64Milli(1447328726371)\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := ToTimeFilterMs(&query, []string{})\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"macroToTimeFilterMs() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestMacroDateFilter(t *testing.T) {\n\tfrom, _ := time.Parse(\"2006-01-02T15:04:05.000Z\", \"2014-11-12T11:45:26.371Z\")\n\tto, _ := time.Parse(\"2006-01-02T15:04:05.000Z\", \"2015-11-12T11:45:26.371Z\")\n\tquery := sqlutil.Query{\n\t\tTimeRange: backend.TimeRange{\n\t\t\tFrom: from,\n\t\t\tTo:   to,\n\t\t},\n\t}\n\tgot, err := DateFilter(&query, []string{\"dateCol\"})\n\tassert.Nil(t, err)\n\tassert.Equal(t, \"dateCol >= toDate('2014-11-12') AND dateCol <= toDate('2015-11-12')\", got)\n}\n\nfunc TestMacroDateTimeFilter(t *testing.T) {\n\tfrom, _ := time.Parse(\"2006-01-02T15:04:05.000Z\", \"2014-11-12T11:45:26.371Z\")\n\tto, _ := time.Parse(\"2006-01-02T15:04:05.000Z\", \"2015-11-12T11:45:26.371Z\")\n\tquery := sqlutil.Query{\n\t\tTimeRange: backend.TimeRange{\n\t\t\tFrom: from,\n\t\t\tTo:   to,\n\t\t},\n\t}\n\tgot, err := DateTimeFilter(&query, []string{\"dateCol\", \"timeCol\"})\n\tassert.Nil(t, err)\n\tassert.Equal(t, \"(dateCol >= toDate('2014-11-12') AND dateCol <= toDate('2015-11-12')) AND (timeCol >= toDateTime(1415792726) AND timeCol <= toDateTime(1447328726))\", got)\n}\n\nfunc TestMacroTimeInterval(t *testing.T) {\n\tquery := sqlutil.Query{\n\t\tRawSQL:   \"select $__timeInterval(col) from foo\",\n\t\tInterval: time.Duration(20000000000),\n\t}\n\tgot, err := TimeInterval(&query, []string{\"col\"})\n\tassert.Nil(t, err)\n\tassert.Equal(t, \"toStartOfInterval(toDateTime(col), INTERVAL 20 second)\", got)\n}\n\nfunc TestMacroTimeIntervalMs(t *testing.T) {\n\tquery := sqlutil.Query{\n\t\tRawSQL:   \"select $__timeInterval_ms(col) from foo\",\n\t\tInterval: time.Duration(20000000000),\n\t}\n\tgot, err := TimeIntervalMs(&query, []string{\"col\"})\n\tassert.Nil(t, err)\n\tassert.Equal(t, \"toStartOfInterval(toDateTime64(col, 3), INTERVAL 20000 millisecond)\", got)\n}\n\nfunc TestMacroIntervalSeconds(t *testing.T) {\n\tquery := sqlutil.Query{\n\t\tRawSQL:   \"select toStartOfInterval(col, INTERVAL $__interval_s second) AS time from foo\",\n\t\tInterval: time.Duration(20000000000),\n\t}\n\tgot, err := IntervalSeconds(&query, []string{})\n\tassert.Nil(t, err)\n\tassert.Equal(t, \"20\", got)\n}\n\n// test sqlds query interpolation with clickhouse filters used\nfunc TestInterpolate(t *testing.T) {\n\tfrom, _ := time.Parse(\"2006-01-02T15:04:05.000Z\", \"2014-11-12T11:45:26.123Z\")\n\tto, _ := time.Parse(\"2006-01-02T15:04:05.000Z\", \"2015-11-12T11:45:26.456Z\")\n\n\ttableName := \"my_table\"\n\ttableColumn := \"my_col\"\n\n\ttype test struct {\n\t\tname   string\n\t\tinput  string\n\t\toutput string\n\t}\n\n\ttests := []test{\n\t\t{input: \"select * from foo where $__timeFilter(cast(sth as timestamp))\", output: \"select * from foo where cast(sth as timestamp) >= toDateTime(1415792726) AND cast(sth as timestamp) <= toDateTime(1447328726)\", name: \"clickhouse timeFilter\"},\n\t\t{input: \"select * from foo where $__timeFilter(cast(sth as timestamp) )\", output: \"select * from foo where cast(sth as timestamp) >= toDateTime(1415792726) AND cast(sth as timestamp) <= toDateTime(1447328726)\", name: \"clickhouse timeFilter with empty spaces\"},\n\t\t{input: \"select * from foo where $__timeFilter_ms(cast(sth as timestamp))\", output: \"select * from foo where cast(sth as timestamp) >= fromUnixTimestamp64Milli(1415792726123) AND cast(sth as timestamp) <= fromUnixTimestamp64Milli(1447328726456)\", name: \"clickhouse timeFilter_ms\"},\n\t\t{input: \"select * from foo where $__timeFilter_ms(cast(sth as timestamp) )\", output: \"select * from foo where cast(sth as timestamp) >= fromUnixTimestamp64Milli(1415792726123) AND cast(sth as timestamp) <= fromUnixTimestamp64Milli(1447328726456)\", name: \"clickhouse timeFilter_ms with empty spaces\"},\n\t\t{input: \"select * from foo where ( date >= $__fromTime and date <= $__toTime ) limit 100\", output: \"select * from foo where ( date >= toDateTime(1415792726) and date <= toDateTime(1447328726) ) limit 100\", name: \"clickhouse fromTime and toTime\"},\n\t\t{input: \"select * from foo where ( date >= $__fromTime ) and ( date <= $__toTime ) limit 100\", output: \"select * from foo where ( date >= toDateTime(1415792726) ) and ( date <= toDateTime(1447328726) ) limit 100\", name: \"clickhouse fromTime and toTime inside a complex clauses\"},\n\t\t{input: \"select * from foo where ( date >= $__fromTime_ms and date <= $__toTime_ms ) limit 100\", output: \"select * from foo where ( date >= fromUnixTimestamp64Milli(1415792726123) and date <= fromUnixTimestamp64Milli(1447328726456) ) limit 100\", name: \"clickhouse fromTime_ms and toTime_ms\"},\n\t\t{input: \"select * from foo where ( date >= $__fromTime_ms ) and ( date <= $__toTime_ms ) limit 100\", output: \"select * from foo where ( date >= fromUnixTimestamp64Milli(1415792726123) ) and ( date <= fromUnixTimestamp64Milli(1447328726456) ) limit 100\", name: \"clickhouse fromTime_ms and toTime_ms inside a complex clauses\"},\n\t}\n\n\tfor i, tc := range tests {\n\t\tdriver := MockDB{}\n\t\tt.Run(fmt.Sprintf(\"[%d/%d] %s\", i+1, len(tests), tc.name), func(t *testing.T) {\n\t\t\tquery := &sqlutil.Query{\n\t\t\t\tRawSQL: tc.input,\n\t\t\t\tTable:  tableName,\n\t\t\t\tColumn: tableColumn,\n\t\t\t\tTimeRange: backend.TimeRange{\n\t\t\t\t\tFrom: from,\n\t\t\t\t\tTo:   to,\n\t\t\t\t},\n\t\t\t}\n\t\t\tinterpolatedQuery, err := sqlds.Interpolate(&driver, query)\n\t\t\trequire.Nil(t, err)\n\t\t\tassert.Equal(t, tc.output, interpolatedQuery)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/main.go",
    "content": "package main\n\nimport (\n\t\"os\"\n\n\t\"github.com/grafana/clickhouse-datasource/pkg/plugin\"\n\t\"github.com/grafana/grafana-plugin-sdk-go/backend/datasource\"\n\t\"github.com/grafana/grafana-plugin-sdk-go/backend/log\"\n)\n\nfunc main() {\n\tif err := datasource.Manage(\"grafana-clickhouse-datasource\", plugin.NewDatasource, datasource.ManageOpts{}); err != nil {\n\t\tlog.DefaultLogger.Error(err.Error())\n\t\tos.Exit(1)\n\t}\n}\n"
  },
  {
    "path": "pkg/plugin/connection_error.go",
    "content": "package plugin\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"net\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/ClickHouse/clickhouse-go/v2\"\n\t\"github.com/pkg/errors\"\n)\n\ntype ConnectionErrorCategory string\n\nconst (\n\tConnectionErrorCategoryAuth    ConnectionErrorCategory = \"auth\"\n\tConnectionErrorCategoryNetwork ConnectionErrorCategory = \"network\"\n\tConnectionErrorCategoryTLS     ConnectionErrorCategory = \"tls\"\n\tConnectionErrorCategoryTimeout ConnectionErrorCategory = \"timeout\"\n\tConnectionErrorCategoryConfig  ConnectionErrorCategory = \"config\"\n\tConnectionErrorCategoryServer  ConnectionErrorCategory = \"server\"\n\tConnectionErrorCategoryUnknown ConnectionErrorCategory = \"unknown\"\n)\n\n// configValidationErrors maps sentinel errors from errors.go to their category.\n// These are returned by LoadSettings before a connection is attempted.\nvar configValidationErrors = map[error]ConnectionErrorCategory{\n\tErrorMessageInvalidHost:       ConnectionErrorCategoryConfig,\n\tErrorMessageInvalidPort:       ConnectionErrorCategoryConfig,\n\tErrorMessageInvalidJSON:       ConnectionErrorCategoryConfig,\n\tErrorMessageInvalidProtocol:   ConnectionErrorCategoryConfig,\n\tErrorInvalidClientCertificate: ConnectionErrorCategoryTLS,\n\tErrorInvalidCACertificate:     ConnectionErrorCategoryTLS,\n}\n\n// authExceptionCodes are ClickHouse exception codes that indicate auth failures.\nvar authExceptionCodes = map[int32]bool{\n\t516: true, // AUTHENTICATION_FAILED\n\t497: true, // NOT_ENOUGH_PRIVILEGES\n\t164: true, // READONLY\n}\n\n// httpStatusCode extracts the HTTP status code from clickhouse-go HTTP transport\n// error strings of the form \"[HTTP 403] ...\". Returns 0 if not found.\nfunc httpStatusCode(errStr string) int {\n\tconst prefix = \"[HTTP \"\n\ti := strings.Index(errStr, prefix)\n\tif i == -1 {\n\t\treturn 0\n\t}\n\trest := errStr[i+len(prefix):]\n\tif len(rest) < 4 || rest[3] != ']' {\n\t\treturn 0\n\t}\n\tcode, err := strconv.Atoi(rest[:3])\n\tif err != nil {\n\t\treturn 0\n\t}\n\treturn code\n}\n\n// CategorizeConnectionError classifies a connection error into a ConnectionErrorCategory.\n// Typed errors are checked first; string matching is used as a fallback for HTTP\n// transport errors that the clickhouse-go driver surfaces as plain strings.\nfunc CategorizeConnectionError(err error) ConnectionErrorCategory {\n\tif err == nil {\n\t\treturn ConnectionErrorCategoryUnknown\n\t}\n\n\tfor sentinel, category := range configValidationErrors {\n\t\tif errors.Is(err, sentinel) {\n\t\t\treturn category\n\t\t}\n\t}\n\n\tif errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {\n\t\treturn ConnectionErrorCategoryTimeout\n\t}\n\tif errors.Is(err, clickhouse.ErrAcquireConnTimeout) {\n\t\treturn ConnectionErrorCategoryTimeout\n\t}\n\tif errors.Is(err, clickhouse.ErrAcquireConnNoAddress) {\n\t\treturn ConnectionErrorCategoryConfig\n\t}\n\n\tvar exception *clickhouse.Exception\n\tif errors.As(err, &exception) {\n\t\tif authExceptionCodes[exception.Code] {\n\t\t\treturn ConnectionErrorCategoryAuth\n\t\t}\n\t\treturn ConnectionErrorCategoryServer\n\t}\n\n\tvar certVerifyErr *tls.CertificateVerificationError\n\tif errors.As(err, &certVerifyErr) {\n\t\treturn ConnectionErrorCategoryTLS\n\t}\n\tvar unknownAuthErr x509.UnknownAuthorityError\n\tif errors.As(err, &unknownAuthErr) {\n\t\treturn ConnectionErrorCategoryTLS\n\t}\n\tvar certInvalidErr x509.CertificateInvalidError\n\tif errors.As(err, &certInvalidErr) {\n\t\treturn ConnectionErrorCategoryTLS\n\t}\n\tvar hostnameErr x509.HostnameError\n\tif errors.As(err, &hostnameErr) {\n\t\treturn ConnectionErrorCategoryTLS\n\t}\n\n\tvar opErr *net.OpError\n\tif errors.As(err, &opErr) && opErr.Op == \"dial\" {\n\t\treturn ConnectionErrorCategoryNetwork\n\t}\n\tvar dnsErr *net.DNSError\n\tif errors.As(err, &dnsErr) {\n\t\treturn ConnectionErrorCategoryNetwork\n\t}\n\n\terrStr := err.Error()\n\n\t// TLS string patterns must come before network patterns — TLS errors are often\n\t// wrapped inside a net.OpError with Op \"read\" rather than \"dial\".\n\tif strings.Contains(errStr, \"tls:\") ||\n\t\tstrings.Contains(errStr, \"x509:\") ||\n\t\tstrings.Contains(errStr, \"certificate\") {\n\t\treturn ConnectionErrorCategoryTLS\n\t}\n\n\tif code := httpStatusCode(errStr); code != 0 {\n\t\tswitch {\n\t\tcase code == 401 || code == 403:\n\t\t\treturn ConnectionErrorCategoryAuth\n\t\tcase code == 408 || code == 504:\n\t\t\treturn ConnectionErrorCategoryTimeout\n\t\tcase code == 502 || code == 503:\n\t\t\treturn ConnectionErrorCategoryNetwork\n\t\tcase code >= 400 && code < 500:\n\t\t\treturn ConnectionErrorCategoryAuth // remaining 4xx treated as access-control\n\t\tcase code >= 500:\n\t\t\treturn ConnectionErrorCategoryServer\n\t\t}\n\t}\n\n\tif strings.Contains(errStr, \"timeout\") || strings.Contains(errStr, \"deadline exceeded\") {\n\t\treturn ConnectionErrorCategoryTimeout\n\t}\n\tif strings.Contains(errStr, \"connection refused\") ||\n\t\tstrings.Contains(errStr, \"no such host\") ||\n\t\tstrings.Contains(errStr, \"network is unreachable\") ||\n\t\tstrings.Contains(errStr, \"connection reset by peer\") {\n\t\treturn ConnectionErrorCategoryNetwork\n\t}\n\tif strings.Contains(errStr, \"DB::Exception\") {\n\t\treturn ConnectionErrorCategoryServer\n\t}\n\n\treturn ConnectionErrorCategoryUnknown\n}\n"
  },
  {
    "path": "pkg/plugin/connection_error_test.go",
    "content": "package plugin\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/ClickHouse/clickhouse-go/v2\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCategorizeConnectionError(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\terr      error\n\t\texpected ConnectionErrorCategory\n\t}{\n\t\t// nil\n\t\t{\n\t\t\tname:     \"nil error\",\n\t\t\terr:      nil,\n\t\t\texpected: ConnectionErrorCategoryUnknown,\n\t\t},\n\n\t\t// --- Timeout ---\n\t\t{\n\t\t\tname:     \"context deadline exceeded\",\n\t\t\terr:      context.DeadlineExceeded,\n\t\t\texpected: ConnectionErrorCategoryTimeout,\n\t\t},\n\t\t{\n\t\t\tname:     \"context canceled\",\n\t\t\terr:      context.Canceled,\n\t\t\texpected: ConnectionErrorCategoryTimeout,\n\t\t},\n\t\t{\n\t\t\tname:     \"clickhouse acquire conn timeout\",\n\t\t\terr:      clickhouse.ErrAcquireConnTimeout,\n\t\t\texpected: ConnectionErrorCategoryTimeout,\n\t\t},\n\t\t{\n\t\t\tname:     \"wrapped context deadline exceeded\",\n\t\t\terr:      fmt.Errorf(\"outer: %w\", context.DeadlineExceeded),\n\t\t\texpected: ConnectionErrorCategoryTimeout,\n\t\t},\n\t\t{\n\t\t\tname:     \"timeout in error string\",\n\t\t\terr:      errors.New(\"read tcp: i/o timeout\"),\n\t\t\texpected: ConnectionErrorCategoryTimeout,\n\t\t},\n\t\t{\n\t\t\tname:     \"deadline exceeded in error string\",\n\t\t\terr:      errors.New(\"operation timed out: deadline exceeded\"),\n\t\t\texpected: ConnectionErrorCategoryTimeout,\n\t\t},\n\t\t{\n\t\t\tname:     \"HTTP 408\",\n\t\t\terr:      errors.New(\"clickhouse [HTTP 408]: request timeout\"),\n\t\t\texpected: ConnectionErrorCategoryTimeout,\n\t\t},\n\t\t{\n\t\t\tname:     \"HTTP 504\",\n\t\t\terr:      errors.New(\"clickhouse [HTTP 504]: gateway timeout\"),\n\t\t\texpected: ConnectionErrorCategoryTimeout,\n\t\t},\n\n\t\t// --- Config ---\n\t\t{\n\t\t\tname:     \"clickhouse no address\",\n\t\t\terr:      clickhouse.ErrAcquireConnNoAddress,\n\t\t\texpected: ConnectionErrorCategoryConfig,\n\t\t},\n\t\t{\n\t\t\tname:     \"wrapped no address\",\n\t\t\terr:      fmt.Errorf(\"connect: %w\", clickhouse.ErrAcquireConnNoAddress),\n\t\t\texpected: ConnectionErrorCategoryConfig,\n\t\t},\n\n\t\t// --- Auth — native, via clickhouse.Exception ---\n\t\t{\n\t\t\tname:     \"exception 516 AUTHENTICATION_FAILED\",\n\t\t\terr:      &clickhouse.Exception{Code: 516, Message: \"Authentication failed\"},\n\t\t\texpected: ConnectionErrorCategoryAuth,\n\t\t},\n\t\t{\n\t\t\tname:     \"exception 497 NOT_ENOUGH_PRIVILEGES\",\n\t\t\terr:      &clickhouse.Exception{Code: 497, Message: \"Not enough privileges\"},\n\t\t\texpected: ConnectionErrorCategoryAuth,\n\t\t},\n\t\t{\n\t\t\tname:     \"exception 164 READONLY\",\n\t\t\terr:      &clickhouse.Exception{Code: 164, Message: \"Attempt to execute query in read-only mode\"},\n\t\t\texpected: ConnectionErrorCategoryAuth,\n\t\t},\n\t\t{\n\t\t\tname:     \"wrapped exception 516\",\n\t\t\terr:      fmt.Errorf(\"ping: %w\", &clickhouse.Exception{Code: 516, Message: \"Authentication failed\"}),\n\t\t\texpected: ConnectionErrorCategoryAuth,\n\t\t},\n\n\t\t// --- Auth — HTTP, via status code strings ---\n\t\t{\n\t\t\tname:     \"HTTP 401\",\n\t\t\terr:      errors.New(\"clickhouse [HTTP 401]: Unauthorized\"),\n\t\t\texpected: ConnectionErrorCategoryAuth,\n\t\t},\n\t\t{\n\t\t\tname:     \"HTTP 403\",\n\t\t\terr:      errors.New(\"clickhouse [HTTP 403]: Forbidden\"),\n\t\t\texpected: ConnectionErrorCategoryAuth,\n\t\t},\n\t\t{\n\t\t\tname:     \"HTTP 400 falls through to auth (4xx bucket)\",\n\t\t\terr:      errors.New(\"clickhouse [HTTP 400]: Bad Request\"),\n\t\t\texpected: ConnectionErrorCategoryAuth,\n\t\t},\n\n\t\t// --- Server — native, via clickhouse.Exception ---\n\t\t{\n\t\t\tname:     \"exception with non-auth code\",\n\t\t\terr:      &clickhouse.Exception{Code: 60, Message: \"Table does not exist\"},\n\t\t\texpected: ConnectionErrorCategoryServer,\n\t\t},\n\t\t{\n\t\t\tname:     \"DB::Exception in string\",\n\t\t\terr:      errors.New(\"DB::Exception: Table test.foo doesn't exist. (UNKNOWN_TABLE)\"),\n\t\t\texpected: ConnectionErrorCategoryServer,\n\t\t},\n\t\t{\n\t\t\tname:     \"HTTP 500\",\n\t\t\terr:      errors.New(\"clickhouse [HTTP 500]: Internal Server Error\"),\n\t\t\texpected: ConnectionErrorCategoryServer,\n\t\t},\n\t\t{\n\t\t\tname:     \"HTTP 504 already handled by timeout, 500 is server\",\n\t\t\terr:      errors.New(\"[HTTP 500]\"),\n\t\t\texpected: ConnectionErrorCategoryServer,\n\t\t},\n\n\t\t// --- Network ---\n\t\t{\n\t\t\tname: \"net.OpError dial connection refused\",\n\t\t\terr: &net.OpError{\n\t\t\t\tOp:  \"dial\",\n\t\t\t\tNet: \"tcp\",\n\t\t\t\tErr: &net.AddrError{Err: \"connection refused\"},\n\t\t\t},\n\t\t\texpected: ConnectionErrorCategoryNetwork,\n\t\t},\n\t\t{\n\t\t\tname:     \"net.DNSError\",\n\t\t\terr:      &net.DNSError{Err: \"no such host\", Name: \"invalid.example.com\"},\n\t\t\texpected: ConnectionErrorCategoryNetwork,\n\t\t},\n\t\t{\n\t\t\tname:     \"connection refused in string\",\n\t\t\terr:      errors.New(\"dial tcp: connect: connection refused\"),\n\t\t\texpected: ConnectionErrorCategoryNetwork,\n\t\t},\n\t\t{\n\t\t\tname:     \"no such host in string\",\n\t\t\terr:      errors.New(\"dial tcp: lookup invalid.example.com: no such host\"),\n\t\t\texpected: ConnectionErrorCategoryNetwork,\n\t\t},\n\t\t{\n\t\t\tname:     \"network is unreachable\",\n\t\t\terr:      errors.New(\"dial tcp: connect: network is unreachable\"),\n\t\t\texpected: ConnectionErrorCategoryNetwork,\n\t\t},\n\t\t{\n\t\t\tname:     \"connection reset by peer\",\n\t\t\terr:      errors.New(\"read tcp: connection reset by peer\"),\n\t\t\texpected: ConnectionErrorCategoryNetwork,\n\t\t},\n\t\t{\n\t\t\tname:     \"HTTP 502\",\n\t\t\terr:      errors.New(\"clickhouse [HTTP 502]: Bad Gateway\"),\n\t\t\texpected: ConnectionErrorCategoryNetwork,\n\t\t},\n\t\t{\n\t\t\tname:     \"HTTP 503\",\n\t\t\terr:      errors.New(\"clickhouse [HTTP 503]: Service Unavailable\"),\n\t\t\texpected: ConnectionErrorCategoryNetwork,\n\t\t},\n\n\t\t// --- TLS ---\n\t\t{\n\t\t\tname: \"tls.CertificateVerificationError\",\n\t\t\terr: &tls.CertificateVerificationError{\n\t\t\t\tUnverifiedCertificates: []*x509.Certificate{},\n\t\t\t\tErr:                    errors.New(\"certificate signed by unknown authority\"),\n\t\t\t},\n\t\t\texpected: ConnectionErrorCategoryTLS,\n\t\t},\n\t\t{\n\t\t\tname:     \"x509.UnknownAuthorityError\",\n\t\t\terr:      x509.UnknownAuthorityError{},\n\t\t\texpected: ConnectionErrorCategoryTLS,\n\t\t},\n\t\t{\n\t\t\tname: \"x509.CertificateInvalidError\",\n\t\t\terr: x509.CertificateInvalidError{\n\t\t\t\tCert:   &x509.Certificate{},\n\t\t\t\tReason: x509.Expired,\n\t\t\t},\n\t\t\texpected: ConnectionErrorCategoryTLS,\n\t\t},\n\t\t{\n\t\t\tname: \"x509.HostnameError\",\n\t\t\terr: x509.HostnameError{\n\t\t\t\tCertificate: &x509.Certificate{},\n\t\t\t\tHost:        \"wrong.host.example.com\",\n\t\t\t},\n\t\t\texpected: ConnectionErrorCategoryTLS,\n\t\t},\n\t\t{\n\t\t\tname:     \"tls: in error string\",\n\t\t\terr:      errors.New(\"tls: handshake failure\"),\n\t\t\texpected: ConnectionErrorCategoryTLS,\n\t\t},\n\t\t{\n\t\t\tname:     \"x509: in error string\",\n\t\t\terr:      errors.New(\"x509: certificate has expired or is not yet valid\"),\n\t\t\texpected: ConnectionErrorCategoryTLS,\n\t\t},\n\t\t{\n\t\t\tname:     \"certificate in error string\",\n\t\t\terr:      errors.New(\"remote error: tls: bad certificate\"),\n\t\t\texpected: ConnectionErrorCategoryTLS,\n\t\t},\n\t\t{\n\t\t\tname: \"net.OpError wrapping TLS error (read op, not dial)\",\n\t\t\terr: &net.OpError{\n\t\t\t\tOp:  \"read\",\n\t\t\t\tNet: \"tcp\",\n\t\t\t\tErr: errors.New(\"tls: certificate signed by unknown authority\"),\n\t\t\t},\n\t\t\texpected: ConnectionErrorCategoryTLS,\n\t\t},\n\n\t\t// --- Config validation sentinel errors (errors.go) ---\n\t\t{\n\t\t\tname:     \"invalid host\",\n\t\t\terr:      ErrorMessageInvalidHost,\n\t\t\texpected: ConnectionErrorCategoryConfig,\n\t\t},\n\t\t{\n\t\t\tname:     \"invalid port\",\n\t\t\terr:      ErrorMessageInvalidPort,\n\t\t\texpected: ConnectionErrorCategoryConfig,\n\t\t},\n\t\t{\n\t\t\tname:     \"invalid json\",\n\t\t\terr:      ErrorMessageInvalidJSON,\n\t\t\texpected: ConnectionErrorCategoryConfig,\n\t\t},\n\t\t{\n\t\t\tname:     \"invalid protocol\",\n\t\t\terr:      ErrorMessageInvalidProtocol,\n\t\t\texpected: ConnectionErrorCategoryConfig,\n\t\t},\n\t\t{\n\t\t\tname:     \"invalid client certificate\",\n\t\t\terr:      ErrorInvalidClientCertificate,\n\t\t\texpected: ConnectionErrorCategoryTLS,\n\t\t},\n\t\t{\n\t\t\tname:     \"invalid CA certificate\",\n\t\t\terr:      ErrorInvalidCACertificate,\n\t\t\texpected: ConnectionErrorCategoryTLS,\n\t\t},\n\t\t{\n\t\t\tname:     \"wrapped invalid host (via backend.DownstreamError)\",\n\t\t\terr:      fmt.Errorf(\"downstream: %w\", ErrorMessageInvalidHost),\n\t\t\texpected: ConnectionErrorCategoryConfig,\n\t\t},\n\n\t\t// --- Unknown ---\n\t\t{\n\t\t\tname:     \"completely unknown error\",\n\t\t\terr:      errors.New(\"something went very wrong\"),\n\t\t\texpected: ConnectionErrorCategoryUnknown,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := CategorizeConnectionError(tt.err)\n\t\t\tassert.Equal(t, tt.expected, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/plugin/datasource.go",
    "content": "package plugin\n\nimport (\n\t\"context\"\n\n\t\"github.com/grafana/grafana-plugin-sdk-go/backend\"\n\t\"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt\"\n\tschemas \"github.com/grafana/schemads\"\n\t\"github.com/grafana/sqlds/v5\"\n)\n\n// clickhouseInstance wraps the sqlds-managed instance so its Dispose also\n// closes the SchemaProvider's shared *sql.DB. Embedding *sqlds.SQLDatasource\n// promotes every handler method (QueryData, CheckHealth, CallResource, …) so\n// type assertions on instancemgmt.Instance keep working unchanged.\ntype clickhouseInstance struct {\n\t*sqlds.SQLDatasource\n\tschema *SchemaProvider\n}\n\nfunc (i *clickhouseInstance) Dispose() {\n\ti.SQLDatasource.Dispose()\n\tif err := i.schema.Close(); err != nil {\n\t\tbackend.Logger.Error(\"failed to close schema provider\", \"error\", err)\n\t}\n}\n\nfunc NewDatasource(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {\n\tclickhousePlugin := Clickhouse{}\n\tds := sqlds.NewDatasource(&clickhousePlugin)\n\tpluginSettings := clickhousePlugin.Settings(ctx, settings)\n\tif pluginSettings.ForwardHeaders {\n\t\tds.EnableMultipleConnections = true\n\t}\n\n\tschemaProvider := NewSchemaProvider(ctx, &clickhousePlugin, settings)\n\tds.ResourceMiddleware = func(next backend.CallResourceHandler) backend.CallResourceHandler {\n\t\treturn schemas.NewSchemaDatasource(\n\t\t\tschemaProvider,\n\t\t\tschemaProvider,\n\t\t\tschemaProvider,\n\t\t\tnil, // no table parameter values handler\n\t\t\tschemaProvider,\n\t\t\tnext,\n\t\t)\n\t}\n\n\tinst, err := ds.NewDatasource(ctx, settings)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tsqlInst, ok := inst.(*sqlds.SQLDatasource)\n\tif !ok {\n\t\t// Defensive: if sqlds ever returns a different concrete type we can't\n\t\t// embed it cleanly. Fall back to the unwrapped instance — the schema\n\t\t// DB will then leak on settings change but the plugin still works.\n\t\treturn inst, nil\n\t}\n\treturn &clickhouseInstance{SQLDatasource: sqlInst, schema: schemaProvider}, nil\n}\n"
  },
  {
    "path": "pkg/plugin/driver.go",
    "content": "package plugin\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/ClickHouse/clickhouse-go/v2\"\n\t\"github.com/grafana/clickhouse-datasource/pkg/converters\"\n\t\"github.com/grafana/clickhouse-datasource/pkg/macros\"\n\t\"github.com/grafana/grafana-plugin-sdk-go/backend\"\nsdkproxy \"github.com/grafana/grafana-plugin-sdk-go/backend/proxy\"\n\t\"github.com/grafana/grafana-plugin-sdk-go/backend/tracing\"\n\t\"github.com/grafana/grafana-plugin-sdk-go/build/buildinfo\"\n\t\"github.com/grafana/grafana-plugin-sdk-go/data\"\n\t\"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil\"\n\tschemas \"github.com/grafana/schemads\"\n\t\"github.com/grafana/sqlds/v5\"\n\t\"github.com/pkg/errors\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/trace\"\n\t\"golang.org/x/net/proxy\"\n)\n\ntype grafanaHeadersKeyType struct{}\n\nvar grafanaHeadersKey = grafanaHeadersKeyType{}\n\ntype grafanaHeaders struct {\n\tDashboardUID string\n\tPanelID      string\n\tRuleUID      string\n}\n\n// Clickhouse defines how to connect to a Clickhouse datasource\ntype Clickhouse struct {\n\tSchemaDatasource *schemas.SchemaDatasource\n}\n\n// getTLSConfig returns tlsConfig from settings\n// logic reused from https://github.com/grafana/grafana/blob/615c153b3a2e4d80cff263e67424af6edb992211/pkg/models/datasource_cache.go#L211\nfunc getTLSConfig(settings Settings) (*tls.Config, error) {\n\ttlsConfig := &tls.Config{\n\t\tInsecureSkipVerify: settings.InsecureSkipVerify,\n\t\tServerName:         settings.Host,\n\t}\n\tif settings.TlsClientAuth || settings.TlsAuthWithCACert {\n\t\tif settings.TlsAuthWithCACert && len(settings.TlsCACert) > 0 {\n\t\t\tcaPool := x509.NewCertPool()\n\t\t\tif ok := caPool.AppendCertsFromPEM([]byte(settings.TlsCACert)); !ok {\n\t\t\t\treturn nil, backend.DownstreamError(ErrorInvalidCACertificate)\n\t\t\t}\n\t\t\ttlsConfig.RootCAs = caPool\n\t\t}\n\t\tif settings.TlsClientAuth {\n\t\t\tcert, err := tls.X509KeyPair([]byte(settings.TlsClientCert), []byte(settings.TlsClientKey))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\ttlsConfig.Certificates = []tls.Certificate{cert}\n\t\t}\n\t}\n\treturn tlsConfig, nil\n}\n\n// getPDCDialContext returns a dialer function for creating a connection to PDC if a secure SOCKS proxy is enabled.\nfunc getPDCDialContext(settings Settings) (func(context.Context, string) (net.Conn, error), error) {\n\tp := sdkproxy.New(settings.ProxyOptions)\n\n\tif !p.SecureSocksProxyEnabled() {\n\t\treturn nil, nil\n\t}\n\n\tdialer, err := p.NewSecureSocksProxyContextDialer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcontextDialer, ok := dialer.(proxy.ContextDialer)\n\tif !ok {\n\t\treturn nil, errors.New(\"unable to cast SOCKS proxy dialer to context proxy dialer\")\n\t}\n\n\treturn func(ctx context.Context, addr string) (net.Conn, error) {\n\t\treturn contextDialer.DialContext(ctx, \"tcp\", addr)\n\t}, nil\n}\n\nfunc getClientInfoProducts(ctx context.Context) (products []struct{ Name, Version string }) {\n\tversion := backend.UserAgentFromContext(ctx).GrafanaVersion()\n\n\tif version != \"\" {\n\t\tproducts = append(products, struct{ Name, Version string }{\n\t\t\tName:    \"grafana\",\n\t\t\tVersion: version,\n\t\t})\n\t}\n\n\tif info, err := buildinfo.GetBuildInfo(); err == nil {\n\t\tproducts = append(products, struct{ Name, Version string }{\n\t\t\tName:    \"clickhouse-datasource\",\n\t\t\tVersion: info.Version,\n\t\t})\n\t}\n\n\treturn products\n}\n\nfunc CheckMinServerVersion(conn *sql.DB, major, minor, patch uint64) (bool, error) {\n\tvar version struct {\n\t\tMajor uint64\n\t\tMinor uint64\n\t\tPatch uint64\n\t}\n\tvar res string\n\tif err := conn.QueryRow(\"SELECT version()\").Scan(&res); err != nil {\n\t\treturn false, err\n\t}\n\tfor i, v := range strings.Split(res, \".\") {\n\t\tswitch i {\n\t\tcase 0:\n\t\t\tversion.Major, _ = strconv.ParseUint(v, 10, 64)\n\t\tcase 1:\n\t\t\tversion.Minor, _ = strconv.ParseUint(v, 10, 64)\n\t\tcase 2:\n\t\t\tversion.Patch, _ = strconv.ParseUint(v, 10, 64)\n\t\t}\n\t}\n\tif version.Major < major || (version.Major == major && version.Minor < minor) ||\n\t\t(version.Major == major && version.Minor == minor && version.Patch < patch) {\n\t\treturn false, nil\n\t}\n\treturn true, nil\n}\n\nfunc wrapCategorizedConnectionError(err error) error {\n\tcategory := CategorizeConnectionError(err)\n\tbackend.Logger.Error(\"failed to create ClickHouse client\", \"error_category\", string(category))\n\treturn backend.DownstreamError(fmt.Errorf(\"[%s] %w\", category, err))\n}\n\n// Connect opens a sql.DB connection using datasource settings\nfunc (h *Clickhouse) Connect(\n\tctx context.Context,\n\tconfig backend.DataSourceInstanceSettings,\n\tmessage json.RawMessage,\n) (*sql.DB, error) {\n\tctx, span := tracing.DefaultTracer().Start(ctx, \"clickhouse connect\", trace.WithAttributes(\n\t\tattribute.String(\"db.system\", \"clickhouse\"),\n\t))\n\n\tdefer span.End()\n\n\tsettings, err := LoadSettings(ctx, config)\n\tif err != nil {\n\t\treturn nil, wrapCategorizedConnectionError(err)\n\t}\n\n\tvar tlsConfig *tls.Config\n\tif settings.TlsAuthWithCACert || settings.TlsClientAuth {\n\t\ttlsConfig, err = getTLSConfig(settings)\n\t\tif err != nil {\n\t\t\treturn nil, wrapCategorizedConnectionError(err)\n\t\t}\n\t} else if settings.Secure {\n\t\ttlsConfig = &tls.Config{\n\t\t\tInsecureSkipVerify: settings.InsecureSkipVerify,\n\t\t}\n\t}\n\n\tt, err := strconv.Atoi(settings.DialTimeout)\n\tif err != nil {\n\t\treturn nil, backend.DownstreamError(errors.New(fmt.Sprintf(\"invalid timeout: %s\", settings.DialTimeout)))\n\t}\n\tqt, err := strconv.Atoi(settings.QueryTimeout)\n\tif err != nil {\n\t\treturn nil, backend.DownstreamError(errors.New(fmt.Sprintf(\"invalid query timeout: %s\", settings.QueryTimeout)))\n\t}\n\n\tprotocol := clickhouse.Native\n\tif settings.Protocol == \"http\" {\n\t\tprotocol = clickhouse.HTTP\n\t}\n\n\tcompression := clickhouse.CompressionLZ4\n\tif protocol == clickhouse.HTTP {\n\t\tcompression = clickhouse.CompressionGZIP\n\t}\n\n\tcustomSettings := make(clickhouse.Settings)\n\tif settings.CustomSettings != nil {\n\t\tfor _, setting := range settings.CustomSettings {\n\t\t\tcustomSettings[setting.Setting] = setting.Value\n\t\t}\n\t}\n\n\tif settings.RowLimit != 0 && settings.EnableRowLimit {\n\t\tcustomSettings[\"limit\"] = settings.RowLimit\n\t}\n\n\thttpHeaders, err := extractForwardedHeadersFromMessage(message)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// merge settings.HttpHeaders with message httpHeaders\n\tfor k, v := range settings.HttpHeaders {\n\t\thttpHeaders[k] = v\n\t}\n\n\topts := &clickhouse.Options{\n\t\tAddr: []string{fmt.Sprintf(\"%s:%d\", settings.Host, settings.Port)},\n\t\tAuth: clickhouse.Auth{\n\t\t\tDatabase: settings.DefaultDatabase,\n\t\t\tPassword: settings.Password,\n\t\t\tUsername: settings.Username,\n\t\t},\n\t\tClientInfo: clickhouse.ClientInfo{\n\t\t\tProducts: getClientInfoProducts(ctx),\n\t\t},\n\t\tCompression: &clickhouse.Compression{\n\t\t\tMethod: compression,\n\t\t},\n\t\tDialTimeout: time.Duration(t) * time.Second,\n\t\tHttpHeaders: httpHeaders,\n\t\tHttpUrlPath: settings.Path,\n\t\tProtocol:    protocol,\n\t\tReadTimeout: time.Duration(qt) * time.Second,\n\t\tSettings:    customSettings,\n\t\tTLS:         tlsConfig,\n\t}\n\n\t// dialCtx is used to create a connection to PDC, if it is enabled\n\tdialCtx, err := getPDCDialContext(settings)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif dialCtx != nil {\n\t\topts.DialContext = dialCtx\n\t}\n\n\tctx, cancel := context.WithTimeout(ctx, time.Duration(t)*time.Second)\n\tdefer cancel()\n\n\tdb := clickhouse.OpenDB(opts)\n\n\t// Set connection pool settings\n\tif i, err := strconv.Atoi(settings.ConnMaxLifetime); err == nil {\n\t\tdb.SetConnMaxLifetime(time.Duration(i) * time.Minute)\n\t}\n\tif i, err := strconv.Atoi(settings.MaxIdleConns); err == nil {\n\t\tdb.SetMaxIdleConns(i)\n\t}\n\tif i, err := strconv.Atoi(settings.MaxOpenConns); err == nil {\n\t\tdb.SetMaxOpenConns(i)\n\t}\n\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn nil, fmt.Errorf(\"the operation was cancelled before starting: %w\", ctx.Err())\n\tdefault:\n\t\t// proceed\n\t}\n\n\t// `sqlds` normally calls `db.PingContext()` to check if the connection is alive,\n\t// however, as ClickHouse returns its own non-standard `Exception` type, we need\n\t// to handle it here so that we can categorize and surface the error correctly.\n\tif err := db.PingContext(ctx); err != nil {\n\t\tif ctx.Err() != nil {\n\t\t\treturn nil, fmt.Errorf(\"the operation was cancelled during execution: %w\", ctx.Err())\n\t\t}\n\n\t\treturn nil, wrapCategorizedConnectionError(err)\n\t}\n\n\t// Honor the (nil-resource-on-error) contract so callers can rely on\n\t// `if err != nil { return err }` without leaking the *sql.DB.\n\tif err := settings.isValid(); err != nil {\n\t\t_ = db.Close()\n\t\treturn nil, err\n\t}\n\treturn db, nil\n}\n\n// Converters defines list of data type converters\nfunc (h *Clickhouse) Converters() []sqlutil.Converter {\n\treturn converters.ClickhouseConverters\n}\n\n// Macros returns list of macro functions convert the macros of raw query\nfunc (h *Clickhouse) Macros() sqlds.Macros {\n\treturn macros.Macros\n}\n\n// MutateQueryError marks ClickHouse errors as downstream errors\nfunc (h *Clickhouse) MutateQueryError(err error) backend.ErrorWithSource {\n\t// Check if any error in the error chain (including multi-errors) is a clickhouse.Exception\n\tif containsClickHouseException(err) {\n\t\treturn backend.NewErrorWithSource(err, backend.ErrorSourceDownstream)\n\t}\n\treturn backend.NewErrorWithSource(err, backend.DefaultErrorSource)\n}\n\n// containsClickHouseException checks if err or any error in its chain is a clickhouse.Exception\n// It also handles errors wrapped in HTTP response bodies\nfunc containsClickHouseException(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\n\t// Check if the current error is directly a clickhouse.Exception\n\tvar wrappedException *clickhouse.Exception\n\tif errors.As(err, &wrappedException) {\n\t\treturn true\n\t}\n\n\terrStr := err.Error()\n\n\t// Look for common ClickHouse error patterns in response bodies\n\tif strings.Contains(errStr, \"DB::Exception\") {\n\t\treturn true\n\t}\n\n\t// Catch legacy ClickHouse HTTP error format.\n\t// This is more general than the above and we attempt the DB::Exception catch first\n\t// as those errors also contain this pattern.\n\t// We're only catching 4xx errors for now but we can expand to 5xx if needed.\n\tmatcher, _ := regexp.Compile(`(\\[HTTP 4\\d\\d\\])`)\n\tif matcher.MatchString(errStr) {\n\t\treturn true\n\t}\n\n\t// Check for multiple wrapped errors (e.g., from errors.Join)\n\ttype multiError interface {\n\t\tUnwrap() []error\n\t}\n\n\tif u, ok := err.(multiError); ok {\n\t\tfor _, e := range u.Unwrap() {\n\t\t\tif containsClickHouseException(e) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (h *Clickhouse) Settings(ctx context.Context, config backend.DataSourceInstanceSettings) sqlds.DriverSettings {\n\tsettings, err := LoadSettings(ctx, config)\n\ttimeout := 60\n\tif err == nil {\n\t\tt, err := strconv.Atoi(settings.QueryTimeout)\n\t\tif err == nil {\n\t\t\ttimeout = t\n\t\t}\n\t}\n\treturn sqlds.DriverSettings{\n\t\tTimeout: time.Second * time.Duration(timeout),\n\t\tFillMode: &data.FillMissing{\n\t\t\tMode: data.FillModeNull,\n\t\t},\n\t\tForwardHeaders: settings.ForwardGrafanaHeaders,\n\t}\n}\n\n// MutateQueryData extracts Grafana contextual headers from the request and\n// stores them in the context for ClickHouse query metadata injection.\nfunc (h *Clickhouse) MutateQueryData(\n\tctx context.Context,\n\treq *backend.QueryDataRequest,\n) (context.Context, *backend.QueryDataRequest) {\n\theaders := req.GetHTTPHeaders()\n\tgh := grafanaHeaders{\n\t\tDashboardUID: headers.Get(\"X-Dashboard-Uid\"),\n\t\tPanelID:      headers.Get(\"X-Panel-Id\"),\n\t\tRuleUID:      headers.Get(\"X-Rule-Uid\"),\n\t}\n\tif gh.DashboardUID != \"\" || gh.PanelID != \"\" || gh.RuleUID != \"\" {\n\t\tctx = context.WithValue(ctx, grafanaHeadersKey, gh)\n\t}\n\n\tinjectGrafanaUserHeader(ctx, req)\n\n\treq = preprocessGrafanaSQL(req)\n\treturn ctx, req\n}\n\n// injectGrafanaUserHeader populates X-Grafana-User from the request's user\n// context when \"Forward Grafana HTTP Headers\" is enabled. Grafana's\n// `dataproxy.send_user_header` setting only adds the header to the proxy\n// path (core HTTP datasources); plugin-initiated connections never see it,\n// so downstream ClickHouse loses the user attribution that operators expect\n// when the toggle is on. See #1451.\nfunc injectGrafanaUserHeader(ctx context.Context, req *backend.QueryDataRequest) {\n\tif req == nil || req.PluginContext.DataSourceInstanceSettings == nil {\n\t\treturn\n\t}\n\tif req.GetHTTPHeader(\"X-Grafana-User\") != \"\" {\n\t\treturn\n\t}\n\tsettings, err := LoadSettings(ctx, *req.PluginContext.DataSourceInstanceSettings)\n\tif err != nil || !settings.ForwardGrafanaHeaders {\n\t\treturn\n\t}\n\tuser := backend.UserFromContext(ctx)\n\tif user == nil || user.Login == \"\" {\n\t\treturn\n\t}\n\treq.SetHTTPHeader(\"X-Grafana-User\", user.Login)\n}\n\nfunc preprocessGrafanaSQL(req *backend.QueryDataRequest) *backend.QueryDataRequest {\n\tif req == nil || len(req.Queries) == 0 {\n\t\treturn req\n\t}\n\n\tqueries := make([]backend.DataQuery, 0, len(req.Queries))\n\tfor _, q := range req.Queries {\n\t\tvar sq schemas.Query\n\n\t\tif err := json.Unmarshal(q.JSON, &sq); err != nil {\n\t\t\t// Cannot unmarshal query JSON, ignoring\n\t\t\tqueries = append(queries, q)\n\t\t\tcontinue\n\t\t}\n\n\t\tif !sq.GrafanaSql {\n\t\t\t// Not a Grafana SQL query, ignoring\n\t\t\tqueries = append(queries, q)\n\t\t\tcontinue\n\t\t}\n\n\t\tsqlQuery, err := sq.ToSQL(schemas.DialectClickHouse)\n\t\tif err != nil {\n\t\t\tbackend.Logger.Error(\"Failed to build SQL query\", \"error\", err.Error())\n\t\t\tcontinue\n\t\t}\n\n\t\t// Build JSON with `sqlutil.Query` shape that will be used to execute the query by sqlds\n\t\tqueryJSON, err := json.Marshal(sqlutil.Query{\n\t\t\tRawSQL:         sqlQuery,\n\t\t\tFormat:         sqlutil.FormatOptionTable, // TODO: Is this correct?\n\t\t\tConnectionArgs: json.RawMessage(\"{}\"),\n\t\t})\n\t\tif err != nil {\n\t\t\tbackend.Logger.Error(\"Failed to marshal SQL query\", \"error\", err.Error())\n\t\t\tcontinue\n\t\t}\n\n\t\tq.JSON = queryJSON\n\t\tqueries = append(queries, q)\n\t}\n\n\treturn &backend.QueryDataRequest{\n\t\tPluginContext: req.PluginContext,\n\t\tHeaders:       req.Headers,\n\t\tQueries:       queries,\n\t}\n}\n\nfunc (h *Clickhouse) MutateQuery(ctx context.Context, req backend.DataQuery) (context.Context, backend.DataQuery) {\n\tctx, span := tracing.DefaultTracer().Start(ctx, \"clickhouse mutate_query\", trace.WithAttributes(\n\t\tattribute.String(\"db.system\", \"clickhouse\"),\n\t))\n\n\tdefer span.End()\n\n\tcomments := make([]string, 0, 4)\n\n\tif user := backend.UserFromContext(ctx); user != nil {\n\t\tcomments = append(comments, \"grafana_user:\"+user.Login)\n\t}\n\n\tif gh, ok := ctx.Value(grafanaHeadersKey).(grafanaHeaders); ok {\n\t\tif gh.DashboardUID != \"\" {\n\t\t\tcomments = append(comments, \"grafana_dashboard:\"+gh.DashboardUID)\n\t\t}\n\t\tif gh.PanelID != \"\" {\n\t\t\tcomments = append(comments, \"grafana_panel:\"+gh.PanelID)\n\t\t}\n\t\tif gh.RuleUID != \"\" {\n\t\t\tcomments = append(comments, \"grafana_rule:\"+gh.RuleUID)\n\t\t}\n\t}\n\n\tif len(comments) > 0 {\n\t\tctx = clickhouse.Context(ctx, clickhouse.WithClientInfo(clickhouse.ClientInfo{\n\t\t\tProducts: nil,\n\t\t\tComment:  comments,\n\t\t}))\n\t}\n\n\tvar dataQuery struct {\n\t\tMeta struct {\n\t\t\tTimeZone string `json:\"timezone\"`\n\t\t} `json:\"meta\"`\n\t\tFormat int `json:\"format\"`\n\t}\n\n\tif err := json.Unmarshal(req.JSON, &dataQuery); err != nil {\n\t\treturn ctx, req\n\t}\n\n\tif dataQuery.Meta.TimeZone == \"\" {\n\t\treturn ctx, req\n\t}\n\n\tloc, _ := time.LoadLocation(dataQuery.Meta.TimeZone)\n\treturn clickhouse.Context(ctx, clickhouse.WithUserLocation(loc)), req\n}\n\n// MutateResponse converts fields of type FieldTypeNullableJSON to string,\n// except for specific visualizations (traces, tables, and logs).\nfunc (h *Clickhouse) MutateResponse(ctx context.Context, res data.Frames) (data.Frames, error) {\n\t_, span := tracing.DefaultTracer().Start(ctx, \"clickhouse mutate_response\", trace.WithAttributes(\n\t\tattribute.String(\"db.system\", \"clickhouse\"),\n\t))\n\n\tdefer span.End()\n\n\tfor _, frame := range res {\n\t\tif frame.Meta.PreferredVisualization == data.VisTypeLogs {\n\t\t\terr := mergeOpenTelemetryLabels(frame)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\tif shouldConvertFields(frame.Meta.PreferredVisualization) {\n\t\t\tif err := convertNullableJSONFields(frame); err != nil {\n\t\t\t\treturn res, err\n\t\t\t}\n\t\t}\n\t}\n\treturn res, nil\n}\n\n// shouldConvertFields determines whether field conversion is needed based on visualization type.\nfunc shouldConvertFields(visType data.VisType) bool {\n\treturn visType != data.VisTypeTrace && visType != data.VisTypeTable && visType != data.VisTypeLogs\n}\n\n// convertNullableJSONFields converts all FieldTypeNullableJSON fields in the given frame to string.\nfunc convertNullableJSONFields(frame *data.Frame) error {\n\tvar convertedFields []*data.Field\n\n\tfor _, field := range frame.Fields {\n\t\tif field.Type() == data.FieldTypeJSON {\n\t\t\tnewField, err := convertFieldToString(field)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tconvertedFields = append(convertedFields, newField)\n\t\t} else {\n\t\t\tconvertedFields = append(convertedFields, field)\n\t\t}\n\t}\n\n\tframe.Fields = convertedFields\n\treturn nil\n}\n\n// convertFieldToString creates a new field where JSON values are marshaled into string representations.\nfunc convertFieldToString(field *data.Field) (*data.Field, error) {\n\tvalues := make([]*string, field.Len())\n\tnewField := data.NewField(field.Name, field.Labels, values)\n\tnewField.SetConfig(field.Config)\n\n\tfor i := 0; i < field.Len(); i++ {\n\t\tval, _ := field.At(i).(*json.RawMessage)\n\t\tif val == nil {\n\t\t\tnewField.Set(i, nil)\n\t\t} else {\n\t\t\tbytes, err := val.MarshalJSON()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tsVal := string(bytes)\n\t\t\tnewField.Set(i, &sVal)\n\t\t}\n\t}\n\n\treturn newField, nil\n}\n\nfunc extractForwardedHeadersFromMessage(message json.RawMessage) (map[string]string, error) {\n\t// An example of the message we're trying to parse:\n\t// {\n\t//   \"grafana-http-headers\": {\n\t//     \"x-grafana-org-id\": [\"12345\"],\n\t//     \"x-grafana-user\": [\"admin\"]\n\t//   }\n\t// }\n\tif len(message) == 0 {\n\t\tmessage = []byte(\"{}\")\n\t}\n\n\tmessageArgs := make(map[string]interface{})\n\terr := json.Unmarshal(message, &messageArgs)\n\tif err != nil {\n\t\tbackend.Logger.Warn(fmt.Sprintf(\"Failed to apply headers: %s\", err.Error()))\n\t\treturn nil, errors.New(\"Couldn't parse message as args\")\n\t}\n\n\thttpHeaders := make(map[string]string)\n\tif grafanaHttpHeaders, ok := messageArgs[sqlds.HeaderKey]; ok {\n\t\tfwdHeaders, ok := grafanaHttpHeaders.(map[string]interface{})\n\t\tif !ok {\n\t\t\treturn nil, errors.New(\"Couldn't parse grafana HTTP headers\")\n\t\t}\n\n\t\tfor k, v := range fwdHeaders {\n\t\t\tanyHeadersArr, ok := v.([]interface{})\n\t\t\tif !ok {\n\t\t\t\treturn nil, errors.New(fmt.Sprintf(\"Couldn't parse header %s as an array\", k))\n\t\t\t}\n\n\t\t\tstrHeadersArr := make([]string, len(anyHeadersArr))\n\t\t\tfor ind, val := range anyHeadersArr {\n\t\t\t\tstrHeadersArr[ind] = val.(string)\n\t\t\t}\n\n\t\t\thttpHeaders[k] = strings.Join(strHeadersArr, \",\")\n\t\t}\n\t}\n\n\treturn httpHeaders, nil\n}\n\nfunc mergeOpenTelemetryLabels(frame *data.Frame) error {\n\tvar attrFields []*data.Field\n\tfor _, field := range frame.Fields {\n\t\tif field.Name == \"labels\" {\n\t\t\treturn nil\n\t\t}\n\n\t\tif field.Type() != data.FieldTypeJSON {\n\t\t\tcontinue\n\t\t}\n\n\t\tif field.Name == \"ResourceAttributes\" || field.Name == \"ScopeAttributes\" || field.Name == \"LogAttributes\" {\n\t\t\tattrFields = append(attrFields, field)\n\t\t}\n\t}\n\n\tif len(attrFields) == 0 {\n\t\treturn nil\n\t}\n\n\trowLen, err := frame.RowLen()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tallLabelsValues := make([]map[string]any, rowLen)\n\n\tfor _, field := range attrFields {\n\t\tfor j := 0; j < rowLen; j++ {\n\t\t\tcurrentVal := allLabelsValues[j]\n\t\t\tif currentVal == nil {\n\t\t\t\tcurrentVal = make(map[string]any)\n\t\t\t}\n\n\t\t\tval := field.At(j).(json.RawMessage)\n\t\t\tif val != nil {\n\t\t\t\tvar valMap map[string]any\n\t\t\t\terr := json.Unmarshal(val, &valMap)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tassignFlattenedPath(currentVal, field.Name, \"\", valMap)\n\n\t\t\t\tallLabelsValues[j] = currentVal\n\t\t\t}\n\t\t}\n\t}\n\n\tallLabelsValuesJSON := make([]json.RawMessage, rowLen)\n\tfor i, value := range allLabelsValues {\n\t\tvalueJSON, err := json.Marshal(value)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tallLabelsValuesJSON[i] = valueJSON\n\t}\n\tallLabels := data.NewField(\"labels\", make(data.Labels), allLabelsValuesJSON)\n\n\tfilteredFields := make([]*data.Field, 0, len(frame.Fields)-len(attrFields))\n\tfor _, field := range frame.Fields {\n\t\tif field.Name == \"ResourceAttributes\" || field.Name == \"ScopeAttributes\" || field.Name == \"LogAttributes\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tfilteredFields = append(filteredFields, field)\n\t}\n\tfilteredFields = append(filteredFields, allLabels)\n\tframe.Fields = filteredFields\n\n\treturn nil\n}\n\n// assignFlattenedPath will flatten a nested map into a map with top level keys separated by dots.\nfunc assignFlattenedPath(flatMap map[string]any, pathPrefix, pathKey string, pathValue any) {\n\tfullPath := fmt.Sprintf(\"%s.%s\", pathPrefix, pathKey)\n\tif pathKey == \"\" {\n\t\tfullPath = pathPrefix\n\t}\n\n\tnestedMap, ok := pathValue.(map[string]any)\n\tif !ok {\n\t\tflatMap[fullPath] = pathValue\n\t\treturn\n\t}\n\n\tfor k, v := range nestedMap {\n\t\tassignFlattenedPath(flatMap, fullPath, k, v)\n\t}\n}\n"
  },
  {
    "path": "pkg/plugin/driver_integration_test.go",
    "content": "//go:build integration\n// +build integration\n\npackage plugin\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\tclickhouse_sql \"github.com/ClickHouse/clickhouse-go/v2\"\n\t\"github.com/moby/moby/api/types/container\"\n\t\"github.com/docker/go-units\"\n\t\"github.com/elazarl/goproxy\"\n\t\"github.com/grafana/clickhouse-datasource/pkg/converters\"\n\t\"github.com/grafana/grafana-plugin-sdk-go/backend\"\n\t\"github.com/grafana/grafana-plugin-sdk-go/data\"\n\t\"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil\"\n\t\"github.com/shopspring/decimal\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/testcontainers/testcontainers-go\"\n\t\"github.com/testcontainers/testcontainers-go/wait\"\n)\n\nconst defaultClickHouseVersion = \"latest\"\n\nfunc GetClickHouseTestVersion() string {\n\treturn GetEnv(\"CLICKHOUSE_VERSION\", defaultClickHouseVersion)\n}\n\nfunc GetEnv(key, fallback string) string {\n\tif value, ok := os.LookupEnv(key); ok {\n\t\treturn value\n\t}\n\treturn fallback\n}\n\nfunc TestMain(m *testing.M) {\n\tuseDocker := strings.ToLower(getEnv(\"CLICKHOUSE_USE_DOCKER\", \"true\"))\n\tif useDocker == \"false\" {\n\t\tfmt.Printf(\"Using external ClickHouse for IT tests -  %s:%s\\n\",\n\t\t\tgetEnv(\"CLICKHOUSE_PORT\", \"9000\"), getEnv(\"CLICKHOUSE_HOST\", \"localhost\"))\n\t\tos.Exit(m.Run())\n\t}\n\t// create a ClickHouse container\n\tctx := context.Background()\n\t// attempt use docker for CI\n\tprovider, err := testcontainers.ProviderDocker.GetProvider()\n\tif err != nil {\n\t\tfmt.Printf(\"Docker is not running and no clickhouse connections details were provided. Skipping IT tests: %s\\n\", err)\n\t\tos.Exit(0)\n\t}\n\terr = provider.Health(ctx)\n\tif err != nil {\n\t\tfmt.Printf(\"Docker is not running and no clickhouse connections details were provided. Skipping IT tests: %s\\n\", err)\n\t\tos.Exit(0)\n\t}\n\tchVersion := GetClickHouseTestVersion()\n\tfmt.Printf(\"Using Docker for IT tests with ClickHouse %s\\n\", chVersion)\n\tcwd, err := os.Getwd()\n\tif err != nil {\n\t\t// can't test without container\n\t\tpanic(err)\n\t}\n\n\tcustomHostPath := \"../../config/custom.xml\"\n\tadminHostPath := \"../../config/admin.xml\"\n\n\treq := testcontainers.ContainerRequest{\n\t\tEnv: map[string]string{\n\t\t\t\"CLICKHOUSE_SKIP_USER_SETUP\": \"1\", // added because of https://github.com/ClickHouse/ClickHouse/commit/65435a3db7214261b793acfb69388567b4c8c9b3\n\t\t\t\"TZ\":                         time.Local.String(),\n\t\t},\n\t\tExposedPorts: []string{\"9000/tcp\", \"8123/tcp\"},\n\t\tFiles: []testcontainers.ContainerFile{\n\t\t\t{\n\t\t\t\tContainerFilePath: \"/etc/clickhouse-server/config.d/custom.xml\",\n\t\t\t\tFileMode:          0644,\n\t\t\t\tHostFilePath:      path.Join(cwd, customHostPath),\n\t\t\t},\n\t\t\t{\n\t\t\t\tContainerFilePath: \"/etc/clickhouse-server/users.d/admin.xml\",\n\t\t\t\tFileMode:          0644,\n\t\t\t\tHostFilePath:      path.Join(cwd, adminHostPath),\n\t\t\t},\n\t\t},\n\t\tImage: fmt.Sprintf(\"clickhouse/clickhouse-server:%s\", chVersion),\n\t\tResources: container.Resources{\n\t\t\tUlimits: []*units.Ulimit{\n\t\t\t\t{\n\t\t\t\t\tName: \"nofile\",\n\t\t\t\t\tHard: 262144,\n\t\t\t\t\tSoft: 262144,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tWaitingFor: wait.ForLog(\"Ready for connections\"),\n\t}\n\tclickhouseContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{\n\t\tContainerRequest: req,\n\t\tStarted:          true,\n\t})\n\tif err != nil {\n\t\t// can't test without container\n\t\tpanic(err)\n\t}\n\tp, _ := clickhouseContainer.MappedPort(ctx, \"9000\")\n\terr = os.Setenv(\"CLICKHOUSE_PORT\", p.Port())\n\tif err != nil {\n\t\t// Can't test without env setup.\n\t\tpanic(err)\n\t}\n\thp, _ := clickhouseContainer.MappedPort(ctx, \"8123\")\n\terr = os.Setenv(\"CLICKHOUSE_HTTP_PORT\", hp.Port())\n\tif err != nil {\n\t\t// Can't test without env setup.\n\t\tpanic(err)\n\t}\n\terr = os.Setenv(\"CLICKHOUSE_HOST\", \"localhost\")\n\tif err != nil {\n\t\t// Can't test without env setup.\n\t\tpanic(err)\n\t}\n\tdefer clickhouseContainer.Terminate(ctx) //nolint\n\tos.Exit(m.Run())\n}\n\nfunc TestConnect(t *testing.T) {\n\tport := getEnv(\"CLICKHOUSE_PORT\", \"9000\")\n\thost := getEnv(\"CLICKHOUSE_HOST\", \"localhost\")\n\tusername := getEnv(\"CLICKHOUSE_USERNAME\", \"default\")\n\tpassword := getEnv(\"CLICKHOUSE_PASSWORD\", \"\")\n\tssl := getEnv(\"CLICKHOUSE_SSL\", \"false\")\n\tqueryTimeoutNumber := 3600\n\tqueryTimeoutString := \"3600\"\n\tpath := \"custom-path\"\n\tclickhouse := Clickhouse{}\n\tctx := context.Background()\n\tctx = backend.WithGrafanaConfig(ctx, backend.NewGrafanaCfg(map[string]string{\n\t\t\"GF_SQL_ROW_LIMIT\":                         \"1000000\",\n\t\t\"GF_SQL_MAX_OPEN_CONNS_DEFAULT\":            \"10\",\n\t\t\"GF_SQL_MAX_IDLE_CONNS_DEFAULT\":            \"10\",\n\t\t\"GF_SQL_MAX_CONN_LIFETIME_SECONDS_DEFAULT\": \"60\",\n\t}))\n\tt.Run(\"should not error when valid settings passed\", func(t *testing.T) {\n\t\tsecure := map[string]string{}\n\t\tsecure[\"password\"] = password\n\t\tsettings := backend.DataSourceInstanceSettings{JSONData: []byte(fmt.Sprintf(`{ \"server\": \"%s\", \"port\": %s, \"path\": \"%s\", \"username\": \"%s\", \"secure\": %s, \"queryTimeout\": \"%s\"}`, host, port, path, username, ssl, queryTimeoutString)), DecryptedSecureJSONData: secure}\n\t\t_, err := clickhouse.Connect(ctx, settings, json.RawMessage{})\n\t\tassert.Equal(t, nil, err)\n\t})\n\tt.Run(\"should not error when valid settings passed - with query timeout as number\", func(t *testing.T) {\n\t\tsecure := map[string]string{}\n\t\tsecure[\"password\"] = password\n\t\tsettings := backend.DataSourceInstanceSettings{JSONData: []byte(fmt.Sprintf(`{ \"server\": \"%s\", \"port\": %s, \"username\": \"%s\", \"secure\": %s, \"queryTimeout\": %d }`, host, port, username, ssl, queryTimeoutNumber)), DecryptedSecureJSONData: secure}\n\t\t_, err := clickhouse.Connect(ctx, settings, json.RawMessage{})\n\t\tassert.Equal(t, nil, err)\n\t})\n}\n\nfunc TestHTTPConnect(t *testing.T) {\n\tport := getEnv(\"CLICKHOUSE_HTTP_PORT\", \"8123\")\n\thost := getEnv(\"CLICKHOUSE_HOST\", \"localhost\")\n\tusername := getEnv(\"CLICKHOUSE_USERNAME\", \"default\")\n\tpassword := getEnv(\"CLICKHOUSE_PASSWORD\", \"\")\n\tssl := getEnv(\"CLICKHOUSE_SSL\", \"false\")\n\tclickhouse := Clickhouse{}\n\tctx := context.Background()\n\tctx = backend.WithGrafanaConfig(ctx, backend.NewGrafanaCfg(map[string]string{\n\t\t\"GF_SQL_ROW_LIMIT\":                         \"1000000\",\n\t\t\"GF_SQL_MAX_OPEN_CONNS_DEFAULT\":            \"10\",\n\t\t\"GF_SQL_MAX_IDLE_CONNS_DEFAULT\":            \"10\",\n\t\t\"GF_SQL_MAX_CONN_LIFETIME_SECONDS_DEFAULT\": \"60\",\n\t}))\n\tt.Run(\"should not error when valid settings passed\", func(t *testing.T) {\n\t\tsecure := map[string]string{}\n\t\tsecure[\"password\"] = password\n\t\tsettings := backend.DataSourceInstanceSettings{JSONData: []byte(fmt.Sprintf(`{ \"server\": \"%s\", \"port\": %s, \"username\": \"%s\", \"secure\": %s, \"protocol\": \"http\" }`, host, port, username, ssl)), DecryptedSecureJSONData: secure}\n\t\t_, err := clickhouse.Connect(ctx, settings, json.RawMessage{})\n\t\tassert.Equal(t, nil, err)\n\t})\n}\n\nfunc getEnv(key, fallback string) string {\n\tif value, ok := os.LookupEnv(key); ok {\n\t\treturn value\n\t}\n\treturn fallback\n}\n\nfunc setupConnection(t *testing.T, protocol clickhouse_sql.Protocol, settings clickhouse_sql.Settings) *sql.DB {\n\tport := getEnv(\"CLICKHOUSE_PORT\", \"9000\")\n\tif protocol == clickhouse_sql.HTTP {\n\t\tport = getEnv(\"CLICKHOUSE_HTTP_PORT\", \"8123\")\n\t}\n\thost := getEnv(\"CLICKHOUSE_HOST\", \"localhost\")\n\tusername := getEnv(\"CLICKHOUSE_USERNAME\", \"default\")\n\tpassword := getEnv(\"CLICKHOUSE_PASSWORD\", \"\")\n\tssl, ok := os.LookupEnv(\"CLICKHOUSE_SSL\")\n\tvar sConfig *tls.Config\n\tif ok && strings.ToLower(ssl) == \"true\" {\n\t\tsConfig = &tls.Config{\n\t\t\tInsecureSkipVerify: false,\n\t\t}\n\t}\n\t// we create a direct connection since we need specific settings for insert\n\n\tconn := clickhouse_sql.OpenDB(&clickhouse_sql.Options{\n\t\tAddr:     []string{fmt.Sprintf(\"%s:%s\", host, port)},\n\t\tSettings: settings,\n\t\tAuth: clickhouse_sql.Auth{\n\t\t\tDatabase: \"default\",\n\t\t\tUsername: username,\n\t\t\tPassword: password,\n\t\t},\n\t\tTLS:      sConfig,\n\t\tProtocol: protocol,\n\t})\n\treturn conn\n}\n\nfunc setupTest(t *testing.T, ddl string, protocol clickhouse_sql.Protocol, settings clickhouse_sql.Settings) (*sql.DB, func(t *testing.T)) {\n\tconn := setupConnection(t, protocol, settings)\n\t_, err := conn.Exec(\"DROP TABLE IF EXISTS simple_table\")\n\trequire.NoError(t, err)\n\t_, err = conn.Exec(fmt.Sprintf(\"CREATE table simple_table(%s) ENGINE = MergeTree ORDER BY tuple();\", ddl))\n\trequire.NoError(t, err)\n\treturn conn, func(t *testing.T) {\n\t\t_, err := conn.Exec(\"DROP TABLE simple_table\")\n\t\trequire.NoError(t, err)\n\t}\n}\n\nfunc insertData(t *testing.T, conn *sql.DB, data ...interface{}) {\n\tscope, err := conn.Begin()\n\trequire.NoError(t, err)\n\tbatch, err := scope.Prepare(\"INSERT INTO simple_table\")\n\trequire.NoError(t, err)\n\tfor _, val := range data {\n\t\t_, err = batch.Exec(val)\n\t\trequire.NoError(t, err)\n\t}\n\trequire.NoError(t, scope.Commit())\n}\n\nfunc toJson(obj interface{}) (json.RawMessage, error) {\n\tbytes, err := json.Marshal(obj)\n\tif err != nil {\n\t\treturn nil, errors.New(\"unable to marshal\")\n\t}\n\tvar rawJSON json.RawMessage\n\terr = json.Unmarshal(bytes, &rawJSON)\n\tif err != nil {\n\t\treturn nil, errors.New(\"unable to unmarshal\")\n\t}\n\treturn rawJSON, nil\n}\n\nfunc checkFieldValue(t *testing.T, field *data.Field, expected ...interface{}) {\n\tfor i, eVal := range expected {\n\t\tval := field.At(i)\n\t\tif eVal == nil {\n\t\t\tassert.Nil(t, val)\n\t\t\treturn\n\t\t}\n\t\tswitch tVal := eVal.(type) {\n\t\tcase float64:\n\t\t\tassert.InDelta(t, tVal, val, 0.01)\n\t\tdefault:\n\t\t\tswitch reflect.ValueOf(eVal).Kind() {\n\t\t\tcase reflect.Map, reflect.Slice:\n\t\t\t\tjsonRaw, err := toJson(tVal)\n\t\t\t\tassert.Nil(t, err)\n\t\t\t\tassert.Equal(t, jsonRaw, val.(json.RawMessage))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Equal(t, eVal, val)\n\t\t}\n\t}\n}\n\nvar Protocols = map[string]clickhouse_sql.Protocol{\"native\": clickhouse_sql.Native, \"http\": clickhouse_sql.HTTP}\n\nfunc checkRows(t *testing.T, conn *sql.DB, rowLimit int64, expectedValues ...interface{}) {\n\trows, err := conn.Query(fmt.Sprintf(\"SELECT * FROM simple_table LIMIT %d\", rowLimit))\n\trequire.NoError(t, err)\n\tframe, err := sqlutil.FrameFromRows(rows, rowLimit, converters.ClickhouseConverters...)\n\trequire.NoError(t, err)\n\tassert.Equal(t, 1, len(frame.Fields))\n\tcheckFieldValue(t, frame.Fields[0], expectedValues...)\n}\n\nfunc TestConvertUInt8(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 UInt8\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, uint8(1))\n\t\t\tcheckRows(t, conn, 1, uint8(1))\n\t\t})\n\t}\n\n}\n\nfunc TestConvertUInt16(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 UInt16\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, uint16(2))\n\t\t\tcheckRows(t, conn, 1, uint16(2))\n\t\t})\n\t}\n}\n\nfunc TestConvertUInt32(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 UInt32\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, uint32(3))\n\t\t\tcheckRows(t, conn, 1, uint32(3))\n\t\t})\n\t}\n}\n\nfunc TestConvertUInt64(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 UInt64\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, uint64(4))\n\t\t\tcheckRows(t, conn, 1, uint64(4))\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableUInt8(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(UInt8)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := uint8(5)\n\t\t\tinsertData(t, conn, val, nil)\n\t\t\tcheckRows(t, conn, 2, &val, nil)\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableUInt16(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(UInt16)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := uint16(6)\n\t\t\tinsertData(t, conn, val, nil)\n\t\t\tcheckRows(t, conn, 2, &val, nil)\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableUInt32(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(UInt16)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := uint16(7)\n\t\t\tinsertData(t, conn, val, nil)\n\t\t\tcheckRows(t, conn, 2, &val, nil)\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableUInt64(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(UInt16)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := uint16(8)\n\t\t\tinsertData(t, conn, val, nil)\n\t\t\tcheckRows(t, conn, 2, &val, nil)\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableInt8(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(Int8)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := int8(9)\n\t\t\tinsertData(t, conn, val, nil)\n\t\t\tcheckRows(t, conn, 2, &val, nil)\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableInt16(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(Int16)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := int16(10)\n\t\t\tinsertData(t, conn, val, nil)\n\t\t\tcheckRows(t, conn, 2, &val, nil)\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableInt32(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(Int32)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := int32(11)\n\t\t\tinsertData(t, conn, val, nil)\n\t\t\tcheckRows(t, conn, 2, &val, nil)\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableInt64(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(Int64)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := int64(12)\n\t\t\tinsertData(t, conn, val, nil)\n\t\t\tcheckRows(t, conn, 2, &val, nil)\n\t\t})\n\t}\n}\n\nfunc TestConvertInt8(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Int8\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := int8(13)\n\t\t\tinsertData(t, conn, val)\n\t\t\tcheckRows(t, conn, 1, val)\n\t\t})\n\t}\n}\n\nfunc TestConvertInt16(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Int16\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, int16(14))\n\t\t\tcheckRows(t, conn, 1, int16(14))\n\t\t})\n\t}\n}\n\nfunc TestConvertInt32(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Int32\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, int32(15))\n\t\t\tcheckRows(t, conn, 1, int32(15))\n\t\t})\n\t}\n}\n\nfunc TestConvertInt64(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Int64\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, int64(16))\n\t\t\tcheckRows(t, conn, 1, int64(16))\n\t\t})\n\t}\n}\n\nfunc TestConvertFloat32(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Float32\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, float32(17.1))\n\t\t\tcheckRows(t, conn, 1, float32(17.1))\n\t\t})\n\t}\n}\n\nfunc TestConvertFloat64(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Float64\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, float64(18.1))\n\t\t\tcheckRows(t, conn, 1, float64(18.1))\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableFloat32(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(Float32)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := float32(19.1)\n\t\t\tinsertData(t, conn, val, nil)\n\t\t\tcheckRows(t, conn, 2, &val, nil)\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableFloat64(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(Float64)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := float64(20.1)\n\t\t\tinsertData(t, conn, val, nil)\n\t\t\tcheckRows(t, conn, 2, &val, nil)\n\t\t})\n\t}\n}\n\nfunc TestConvertBool(t *testing.T) {\n\tconn := setupConnection(t, clickhouse_sql.Native, nil)\n\tuseBool, err := CheckMinServerVersion(conn, 21, 9, 0)\n\trequire.NoError(t, err)\n\t// 21.8 uses int8\n\tvar expected interface{} = int8(1)\n\tif useBool {\n\t\texpected = true\n\t}\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Bool\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, true)\n\n\t\t\tcheckRows(t, conn, 1, expected)\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableBool(t *testing.T) {\n\tconn := setupConnection(t, clickhouse_sql.Native, nil)\n\tuseBool, err := CheckMinServerVersion(conn, 21, 9, 0)\n\trequire.NoError(t, err)\n\t// 21.8 uses int8\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(Bool)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tif useBool {\n\t\t\t\tval := true\n\t\t\t\tinsertData(t, conn, val, nil)\n\t\t\t\tcheckRows(t, conn, 2, &val, nil)\n\t\t\t} else {\n\t\t\t\tval := int8(1)\n\t\t\t\tinsertData(t, conn, val, nil)\n\t\t\t\tcheckRows(t, conn, 2, &val, nil)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestConvertInt128(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Int128\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, big.NewInt(23))\n\t\t\tcheckRows(t, conn, 1, float64(23))\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableInt128(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(Int128)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, big.NewInt(24), nil)\n\t\t\tval := float64(24)\n\t\t\tcheckRows(t, conn, 2, &val, nil)\n\t\t})\n\t}\n}\n\nfunc TestConvertInt256(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Int256\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, big.NewInt(25))\n\t\t\tcheckRows(t, conn, 1, float64(25))\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableInt256(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(Int256)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, big.NewInt(26), nil)\n\t\t\tval := float64(26)\n\t\t\tcheckRows(t, conn, 2, &val, nil)\n\t\t})\n\t}\n}\n\nfunc TestConvertUInt128(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 UInt128\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, big.NewInt(27))\n\t\t\tcheckRows(t, conn, 1, float64(27))\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableUInt128(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(UInt128)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, big.NewInt(28), nil)\n\t\t\tval := float64(28)\n\t\t\tcheckRows(t, conn, 2, &val, nil)\n\t\t})\n\t}\n}\n\nfunc TestConvertUInt256(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 UInt256\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, big.NewInt(29))\n\t\t\tcheckRows(t, conn, 1, float64(29))\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableUInt256(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(UInt256)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, big.NewInt(30), nil)\n\t\t\tval := float64(30)\n\t\t\tcheckRows(t, conn, 2, &val, nil)\n\t\t})\n\t}\n}\n\nvar date, _ = time.ParseInLocation(\"2006-01-02\", \"2022-01-12\", time.UTC)\n\nfunc TestConvertDate(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Date\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, date)\n\t\t\tcheckRows(t, conn, 1, date)\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableDate(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(Date)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, date, nil)\n\t\t\tcheckRows(t, conn, 2, &date, nil)\n\t\t})\n\t}\n}\n\nvar datetime, _ = time.Parse(\"2006-01-02 15:04:05\", \"2022-01-12 00:00:00\")\n\nfunc TestConvertDateTime(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tloc, _ := time.LoadLocation(\"Europe/London\")\n\t\t\tlocalTime := datetime.In(loc)\n\t\t\tconn, close := setupTest(t, \"col1 DateTime('Europe/London')\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, localTime)\n\t\t\tcheckRows(t, conn, 1, localTime)\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableDateTime(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tloc, _ := time.LoadLocation(\"Europe/London\")\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(DateTime('Europe/London'))\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tlocTime := datetime.In(loc)\n\t\t\tinsertData(t, conn, locTime, nil)\n\t\t\tcheckRows(t, conn, 2, &locTime, nil)\n\t\t})\n\t}\n}\n\nfunc TestConvertDateTime64(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tloc, _ := time.LoadLocation(\"Europe/London\")\n\t\t\tconn, close := setupTest(t, \"col1 DateTime64(3, 'Europe/London')\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tlocTime := datetime.In(loc)\n\t\t\tlocTime = locTime.Add(123 * time.Millisecond)\n\t\t\tinsertData(t, conn, locTime)\n\t\t\tcheckRows(t, conn, 1, locTime)\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableDateTime64(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tloc, _ := time.LoadLocation(\"Europe/London\")\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(DateTime64(3, 'Europe/London'))\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tlocTime := datetime.In(loc)\n\t\t\tlocTime = locTime.Add(123 * time.Millisecond)\n\t\t\tinsertData(t, conn, locTime, nil)\n\t\t\tcheckRows(t, conn, 2, &locTime, nil)\n\t\t})\n\t}\n}\n\nfunc TestConvertString(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 String\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, \"37\")\n\t\t\tcheckRows(t, conn, 1, \"37\")\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableString(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(String)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, \"38\", nil)\n\t\t\tval := \"38\"\n\t\t\tcheckRows(t, conn, 2, &val, nil)\n\t\t})\n\t}\n}\n\nfunc TestConvertDecimal(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Decimal(15,3)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, decimal.New(39, 10))\n\t\t\tval, _ := decimal.New(39, 10).Float64()\n\t\t\tcheckRows(t, conn, 1, val)\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableDecimal(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(Decimal(15,3))\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, decimal.New(40, 10), nil)\n\t\t\tval, _ := decimal.New(40, 10).Float64()\n\t\t\tcheckRows(t, conn, 2, &val, nil)\n\t\t})\n\t}\n}\n\nfunc TestTuple(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Tuple(s String, i Int64)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := map[string]interface{}{\"s\": \"41\", \"i\": int64(41)}\n\t\t\tinsertData(t, conn, val)\n\t\t\tcheckRows(t, conn, 1, val)\n\t\t})\n\t}\n}\n\nfunc TestNested(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nested(s String, i Int64)\", protocol, clickhouse_sql.Settings{\n\t\t\t\t\"flatten_nested\": 0,\n\t\t\t})\n\t\t\tdefer close(t)\n\t\t\tval := []map[string]interface{}{{\"s\": \"42\", \"i\": int64(42)}}\n\t\t\tinsertData(t, conn, val)\n\t\t\tcheckRows(t, conn, 1, val)\n\t\t})\n\t}\n}\n\nfunc TestArrayTuple(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Array(Tuple(s String, i Int32))\", protocol, clickhouse_sql.Settings{\n\t\t\t\t\"flatten_nested\": 0,\n\t\t\t})\n\t\t\tdefer close(t)\n\t\t\tval := []map[string]interface{}{{\"s\": \"43\", \"i\": int32(43)}}\n\t\t\tinsertData(t, conn, val)\n\t\t\tcheckRows(t, conn, 1, val)\n\t\t})\n\t}\n}\n\nfunc TestArrayInt64(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Array(Int64)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := []int64{int64(45), int64(45)}\n\t\t\tinsertData(t, conn, val)\n\t\t\tcheckRows(t, conn, 1, val)\n\t\t})\n\t}\n}\n\nfunc TestArrayNullableInt64(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Array(Nullable(Int64))\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tv := int64(45)\n\t\t\tval := []*int64{&v, nil}\n\t\t\tinsertData(t, conn, val)\n\t\t\tcheckRows(t, conn, 1, val)\n\t\t})\n\t}\n}\n\nfunc TestArrayUInt256(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Array(UInt256)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := []*big.Int{big.NewInt(47), big.NewInt(47)}\n\t\t\tinsertData(t, conn, val)\n\t\t\tcheckRows(t, conn, 1, val)\n\t\t})\n\t}\n}\n\nfunc TestArrayNullableUInt256(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Array(Nullable(UInt256))\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := []*big.Int{big.NewInt(47), nil}\n\t\t\tinsertData(t, conn, val)\n\t\t\tcheckRows(t, conn, 1, val)\n\t\t})\n\t}\n}\n\nfunc TestArrayNullableString(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tvar val []*string\n\t\t\tconn, close := setupTest(t, \"col1 Array(Nullable(String))\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tv := \"48\"\n\t\t\tval = append(val, &v, nil)\n\t\t\tinsertData(t, conn, val)\n\t\t\tcheckRows(t, conn, 1, val)\n\t\t})\n\t}\n}\n\nfunc TestArrayNullableIPv4(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Array(Nullable(IPv4))\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tipv4Addr := net.ParseIP(\"192.0.2.1\")\n\t\t\tval := []*net.IP{&ipv4Addr, nil}\n\t\t\tinsertData(t, conn, val)\n\t\t\tcheckRows(t, conn, 1, val)\n\t\t})\n\t}\n}\n\nfunc TestArrayNullableIPv6(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tvar val []*net.IP\n\t\t\tconn, close := setupTest(t, \"col1 Array(Nullable(IPv6))\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tipv6Addr := net.ParseIP(\"2001:44c8:129:2632:33:0:252:2\")\n\t\t\tval = append(val, &ipv6Addr, nil)\n\t\t\tinsertData(t, conn, val)\n\t\t\tcheckRows(t, conn, 1, val)\n\t\t})\n\t}\n}\n\nfunc TestMap(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Map(String, UInt8)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := map[string]uint8{\"49\": uint8(49)}\n\t\t\tinsertData(t, conn, val)\n\t\t\tcheckRows(t, conn, 1, val)\n\t\t})\n\t}\n}\n\nfunc TestFixedString(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 FixedString(2)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := \"51\"\n\t\t\tinsertData(t, conn, val)\n\t\t\tcheckRows(t, conn, 1, val)\n\t\t})\n\t}\n}\n\nfunc TestNullableFixedString(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(FixedString(2))\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := \"52\"\n\t\t\tinsertData(t, conn, val)\n\t\t\tinsertData(t, conn, nil)\n\t\t\tcheckRows(t, conn, 2, &val, nil)\n\t\t})\n\t}\n}\n\nfunc TestLowCardinalityString(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 LowCardinality(String)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := \"53\"\n\t\t\tinsertData(t, conn, val)\n\t\t\tcheckRows(t, conn, 1, val)\n\t\t})\n\t}\n}\n\nfunc TestLowCardinalityNullableString(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 LowCardinality(Nullable(String))\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := \"53\"\n\t\t\tinsertData(t, conn, val)\n\t\t\tcheckRows(t, conn, 1, &val)\n\t\t})\n\t}\n}\n\nfunc TestConvertDate32(t *testing.T) {\n\tconn := setupConnection(t, clickhouse_sql.Native, nil)\n\tcanTest, err := CheckMinServerVersion(conn, 22, 3, 0)\n\tif err != nil {\n\t\tt.Skip(err.Error())\n\t\treturn\n\t}\n\tif !canTest {\n\t\tt.Skipf(\"Skipping Date32 test as version is < 22.3.0\")\n\t\treturn\n\t}\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Date32\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, date)\n\t\t\tcheckRows(t, conn, 1, date)\n\t\t})\n\t}\n}\n\nfunc TestConvertEnum(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Enum('55' = 55)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, \"55\")\n\t\t\tcheckRows(t, conn, 1, \"55\")\n\t\t})\n\t}\n}\n\nfunc TestConvertEnum8(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Enum8('WRITABLE' = 0, 'CONST' = 1, 'CHANGEABLE_IN_READONLY' = 2)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, \"CONST\")\n\t\t\tcheckRows(t, conn, 1, \"CONST\")\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableEnum8(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(Enum8('WRITABLE' = 0, 'CONST' = 1, 'CHANGEABLE_IN_READONLY' = 2))\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, \"CONST\")\n\t\t\tval := \"CONST\"\n\t\t\tcheckRows(t, conn, 1, &val)\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableEnum8WithNull(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(Enum8('WRITABLE' = 0, 'CONST' = 1, 'CHANGEABLE_IN_READONLY' = 2))\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, nil)\n\t\t\tcheckRows(t, conn, 1, (*string)(nil))\n\t\t})\n\t}\n}\n\nfunc TestConvertEnum16(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Enum16('option1' = 1000, 'option2' = 2000)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, \"option1\")\n\t\t\tcheckRows(t, conn, 1, \"option1\")\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableEnum16WithNull(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(Enum16('option1' = 1000, 'option2' = 2000))\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tinsertData(t, conn, nil)\n\t\t\tcheckRows(t, conn, 1, (*string)(nil))\n\t\t})\n\t}\n}\n\nfunc TestConvertUUID(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 UUID\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := \"417ddc5d-e556-4d27-95dd-a34d84e46a50\"\n\t\t\tinsertData(t, conn, val)\n\t\t\tcheckRows(t, conn, 1, &val)\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableUUID(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(UUID)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := \"417ddc5d-e556-4d27-95dd-a34d84e46a50\"\n\t\t\tinsertData(t, conn, val)\n\t\t\tcheckRows(t, conn, 1, &val)\n\t\t})\n\t}\n}\n\n// func TestConvertJSON(t *testing.T) {\n// \tconn := setupConnection(t, clickhouse_sql.Native, nil)\n// \tcanTest, err := plugin.CheckMinServerVersion(conn, 22, 6, 1)\n// \tif err != nil {\n// \t\tt.Skip(err.Error())\n// \t\treturn\n// \t}\n// \tif !canTest {\n// \t\tt.Skipf(\"Skipping JSON test as version is < 22.6.1\")\n// \t\treturn\n// \t}\n// \tfor name, protocol := range Protocols {\n// \t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n// \t\t\tconn, close := setupTest(t, \"col1 JSON\", protocol, clickhouse_sql.Settings{\n// \t\t\t\t\"allow_experimental_object_type\": 1,\n// \t\t\t})\n// \t\t\tdefer close(t)\n// \t\t\tval := map[string]interface{}{\n// \t\t\t\t\"test\": map[string][]string{\n// \t\t\t\t\t\"test\": {\"2\", \"3\"},\n// \t\t\t\t},\n// \t\t\t}\n// \t\t\tinsertData(t, conn, val)\n// \t\t\tcheckRows(t, conn, 1, val)\n// \t\t})\n// \t}\n// }\n\nfunc TestConvertIPv4(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 IPv4\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := net.ParseIP(\"127.0.0.1\")\n\t\t\tinsertData(t, conn, val)\n\t\t\tsVal := val.String()\n\t\t\tcheckRows(t, conn, 1, sVal)\n\t\t})\n\t}\n}\n\nfunc TestConvertIPv6(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 IPv6\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := net.ParseIP(\"2001:44c8:129:2632:33:0:252:2\")\n\t\t\tinsertData(t, conn, val)\n\t\t\tsVal := val.String()\n\t\t\tcheckRows(t, conn, 1, sVal)\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableIPv4(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(IPv4)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := net.ParseIP(\"127.0.0.1\")\n\t\t\tinsertData(t, conn, val, nil)\n\t\t\tsVal := val.String()\n\t\t\tcheckRows(t, conn, 2, &sVal, nil)\n\t\t})\n\t}\n}\n\nfunc TestConvertNullableIPv6(t *testing.T) {\n\tfor name, protocol := range Protocols {\n\t\tt.Run(fmt.Sprintf(\"using %s\", name), func(t *testing.T) {\n\t\t\tconn, close := setupTest(t, \"col1 Nullable(IPv6)\", protocol, nil)\n\t\t\tdefer close(t)\n\t\t\tval := net.ParseIP(\"2001:44c8:129:2632:33:0:252:2\")\n\t\t\tinsertData(t, conn, val, nil)\n\t\t\tsVal := val.String()\n\t\t\tcheckRows(t, conn, 2, &sVal, nil)\n\t\t})\n\t}\n}\n\n// disabled due to new JSON type in latest ClickHouse versions\n// func TestMutateResponse(t *testing.T) {\n// \tconn := setupConnection(t, clickhouse_sql.Native, nil)\n\n// \tcanTest, err := plugin.CheckMinServerVersion(conn, 22, 6, 1)\n// \tif err != nil {\n// \t\tt.Skip(err.Error())\n// \t\treturn\n// \t}\n// \tif !canTest {\n// \t\tt.Skipf(\"Skipping Mutate Response as version is < 22.6.1\")\n// \t\treturn\n// \t}\n\n// \tclickhouse := plugin.Clickhouse{}\n// \tassert.Equal(t, nil, err)\n// \tconn, close := setupTest(t, \"col1 JSON\", clickhouse_sql.Native, clickhouse_sql.Settings{\n// \t\t\"allow_experimental_object_type\": 1,\n// \t})\n// \tdefer close(t)\n// \tval := map[string]interface{}{\n// \t\t\"test\": map[string][]string{\n// \t\t\t\"test\": {\"2\", \"3\"},\n// \t\t},\n// \t}\n// \tinsertData(t, conn, val)\n\n// \tt.Run(\"doesn't mutate traces\", func(t *testing.T) {\n// \t\trows, err := conn.Query(\"SELECT * FROM simple_table LIMIT 1\")\n// \t\trequire.NoError(t, err)\n// \t\tframe, err := sqlutil.FrameFromRows(rows, 1, converters.ClickhouseConverters...)\n// \t\trequire.NoError(t, err)\n// \t\tframe.Meta = &data.FrameMeta{PreferredVisualization: data.VisType(data.VisTypeTrace)}\n// \t\tframes, err := clickhouse.MutateResponse(context.Background(), []*data.Frame{frame})\n// \t\trequire.NoError(t, err)\n// \t\trequire.NotNil(t, frames)\n// \t\tassert.Equal(t, frames[0].Fields[0].Type(), data.FieldTypeNullableJSON)\n// \t})\n\n// \tt.Run(\"doesn't mutate logs\", func(t *testing.T) {\n// \t\trows, err := conn.Query(\"SELECT * FROM simple_table LIMIT 1\")\n// \t\trequire.NoError(t, err)\n// \t\tframe, err := sqlutil.FrameFromRows(rows, 1, converters.ClickhouseConverters...)\n// \t\trequire.NoError(t, err)\n// \t\tframe.Meta = &data.FrameMeta{PreferredVisualization: data.VisType(data.VisTypeLogs)}\n// \t\tframes, err := clickhouse.MutateResponse(context.Background(), []*data.Frame{frame})\n// \t\trequire.NoError(t, err)\n// \t\trequire.NotNil(t, frames)\n// \t\tassert.Equal(t, frames[0].Fields[0].Type(), data.FieldTypeNullableJSON)\n// \t})\n\n// \tt.Run(\"mutates other types\", func(t *testing.T) {\n// \t\trows, err := conn.Query(\"SELECT * FROM simple_table LIMIT 1\")\n// \t\trequire.NoError(t, err)\n// \t\tframe, err := sqlutil.FrameFromRows(rows, 1, converters.ClickhouseConverters...)\n// \t\trequire.NoError(t, err)\n// \t\tframe.Meta = &data.FrameMeta{PreferredVisualization: data.VisType(data.VisTypeGraph)}\n// \t\tframes, err := clickhouse.MutateResponse(context.Background(), []*data.Frame{frame})\n// \t\trequire.NoError(t, err)\n// \t\trequire.NotNil(t, frames)\n// \t\tassert.Equal(t, frames[0].Fields[0].Type(), data.FieldTypeNullableString)\n// \t\tassert.NoError(t, err)\n// \t\tassert.Equal(t, \"{\\\"test\\\":{\\\"test\\\":[\\\"2\\\",\\\"3\\\"]}}\", *frames[0].Fields[0].At(0).(*string))\n// \t})\n// }\n\nfunc TestHTTPConnectWithHeaders(t *testing.T) {\n\tproxy := goproxy.NewProxyHttpServer()\n\tproxyHost := \"localhost\"\n\tproxyPort := getEnv(\"CLICKHOUSE_PROXY_PORT\", \"8122\")\n\tport := getEnv(\"CLICKHOUSE_HTTP_PORT\", \"8123\")\n\thost := getEnv(\"CLICKHOUSE_HOST\", \"localhost\")\n\n\tgo func() {\n\t\terr := http.ListenAndServe(fmt.Sprintf(\"%s:%s\", proxyHost, proxyPort), proxy)\n\t\tassert.Equal(t, nil, err)\n\t}()\n\n\t// Wait for proxy server to be ready\n\tproxyAddr := net.JoinHostPort(proxyHost, proxyPort)\n\n\twaitCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\n\td := &net.Dialer{}\n\ttick := time.NewTicker(100 * time.Millisecond)\n\tdefer tick.Stop()\n\n\tfor {\n\t\tconn, err := d.DialContext(waitCtx, \"tcp\", proxyAddr)\n\t\tif err == nil {\n\t\t\t_ = conn.Close()\n\t\t\tbreak\n\t\t}\n\n\t\tselect {\n\t\tcase <-waitCtx.Done():\n\t\t\tt.Fatalf(\"proxy server failed to start within 5s: %v\", err)\n\t\tcase <-tick.C:\n\t\t}\n\t}\n\n\tusername := getEnv(\"CLICKHOUSE_USERNAME\", \"default\")\n\tpassword := getEnv(\"CLICKHOUSE_PASSWORD\", \"\")\n\tsecure := map[string]string{}\n\tsecure[\"password\"] = password\n\n\tctx := context.Background()\n\tctx = backend.WithGrafanaConfig(ctx, backend.NewGrafanaCfg(map[string]string{\n\t\t\"GF_SQL_ROW_LIMIT\":                         \"1000000\",\n\t\t\"GF_SQL_MAX_OPEN_CONNS_DEFAULT\":            \"10\",\n\t\t\"GF_SQL_MAX_IDLE_CONNS_DEFAULT\":            \"10\",\n\t\t\"GF_SQL_MAX_CONN_LIFETIME_SECONDS_DEFAULT\": \"60\",\n\t}))\n\n\tproxyHandlerGenerator := func(t *testing.T, expectedHeaders map[string]string) http.HandlerFunc {\n\t\treturn http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\t\tfor k, v := range expectedHeaders {\n\t\t\t\tassert.Equal(t, v, req.Header.Get(k))\n\t\t\t}\n\t\t\treq.URL.Scheme = \"http\"\n\t\t\treq.URL.Host = fmt.Sprintf(\"%s:%s\", host, port)\n\t\t\tproxy.ServeHTTP(w, req)\n\t\t})\n\t}\n\n\treq := &backend.QueryDataRequest{\n\t\tPluginContext: backend.PluginContext{},\n\t\tHeaders: map[string]string{\n\t\t\t// only headers starting with http_ are forwarded, except for Authorization, Cookie and X-Id-Token\n\t\t\t\"http_X-Test\": \"Hello World!\",\n\t\t},\n\t\tQueries: []backend.DataQuery{\n\t\t\t{\n\t\t\t\tRefID: \"A\",\n\t\t\t\tJSON: []byte(`{\n\t\t\t\t\t\"rawSql\": \"SELECT 1\"\n\t\t\t\t}`),\n\t\t\t},\n\t\t},\n\t}\n\tt.Run(\"should not forward http headers\", func(t *testing.T) {\n\t\tsettings := backend.DataSourceInstanceSettings{JSONData: []byte(fmt.Sprintf(`{\"server\": \"localhost\", \"port\": %s, \"username\": \"%s\", \"protocol\": \"http\", \"forwardGrafanaHeaders\": false}`, proxyPort, username)), DecryptedSecureJSONData: secure}\n\t\tproxy.NonproxyHandler = proxyHandlerGenerator(t, map[string]string{\"X-Test\": \"\"})\n\t\tdsInstance, err := NewDatasource(ctx, settings)\n\t\tassert.Equal(t, nil, err)\n\n\t\tds, ok := dsInstance.(backend.QueryDataHandler)\n\t\trequire.True(t, ok, \"instance must implement backend.QueryDataHandler\")\n\n\t\t// We test that the X-Test header is absent\n\t\treq.PluginContext.DataSourceInstanceSettings = &settings\n\n\t\t_, err = ds.QueryData(ctx, req)\n\n\t\tassert.Equal(t, nil, err)\n\t})\n\n\tt.Run(\"should forward http headers\", func(t *testing.T) {\n\t\tsettings := backend.DataSourceInstanceSettings{JSONData: []byte(fmt.Sprintf(`{\"server\": \"localhost\", \"port\": %s, \"username\": \"%s\", \"protocol\": \"http\", \"forwardGrafanaHeaders\": true}`, proxyPort, username)), DecryptedSecureJSONData: secure}\n\t\tproxy.NonproxyHandler = proxyHandlerGenerator(t, map[string]string{\"X-Test\": \"\"})\n\t\tdsInstance, err := NewDatasource(ctx, settings)\n\t\tassert.Equal(t, nil, err)\n\n\t\tds, ok := dsInstance.(backend.QueryDataHandler)\n\t\trequire.True(t, ok, \"instance must implement backend.QueryDataHandler\")\n\n\t\t// We test that the X-Test header exists\n\t\tproxy.NonproxyHandler = proxyHandlerGenerator(t, map[string]string{\"X-Test\": \"Hello World!\"})\n\t\treq.PluginContext.DataSourceInstanceSettings = &settings\n\t\t_, err = ds.QueryData(ctx, req)\n\n\t\tassert.Equal(t, nil, err)\n\t})\n\n\tt.Run(\"should forward http headers alongside custom http headers\", func(t *testing.T) {\n\t\tsettings := backend.DataSourceInstanceSettings{JSONData: []byte(fmt.Sprintf(`{\"server\": \"localhost\", \"port\": %s, \"username\": \"%s\", \"protocol\": \"http\", \"forwardGrafanaHeaders\": true, \"httpHeaders\": [{ \"name\": \"custom-test-header\", \"value\": \"value-1\", \"secure\": false}]}`, proxyPort, username)), DecryptedSecureJSONData: secure}\n\t\tproxy.NonproxyHandler = proxyHandlerGenerator(t, map[string]string{\"custom-test-header\": \"value-1\"})\n\t\tdsInstance, err := NewDatasource(ctx, settings)\n\t\tassert.Equal(t, nil, err)\n\n\t\tds, ok := dsInstance.(backend.QueryDataHandler)\n\t\trequire.True(t, ok, \"instance must implement backend.QueryDataHandler\")\n\n\t\t// We test that the X-Test header exists\n\t\tproxy.NonproxyHandler = proxyHandlerGenerator(t, map[string]string{\"custom-test-header\": \"value-1\", \"X-Test\": \"Hello World!\"})\n\t\treq.PluginContext.DataSourceInstanceSettings = &settings\n\t\t_, err = ds.QueryData(ctx, req)\n\n\t\tassert.Equal(t, nil, err)\n\t})\n\n\tt.Run(\"should override forward headers with custom http headers\", func(t *testing.T) {\n\t\tsettings := backend.DataSourceInstanceSettings{JSONData: []byte(fmt.Sprintf(`{\"server\": \"localhost\", \"port\": %s, \"username\": \"%s\", \"protocol\": \"http\", \"forwardGrafanaHeaders\": true, \"httpHeaders\": [{ \"name\": \"X-Test\", \"value\": \"Override\", \"secure\": false}]}`, proxyPort, username)), DecryptedSecureJSONData: secure}\n\t\tproxy.NonproxyHandler = proxyHandlerGenerator(t, map[string]string{\"X-Test\": \"Override\"})\n\t\tdsInstance, err := NewDatasource(ctx, settings)\n\t\tassert.Equal(t, nil, err)\n\n\t\tds, ok := dsInstance.(backend.QueryDataHandler)\n\t\trequire.True(t, ok, \"instance must implement backend.QueryDataHandler\")\n\n\t\t// We test that the X-Test header exists\n\t\tproxy.NonproxyHandler = proxyHandlerGenerator(t, map[string]string{\"X-Test\": \"Override\"})\n\t\treq.PluginContext.DataSourceInstanceSettings = &settings\n\t\t_, err = ds.QueryData(ctx, req)\n\n\t\tassert.Equal(t, nil, err)\n\t})\n}\n"
  },
  {
    "path": "pkg/plugin/driver_test.go",
    "content": "package plugin\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/ClickHouse/clickhouse-go/v2\"\n\t\"github.com/grafana/grafana-plugin-sdk-go/backend\"\n\t\"github.com/grafana/grafana-plugin-sdk-go/data\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestMergeOpenTelemetryLabels(t *testing.T) {\n\tt.Run(\"Merge\", func(t *testing.T) {\n\t\tresourceAttrs := []json.RawMessage{\n\t\t\tjson.RawMessage(`{\"foo\":\"bar\"}`),\n\t\t\tjson.RawMessage(`{\"baz\":\"qux\"}`),\n\t\t}\n\t\tscopeAttrs := []json.RawMessage{\n\t\t\tjson.RawMessage(`{\"scopeA\":\"123\"}`),\n\t\t\tjson.RawMessage(`{\"scopeB\":\"456\"}`),\n\t\t}\n\t\totherField := []int64{1, 2}\n\n\t\tframe := &data.Frame{\n\t\t\tFields: []*data.Field{\n\t\t\t\tdata.NewField(\"ResourceAttributes\", nil, resourceAttrs),\n\t\t\t\tdata.NewField(\"ScopeAttributes\", nil, scopeAttrs),\n\t\t\t\tdata.NewField(\"other\", nil, otherField),\n\t\t\t},\n\t\t}\n\n\t\terr := mergeOpenTelemetryLabels(frame)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, len(frame.Fields))\n\t\tassert.Equal(t, \"other\", frame.Fields[0].Name)\n\t\tassert.Equal(t, \"labels\", frame.Fields[1].Name)\n\n\t\tlabelsLen := frame.Fields[1].Len()\n\t\tfor i := 0; i < labelsLen; i++ {\n\t\t\tlabelValue, _ := frame.Fields[1].ConcreteAt(i)\n\t\t\tvar labelsMap map[string]interface{}\n\t\t\tassert.NoError(t, json.Unmarshal(labelValue.(json.RawMessage), &labelsMap))\n\t\t\t// Keys should be prefixed\n\t\t\tif i == 0 {\n\t\t\t\tassert.Equal(t, \"bar\", labelsMap[\"ResourceAttributes.foo\"])\n\t\t\t\tassert.Equal(t, \"123\", labelsMap[\"ScopeAttributes.scopeA\"])\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, \"qux\", labelsMap[\"ResourceAttributes.baz\"])\n\t\t\t\tassert.Equal(t, \"456\", labelsMap[\"ScopeAttributes.scopeB\"])\n\t\t\t}\n\t\t}\n\t})\n\n\tt.Run(\"LabelsFieldPresent\", func(t *testing.T) {\n\t\tframe := &data.Frame{\n\t\t\tFields: []*data.Field{\n\t\t\t\tdata.NewField(\"labels\", nil, []json.RawMessage{json.RawMessage(`{}`)}),\n\t\t\t\tdata.NewField(\"ResourceAttributes\", nil, []json.RawMessage{json.RawMessage(`{\"foo\":\"bar\"}`)}),\n\t\t\t},\n\t\t}\n\t\terr := mergeOpenTelemetryLabels(frame)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, len(frame.Fields))\n\t\tassert.Equal(t, \"labels\", frame.Fields[0].Name) // Should not modify fields\n\t})\n\n\tt.Run(\"EmptyFields\", func(t *testing.T) {\n\t\tframe := &data.Frame{\n\t\t\tFields: []*data.Field{},\n\t\t}\n\t\terr := mergeOpenTelemetryLabels(frame)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 0, len(frame.Fields))\n\t})\n\n\tt.Run(\"FieldTypeFilter\", func(t *testing.T) {\n\t\t// Should ignore non-JSON fields\n\t\tframe := &data.Frame{\n\t\t\tFields: []*data.Field{\n\t\t\t\tdata.NewField(\"ResourceAttributes\", nil, []string{`{\"foo\":\"bar\"}`, `{\"zoo\": \"car\"}`}),\n\t\t\t\tdata.NewField(\"ScopeAttributes\", nil, []int64{1, 2}),\n\t\t\t},\n\t\t}\n\t\terr := mergeOpenTelemetryLabels(frame)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 2, len(frame.Fields))\n\t\tassert.Equal(t, \"ResourceAttributes\", frame.Fields[0].Name)\n\t\tassert.Equal(t, \"ScopeAttributes\", frame.Fields[1].Name)\n\t})\n}\n\nfunc TestAssignFlattenedPath(t *testing.T) {\n\tt.Run(\"simple value\", func(t *testing.T) {\n\t\tflatMap := make(map[string]any)\n\t\tassignFlattenedPath(flatMap, \"root\", \"key\", \"value\")\n\n\t\texpected := map[string]any{\n\t\t\t\"root.key\": \"value\",\n\t\t}\n\n\t\tassert.Equal(t, expected, flatMap)\n\t})\n\n\tt.Run(\"empty path key\", func(t *testing.T) {\n\t\tflatMap := make(map[string]any)\n\t\tassignFlattenedPath(flatMap, \"root\", \"\", \"value\")\n\n\t\texpected := map[string]any{\n\t\t\t\"root\": \"value\",\n\t\t}\n\n\t\tassert.Equal(t, expected, flatMap)\n\t})\n\n\tt.Run(\"nested map\", func(t *testing.T) {\n\t\tflatMap := make(map[string]any)\n\t\tnestedValue := map[string]any{\n\t\t\t\"a\": \"val1\",\n\t\t\t\"b\": \"val2\",\n\t\t}\n\n\t\tassignFlattenedPath(flatMap, \"root\", \"nested\", nestedValue)\n\n\t\texpected := map[string]any{\n\t\t\t\"root.nested.a\": \"val1\",\n\t\t\t\"root.nested.b\": \"val2\",\n\t\t}\n\n\t\tassert.Equal(t, expected, flatMap)\n\t})\n\n\tt.Run(\"deeply nested map\", func(t *testing.T) {\n\t\tflatMap := make(map[string]any)\n\t\tdeeplyNested := map[string]any{\n\t\t\t\"level1\": map[string]any{\n\t\t\t\t\"level2\": map[string]any{\n\t\t\t\t\t\"level3\":     \"l3_value\",\n\t\t\t\t\t\"level3_alt\": \"l3_value2\",\n\t\t\t\t},\n\t\t\t\t\"level2_alt\": \"l2_value\",\n\t\t\t},\n\t\t\t\"level1_alt\": \"l1_value\",\n\t\t}\n\n\t\tassignFlattenedPath(flatMap, \"root\", \"deep\", deeplyNested)\n\n\t\texpected := map[string]any{\n\t\t\t\"root.deep.level1.level2.level3\":     \"l3_value\",\n\t\t\t\"root.deep.level1.level2.level3_alt\": \"l3_value2\",\n\t\t\t\"root.deep.level1.level2_alt\":        \"l2_value\",\n\t\t\t\"root.deep.level1_alt\":               \"l1_value\",\n\t\t}\n\n\t\tassert.Equal(t, expected, flatMap)\n\t})\n\n\tt.Run(\"empty nested map\", func(t *testing.T) {\n\t\tflatMap := make(map[string]any)\n\t\temptyMap := map[string]any{}\n\n\t\tassignFlattenedPath(flatMap, \"root\", \"empty\", emptyMap)\n\n\t\texpected := map[string]any{}\n\t\tassert.Equal(t, expected, flatMap)\n\t})\n\n\tt.Run(\"mixed types\", func(t *testing.T) {\n\t\tflatMap := make(map[string]any)\n\t\tmixedValue := map[string]any{\n\t\t\t\"string\":  \"test\",\n\t\t\t\"number\":  42,\n\t\t\t\"boolean\": true,\n\t\t\t\"float\":   3.14,\n\t\t\t\"null\":    nil,\n\t\t}\n\n\t\tassignFlattenedPath(flatMap, \"data\", \"mixed\", mixedValue)\n\n\t\texpected := map[string]any{\n\t\t\t\"data.mixed.string\":  \"test\",\n\t\t\t\"data.mixed.number\":  42,\n\t\t\t\"data.mixed.boolean\": true,\n\t\t\t\"data.mixed.float\":   3.14,\n\t\t\t\"data.mixed.null\":    nil,\n\t\t}\n\n\t\tassert.Equal(t, expected, flatMap)\n\t})\n\n\tt.Run(\"non-map values\", func(t *testing.T) {\n\t\ttests := []struct {\n\t\t\tname     string\n\t\t\tvalue    any\n\t\t\texpected any\n\t\t}{\n\t\t\t{\"string\", \"hello\", \"hello\"},\n\t\t\t{\"int\", 123, 123},\n\t\t\t{\"bool\", false, false},\n\t\t\t{\"slice\", []int{1, 2, 3}, []int{1, 2, 3}},\n\t\t\t{\"nil\", nil, nil},\n\t\t}\n\n\t\tfor _, tt := range tests {\n\t\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t\tflatMap := make(map[string]any)\n\t\t\t\tassignFlattenedPath(flatMap, \"root\", \"key\", tt.value)\n\n\t\t\t\texpected := map[string]any{\n\t\t\t\t\t\"root.key\": tt.expected,\n\t\t\t\t}\n\n\t\t\t\tassert.Equal(t, expected, flatMap)\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"complex nesting multiple calls\", func(t *testing.T) {\n\t\tflatMap := make(map[string]any)\n\n\t\tassignFlattenedPath(flatMap, \"config\", \"database\", map[string]any{\n\t\t\t\"host\": \"localhost\",\n\t\t\t\"port\": 8123,\n\t\t\t\"credentials\": map[string]any{\n\t\t\t\t\"username\": \"admin\",\n\t\t\t\t\"password\": \"pass\",\n\t\t\t},\n\t\t})\n\n\t\tassignFlattenedPath(flatMap, \"config\", \"server\", map[string]any{\n\t\t\t\"port\":   9000,\n\t\t\t\"secure\": true,\n\t\t})\n\n\t\tassignFlattenedPath(flatMap, \"config\", \"some_key\", \"some_value\")\n\n\t\texpected := map[string]any{\n\t\t\t\"config.database.host\":                 \"localhost\",\n\t\t\t\"config.database.port\":                 8123,\n\t\t\t\"config.database.credentials.username\": \"admin\",\n\t\t\t\"config.database.credentials.password\": \"pass\",\n\t\t\t\"config.server.port\":                   9000,\n\t\t\t\"config.server.secure\":                 true,\n\t\t\t\"config.some_key\":                      \"some_value\",\n\t\t}\n\n\t\tassert.Equal(t, expected, flatMap)\n\t})\n\n\tt.Run(\"empty path prefix\", func(t *testing.T) {\n\t\tflatMap := make(map[string]any)\n\n\t\tassignFlattenedPath(flatMap, \"\", \"key\", \"value\")\n\n\t\texpected := map[string]any{\n\t\t\t\".key\": \"value\",\n\t\t}\n\n\t\tassert.Equal(t, expected, flatMap)\n\t})\n}\n\nfunc TestContainsClickHouseException(t *testing.T) {\n\tt.Run(\"nil error\", func(t *testing.T) {\n\t\tresult := containsClickHouseException(nil)\n\t\tassert.False(t, result)\n\t})\n\n\tt.Run(\"direct clickhouse exception\", func(t *testing.T) {\n\t\tchErr := &clickhouse.Exception{\n\t\t\tCode:    60,\n\t\t\tMessage: \"Unknown table\",\n\t\t}\n\t\tresult := containsClickHouseException(chErr)\n\t\tassert.True(t, result)\n\t})\n\n\tt.Run(\"wrapped clickhouse exception\", func(t *testing.T) {\n\t\tchErr := &clickhouse.Exception{\n\t\t\tCode:    62,\n\t\t\tMessage: \"Syntax error\",\n\t\t}\n\t\twrappedErr := fmt.Errorf(\"query failed: %w\", chErr)\n\t\tresult := containsClickHouseException(wrappedErr)\n\t\tassert.True(t, result)\n\t})\n\n\tt.Run(\"HTTP response body with clickhouse error\", func(t *testing.T) {\n\t\terrMsg := `error querying the database: sendQuery: [HTTP 404] response body: \\\"Code: 60. DB::Exception: Unknown table expression identifier 'hello' in scope SELECT * FROM hello. (UNKNOWN_TABLE) (version 25.1.3.23 (official build))\\n\\\"`\n\t\terr := errors.New(errMsg)\n\t\tresult := containsClickHouseException(err)\n\t\tassert.True(t, result)\n\t})\n\n\tt.Run(\"HTTP response body with legacy clickhouse error\", func(t *testing.T) {\n\t\terrMsg := `error querying the database: sendQuery: [HTTP 404] response body: \\\"[Error] Unknown table expression identifier 'hello' in scope SELECT * FROM hello. (UNKNOWN_TABLE) (version 25.1.3.23 (official build))\\n\\\"`\n\t\terr := errors.New(errMsg)\n\t\tresult := containsClickHouseException(err)\n\t\tassert.True(t, result)\n\t})\n\n\tt.Run(\"regular error without clickhouse patterns\", func(t *testing.T) {\n\t\terr := errors.New(\"connection timeout\")\n\t\tresult := containsClickHouseException(err)\n\t\tassert.False(t, result)\n\t})\n\n\tt.Run(\"multi-error with clickhouse exception\", func(t *testing.T) {\n\t\tchErr := &clickhouse.Exception{\n\t\t\tCode:    60,\n\t\t\tMessage: \"Unknown table\",\n\t\t}\n\t\tregularErr := errors.New(\"regular error\")\n\t\tmultiErr := errors.Join(regularErr, chErr)\n\t\tresult := containsClickHouseException(multiErr)\n\t\tassert.True(t, result)\n\t})\n}\n\nfunc TestMutateQueryData(t *testing.T) {\n\th := &Clickhouse{}\n\n\ttests := []struct {\n\t\tname   string\n\t\theaders map[string]string\n\t\twant   grafanaHeaders\n\t\tstored bool\n\t}{\n\t\t{\n\t\t\tname: \"all headers\",\n\t\t\theaders: map[string]string{\n\t\t\t\t\"http_X-Dashboard-Uid\": \"dash-abc123\",\n\t\t\t\t\"http_X-Panel-Id\":      \"42\",\n\t\t\t\t\"http_X-Rule-Uid\":      \"rule-xyz\",\n\t\t\t},\n\t\t\twant:   grafanaHeaders{DashboardUID: \"dash-abc123\", PanelID: \"42\", RuleUID: \"rule-xyz\"},\n\t\t\tstored: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"empty headers\",\n\t\t\theaders: map[string]string{},\n\t\t\tstored:  false,\n\t\t},\n\t\t{\n\t\t\tname:    \"only dashboard\",\n\t\t\theaders: map[string]string{\"http_X-Dashboard-Uid\": \"dash-only\"},\n\t\t\twant:    grafanaHeaders{DashboardUID: \"dash-only\"},\n\t\t\tstored:  true,\n\t\t},\n\t\t{\n\t\t\tname:    \"only panel\",\n\t\t\theaders: map[string]string{\"http_X-Panel-Id\": \"99\"},\n\t\t\twant:    grafanaHeaders{PanelID: \"99\"},\n\t\t\tstored:  true,\n\t\t},\n\t\t{\n\t\t\tname:    \"only rule\",\n\t\t\theaders: map[string]string{\"http_X-Rule-Uid\": \"alert-rule-1\"},\n\t\t\twant:    grafanaHeaders{RuleUID: \"alert-rule-1\"},\n\t\t\tstored:  true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\treq := &backend.QueryDataRequest{Headers: tt.headers}\n\t\t\tnewCtx, _ := h.MutateQueryData(t.Context(), req)\n\n\t\t\tgh, ok := newCtx.Value(grafanaHeadersKey).(grafanaHeaders)\n\t\t\tassert.Equal(t, tt.stored, ok)\n\t\t\tif tt.stored {\n\t\t\t\tassert.Equal(t, tt.want, gh)\n\t\t\t}\n\t\t})\n\t}\n\n\tt.Run(\"nil headers does not panic\", func(t *testing.T) {\n\t\tnewCtx, newReq := h.MutateQueryData(t.Context(), &backend.QueryDataRequest{})\n\t\tassert.NotNil(t, newCtx)\n\t\tassert.NotNil(t, newReq)\n\t})\n}\n\nfunc TestMutateQuery_GrafanaMetadata(t *testing.T) {\n\th := &Clickhouse{}\n\n\tt.Run(\"includes dashboard and panel from context\", func(t *testing.T) {\n\t\tctx := context.WithValue(t.Context(), grafanaHeadersKey, grafanaHeaders{\n\t\t\tDashboardUID: \"my-dashboard\",\n\t\t\tPanelID:      \"7\",\n\t\t\tRuleUID:      \"alert-1\",\n\t\t})\n\n\t\tnewCtx, _ := h.MutateQuery(ctx, backend.DataQuery{\n\t\t\tJSON: []byte(`{}`),\n\t\t})\n\n\t\tassert.NotEqual(t, ctx, newCtx)\n\t})\n\n\tt.Run(\"no grafana headers in context still works\", func(t *testing.T) {\n\t\tctx := t.Context()\n\n\t\tnewCtx, _ := h.MutateQuery(ctx, backend.DataQuery{\n\t\t\tJSON: []byte(`{}`),\n\t\t})\n\n\t\tassert.NotNil(t, newCtx)\n\t\t_, ok := newCtx.Value(grafanaHeadersKey).(grafanaHeaders)\n\t\tassert.False(t, ok)\n\t})\n\n\tt.Run(\"handles invalid JSON gracefully\", func(t *testing.T) {\n\t\tctx := context.WithValue(t.Context(), grafanaHeadersKey, grafanaHeaders{\n\t\t\tDashboardUID: \"dash1\",\n\t\t})\n\n\t\tnewCtx, _ := h.MutateQuery(ctx, backend.DataQuery{\n\t\t\tJSON: []byte(`invalid json`),\n\t\t})\n\n\t\tassert.NotEqual(t, ctx, newCtx)\n\t})\n}\n\nfunc TestMutateQueryData_XGrafanaUserForwarding(t *testing.T) {\n\th := &Clickhouse{}\n\n\tnewRequest := func(forward bool) *backend.QueryDataRequest {\n\t\tjsonBytes, _ := json.Marshal(map[string]any{\n\t\t\t\"host\":                  \"localhost\",\n\t\t\t\"port\":                  9000,\n\t\t\t\"forwardGrafanaHeaders\": forward,\n\t\t})\n\t\treturn &backend.QueryDataRequest{\n\t\t\tPluginContext: backend.PluginContext{\n\t\t\t\tDataSourceInstanceSettings: &backend.DataSourceInstanceSettings{\n\t\t\t\t\tJSONData: jsonBytes,\n\t\t\t\t},\n\t\t\t},\n\t\t\tHeaders: map[string]string{},\n\t\t}\n\t}\n\n\tt.Run(\"populates X-Grafana-User from context when forwardGrafanaHeaders is enabled\", func(t *testing.T) {\n\t\treq := newRequest(true)\n\t\tctx := backend.WithUser(t.Context(), &backend.User{Login: \"alice\"})\n\n\t\th.MutateQueryData(ctx, req)\n\n\t\tassert.Equal(t, \"alice\", req.GetHTTPHeader(\"X-Grafana-User\"))\n\t})\n\n\tt.Run(\"does not inject when forwardGrafanaHeaders is disabled\", func(t *testing.T) {\n\t\treq := newRequest(false)\n\t\tctx := backend.WithUser(t.Context(), &backend.User{Login: \"alice\"})\n\n\t\th.MutateQueryData(ctx, req)\n\n\t\tassert.Empty(t, req.GetHTTPHeader(\"X-Grafana-User\"))\n\t})\n\n\tt.Run(\"does not override header already set by Grafana proxy\", func(t *testing.T) {\n\t\treq := newRequest(true)\n\t\treq.SetHTTPHeader(\"X-Grafana-User\", \"from-proxy\")\n\t\tctx := backend.WithUser(t.Context(), &backend.User{Login: \"alice\"})\n\n\t\th.MutateQueryData(ctx, req)\n\n\t\tassert.Equal(t, \"from-proxy\", req.GetHTTPHeader(\"X-Grafana-User\"))\n\t})\n\n\tt.Run(\"no user in context is a no-op\", func(t *testing.T) {\n\t\treq := newRequest(true)\n\n\t\th.MutateQueryData(t.Context(), req)\n\n\t\tassert.Empty(t, req.GetHTTPHeader(\"X-Grafana-User\"))\n\t})\n\n\tt.Run(\"nil DataSourceInstanceSettings is a no-op\", func(t *testing.T) {\n\t\treq := &backend.QueryDataRequest{Headers: map[string]string{}}\n\t\tctx := backend.WithUser(t.Context(), &backend.User{Login: \"alice\"})\n\n\t\t// Should not panic and should not set the header.\n\t\th.MutateQueryData(ctx, req)\n\n\t\tassert.Empty(t, req.GetHTTPHeader(\"X-Grafana-User\"))\n\t})\n\n\tt.Run(\"empty Login is a no-op\", func(t *testing.T) {\n\t\treq := newRequest(true)\n\t\tctx := backend.WithUser(t.Context(), &backend.User{Login: \"\"})\n\n\t\th.MutateQueryData(ctx, req)\n\n\t\tassert.Empty(t, req.GetHTTPHeader(\"X-Grafana-User\"))\n\t})\n}\n"
  },
  {
    "path": "pkg/plugin/errors.go",
    "content": "package plugin\n\nimport \"github.com/pkg/errors\"\n\nvar (\n\tErrorMessageInvalidJSON       = errors.New(\"could not parse json\")\n\tErrorMessageInvalidHost       = errors.New(\"invalid server host. Either empty or not set\")\n\tErrorMessageInvalidPort       = errors.New(\"invalid port\")\n\tErrorMessageInvalidUserName   = errors.New(\"username is either empty or not set\")\n\tErrorMessageInvalidPassword   = errors.New(\"password is either empty or not set\")\n\tErrorMessageInvalidProtocol   = errors.New(\"protocol is invalid, use native or http\")\n\tErrorInvalidClientCertificate = errors.New(\"tls: failed to find any PEM data in certificate input\")\n\tErrorInvalidCACertificate     = errors.New(\"failed to parse TLS CA PEM certificate\")\n)\n"
  },
  {
    "path": "pkg/plugin/schema.go",
    "content": "package plugin\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/grafana/clickhouse-datasource/pkg/plugin/schemacache\"\n\t\"github.com/grafana/grafana-plugin-sdk-go/backend\"\n\tschemas \"github.com/grafana/schemads\"\n)\n\n// dbConnector is the SchemaProvider's view of Clickhouse, narrowed for tests.\ntype dbConnector interface {\n\tConnect(ctx context.Context, config backend.DataSourceInstanceSettings, message json.RawMessage) (*sql.DB, error)\n}\n\nvar (\n\tnumberOperators = []schemas.Operator{\n\t\tschemas.OperatorGreaterThan,\n\t\tschemas.OperatorGreaterThanOrEqual,\n\t\tschemas.OperatorLessThan,\n\t\tschemas.OperatorLessThanOrEqual,\n\t\tschemas.OperatorEquals,\n\t\tschemas.OperatorNotEquals,\n\t\tschemas.OperatorIn,\n\t}\n\ttimeRangeOperators = []schemas.Operator{\n\t\tschemas.OperatorGreaterThan,\n\t\tschemas.OperatorGreaterThanOrEqual,\n\t\tschemas.OperatorLessThan,\n\t\tschemas.OperatorLessThanOrEqual,\n\t\tschemas.OperatorEquals,\n\t\tschemas.OperatorNotEquals,\n\t}\n\tequalityOperators = []schemas.Operator{\n\t\tschemas.OperatorEquals,\n\t\tschemas.OperatorIn,\n\t\tschemas.OperatorNotEquals,\n\t}\n\tstringOperators = []schemas.Operator{\n\t\tschemas.OperatorEquals,\n\t\tschemas.OperatorNotEquals,\n\t\tschemas.OperatorIn,\n\t\tschemas.OperatorLike,\n\t}\n)\n\ntype SchemaProvider struct {\n\tclickhousePlugin dbConnector\n\tsettings         backend.DataSourceInstanceSettings\n\n\t// db is the long-lived *sql.DB shared by every schema introspection call.\n\t// Lazily built on first use under dbMu and closed by Close. nil after Close.\n\tdbMu sync.Mutex\n\tdb   *sql.DB\n\n\t// The three caches below are populated from datasource settings at\n\t// construction time. A nil cache means caching is disabled for that\n\t// handler — the handler code must treat nil as \"always miss, no set\".\n\ttablesCache  *schemacache.Cache[[]string]\n\tcolumnsCache *schemacache.Cache[map[string][]schemas.Column]\n\tvaluesCache  *schemacache.Cache[map[string][]string]\n}\n\n// getDB returns the provider's shared *sql.DB, building it on first use.\n// On Connect failure the result is not cached so transient errors recover on\n// the next call. After Close, getDB rebuilds — Close is intended for shutdown\n// but the contract stays simple either way.\nfunc (p *SchemaProvider) getDB(ctx context.Context) (*sql.DB, error) {\n\tp.dbMu.Lock()\n\tdefer p.dbMu.Unlock()\n\tif p.db != nil {\n\t\treturn p.db, nil\n\t}\n\tdb, err := p.clickhousePlugin.Connect(ctx, p.settings, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tp.db = db\n\treturn db, nil\n}\n\n// Close releases the shared *sql.DB. Safe to call multiple times.\nfunc (p *SchemaProvider) Close() error {\n\tp.dbMu.Lock()\n\tdefer p.dbMu.Unlock()\n\tif p.db == nil {\n\t\treturn nil\n\t}\n\terr := p.db.Close()\n\tp.db = nil\n\treturn err\n}\n\n// Schema implements [schemas.SchemaHandler].\nfunc (p *SchemaProvider) Schema(ctx context.Context, req *schemas.SchemaRequest) (*schemas.SchemaResponse, error) {\n\ttableResponse, err := p.Tables(ctx, &schemas.TablesRequest{})\n\tif err != nil {\n\t\treturn &schemas.SchemaResponse{\n\t\t\tErrors: err.Error(),\n\t\t}, nil\n\t}\n\n\tcolumnsMap, err := p.cachedFetchColumns(ctx, tableResponse.Tables, nil)\n\tif err != nil {\n\t\treturn &schemas.SchemaResponse{\n\t\t\tErrors: err.Error(),\n\t\t}, nil\n\t}\n\n\tresponse := &schemas.SchemaResponse{\n\t\tFullSchema: &schemas.Schema{\n\t\t\tTables: make([]schemas.Table, 0),\n\t\t},\n\t}\n\tfor _, table := range tableResponse.Tables {\n\t\tcolumns := columnsMap[table]\n\t\tresponse.FullSchema.Tables = append(response.FullSchema.Tables, schemas.Table{\n\t\t\tName:    table,\n\t\t\tColumns: columns,\n\t\t})\n\t}\n\treturn response, nil\n}\n\n// Tables implements [schemas.TablesHandler].\n//\n// Results are memoised via the per-datasource tables cache when enabled. The\n// cache holds a single entry (\"all\") because the query is parameter-free —\n// every caller gets the same list. The 60s TTL (see NewSchemaProvider) means\n// a newly-created table shows up within one TTL window, which is acceptable\n// for a schema picker but explicit here so future readers know the trade-off.\nfunc (p *SchemaProvider) Tables(ctx context.Context, req *schemas.TablesRequest) (*schemas.TablesResponse, error) {\n\tif p.tablesCache == nil {\n\t\ttables, err := p.fetchTables(ctx)\n\t\tif err != nil {\n\t\t\treturn &schemas.TablesResponse{Errors: map[string]string{\"A\": err.Error()}}, nil\n\t\t}\n\t\treturn &schemas.TablesResponse{Tables: tables}, nil\n\t}\n\n\ttables, err := p.tablesCache.Do(ctx, \"all\", func(ctx context.Context) ([]string, error) {\n\t\treturn p.fetchTables(ctx)\n\t})\n\tif err != nil {\n\t\treturn &schemas.TablesResponse{Errors: map[string]string{\"A\": err.Error()}}, nil\n\t}\n\treturn &schemas.TablesResponse{Tables: tables}, nil\n}\n\n// fetchTables runs the underlying system.tables query and is the function\n// wrapped by the cache in [SchemaProvider.Tables]. Kept as a method so a\n// singleflight-collapsed caller can reuse the same code path as a\n// cache-disabled caller.\nfunc (p *SchemaProvider) fetchTables(ctx context.Context) ([]string, error) {\n\tds, err := p.getDB(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trows, err := ds.QueryContext(ctx, \"SELECT database, name FROM system.tables ORDER BY database, name\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\tif err := rows.Close(); err != nil {\n\t\t\tbackend.Logger.Error(\"failed to close rows\", \"error\", err)\n\t\t}\n\t}()\n\n\ttables := make([]string, 0)\n\tfor rows.Next() {\n\t\tvar database, table string\n\t\tif err := rows.Scan(&database, &table); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif database == \"system\" {\n\t\t\tcontinue\n\t\t}\n\t\ttables = append(tables, fmt.Sprintf(\"%s.%s\", database, table))\n\t}\n\treturn tables, nil\n}\n\n// Columns implements [schemas.ColumnsHandler].\nfunc (p *SchemaProvider) Columns(ctx context.Context, req *schemas.ColumnsRequest) (*schemas.ColumnsResponse, error) {\n\tcolumns := make(map[string][]schemas.Column)\n\terrors := make(map[string]string)\n\n\ttables := make([]string, 0, len(req.Tables))\n\tfor _, table := range req.Tables {\n\t\tif table != \"\" {\n\t\t\ttables = append(tables, table)\n\t\t}\n\t}\n\tif len(tables) == 0 {\n\t\treturn &schemas.ColumnsResponse{Columns: columns, Errors: errors}, nil\n\t}\n\n\tcolumnsMap, err := p.cachedFetchColumns(ctx, tables, req.Headers)\n\tif err != nil {\n\t\tfor _, table := range tables {\n\t\t\terrors[table] = err.Error()\n\t\t}\n\t\treturn &schemas.ColumnsResponse{Columns: columns, Errors: errors}, nil\n\t}\n\n\tfor _, table := range tables {\n\t\tcols := columnsMap[table]\n\t\tif len(cols) > 0 {\n\t\t\tcolumns[table] = cols\n\t\t} else {\n\t\t\terrors[table] = \"table not found or has no columns\"\n\t\t}\n\t}\n\n\treturn &schemas.ColumnsResponse{\n\t\tColumns: columns,\n\t\tErrors:  errors,\n\t}, nil\n}\n\nfunc splitTable(table string) (string, string) {\n\tparts := strings.Split(table, \".\")\n\tif len(parts) == 1 {\n\t\treturn \"\", parts[0]\n\t}\n\treturn parts[0], parts[1]\n}\n\n// escapeSQLString escapes single quotes for use in SQL string literals.\nfunc escapeSQLString(s string) string {\n\treturn strings.ReplaceAll(s, \"'\", \"''\")\n}\n\n// quoteIdentifier quotes a ClickHouse identifier with backticks.\nfunc quoteIdentifier(s string) string {\n\treturn \"`\" + strings.ReplaceAll(s, \"`\", \"``\") + \"`\"\n}\n\n// cachedFetchColumns wraps fetchColumnsForAllTables with a per-datasource\n// cache keyed by the sorted table list. The sort is load-bearing: Grafana's\n// query builder sometimes asks for the same tables in a different order\n// (e.g. as the user toggles JOIN targets), and without the sort we'd cache\n// each permutation separately — a high miss rate with no upside since the\n// upstream query is order-independent.\n//\n// Headers intentionally do not participate in the key: they are per-request\n// forwarded auth tokens that do not change the schema ClickHouse sees for\n// the same datasource. If we ever need per-user schema filtering this\n// assumption must be revisited.\nfunc (p *SchemaProvider) cachedFetchColumns(ctx context.Context, tables []string, headers map[string]string) (map[string][]schemas.Column, error) {\n\tif p.columnsCache == nil {\n\t\treturn p.fetchColumnsForAllTables(ctx, tables, headers)\n\t}\n\tsorted := make([]string, len(tables))\n\tcopy(sorted, tables)\n\tsort.Strings(sorted)\n\tkey := strings.Join(sorted, \"|\")\n\treturn p.columnsCache.Do(ctx, key, func(ctx context.Context) (map[string][]schemas.Column, error) {\n\t\treturn p.fetchColumnsForAllTables(ctx, tables, headers)\n\t})\n}\n\n// fetchColumnsForAllTables queries system.columns once for all tables and returns columns keyed by \"database.table\".\nfunc (p *SchemaProvider) fetchColumnsForAllTables(ctx context.Context, tables []string, headers map[string]string) (map[string][]schemas.Column, error) {\n\tresult := make(map[string][]schemas.Column)\n\tif len(tables) == 0 {\n\t\treturn result, nil\n\t}\n\n\tvar inClauses []string\n\tvar tableOnlyNames []string // table names requested without database (e.g. \"myTable\")\n\tfor _, table := range tables {\n\t\tif table == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tdatabase, tableName := splitTable(table)\n\t\tif database != \"\" {\n\t\t\tinClauses = append(inClauses, fmt.Sprintf(\"('%s', '%s')\", escapeSQLString(database), escapeSQLString(tableName)))\n\t\t} else {\n\t\t\ttableOnlyNames = append(tableOnlyNames, tableName)\n\t\t}\n\t}\n\tif len(inClauses) == 0 && len(tableOnlyNames) == 0 {\n\t\treturn result, nil\n\t}\n\n\tvar whereParts []string\n\tif len(inClauses) > 0 {\n\t\twhereParts = append(whereParts, fmt.Sprintf(\"(database, table) IN (%s)\", strings.Join(inClauses, \", \")))\n\t}\n\tif len(tableOnlyNames) > 0 {\n\t\tescaped := make([]string, len(tableOnlyNames))\n\t\tfor i, t := range tableOnlyNames {\n\t\t\tescaped[i] = fmt.Sprintf(\"'%s'\", escapeSQLString(t))\n\t\t}\n\t\twhereParts = append(whereParts, fmt.Sprintf(\"(database = currentDatabase() AND table IN (%s))\", strings.Join(escaped, \", \")))\n\t}\n\trawSQL := fmt.Sprintf(\"SELECT database, table, name, type, comment FROM system.columns WHERE %s ORDER BY database, table, position\",\n\t\tstrings.Join(whereParts, \" OR \"))\n\n\tds, err := p.getDB(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar currentDb string\n\tif len(tableOnlyNames) > 0 {\n\t\tif err := ds.QueryRowContext(ctx, \"SELECT currentDatabase()\").Scan(&currentDb); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\trows, err := ds.QueryContext(ctx, rawSQL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\tif err := rows.Close(); err != nil {\n\t\t\tbackend.Logger.Error(\"failed to close rows\", \"error\", err)\n\t\t}\n\t}()\n\n\tfor rows.Next() {\n\t\tvar database, tableName, name, chType, comment string\n\t\tif err := rows.Scan(&database, &tableName, &name, &chType, &comment); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ttableKey := fmt.Sprintf(\"%s.%s\", database, tableName)\n\t\tcolType, operators := mapClickHouseTypeToSchema(chType)\n\t\tresult[tableKey] = append(result[tableKey], schemas.Column{\n\t\t\tName:        name,\n\t\t\tType:        colType,\n\t\t\tOperators:   operators,\n\t\t\tDescription: comment,\n\t\t})\n\t}\n\t// Add short-form keys for tables requested without database; remove full-form key so we only have \"myTable\", not \"db.myTable\"\n\ttablesSet := make(map[string]bool)\n\tfor _, t := range tables {\n\t\ttablesSet[t] = true\n\t}\n\tfor _, tableName := range tableOnlyNames {\n\t\tpreferredKey := fmt.Sprintf(\"%s.%s\", currentDb, tableName)\n\t\tif cols, ok := result[preferredKey]; ok && len(cols) > 0 {\n\t\t\tresult[tableName] = cols\n\t\t\tif !tablesSet[preferredKey] {\n\t\t\t\tdelete(result, preferredKey)\n\t\t\t}\n\t\t}\n\t}\n\treturn result, nil\n}\n\n// fetchColumnsForTable runs DESCRIBE TABLE for the given table and returns schema columns.\nfunc (p *SchemaProvider) fetchColumnsForTable(ctx context.Context, table string, headers map[string]string) ([]schemas.Column, error) {\n\tdatabase, table := splitTable(table)\n\trawSQL := fmt.Sprintf(\"DESCRIBE TABLE \\\"%s\\\".\\\"%s\\\"\", database, table)\n\tds, err := p.getDB(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trows, err := ds.QueryContext(ctx, rawSQL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\tif err := rows.Close(); err != nil {\n\t\t\tbackend.Logger.Error(\"failed to close rows\", \"error\", err)\n\t\t}\n\t}()\n\tcols := make([]schemas.Column, 0)\n\tfor rows.Next() {\n\t\tvar name string\n\t\tvar chType string\n\t\tvar defaultType string\n\t\tvar defaultExpr string\n\t\tvar comment string\n\t\tvar codec_expr string\n\t\tvar ttl_expr string\n\t\terr := rows.Scan(&name, &chType, &defaultType, &defaultExpr, &comment, &codec_expr, &ttl_expr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcolType, operators := mapClickHouseTypeToSchema(chType)\n\t\tcols = append(cols, schemas.Column{\n\t\t\tName:        name,\n\t\t\tType:        colType,\n\t\t\tOperators:   operators,\n\t\t\tDescription: comment,\n\t\t})\n\t}\n\treturn cols, nil\n}\n\n// mapClickHouseTypeToSchema maps ClickHouse types to schemads ColumnType.\nfunc mapClickHouseTypeToSchema(chType string) (schemas.ColumnType, []schemas.Operator) {\n\ttrimmed := strings.TrimSpace(chType)\n\tvar baseType, innerType string\n\tif idx := strings.Index(trimmed, \"(\"); idx >= 0 {\n\t\tbaseType = strings.ToLower(trimmed[:idx])\n\t\t// Extract the inner argument (everything between the first \"(\" and last \")\")\n\t\tinner := trimmed[idx+1:]\n\t\tif strings.HasSuffix(inner, \")\") {\n\t\t\tinnerType = inner[:len(inner)-1]\n\t\t} else {\n\t\t\tinnerType = inner\n\t\t}\n\t} else {\n\t\tbaseType = strings.ToLower(trimmed)\n\t}\n\n\tswitch baseType {\n\tcase \"int8\":\n\t\treturn schemas.ColumnTypeInt8, numberOperators\n\tcase \"int16\":\n\t\treturn schemas.ColumnTypeInt16, numberOperators\n\tcase \"int32\":\n\t\treturn schemas.ColumnTypeInt32, numberOperators\n\tcase \"int64\":\n\t\treturn schemas.ColumnTypeInt64, numberOperators\n\tcase \"uint8\":\n\t\treturn schemas.ColumnTypeUint8, numberOperators\n\tcase \"uint16\":\n\t\treturn schemas.ColumnTypeUint16, numberOperators\n\tcase \"uint32\", \"ipv4\":\n\t\treturn schemas.ColumnTypeUint32, numberOperators\n\tcase \"uint64\":\n\t\treturn schemas.ColumnTypeUint64, numberOperators\n\tcase \"float32\":\n\t\treturn schemas.ColumnTypeFloat32, numberOperators\n\tcase \"float64\", \"int128\", \"int256\", \"uint128\", \"uint256\":\n\t\treturn schemas.ColumnTypeFloat64, numberOperators\n\tcase \"bool\":\n\t\treturn schemas.ColumnTypeBoolean, equalityOperators\n\tcase \"date\", \"date32\":\n\t\treturn schemas.ColumnTypeDate, timeRangeOperators\n\tcase \"datetime\", \"datetime64\":\n\t\treturn schemas.ColumnTypeDatetime, timeRangeOperators\n\tcase \"timestamp\":\n\t\treturn schemas.ColumnTypeTimestamp, timeRangeOperators\n\tcase \"string\", \"fixedstring\":\n\t\treturn schemas.ColumnTypeString, stringOperators\n\tcase \"ipv6\", \"uuid\":\n\t\treturn schemas.ColumnTypeString, equalityOperators\n\tcase \"decimal\", \"decimal32\", \"decimal64\", \"decimal128\", \"decimal256\":\n\t\treturn schemas.ColumnTypeDecimal, numberOperators\n\tcase \"enum\", \"enum8\", \"enum16\":\n\t\treturn schemas.ColumnTypeEnum, numberOperators\n\tcase \"json\", \"dynamic\", \"array\", \"map\", \"tuple\", \"variant\", \"nested\":\n\t\treturn schemas.ColumnTypeJSON, equalityOperators\n\tcase \"nullable\", \"lowcardinality\":\n\t\t// Nullable(X) and LowCardinality(X) are wrappers; the logical type is the inner type\n\t\tif innerType != \"\" {\n\t\t\treturn mapClickHouseTypeToSchema(innerType)\n\t\t}\n\tdefault:\n\t\tbackend.Logger.Error(\"mapClickHouseTypeToSchema\", \"unknown type\", chType)\n\t}\n\treturn schemas.ColumnTypeJSON, equalityOperators\n}\n\n// ColumnValues implements [schemas.ColumnValuesHandler].\n//\n// Caching rationale: the response is DISTINCT user data from the target\n// table, so a short TTL (60s) trades at most one TTL-window of staleness\n// for filter dropdowns against what is typically the heaviest of the three\n// schema queries (a DISTINCT scan of potentially many columns). If the user\n// wants fresh values they hit \"refresh\". The cache is keyed on\n// (table, sorted columns) so a caller asking for columns A,B gets the same\n// entry as one asking for B,A.\nfunc (p *SchemaProvider) ColumnValues(ctx context.Context, req *schemas.ColumnValuesRequest) (*schemas.ColumnValuesResponse, error) {\n\tif p.valuesCache == nil {\n\t\tvalues, err := p.fetchColumnValues(ctx, req.Table, req.Columns)\n\t\tif err != nil {\n\t\t\treturn &schemas.ColumnValuesResponse{Errors: map[string]string{req.Table: err.Error()}}, nil\n\t\t}\n\t\treturn &schemas.ColumnValuesResponse{ColumnValues: values}, nil\n\t}\n\n\tsortedCols := make([]string, len(req.Columns))\n\tcopy(sortedCols, req.Columns)\n\tsort.Strings(sortedCols)\n\tkey := req.Table + \"::\" + strings.Join(sortedCols, \"|\")\n\n\tvalues, err := p.valuesCache.Do(ctx, key, func(ctx context.Context) (map[string][]string, error) {\n\t\treturn p.fetchColumnValues(ctx, req.Table, req.Columns)\n\t})\n\tif err != nil {\n\t\treturn &schemas.ColumnValuesResponse{Errors: map[string]string{req.Table: err.Error()}}, nil\n\t}\n\treturn &schemas.ColumnValuesResponse{ColumnValues: values}, nil\n}\n\n// fetchColumnValues runs the DISTINCT-per-column UNION ALL probe. It is the\n// function wrapped by the cache in [SchemaProvider.ColumnValues].\nfunc (p *SchemaProvider) fetchColumnValues(ctx context.Context, table string, requestedColumns []string) (map[string][]string, error) {\n\tds, err := p.getDB(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcolumns := requestedColumns\n\tif len(columns) == 0 {\n\t\tcols, err := p.fetchColumnsForTable(ctx, table, nil)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcolumns = make([]string, len(cols))\n\t\tfor i, col := range cols {\n\t\t\tcolumns[i] = col.Name\n\t\t}\n\t}\n\n\tvalues := make(map[string][]string)\n\tif len(columns) == 0 {\n\t\treturn values, nil\n\t}\n\n\t// Build a single UNION ALL query so all columns are fetched in one round-trip\n\t// instead of one query per column.\n\tparts := make([]string, len(columns))\n\tfor i, col := range columns {\n\t\tparts[i] = fmt.Sprintf(\n\t\t\t\"SELECT '%s' AS col_name, toString(%s) AS val FROM (SELECT DISTINCT %s FROM %s SETTINGS max_execution_time=10)\",\n\t\t\tescapeSQLString(col), quoteIdentifier(col), quoteIdentifier(col), table,\n\t\t)\n\t\tvalues[col] = make([]string, 0)\n\t}\n\tquery := strings.Join(parts, \" UNION ALL \")\n\n\trows, err := ds.QueryContext(ctx, query)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\tif err := rows.Close(); err != nil {\n\t\t\tbackend.Logger.Error(\"failed to close rows\", \"error\", err)\n\t\t}\n\t}()\n\n\tfor rows.Next() {\n\t\tvar colName, value string\n\t\tif err := rows.Scan(&colName, &value); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvalues[colName] = append(values[colName], value)\n\t}\n\treturn values, nil\n}\n\n// NewSchemaProvider builds a SchemaProvider and, if schema caching is enabled\n// in the datasource settings, initializes per-handler TTL caches. Settings\n// parsing failures degrade to a cache-disabled provider so the query builder\n// still works — we never want schema introspection to be gated on cache\n// availability.\nfunc NewSchemaProvider(ctx context.Context, clickhousePlugin *Clickhouse, settings backend.DataSourceInstanceSettings) *SchemaProvider {\n\tp := &SchemaProvider{clickhousePlugin: clickhousePlugin, settings: settings}\n\n\tparsed, err := LoadSettings(ctx, settings)\n\tif err != nil || !parsed.EnableSchemaCache {\n\t\treturn p\n\t}\n\n\tttl := time.Duration(parsed.SchemaCacheTTLSeconds) * time.Second\n\t// ±5% jitter to avoid stampedes when many entries are populated in a\n\t// burst (typical on first dashboard load). Width of the uniform band,\n\t// not the half-width — see schemacache.New godoc.\n\tjitter := ttl / 10\n\n\t// Tables: one entry ever (the full \"all tables\" list). Use max=1 to\n\t// make that explicit and keep the map tiny.\n\tp.tablesCache = schemacache.New[[]string](ttl, jitter, 1)\n\t// Columns and values: the key is the requested table set, so there's\n\t// one entry per distinct request shape. 256 is generous for typical\n\t// dashboards; the bound matters mainly for ad-hoc filter use.\n\tp.columnsCache = schemacache.New[map[string][]schemas.Column](ttl, jitter, 256)\n\tp.valuesCache = schemacache.New[map[string][]string](ttl, jitter, 256)\n\n\treturn p\n}\n"
  },
  {
    "path": "pkg/plugin/schema_test.go",
    "content": "package plugin\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"database/sql/driver\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\n\t\"github.com/grafana/grafana-plugin-sdk-go/backend\"\n)\n\n// trackingConnector hands out *sql.DBs and records them so tests can assert\n// reuse (one DB across calls) and Close behavior at end-of-life.\ntype trackingConnector struct {\n\tmu       sync.Mutex\n\topened   []*sql.DB\n\tconnErr  error                                   // injected error from Connect\n\trowsFn   func(query string) (driver.Rows, error) // injected Query handler\n\tconnects atomic.Int32\n}\n\nfunc (t *trackingConnector) Connect(_ context.Context, _ backend.DataSourceInstanceSettings, _ json.RawMessage) (*sql.DB, error) {\n\tt.connects.Add(1)\n\tif t.connErr != nil {\n\t\treturn nil, t.connErr\n\t}\n\tdb := sql.OpenDB(&fakeDriverConnector{rowsFn: t.rowsFn})\n\tt.mu.Lock()\n\tt.opened = append(t.opened, db)\n\tt.mu.Unlock()\n\treturn db, nil\n}\n\n// allClosed: zero open conns on every handed-out pool ⇒ DB.Close() ran.\nfunc (t *trackingConnector) allClosed() bool {\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\tfor _, db := range t.opened {\n\t\tif db.Stats().OpenConnections != 0 {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (t *trackingConnector) openedCount() int {\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\treturn len(t.opened)\n}\n\n// Minimal driver/connector implementing only what schema.go touches.\n\ntype fakeDriverConnector struct {\n\trowsFn func(query string) (driver.Rows, error)\n}\n\nfunc (c *fakeDriverConnector) Connect(context.Context) (driver.Conn, error) {\n\treturn &fakeConn{rowsFn: c.rowsFn}, nil\n}\nfunc (c *fakeDriverConnector) Driver() driver.Driver { return fakeDriver{} }\n\ntype fakeDriver struct{}\n\nfunc (fakeDriver) Open(string) (driver.Conn, error) { return nil, errors.New(\"unused\") }\n\ntype fakeConn struct {\n\trowsFn func(query string) (driver.Rows, error)\n}\n\nfunc (c *fakeConn) Prepare(string) (driver.Stmt, error) { return nil, errors.New(\"not implemented\") }\nfunc (c *fakeConn) Close() error                        { return nil }\nfunc (c *fakeConn) Begin() (driver.Tx, error)           { return nil, errors.New(\"not implemented\") }\n\n// QueryerContext lets database/sql skip Prepare.\nfunc (c *fakeConn) QueryContext(_ context.Context, query string, _ []driver.NamedValue) (driver.Rows, error) {\n\treturn c.rowsFn(query)\n}\n\ntype fakeRows struct {\n\tcols []string\n\tdata [][]driver.Value\n\tidx  int\n}\n\nfunc (r *fakeRows) Columns() []string { return r.cols }\nfunc (r *fakeRows) Close() error      { return nil }\nfunc (r *fakeRows) Next(dest []driver.Value) error {\n\tif r.idx >= len(r.data) {\n\t\treturn io.EOF\n\t}\n\trow := r.data[r.idx]\n\tfor i := range dest {\n\t\tdest[i] = row[i]\n\t}\n\tr.idx++\n\treturn nil\n}\n\nfunc newProvider(t *testing.T, conn *trackingConnector) *SchemaProvider {\n\tt.Helper()\n\treturn &SchemaProvider{\n\t\tclickhousePlugin: conn,\n\t\tsettings:         backend.DataSourceInstanceSettings{},\n\t}\n}\n\nfunc tablesRows() (driver.Rows, error) {\n\treturn &fakeRows{\n\t\tcols: []string{\"database\", \"name\"},\n\t\tdata: [][]driver.Value{\n\t\t\t{\"default\", \"users\"},\n\t\t\t{\"default\", \"events\"},\n\t\t\t{\"system\", \"tables\"}, // skipped by schema.go\n\t\t},\n\t}, nil\n}\n\nfunc columnsRows() (driver.Rows, error) {\n\treturn &fakeRows{\n\t\tcols: []string{\"database\", \"table\", \"name\", \"type\", \"comment\"},\n\t\tdata: [][]driver.Value{\n\t\t\t{\"default\", \"users\", \"id\", \"UInt64\", \"\"},\n\t\t\t{\"default\", \"users\", \"name\", \"String\", \"user display name\"},\n\t\t},\n\t}, nil\n}\n\nfunc describeRows() (driver.Rows, error) {\n\treturn &fakeRows{\n\t\tcols: []string{\"name\", \"type\", \"default_type\", \"default_expression\", \"comment\", \"codec_expression\", \"ttl_expression\"},\n\t\tdata: [][]driver.Value{\n\t\t\t{\"id\", \"UInt64\", \"\", \"\", \"\", \"\", \"\"},\n\t\t\t{\"name\", \"String\", \"\", \"\", \"\", \"\", \"\"},\n\t\t},\n\t}, nil\n}\n\n// --- success paths still return correct data ---\n\nfunc TestFetchTables_Success(t *testing.T) {\n\tconn := &trackingConnector{rowsFn: func(string) (driver.Rows, error) { return tablesRows() }}\n\tp := newProvider(t, conn)\n\n\ttables, err := p.fetchTables(context.Background())\n\tif err != nil {\n\t\tt.Fatalf(\"fetchTables: %v\", err)\n\t}\n\tif want := []string{\"default.users\", \"default.events\"}; !equalStrings(tables, want) {\n\t\tt.Errorf(\"tables = %v, want %v\", tables, want)\n\t}\n}\n\nfunc TestFetchColumnsForAllTables_Success(t *testing.T) {\n\tconn := &trackingConnector{rowsFn: func(string) (driver.Rows, error) { return columnsRows() }}\n\tp := newProvider(t, conn)\n\n\tcols, err := p.fetchColumnsForAllTables(context.Background(), []string{\"default.users\"}, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"fetchColumnsForAllTables: %v\", err)\n\t}\n\tif got := len(cols[\"default.users\"]); got != 2 {\n\t\tt.Errorf(\"default.users columns = %d, want 2\", got)\n\t}\n}\n\nfunc TestFetchColumnsForTable_Success(t *testing.T) {\n\tconn := &trackingConnector{rowsFn: func(string) (driver.Rows, error) { return describeRows() }}\n\tp := newProvider(t, conn)\n\n\tcols, err := p.fetchColumnsForTable(context.Background(), \"default.users\", nil)\n\tif err != nil {\n\t\tt.Fatalf(\"fetchColumnsForTable: %v\", err)\n\t}\n\tif len(cols) != 2 {\n\t\tt.Errorf(\"columns = %d, want 2\", len(cols))\n\t}\n}\n\n// --- query-error paths still surface the error and don't cache it ---\n\nfunc TestFetchTables_QueryErrorIsReturned(t *testing.T) {\n\tconn := &trackingConnector{rowsFn: func(string) (driver.Rows, error) {\n\t\treturn nil, errors.New(\"query failed\")\n\t}}\n\tp := newProvider(t, conn)\n\n\tif _, err := p.fetchTables(context.Background()); err == nil {\n\t\tt.Fatal(\"fetchTables: expected error, got nil\")\n\t}\n}\n\nfunc TestFetchColumnsForAllTables_QueryErrorIsReturned(t *testing.T) {\n\tconn := &trackingConnector{rowsFn: func(string) (driver.Rows, error) {\n\t\treturn nil, errors.New(\"query failed\")\n\t}}\n\tp := newProvider(t, conn)\n\n\tif _, err := p.fetchColumnsForAllTables(context.Background(), []string{\"default.users\"}, nil); err == nil {\n\t\tt.Fatal(\"expected error, got nil\")\n\t}\n}\n\nfunc TestFetchColumnsForTable_QueryErrorIsReturned(t *testing.T) {\n\tconn := &trackingConnector{rowsFn: func(string) (driver.Rows, error) {\n\t\treturn nil, errors.New(\"query failed\")\n\t}}\n\tp := newProvider(t, conn)\n\n\tif _, err := p.fetchColumnsForTable(context.Background(), \"default.users\", nil); err == nil {\n\t\tt.Fatal(\"expected error, got nil\")\n\t}\n}\n\n// --- held-DB lifecycle ---\n\nfunc TestGetDB_ReusedAcrossCalls(t *testing.T) {\n\tconn := &trackingConnector{rowsFn: func(query string) (driver.Rows, error) {\n\t\tswitch {\n\t\tcase containsAny(query, \"system.tables\"):\n\t\t\treturn tablesRows()\n\t\tcase containsAny(query, \"system.columns\"):\n\t\t\treturn columnsRows()\n\t\tcase containsAny(query, \"DESCRIBE\"):\n\t\t\treturn describeRows()\n\t\t}\n\t\treturn &fakeRows{cols: []string{\"x\"}, data: [][]driver.Value{{int64(1)}}}, nil\n\t}}\n\tp := newProvider(t, conn)\n\n\tif _, err := p.fetchTables(context.Background()); err != nil {\n\t\tt.Fatalf(\"fetchTables: %v\", err)\n\t}\n\tif _, err := p.fetchColumnsForAllTables(context.Background(), []string{\"default.users\"}, nil); err != nil {\n\t\tt.Fatalf(\"fetchColumnsForAllTables: %v\", err)\n\t}\n\tif _, err := p.fetchColumnsForTable(context.Background(), \"default.users\", nil); err != nil {\n\t\tt.Fatalf(\"fetchColumnsForTable: %v\", err)\n\t}\n\n\tif got := conn.openedCount(); got != 1 {\n\t\tt.Errorf(\"opened DBs = %d, want 1 (held DB should be reused)\", got)\n\t}\n\tif got := conn.connects.Load(); got != 1 {\n\t\tt.Errorf(\"Connect calls = %d, want 1\", got)\n\t}\n}\n\nfunc TestGetDB_ConnectErrorNotCached(t *testing.T) {\n\trows := func(string) (driver.Rows, error) { return tablesRows() }\n\tconn := &trackingConnector{rowsFn: rows, connErr: errors.New(\"transient\")}\n\tp := newProvider(t, conn)\n\n\t// First call fails because Connect errors.\n\tif _, err := p.fetchTables(context.Background()); err == nil {\n\t\tt.Fatal(\"expected connect error, got nil\")\n\t}\n\n\t// Recover from the transient error and retry; the provider must call\n\t// Connect again (no sticky cached error).\n\tconn.connErr = nil\n\tif _, err := p.fetchTables(context.Background()); err != nil {\n\t\tt.Fatalf(\"retry: %v\", err)\n\t}\n\tif got := conn.connects.Load(); got != 2 {\n\t\tt.Errorf(\"Connect calls = %d, want 2 (one failed, one succeeded)\", got)\n\t}\n\tif got := conn.openedCount(); got != 1 {\n\t\tt.Errorf(\"opened DBs = %d, want 1\", got)\n\t}\n}\n\nfunc TestClose_ClosesHeldDBAndIsIdempotent(t *testing.T) {\n\tconn := &trackingConnector{rowsFn: func(string) (driver.Rows, error) { return tablesRows() }}\n\tp := newProvider(t, conn)\n\n\tif _, err := p.fetchTables(context.Background()); err != nil {\n\t\tt.Fatalf(\"fetchTables: %v\", err)\n\t}\n\tif conn.allClosed() {\n\t\tt.Fatal(\"DB closed before Close was called; held-DB invariant broken\")\n\t}\n\n\tif err := p.Close(); err != nil {\n\t\tt.Fatalf(\"Close: %v\", err)\n\t}\n\tif !conn.allClosed() {\n\t\tt.Error(\"Close did not close the held DB\")\n\t}\n\n\t// Idempotent: a second Close on a provider with no held DB is a no-op.\n\tif err := p.Close(); err != nil {\n\t\tt.Errorf(\"second Close: %v\", err)\n\t}\n}\n\nfunc TestClose_WithoutAnyFetchIsNoOp(t *testing.T) {\n\tconn := &trackingConnector{rowsFn: func(string) (driver.Rows, error) { return tablesRows() }}\n\tp := newProvider(t, conn)\n\n\tif err := p.Close(); err != nil {\n\t\tt.Errorf(\"Close on never-used provider: %v\", err)\n\t}\n\tif got := conn.openedCount(); got != 0 {\n\t\tt.Errorf(\"opened DBs = %d, want 0\", got)\n\t}\n}\n\n// TestGetDB_ConcurrentFirstUse asserts that under simultaneous first-call\n// pressure the provider builds exactly one DB. Run with -race.\nfunc TestGetDB_ConcurrentFirstUse(t *testing.T) {\n\tconn := &trackingConnector{rowsFn: func(string) (driver.Rows, error) { return tablesRows() }}\n\tp := newProvider(t, conn)\n\n\tconst goroutines = 32\n\tvar ready, start sync.WaitGroup\n\tready.Add(goroutines)\n\tstart.Add(1)\n\terrs := make(chan error, goroutines)\n\tfor range goroutines {\n\t\tgo func() {\n\t\t\tready.Done()\n\t\t\tstart.Wait()\n\t\t\t_, err := p.fetchTables(context.Background())\n\t\t\terrs <- err\n\t\t}()\n\t}\n\tready.Wait()\n\tstart.Done()\n\tfor range goroutines {\n\t\tif err := <-errs; err != nil {\n\t\t\tt.Errorf(\"fetchTables: %v\", err)\n\t\t}\n\t}\n\n\tif got := conn.openedCount(); got != 1 {\n\t\tt.Errorf(\"opened DBs = %d under concurrent first-use, want 1\", got)\n\t}\n}\n\n// Run with -race; catches any sync issue around the held-DB and Close paths.\nfunc TestSchema_Concurrent_NoRace(t *testing.T) {\n\tconn := &trackingConnector{rowsFn: func(query string) (driver.Rows, error) {\n\t\tswitch {\n\t\tcase containsAny(query, \"system.tables\"):\n\t\t\treturn tablesRows()\n\t\tcase containsAny(query, \"system.columns\"):\n\t\t\treturn columnsRows()\n\t\tcase containsAny(query, \"DESCRIBE\"):\n\t\t\treturn describeRows()\n\t\t}\n\t\treturn &fakeRows{cols: []string{\"x\"}, data: [][]driver.Value{{int64(1)}}}, nil\n\t}}\n\tp := newProvider(t, conn)\n\n\tconst goroutines = 16\n\tconst iterations = 25\n\tvar wg sync.WaitGroup\n\twg.Add(goroutines)\n\tfor range goroutines {\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tfor range iterations {\n\t\t\t\tif _, err := p.fetchTables(context.Background()); err != nil {\n\t\t\t\t\tt.Errorf(\"fetchTables: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif _, err := p.fetchColumnsForAllTables(context.Background(), []string{\"default.users\"}, nil); err != nil {\n\t\t\t\t\tt.Errorf(\"fetchColumnsForAllTables: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif _, err := p.fetchColumnsForTable(context.Background(), \"default.users\", nil); err != nil {\n\t\t\t\t\tt.Errorf(\"fetchColumnsForTable: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n\twg.Wait()\n\n\tif got := conn.openedCount(); got != 1 {\n\t\tt.Errorf(\"after concurrent run, opened DBs = %d, want 1 (held DB reused)\", got)\n\t}\n\n\tif err := p.Close(); err != nil {\n\t\tt.Errorf(\"Close: %v\", err)\n\t}\n\tif !conn.allClosed() {\n\t\tt.Errorf(\"after Close, %d DBs still hold connections\", leakedCount(conn))\n\t}\n}\n\nfunc leakedCount(c *trackingConnector) int {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\tleaked := 0\n\tfor _, db := range c.opened {\n\t\tif db.Stats().OpenConnections != 0 {\n\t\t\tleaked++\n\t\t}\n\t}\n\treturn leaked\n}\n\nfunc containsAny(s string, sub string) bool {\n\treturn len(s) >= len(sub) && indexOf(s, sub) >= 0\n}\n\nfunc indexOf(s, sub string) int {\n\tfor i := 0; i+len(sub) <= len(s); i++ {\n\t\tif s[i:i+len(sub)] == sub {\n\t\t\treturn i\n\t\t}\n\t}\n\treturn -1\n}\n\nfunc equalStrings(a, b []string) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tif a[i] != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "pkg/plugin/schemacache/cache.go",
    "content": "// Package schemacache is a TTL + singleflight cache for ClickHouse schema\n// introspection queries (system.tables, system.columns, DISTINCT column probes).\n//\n// One cache lives per SchemaProvider, so entries never cross datasource or\n// tenant boundaries.\npackage schemacache\n\nimport (\n\t\"context\"\n\t\"math/rand/v2\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\t\"golang.org/x/sync/singleflight\"\n)\n\nconst DefaultMaxItems = 256\n\n// Cache is a TTL-bounded in-process cache with singleflight miss dedup.\n// Safe for concurrent use.\ntype Cache[V any] struct {\n\tttl      time.Duration\n\tjitter   time.Duration\n\tmaxItems int\n\n\tmu      sync.Mutex\n\tentries map[string]entry[V]\n\tsf      singleflight.Group\n\n\tnow func() time.Time // test seam\n}\n\ntype entry[V any] struct {\n\tvalue   V\n\texpires time.Time\n}\n\n// New returns a cache with the given TTL, expiry jitter (±jitter/2 around ttl\n// per Set, to avoid stampedes), and max item count. maxItems <= 0 uses\n// DefaultMaxItems.\nfunc New[V any](ttl, jitter time.Duration, maxItems int) *Cache[V] {\n\tif maxItems <= 0 {\n\t\tmaxItems = DefaultMaxItems\n\t}\n\treturn &Cache[V]{\n\t\tttl:      ttl,\n\t\tjitter:   jitter,\n\t\tmaxItems: maxItems,\n\t\tentries:  make(map[string]entry[V]),\n\t\tnow:      time.Now,\n\t}\n}\n\n// Get returns the cached value if present and unexpired. Expired entries are\n// dropped on access.\nfunc (c *Cache[V]) Get(key string) (V, bool) {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\treturn c.getLocked(key)\n}\n\nfunc (c *Cache[V]) getLocked(key string) (V, bool) {\n\tvar zero V\n\te, ok := c.entries[key]\n\tif !ok {\n\t\treturn zero, false\n\t}\n\tif c.now().After(e.expires) {\n\t\tdelete(c.entries, key)\n\t\treturn zero, false\n\t}\n\treturn e.value, true\n}\n\n// Set stores value under key with the cache's TTL (±jitter).\nfunc (c *Cache[V]) Set(key string, value V) {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\tc.entries[key] = entry[V]{\n\t\tvalue:   value,\n\t\texpires: c.now().Add(c.ttlWithJitter()),\n\t}\n\tc.evictIfNeededLocked()\n}\n\n// Delete removes key from the cache.\nfunc (c *Cache[V]) Delete(key string) {\n\tc.mu.Lock()\n\tdelete(c.entries, key)\n\tc.mu.Unlock()\n}\n\n// Clear drops all entries.\nfunc (c *Cache[V]) Clear() {\n\tc.mu.Lock()\n\tc.entries = make(map[string]entry[V])\n\tc.mu.Unlock()\n}\n\n// Len returns the entry count, including expired-but-not-yet-evicted entries.\nfunc (c *Cache[V]) Len() int {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\treturn len(c.entries)\n}\n\n// Do returns the cached value for key if present; otherwise calls fn and\n// caches its result. Concurrent misses on the same key collapse into one fn\n// invocation via singleflight. Errors are not cached.\nfunc (c *Cache[V]) Do(ctx context.Context, key string, fn func(context.Context) (V, error)) (V, error) {\n\tif v, ok := c.Get(key); ok {\n\t\treturn v, nil\n\t}\n\tresult, err, _ := c.sf.Do(key, func() (any, error) {\n\t\t// Recheck under singleflight: another caller may have populated it.\n\t\tif v, ok := c.Get(key); ok {\n\t\t\treturn v, nil\n\t\t}\n\t\t// Detach from the leader's ctx — singleflight broadcasts fn's error\n\t\t// to every waiter, so leader cancellation would poison them too.\n\t\tv, err := fn(context.WithoutCancel(ctx))\n\t\tif err != nil {\n\t\t\treturn v, err\n\t\t}\n\t\tc.Set(key, v)\n\t\treturn v, nil\n\t})\n\tif err != nil {\n\t\tvar zero V\n\t\treturn zero, err\n\t}\n\treturn result.(V), nil\n}\n\nfunc (c *Cache[V]) ttlWithJitter() time.Duration {\n\tif c.jitter <= 0 {\n\t\treturn c.ttl\n\t}\n\tdelta := time.Duration(rand.Int64N(int64(c.jitter))) - c.jitter/2\n\treturn c.ttl + delta\n}\n\n// evictIfNeededLocked keeps len <= maxItems by dropping expired entries first,\n// then earliest-expiring entries (cheap LRU approximation). Caller holds c.mu.\nfunc (c *Cache[V]) evictIfNeededLocked() {\n\tif len(c.entries) <= c.maxItems {\n\t\treturn\n\t}\n\tnow := c.now()\n\tfor k, e := range c.entries {\n\t\tif now.After(e.expires) {\n\t\t\tdelete(c.entries, k)\n\t\t}\n\t}\n\tif len(c.entries) <= c.maxItems {\n\t\treturn\n\t}\n\ttype kv struct {\n\t\tkey     string\n\t\texpires time.Time\n\t}\n\titems := make([]kv, 0, len(c.entries))\n\tfor k, e := range c.entries {\n\t\titems = append(items, kv{k, e.expires})\n\t}\n\tsort.Slice(items, func(i, j int) bool { return items[i].expires.Before(items[j].expires) })\n\toverflow := len(c.entries) - c.maxItems\n\tfor i := 0; i < overflow && i < len(items); i++ {\n\t\tdelete(c.entries, items[i].key)\n\t}\n}\n"
  },
  {
    "path": "pkg/plugin/schemacache/cache_bench_test.go",
    "content": "package schemacache\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n)\n\n// simulatedRoundTrip mimics the latency profile of a ClickHouse system.tables\n// query as seen from a MT api-server Pod. 2ms is roughly the observed floor\n// for intra-cluster queries (read_rows in the low thousands, no scan). The\n// test doesn't care about the exact number — it just needs something large\n// enough that the cache path is measurably cheaper than the upstream path,\n// while being small enough that benchmarks finish in a reasonable time.\nconst simulatedRoundTrip = 2 * time.Millisecond\n\nvar sinkStrings []string\n\nfunc fetchSimulated(ctx context.Context) ([]string, error) {\n\ttime.Sleep(simulatedRoundTrip)\n\t// Return a non-trivial payload so the benchmark reflects real allocation\n\t// patterns (short-form hit shouldn't be free purely because V is a zero).\n\treturn []string{\"default.events\", \"default.logs\", \"default.traces\"}, nil\n}\n\n// BenchmarkTablesFetch_Uncached measures the cost of calling fetchSimulated\n// directly — the baseline before the cache. Every call pays simulatedRoundTrip.\nfunc BenchmarkTablesFetch_Uncached(b *testing.B) {\n\tctx := context.Background()\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tv, err := fetchSimulated(ctx)\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t\tsinkStrings = v\n\t}\n}\n\n// BenchmarkTablesFetch_Cached measures the same call path wrapped in the\n// cache. The first iteration incurs simulatedRoundTrip; all subsequent\n// iterations should be cache hits — so ns/op is expected to drop by ~3 orders\n// of magnitude (from ~2ms to a few hundred ns) and allocs/op to near zero.\nfunc BenchmarkTablesFetch_Cached(b *testing.B) {\n\tctx := context.Background()\n\tc := New[[]string](time.Hour, 0, 4)\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tv, err := c.Do(ctx, \"all\", fetchSimulated)\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t\tsinkStrings = v\n\t}\n}\n\n// BenchmarkTablesFetch_CachedConcurrent is the shape that actually motivated\n// the cache: a 20-panel dashboard opens and fires 20 parallel schema resource\n// calls. Without singleflight every cold caller would pay simulatedRoundTrip\n// independently; with it, exactly one does and the other 19 block on the\n// shared result.\nfunc BenchmarkTablesFetch_CachedConcurrent(b *testing.B) {\n\tctx := context.Background()\n\tconst panels = 20\n\n\tb.ReportAllocs()\n\ti := 0\n\tfor b.Loop() {\n\t\t// Fresh cache per outer iteration so every iteration pays the singleflight\n\t\t// cost and we're not just measuring hot-cache hits. The singleflight\n\t\t// behavior — collapsing concurrent misses — is the thing under test.\n\t\tc := New[[]string](time.Hour, 0, 4)\n\t\tvar wg sync.WaitGroup\n\t\twg.Add(panels)\n\t\titer := i\n\t\tfor range panels {\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tv, err := c.Do(ctx, fmt.Sprintf(\"iter-%d\", iter), fetchSimulated)\n\t\t\t\tif err != nil {\n\t\t\t\t\tb.Error(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tsinkStrings = v\n\t\t\t}()\n\t\t}\n\t\twg.Wait()\n\t\ti++\n\t}\n}\n"
  },
  {
    "path": "pkg/plugin/schemacache/cache_test.go",
    "content": "package schemacache\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestCache_GetSet(t *testing.T) {\n\tc := New[string](time.Minute, 0, 10)\n\n\tif _, ok := c.Get(\"missing\"); ok {\n\t\tt.Fatal(\"expected miss on empty cache\")\n\t}\n\n\tc.Set(\"k\", \"v\")\n\tgot, ok := c.Get(\"k\")\n\tif !ok || got != \"v\" {\n\t\tt.Fatalf(\"expected (v,true), got (%q,%v)\", got, ok)\n\t}\n}\n\nfunc TestCache_TTLExpiry(t *testing.T) {\n\t// Use manual clock to keep the test fast and deterministic.\n\tnow := time.Unix(0, 0)\n\tc := New[string](100*time.Millisecond, 0, 10)\n\tc.now = func() time.Time { return now }\n\n\tc.Set(\"k\", \"v\")\n\n\t// Just before expiry.\n\tnow = now.Add(99 * time.Millisecond)\n\tif _, ok := c.Get(\"k\"); !ok {\n\t\tt.Fatal(\"entry should still be valid just before TTL\")\n\t}\n\n\t// After expiry.\n\tnow = now.Add(2 * time.Millisecond)\n\tif _, ok := c.Get(\"k\"); ok {\n\t\tt.Fatal(\"entry should be expired\")\n\t}\n\tif c.Len() != 0 {\n\t\tt.Fatalf(\"expired entry should be evicted on access, Len()=%d\", c.Len())\n\t}\n}\n\nfunc TestCache_Delete(t *testing.T) {\n\tc := New[int](time.Minute, 0, 10)\n\tc.Set(\"k\", 1)\n\tc.Delete(\"k\")\n\tif _, ok := c.Get(\"k\"); ok {\n\t\tt.Fatal(\"entry should have been deleted\")\n\t}\n}\n\nfunc TestCache_Clear(t *testing.T) {\n\tc := New[int](time.Minute, 0, 10)\n\tc.Set(\"a\", 1)\n\tc.Set(\"b\", 2)\n\tc.Clear()\n\tif c.Len() != 0 {\n\t\tt.Fatalf(\"Clear() should empty the cache, Len()=%d\", c.Len())\n\t}\n}\n\nfunc TestCache_MaxItemsEviction(t *testing.T) {\n\tnow := time.Unix(0, 0)\n\tc := New[int](time.Minute, 0, 3)\n\tc.now = func() time.Time { return now }\n\n\t// Fill beyond capacity with strictly-increasing expiry times so the\n\t// earliest-expiring entry is predictable.\n\tfor i := range 5 {\n\t\tc.Set(fmt.Sprintf(\"k%d\", i), i)\n\t\tnow = now.Add(time.Millisecond) // pushes each subsequent expiry further out\n\t}\n\n\tif got := c.Len(); got > 3 {\n\t\tt.Fatalf(\"expected cache to be bounded at 3, Len()=%d\", got)\n\t}\n\t// The first two keys should have been evicted (earliest expiries).\n\tfor _, k := range []string{\"k0\", \"k1\"} {\n\t\tif _, ok := c.Get(k); ok {\n\t\t\tt.Fatalf(\"key %q should have been evicted\", k)\n\t\t}\n\t}\n}\n\nfunc TestCache_DoReturnsCachedValue(t *testing.T) {\n\tc := New[string](time.Minute, 0, 10)\n\tc.Set(\"k\", \"cached\")\n\n\tcalled := false\n\tgot, err := c.Do(context.Background(), \"k\", func(ctx context.Context) (string, error) {\n\t\tcalled = true\n\t\treturn \"fresh\", nil\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif got != \"cached\" {\n\t\tt.Fatalf(\"expected cached value, got %q\", got)\n\t}\n\tif called {\n\t\tt.Fatal(\"fn should not be invoked on cache hit\")\n\t}\n}\n\nfunc TestCache_DoCachesResult(t *testing.T) {\n\tc := New[int](time.Minute, 0, 10)\n\n\tcalls := 0\n\tfn := func(ctx context.Context) (int, error) {\n\t\tcalls++\n\t\treturn 42, nil\n\t}\n\n\tfor range 3 {\n\t\tgot, err := c.Do(context.Background(), \"k\", fn)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\t\tif got != 42 {\n\t\t\tt.Fatalf(\"got %d, want 42\", got)\n\t\t}\n\t}\n\tif calls != 1 {\n\t\tt.Fatalf(\"fn should be called exactly once, got %d\", calls)\n\t}\n}\n\nfunc TestCache_DoErrorNotCached(t *testing.T) {\n\tc := New[int](time.Minute, 0, 10)\n\n\terrBoom := errors.New(\"boom\")\n\t_, err := c.Do(context.Background(), \"k\", func(ctx context.Context) (int, error) {\n\t\treturn 0, errBoom\n\t})\n\tif !errors.Is(err, errBoom) {\n\t\tt.Fatalf(\"expected boom error, got %v\", err)\n\t}\n\tif c.Len() != 0 {\n\t\tt.Fatal(\"errored fn results should not be cached\")\n\t}\n}\n\nfunc TestCache_DoSingleflight(t *testing.T) {\n\t// Concurrent callers missing the same key should collapse into exactly\n\t// one upstream invocation. This is the main reason we use the cache at\n\t// all — a 20-panel dashboard opens with 20 parallel resource calls.\n\tc := New[int](time.Minute, 0, 10)\n\n\tconst callers = 50\n\tvar inFlight int32\n\tstarted := make(chan struct{})\n\n\tfn := func(ctx context.Context) (int, error) {\n\t\tatomic.AddInt32(&inFlight, 1)\n\t\t<-started // hold all concurrent miss-holders inside fn until released\n\t\treturn 7, nil\n\t}\n\n\tvar wg sync.WaitGroup\n\tresults := make([]int, callers)\n\terrs := make([]error, callers)\n\tfor i := range callers {\n\t\twg.Add(1)\n\t\tgo func(i int) {\n\t\t\tdefer wg.Done()\n\t\t\tresults[i], errs[i] = c.Do(context.Background(), \"k\", fn)\n\t\t}(i)\n\t}\n\n\t// Give goroutines a moment to arrive at the singleflight gate, then\n\t// release. We can't observe \"all are waiting\" directly, but in practice\n\t// 50 goroutines schedule quickly and any that arrive late will hit the\n\t// cached value instead.\n\ttime.Sleep(10 * time.Millisecond)\n\tclose(started)\n\twg.Wait()\n\n\tif atomic.LoadInt32(&inFlight) != 1 {\n\t\tt.Fatalf(\"expected singleflight to collapse to 1 upstream call, got %d\", inFlight)\n\t}\n\tfor i := range callers {\n\t\tif errs[i] != nil {\n\t\t\tt.Fatalf(\"caller %d errored: %v\", i, errs[i])\n\t\t}\n\t\tif results[i] != 7 {\n\t\t\tt.Fatalf(\"caller %d got %d, want 7\", i, results[i])\n\t\t}\n\t}\n}\n\nfunc TestCache_DoLeaderCancelDoesNotPoisonWaiters(t *testing.T) {\n\t// Cancelling the singleflight leader must not propagate to waiters whose\n\t// own contexts are still live — a single closed panel shouldn't abort the\n\t// fetch for every other panel sharing the call.\n\tc := New[int](time.Minute, 0, 10)\n\n\trelease := make(chan struct{})\n\tfn := func(ctx context.Context) (int, error) {\n\t\t<-release\n\t\tif err := ctx.Err(); err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\treturn 11, nil\n\t}\n\n\tleaderCtx, cancelLeader := context.WithCancel(context.Background())\n\tvar wg sync.WaitGroup\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\t_, _ = c.Do(leaderCtx, \"k\", fn)\n\t}()\n\n\ttime.Sleep(10 * time.Millisecond) // let the leader enter singleflight\n\n\tconst waiters = 5\n\tresults := make([]int, waiters)\n\terrs := make([]error, waiters)\n\tfor i := range waiters {\n\t\twg.Add(1)\n\t\tgo func(i int) {\n\t\t\tdefer wg.Done()\n\t\t\tresults[i], errs[i] = c.Do(context.Background(), \"k\", fn)\n\t\t}(i)\n\t}\n\n\ttime.Sleep(10 * time.Millisecond)\n\tcancelLeader()\n\tclose(release)\n\twg.Wait()\n\n\tfor i := range waiters {\n\t\tif errs[i] != nil {\n\t\t\tt.Fatalf(\"waiter %d saw leader's cancellation: %v\", i, errs[i])\n\t\t}\n\t\tif results[i] != 11 {\n\t\t\tt.Fatalf(\"waiter %d got %d, want 11\", i, results[i])\n\t\t}\n\t}\n}\n\nfunc TestCache_KeyIsolation(t *testing.T) {\n\t// A primitive defense-in-depth check: different keys must not collide.\n\t// If a future refactor regresses this, integration tests won't catch it.\n\tc := New[string](time.Minute, 0, 10)\n\tc.Set(\"a\", \"value-a\")\n\tc.Set(\"b\", \"value-b\")\n\n\tif v, _ := c.Get(\"a\"); v != \"value-a\" {\n\t\tt.Fatalf(\"a: got %q\", v)\n\t}\n\tif v, _ := c.Get(\"b\"); v != \"value-b\" {\n\t\tt.Fatalf(\"b: got %q\", v)\n\t}\n}\n\nfunc TestCache_JitterStaysWithinBand(t *testing.T) {\n\tttl := 100 * time.Millisecond\n\tjitter := 40 * time.Millisecond\n\tc := New[int](ttl, jitter, 10)\n\n\t// Sample the jitter band a few times and assert every value lies within\n\t// [ttl - jitter/2, ttl + jitter/2). This is a weak property test — we\n\t// don't care about the distribution, only the bounds.\n\tfor range 50 {\n\t\td := c.ttlWithJitter()\n\t\tif d < ttl-jitter/2 || d >= ttl+jitter/2 {\n\t\t\tt.Fatalf(\"ttl with jitter %v out of band [%v, %v)\", d, ttl-jitter/2, ttl+jitter/2)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/plugin/settings.go",
    "content": "package plugin\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/ClickHouse/clickhouse-go/v2\"\n\n\t\"github.com/grafana/grafana-plugin-sdk-go/backend\"\n\t\"github.com/grafana/grafana-plugin-sdk-go/backend/proxy\"\n\tsdkconfig \"github.com/grafana/grafana-plugin-sdk-go/config\"\n)\n\n// Settings - data loaded from grafana settings database\ntype Settings struct {\n\tHost     string `json:\"host,omitempty\"`\n\tPort     int64  `json:\"port,omitempty\"`\n\tProtocol string `json:\"protocol\"`\n\tSecure   bool   `json:\"secure,omitempty\"`\n\tPath     string `json:\"path,omitempty\"`\n\n\tInsecureSkipVerify bool `json:\"tlsSkipVerify,omitempty\"`\n\tTlsClientAuth      bool `json:\"tlsAuth,omitempty\"`\n\tTlsAuthWithCACert  bool `json:\"tlsAuthWithCACert,omitempty\"`\n\tTlsClientCert      string\n\tTlsCACert          string\n\tTlsClientKey       string\n\n\tUsername string `json:\"username,omitempty\"`\n\tPassword string `json:\"-,omitempty\"` //nolint\n\n\tDefaultDatabase string `json:\"defaultDatabase,omitempty\"`\n\n\tConnMaxLifetime string `json:\"connMaxLifetime,omitempty\"`\n\tDialTimeout     string `json:\"dialTimeout,omitempty\"`\n\tQueryTimeout    string `json:\"queryTimeout,omitempty\"`\n\tMaxIdleConns    string `json:\"maxIdleConns,omitempty\"`\n\tMaxOpenConns    string `json:\"maxOpenConns,omitempty\"`\n\n\tHttpHeaders           map[string]string `json:\"-\"`\n\tForwardGrafanaHeaders bool              `json:\"forwardGrafanaHeaders,omitempty\"`\n\tCustomSettings        []CustomSetting   `json:\"customSettings\"`\n\tProxyOptions          *proxy.Options\n\n\tRowLimit       int64 `json:\"rowLimit,omitempty\"`\n\tEnableRowLimit bool  `json:\"enableRowLimit,omitempty\"`\n\n\t// EnableSchemaCache gates the in-process cache that memoizes\n\t// system.tables / system.columns / DISTINCT column-value lookups used\n\t// by the query builder. Defaults to true.\n\tEnableSchemaCache bool `json:\"enableSchemaCache,omitempty\"`\n\t// SchemaCacheTTLSeconds controls how long schema-introspection results\n\t// are considered fresh. Defaults to 60. Set lower if users commonly run\n\t// ALTER TABLE and expect the builder to reflect changes immediately.\n\tSchemaCacheTTLSeconds int `json:\"schemaCacheTTLSeconds,omitempty\"`\n}\n\ntype CustomSetting struct {\n\tSetting string `json:\"setting\"`\n\tValue   string `json:\"value\"`\n}\n\nconst secureHeaderKeyPrefix = \"secureHttpHeaders.\"\n\nfunc (settings *Settings) isValid() (err error) {\n\tif settings.Host == \"\" {\n\t\treturn backend.DownstreamError(ErrorMessageInvalidHost)\n\t}\n\tif settings.Port == 0 {\n\t\treturn backend.DownstreamError(ErrorMessageInvalidPort)\n\t}\n\treturn nil\n}\n\n// LoadSettings will read and validate Settings from the DataSourceConfig\nfunc LoadSettings(ctx context.Context, config backend.DataSourceInstanceSettings) (settings Settings, err error) {\n\tvar jsonData map[string]interface{}\n\tif err := json.Unmarshal(config.JSONData, &jsonData); err != nil {\n\t\treturn settings, fmt.Errorf(\"%s: %w\", err.Error(), ErrorMessageInvalidJSON)\n\t}\n\n\t// Deprecated: Replaced with Host for v4. Deserializes \"server\" field for old v3 configs.\n\tif jsonData[\"server\"] != nil {\n\t\tsettings.Host = jsonData[\"server\"].(string)\n\t}\n\tif jsonData[\"host\"] != nil {\n\t\tsettings.Host = jsonData[\"host\"].(string)\n\t}\n\n\tif jsonData[\"port\"] != nil {\n\t\tif port, ok := jsonData[\"port\"].(string); ok {\n\t\t\tsettings.Port, err = strconv.ParseInt(port, 0, 64)\n\t\t\tif err != nil {\n\t\t\t\treturn settings, backend.DownstreamError(fmt.Errorf(\"could not parse port value: %w\", err))\n\t\t\t}\n\t\t} else {\n\t\t\tsettings.Port = int64(jsonData[\"port\"].(float64))\n\t\t}\n\t}\n\tif jsonData[\"protocol\"] != nil {\n\t\tsettings.Protocol = jsonData[\"protocol\"].(string)\n\t}\n\tif jsonData[\"secure\"] != nil {\n\t\tif secure, ok := jsonData[\"secure\"].(string); ok {\n\t\t\tsettings.Secure, err = strconv.ParseBool(secure)\n\t\t\tif err != nil {\n\t\t\t\treturn settings, backend.DownstreamError(fmt.Errorf(\"could not parse secure value: %w\", err))\n\t\t\t}\n\t\t} else {\n\t\t\tsettings.Secure = jsonData[\"secure\"].(bool)\n\t\t}\n\t}\n\tif jsonData[\"path\"] != nil {\n\t\tsettings.Path = jsonData[\"path\"].(string)\n\t}\n\n\tif jsonData[\"tlsSkipVerify\"] != nil {\n\t\tif tlsSkipVerify, ok := jsonData[\"tlsSkipVerify\"].(string); ok {\n\t\t\tsettings.InsecureSkipVerify, err = strconv.ParseBool(tlsSkipVerify)\n\t\t\tif err != nil {\n\t\t\t\treturn settings, backend.DownstreamError(fmt.Errorf(\"could not parse tlsSkipVerify value: %w\", err))\n\t\t\t}\n\t\t} else {\n\t\t\tsettings.InsecureSkipVerify = jsonData[\"tlsSkipVerify\"].(bool)\n\t\t}\n\t}\n\tif jsonData[\"tlsAuth\"] != nil {\n\t\tif tlsAuth, ok := jsonData[\"tlsAuth\"].(string); ok {\n\t\t\tsettings.TlsClientAuth, err = strconv.ParseBool(tlsAuth)\n\t\t\tif err != nil {\n\t\t\t\treturn settings, backend.DownstreamError(fmt.Errorf(\"could not parse tlsAuth value: %w\", err))\n\t\t\t}\n\t\t} else {\n\t\t\tsettings.TlsClientAuth = jsonData[\"tlsAuth\"].(bool)\n\t\t}\n\t}\n\tif jsonData[\"tlsAuthWithCACert\"] != nil {\n\t\tif tlsAuthWithCACert, ok := jsonData[\"tlsAuthWithCACert\"].(string); ok {\n\t\t\tsettings.TlsAuthWithCACert, err = strconv.ParseBool(tlsAuthWithCACert)\n\t\t\tif err != nil {\n\t\t\t\treturn settings, backend.DownstreamError(fmt.Errorf(\"could not parse tlsAuthWithCACert value: %w\", err))\n\t\t\t}\n\t\t} else {\n\t\t\tsettings.TlsAuthWithCACert = jsonData[\"tlsAuthWithCACert\"].(bool)\n\t\t}\n\t}\n\n\tif jsonData[\"username\"] != nil {\n\t\tsettings.Username = jsonData[\"username\"].(string)\n\t}\n\tif jsonData[\"defaultDatabase\"] != nil {\n\t\tsettings.DefaultDatabase = jsonData[\"defaultDatabase\"].(string)\n\t}\n\n\t// Deprecated: Replaced with DialTimeout for v4. Deserializes \"timeout\" field for old v3 configs.\n\tif jsonData[\"timeout\"] != nil {\n\t\tif val, ok := jsonData[\"timeout\"].(string); !ok {\n\t\t\tif val, ok := jsonData[\"timeout\"].(float64); ok {\n\t\t\t\tsettings.DialTimeout = fmt.Sprintf(\"%d\", int64(val))\n\t\t\t}\n\t\t} else {\n\t\t\tsettings.DialTimeout = val\n\t\t}\n\t}\n\tif jsonData[\"dialTimeout\"] != nil {\n\t\tif val, ok := jsonData[\"dialTimeout\"].(string); !ok {\n\t\t\tif val, ok := jsonData[\"dialTimeout\"].(float64); ok {\n\t\t\t\tsettings.DialTimeout = fmt.Sprintf(\"%d\", int64(val))\n\t\t\t}\n\t\t} else {\n\t\t\tsettings.DialTimeout = val\n\t\t}\n\t}\n\n\tif jsonData[\"queryTimeout\"] != nil {\n\t\tif val, ok := jsonData[\"queryTimeout\"].(string); ok {\n\t\t\tsettings.QueryTimeout = val\n\t\t}\n\t\tif val, ok := jsonData[\"queryTimeout\"].(float64); ok {\n\t\t\tsettings.QueryTimeout = fmt.Sprintf(\"%d\", int64(val))\n\t\t}\n\t}\n\tif jsonData[\"customSettings\"] != nil {\n\t\tcustomSettingsRaw := jsonData[\"customSettings\"].([]interface{})\n\t\tcustomSettings := make([]CustomSetting, len(customSettingsRaw))\n\n\t\tfor i, raw := range customSettingsRaw {\n\t\t\trawMap := raw.(map[string]interface{})\n\t\t\tcustomSettings[i] = CustomSetting{\n\t\t\t\tSetting: rawMap[\"setting\"].(string),\n\t\t\t\tValue:   rawMap[\"value\"].(string),\n\t\t\t}\n\t\t}\n\n\t\tsettings.CustomSettings = customSettings\n\t}\n\tif jsonData[\"forwardGrafanaHeaders\"] != nil {\n\t\tif forwardGrafanaHeaders, ok := jsonData[\"forwardGrafanaHeaders\"].(string); ok {\n\t\t\tsettings.ForwardGrafanaHeaders, err = strconv.ParseBool(forwardGrafanaHeaders)\n\t\t\tif err != nil {\n\t\t\t\treturn settings, backend.DownstreamError(fmt.Errorf(\"could not parse forwardGrafanaHeaders value: %w\", err))\n\t\t\t}\n\t\t} else {\n\t\t\tsettings.ForwardGrafanaHeaders = jsonData[\"forwardGrafanaHeaders\"].(bool)\n\t\t}\n\t}\n\n\tif jsonData[\"enableRowLimit\"] != nil {\n\t\tif enableRowLimitString, ok := jsonData[\"enableRowLimit\"].(string); ok {\n\t\t\tsettings.EnableRowLimit, err = strconv.ParseBool(enableRowLimitString)\n\t\t\tif err != nil {\n\t\t\t\tbackend.Logger.Warn(\"Failed to parse enableRowLimit value, defaulting to false\", \"error\", err)\n\t\t\t}\n\t\t} else {\n\t\t\tsettings.EnableRowLimit = jsonData[\"enableRowLimit\"].(bool)\n\t\t}\n\t}\n\n\t// Default schema cache on; surface both as booleans and strings to stay\n\t// consistent with the existing settings-parsing style in this file.\n\tsettings.EnableSchemaCache = true\n\tif raw, ok := jsonData[\"enableSchemaCache\"]; ok && raw != nil {\n\t\tswitch v := raw.(type) {\n\t\tcase bool:\n\t\t\tsettings.EnableSchemaCache = v\n\t\tcase string:\n\t\t\tif parsed, parseErr := strconv.ParseBool(v); parseErr == nil {\n\t\t\t\tsettings.EnableSchemaCache = parsed\n\t\t\t} else {\n\t\t\t\tbackend.Logger.Warn(\"Failed to parse enableSchemaCache value, defaulting to true\", \"error\", parseErr)\n\t\t\t}\n\t\t}\n\t}\n\tif raw, ok := jsonData[\"schemaCacheTTLSeconds\"]; ok && raw != nil {\n\t\tswitch v := raw.(type) {\n\t\tcase float64:\n\t\t\tsettings.SchemaCacheTTLSeconds = int(v)\n\t\tcase string:\n\t\t\tif parsed, parseErr := strconv.Atoi(v); parseErr == nil {\n\t\t\t\tsettings.SchemaCacheTTLSeconds = parsed\n\t\t\t} else {\n\t\t\t\tbackend.Logger.Warn(\"Failed to parse schemaCacheTTLSeconds value, using default\", \"error\", parseErr)\n\t\t\t}\n\t\t}\n\t}\n\tif settings.SchemaCacheTTLSeconds <= 0 {\n\t\tsettings.SchemaCacheTTLSeconds = 60\n\t}\n\n\t// Set default values\n\tif strings.TrimSpace(settings.DialTimeout) == \"\" {\n\t\tsettings.DialTimeout = \"10\"\n\t}\n\tif strings.TrimSpace(settings.QueryTimeout) == \"\" {\n\t\tsettings.QueryTimeout = \"60\"\n\t}\n\tif strings.TrimSpace(settings.ConnMaxLifetime) == \"\" {\n\t\tsettings.ConnMaxLifetime = \"5\"\n\t}\n\tif strings.TrimSpace(settings.MaxIdleConns) == \"\" {\n\t\tsettings.MaxIdleConns = \"25\"\n\t}\n\tif strings.TrimSpace(settings.MaxOpenConns) == \"\" {\n\t\tsettings.MaxOpenConns = \"50\"\n\t}\n\n\t// Load secure settings\n\tpassword, ok := config.DecryptedSecureJSONData[\"password\"]\n\tif ok {\n\t\tsettings.Password = password\n\t}\n\ttlsCACert, ok := config.DecryptedSecureJSONData[\"tlsCACert\"]\n\tif ok {\n\t\tsettings.TlsCACert = tlsCACert\n\t}\n\ttlsClientCert, ok := config.DecryptedSecureJSONData[\"tlsClientCert\"]\n\tif ok {\n\t\tsettings.TlsClientCert = tlsClientCert\n\t}\n\ttlsClientKey, ok := config.DecryptedSecureJSONData[\"tlsClientKey\"]\n\tif ok {\n\t\tsettings.TlsClientKey = tlsClientKey\n\t}\n\n\tif settings.Protocol == clickhouse.HTTP.String() {\n\t\tsettings.HttpHeaders = loadHttpHeaders(jsonData, config.DecryptedSecureJSONData)\n\t}\n\n\tproxyOpts, err := config.ProxyOptionsFromContext(ctx)\n\n\tif err == nil && proxyOpts != nil {\n\t\t// the sdk expects the timeout to not be a string\n\t\ttimeout, err := strconv.ParseFloat(settings.DialTimeout, 64)\n\t\tif err == nil {\n\t\t\tproxyOpts.Timeouts.Timeout = time.Duration(timeout) * time.Second\n\t\t}\n\n\t\tsettings.ProxyOptions = proxyOpts\n\t}\n\n\t// This condition can be removed once the minimum supported Grafana version is 11.0.0\n\tif settings.EnableRowLimit {\n\t\tcfg := sdkconfig.GrafanaConfigFromContext(ctx)\n\t\tsqlCfg, err := cfg.SQL()\n\t\tif err != nil {\n\t\t\treturn settings, err\n\t\t}\n\n\t\tsettings.RowLimit = sqlCfg.RowLimit\n\t}\n\n\treturn settings, settings.isValid()\n}\n\n// loadHttpHeaders loads secure and plain text headers from the config\nfunc loadHttpHeaders(jsonData map[string]interface{}, secureJsonData map[string]string) map[string]string {\n\thttpHeaders := make(map[string]string)\n\n\tif jsonData[\"httpHeaders\"] != nil {\n\t\thttpHeadersRaw := jsonData[\"httpHeaders\"].([]interface{})\n\n\t\tfor _, rawHeader := range httpHeadersRaw {\n\t\t\theader, _ := rawHeader.(map[string]interface{})\n\t\t\theaderName, _ := header[\"name\"].(string)\n\t\t\theaderName = strings.TrimSpace(headerName)\n\t\t\theaderValue, _ := header[\"value\"].(string)\n\t\t\tif headerName != \"\" && headerValue != \"\" {\n\t\t\t\thttpHeaders[headerName] = headerValue\n\t\t\t}\n\t\t}\n\t}\n\n\tfor k, v := range secureJsonData {\n\t\tif v != \"\" && strings.HasPrefix(k, secureHeaderKeyPrefix) {\n\t\t\theaderName := strings.TrimSpace(k[len(secureHeaderKeyPrefix):])\n\t\t\thttpHeaders[headerName] = v\n\t\t}\n\t}\n\n\treturn httpHeaders\n}\n"
  },
  {
    "path": "pkg/plugin/settings_test.go",
    "content": "package plugin\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/ClickHouse/clickhouse-go/v2\"\n\n\t\"github.com/grafana/grafana-plugin-sdk-go/backend\"\n\t\"github.com/grafana/grafana-plugin-sdk-go/backend/proxy\"\n\tsdkconfig \"github.com/grafana/grafana-plugin-sdk-go/config\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestLoadSettings(t *testing.T) {\n\tt.Run(\"should parse settings correctly\", func(t *testing.T) {\n\n\t\tctx := context.Background()\n\t\tctx = sdkconfig.WithGrafanaConfig(ctx, sdkconfig.NewGrafanaCfg(map[string]string{\n\t\t\t\"GF_SQL_ROW_LIMIT\":                         \"1000000\",\n\t\t\t\"GF_SQL_MAX_OPEN_CONNS_DEFAULT\":            \"10\",\n\t\t\t\"GF_SQL_MAX_IDLE_CONNS_DEFAULT\":            \"10\",\n\t\t\t\"GF_SQL_MAX_CONN_LIFETIME_SECONDS_DEFAULT\": \"60\",\n\t\t}))\n\n\t\ttype args struct {\n\t\t\tconfig backend.DataSourceInstanceSettings\n\t\t}\n\t\ttests := []struct {\n\t\t\tname         string\n\t\t\targs         args\n\t\t\twantSettings Settings\n\t\t\twantErr      error\n\t\t\ttestCtx      context.Context\n\t\t}{\n\t\t\t{\n\t\t\t\tname: \"should parse and set all json fields correctly\",\n\t\t\t\targs: args{\n\t\t\t\t\tconfig: backend.DataSourceInstanceSettings{\n\t\t\t\t\t\tUID: \"ds-uid\",\n\t\t\t\t\t\tJSONData: []byte(`{\n\t\t\t\t\t\t\t\"host\": \"foo\", \"port\": 443,\n\t\t\t\t\t\t\t\"path\": \"custom-path\", \"protocol\": \"http\",\n\t\t\t\t\t\t\t\"username\": \"baz\",\n\t\t\t\t\t\t\t\"defaultDatabase\":\"example\", \"tlsSkipVerify\": true, \"tlsAuth\" : true,\n\t\t\t\t\t\t\t\"tlsAuthWithCACert\": true, \"dialTimeout\": \"10\", \"enableSecureSocksProxy\": true,\n\t\t\t\t\t\t\t\"httpHeaders\": [{ \"name\": \" test-plain-1 \", \"value\": \"value-1\", \"secure\": false }],\n\t\t\t\t\t\t\t\"forwardGrafanaHeaders\": true,\n\t\t\t\t\t\t\t\"enableRowLimit\": true\n\t\t\t\t\t\t}`),\n\t\t\t\t\t\tDecryptedSecureJSONData: map[string]string{\n\t\t\t\t\t\t\t\"password\":  \"bar\",\n\t\t\t\t\t\t\t\"tlsCACert\": \"caCert\", \"tlsClientCert\": \"clientCert\", \"tlsClientKey\": \"clientKey\",\n\t\t\t\t\t\t\t\"secureSocksProxyPassword\":          \"test\",\n\t\t\t\t\t\t\t\"secureHttpHeaders. test-secure-2 \": \"value-2\",\n\t\t\t\t\t\t\t\"secureHttpHeaders.test-secure-3\":   \"value-3\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantSettings: Settings{\n\t\t\t\t\tHost:               \"foo\",\n\t\t\t\t\tPort:               443,\n\t\t\t\t\tPath:               \"custom-path\",\n\t\t\t\t\tProtocol:           clickhouse.HTTP.String(),\n\t\t\t\t\tUsername:           \"baz\",\n\t\t\t\t\tDefaultDatabase:    \"example\",\n\t\t\t\t\tInsecureSkipVerify: true,\n\t\t\t\t\tTlsClientAuth:      true,\n\t\t\t\t\tTlsAuthWithCACert:  true,\n\t\t\t\t\tPassword:           \"bar\",\n\t\t\t\t\tTlsCACert:          \"caCert\",\n\t\t\t\t\tTlsClientCert:      \"clientCert\",\n\t\t\t\t\tTlsClientKey:       \"clientKey\",\n\t\t\t\t\tConnMaxLifetime:    \"5\",\n\t\t\t\t\tDialTimeout:        \"10\",\n\t\t\t\t\tMaxIdleConns:       \"25\",\n\t\t\t\t\tMaxOpenConns:       \"50\",\n\t\t\t\t\tQueryTimeout:       \"60\",\n\t\t\t\t\tHttpHeaders: map[string]string{\n\t\t\t\t\t\t\"test-plain-1\":  \"value-1\",\n\t\t\t\t\t\t\"test-secure-2\": \"value-2\",\n\t\t\t\t\t\t\"test-secure-3\": \"value-3\",\n\t\t\t\t\t},\n\t\t\t\t\tForwardGrafanaHeaders: true,\n\t\t\t\t\tProxyOptions: &proxy.Options{\n\t\t\t\t\t\tEnabled: true,\n\t\t\t\t\t\tAuth: &proxy.AuthOptions{\n\t\t\t\t\t\t\tUsername: \"ds-uid\",\n\t\t\t\t\t\t\tPassword: \"test\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTimeouts: &proxy.TimeoutOptions{\n\t\t\t\t\t\t\tTimeout:   10 * time.Second,\n\t\t\t\t\t\t\tKeepAlive: proxy.DefaultTimeoutOptions.KeepAlive,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tEnableRowLimit:        true,\n\t\t\t\t\tRowLimit:              1000000,\n\t\t\t\t\tEnableSchemaCache:     true,\n\t\t\t\t\tSchemaCacheTTLSeconds: 60,\n\t\t\t\t},\n\t\t\t\twantErr: nil,\n\t\t\t\ttestCtx: ctx,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: \"should convert string values to the correct type\",\n\t\t\t\targs: args{\n\t\t\t\t\tconfig: backend.DataSourceInstanceSettings{\n\t\t\t\t\t\tJSONData:                []byte(`{\"host\": \"test\", \"port\": \"443\", \"path\": \"custom-path\", \"tlsSkipVerify\": \"true\", \"tlsAuth\" : \"true\", \"tlsAuthWithCACert\": \"true\", \"enableRowLimit\": \"true\"}`),\n\t\t\t\t\t\tDecryptedSecureJSONData: map[string]string{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantSettings: Settings{\n\t\t\t\t\tHost:               \"test\",\n\t\t\t\t\tPort:               443,\n\t\t\t\t\tPath:               \"custom-path\",\n\t\t\t\t\tInsecureSkipVerify: true,\n\t\t\t\t\tTlsClientAuth:      true,\n\t\t\t\t\tTlsAuthWithCACert:  true,\n\t\t\t\t\tConnMaxLifetime:    \"5\",\n\t\t\t\t\tDialTimeout:        \"10\",\n\t\t\t\t\tMaxIdleConns:       \"25\",\n\t\t\t\t\tMaxOpenConns:       \"50\",\n\t\t\t\t\tQueryTimeout:       \"60\",\n\t\t\t\t\tProxyOptions:          nil,\n\t\t\t\t\tEnableRowLimit:        true,\n\t\t\t\t\tRowLimit:              1000000,\n\t\t\t\t\tEnableSchemaCache:     true,\n\t\t\t\t\tSchemaCacheTTLSeconds: 60,\n\t\t\t\t},\n\t\t\t\twantErr: nil,\n\t\t\t\ttestCtx: ctx,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: \"should parse v3 config fields into new fields\",\n\t\t\t\targs: args{\n\t\t\t\t\tconfig: backend.DataSourceInstanceSettings{\n\t\t\t\t\t\tJSONData:                []byte(`{\"server\": \"test\", \"port\": 443, \"timeout\": \"10\", \"enableRowLimit\": true}`),\n\t\t\t\t\t\tDecryptedSecureJSONData: map[string]string{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantSettings: Settings{\n\t\t\t\t\tHost:            \"test\",\n\t\t\t\t\tPort:            443,\n\t\t\t\t\tConnMaxLifetime: \"5\",\n\t\t\t\t\tDialTimeout:     \"10\",\n\t\t\t\t\tMaxIdleConns:    \"25\",\n\t\t\t\t\tMaxOpenConns:    \"50\",\n\t\t\t\t\tQueryTimeout:          \"60\",\n\t\t\t\t\tRowLimit:              1000000,\n\t\t\t\t\tEnableRowLimit:        true,\n\t\t\t\t\tEnableSchemaCache:     true,\n\t\t\t\t\tSchemaCacheTTLSeconds: 60,\n\t\t\t\t},\n\t\t\t\twantErr: nil,\n\t\t\t\ttestCtx: ctx,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: \"should disable row limit\",\n\t\t\t\targs: args{\n\t\t\t\t\tconfig: backend.DataSourceInstanceSettings{\n\t\t\t\t\t\tUID: \"ds-uid\",\n\t\t\t\t\t\tJSONData: []byte(`{\n\t\t\t\t\t\t\t\"host\": \"foo\", \"port\": 443,\n\t\t\t\t\t\t\t\"path\": \"custom-path\", \"protocol\": \"http\",\n\t\t\t\t\t\t\t\"username\": \"baz\",\n\t\t\t\t\t\t\t\"defaultDatabase\":\"example\", \"tlsSkipVerify\": true, \"tlsAuth\" : true,\n\t\t\t\t\t\t\t\"tlsAuthWithCACert\": true, \"dialTimeout\": \"10\", \"enableSecureSocksProxy\": true,\n\t\t\t\t\t\t\t\"httpHeaders\": [{ \"name\": \" test-plain-1 \", \"value\": \"value-1\", \"secure\": false }],\n\t\t\t\t\t\t\t\"forwardGrafanaHeaders\": true,\n\t\t\t\t\t\t\t\"enableRowLimit\": false\n\t\t\t\t\t\t}`),\n\t\t\t\t\t\tDecryptedSecureJSONData: map[string]string{\n\t\t\t\t\t\t\t\"password\":  \"bar\",\n\t\t\t\t\t\t\t\"tlsCACert\": \"caCert\", \"tlsClientCert\": \"clientCert\", \"tlsClientKey\": \"clientKey\",\n\t\t\t\t\t\t\t\"secureSocksProxyPassword\":          \"test\",\n\t\t\t\t\t\t\t\"secureHttpHeaders. test-secure-2 \": \"value-2\",\n\t\t\t\t\t\t\t\"secureHttpHeaders.test-secure-3\":   \"value-3\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantSettings: Settings{\n\t\t\t\t\tHost:               \"foo\",\n\t\t\t\t\tPort:               443,\n\t\t\t\t\tPath:               \"custom-path\",\n\t\t\t\t\tProtocol:           clickhouse.HTTP.String(),\n\t\t\t\t\tUsername:           \"baz\",\n\t\t\t\t\tDefaultDatabase:    \"example\",\n\t\t\t\t\tInsecureSkipVerify: true,\n\t\t\t\t\tTlsClientAuth:      true,\n\t\t\t\t\tTlsAuthWithCACert:  true,\n\t\t\t\t\tPassword:           \"bar\",\n\t\t\t\t\tTlsCACert:          \"caCert\",\n\t\t\t\t\tTlsClientCert:      \"clientCert\",\n\t\t\t\t\tTlsClientKey:       \"clientKey\",\n\t\t\t\t\tConnMaxLifetime:    \"5\",\n\t\t\t\t\tDialTimeout:        \"10\",\n\t\t\t\t\tMaxIdleConns:       \"25\",\n\t\t\t\t\tMaxOpenConns:       \"50\",\n\t\t\t\t\tQueryTimeout:       \"60\",\n\t\t\t\t\tHttpHeaders: map[string]string{\n\t\t\t\t\t\t\"test-plain-1\":  \"value-1\",\n\t\t\t\t\t\t\"test-secure-2\": \"value-2\",\n\t\t\t\t\t\t\"test-secure-3\": \"value-3\",\n\t\t\t\t\t},\n\t\t\t\t\tForwardGrafanaHeaders: true,\n\t\t\t\t\tProxyOptions: &proxy.Options{\n\t\t\t\t\t\tEnabled: true,\n\t\t\t\t\t\tAuth: &proxy.AuthOptions{\n\t\t\t\t\t\t\tUsername: \"ds-uid\",\n\t\t\t\t\t\t\tPassword: \"test\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTimeouts: &proxy.TimeoutOptions{\n\t\t\t\t\t\t\tTimeout:   10 * time.Second,\n\t\t\t\t\t\t\tKeepAlive: proxy.DefaultTimeoutOptions.KeepAlive,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tEnableRowLimit:        false,\n\t\t\t\t\tEnableSchemaCache:     true,\n\t\t\t\t\tSchemaCacheTTLSeconds: 60,\n\t\t\t\t},\n\t\t\t\twantErr: nil,\n\t\t\t\ttestCtx: ctx,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: \"should accept numeric dialTimeout and queryTimeout values\",\n\t\t\t\targs: args{\n\t\t\t\t\tconfig: backend.DataSourceInstanceSettings{\n\t\t\t\t\t\tJSONData:                []byte(`{\"host\": \"test\", \"port\": 443, \"dialTimeout\": 15, \"queryTimeout\": 120}`),\n\t\t\t\t\t\tDecryptedSecureJSONData: map[string]string{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantSettings: Settings{\n\t\t\t\t\tHost:            \"test\",\n\t\t\t\t\tPort:            443,\n\t\t\t\t\tConnMaxLifetime: \"5\",\n\t\t\t\t\tDialTimeout:     \"15\",\n\t\t\t\t\tMaxIdleConns:    \"25\",\n\t\t\t\t\tMaxOpenConns:    \"50\",\n\t\t\t\t\tQueryTimeout:          \"120\",\n\t\t\t\t\tEnableRowLimit:        false,\n\t\t\t\t\tEnableSchemaCache:     true,\n\t\t\t\t\tSchemaCacheTTLSeconds: 60,\n\t\t\t\t},\n\t\t\t\twantErr: nil,\n\t\t\t\ttestCtx: ctx,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: \"should accept numeric timeout value (v3 deprecated field)\",\n\t\t\t\targs: args{\n\t\t\t\t\tconfig: backend.DataSourceInstanceSettings{\n\t\t\t\t\t\tJSONData:                []byte(`{\"server\": \"test\", \"port\": 443, \"timeout\": 25}`),\n\t\t\t\t\t\tDecryptedSecureJSONData: map[string]string{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantSettings: Settings{\n\t\t\t\t\tHost:            \"test\",\n\t\t\t\t\tPort:            443,\n\t\t\t\t\tConnMaxLifetime: \"5\",\n\t\t\t\t\tDialTimeout:     \"25\",\n\t\t\t\t\tMaxIdleConns:    \"25\",\n\t\t\t\t\tMaxOpenConns:    \"50\",\n\t\t\t\t\tQueryTimeout:          \"60\",\n\t\t\t\t\tEnableRowLimit:        false,\n\t\t\t\t\tEnableSchemaCache:     true,\n\t\t\t\t\tSchemaCacheTTLSeconds: 60,\n\t\t\t\t},\n\t\t\t\twantErr: nil,\n\t\t\t\ttestCtx: ctx,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: \"should accept numeric timeout values with floating point precision\",\n\t\t\t\targs: args{\n\t\t\t\t\tconfig: backend.DataSourceInstanceSettings{\n\t\t\t\t\t\tJSONData:                []byte(`{\"host\": \"test\", \"port\": 443, \"dialTimeout\": 10.5, \"queryTimeout\": 60.7}`),\n\t\t\t\t\t\tDecryptedSecureJSONData: map[string]string{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantSettings: Settings{\n\t\t\t\t\tHost:            \"test\",\n\t\t\t\t\tPort:            443,\n\t\t\t\t\tConnMaxLifetime: \"5\",\n\t\t\t\t\tDialTimeout:     \"10\",\n\t\t\t\t\tMaxIdleConns:    \"25\",\n\t\t\t\t\tMaxOpenConns:    \"50\",\n\t\t\t\t\tQueryTimeout:          \"60\",\n\t\t\t\t\tEnableRowLimit:        false,\n\t\t\t\t\tEnableSchemaCache:     true,\n\t\t\t\t\tSchemaCacheTTLSeconds: 60,\n\t\t\t\t},\n\t\t\t\twantErr: nil,\n\t\t\t\ttestCtx: ctx,\n\t\t\t},\n\t\t}\n\t\tfor _, tt := range tests {\n\t\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t\tgotSettings, err := LoadSettings(tt.testCtx, tt.args.config)\n\t\t\t\tassert.Equal(t, tt.wantErr, err)\n\t\t\t\tif !reflect.DeepEqual(gotSettings, tt.wantSettings) {\n\t\t\t\t\tt.Errorf(\"LoadSettings() = %v, want %v\", gotSettings, tt.wantSettings)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n\tt.Run(\"should capture invalid settings\", func(t *testing.T) {\n\t\tctx := context.Background()\n\t\tctx = sdkconfig.WithGrafanaConfig(ctx, sdkconfig.NewGrafanaCfg(map[string]string{\n\t\t\t\"GF_SQL_ROW_LIMIT\":                         \"1000000\",\n\t\t\t\"GF_SQL_MAX_OPEN_CONNS_DEFAULT\":            \"10\",\n\t\t\t\"GF_SQL_MAX_IDLE_CONNS_DEFAULT\":            \"10\",\n\t\t\t\"GF_SQL_MAX_CONN_LIFETIME_SECONDS_DEFAULT\": \"60\",\n\t\t}))\n\n\t\ttests := []struct {\n\t\t\tjsonData    string\n\t\t\tpassword    string\n\t\t\twantErr     error\n\t\t\tdescription string\n\t\t}{\n\t\t\t{jsonData: `{ \"host\": \"\", \"port\": 443 }`, password: \"\", wantErr: ErrorMessageInvalidHost, description: \"should capture empty server name\"},\n\t\t\t{jsonData: `{ \"host\": \"foo\" }`, password: \"\", wantErr: ErrorMessageInvalidPort, description: \"should capture nil port\"},\n\t\t\t{jsonData: `  \"host\": \"foo\", \"port\": 443, \"username\" : \"foo\" }`, password: \"\", wantErr: ErrorMessageInvalidJSON, description: \"should capture invalid json\"},\n\t\t}\n\t\tfor i, tc := range tests {\n\t\t\tt.Run(fmt.Sprintf(\"[%v/%v] %s\", i+1, len(tests), tc.description), func(t *testing.T) {\n\t\t\t\t_, err := LoadSettings(ctx, backend.DataSourceInstanceSettings{\n\t\t\t\t\tJSONData:                []byte(tc.jsonData),\n\t\t\t\t\tDecryptedSecureJSONData: map[string]string{\"password\": tc.password},\n\t\t\t\t})\n\t\t\t\tif !errors.Is(err, tc.wantErr) {\n\t\t\t\t\tt.Errorf(\"%s not captured. %s\", tc.wantErr, err.Error())\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "playwright.config.ts",
    "content": "import { dirname } from 'path';\n\nimport { defineConfig, devices } from '@playwright/test';\nimport type { PluginOptions } from '@grafana/plugin-e2e';\n\n/**\n * Read environment variables from file.\n * https://github.com/motdotla/dotenv\n */\n// import dotenv from 'dotenv';\n// dotenv.config({ path: path.resolve(__dirname, '.env') });\n\n/**\n * See https://playwright.dev/docs/test-configuration.\n */\nexport default defineConfig<PluginOptions>({\n  testDir: './tests/e2e',\n  /* Run tests in files in parallel */\n  fullyParallel: true,\n  /* Fail the build on CI if you accidentally left test.only in the source code. */\n  forbidOnly: !!process.env.CI,\n  /* Retry on CI only */\n  retries: process.env.CI ? 2 : 0,\n  /* Opt out of parallel tests on CI. */\n  workers: process.env.CI ? 1 : undefined,\n  /* Reporter to use. See https://playwright.dev/docs/test-reporters */\n  reporter: 'html',\n  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */\n  use: {\n    /* Base URL to use in actions like `await page.goto('/')`. */\n    baseURL: process.env.GRAFANA_URL || `http://localhost:${process.env.PORT || 3000}`,\n\n    launchOptions: {\n      executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,\n    },\n\n    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */\n    trace: 'on-first-retry',\n    screenshot: 'only-on-failure',\n    video: 'on',\n  },\n\n  /* Configure projects for major browsers */\n  projects: [\n    {\n      name: 'auth',\n      testDir: `${dirname(require.resolve('@grafana/plugin-e2e'))}/auth`,\n      testMatch: [/.*\\.js/],\n    },\n    {\n      name: 'run-tests',\n      use: {\n        ...devices['Desktop Chrome'],\n        storageState: `playwright/.auth/${process.env.GRAFANA_ADMIN_USER || 'admin'}.json`,\n      },\n      dependencies: ['auth'],\n    },\n  ],\n});\n"
  },
  {
    "path": "provisioning/datasources/clickhouse.yml",
    "content": "# Configuration file version\napiVersion: 1\n\n# List of data sources to delete from the database.\ndeleteDatasources:\n  - name: ClickHouse\n    orgId: 1\n\ndatasources:\n  - name: ClickHouse\n    uid: clickhouse-e2e\n    type: grafana-clickhouse-datasource\n    access: proxy\n    url: clickhouse-server\n    orgId: 1\n    jsonData:\n      host: clickhouse-server\n      database: \"test\"\n      username: \"default\"\n      port: 9000\n      protocol: native\n      # Enables the Monaco SQL validator in the query editor. Turned on in the\n      # E2E datasource so tests/e2e/sqlValidation.spec.ts can regression-guard\n      # against false positives on ClickHouse-specific syntax.\n      validateSql: true\n    secureJsonData:\n      # password: \"\"\n"
  },
  {
    "path": "scripts/ca-cert.sh",
    "content": "# Generate server.key and server.crt signed by our local CA. \nopenssl genrsa -out $PWD/config-secure/server.key 2048\n# TODO - use localhost instead of foo?\nopenssl req -sha256 -new -key $PWD/config-secure/server.key -out $PWD/config-secure/server.csr \\\n  -subj \"/CN=foo\" \\\n\nopenssl x509 -req -in $PWD/config-secure/server.csr -CA $PWD/config-secure/my-own-ca.crt -CAkey $PWD/config-secure/my-own-ca.key \\\n-CAcreateserial -out $PWD/config-secure/server.crt -days 825 -sha256 -extfile $PWD/config-secure/server.ext\n\n# Confirm the certificate is valid. \nopenssl verify -CAfile $PWD/config-secure/my-own-ca.crt $PWD/config-secure/server.crt"
  },
  {
    "path": "scripts/ca.ext",
    "content": "authorityKeyIdentifier=keyid,issuer\nbasicConstraints=CA:FALSE\nkeyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment\nsubjectAltName = @alt_names\n\n[alt_names]\nDNS.1 = root"
  },
  {
    "path": "scripts/ca.sh",
    "content": "# create a ca certificate\n\nopenssl genrsa -out $PWD/config-secure/my-own-ca.key 2048\nopenssl req -new -x509 -days 3650 -key $PWD/config-secure/my-own-ca.key \\\n  -subj \"/CN=root\" \\\n  -addext \"subjectAltName = DNS:root\" \\\n  -sha256 -extensions v3_ca -out $PWD/config-secure/my-own-ca.crt\n\n# if running an older version of openssl\n# openssl req -new -x509 -days 3650 -key $PWD/config-secure/my-own-ca.key \\\n#   -subj \"/CN=root\" \\\n#   -sha256 -extensions v3_ca -out $PWD/config-secure/my-own-ca.crt \\\n#   -extfile $PWD/scripts/ca.ext"
  },
  {
    "path": "scripts/certs.sh",
    "content": "openssl req -subj \"/CN=foo\" -new \\\n-newkey rsa:2048 -days 365 -nodes -x509 \\\n-keyout $PWD/config/server.key \\\n-out $PWD/config/server.crt \n\n"
  },
  {
    "path": "src/__mocks__/ConfigEditor.ts",
    "content": "import * as fs from 'fs';\nimport { ConfigEditorProps } from 'views/CHConfigEditor';\nimport { CHConfig } from 'types/config';\n\nconst pluginJson = JSON.parse(fs.readFileSync('./src/plugin.json', 'utf-8'));\n\nexport const mockConfigEditorProps = (overrides?: Partial<CHConfig>): ConfigEditorProps => ({\n  options: {\n    ...pluginJson,\n    jsonData: {\n      server: 'foo.com',\n      port: 443,\n      path: '',\n      username: 'user',\n      protocol: 'http',\n      ...overrides,\n    },\n  },\n  onOptionsChange: jest.fn(),\n});\n"
  },
  {
    "path": "src/__mocks__/datasource.ts",
    "content": "import { PluginType } from '@grafana/data';\nimport { Protocol } from 'types/config';\nimport { CHQuery, EditorType } from 'types/sql';\nimport { QueryType } from 'types/queryBuilder';\nimport { Datasource } from '../data/CHDatasource';\nimport { pluginVersion } from 'utils/version';\n\nexport const newMockDatasource = (): Datasource => {\n  const mockDatasource = new Datasource({\n    id: 1,\n    uid: 'clickhouse_ds',\n    type: 'grafana-clickhouse-datasource',\n    name: 'ClickHouse',\n    jsonData: {\n      version: pluginVersion,\n      host: 'foo.com',\n      port: 443,\n      path: '',\n      username: 'user',\n      defaultDatabase: 'foo',\n      defaultTable: 'bar',\n      aliasTables: [],\n      protocol: Protocol.Native,\n    },\n    readOnly: true,\n    access: 'direct',\n    meta: {\n      id: 'grafana-clickhouse-datasource',\n      name: 'ClickHouse',\n      type: PluginType.datasource,\n      module: '',\n      baseUrl: '',\n      info: {\n        description: '',\n        screenshots: [],\n        updated: '',\n        version: '',\n        logos: {\n          small: '',\n          large: '',\n        },\n        author: {\n          name: '',\n        },\n        links: [],\n      },\n    },\n  });\n\n  mockDatasource.adHocFiltersStatus = 1; // most tests should skip checking the CH version. We will set ad hoc filters to enabled to avoid running the CH version check\n  return mockDatasource;\n};\n\nexport const mockDatasource = newMockDatasource();\n\nexport const mockQuery: CHQuery = {\n  pluginVersion: '',\n  rawSql: 'select * from foo',\n  refId: '',\n  editorType: EditorType.SQL,\n  queryType: QueryType.Table,\n};\n"
  },
  {
    "path": "src/ch-parser/helpers.ts",
    "content": "/**\n * Helper functions for character classification and string handling\n */\n\n/**\n * Check if a character is a whitespace ASCII character\n */\nexport function isWhitespaceASCII(c: string): boolean {\n  return c === ' ' || c === '\\t' || c === '\\n' || c === '\\r' || c === '\\f' || c === '\\v';\n}\n\n/**\n * Check if a character is a numeric ASCII character\n */\nexport function isNumericASCII(c: string): boolean {\n  return c >= '0' && c <= '9';\n}\n\n/**\n * Check if a character is a word character (letter, digit, or underscore)\n */\nexport function isWordCharASCII(c: string): boolean {\n  return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c === '_';\n}\n\n/**\n * Check if a character is a hexadecimal digit\n */\nexport function isHexDigit(c: string): boolean {\n  return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');\n}\n\n/**\n * Check if a character is a valid number separator, like underscore in 1_000_000\n */\nexport function isNumberSeparator(startOfBlock: boolean, hex: boolean, pos: number, text: string): boolean {\n  if (startOfBlock) {\n    return false;\n  }\n\n  if (pos >= text.length) {\n    return false;\n  }\n\n  if (text[pos] !== '_') {\n    return false;\n  }\n\n  if (pos + 1 >= text.length) {\n    return false;\n  }\n\n  if (hex) {\n    return isHexDigit(text[pos + 1]);\n  }\n\n  return isNumericASCII(text[pos + 1]);\n}\n\n/**\n * Find the first occurrence of any of the given characters\n */\nexport function findFirstSymbols(text: string, pos: number, end: number, ...symbols: string[]): number {\n  while (pos < end) {\n    if (symbols.includes(text[pos])) {\n      return pos;\n    }\n    pos++;\n  }\n  return end;\n}\n\n/**\n * Find the first character that is not any of the given characters\n */\nexport function findFirstNotSymbols(text: string, pos: number, end: number, ...symbols: string[]): number {\n  while (pos < end) {\n    if (!symbols.includes(text[pos])) {\n      return pos;\n    }\n    pos++;\n  }\n  return end;\n}\n\n/**\n * Skip UTF-8 whitespaces (including Unicode ones)\n */\nexport function skipWhitespacesUTF8(text: string, pos: number, end: number): number {\n  // Skip whitespace characters in Unicode\n  // This is a simplified version that just skips common Unicode whitespace\n  while (pos < end) {\n    const code = text.charCodeAt(pos);\n\n    // Skip ASCII whitespace\n    if (code <= 127 && isWhitespaceASCII(String.fromCharCode(code))) {\n      pos++;\n      continue;\n    }\n\n    // Skip some common Unicode whitespace\n    // U+00A0 - NO-BREAK SPACE\n    // U+2000 to U+200A - Various space characters\n    // U+2028 - LINE SEPARATOR\n    // U+2029 - PARAGRAPH SEPARATOR\n    // U+202F - NARROW NO-BREAK SPACE\n    // U+205F - MEDIUM MATHEMATICAL SPACE\n    // U+3000 - IDEOGRAPHIC SPACE\n    if (\n      code === 0x00a0 ||\n      (code >= 0x2000 && code <= 0x200a) ||\n      code === 0x2028 ||\n      code === 0x2029 ||\n      code === 0x202f ||\n      code === 0x205f ||\n      code === 0x3000\n    ) {\n      pos++;\n      continue;\n    }\n\n    break;\n  }\n\n  return pos;\n}\n\n/**\n * Check if a character is a UTF-8 continuation octet\n */\nexport function isContinuationOctet(c: string): boolean {\n  const code = c.charCodeAt(0);\n  return (code & 0xc0) === 0x80;\n}\n"
  },
  {
    "path": "src/ch-parser/lexer.ts",
    "content": "import { Token, TokenType } from './types';\nimport {\n  isWhitespaceASCII,\n  isNumericASCII,\n  isWordCharASCII,\n  isHexDigit,\n  isNumberSeparator,\n  findFirstNotSymbols,\n  skipWhitespacesUTF8,\n  isContinuationOctet,\n} from './helpers';\n\n/**\n * Lexer class for tokenizing input text\n */\nexport class Lexer {\n  private readonly text: string;\n  private pos: number;\n  private readonly end: number;\n  private readonly maxQuerySize: number;\n  private prevSignificantTokenType: TokenType = TokenType.Whitespace;\n\n  /**\n   * Create a new lexer for the given input\n   * @param text The input text to tokenize\n   * @param maxQuerySize Optional maximum query size (0 for unlimited)\n   */\n  public constructor(text: string, maxQuerySize = 0) {\n    this.text = text;\n    this.pos = 0;\n    this.end = text.length;\n    this.maxQuerySize = maxQuerySize;\n  }\n\n  /**\n   * Get the next token from the input\n   */\n  public nextToken(): Token {\n    const res = this.nextTokenImpl();\n    if (this.maxQuerySize && res.end > this.maxQuerySize) {\n      return new Token(\n        TokenType.ErrorMaxQuerySizeExceeded,\n        res.begin,\n        res.end,\n        this.text.substring(res.begin, res.end)\n      );\n    }\n    if (res.isSignificant()) {\n      this.prevSignificantTokenType = res.type;\n    }\n    return res;\n  }\n\n  /**\n   * Parse a quoted string\n   */\n  private parseQuotedString(quote: string, successToken: TokenType, errorToken: TokenType): Token {\n    const tokenBegin = this.pos;\n\n    // Skip opening quote\n    this.pos++;\n\n    while (this.pos < this.end) {\n      const nextQuotePos = this.text.indexOf(quote, this.pos);\n      const nextEscapePos = this.text.indexOf('\\\\', this.pos);\n\n      if (nextQuotePos === -1) {\n        // No closing quote found\n        this.pos = this.end;\n        return new Token(errorToken, tokenBegin, this.pos, this.text.substring(tokenBegin, this.pos));\n      }\n\n      if (nextEscapePos !== -1 && nextEscapePos < nextQuotePos) {\n        // Found escape character before quote\n        this.pos = nextEscapePos + 2; // Skip escape and the escaped character\n        continue;\n      }\n\n      // Found quote\n      this.pos = nextQuotePos + 1;\n\n      // Check for doubled quote which represents a single quote character\n      if (this.pos < this.end && this.text[this.pos] === quote) {\n        // Skip the second quote and continue searching\n        this.pos++;\n        continue;\n      }\n\n      // Found end of quoted string\n      return new Token(successToken, tokenBegin, this.pos, this.text.substring(tokenBegin, this.pos));\n    }\n\n    // Reached end of input without closing quote\n    return new Token(errorToken, tokenBegin, this.pos, this.text.substring(tokenBegin, this.pos));\n  }\n\n  /**\n   * Parse a quoted hex or binary string (x'AB' or b'101')\n   */\n  private parseQuotedHexOrBinString(): Token {\n    const tokenBegin = this.pos;\n    const leadChar = this.text[this.pos];\n    const isHex = leadChar === 'x' || leadChar === 'X';\n\n    // Skip 'x' and opening quote\n    this.pos += 2;\n\n    if (isHex) {\n      // Find the first non-hex digit\n      while (this.pos < this.end && isHexDigit(this.text[this.pos])) {\n        this.pos++;\n      }\n    } else {\n      // Find the first non-binary digit\n      this.pos = findFirstNotSymbols(this.text, this.pos, this.end, '0', '1');\n    }\n\n    if (this.pos >= this.end || this.text[this.pos] !== \"'\") {\n      this.pos = this.end;\n      return new Token(\n        TokenType.ErrorSingleQuoteIsNotClosed,\n        tokenBegin,\n        this.pos,\n        this.text.substring(tokenBegin, this.pos)\n      );\n    }\n\n    this.pos++; // Skip closing quote\n    return new Token(TokenType.StringLiteral, tokenBegin, this.pos, this.text.substring(tokenBegin, this.pos));\n  }\n\n  /**\n   * Handle comment until end of line\n   */\n  private commentUntilEndOfLine(): Token {\n    const tokenBegin = this.pos;\n    const newlinePos = this.text.indexOf('\\n', this.pos);\n\n    if (newlinePos === -1) {\n      this.pos = this.end;\n    } else {\n      this.pos = newlinePos;\n    }\n\n    return new Token(TokenType.Comment, tokenBegin, this.pos, this.text.substring(tokenBegin, this.pos));\n  }\n\n  /**\n   * Parse a Unicode-quoted string or identifier. The opening codepoint is at this.pos\n   * (about to be consumed by this function); closeChar is the single JS codepoint that\n   * terminates the literal. ClickHouse's C++ lexer does this at the UTF-8 byte level\n   * (looking for `E2 80 99` etc.); the JavaScript equivalent operates on BMP codepoints\n   * directly since JS strings are UTF-16 code units, not UTF-8 bytes.\n   */\n  private parseUnicodeQuotedString(closeChar: string, successToken: TokenType, errorToken: TokenType): Token {\n    const tokenBegin = this.pos;\n\n    // Skip the opening quote\n    this.pos++;\n\n    const closePos = this.text.indexOf(closeChar, this.pos);\n    if (closePos === -1) {\n      this.pos = this.end;\n      return new Token(errorToken, tokenBegin, this.pos, this.text.substring(tokenBegin, this.pos));\n    }\n\n    this.pos = closePos + 1;\n    return new Token(successToken, tokenBegin, this.pos, this.text.substring(tokenBegin, this.pos));\n  }\n\n  /**\n   * Implementation of nextToken that actually does the tokenization\n   */\n  private nextTokenImpl(): Token {\n    if (this.pos >= this.end) {\n      return new Token(TokenType.EndOfStream, this.end, this.end, '');\n    }\n\n    const tokenBegin = this.pos;\n    const currentChar = this.text[this.pos];\n\n    // Handle whitespace\n    if (isWhitespaceASCII(currentChar)) {\n      this.pos++;\n      while (this.pos < this.end && isWhitespaceASCII(this.text[this.pos])) {\n        this.pos++;\n      }\n      return new Token(TokenType.Whitespace, tokenBegin, this.pos, this.text.substring(tokenBegin, this.pos));\n    }\n\n    // Handle numbers\n    if (isNumericASCII(currentChar)) {\n      // For chained tuple access operators (x.1.1)\n      if (this.prevSignificantTokenType === TokenType.Dot) {\n        this.pos++;\n        while (\n          this.pos < this.end &&\n          (isNumericASCII(this.text[this.pos]) || isNumberSeparator(false, false, this.pos, this.text))\n        ) {\n          this.pos++;\n        }\n      } else {\n        let startOfBlock = false;\n        let hex = false;\n\n        // Check for hex (0x) or binary (0b) notation\n        const prefixChar = this.pos + 2 < this.end && currentChar === '0' ? this.text[this.pos + 1] : '';\n        const isHexPrefix = prefixChar === 'x' || prefixChar === 'X';\n        const isBinPrefix = prefixChar === 'b' || prefixChar === 'B';\n        if (isHexPrefix || isBinPrefix) {\n          let isValid = false;\n          if (isHexPrefix) {\n            if (this.pos + 2 < this.end && isHexDigit(this.text[this.pos + 2])) {\n              hex = true;\n              isValid = true; // hex\n            }\n          } else if (this.text[this.pos + 2] === '0' || this.text[this.pos + 2] === '1') {\n            isValid = true; // binary\n          }\n\n          if (isValid) {\n            this.pos += 2;\n            startOfBlock = true;\n          } else {\n            this.pos++; // consume the leading zero\n          }\n        } else {\n          this.pos++;\n        }\n\n        // Parse integer part\n        while (\n          this.pos < this.end &&\n          ((hex ? isHexDigit(this.text[this.pos]) : isNumericASCII(this.text[this.pos])) ||\n            isNumberSeparator(startOfBlock, hex, this.pos, this.text))\n        ) {\n          this.pos++;\n          startOfBlock = false;\n        }\n\n        // Check for decimal point\n        if (this.pos < this.end && this.text[this.pos] === '.') {\n          startOfBlock = true;\n          this.pos++;\n\n          // Parse fractional part\n          while (\n            this.pos < this.end &&\n            ((hex ? isHexDigit(this.text[this.pos]) : isNumericASCII(this.text[this.pos])) ||\n              isNumberSeparator(startOfBlock, hex, this.pos, this.text))\n          ) {\n            this.pos++;\n            startOfBlock = false;\n          }\n        }\n\n        // Check for exponent\n        if (\n          this.pos + 1 < this.end &&\n          (hex\n            ? this.text[this.pos] === 'p' || this.text[this.pos] === 'P'\n            : this.text[this.pos] === 'e' || this.text[this.pos] === 'E')\n        ) {\n          startOfBlock = true;\n          this.pos++;\n\n          // Check for sign of exponent\n          if (this.pos + 1 < this.end && (this.text[this.pos] === '-' || this.text[this.pos] === '+')) {\n            this.pos++;\n          }\n\n          // Parse exponent\n          while (\n            this.pos < this.end &&\n            (isNumericASCII(this.text[this.pos]) || isNumberSeparator(startOfBlock, false, this.pos, this.text))\n          ) {\n            this.pos++;\n            startOfBlock = false;\n          }\n        }\n      }\n\n      // Check if this is actually a numeric identifier (1identifier)\n      if (this.pos < this.end && isWordCharASCII(this.text[this.pos])) {\n        this.pos++;\n        while (this.pos < this.end && isWordCharASCII(this.text[this.pos])) {\n          this.pos++;\n        }\n\n        // Check if it's a valid identifier or an error\n        for (let i = tokenBegin; i < this.pos; i++) {\n          if (!isWordCharASCII(this.text[i]) && this.text[i] !== '$') {\n            return new Token(\n              TokenType.ErrorWrongNumber,\n              tokenBegin,\n              this.pos,\n              this.text.substring(tokenBegin, this.pos)\n            );\n          }\n        }\n\n        return new Token(TokenType.BareWord, tokenBegin, this.pos, this.text.substring(tokenBegin, this.pos));\n      }\n\n      return new Token(TokenType.Number, tokenBegin, this.pos, this.text.substring(tokenBegin, this.pos));\n    }\n\n    // Handle quoted strings\n    switch (currentChar) {\n      case \"'\":\n        return this.parseQuotedString(\"'\", TokenType.StringLiteral, TokenType.ErrorSingleQuoteIsNotClosed);\n      case '\"':\n        return this.parseQuotedString('\"', TokenType.QuotedIdentifier, TokenType.ErrorDoubleQuoteIsNotClosed);\n      case '`':\n        return this.parseQuotedString('`', TokenType.QuotedIdentifier, TokenType.ErrorBackQuoteIsNotClosed);\n\n      // Handle brackets\n      case '(':\n        return new Token(TokenType.OpeningRoundBracket, tokenBegin, ++this.pos, '(');\n      case ')':\n        return new Token(TokenType.ClosingRoundBracket, tokenBegin, ++this.pos, ')');\n      case '[':\n        return new Token(TokenType.OpeningSquareBracket, tokenBegin, ++this.pos, '[');\n      case ']':\n        return new Token(TokenType.ClosingSquareBracket, tokenBegin, ++this.pos, ']');\n      case '{':\n        return new Token(TokenType.OpeningCurlyBrace, tokenBegin, ++this.pos, '{');\n      case '}':\n        return new Token(TokenType.ClosingCurlyBrace, tokenBegin, ++this.pos, '}');\n\n      // Handle simple punctuation\n      case ',':\n        return new Token(TokenType.Comma, tokenBegin, ++this.pos, ',');\n      case ';':\n        return new Token(TokenType.Semicolon, tokenBegin, ++this.pos, ';');\n\n      // Handle dot (qualifier, tuple access operator or start of floating point number)\n      case '.': {\n        // Check if dot follows an identifier, complex expression or number\n        if (\n          this.pos > 0 &&\n          (!(this.pos + 1 < this.end && isNumericASCII(this.text[this.pos + 1])) ||\n            this.prevSignificantTokenType === TokenType.ClosingRoundBracket ||\n            this.prevSignificantTokenType === TokenType.ClosingSquareBracket ||\n            this.prevSignificantTokenType === TokenType.BareWord ||\n            this.prevSignificantTokenType === TokenType.QuotedIdentifier ||\n            this.prevSignificantTokenType === TokenType.Number)\n        ) {\n          return new Token(TokenType.Dot, tokenBegin, ++this.pos, '.');\n        }\n\n        // Otherwise it's a number with fractional part but no integer part\n        let startOfBlock = true;\n        this.pos++;\n\n        while (\n          this.pos < this.end &&\n          (isNumericASCII(this.text[this.pos]) || isNumberSeparator(startOfBlock, false, this.pos, this.text))\n        ) {\n          this.pos++;\n          startOfBlock = false;\n        }\n\n        // Check for exponent\n        if (this.pos + 1 < this.end && (this.text[this.pos] === 'e' || this.text[this.pos] === 'E')) {\n          startOfBlock = true;\n          this.pos++;\n\n          // Check for sign of exponent\n          if (this.pos + 1 < this.end && (this.text[this.pos] === '-' || this.text[this.pos] === '+')) {\n            this.pos++;\n          }\n\n          // Parse exponent\n          while (\n            this.pos < this.end &&\n            (isNumericASCII(this.text[this.pos]) || isNumberSeparator(startOfBlock, false, this.pos, this.text))\n          ) {\n            this.pos++;\n            startOfBlock = false;\n          }\n        }\n\n        return new Token(TokenType.Number, tokenBegin, this.pos, this.text.substring(tokenBegin, this.pos));\n      }\n\n      // Handle operators\n      case '+':\n        return new Token(TokenType.Plus, tokenBegin, ++this.pos, '+');\n\n      case '-': {\n        this.pos++;\n\n        // Check for arrow operator\n        if (this.pos < this.end && this.text[this.pos] === '>') {\n          return new Token(TokenType.Arrow, tokenBegin, ++this.pos, '->');\n        }\n\n        // Check for comment\n        if (this.pos < this.end && this.text[this.pos] === '-') {\n          this.pos++;\n          return this.commentUntilEndOfLine();\n        }\n\n        return new Token(TokenType.Minus, tokenBegin, this.pos, '-');\n      }\n\n      case '*':\n        return new Token(TokenType.Asterisk, tokenBegin, ++this.pos, '*');\n\n      case '/': {\n        this.pos++;\n\n        // Check for comment\n        if (this.pos < this.end) {\n          if (this.text[this.pos] === '/') {\n            this.pos++;\n            return this.commentUntilEndOfLine();\n          }\n\n          if (this.text[this.pos] === '*') {\n            this.pos++;\n            let nestingLevel = 1;\n\n            while (this.pos + 1 < this.end) {\n              if (this.text[this.pos] === '/' && this.text[this.pos + 1] === '*') {\n                this.pos += 2;\n                nestingLevel++;\n              } else if (this.text[this.pos] === '*' && this.text[this.pos + 1] === '/') {\n                this.pos += 2;\n                nestingLevel--;\n\n                if (nestingLevel === 0) {\n                  return new Token(TokenType.Comment, tokenBegin, this.pos, this.text.substring(tokenBegin, this.pos));\n                }\n              } else {\n                this.pos++;\n              }\n            }\n\n            this.pos = this.end;\n            return new Token(\n              TokenType.ErrorMultilineCommentIsNotClosed,\n              tokenBegin,\n              this.pos,\n              this.text.substring(tokenBegin, this.pos)\n            );\n          }\n        }\n\n        return new Token(TokenType.Slash, tokenBegin, this.pos, '/');\n      }\n\n      case '#': {\n        this.pos++;\n\n        // Comments only if followed by space or '!'\n        if (this.pos < this.end && (this.text[this.pos] === ' ' || this.text[this.pos] === '!')) {\n          return this.commentUntilEndOfLine();\n        }\n\n        return new Token(TokenType.Error, tokenBegin, this.pos, this.text.substring(tokenBegin, this.pos));\n      }\n\n      case '%':\n        return new Token(TokenType.Percent, tokenBegin, ++this.pos, '%');\n\n      case '=': {\n        this.pos++;\n\n        // Check for == operator\n        if (this.pos < this.end && this.text[this.pos] === '=') {\n          this.pos++;\n        }\n\n        return new Token(TokenType.Equals, tokenBegin, this.pos, this.text.substring(tokenBegin, this.pos));\n      }\n\n      case '!': {\n        this.pos++;\n\n        // Only valid as != operator\n        if (this.pos < this.end && this.text[this.pos] === '=') {\n          return new Token(TokenType.NotEquals, tokenBegin, ++this.pos, '!=');\n        }\n\n        return new Token(TokenType.ErrorSingleExclamationMark, tokenBegin, this.pos, '!');\n      }\n\n      case '<': {\n        this.pos++;\n\n        // Check for <=>, <=, <>\n        if (this.pos + 1 < this.end && this.text[this.pos] === '=' && this.text[this.pos + 1] === '>') {\n          this.pos += 2;\n          return new Token(TokenType.Spaceship, tokenBegin, this.pos, '<=>');\n        }\n\n        if (this.pos < this.end && this.text[this.pos] === '=') {\n          return new Token(TokenType.LessOrEquals, tokenBegin, ++this.pos, '<=');\n        }\n\n        if (this.pos < this.end && this.text[this.pos] === '>') {\n          return new Token(TokenType.NotEquals, tokenBegin, ++this.pos, '<>');\n        }\n\n        return new Token(TokenType.Less, tokenBegin, this.pos, '<');\n      }\n\n      case '>': {\n        this.pos++;\n\n        // Check for >= operator\n        if (this.pos < this.end && this.text[this.pos] === '=') {\n          return new Token(TokenType.GreaterOrEquals, tokenBegin, ++this.pos, '>=');\n        }\n\n        return new Token(TokenType.Greater, tokenBegin, this.pos, '>');\n      }\n\n      case '?':\n        return new Token(TokenType.QuestionMark, tokenBegin, ++this.pos, '?');\n\n      case '^':\n        return new Token(TokenType.Caret, tokenBegin, ++this.pos, '^');\n\n      case ':': {\n        this.pos++;\n\n        // Check for :: operator\n        if (this.pos < this.end && this.text[this.pos] === ':') {\n          return new Token(TokenType.DoubleColon, tokenBegin, ++this.pos, '::');\n        }\n\n        return new Token(TokenType.Colon, tokenBegin, this.pos, ':');\n      }\n\n      case '|': {\n        this.pos++;\n\n        // Check for || operator (concatenation)\n        if (this.pos < this.end && this.text[this.pos] === '|') {\n          return new Token(TokenType.Concatenation, tokenBegin, ++this.pos, '||');\n        }\n\n        return new Token(TokenType.PipeMark, tokenBegin, this.pos, '|');\n      }\n\n      case '@': {\n        this.pos++;\n\n        // Check for @@ operator\n        if (this.pos < this.end && this.text[this.pos] === '@') {\n          return new Token(TokenType.DoubleAt, tokenBegin, ++this.pos, '@@');\n        }\n\n        return new Token(TokenType.At, tokenBegin, this.pos, '@');\n      }\n\n      case '\\\\': {\n        this.pos++;\n\n        // Check for \\G vertical delimiter\n        if (this.pos < this.end && this.text[this.pos] === 'G') {\n          return new Token(TokenType.VerticalDelimiter, tokenBegin, ++this.pos, '\\\\G');\n        }\n\n        return new Token(TokenType.Error, tokenBegin, this.pos, '\\\\');\n      }\n\n      // Unicode special cases. ClickHouse's C++ lexer matches these via UTF-8 byte\n      // triples (E2 88 92, E2 80 98, E2 80 9C); JavaScript strings are UTF-16 code\n      // units, so we match the codepoints directly.\n\n      // U+2212 MINUS SIGN — treated as a regular minus operator.\n      case '\\u2212':\n        return new Token(TokenType.Minus, tokenBegin, ++this.pos, '\\u2212');\n\n      // U+2018 LEFT SINGLE QUOTATION MARK — opens a StringLiteral,\n      // closed by U+2019 RIGHT SINGLE QUOTATION MARK.\n      case '\\u2018':\n        return this.parseUnicodeQuotedString(\n          '\\u2019',\n          TokenType.StringLiteral,\n          TokenType.ErrorSingleQuoteIsNotClosed\n        );\n\n      // U+201C LEFT DOUBLE QUOTATION MARK — opens a QuotedIdentifier,\n      // closed by U+201D RIGHT DOUBLE QUOTATION MARK.\n      case '\\u201C':\n        return this.parseUnicodeQuotedString(\n          '\\u201D',\n          TokenType.QuotedIdentifier,\n          TokenType.ErrorDoubleQuoteIsNotClosed\n        );\n    }\n\n    // Handle special cases\n\n    // Dollar sign and here-document\n    if (currentChar === '$') {\n      // Try to parse here-doc ($tag$...$tag$). ClickHouse requires the tag to consist\n      // solely of ASCII word characters (letters, digits, underscore). An empty tag\n      // ($$...$$) is valid by vacuous truth. We scan using absolute positions in\n      // this.text rather than taking a substring of the remaining input, which\n      // would allocate O(n) per $ — a real hot path in queries with many Grafana\n      // macros ($__timeFilter, ${variable}, ...).\n      const tagEndPos = this.text.indexOf('$', this.pos + 1);\n\n      if (tagEndPos !== -1) {\n        let tagIsValid = true;\n        for (let i = this.pos + 1; i < tagEndPos; i++) {\n          if (!isWordCharASCII(this.text[i])) {\n            tagIsValid = false;\n            break;\n          }\n        }\n\n        if (tagIsValid) {\n          const heredocSize = tagEndPos - this.pos + 1;\n          const heredoc = this.text.substring(this.pos, this.pos + heredocSize);\n\n          const heredocEndPos = this.text.indexOf(heredoc, this.pos + heredocSize);\n          if (heredocEndPos !== -1) {\n            this.pos = heredocEndPos + heredocSize;\n            return new Token(TokenType.HereDoc, tokenBegin, this.pos, this.text.substring(tokenBegin, this.pos));\n          }\n        }\n      }\n\n      // Standalone dollar sign\n      if ((this.pos + 1 < this.end && !isWordCharASCII(this.text[this.pos + 1])) || this.pos + 1 === this.end) {\n        return new Token(TokenType.DollarSign, tokenBegin, ++this.pos, '$');\n      }\n    }\n\n    // Hex or binary string literals (x'AB' / X'AB' / b'101' / B'101')\n    if (\n      this.pos + 2 < this.end &&\n      this.text[this.pos + 1] === \"'\" &&\n      (currentChar === 'x' || currentChar === 'X' || currentChar === 'b' || currentChar === 'B')\n    ) {\n      return this.parseQuotedHexOrBinString();\n    }\n\n    // Bare words (identifiers or keywords)\n    if (isWordCharASCII(currentChar) || currentChar === '$') {\n      this.pos++;\n      while (this.pos < this.end && (isWordCharASCII(this.text[this.pos]) || this.text[this.pos] === '$')) {\n        this.pos++;\n      }\n      return new Token(TokenType.BareWord, tokenBegin, this.pos, this.text.substring(tokenBegin, this.pos));\n    }\n\n    // Try to skip Unicode whitespace\n    const newPos = skipWhitespacesUTF8(this.text, this.pos, this.end);\n    if (newPos > this.pos) {\n      this.pos = newPos;\n      return new Token(TokenType.Whitespace, tokenBegin, this.pos, this.text.substring(tokenBegin, this.pos));\n    }\n\n    // Skip over any UTF-8 continuation bytes\n    this.pos++;\n    while (this.pos < this.end && isContinuationOctet(this.text[this.pos])) {\n      this.pos++;\n    }\n\n    return new Token(TokenType.Error, tokenBegin, this.pos, this.text.substring(tokenBegin, this.pos));\n  }\n}\n"
  },
  {
    "path": "src/ch-parser/parser.ts",
    "content": "import { Token, TokenType } from './types';\n\nexport class QueryNodeParser {\n  private tokens: Token[];\n  private offset: number;\n\n  constructor(tokens: Token[]) {\n    this.tokens = tokens;\n    this.offset = 0;\n  }\n\n  public advance() {\n    this.offset++;\n  }\n\n  public hasNext(): boolean {\n    return this.offset < this.tokens.length;\n  }\n\n  public next(): Token {\n    const token = this.tokens[this.offset];\n    this.advance();\n\n    return token;\n  }\n\n  public peek(): Token {\n    return this.tokens[this.offset];\n  }\n\n  public nextIs(type: TokenType): boolean {\n    const next = this.peek();\n    if (next.type === type) {\n      this.advance();\n      return true;\n    }\n\n    return false;\n  }\n\n  public peekIs(type: TokenType): boolean {\n    return this.peek().type === type;\n  }\n}\n\nexport enum ClauseType {\n  None,\n  With,\n  Select,\n  From,\n  Join,\n  Where,\n  GroupBy,\n  Having,\n  OrderBy,\n  Limit,\n  Identifier,\n}\n\nexport enum QueryNodeType {\n  Default,\n  Select,\n  From,\n  Identifier,\n}\n\nexport interface QueryNode {\n  type: QueryNodeType;\n  token: Token;\n  clause: ClauseType;\n  children?: QueryNode[];\n}\n\nexport interface FromQueryNode extends QueryNode {\n  token: Token;\n  database?: string;\n  table?: string;\n  prefix?: string;\n}\n\nexport interface IdentifierQueryNode extends QueryNode {\n  prefix?: string;\n}\n\nexport interface SelectQueryNode extends QueryNode {\n  from?: FromQueryNode;\n}\n\nexport function parseSelectQueryNode(parser: QueryNodeParser): SelectQueryNode | null {\n  if (!parser.hasNext()) {\n    return null;\n  }\n\n  const firstToken = parser.peek();\n  const node: SelectQueryNode = {\n    type: QueryNodeType.Select,\n    clause: ClauseType.Select,\n    children: [],\n    token: null!,\n  };\n\n  if (firstToken.matchKeyword('WITH')) {\n    node.children!.push({ type: QueryNodeType.Default, token: firstToken, clause: ClauseType.With });\n  } else if (firstToken.matchKeyword('SELECT')) {\n    node.token = firstToken;\n  } else {\n    return null;\n  }\n  parser.advance();\n\n  let parenDepth = 0;\n  let endOfNode = false;\n  while (!endOfNode && parser.hasNext()) {\n    const token = parser.next();\n\n    if (token.matchKeyword('SELECT')) {\n      node.token = token;\n    } else if (token.matchKeyword('FROM') || token.matchKeyword('JOIN')) {\n      const fromNode: FromQueryNode = { type: QueryNodeType.From, token, clause: ClauseType.From };\n      node.children!.push(fromNode);\n      if (!node.from) {\n        node.from = fromNode;\n      }\n\n      if (parser.hasNext() && parser.peek().isError()) {\n        fromNode.prefix = parser.peek().text;\n      }\n\n      if (\n        parser.hasNext() &&\n        ((parser.peek().type === TokenType.BareWord && !parser.peek().isKeyword()) ||\n          parser.peek().type === TokenType.QuotedIdentifier)\n      ) {\n        const databaseOrTable = parser.next().text;\n        if (parser.hasNext() && parser.peek().type === TokenType.Dot) {\n          parser.next();\n          fromNode.database = databaseOrTable;\n\n          if (parser.hasNext() && parser.peek().isError()) {\n            fromNode.prefix = parser.peek().text;\n          }\n\n          if (\n            parser.hasNext() &&\n            ((parser.peek().type === TokenType.BareWord && !parser.peek().isKeyword()) ||\n              parser.peek().type === TokenType.QuotedIdentifier)\n          ) {\n            fromNode.table = parser.next().text;\n          }\n        } else {\n          fromNode.table = databaseOrTable;\n        }\n      }\n    } else if (token.type === TokenType.OpeningRoundBracket) {\n      const nestedNode = parseSelectQueryNode(parser);\n      if (nestedNode === null) {\n        parenDepth++;\n      } else {\n        node.children!.push(nestedNode);\n      }\n    } else if (token.type === TokenType.ClosingRoundBracket) {\n      if (parenDepth === 0) {\n        endOfNode = true;\n      } else {\n        parenDepth--;\n      }\n    } else if (token.matchKeyword('JOIN')) {\n      node.children!.push({ type: QueryNodeType.Default, token, clause: ClauseType.Join });\n    } else if (token.matchKeyword('GROUP') && parser.hasNext() && parser.peek().matchKeyword('BY')) {\n      node.children!.push({ type: QueryNodeType.Default, token: parser.next(), clause: ClauseType.GroupBy });\n    } else if (token.matchKeyword('WHERE')) {\n      node.children!.push({ type: QueryNodeType.Default, token, clause: ClauseType.Where });\n    } else if (token.matchKeyword('HAVING')) {\n      node.children!.push({ type: QueryNodeType.Default, token, clause: ClauseType.Having });\n    } else if (token.matchKeyword('ORDER') && parser.hasNext() && parser.peek().matchKeyword('BY')) {\n      node.children!.push({ type: QueryNodeType.Default, token: parser.next(), clause: ClauseType.OrderBy });\n    } else if (token.matchKeyword('LIMIT')) {\n      node.children!.push({ type: QueryNodeType.Default, token, clause: ClauseType.Limit });\n    } else if (token.type === TokenType.BareWord && !token.isKeyword()) {\n      let fullIdent = token.text;\n      let identToken = token;\n      while (\n        parser.hasNext() &&\n        (parser.peekIs(TokenType.Dot) || (parser.peekIs(TokenType.BareWord) && !parser.peek().isKeyword()))\n      ) {\n        identToken = parser.next();\n        fullIdent += identToken.text;\n      }\n      node.children!.push({\n        type: QueryNodeType.Identifier,\n        token: identToken,\n        prefix: fullIdent,\n        clause: ClauseType.Identifier,\n      } as IdentifierQueryNode);\n    } else if (token.type === TokenType.DollarSign) {\n      node.children!.push({\n        type: QueryNodeType.Identifier,\n        token,\n        prefix: '$',\n        clause: ClauseType.Identifier,\n      } as IdentifierQueryNode);\n    } else {\n      node.children!.push({ type: QueryNodeType.Default, token, clause: ClauseType.None });\n    }\n  }\n\n  return node;\n}\n"
  },
  {
    "path": "src/ch-parser/pluginMacros.ts",
    "content": "export interface PluginMacro {\n  name: string;\n  isFunction: boolean;\n  columnType?: string;\n  documentation: string;\n  example?: string;\n}\n\n// Taken from README/docs\nexport const pluginMacros: PluginMacro[] = [\n  {\n    name: '$__dateFilter',\n    isFunction: true,\n    documentation: 'Filters the data based on the date range of the panel',\n    example: \"date >= toDate('2022-10-21') AND date <= toDate('2022-10-23')\",\n  },\n  {\n    name: '$__timeFilter',\n    isFunction: true,\n    documentation: 'Filters the data based on the time range of the panel in seconds',\n    example: 'time >= toDateTime(1415792726) AND time <= toDateTime(1447328726)',\n  },\n  {\n    name: '$__timeFilter_ms',\n    isFunction: true,\n    documentation: 'Filters the data based on the time range of the panel in milliseconds',\n    example: 'time >= fromUnixTimestamp64Milli(1415792726123) AND time <= fromUnixTimestamp64Milli(1447328726456)',\n  },\n  {\n    name: '$__dateTimeFilter',\n    isFunction: true,\n    documentation:\n      'Shorthand that combines $__dateFilter() AND $__timeFilter() using separate Date and DateTime columns',\n    example: '$__dateFilter(dateColumn) AND $__timeFilter(timeColumn)',\n  },\n  {\n    name: '$__fromTime',\n    isFunction: false,\n    columnType: 'DateTime',\n    documentation: 'Replaced by the starting time of the range of the panel casted to DateTime',\n    example: 'toDateTime(1415792726)',\n  },\n  {\n    name: '$__toTime',\n    isFunction: false,\n    columnType: 'DateTime',\n    documentation: 'Replaced by the ending time of the range of the panel casted to DateTime',\n    example: 'toDateTime(1447328726)',\n  },\n  {\n    name: '$__fromTime_ms',\n    isFunction: false,\n    columnType: 'DateTime64(3)',\n    documentation: 'Replaced by the starting time of the range of the panel casted to DateTime64(3)',\n    example: 'fromUnixTimestamp64Milli(1415792726123)',\n  },\n  {\n    name: '$__toTime_ms',\n    isFunction: false,\n    columnType: 'Datetime64(3)',\n    documentation: 'Replaced by the ending time of the range of the panel casted to DateTime64(3)',\n    example: 'fromUnixTimestamp64Milli(1447328726456)',\n  },\n  {\n    name: '$__interval_s',\n    isFunction: false,\n    columnType: 'INTERVAL',\n    documentation: 'Replaced by the interval in seconds',\n    example: '20',\n  },\n  {\n    name: '$__timeInterval',\n    isFunction: true,\n    columnType: 'DateTime',\n    documentation:\n      'Replaced by a function calculating the interval based on window size in seconds, useful when grouping',\n    example: 'toStartOfInterval(toDateTime(column), INTERVAL 20 second)',\n  },\n  {\n    name: '$__timeInterval_ms',\n    isFunction: true,\n    columnType: 'DateTime64(3)',\n    documentation:\n      'Replaced by a function calculating the interval based on window size in milliseconds, useful when grouping',\n    example: 'toStartOfInterval(toDateTime64(column, 3), INTERVAL 20 millisecond)',\n  },\n  {\n    name: '$__conditionalAll',\n    isFunction: true,\n    columnType: 'Condition',\n    documentation:\n      'Replaced by the first parameter when the template variable in the second parameter does not select every value. Replaced by 1=1 when the template variable selects every value',\n    example: 'condition or 1=1',\n  },\n  {\n    name: '$__adHocFilters',\n    isFunction: true,\n    documentation:\n      'Manually applies ad-hoc filters to specific table(s). Useful for complex queries where automatic filter detection fails. Supports multiple tables by passing comma-separated table names. Use in SETTINGS clause to specify the target table(s) for ad-hoc filters',\n    example: \"additional_table_filters={'table1': 'column = \\\\'value\\\\'', 'table2': 'column = \\\\'value\\\\''} (for multiple tables)\",\n  },\n];\n"
  },
  {
    "path": "src/ch-parser/types.ts",
    "content": "/**\n * Enum for all token types supported by the lexer\n */\nexport enum TokenType {\n  Whitespace,\n  Comment,\n\n  BareWord, // Either keyword (SELECT) or identifier (column)\n\n  Number, // Always non-negative. No leading plus. 123 or something like 123.456e12, 0x123p12\n  StringLiteral, // 'hello word', 'hello''word', 'hello\\'word\\\\'\n\n  QuotedIdentifier, // \"x\", `x`\n\n  OpeningRoundBracket,\n  ClosingRoundBracket,\n\n  OpeningSquareBracket,\n  ClosingSquareBracket,\n\n  OpeningCurlyBrace,\n  ClosingCurlyBrace,\n\n  Comma,\n  Semicolon,\n  VerticalDelimiter, // Vertical delimiter \\G\n  Dot, // Compound identifiers, like a.b or tuple access operator a.1, (x, y).2.\n  // Need to be distinguished from floating point number with omitted integer part: .1\n\n  Asterisk, // Could be used as multiplication operator or on it's own: \"SELECT *\"\n\n  HereDoc,\n\n  DollarSign,\n  Plus,\n  Minus,\n  Slash,\n  Percent,\n  Arrow, // ->. Should be distinguished from minus operator.\n  QuestionMark,\n  Colon,\n  Caret,\n  DoubleColon,\n  Equals,\n  NotEquals,\n  Less,\n  Greater,\n  LessOrEquals,\n  GreaterOrEquals,\n  Spaceship, // <=>. Used in MySQL for NULL-safe equality comparison.\n  PipeMark,\n  Concatenation, // String concatenation operator: ||\n\n  At, // @. Used for specifying user names and also for MySQL-style variables.\n  DoubleAt, // @@. Used for MySQL-style global variables.\n\n  // Order is important. EndOfStream goes after all usual tokens, and special error tokens goes after EndOfStream.\n\n  EndOfStream,\n\n  // Something unrecognized.\n  Error,\n  // Something is wrong and we have more information.\n  ErrorMultilineCommentIsNotClosed,\n  ErrorSingleQuoteIsNotClosed,\n  ErrorDoubleQuoteIsNotClosed,\n  ErrorBackQuoteIsNotClosed,\n  ErrorSingleExclamationMark,\n  ErrorSinglePipeMark,\n  ErrorWrongNumber,\n  ErrorMaxQuerySizeExceeded,\n}\n\nexport const keywords = new Set([\n  // Standard SQL clauses\n  'SELECT',\n  'FROM',\n  'WHERE',\n  'GROUP',\n  'BY',\n  'HAVING',\n  'ORDER',\n  'LIMIT',\n  'OFFSET',\n  'JOIN',\n  'INNER',\n  'OUTER',\n  'LEFT',\n  'RIGHT',\n  'FULL',\n  'CROSS',\n  'ON',\n  'USING',\n  'AS',\n  'WITH',\n  'UNION',\n  'ALL',\n  'DISTINCT',\n  'CASE',\n  'WHEN',\n  'THEN',\n  'ELSE',\n  'END',\n  'AND',\n  'OR',\n  'NOT',\n  'IN',\n  'EXISTS',\n  'BETWEEN',\n  'LIKE',\n  'IS',\n  'NULL',\n  'ASC',\n  'DESC',\n\n  // ClickHouse-specific SELECT clauses\n  'PREWHERE',\n  'FINAL',\n  'SAMPLE',\n  'SETTINGS',\n  'FORMAT',\n  'INTERVAL',\n\n  // JOIN modifiers\n  'ASOF',\n  'ANY',\n  'ANTI',\n  'SEMI',\n  'GLOBAL',\n  'LATERAL',\n  'ARRAY',\n\n  // CTE\n  'RECURSIVE',\n\n  // ORDER BY / LIMIT modifiers\n  'NULLS',\n  'FIRST',\n  'LAST',\n  'TIES',\n\n  // DML\n  'INSERT',\n  'INTO',\n  'VALUES',\n  'UPDATE',\n  'DELETE',\n  'SET',\n  'TRUNCATE',\n\n  // DDL\n  'CREATE',\n  'ALTER',\n  'DROP',\n  'RENAME',\n  'TABLE',\n  'DATABASE',\n  'VIEW',\n  'MATERIALIZED',\n  'INDEX',\n  'DICTIONARY',\n  'FUNCTION',\n  'TEMPORARY',\n  'IF',\n  'TO',\n\n  // Window functions\n  'OVER',\n  'WINDOW',\n  'PARTITION',\n  'PRECEDING',\n  'FOLLOWING',\n  'CURRENT',\n  'UNBOUNDED',\n  'RANGE',\n  'ROWS',\n]);\n\n/**\n * A token representing a lexical unit in the input\n */\nexport class Token {\n  type: TokenType;\n  begin: number;\n  end: number;\n  text: string;\n  private _upperText?: string;\n\n  constructor(type: TokenType, begin: number, end: number, text: string) {\n    this.type = type;\n    this.begin = begin;\n    this.end = end;\n    this.text = text;\n  }\n\n  /**\n   * Lazily-cached uppercase form of the token text, used for case-insensitive\n   * keyword comparisons. Avoids re-allocating an uppercase copy on every\n   * isKeyword/matchKeyword call from the parser's hot loops.\n   */\n  private upperText(): string {\n    return (this._upperText ??= this.text.toUpperCase());\n  }\n\n  size(): number {\n    return this.end - this.begin;\n  }\n\n  isSignificant(): boolean {\n    return this.type !== TokenType.Whitespace && this.type !== TokenType.Comment;\n  }\n\n  /**\n   * Returns true if this token is a BareWord matching the given keyword\n   * case-insensitively. Callers should pass the keyword in uppercase\n   * (all internal call sites already do, e.g. `matchKeyword('SELECT')`).\n   */\n  matchKeyword(keyword: string): boolean {\n    return this.type === TokenType.BareWord && this.upperText() === keyword;\n  }\n\n  isKeyword(): boolean {\n    return this.type === TokenType.BareWord && keywords.has(this.upperText());\n  }\n\n  isError(): boolean {\n    return this.type > TokenType.EndOfStream;\n  }\n\n  isEnd(): boolean {\n    return this.type === TokenType.EndOfStream;\n  }\n}\n\n/**\n * Get the name of a token type (for debugging)\n */\nexport function getTokenName(type: TokenType): string {\n  return TokenType[type];\n}\n\n/**\n * Get the description of an error token\n */\nexport function getErrorTokenDescription(type: TokenType): string {\n  switch (type) {\n    case TokenType.Error:\n      return 'Unrecognized token';\n    case TokenType.ErrorMultilineCommentIsNotClosed:\n      return 'Multiline comment is not closed';\n    case TokenType.ErrorSingleQuoteIsNotClosed:\n      return 'Single quoted string is not closed';\n    case TokenType.ErrorDoubleQuoteIsNotClosed:\n      return 'Double quoted string is not closed';\n    case TokenType.ErrorBackQuoteIsNotClosed:\n      return 'Back quoted string is not closed';\n    case TokenType.ErrorSingleExclamationMark:\n      return 'Exclamation mark can only occur in != operator';\n    case TokenType.ErrorSinglePipeMark:\n      return 'Pipe symbol could only occur in || operator';\n    case TokenType.ErrorWrongNumber:\n      return 'Wrong number';\n    case TokenType.ErrorMaxQuerySizeExceeded:\n      return 'Max query size exceeded (can be increased with the `max_query_size` setting)';\n    default:\n      return 'Not an error';\n  }\n}\n"
  },
  {
    "path": "src/components/Divider.tsx",
    "content": "import React from 'react';\nimport { Divider as GrafanaDivider, useTheme2 } from '@grafana/ui';\nimport { config } from '@grafana/runtime';\nimport { isVersionGtOrEq } from 'utils/version';\n\nexport function Divider() {\n  const theme = useTheme2();\n  return isVersionGtOrEq(config.buildInfo.version, '10.1.0') ? (\n    <GrafanaDivider />\n  ) : (\n    <div\n      style={{ borderTop: `1px solid ${theme.colors.border.weak}`, margin: theme.spacing(2, 0), width: '100%' }}\n    ></div>\n  );\n}\n"
  },
  {
    "path": "src/components/LogContextPanel.test.tsx",
    "content": "import React from 'react';\nimport { render } from '@testing-library/react';\nimport LogsContextPanel, { _testExports } from './LogsContextPanel';\nimport { Components } from 'selectors';\n\ndescribe('LogsContextPanel', () => {\n  it('shows an alert when no columns are matched', () => {\n    const result = render(<LogsContextPanel columns={[]} datasourceUid=\"test-uid\" />);\n    expect(result.getByTestId(Components.LogsContextPanel.alert)).toBeInTheDocument();\n  });\n\n  it('renders LogContextKey components for each column', () => {\n    const mockColumns = [\n      { name: 'host', value: '127.0.0.1' },\n      { name: 'service', value: 'test-api' },\n    ];\n\n    const result = render(<LogsContextPanel columns={mockColumns} datasourceUid=\"test-uid\" />);\n\n    expect(result.getAllByTestId(Components.LogsContextPanel.LogsContextKey)).toHaveLength(2);\n    expect(result.getByText('host')).toBeInTheDocument();\n    expect(result.getByText('127.0.0.1')).toBeInTheDocument();\n    expect(result.getByText('service')).toBeInTheDocument();\n    expect(result.getByText('test-api')).toBeInTheDocument();\n  });\n});\n\ndescribe('LogContextKey', () => {\n  const LogContextKey = _testExports.LogContextKey;\n\n  it('renders the expected keys', () => {\n    const props = {\n      name: 'testName',\n      value: 'testValue',\n      primaryColor: '#000',\n      primaryTextColor: '#aaa',\n      secondaryColor: '#111',\n      secondaryTextColor: '#bbb',\n    };\n\n    const result = render(<LogContextKey {...props} />);\n\n    expect(result.getByTestId(Components.LogsContextPanel.LogsContextKey)).toBeInTheDocument();\n    expect(result.getByText('testName')).toBeInTheDocument();\n    expect(result.getByText('testValue')).toBeInTheDocument();\n  });\n});\n\ndescribe('iconMatcher', () => {\n  const iconMatcher = _testExports.iconMatcher;\n\n  it('returns correct icons for different context names', () => {\n    expect(iconMatcher('database')).toBe('database');\n    expect(iconMatcher('???')).toBe('align-left');\n  });\n});\n"
  },
  {
    "path": "src/components/LogsContextPanel.tsx",
    "content": "import React from 'react';\nimport { Alert, Icon, IconName, Stack, useTheme2 } from '@grafana/ui';\nimport { css } from '@emotion/css';\nimport { LogContextColumn } from 'data/CHDatasource';\nimport { Components } from 'selectors';\n\nconst LogsContextPanelStyles = css`\n  display: flex;\n  justify-content: flex-start;\n  flex-wrap: wrap;\n  width: 100%;\n`;\n\ninterface LogContextPanelProps {\n  columns: LogContextColumn[];\n  datasourceUid: string;\n}\n\nconst LogsContextPanel = (props: LogContextPanelProps) => {\n  const { columns, datasourceUid } = props;\n  const theme = useTheme2();\n\n  if (!columns || columns.length === 0) {\n    return (\n      <Alert data-testid={Components.LogsContextPanel.alert} title=\"\" severity=\"warning\">\n        <Stack direction=\"column\">\n          <div>\n            {\n              'Unable to match any context columns. Make sure your query returns at least one log context column from your '\n            }\n            <a\n              style={{ textDecoration: 'underline' }}\n              href={`/connections/datasources/edit/${encodeURIComponent(datasourceUid)}#logs-config`}\n            >\n              ClickHouse Data Source settings\n            </a>\n          </div>\n        </Stack>\n      </Alert>\n    );\n  }\n\n  return (\n    <div className={LogsContextPanelStyles}>\n      {columns.map((p) => (\n        <LogContextKey\n          key={p.name}\n          name={p.name}\n          value={p.value}\n          primaryColor={theme.colors.secondary.main}\n          primaryTextColor={theme.colors.text.primary}\n          secondaryColor={theme.colors.background.secondary}\n          secondaryTextColor={theme.colors.info.text}\n        />\n      ))}\n    </div>\n  );\n};\n\n/**\n * Roughly match an icon with the context column name.\n */\nconst iconMatcher = (contextName: string): IconName => {\n  contextName = contextName.toLowerCase();\n\n  if (contextName === 'db' || contextName === 'database' || contextName.includes('data')) {\n    return 'database';\n  } else if (contextName.includes('service')) {\n    return 'building';\n  } else if (\n    contextName.includes('error') ||\n    contextName.includes('warn') ||\n    contextName.includes('critical') ||\n    contextName.includes('fatal')\n  ) {\n    return 'exclamation-triangle';\n  } else if (contextName.includes('user') || contextName.includes('admin')) {\n    return 'user';\n  } else if (contextName.includes('email')) {\n    return 'at';\n  } else if (contextName.includes('file')) {\n    return 'file-alt';\n  } else if (contextName.includes('bug')) {\n    return 'bug';\n  } else if (contextName.includes('search')) {\n    return 'search';\n  } else if (contextName.includes('tag')) {\n    return 'tag-alt';\n  } else if (contextName.includes('span') || contextName.includes('stack')) {\n    return 'brackets-curly';\n  }\n  if (contextName === 'host' || contextName === 'hostname' || contextName.includes('host')) {\n    return 'cloud';\n  }\n  if (contextName === 'url' || contextName.includes('url')) {\n    return 'link';\n  } else if (contextName.includes('container') || contextName.includes('pod')) {\n    return 'cube';\n  }\n\n  return 'align-left';\n};\n\ninterface LogContextKeyProps {\n  name: string;\n  value: string;\n  primaryColor: string;\n  primaryTextColor: string;\n  secondaryColor: string;\n  secondaryTextColor: string;\n}\n\nconst LogContextKey = (props: LogContextKeyProps) => {\n  const { name, value, primaryColor, primaryTextColor, secondaryColor, secondaryTextColor } = props;\n\n  const styles = {\n    container: css`\n      display: flex;\n      justify-content: center;\n      align-items: center;\n      margin: 0.25em;\n      color: ${primaryTextColor};\n    `,\n    containerLeft: css`\n      display: flex;\n      align-items: center;\n      background-color: ${primaryColor};\n      border-radius: 2px;\n      border-top-right-radius: 0;\n      border-bottom-right-radius: 0;\n\n      padding-top: 0.15em;\n      padding-bottom: 0.15em;\n      padding-left: 0.25em;\n      padding-right: 0.25em;\n    `,\n    contextName: css`\n      font-weight: bold;\n      padding-left: 0.25em;\n      user-select: all;\n    `,\n    contextValue: css`\n      background-color: ${secondaryColor};\n      color: ${secondaryTextColor};\n      border-radius: 2px;\n      border-top-left-radius: 0;\n      border-bottom-left-radius: 0;\n      user-select: all;\n      font-family: monospace;\n\n      padding-top: 0.15em;\n      padding-bottom: 0.15em;\n      padding-left: 0.25em;\n      padding-right: 0.25em;\n    `,\n  };\n\n  return (\n    <div className={styles.container} data-testid={Components.LogsContextPanel.LogsContextKey}>\n      <div className={styles.containerLeft}>\n        <Icon name={iconMatcher(name)} size=\"md\" />\n        <span className={styles.contextName}>{name}</span>\n      </div>\n      <span className={styles.contextValue}>{value}</span>\n    </div>\n  );\n};\n\nexport default LogsContextPanel;\n\nexport const _testExports = {\n  iconMatcher,\n  LogContextKey,\n};\n"
  },
  {
    "path": "src/components/QueryToolbox.tsx",
    "content": "import React, { useMemo } from 'react';\nimport { css } from '@emotion/css';\n\nimport { Icon, IconButton, Stack, Tooltip, useTheme2 } from '@grafana/ui';\n\ninterface QueryToolboxProps {\n  showTools?: boolean;\n  onFormatCode?: () => void;\n}\n\nexport function QueryToolbox({ showTools, onFormatCode }: QueryToolboxProps) {\n  const theme = useTheme2();\n\n  const styles = useMemo(() => {\n    return {\n      container: css({\n        border: `1px solid ${theme.colors.border.medium}`,\n        borderTop: 'none',\n        padding: theme.spacing(0.5, 0.5, 0.5, 0.5),\n        display: 'flex',\n        flexGrow: 1,\n        justifyContent: 'space-between',\n        fontSize: theme.typography.bodySmall.fontSize,\n      }),\n      error: css({\n        color: theme.colors.error.text,\n        fontSize: theme.typography.bodySmall.fontSize,\n        fontFamily: theme.typography.fontFamilyMonospace,\n      }),\n      valid: css({\n        color: theme.colors.success.text,\n      }),\n      info: css({\n        color: theme.colors.text.secondary,\n      }),\n      hint: css({\n        color: theme.colors.text.disabled,\n        whiteSpace: 'nowrap',\n        cursor: 'help',\n      }),\n    };\n  }, [theme]);\n\n  let style = {};\n\n  if (!showTools) {\n    style = { height: 0, padding: 0, visibility: 'hidden' };\n  }\n\n  return (\n    <div className={styles.container} style={style}>\n      {showTools && (\n        <div>\n          <Stack>\n            {onFormatCode && (\n              <IconButton\n                onClick={() => {\n                  onFormatCode();\n                }}\n                name=\"brackets-curly\"\n                size=\"xs\"\n                tooltip=\"Format query\"\n              />\n            )}\n            <Tooltip content=\"Hit CTRL/CMD+Return to run query\">\n              <Icon className={styles.hint} name=\"keyboard\" />\n            </Tooltip>\n          </Stack>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/SqlEditor.test.tsx",
    "content": "import React from 'react';\nimport { render, screen } from '@testing-library/react';\nimport '@testing-library/jest-dom';\nimport { SqlEditor } from './SqlEditor';\nimport * as ui from '@grafana/ui';\nimport { mockDatasource } from '__mocks__/datasource';\nimport { EditorType } from 'types/sql';\n\nconst mockCompletionDispose = jest.fn();\nconst mockFormattingDispose = jest.fn();\n\n// Mock the Monaco editor types\nconst mockMonaco = {\n  KeyMod: {\n    CtrlCmd: 2048,\n    Shift: 1024,\n  },\n  KeyCode: {\n    Enter: 3,\n  },\n  languages: {\n    getLanguages: () => [],\n    register: jest.fn(),\n    setMonarchTokensProvider: jest.fn(),\n    registerCompletionItemProvider: jest.fn(() => ({ dispose: mockCompletionDispose })),\n    registerDocumentFormattingEditProvider: jest.fn(() => ({ dispose: mockFormattingDispose })),\n  },\n};\n\n// Mock the editor instance with all required methods\nconst mockEditor = (value: string) => ({\n  getValue: () => value,\n  getDomNode: () => ({\n    style: { width: '100', height: '100' },\n  }),\n  getContentHeight: () => 100,\n  layout: jest.fn(),\n  onDidContentSizeChange: jest.fn(),\n  onKeyUp: jest.fn(),\n  addAction: jest.fn(),\n  getModel: () => ({\n    getValue: () => value,\n    onDidChangeContent: jest.fn(),\n  }),\n  updateOptions: jest.fn(),\n  setValue: jest.fn(),\n  dispose: jest.fn(),\n});\n\n// Add monaco to the window object as it's expected by sqlProvider\n(window as any).monaco = mockMonaco;\n\nlet mockEditorInstance: ReturnType<typeof mockEditor>;\n\njest.mock('@grafana/ui', () => ({\n  ...jest.requireActual<typeof ui>('@grafana/ui'),\n  CodeEditor: function CodeEditor({\n    onEditorDidMount,\n    onEditorWillUnmount,\n    value,\n  }: {\n    onEditorDidMount: any;\n    onEditorWillUnmount?: () => void;\n    value: string;\n  }) {\n    React.useEffect(() => {\n      if (onEditorDidMount) {\n        mockEditorInstance = mockEditor(value);\n        onEditorDidMount(mockEditorInstance, mockMonaco);\n      }\n      return () => {\n        onEditorWillUnmount?.();\n      };\n    }, [onEditorDidMount, onEditorWillUnmount, value]);\n\n    return <div data-testid=\"code-editor\">{value}</div>;\n  },\n}));\n\ndescribe('SQL Editor', () => {\n  beforeEach(() => {\n    // Reset the mock editor instance before each test\n    mockEditorInstance = undefined as any;\n    mockCompletionDispose.mockClear();\n    mockFormattingDispose.mockClear();\n    mockMonaco.languages.registerCompletionItemProvider.mockClear();\n    mockMonaco.languages.registerDocumentFormattingEditProvider.mockClear();\n  });\n\n  it('Should display sql in the editor', () => {\n    const rawSql = 'foo';\n    render(\n      <SqlEditor\n        query={{ pluginVersion: '', rawSql, refId: 'A', editorType: EditorType.SQL }}\n        onChange={jest.fn()}\n        onRunQuery={jest.fn()}\n        datasource={mockDatasource}\n      />\n    );\n    expect(screen.queryByText(rawSql)).toBeInTheDocument();\n  });\n\n  // This unit test checks that the shortcut was set and is associated with 'run-query'\n  it('Should have the run-query action with the Ctrl + Enter keybinding', () => {\n    const mockOnRunQuery = jest.fn();\n    const rawSql = 'SELECT 1';\n\n    render(\n      <SqlEditor\n        query={{ pluginVersion: '', rawSql, refId: 'A', editorType: EditorType.SQL }}\n        onChange={jest.fn()}\n        onRunQuery={mockOnRunQuery}\n        datasource={mockDatasource}\n      />\n    );\n\n    // Verify that we have a mock editor instance\n    expect(mockEditorInstance).toBeDefined();\n\n    // Get the addAction mock\n    // It expects the id, label, keybindings and to run on mockOnRunQuery\n    const addActionMock = mockEditorInstance.addAction as jest.Mock;\n\n    // Verify addAction was called\n    expect(addActionMock).toHaveBeenCalled();\n\n    // Get the registered action\n    const registeredAction = addActionMock.mock.calls[0][0];\n\n    // Verify it's the run-query action\n    expect(registeredAction.id).toBe('run-query');\n    expect(registeredAction.label).toBe('Run Query');\n\n    // Verify keybinding is correct\n    expect(registeredAction.keybindings).toEqual([mockMonaco.KeyMod.CtrlCmd | mockMonaco.KeyCode.Enter]);\n    expect(registeredAction.keybindings).not.toEqual([mockMonaco.KeyMod.Shift | mockMonaco.KeyCode.Enter]);\n\n    // Trigger the action\n    registeredAction.run(mockEditorInstance);\n\n    // Verify mockOnRunQuery was called once\n    expect(mockOnRunQuery).toHaveBeenCalledTimes(1);\n  });\n\n  it('disposes Monaco language providers when the editor unmounts', () => {\n    const rawSql = 'SELECT 1';\n\n    const { unmount } = render(\n      <SqlEditor\n        query={{ pluginVersion: '', rawSql, refId: 'A', editorType: EditorType.SQL }}\n        onChange={jest.fn()}\n        onRunQuery={jest.fn()}\n        datasource={mockDatasource}\n      />\n    );\n\n    expect(mockMonaco.languages.registerCompletionItemProvider).toHaveBeenCalledTimes(1);\n    expect(mockMonaco.languages.registerDocumentFormattingEditProvider).toHaveBeenCalledTimes(1);\n    expect(mockCompletionDispose).not.toHaveBeenCalled();\n    expect(mockFormattingDispose).not.toHaveBeenCalled();\n\n    unmount();\n\n    expect(mockCompletionDispose).toHaveBeenCalledTimes(1);\n    expect(mockFormattingDispose).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "src/components/SqlEditor.tsx",
    "content": "import React, { useRef } from 'react';\nimport { QueryEditorProps } from '@grafana/data';\nimport { CodeEditor, monacoTypes } from '@grafana/ui';\nimport { Datasource } from 'data/CHDatasource';\nimport { registerSQL, Range, Fetcher } from './sqlProvider';\nimport { CHConfig } from 'types/config';\nimport { CHQuery, EditorType, CHSqlQuery } from 'types/sql';\nimport { styles } from 'styles';\nimport { getSuggestions } from './suggestions';\nimport { validate } from 'data/validate';\nimport { mapQueryTypeToGrafanaFormat } from 'data/utils';\nimport { QueryType } from 'types/queryBuilder';\nimport { QueryTypeSwitcher } from 'components/queryBuilder/QueryTypeSwitcher';\nimport { pluginVersion } from 'utils/version';\nimport { useSchemaSuggestionsProvider } from 'hooks/useSchemaSuggestionsProvider';\nimport { QueryToolbox } from './QueryToolbox';\n\ntype SqlEditorProps = QueryEditorProps<Datasource, CHQuery, CHConfig>;\n\nfunction setupAutoSize(editor: monacoTypes.editor.IStandaloneCodeEditor) {\n  const container = editor.getDomNode();\n  const updateHeight = () => {\n    if (container) {\n      const contentHeight = Math.max(100, Math.min(1000, editor.getContentHeight()));\n      const width = parseInt(container.style.width, 10);\n      container.style.width = `${width}px`;\n      container.style.height = `${contentHeight}px`;\n      editor.layout({ width, height: contentHeight });\n    }\n  };\n  editor.onDidContentSizeChange(updateHeight);\n  updateHeight();\n}\n\nexport const SqlEditor = (props: SqlEditorProps) => {\n  const { query, onChange, datasource } = props;\n  const editorRef = useRef<monacoTypes.editor.IStandaloneCodeEditor | null>(null);\n  const disposeRegistrationRef = useRef<(() => void) | null>(null);\n  const sqlQuery = query as CHSqlQuery;\n  const queryType = sqlQuery.queryType || QueryType.Table;\n\n  const saveChanges = (changes: Partial<CHSqlQuery>) => {\n    onChange({\n      ...sqlQuery,\n      pluginVersion,\n      editorType: EditorType.SQL,\n      format: mapQueryTypeToGrafanaFormat(changes.queryType || queryType),\n      ...changes,\n    });\n  };\n\n  const schema = useSchemaSuggestionsProvider(datasource);\n\n  const _getSuggestions: Fetcher = async (text: string, range: Range, cursorPosition: number) => {\n    const suggestions = await getSuggestions(text, schema, range, cursorPosition);\n    return { suggestions };\n  };\n\n  const validateSql = (sql: string, model: any, me: any) => {\n    const v = validate(sql);\n    const errorSeverity = 8;\n    if (v.valid) {\n      me.setModelMarkers(model, 'clickhouse', []);\n    } else {\n      const err = v.error!;\n      me.setModelMarkers(model, 'clickhouse', [\n        {\n          startLineNumber: err.startLine,\n          startColumn: err.startCol,\n          endLineNumber: err.endLine,\n          endColumn: err.endCol,\n          message: err.expected,\n          severity: errorSeverity,\n        },\n      ]);\n    }\n  };\n\n  const handleMount = (editor: monacoTypes.editor.IStandaloneCodeEditor, monaco: typeof monacoTypes) => {\n    editorRef.current = editor;\n    const registration = registerSQL('sql', editor, _getSuggestions);\n    disposeRegistrationRef.current = registration.dispose;\n    setupAutoSize(editor);\n    editor.onKeyUp((e: any) => {\n      if (datasource.settings.jsonData.validateSql) {\n        const sql = editor.getValue();\n        validateSql(sql, editor.getModel(), registration.monacoEditor);\n      }\n    });\n\n    editor.addAction({\n      id: 'run-query',\n      label: 'Run Query',\n      keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],\n      contextMenuGroupId: 'navigation',\n      contextMenuOrder: 1.5,\n      run: (editor: monacoTypes.editor.IStandaloneCodeEditor) => {\n        saveChanges({ rawSql: editor.getValue() });\n        props.onRunQuery();\n      },\n    });\n  };\n\n  const onEditorWillUnmount = () => {\n    disposeRegistrationRef.current?.();\n    disposeRegistrationRef.current = null;\n    editorRef.current = null;\n  };\n  const triggerFormat = () => {\n    if (editorRef.current !== null) {\n      editorRef.current.trigger('editor', 'editor.action.formatDocument', '');\n    }\n  };\n\n  return (\n    <>\n      <div className={'gf-form ' + styles.QueryEditor.queryType}>\n        <QueryTypeSwitcher queryType={queryType} onChange={(queryType) => saveChanges({ queryType })} sqlEditor />\n      </div>\n      <div className={styles.Common.wrapper}>\n        <CodeEditor\n          aria-label=\"SQL Editor\"\n          language=\"sql\"\n          value={query.rawSql}\n          onSave={(sql) => saveChanges({ rawSql: sql })}\n          showMiniMap={false}\n          showLineNumbers={true}\n          onBlur={(sql) => saveChanges({ rawSql: sql })}\n          onEditorDidMount={handleMount}\n          onEditorWillUnmount={onEditorWillUnmount}\n        />\n        <QueryToolbox showTools onFormatCode={triggerFormat} />\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "src/components/configEditor/AliasTableConfig.test.tsx",
    "content": "import React from 'react';\nimport { render, fireEvent } from '@testing-library/react';\nimport { AliasTableConfig } from './AliasTableConfig';\nimport { selectors as allSelectors } from 'selectors';\nimport { AliasTableEntry } from 'types/config';\n\ndescribe('AliasTableConfig', () => {\n  const selectors = allSelectors.components.Config.AliasTableConfig;\n\n  it('should render', () => {\n    const result = render(<AliasTableConfig aliasTables={[]} onAliasTablesChange={() => {}} />);\n    expect(result.container.firstChild).not.toBeNull();\n  });\n\n  it('should not call onAliasTablesChange when entry is added', () => {\n    const onAliasTablesChange = jest.fn();\n    const result = render(<AliasTableConfig aliasTables={[]} onAliasTablesChange={onAliasTablesChange} />);\n    expect(result.container.firstChild).not.toBeNull();\n\n    const addEntryButton = result.getByTestId(selectors.addEntryButton);\n    expect(addEntryButton).toBeInTheDocument();\n    fireEvent.click(addEntryButton);\n\n    expect(onAliasTablesChange).toHaveBeenCalledTimes(0);\n  });\n\n  it('should call onAliasTablesChange when entry is updated', () => {\n    const onAliasTablesChange = jest.fn();\n    const result = render(<AliasTableConfig aliasTables={[]} onAliasTablesChange={onAliasTablesChange} />);\n    expect(result.container.firstChild).not.toBeNull();\n\n    const addEntryButton = result.getByTestId(selectors.addEntryButton);\n    expect(addEntryButton).toBeInTheDocument();\n    fireEvent.click(addEntryButton);\n\n    const aliasEditor = result.getByTestId(selectors.aliasEditor);\n    expect(aliasEditor).toBeInTheDocument();\n\n    const targetDatabaseInput = result.getByTestId(selectors.targetDatabaseInput);\n    expect(targetDatabaseInput).toBeInTheDocument();\n    fireEvent.change(targetDatabaseInput, { target: { value: 'default ' } }); // with space in name\n    fireEvent.blur(targetDatabaseInput);\n    expect(targetDatabaseInput).toHaveValue('default ');\n    expect(onAliasTablesChange).toHaveBeenCalledTimes(1);\n\n    const targetTableInput = result.getByTestId(selectors.targetTableInput);\n    expect(targetTableInput).toBeInTheDocument();\n    fireEvent.change(targetTableInput, { target: { value: 'query_log' } });\n    fireEvent.blur(targetTableInput);\n    expect(targetTableInput).toHaveValue('query_log');\n    expect(onAliasTablesChange).toHaveBeenCalledTimes(2);\n\n    const aliasDatabaseInput = result.getByTestId(selectors.aliasDatabaseInput);\n    expect(aliasDatabaseInput).toBeInTheDocument();\n    fireEvent.change(aliasDatabaseInput, { target: { value: 'default_aliases ' } }); // with space in name\n    fireEvent.blur(aliasDatabaseInput);\n    expect(aliasDatabaseInput).toHaveValue('default_aliases ');\n    expect(onAliasTablesChange).toHaveBeenCalledTimes(3);\n\n    const aliasTableInput = result.getByTestId(selectors.aliasTableInput);\n    expect(aliasTableInput).toBeInTheDocument();\n    fireEvent.change(aliasTableInput, { target: { value: 'query_log_aliases' } });\n    fireEvent.blur(aliasTableInput);\n    expect(aliasTableInput).toHaveValue('query_log_aliases');\n    expect(onAliasTablesChange).toHaveBeenCalledTimes(4);\n\n    const expected: AliasTableEntry[] = [\n      {\n        targetDatabase: 'default', // without space in name\n        targetTable: 'query_log',\n        aliasDatabase: 'default_aliases', // without space in name\n        aliasTable: 'query_log_aliases',\n      },\n    ];\n    expect(onAliasTablesChange).toHaveBeenCalledWith(expect.objectContaining(expected));\n  });\n\n  it('should call onAliasTablesChange when entry is removed', () => {\n    const onAliasTablesChange = jest.fn();\n    const result = render(\n      <AliasTableConfig\n        aliasTables={[\n          {\n            targetDatabase: '',\n            targetTable: 'query_log',\n            aliasDatabase: '',\n            aliasTable: 'query_log_aliases',\n          },\n          {\n            targetDatabase: '',\n            targetTable: 'query_log2',\n            aliasDatabase: '',\n            aliasTable: 'query_log2_aliases',\n          },\n        ]}\n        onAliasTablesChange={onAliasTablesChange}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const removeEntryButton = result.getAllByTestId(selectors.removeEntryButton)[0]; // Get 1st\n    expect(removeEntryButton).toBeInTheDocument();\n    fireEvent.click(removeEntryButton);\n\n    const expected: AliasTableEntry[] = [\n      {\n        targetDatabase: '',\n        targetTable: 'query_log2',\n        aliasDatabase: '',\n        aliasTable: 'query_log2_aliases',\n      },\n    ];\n    expect(onAliasTablesChange).toHaveBeenCalledTimes(1);\n    expect(onAliasTablesChange).toHaveBeenCalledWith(expect.objectContaining(expected));\n  });\n});\n"
  },
  {
    "path": "src/components/configEditor/AliasTableConfig.tsx",
    "content": "import React, { ChangeEvent, useState } from 'react';\nimport { ConfigSection } from 'components/experimental/ConfigSection';\nimport { Input, Field, Stack, Button } from '@grafana/ui';\nimport { AliasTableEntry } from 'types/config';\nimport allLabels from 'labels';\nimport { styles } from 'styles';\nimport { selectors as allSelectors } from 'selectors';\nimport { trackClickhouseConfigV2ColumnAliasTableAdded } from 'views/config-v2/tracking';\n\ninterface AliasTablesConfigProps {\n  aliasTables?: AliasTableEntry[];\n  onAliasTablesChange: (v: AliasTableEntry[]) => void;\n}\n\nexport const AliasTableConfig = (props: AliasTablesConfigProps) => {\n  const { onAliasTablesChange } = props;\n  const [entries, setEntries] = useState<AliasTableEntry[]>(props.aliasTables || []);\n  const labels = allLabels.components.Config.AliasTableConfig;\n  const selectors = allSelectors.components.Config.AliasTableConfig;\n\n  const entryToUniqueKey = (entry: AliasTableEntry) =>\n    `\"${entry.targetDatabase}\".\"${entry.targetTable}\":\"${entry.aliasDatabase}\".\"${entry.aliasTable}\"`;\n  const removeDuplicateEntries = (entries: AliasTableEntry[]): AliasTableEntry[] => {\n    const duplicateKeys = new Set();\n    return entries.filter((entry) => {\n      const key = entryToUniqueKey(entry);\n      if (duplicateKeys.has(key)) {\n        return false;\n      }\n\n      duplicateKeys.add(key);\n      return true;\n    });\n  };\n\n  const addEntry = () => {\n    setEntries(\n      removeDuplicateEntries([\n        ...entries,\n        {\n          targetDatabase: '',\n          targetTable: '',\n          aliasDatabase: '',\n          aliasTable: '',\n        },\n      ])\n    );\n  };\n  const removeEntry = (index: number) => {\n    let nextEntries: AliasTableEntry[] = entries.slice();\n    nextEntries.splice(index, 1);\n    nextEntries = removeDuplicateEntries(nextEntries);\n    setEntries(nextEntries);\n    onAliasTablesChange(nextEntries);\n  };\n  const updateEntry = (index: number, entry: AliasTableEntry) => {\n    let nextEntries: AliasTableEntry[] = entries.slice();\n    entry.targetDatabase = entry.targetDatabase.trim();\n    entry.targetTable = entry.targetTable.trim();\n    entry.aliasDatabase = entry.aliasDatabase.trim();\n    entry.aliasTable = entry.aliasTable.trim();\n    nextEntries[index] = entry;\n\n    nextEntries = removeDuplicateEntries(nextEntries);\n    setEntries(nextEntries);\n    onAliasTablesChange(nextEntries);\n  };\n\n  return (\n    <ConfigSection title={labels.title}>\n      <div>\n        <span>{labels.descriptionParts[0]}</span>\n        <code>{labels.descriptionParts[1]}</code>\n        <span>{labels.descriptionParts[2]}</span>\n      </div>\n      <br />\n\n      {entries.map((entry, index) => (\n        <AliasTableEditor\n          key={entryToUniqueKey(entry)}\n          targetDatabase={entry.targetDatabase}\n          targetTable={entry.targetTable}\n          aliasDatabase={entry.aliasDatabase}\n          aliasTable={entry.aliasTable}\n          onEntryChange={(e) => updateEntry(index, e)}\n          onRemove={() => removeEntry(index)}\n        />\n      ))}\n      <Button\n        data-testid={selectors.addEntryButton}\n        icon=\"plus-circle\"\n        variant=\"secondary\"\n        size=\"sm\"\n        onClick={() => {\n          addEntry();\n          trackClickhouseConfigV2ColumnAliasTableAdded();\n        }}\n        className={styles.Common.smallBtn}\n      >\n        {labels.addTableLabel}\n      </Button>\n    </ConfigSection>\n  );\n};\n\ninterface AliasTableEditorProps {\n  targetDatabase: string;\n  targetTable: string;\n  aliasDatabase: string;\n  aliasTable: string;\n  onEntryChange: (v: AliasTableEntry) => void;\n  onRemove?: () => void;\n}\n\nconst AliasTableEditor = (props: AliasTableEditorProps) => {\n  const { onEntryChange, onRemove } = props;\n  const [targetDatabase, setTargetDatabase] = useState<string>(props.targetDatabase);\n  const [targetTable, setTargetTable] = useState<string>(props.targetTable);\n  const [aliasDatabase, setAliasDatabase] = useState<string>(props.aliasDatabase);\n  const [aliasTable, setAliasTable] = useState<string>(props.aliasTable);\n  const labels = allLabels.components.Config.AliasTableConfig;\n  const selectors = allSelectors.components.Config.AliasTableConfig;\n\n  const onUpdate = () => {\n    onEntryChange({ targetDatabase, targetTable, aliasDatabase, aliasTable });\n  };\n\n  return (\n    <div data-testid={selectors.aliasEditor}>\n      <Stack>\n        <Field label={labels.targetDatabaseLabel} aria-label={labels.targetDatabaseLabel}>\n          <Input\n            data-testid={selectors.targetDatabaseInput}\n            value={targetDatabase}\n            placeholder={labels.targetDatabasePlaceholder}\n            onChange={(e: ChangeEvent<HTMLInputElement>) => setTargetDatabase(e.target.value)}\n            onBlur={() => onUpdate()}\n          />\n        </Field>\n        <Field label={labels.targetTableLabel} aria-label={labels.targetTableLabel}>\n          <Input\n            data-testid={selectors.targetTableInput}\n            value={targetTable}\n            placeholder={labels.targetTableLabel}\n            onChange={(e: ChangeEvent<HTMLInputElement>) => setTargetTable(e.target.value)}\n            onBlur={() => onUpdate()}\n          />\n        </Field>\n        <Field label={labels.aliasDatabaseLabel} aria-label={labels.aliasDatabaseLabel}>\n          <Input\n            data-testid={selectors.aliasDatabaseInput}\n            value={aliasDatabase}\n            placeholder={labels.aliasDatabasePlaceholder}\n            onChange={(e: ChangeEvent<HTMLInputElement>) => setAliasDatabase(e.target.value)}\n            onBlur={() => onUpdate()}\n          />\n        </Field>\n        <Field label={labels.aliasTableLabel} aria-label={labels.aliasTableLabel}>\n          <Input\n            data-testid={selectors.aliasTableInput}\n            value={aliasTable}\n            placeholder={labels.aliasTableLabel}\n            onChange={(e: ChangeEvent<HTMLInputElement>) => setAliasTable(e.target.value)}\n            onBlur={() => onUpdate()}\n          />\n        </Field>\n        {onRemove && (\n          <Button\n            data-testid={selectors.removeEntryButton}\n            className={styles.Common.smallBtn}\n            variant=\"destructive\"\n            size=\"sm\"\n            icon=\"trash-alt\"\n            onClick={onRemove}\n            aria-label=\"alias-remove-entry\"\n          />\n        )}\n      </Stack>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/configEditor/DefaultDatabaseTableConfig.test.tsx",
    "content": "import React from 'react';\nimport { render, fireEvent } from '@testing-library/react';\nimport { DefaultDatabaseTableConfig } from './DefaultDatabaseTableConfig';\nimport allLabels from 'labels';\n\ndescribe('DefaultDatabaseTableConfig', () => {\n  it('should render', () => {\n    const result = render(\n      <DefaultDatabaseTableConfig\n        defaultDatabase=\"\"\n        defaultTable=\"\"\n        onDefaultDatabaseChange={() => {}}\n        onDefaultTableChange={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n  });\n\n  it('should call onDefaultDatabaseChange when default database is changed', () => {\n    const onDefaultDatabaseChange = jest.fn();\n    const result = render(\n      <DefaultDatabaseTableConfig\n        defaultDatabase=\"\"\n        defaultTable=\"\"\n        onDefaultDatabaseChange={onDefaultDatabaseChange}\n        onDefaultTableChange={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const databaseInput = result.getByLabelText(allLabels.components.Config.DefaultDatabaseTableConfig.database.label);\n    expect(databaseInput).toBeInTheDocument();\n    fireEvent.change(databaseInput, { target: { value: 'test' } });\n    fireEvent.blur(databaseInput);\n    expect(onDefaultDatabaseChange).toHaveBeenCalledTimes(1);\n    expect(onDefaultDatabaseChange).toHaveBeenCalledWith(expect.any(Object));\n  });\n\n  it('should call onDefaultTableChange when default table is changed', () => {\n    const onDefaultTableChange = jest.fn();\n    const result = render(\n      <DefaultDatabaseTableConfig\n        defaultDatabase=\"\"\n        defaultTable=\"\"\n        onDefaultDatabaseChange={() => {}}\n        onDefaultTableChange={onDefaultTableChange}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const tableInput = result.getByLabelText(allLabels.components.Config.DefaultDatabaseTableConfig.table.label);\n    expect(tableInput).toBeInTheDocument();\n    fireEvent.change(tableInput, { target: { value: 'test' } });\n    fireEvent.blur(tableInput);\n    expect(onDefaultTableChange).toHaveBeenCalledTimes(1);\n    expect(onDefaultTableChange).toHaveBeenCalledWith(expect.any(Object));\n  });\n});\n"
  },
  {
    "path": "src/components/configEditor/DefaultDatabaseTableConfig.tsx",
    "content": "import React, { SyntheticEvent } from 'react';\nimport { ConfigSection } from 'components/experimental/ConfigSection';\nimport { Input, Field } from '@grafana/ui';\nimport allLabels from 'labels';\n\ninterface DefaultDatabaseTableConfigProps {\n  defaultDatabase?: string;\n  defaultTable?: string;\n  onDefaultDatabaseChange: (e: SyntheticEvent<HTMLInputElement | HTMLSelectElement, Event>) => void;\n  onDefaultTableChange: (e: SyntheticEvent<HTMLInputElement | HTMLSelectElement, Event>) => void;\n}\n\nexport const DefaultDatabaseTableConfig = (props: DefaultDatabaseTableConfigProps) => {\n  const { defaultDatabase, defaultTable, onDefaultDatabaseChange, onDefaultTableChange } = props;\n  const labels = allLabels.components.Config.DefaultDatabaseTableConfig;\n\n  return (\n    <ConfigSection title={labels.title}>\n      <Field label={labels.database.label} description={labels.database.description}>\n        <Input\n          name={labels.database.name}\n          width={40}\n          value={defaultDatabase || ''}\n          onChange={onDefaultDatabaseChange}\n          label={labels.database.label}\n          aria-label={labels.database.label}\n          placeholder={labels.database.placeholder}\n          type=\"text\"\n        />\n      </Field>\n      <Field label={labels.table.label} description={labels.table.description}>\n        <Input\n          name={labels.table.name}\n          width={40}\n          value={defaultTable || ''}\n          onChange={onDefaultTableChange}\n          label={labels.table.label}\n          aria-label={labels.table.label}\n          placeholder={labels.table.placeholder}\n          type=\"text\"\n        />\n      </Field>\n    </ConfigSection>\n  );\n};\n"
  },
  {
    "path": "src/components/configEditor/HttpHeadersConfig.test.tsx",
    "content": "import React from 'react';\nimport { render, fireEvent, renderHook } from '@testing-library/react';\nimport { HttpHeadersConfig, useConfiguredSecureHttpHeaders } from './HttpHeadersConfig';\nimport { selectors as allSelectors } from 'selectors';\nimport { CHHttpHeader } from 'types/config';\nimport { KeyValue } from '@grafana/data';\n\ndescribe('HttpHeadersConfig', () => {\n  const selectors = allSelectors.components.Config.HttpHeaderConfig;\n\n  it('should render', () => {\n    const result = render(\n      <HttpHeadersConfig\n        headers={[]}\n        secureFields={{}}\n        onHttpHeadersChange={() => {}}\n        onForwardGrafanaHeadersChange={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n  });\n\n  it('should not call onHttpHeadersChange when header is added', () => {\n    const onHttpHeadersChange = jest.fn();\n    const onForwardGrafanaHeadersChange = jest.fn();\n    const result = render(\n      <HttpHeadersConfig\n        headers={[]}\n        secureFields={{}}\n        onHttpHeadersChange={onHttpHeadersChange}\n        onForwardGrafanaHeadersChange={onForwardGrafanaHeadersChange}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const addHeaderButton = result.getByTestId(selectors.addHeaderButton);\n    expect(addHeaderButton).toBeInTheDocument();\n    fireEvent.click(addHeaderButton);\n\n    expect(onHttpHeadersChange).toHaveBeenCalledTimes(0);\n  });\n\n  it('should call onHttpHeadersChange when header is updated', () => {\n    const onHttpHeadersChange = jest.fn();\n    const onForwardGrafanaHeadersChange = jest.fn();\n    const result = render(\n      <HttpHeadersConfig\n        headers={[]}\n        secureFields={{}}\n        onHttpHeadersChange={onHttpHeadersChange}\n        onForwardGrafanaHeadersChange={onForwardGrafanaHeadersChange}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const addHeaderButton = result.getByTestId(selectors.addHeaderButton);\n    expect(addHeaderButton).toBeInTheDocument();\n    fireEvent.click(addHeaderButton);\n\n    const headerEditor = result.getByTestId(selectors.headerEditor);\n    expect(headerEditor).toBeInTheDocument();\n\n    const headerNameInput = result.getByTestId(selectors.headerNameInput);\n    expect(headerNameInput).toBeInTheDocument();\n    fireEvent.change(headerNameInput, { target: { value: 'x-test ' } }); // with space in name\n    fireEvent.blur(headerNameInput);\n    expect(headerNameInput).toHaveValue('x-test ');\n    expect(onHttpHeadersChange).toHaveBeenCalledTimes(1);\n\n    const headerValueInput = result.getByTestId(selectors.headerValueInput);\n    expect(headerValueInput).toBeInTheDocument();\n    fireEvent.change(headerValueInput, { target: { value: 'test value' } });\n    fireEvent.blur(headerValueInput);\n    expect(headerValueInput).toHaveValue('test value');\n    expect(onHttpHeadersChange).toHaveBeenCalledTimes(2);\n\n    const headerSecureSwitch = result.getByTestId(selectors.headerSecureSwitch);\n    expect(headerSecureSwitch).toBeInTheDocument();\n    fireEvent.click(headerSecureSwitch);\n    fireEvent.blur(headerSecureSwitch);\n    expect(onHttpHeadersChange).toHaveBeenCalledTimes(3);\n\n    const expected: CHHttpHeader[] = [\n      { name: 'x-test', value: 'test value', secure: true }, // without space in name\n    ];\n    expect(onHttpHeadersChange).toHaveBeenCalledWith(expect.objectContaining(expected));\n  });\n\n  it('should call onHttpHeadersChange when header is removed', () => {\n    const onHttpHeadersChange = jest.fn();\n    const onForwardGrafanaHeadersChange = jest.fn();\n    const result = render(\n      <HttpHeadersConfig\n        headers={[\n          { name: 'x-test', value: 'test value', secure: false },\n          { name: 'x-test-2', value: 'test value 2', secure: false },\n        ]}\n        secureFields={{}}\n        onHttpHeadersChange={onHttpHeadersChange}\n        onForwardGrafanaHeadersChange={onForwardGrafanaHeadersChange}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const removeHeaderButton = result.getAllByTestId(selectors.removeHeaderButton)[0]; // Get 1st\n    expect(removeHeaderButton).toBeInTheDocument();\n    fireEvent.click(removeHeaderButton);\n\n    const expected: CHHttpHeader[] = [{ name: 'x-test-2', value: 'test value 2', secure: false }];\n    expect(onHttpHeadersChange).toHaveBeenCalledTimes(1);\n    expect(onHttpHeadersChange).toHaveBeenCalledWith(expect.objectContaining(expected));\n  });\n});\n\ndescribe('useConfiguredSecureHttpHeaders', () => {\n  it('returns unique set of secure header keys', async () => {\n    const fields: KeyValue<boolean> = {\n      otherKey: true,\n      otherOtherKey: false,\n      'secureHttpHeaders.a': true,\n      'secureHttpHeaders.b': true,\n      'secureHttpHeaders.c': false,\n    };\n\n    const hook = renderHook(() => useConfiguredSecureHttpHeaders(fields));\n    const result = hook.result.current;\n\n    expect(result.size).toBe(2);\n    expect(result.has('a')).toBe(true);\n    expect(result.has('b')).toBe(true);\n    expect(result.has('c')).toBe(false);\n  });\n});\n\ndescribe('forwardGrafanaHTTPHeaders', () => {\n  const selectors = allSelectors.components.Config.HttpHeaderConfig;\n\n  it('should call onForwardGrafanaHeadersChange when switch is clicked', () => {\n    const onHttpHeadersChange = jest.fn();\n    const onForwardGrafanaHeadersChange = jest.fn();\n    const result = render(\n      <HttpHeadersConfig\n        headers={[]}\n        secureFields={{}}\n        onHttpHeadersChange={onHttpHeadersChange}\n        onForwardGrafanaHeadersChange={onForwardGrafanaHeadersChange}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const forwardGrafanaHeadersSwitch = result.getByTestId(selectors.forwardGrafanaHeadersSwitch);\n    expect(forwardGrafanaHeadersSwitch).toBeInTheDocument();\n    fireEvent.click(forwardGrafanaHeadersSwitch);\n    expect(onForwardGrafanaHeadersChange).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "src/components/configEditor/HttpHeadersConfig.tsx",
    "content": "import React, { ChangeEvent, useMemo, useState } from 'react';\nimport { ConfigSection } from 'components/experimental/ConfigSection';\nimport { Input, Field, Stack, Switch, SecretInput, Button } from '@grafana/ui';\nimport { CHHttpHeader } from 'types/config';\nimport allLabels from 'labels';\nimport { styles } from 'styles';\nimport { selectors as allSelectors } from 'selectors';\nimport { KeyValue } from '@grafana/data';\n\ninterface HttpHeadersConfigProps {\n  headers?: CHHttpHeader[];\n  forwardGrafanaHeaders?: boolean;\n  secureFields: KeyValue<boolean>;\n  onHttpHeadersChange: (v: CHHttpHeader[]) => void;\n  onForwardGrafanaHeadersChange: (v: boolean) => void;\n}\n\nexport const HttpHeadersConfig = (props: HttpHeadersConfigProps) => {\n  const { secureFields, onHttpHeadersChange } = props;\n  const configuredSecureHeaders = useConfiguredSecureHttpHeaders(secureFields);\n  const [headers, setHeaders] = useState<CHHttpHeader[]>(props.headers || []);\n  const [forwardGrafanaHeaders, setForwardGrafanaHeaders] = useState<boolean>(props.forwardGrafanaHeaders || false);\n  const labels = allLabels.components.Config.HttpHeadersConfig;\n  const selectors = allSelectors.components.Config.HttpHeaderConfig;\n\n  const addHeader = () => setHeaders([...headers, { name: '', value: '', secure: false }]);\n  const removeHeader = (index: number) => {\n    const nextHeaders: CHHttpHeader[] = headers.slice();\n    nextHeaders.splice(index, 1);\n    setHeaders(nextHeaders);\n    onHttpHeadersChange(nextHeaders);\n  };\n  const updateHeader = (index: number, header: CHHttpHeader) => {\n    const nextHeaders: CHHttpHeader[] = headers.slice();\n    header.name = header.name.trim();\n    nextHeaders[index] = header;\n    setHeaders(nextHeaders);\n    onHttpHeadersChange(nextHeaders);\n  };\n  const updateForwardGrafanaHeaders = (value: boolean) => {\n    setForwardGrafanaHeaders(value);\n    props.onForwardGrafanaHeadersChange(value);\n  };\n\n  return (\n    <ConfigSection title={labels.title}>\n      <Field label={labels.label} description={labels.description}>\n        <>\n          {headers.map((header, index) => (\n            <HttpHeaderEditor\n              key={header.name + index}\n              name={header.name}\n              value={header.value}\n              secure={header.secure}\n              isSecureConfigured={configuredSecureHeaders.has(header.name)}\n              onHeaderChange={(header) => updateHeader(index, header)}\n              onRemove={() => removeHeader(index)}\n            />\n          ))}\n          <Button\n            data-testid={selectors.addHeaderButton}\n            icon=\"plus-circle\"\n            variant=\"secondary\"\n            size=\"sm\"\n            onClick={addHeader}\n            className={styles.Common.smallBtn}\n          >\n            {labels.addHeaderLabel}\n          </Button>\n        </>\n      </Field>\n      <Field label={labels.forwardGrafanaHeaders.label} description={labels.forwardGrafanaHeaders.tooltip}>\n        <Switch\n          data-testid={selectors.forwardGrafanaHeadersSwitch}\n          className={'gf-form'}\n          value={forwardGrafanaHeaders}\n          onChange={(e) => updateForwardGrafanaHeaders(e.currentTarget.checked)}\n        />\n      </Field>\n    </ConfigSection>\n  );\n};\n\ninterface HttpHeaderEditorProps {\n  name: string;\n  value: string;\n  secure: boolean;\n  isSecureConfigured: boolean;\n  onHeaderChange: (v: CHHttpHeader) => void;\n  onRemove?: () => void;\n}\n\nconst HttpHeaderEditor = (props: HttpHeaderEditorProps) => {\n  const { onHeaderChange, onRemove } = props;\n  const [name, setName] = useState<string>(props.name);\n  const [value, setValue] = useState<string>(props.value);\n  const [secure, setSecure] = useState<boolean>(props.secure);\n  const [isSecureConfigured, setSecureConfigured] = useState<boolean>(props.isSecureConfigured);\n  const labels = allLabels.components.Config.HttpHeadersConfig;\n  const selectors = allSelectors.components.Config.HttpHeaderConfig;\n\n  const onUpdate = () => {\n    onHeaderChange({\n      name,\n      value,\n      secure,\n    });\n  };\n\n  let valueInput;\n  if (secure) {\n    valueInput = (\n      <SecretInput\n        data-testid={selectors.headerValueInput}\n        width={65}\n        placeholder={labels.secureHeaderValueLabel}\n        value={value}\n        isConfigured={isSecureConfigured}\n        onReset={() => setSecureConfigured(false)}\n        onChange={(e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value)}\n        onBlur={() => onUpdate()}\n      />\n    );\n  } else {\n    valueInput = (\n      <Input\n        data-testid={selectors.headerValueInput}\n        width={65}\n        value={value}\n        placeholder={labels.insecureHeaderValueLabel}\n        onChange={(e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value)}\n        onBlur={() => onUpdate()}\n      />\n    );\n  }\n\n  const headerValueLabel = secure ? labels.secureHeaderValueLabel : labels.insecureHeaderValueLabel;\n  return (\n    <div data-testid={selectors.headerEditor}>\n      <Stack>\n        <Field label={labels.headerNameLabel} aria-label={labels.headerNameLabel}>\n          <Input\n            data-testid={selectors.headerNameInput}\n            value={name}\n            disabled={isSecureConfigured}\n            placeholder={labels.headerNamePlaceholder}\n            onChange={(e: ChangeEvent<HTMLInputElement>) => setName(e.target.value)}\n            onBlur={() => onUpdate()}\n          />\n        </Field>\n        <Field label={headerValueLabel} aria-label={headerValueLabel}>\n          {valueInput}\n        </Field>\n        {!isSecureConfigured && (\n          <Field label={labels.secureLabel}>\n            <Switch\n              data-testid={selectors.headerSecureSwitch}\n              className=\"gf-form\"\n              value={secure}\n              onChange={(e) => setSecure(e.currentTarget.checked)}\n              onBlur={() => onUpdate()}\n            />\n          </Field>\n        )}\n        {onRemove && (\n          <Button\n            data-testid={selectors.removeHeaderButton}\n            className={styles.Common.smallBtn}\n            variant=\"destructive\"\n            size=\"sm\"\n            icon=\"trash-alt\"\n            onClick={onRemove}\n            aria-label=\"http-header-remove\"\n          />\n        )}\n      </Stack>\n    </div>\n  );\n};\n\n/**\n * Returns a Set of all secured headers that are configured\n */\nexport const useConfiguredSecureHttpHeaders = (secureJsonFields: KeyValue<boolean>): Set<string> => {\n  return useMemo(() => {\n    const secureHeaders = new Set<string>();\n    for (let key in secureJsonFields) {\n      if (key.startsWith('secureHttpHeaders.') && secureJsonFields[key]) {\n        secureHeaders.add(key.substring(key.indexOf('.') + 1));\n      }\n    }\n    return secureHeaders;\n  }, [secureJsonFields]);\n};\n"
  },
  {
    "path": "src/components/configEditor/LabeledInput.test.tsx",
    "content": "import React from 'react';\nimport { render, fireEvent } from '@testing-library/react';\nimport { LabeledInput } from './LabeledInput';\n\ndescribe('LabeledInput', () => {\n  it('should render', () => {\n    const result = render(<LabeledInput label=\"test\" value=\"test\" onChange={() => {}} />);\n    expect(result.container.firstChild).not.toBeNull();\n  });\n\n  it('should call onChange when input is changed', async () => {\n    const onChange = jest.fn();\n    const result = render(<LabeledInput label=\"test\" value=\"test\" placeholder=\"test\" onChange={onChange} />);\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText('test');\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onChange).toHaveBeenCalledTimes(1);\n    expect(onChange).toHaveBeenCalledWith('changed');\n  });\n});\n"
  },
  {
    "path": "src/components/configEditor/LabeledInput.tsx",
    "content": "import React from 'react';\nimport { Input, InlineFormLabel } from '@grafana/ui';\n\ninterface LabeledInputProps {\n  label: string;\n  tooltip?: string;\n  placeholder?: string;\n  disabled?: boolean;\n  value: string;\n  onChange: (value: string) => void;\n}\n\nexport function LabeledInput(props: LabeledInputProps) {\n  const { label, tooltip, placeholder, disabled, value, onChange } = props;\n\n  return (\n    <div className=\"gf-form\">\n      <InlineFormLabel width={12} className=\"query-keyword\" tooltip={tooltip || label}>\n        {label}\n      </InlineFormLabel>\n      <Input\n        disabled={disabled}\n        width={30}\n        value={value}\n        onChange={(e) => onChange(e.currentTarget.value)}\n        placeholder={placeholder}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/configEditor/LogsConfig.test.tsx",
    "content": "import React from 'react';\nimport { render, fireEvent } from '@testing-library/react';\nimport { LogsConfig } from './LogsConfig';\nimport allLabels from 'labels';\nimport { columnLabelToPlaceholder } from 'data/utils';\nimport { defaultCHAdditionalSettingsConfig } from 'types/config';\n\ndescribe('LogsConfig', () => {\n  it('should render', () => {\n    const result = render(\n      <LogsConfig\n        logsConfig={{}}\n        onDefaultDatabaseChange={() => {}}\n        onDefaultTableChange={() => {}}\n        onOtelEnabledChange={() => {}}\n        onOtelVersionChange={() => {}}\n        onFilterTimeColumnChange={() => {}}\n        onTimeColumnChange={() => {}}\n        onLevelColumnChange={() => {}}\n        onMessageColumnChange={() => {}}\n        onSelectContextColumnsChange={() => {}}\n        onContextColumnsChange={() => {}}\n        onShowLogLinksChange={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n  });\n\n  it('should call onDefaultDatabase when changed', () => {\n    const onDefaultDatabaseChange = jest.fn();\n    const result = render(\n      <LogsConfig\n        logsConfig={{}}\n        onDefaultDatabaseChange={onDefaultDatabaseChange}\n        onDefaultTableChange={() => {}}\n        onOtelEnabledChange={() => {}}\n        onOtelVersionChange={() => {}}\n        onFilterTimeColumnChange={() => {}}\n        onTimeColumnChange={() => {}}\n        onLevelColumnChange={() => {}}\n        onMessageColumnChange={() => {}}\n        onSelectContextColumnsChange={() => {}}\n        onContextColumnsChange={() => {}}\n        onShowLogLinksChange={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(allLabels.components.Config.LogsConfig.defaultDatabase.placeholder);\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onDefaultDatabaseChange).toHaveBeenCalledTimes(1);\n    expect(onDefaultDatabaseChange).toHaveBeenCalledWith('changed');\n  });\n\n  it('should call onDefaultTable when changed', () => {\n    const onDefaultTableChange = jest.fn();\n    const result = render(\n      <LogsConfig\n        logsConfig={{}}\n        onDefaultDatabaseChange={() => {}}\n        onDefaultTableChange={onDefaultTableChange}\n        onOtelEnabledChange={() => {}}\n        onOtelVersionChange={() => {}}\n        onFilterTimeColumnChange={() => {}}\n        onTimeColumnChange={() => {}}\n        onLevelColumnChange={() => {}}\n        onMessageColumnChange={() => {}}\n        onSelectContextColumnsChange={() => {}}\n        onContextColumnsChange={() => {}}\n        onShowLogLinksChange={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(defaultCHAdditionalSettingsConfig.logs?.defaultTable!);\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onDefaultTableChange).toHaveBeenCalledTimes(1);\n    expect(onDefaultTableChange).toHaveBeenCalledWith('changed');\n  });\n\n  // Commented out as it's broken post npm upgrade - needs investigation\n  // it('should call onOtelEnabled when changed', () => {\n  //   const onOtelEnabledChange = jest.fn();\n  //   const result = render(\n  //     <LogsConfig\n  //       logsConfig={{}}\n  //       onDefaultDatabaseChange={() => {}}\n  //       onDefaultTableChange={() => {}}\n  //       onOtelEnabledChange={onOtelEnabledChange}\n  //       onOtelVersionChange={() => {}}\n  //       onTimeColumnChange={() => {}}\n  //       onLevelColumnChange={() => {}}\n  //       onMessageColumnChange={() => {}}\n  //       onSelectContextColumnsChange={() => {}}\n  //       onContextColumnsChange={() => {}}\n  //     />\n  //   );\n  //   expect(result.container.firstChild).not.toBeNull();\n\n  //   const checkboxes = result.getAllByRole('checkbox');\n  //   expect(checkboxes).toHaveLength(2);\n  //   const input = checkboxes[0];\n  //   expect(input).toBeInTheDocument();\n  //   fireEvent.click(input);\n  //   expect(onOtelEnabledChange).toHaveBeenCalledTimes(1);\n  //   expect(onOtelEnabledChange).toHaveBeenCalledWith(true);\n  // });\n\n  it('should call onOtelVersionChange when changed', () => {\n    const onOtelVersionChange = jest.fn();\n    const result = render(\n      <LogsConfig\n        logsConfig={{ otelEnabled: true }}\n        onDefaultDatabaseChange={() => {}}\n        onDefaultTableChange={() => {}}\n        onOtelEnabledChange={() => {}}\n        onOtelVersionChange={onOtelVersionChange}\n        onFilterTimeColumnChange={() => {}}\n        onTimeColumnChange={() => {}}\n        onLevelColumnChange={() => {}}\n        onMessageColumnChange={() => {}}\n        onSelectContextColumnsChange={() => {}}\n        onContextColumnsChange={() => {}}\n        onShowLogLinksChange={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const select = result.getByRole('combobox');\n    expect(select).toBeInTheDocument();\n    fireEvent.keyDown(select, { key: 'ArrowDown' });\n    fireEvent.keyDown(select, { key: 'Enter' });\n    expect(onOtelVersionChange).toHaveBeenCalledTimes(2); // 2 from hook\n    expect(onOtelVersionChange).toHaveBeenCalledWith(expect.any(String));\n  });\n\n  it('should call onFilterTimeColumnChange when changed', () => {\n    const onFilterTimeColumnChange = jest.fn();\n    const result = render(\n      <LogsConfig\n        logsConfig={{}}\n        onDefaultDatabaseChange={() => {}}\n        onDefaultTableChange={() => {}}\n        onOtelEnabledChange={() => {}}\n        onOtelVersionChange={() => {}}\n        onFilterTimeColumnChange={onFilterTimeColumnChange}\n        onTimeColumnChange={() => {}}\n        onLevelColumnChange={() => {}}\n        onMessageColumnChange={() => {}}\n        onSelectContextColumnsChange={() => {}}\n        onContextColumnsChange={() => {}}\n        onShowLogLinksChange={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(\n      columnLabelToPlaceholder(allLabels.components.Config.LogsConfig.columns.filterTime.label)\n    );\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onFilterTimeColumnChange).toHaveBeenCalledTimes(1);\n    expect(onFilterTimeColumnChange).toHaveBeenCalledWith('changed');\n  });\n\n  it('should call onTimeColumnChange when changed', () => {\n    const onTimeColumnChange = jest.fn();\n    const result = render(\n      <LogsConfig\n        logsConfig={{}}\n        onDefaultDatabaseChange={() => {}}\n        onDefaultTableChange={() => {}}\n        onOtelEnabledChange={() => {}}\n        onOtelVersionChange={() => {}}\n        onFilterTimeColumnChange={() => {}}\n        onTimeColumnChange={onTimeColumnChange}\n        onLevelColumnChange={() => {}}\n        onMessageColumnChange={() => {}}\n        onSelectContextColumnsChange={() => {}}\n        onContextColumnsChange={() => {}}\n        onShowLogLinksChange={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(\n      columnLabelToPlaceholder(allLabels.components.Config.LogsConfig.columns.time.label)\n    );\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onTimeColumnChange).toHaveBeenCalledTimes(1);\n    expect(onTimeColumnChange).toHaveBeenCalledWith('changed');\n  });\n\n  it('should call onLevelColumnChange when changed', () => {\n    const onLevelColumnChange = jest.fn();\n    const result = render(\n      <LogsConfig\n        logsConfig={{}}\n        onDefaultDatabaseChange={() => {}}\n        onDefaultTableChange={() => {}}\n        onOtelEnabledChange={() => {}}\n        onOtelVersionChange={() => {}}\n        onFilterTimeColumnChange={() => {}}\n        onTimeColumnChange={() => {}}\n        onLevelColumnChange={onLevelColumnChange}\n        onMessageColumnChange={() => {}}\n        onSelectContextColumnsChange={() => {}}\n        onContextColumnsChange={() => {}}\n        onShowLogLinksChange={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(\n      columnLabelToPlaceholder(allLabels.components.Config.LogsConfig.columns.level.label)\n    );\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onLevelColumnChange).toHaveBeenCalledTimes(1);\n    expect(onLevelColumnChange).toHaveBeenCalledWith('changed');\n  });\n\n  it('should call onMessageColumnChange when changed', () => {\n    const onMessageColumnChange = jest.fn();\n    const result = render(\n      <LogsConfig\n        logsConfig={{}}\n        onDefaultDatabaseChange={() => {}}\n        onDefaultTableChange={() => {}}\n        onOtelEnabledChange={() => {}}\n        onOtelVersionChange={() => {}}\n        onFilterTimeColumnChange={() => {}}\n        onTimeColumnChange={() => {}}\n        onLevelColumnChange={() => {}}\n        onMessageColumnChange={onMessageColumnChange}\n        onSelectContextColumnsChange={() => {}}\n        onContextColumnsChange={() => {}}\n        onShowLogLinksChange={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(\n      columnLabelToPlaceholder(allLabels.components.Config.LogsConfig.columns.message.label)\n    );\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onMessageColumnChange).toHaveBeenCalledTimes(1);\n    expect(onMessageColumnChange).toHaveBeenCalledWith('changed');\n  });\n\n  it('should call onShowLogLinksChange when toggled', async () => {\n    const onShowLogLinksChange = jest.fn();\n    const result = render(\n      <LogsConfig\n        logsConfig={{}}\n        onDefaultDatabaseChange={() => {}}\n        onDefaultTableChange={() => {}}\n        onOtelEnabledChange={() => {}}\n        onOtelVersionChange={() => {}}\n        onFilterTimeColumnChange={() => {}}\n        onTimeColumnChange={() => {}}\n        onLevelColumnChange={() => {}}\n        onMessageColumnChange={() => {}}\n        onSelectContextColumnsChange={() => {}}\n        onContextColumnsChange={() => {}}\n        onShowLogLinksChange={onShowLogLinksChange}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    // showLogLinks is the 1st role=\"switch\" (index 0), before selectContextColumns\n    const switches = await result.findAllByRole('switch');\n    const input = switches[0];\n    expect(input).toBeInTheDocument();\n    fireEvent.click(input);\n    expect(onShowLogLinksChange).toHaveBeenCalledTimes(1);\n    expect(onShowLogLinksChange).toHaveBeenCalledWith(false);\n  });\n});\n"
  },
  {
    "path": "src/components/configEditor/LogsConfig.tsx",
    "content": "import React from 'react';\nimport { ConfigSection, ConfigSubSection } from 'components/experimental/ConfigSection';\nimport { Input, Field, InlineFormLabel, TagsInput } from '@grafana/ui';\nimport { OtelVersionSelect } from 'components/queryBuilder/OtelVersionSelect';\nimport { ColumnHint } from 'types/queryBuilder';\nimport otel from 'otel';\nimport { LabeledInput } from './LabeledInput';\nimport { CHLogsConfig, defaultCHAdditionalSettingsConfig } from 'types/config';\nimport allLabels from 'labels';\nimport { columnLabelToPlaceholder } from 'data/utils';\nimport { Switch } from 'components/queryBuilder/Switch';\n\ninterface LogsConfigProps {\n  logsConfig?: CHLogsConfig;\n  onDefaultDatabaseChange: (v: string) => void;\n  onDefaultTableChange: (v: string) => void;\n  onOtelEnabledChange: (v: boolean) => void;\n  onOtelVersionChange: (v: string) => void;\n  onFilterTimeColumnChange: (v: string) => void;\n  onTimeColumnChange: (v: string) => void;\n  onLevelColumnChange: (v: string) => void;\n  onMessageColumnChange: (v: string) => void;\n  onSelectContextColumnsChange: (v: boolean) => void;\n  onContextColumnsChange: (v: string[]) => void;\n  onShowLogLinksChange: (v: boolean) => void;\n}\n\nexport const LogsConfig = (props: LogsConfigProps) => {\n  const {\n    onDefaultDatabaseChange,\n    onDefaultTableChange,\n    onOtelEnabledChange,\n    onOtelVersionChange,\n    onFilterTimeColumnChange,\n    onTimeColumnChange,\n    onLevelColumnChange,\n    onMessageColumnChange,\n    onSelectContextColumnsChange,\n    onContextColumnsChange,\n    onShowLogLinksChange,\n  } = props;\n  let {\n    defaultDatabase,\n    defaultTable,\n    otelEnabled,\n    otelVersion,\n    filterTimeColumn,\n    timeColumn,\n    levelColumn,\n    messageColumn,\n    selectContextColumns,\n    contextColumns,\n    showLogLinks,\n  } = props.logsConfig || {};\n  const labels = allLabels.components.Config.LogsConfig;\n\n  const otelConfig = otel.getVersion(otelVersion);\n  if (otelEnabled && otelConfig) {\n    filterTimeColumn = otelConfig.logColumnMap.get(ColumnHint.FilterTime);\n    timeColumn = otelConfig.logColumnMap.get(ColumnHint.Time);\n    levelColumn = otelConfig.logColumnMap.get(ColumnHint.LogLevel);\n    messageColumn = otelConfig.logColumnMap.get(ColumnHint.LogMessage);\n  }\n\n  const onContextColumnsChangeTrimmed = (columns: string[]) =>\n    onContextColumnsChange(columns.map((c) => c.trim()).filter((c) => c));\n\n  return (\n    <ConfigSection title={labels.title} description={labels.description}>\n      <div id=\"logs-config\" />\n      <Field label={labels.defaultDatabase.label} description={labels.defaultDatabase.description}>\n        <Input\n          name={labels.defaultDatabase.name}\n          width={40}\n          value={defaultDatabase || ''}\n          onChange={(e) => onDefaultDatabaseChange(e.currentTarget.value)}\n          label={labels.defaultDatabase.label}\n          aria-label={labels.defaultDatabase.label}\n          placeholder={labels.defaultDatabase.placeholder}\n        />\n      </Field>\n      <Field label={labels.defaultTable.label} description={labels.defaultTable.description}>\n        <Input\n          name={labels.defaultTable.name}\n          width={40}\n          value={defaultTable || ''}\n          onChange={(e) => onDefaultTableChange(e.currentTarget.value)}\n          label={labels.defaultTable.label}\n          aria-label={labels.defaultTable.label}\n          placeholder={defaultCHAdditionalSettingsConfig.logs?.defaultTable!}\n        />\n      </Field>\n      <ConfigSubSection title={labels.columns.title} description={labels.columns.description}>\n        <OtelVersionSelect\n          enabled={otelEnabled || false}\n          selectedVersion={otelVersion || ''}\n          onEnabledChange={onOtelEnabledChange}\n          onVersionChange={onOtelVersionChange}\n          wide\n        />\n        <LabeledInput\n          disabled={otelEnabled}\n          label={labels.columns.filterTime.label}\n          placeholder={columnLabelToPlaceholder(labels.columns.filterTime.label)}\n          tooltip={labels.columns.filterTime.tooltip}\n          value={filterTimeColumn || ''}\n          onChange={onFilterTimeColumnChange}\n        />\n        <LabeledInput\n          disabled={otelEnabled}\n          label={labels.columns.time.label}\n          placeholder={columnLabelToPlaceholder(labels.columns.time.label)}\n          tooltip={labels.columns.time.tooltip}\n          value={timeColumn || ''}\n          onChange={onTimeColumnChange}\n        />\n        <LabeledInput\n          disabled={otelEnabled}\n          label={labels.columns.level.label}\n          placeholder={columnLabelToPlaceholder(labels.columns.level.label)}\n          tooltip={labels.columns.level.tooltip}\n          value={levelColumn || ''}\n          onChange={onLevelColumnChange}\n        />\n        <LabeledInput\n          disabled={otelEnabled}\n          label={labels.columns.message.label}\n          placeholder={columnLabelToPlaceholder(labels.columns.message.label)}\n          tooltip={labels.columns.message.tooltip}\n          value={messageColumn || ''}\n          onChange={onMessageColumnChange}\n        />\n      </ConfigSubSection>\n      <br />\n      <ConfigSubSection title={labels.traceIdCorrelation.title} description={labels.traceIdCorrelation.description}>\n        <Switch\n          label={labels.traceIdCorrelation.showLogLinks.label}\n          tooltip={labels.traceIdCorrelation.showLogLinks.tooltip}\n          value={showLogLinks ?? true}\n          onChange={onShowLogLinksChange}\n          wide\n        />\n      </ConfigSubSection>\n      <br />\n      <ConfigSubSection title={labels.contextColumns.title} description={labels.contextColumns.description}>\n        <Switch\n          label={labels.contextColumns.selectContextColumns.label}\n          tooltip={labels.contextColumns.selectContextColumns.tooltip}\n          value={selectContextColumns || false}\n          onChange={onSelectContextColumnsChange}\n          wide\n        />\n        <div className=\"gf-form\">\n          <InlineFormLabel width={12} className=\"query-keyword\" tooltip={labels.contextColumns.columns.tooltip}>\n            {labels.contextColumns.columns.label}\n          </InlineFormLabel>\n          <TagsInput\n            placeholder={labels.contextColumns.columns.placeholder}\n            tags={contextColumns || []}\n            onChange={onContextColumnsChangeTrimmed}\n            width={60}\n          />\n        </div>\n      </ConfigSubSection>\n    </ConfigSection>\n  );\n};\n"
  },
  {
    "path": "src/components/configEditor/QuerySettingsConfig.test.tsx",
    "content": "import React from 'react';\nimport { render, fireEvent } from '@testing-library/react';\nimport { QuerySettingsConfig } from './QuerySettingsConfig';\nimport allLabels from 'labels';\n\ndescribe('QuerySettingsConfig', () => {\n  it('should render', () => {\n    const result = render(\n      <QuerySettingsConfig\n        connMaxLifetime={'5'}\n        dialTimeout={'5'}\n        maxIdleConns={'5'}\n        maxOpenConns={'5'}\n        queryTimeout={'5'}\n        validateSql={true}\n        onConnMaxIdleConnsChange={() => {}}\n        onConnMaxLifetimeChange={() => {}}\n        onConnMaxOpenConnsChange={() => {}}\n        onDialTimeoutChange={() => {}}\n        onQueryTimeoutChange={() => {}}\n        onValidateSqlChange={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n  });\n\n  it('should call onDialTimeout when changed', () => {\n    const onDialTimeout = jest.fn();\n    const result = render(\n      <QuerySettingsConfig\n        onConnMaxIdleConnsChange={() => {}}\n        onConnMaxLifetimeChange={() => {}}\n        onConnMaxOpenConnsChange={() => {}}\n        onDialTimeoutChange={onDialTimeout}\n        onQueryTimeoutChange={() => {}}\n        onValidateSqlChange={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(allLabels.components.Config.QuerySettingsConfig.dialTimeout.placeholder);\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: '10' } });\n    fireEvent.blur(input);\n    expect(onDialTimeout).toHaveBeenCalledTimes(1);\n    expect(onDialTimeout).toHaveBeenCalledWith(expect.any(Object));\n  });\n\n  it('should call onQueryTimeout when changed', () => {\n    const onQueryTimeout = jest.fn();\n    const result = render(\n      <QuerySettingsConfig\n        onConnMaxIdleConnsChange={() => {}}\n        onConnMaxLifetimeChange={() => {}}\n        onConnMaxOpenConnsChange={() => {}}\n        onDialTimeoutChange={() => {}}\n        onQueryTimeoutChange={onQueryTimeout}\n        onValidateSqlChange={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(allLabels.components.Config.QuerySettingsConfig.queryTimeout.placeholder);\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: '10' } });\n    fireEvent.blur(input);\n    expect(onQueryTimeout).toHaveBeenCalledTimes(1);\n    expect(onQueryTimeout).toHaveBeenCalledWith(expect.any(Object));\n  });\n\n  it('should call onValidateSqlChange when changed', () => {\n    const onValidateSqlChange = jest.fn();\n    const result = render(\n      <QuerySettingsConfig\n        onConnMaxIdleConnsChange={() => {}}\n        onConnMaxLifetimeChange={() => {}}\n        onConnMaxOpenConnsChange={() => {}}\n        onDialTimeoutChange={() => {}}\n        onQueryTimeoutChange={() => {}}\n        onValidateSqlChange={onValidateSqlChange}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByRole('checkbox');\n    expect(input).toBeInTheDocument();\n    fireEvent.click(input);\n    expect(onValidateSqlChange).toHaveBeenCalledTimes(1);\n    expect(onValidateSqlChange).toHaveBeenCalledWith(expect.any(Object));\n  });\n\n  it('should call onConnMaxIdleConnsChange when changed', () => {\n    const onConnMaxIdleConnsChange = jest.fn();\n    const result = render(\n      <QuerySettingsConfig\n        onConnMaxIdleConnsChange={onConnMaxIdleConnsChange}\n        onConnMaxLifetimeChange={() => {}}\n        onConnMaxOpenConnsChange={() => {}}\n        onDialTimeoutChange={() => {}}\n        onQueryTimeoutChange={() => {}}\n        onValidateSqlChange={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(allLabels.components.Config.QuerySettingsConfig.maxIdleConns.placeholder);\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: '10' } });\n    fireEvent.blur(input);\n    expect(onConnMaxIdleConnsChange).toHaveBeenCalledTimes(1);\n    expect(onConnMaxIdleConnsChange).toHaveBeenCalledWith(expect.any(Object));\n  });\n\n  it('should call onConnMaxLifetimeChange when changed', () => {\n    const onConnMaxLifetimeChange = jest.fn();\n    const result = render(\n      <QuerySettingsConfig\n        onConnMaxIdleConnsChange={() => {}}\n        onConnMaxLifetimeChange={onConnMaxLifetimeChange}\n        onConnMaxOpenConnsChange={() => {}}\n        onDialTimeoutChange={() => {}}\n        onQueryTimeoutChange={() => {}}\n        onValidateSqlChange={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(\n      allLabels.components.Config.QuerySettingsConfig.connMaxLifetime.placeholder\n    );\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: '10' } });\n    fireEvent.blur(input);\n    expect(onConnMaxLifetimeChange).toHaveBeenCalledTimes(1);\n    expect(onConnMaxLifetimeChange).toHaveBeenCalledWith(expect.any(Object));\n  });\n\n  it('should call onConnMaxOpenConnsChange when changed', () => {\n    const onConnMaxOpenConnsChange = jest.fn();\n    const result = render(\n      <QuerySettingsConfig\n        onConnMaxIdleConnsChange={() => {}}\n        onConnMaxLifetimeChange={() => {}}\n        onConnMaxOpenConnsChange={onConnMaxOpenConnsChange}\n        onDialTimeoutChange={() => {}}\n        onQueryTimeoutChange={() => {}}\n        onValidateSqlChange={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(allLabels.components.Config.QuerySettingsConfig.maxOpenConns.placeholder);\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: '10' } });\n    fireEvent.blur(input);\n    expect(onConnMaxOpenConnsChange).toHaveBeenCalledTimes(1);\n    expect(onConnMaxOpenConnsChange).toHaveBeenCalledWith(expect.any(Object));\n  });\n});\n"
  },
  {
    "path": "src/components/configEditor/QuerySettingsConfig.tsx",
    "content": "import React, { FormEvent } from 'react';\nimport { Switch, Input, Field } from '@grafana/ui';\nimport { ConfigSection } from 'components/experimental/ConfigSection';\nimport allLabels from 'labels';\n\ninterface QuerySettingsConfigProps {\n  connMaxLifetime?: string;\n  dialTimeout?: string;\n  maxIdleConns?: string;\n  maxOpenConns?: string;\n  queryTimeout?: string;\n  validateSql?: boolean;\n  onConnMaxIdleConnsChange: (e: FormEvent<HTMLInputElement>) => void;\n  onConnMaxLifetimeChange: (e: FormEvent<HTMLInputElement>) => void;\n  onConnMaxOpenConnsChange: (e: FormEvent<HTMLInputElement>) => void;\n  onDialTimeoutChange: (e: FormEvent<HTMLInputElement>) => void;\n  onQueryTimeoutChange: (e: FormEvent<HTMLInputElement>) => void;\n  onValidateSqlChange: (e: FormEvent<HTMLInputElement>) => void;\n}\n\nexport const QuerySettingsConfig = (props: QuerySettingsConfigProps) => {\n  const {\n    connMaxLifetime,\n    dialTimeout,\n    maxIdleConns,\n    maxOpenConns,\n    queryTimeout,\n    validateSql,\n    onConnMaxIdleConnsChange,\n    onConnMaxLifetimeChange,\n    onConnMaxOpenConnsChange,\n    onDialTimeoutChange,\n    onQueryTimeoutChange,\n    onValidateSqlChange,\n  } = props;\n\n  const labels = allLabels.components.Config.QuerySettingsConfig;\n\n  return (\n    <ConfigSection title={labels.title}>\n      <Field label={labels.dialTimeout.label} description={labels.dialTimeout.tooltip}>\n        <Input\n          name={labels.dialTimeout.name}\n          width={40}\n          value={dialTimeout || ''}\n          onChange={onDialTimeoutChange}\n          label={labels.dialTimeout.label}\n          aria-label={labels.dialTimeout.label}\n          placeholder={labels.dialTimeout.placeholder}\n          type=\"number\"\n        />\n      </Field>\n      <Field label={labels.queryTimeout.label} description={labels.queryTimeout.tooltip}>\n        <Input\n          name={labels.queryTimeout.name}\n          width={40}\n          value={queryTimeout || ''}\n          onChange={onQueryTimeoutChange}\n          label={labels.queryTimeout.label}\n          aria-label={labels.queryTimeout.label}\n          placeholder={labels.queryTimeout.placeholder}\n          type=\"number\"\n        />\n      </Field>\n      <Field label={labels.connMaxLifetime.label} description={labels.connMaxLifetime.tooltip}>\n        <Input\n          name={labels.connMaxLifetime.name}\n          width={40}\n          value={connMaxLifetime || ''}\n          onChange={onConnMaxLifetimeChange}\n          label={labels.connMaxLifetime.label}\n          aria-label={labels.connMaxLifetime.label}\n          placeholder={labels.connMaxLifetime.placeholder}\n          type=\"number\"\n        />\n      </Field>\n      <Field label={labels.maxIdleConns.label} description={labels.maxIdleConns.tooltip}>\n        <Input\n          name={labels.maxIdleConns.name}\n          width={40}\n          value={maxIdleConns || ''}\n          onChange={onConnMaxIdleConnsChange}\n          label={labels.maxIdleConns.label}\n          aria-label={labels.maxIdleConns.label}\n          placeholder={labels.maxIdleConns.placeholder}\n          type=\"number\"\n        />\n      </Field>\n      <Field label={labels.maxOpenConns.label} description={labels.maxOpenConns.tooltip}>\n        <Input\n          name={labels.maxOpenConns.name}\n          width={40}\n          value={maxOpenConns || ''}\n          onChange={onConnMaxOpenConnsChange}\n          label={labels.maxOpenConns.label}\n          aria-label={labels.maxOpenConns.label}\n          placeholder={labels.maxOpenConns.placeholder}\n          type=\"number\"\n        />\n      </Field>\n\n      <Field label={labels.validateSql.label} description={labels.validateSql.tooltip}>\n        <Switch className=\"gf-form\" value={validateSql || false} onChange={onValidateSqlChange} role=\"checkbox\" />\n      </Field>\n    </ConfigSection>\n  );\n};\n"
  },
  {
    "path": "src/components/configEditor/TracesConfig.test.tsx",
    "content": "import React from 'react';\nimport { render, fireEvent } from '@testing-library/react';\nimport { TracesConfig, TraceConfigProps } from './TracesConfig';\nimport allLabels from 'labels';\nimport { columnLabelToPlaceholder } from 'data/utils';\nimport { defaultCHAdditionalSettingsConfig } from 'types/config';\n\nfunction defaultTraceConfigProps(): TraceConfigProps {\n  return {\n    tracesConfig: {},\n    onDefaultDatabaseChange: () => {},\n    onDefaultTableChange: () => {},\n    onOtelEnabledChange: () => {},\n    onOtelVersionChange: () => {},\n    onTraceIdColumnChange: () => {},\n    onSpanIdColumnChange: () => {},\n    onOperationNameColumnChange: () => {},\n    onParentSpanIdColumnChange: () => {},\n    onServiceNameColumnChange: () => {},\n    onDurationColumnChange: () => {},\n    onDurationUnitChange: () => {},\n    onStartTimeColumnChange: () => {},\n    onTagsColumnChange: () => {},\n    onServiceTagsColumnChange: () => {},\n    onKindColumnChange: () => {},\n    onStatusCodeColumnChange: () => {},\n    onStatusMessageColumnChange: () => {},\n    onStateColumnChange: () => {},\n    onInstrumentationLibraryNameColumnChange: () => {},\n    onInstrumentationLibraryVersionColumnChange: () => {},\n    onFlattenNestedChange: () => {},\n    onEventsColumnPrefixChange: () => {},\n    onLinksColumnPrefixChange: () => {},\n    onShowTraceLinksChange: () => {},\n    onTraceTimestampTableSuffixChange: () => {},\n  };\n}\n\ndescribe('TracesConfig', () => {\n  it('should render', () => {\n    const result = render(<TracesConfig {...defaultTraceConfigProps()} />);\n    expect(result.container.firstChild).not.toBeNull();\n  });\n\n  it('should call onDefaultDatabase when changed', () => {\n    const onDefaultDatabaseChange = jest.fn();\n    const result = render(\n      <TracesConfig {...defaultTraceConfigProps()} onDefaultDatabaseChange={onDefaultDatabaseChange} />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(allLabels.components.Config.TracesConfig.defaultDatabase.placeholder);\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onDefaultDatabaseChange).toHaveBeenCalledTimes(1);\n    expect(onDefaultDatabaseChange).toHaveBeenCalledWith('changed');\n  });\n\n  it('should call onDefaultTable when changed', () => {\n    const onDefaultTableChange = jest.fn();\n    const result = render(<TracesConfig {...defaultTraceConfigProps()} onDefaultTableChange={onDefaultTableChange} />);\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(defaultCHAdditionalSettingsConfig.traces?.defaultTable!);\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onDefaultTableChange).toHaveBeenCalledTimes(1);\n    expect(onDefaultTableChange).toHaveBeenCalledWith('changed');\n  });\n\n  it('should call onOtelEnabled when changed', async () => {\n    const onOtelEnabledChange = jest.fn();\n    const result = render(<TracesConfig {...defaultTraceConfigProps()} onOtelEnabledChange={onOtelEnabledChange} />);\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = (await result.findAllByRole('checkbox'))[0];\n    expect(input).toBeInTheDocument();\n    fireEvent.click(input);\n    expect(onOtelEnabledChange).toHaveBeenCalledTimes(1);\n    expect(onOtelEnabledChange).toHaveBeenCalledWith(true);\n  });\n\n  it('should call onOtelVersionChange when changed', () => {\n    const onOtelVersionChange = jest.fn();\n    const result = render(\n      <TracesConfig\n        {...defaultTraceConfigProps()}\n        tracesConfig={{ otelEnabled: true }}\n        onOtelVersionChange={onOtelVersionChange}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const select = result.getByRole('combobox');\n    expect(select).toBeInTheDocument();\n    fireEvent.keyDown(select, { key: 'ArrowDown' });\n    fireEvent.keyDown(select, { key: 'Enter' });\n    expect(onOtelVersionChange).toHaveBeenCalledTimes(2); // 2 from hook\n    expect(onOtelVersionChange).toHaveBeenCalledWith(expect.any(String));\n  });\n\n  it('should call onTraceIdColumnChange when changed', () => {\n    const onTraceIdColumnChange = jest.fn();\n    const result = render(\n      <TracesConfig {...defaultTraceConfigProps()} onTraceIdColumnChange={onTraceIdColumnChange} />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(\n      columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.traceId.label)\n    );\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onTraceIdColumnChange).toHaveBeenCalledTimes(1);\n    expect(onTraceIdColumnChange).toHaveBeenCalledWith('changed');\n  });\n\n  it('should call onSpanIdColumnChange when changed', () => {\n    const onSpanIdColumnChange = jest.fn();\n    const result = render(<TracesConfig {...defaultTraceConfigProps()} onSpanIdColumnChange={onSpanIdColumnChange} />);\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(\n      columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.spanId.label)\n    );\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onSpanIdColumnChange).toHaveBeenCalledTimes(1);\n    expect(onSpanIdColumnChange).toHaveBeenCalledWith('changed');\n  });\n\n  it('should call onOperationNameColumnChange when changed', () => {\n    const onOperationNameColumnChange = jest.fn();\n    const result = render(\n      <TracesConfig {...defaultTraceConfigProps()} onOperationNameColumnChange={onOperationNameColumnChange} />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(\n      columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.operationName.label)\n    );\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onOperationNameColumnChange).toHaveBeenCalledTimes(1);\n    expect(onOperationNameColumnChange).toHaveBeenCalledWith('changed');\n  });\n\n  it('should call onParentSpanIdColumnChange when changed', () => {\n    const onParentSpanIdColumnChange = jest.fn();\n    const result = render(\n      <TracesConfig {...defaultTraceConfigProps()} onParentSpanIdColumnChange={onParentSpanIdColumnChange} />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(\n      columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.parentSpanId.label)\n    );\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onParentSpanIdColumnChange).toHaveBeenCalledTimes(1);\n    expect(onParentSpanIdColumnChange).toHaveBeenCalledWith('changed');\n  });\n\n  it('should call onServiceNameColumnChange when changed', () => {\n    const onServiceNameColumnChange = jest.fn();\n    const result = render(\n      <TracesConfig {...defaultTraceConfigProps()} onServiceNameColumnChange={onServiceNameColumnChange} />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(\n      columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.serviceName.label)\n    );\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onServiceNameColumnChange).toHaveBeenCalledTimes(1);\n    expect(onServiceNameColumnChange).toHaveBeenCalledWith('changed');\n  });\n\n  it('should call onDurationColumnChange when changed', () => {\n    const onDurationColumnChange = jest.fn();\n    const result = render(\n      <TracesConfig {...defaultTraceConfigProps()} onDurationColumnChange={onDurationColumnChange} />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(\n      columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.durationTime.label)\n    );\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onDurationColumnChange).toHaveBeenCalledTimes(1);\n    expect(onDurationColumnChange).toHaveBeenCalledWith('changed');\n  });\n\n  it('should call onDurationUnitChange when changed', () => {\n    const onDurationUnitChange = jest.fn();\n    const result = render(<TracesConfig {...defaultTraceConfigProps()} onDurationUnitChange={onDurationUnitChange} />);\n    expect(result.container.firstChild).not.toBeNull();\n\n    const select = result.getByRole('combobox');\n    expect(select).toBeInTheDocument();\n    fireEvent.keyDown(select, { key: 'ArrowDown' });\n    fireEvent.keyDown(select, { key: 'Enter' });\n    expect(onDurationUnitChange).toHaveBeenCalledTimes(1);\n    expect(onDurationUnitChange).toHaveBeenCalledWith(expect.any(String));\n  });\n\n  it('should call onStartTimeColumnChange when changed', () => {\n    const onStartTimeColumnChange = jest.fn();\n    const result = render(\n      <TracesConfig {...defaultTraceConfigProps()} onStartTimeColumnChange={onStartTimeColumnChange} />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(\n      columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.startTime.label)\n    );\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onStartTimeColumnChange).toHaveBeenCalledTimes(1);\n    expect(onStartTimeColumnChange).toHaveBeenCalledWith('changed');\n  });\n\n  it('should call onTagsColumnChange when changed', () => {\n    const onTagsColumnChange = jest.fn();\n    const result = render(<TracesConfig {...defaultTraceConfigProps()} onTagsColumnChange={onTagsColumnChange} />);\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(\n      columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.tags.label)\n    );\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onTagsColumnChange).toHaveBeenCalledTimes(1);\n    expect(onTagsColumnChange).toHaveBeenCalledWith('changed');\n  });\n\n  it('should call onServiceTagsColumnChange when changed', () => {\n    const onServiceTagsColumnChange = jest.fn();\n    const result = render(\n      <TracesConfig {...defaultTraceConfigProps()} onServiceTagsColumnChange={onServiceTagsColumnChange} />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(\n      columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.serviceTags.label)\n    );\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onServiceTagsColumnChange).toHaveBeenCalledTimes(1);\n    expect(onServiceTagsColumnChange).toHaveBeenCalledWith('changed');\n  });\n\n  it('should call onKindColumnChange when changed', () => {\n    const onKindColumnChange = jest.fn();\n    const result = render(<TracesConfig {...defaultTraceConfigProps()} onKindColumnChange={onKindColumnChange} />);\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(\n      columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.kind.label)\n    );\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onKindColumnChange).toHaveBeenCalledTimes(1);\n    expect(onKindColumnChange).toHaveBeenCalledWith('changed');\n  });\n\n  it('should call onStatusCodeColumnChange when changed', () => {\n    const onStatusCodeColumnChange = jest.fn();\n    const result = render(\n      <TracesConfig {...defaultTraceConfigProps()} onStatusCodeColumnChange={onStatusCodeColumnChange} />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(\n      columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.statusCode.label)\n    );\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onStatusCodeColumnChange).toHaveBeenCalledTimes(1);\n    expect(onStatusCodeColumnChange).toHaveBeenCalledWith('changed');\n  });\n\n  it('should call onStatusMessageColumnChange when changed', () => {\n    const onStatusMessageColumnChange = jest.fn();\n    const result = render(\n      <TracesConfig {...defaultTraceConfigProps()} onStatusMessageColumnChange={onStatusMessageColumnChange} />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(\n      columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.statusMessage.label)\n    );\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onStatusMessageColumnChange).toHaveBeenCalledTimes(1);\n    expect(onStatusMessageColumnChange).toHaveBeenCalledWith('changed');\n  });\n\n  it('should call onStateColumnChange when changed', () => {\n    const onStateColumnChange = jest.fn();\n    const result = render(<TracesConfig {...defaultTraceConfigProps()} onStateColumnChange={onStateColumnChange} />);\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(\n      columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.state.label)\n    );\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onStateColumnChange).toHaveBeenCalledTimes(1);\n    expect(onStateColumnChange).toHaveBeenCalledWith('changed');\n  });\n\n  it('should call onInstrumentationLibraryNameColumnChange when changed', () => {\n    const onInstrumentationLibraryNameColumnChange = jest.fn();\n    const result = render(\n      <TracesConfig\n        {...defaultTraceConfigProps()}\n        onInstrumentationLibraryNameColumnChange={onInstrumentationLibraryNameColumnChange}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(\n      columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.instrumentationLibraryName.label)\n    );\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onInstrumentationLibraryNameColumnChange).toHaveBeenCalledTimes(1);\n    expect(onInstrumentationLibraryNameColumnChange).toHaveBeenCalledWith('changed');\n  });\n\n  it('should call onInstrumentationLibraryVersionColumnChange when changed', () => {\n    const onInstrumentationLibraryVersionColumnChange = jest.fn();\n    const result = render(\n      <TracesConfig\n        {...defaultTraceConfigProps()}\n        onInstrumentationLibraryVersionColumnChange={onInstrumentationLibraryVersionColumnChange}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(\n      columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.instrumentationLibraryVersion.label)\n    );\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onInstrumentationLibraryVersionColumnChange).toHaveBeenCalledTimes(1);\n    expect(onInstrumentationLibraryVersionColumnChange).toHaveBeenCalledWith('changed');\n  });\n\n  // Commented out as it's broken post npm upgrade - needs investigation\n  // it('should call onFlattenNestedChange when changed', async () => {\n  //   const onFlattenNestedChange = jest.fn();\n  //   const result = render(\n  //     <TracesConfig {...defaultTraceConfigProps()} onFlattenNestedChange={onFlattenNestedChange} />\n  //   );\n  //   expect(result.container.firstChild).not.toBeNull();\n\n  //   const input = (await result.findByLabelText(\n  //     allLabels.components.Config.TracesConfig.columns.flattenNested.label\n  //   )) as HTMLInputElement;\n  //   expect(input).toBeInTheDocument();\n  //   fireEvent.click(input);\n  //   expect(onFlattenNestedChange).toHaveBeenCalledTimes(1);\n  //   expect(onFlattenNestedChange).toHaveBeenCalledWith(true);\n  // });\n\n  it('should call onEventsColumnPrefixChange when changed', () => {\n    const onEventsColumnPrefixChange = jest.fn();\n    const result = render(\n      <TracesConfig {...defaultTraceConfigProps()} onEventsColumnPrefixChange={onEventsColumnPrefixChange} />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(\n      columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.eventsPrefix.label)\n    );\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onEventsColumnPrefixChange).toHaveBeenCalledTimes(1);\n    expect(onEventsColumnPrefixChange).toHaveBeenCalledWith('changed');\n  });\n\n  it('should call onLinksColumnPrefixChange when changed', () => {\n    const onLinksColumnPrefixChange = jest.fn();\n    const result = render(\n      <TracesConfig {...defaultTraceConfigProps()} onLinksColumnPrefixChange={onLinksColumnPrefixChange} />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText(\n      columnLabelToPlaceholder(allLabels.components.Config.TracesConfig.columns.linksPrefix.label)\n    );\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: 'changed' } });\n    fireEvent.blur(input);\n    expect(onLinksColumnPrefixChange).toHaveBeenCalledTimes(1);\n    expect(onLinksColumnPrefixChange).toHaveBeenCalledWith('changed');\n  });\n\n  it('should call onTraceTimestampTableSuffixChange when changed', () => {\n    const onTraceTimestampTableSuffixChange = jest.fn();\n    const result = render(\n      <TracesConfig\n        {...defaultTraceConfigProps()}\n        onTraceTimestampTableSuffixChange={onTraceTimestampTableSuffixChange}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const input = result.getByPlaceholderText('_trace_id_ts');\n    expect(input).toBeInTheDocument();\n    fireEvent.change(input, { target: { value: '_ts_index' } });\n    fireEvent.blur(input);\n    expect(onTraceTimestampTableSuffixChange).toHaveBeenCalledTimes(1);\n    expect(onTraceTimestampTableSuffixChange).toHaveBeenCalledWith('_ts_index');\n  });\n\n  it('should call onShowTraceLinksChange when toggled', async () => {\n    const onShowTraceLinksChange = jest.fn();\n    const result = render(\n      <TracesConfig {...defaultTraceConfigProps()} onShowTraceLinksChange={onShowTraceLinksChange} />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    // showTraceLinks is the 2nd role=\"switch\" (index 1), after flattenNested\n    const switches = await result.findAllByRole('switch');\n    const input = switches[1];\n    expect(input).toBeInTheDocument();\n    fireEvent.click(input);\n    expect(onShowTraceLinksChange).toHaveBeenCalledTimes(1);\n    expect(onShowTraceLinksChange).toHaveBeenCalledWith(false);\n  });\n});\n"
  },
  {
    "path": "src/components/configEditor/TracesConfig.tsx",
    "content": "import React from 'react';\nimport { ConfigSection, ConfigSubSection } from 'components/experimental/ConfigSection';\nimport { Input, Field } from '@grafana/ui';\nimport { OtelVersionSelect } from 'components/queryBuilder/OtelVersionSelect';\nimport { ColumnHint, TimeUnit } from 'types/queryBuilder';\nimport otel, { traceTimestampTableSuffix as defaultTraceTimestampTableSuffix } from 'otel';\nimport { LabeledInput } from './LabeledInput';\nimport { DurationUnitSelect } from 'components/queryBuilder/DurationUnitSelect';\nimport { CHTracesConfig, defaultCHAdditionalSettingsConfig } from 'types/config';\nimport allLabels from 'labels';\nimport { columnLabelToPlaceholder } from 'data/utils';\nimport { Switch } from 'components/queryBuilder/Switch';\n\nexport interface TraceConfigProps {\n  tracesConfig?: CHTracesConfig;\n  onDefaultDatabaseChange: (v: string) => void;\n  onDefaultTableChange: (v: string) => void;\n  onOtelEnabledChange: (v: boolean) => void;\n  onOtelVersionChange: (v: string) => void;\n  onTraceIdColumnChange: (v: string) => void;\n  onSpanIdColumnChange: (v: string) => void;\n  onOperationNameColumnChange: (v: string) => void;\n  onParentSpanIdColumnChange: (v: string) => void;\n  onServiceNameColumnChange: (v: string) => void;\n  onDurationColumnChange: (v: string) => void;\n  onDurationUnitChange: (v: TimeUnit) => void;\n  onStartTimeColumnChange: (v: string) => void;\n  onTagsColumnChange: (v: string) => void;\n  onServiceTagsColumnChange: (v: string) => void;\n  onKindColumnChange: (v: string) => void;\n  onStatusCodeColumnChange: (v: string) => void;\n  onStatusMessageColumnChange: (v: string) => void;\n  onStateColumnChange: (v: string) => void;\n  onInstrumentationLibraryNameColumnChange: (v: string) => void;\n  onInstrumentationLibraryVersionColumnChange: (v: string) => void;\n  onFlattenNestedChange: (v: boolean) => void;\n  onEventsColumnPrefixChange: (v: string) => void;\n  onLinksColumnPrefixChange: (v: string) => void;\n  onShowTraceLinksChange: (v: boolean) => void;\n  onTraceTimestampTableSuffixChange: (v: string) => void;\n}\n\nexport const TracesConfig = (props: TraceConfigProps) => {\n  const {\n    onDefaultDatabaseChange,\n    onDefaultTableChange,\n    onOtelEnabledChange,\n    onOtelVersionChange,\n    onTraceIdColumnChange,\n    onSpanIdColumnChange,\n    onOperationNameColumnChange,\n    onParentSpanIdColumnChange,\n    onServiceNameColumnChange,\n    onDurationColumnChange,\n    onDurationUnitChange,\n    onStartTimeColumnChange,\n    onTagsColumnChange,\n    onServiceTagsColumnChange,\n    onKindColumnChange,\n    onStatusCodeColumnChange,\n    onStatusMessageColumnChange,\n    onStateColumnChange,\n    onInstrumentationLibraryNameColumnChange,\n    onInstrumentationLibraryVersionColumnChange,\n    onFlattenNestedChange,\n    onEventsColumnPrefixChange,\n    onLinksColumnPrefixChange,\n    onShowTraceLinksChange,\n    onTraceTimestampTableSuffixChange,\n  } = props;\n  let {\n    defaultDatabase,\n    defaultTable,\n    otelEnabled,\n    otelVersion,\n    traceIdColumn,\n    spanIdColumn,\n    operationNameColumn,\n    parentSpanIdColumn,\n    serviceNameColumn,\n    durationColumn,\n    durationUnit,\n    startTimeColumn,\n    tagsColumn,\n    serviceTagsColumn,\n    kindColumn,\n    statusCodeColumn,\n    statusMessageColumn,\n    stateColumn,\n    instrumentationLibraryNameColumn,\n    instrumentationLibraryVersionColumn,\n    flattenNested,\n    traceEventsColumnPrefix,\n    traceLinksColumnPrefix,\n    showTraceLinks,\n    traceTimestampTableSuffix,\n  } = (props.tracesConfig || {}) as CHTracesConfig;\n  const labels = allLabels.components.Config.TracesConfig;\n\n  const otelConfig = otel.getVersion(otelVersion);\n  if (otelEnabled && otelConfig) {\n    startTimeColumn = otelConfig.traceColumnMap.get(ColumnHint.Time);\n    traceIdColumn = otelConfig.traceColumnMap.get(ColumnHint.TraceId);\n    spanIdColumn = otelConfig.traceColumnMap.get(ColumnHint.TraceSpanId);\n    parentSpanIdColumn = otelConfig.traceColumnMap.get(ColumnHint.TraceParentSpanId);\n    serviceNameColumn = otelConfig.traceColumnMap.get(ColumnHint.TraceServiceName);\n    operationNameColumn = otelConfig.traceColumnMap.get(ColumnHint.TraceOperationName);\n    durationColumn = otelConfig.traceColumnMap.get(ColumnHint.TraceDurationTime);\n    tagsColumn = otelConfig.traceColumnMap.get(ColumnHint.TraceTags);\n    serviceTagsColumn = otelConfig.traceColumnMap.get(ColumnHint.TraceServiceTags);\n    kindColumn = otelConfig.traceColumnMap.get(ColumnHint.TraceKind);\n    statusCodeColumn = otelConfig.traceColumnMap.get(ColumnHint.TraceStatusCode);\n    statusMessageColumn = otelConfig.traceColumnMap.get(ColumnHint.TraceStatusMessage);\n    stateColumn = otelConfig.traceColumnMap.get(ColumnHint.TraceState);\n    instrumentationLibraryNameColumn = otelConfig.traceColumnMap.get(ColumnHint.TraceInstrumentationLibraryName);\n    instrumentationLibraryVersionColumn = otelConfig.traceColumnMap.get(ColumnHint.TraceInstrumentationLibraryVersion);\n    durationUnit = otelConfig.traceDurationUnit.toString();\n    flattenNested = otelConfig.flattenNested;\n    traceEventsColumnPrefix = otelConfig.traceEventsColumnPrefix;\n    traceLinksColumnPrefix = otelConfig.traceLinksColumnPrefix;\n  }\n\n  return (\n    <ConfigSection title={labels.title} description={labels.description}>\n      <div id=\"traces-config\" />\n      <Field label={labels.defaultDatabase.label} description={labels.defaultDatabase.description}>\n        <Input\n          name={labels.defaultDatabase.name}\n          width={40}\n          value={defaultDatabase || ''}\n          onChange={(e) => onDefaultDatabaseChange(e.currentTarget.value)}\n          label={labels.defaultDatabase.label}\n          aria-label={labels.defaultDatabase.label}\n          placeholder={labels.defaultDatabase.placeholder}\n        />\n      </Field>\n      <Field label={labels.defaultTable.label} description={labels.defaultTable.description}>\n        <Input\n          name={labels.defaultTable.name}\n          width={40}\n          value={defaultTable || ''}\n          onChange={(e) => onDefaultTableChange(e.currentTarget.value)}\n          label={labels.defaultTable.label}\n          aria-label={labels.defaultTable.label}\n          placeholder={defaultCHAdditionalSettingsConfig.traces?.defaultTable!}\n        />\n      </Field>\n      <ConfigSubSection title={labels.columns.title} description={labels.columns.description}>\n        <OtelVersionSelect\n          enabled={otelEnabled || false}\n          selectedVersion={otelVersion || ''}\n          onEnabledChange={onOtelEnabledChange}\n          onVersionChange={onOtelVersionChange}\n          wide\n        />\n        <LabeledInput\n          disabled={otelEnabled}\n          label={labels.columns.traceId.label}\n          placeholder={columnLabelToPlaceholder(labels.columns.traceId.label)}\n          tooltip={labels.columns.traceId.tooltip}\n          value={traceIdColumn || ''}\n          onChange={onTraceIdColumnChange}\n        />\n        <LabeledInput\n          disabled={otelEnabled}\n          label={labels.columns.spanId.label}\n          placeholder={columnLabelToPlaceholder(labels.columns.spanId.label)}\n          tooltip={labels.columns.spanId.tooltip}\n          value={spanIdColumn || ''}\n          onChange={onSpanIdColumnChange}\n        />\n        <LabeledInput\n          disabled={otelEnabled}\n          label={labels.columns.operationName.label}\n          placeholder={columnLabelToPlaceholder(labels.columns.operationName.label)}\n          tooltip={labels.columns.operationName.tooltip}\n          value={operationNameColumn || ''}\n          onChange={onOperationNameColumnChange}\n        />\n        <LabeledInput\n          disabled={otelEnabled}\n          label={labels.columns.parentSpanId.label}\n          placeholder={columnLabelToPlaceholder(labels.columns.parentSpanId.label)}\n          tooltip={labels.columns.parentSpanId.tooltip}\n          value={parentSpanIdColumn || ''}\n          onChange={onParentSpanIdColumnChange}\n        />\n        <LabeledInput\n          disabled={otelEnabled}\n          label={labels.columns.serviceName.label}\n          placeholder={columnLabelToPlaceholder(labels.columns.serviceName.label)}\n          tooltip={labels.columns.serviceName.tooltip}\n          value={serviceNameColumn || ''}\n          onChange={onServiceNameColumnChange}\n        />\n        <LabeledInput\n          disabled={otelEnabled}\n          label={labels.columns.durationTime.label}\n          placeholder={columnLabelToPlaceholder(labels.columns.durationTime.label)}\n          tooltip={labels.columns.durationTime.tooltip}\n          value={durationColumn || ''}\n          onChange={onDurationColumnChange}\n        />\n        <DurationUnitSelect\n          disabled={otelEnabled}\n          unit={(durationUnit as TimeUnit) || defaultCHAdditionalSettingsConfig.traces?.durationUnit}\n          onChange={onDurationUnitChange}\n        />\n        <LabeledInput\n          disabled={otelEnabled}\n          label={labels.columns.startTime.label}\n          placeholder={columnLabelToPlaceholder(labels.columns.startTime.label)}\n          tooltip={labels.columns.startTime.tooltip}\n          value={startTimeColumn || ''}\n          onChange={onStartTimeColumnChange}\n        />\n        <LabeledInput\n          disabled={otelEnabled}\n          label={labels.columns.tags.label}\n          placeholder={columnLabelToPlaceholder(labels.columns.tags.label)}\n          tooltip={labels.columns.tags.tooltip}\n          value={tagsColumn || ''}\n          onChange={onTagsColumnChange}\n        />\n        <LabeledInput\n          disabled={otelEnabled}\n          label={labels.columns.serviceTags.label}\n          placeholder={columnLabelToPlaceholder(labels.columns.serviceTags.label)}\n          tooltip={labels.columns.serviceTags.tooltip}\n          value={serviceTagsColumn || ''}\n          onChange={onServiceTagsColumnChange}\n        />\n        <LabeledInput\n          disabled={otelEnabled}\n          label={labels.columns.kind.label}\n          placeholder={columnLabelToPlaceholder(labels.columns.kind.label)}\n          tooltip={labels.columns.kind.tooltip}\n          value={kindColumn || ''}\n          onChange={onKindColumnChange}\n        />\n        <LabeledInput\n          disabled={otelEnabled}\n          label={labels.columns.statusCode.label}\n          placeholder={columnLabelToPlaceholder(labels.columns.statusCode.label)}\n          tooltip={labels.columns.statusCode.tooltip}\n          value={statusCodeColumn || ''}\n          onChange={onStatusCodeColumnChange}\n        />\n        <LabeledInput\n          disabled={otelEnabled}\n          label={labels.columns.statusMessage.label}\n          placeholder={columnLabelToPlaceholder(labels.columns.statusMessage.label)}\n          tooltip={labels.columns.statusMessage.tooltip}\n          value={statusMessageColumn || ''}\n          onChange={onStatusMessageColumnChange}\n        />\n        <LabeledInput\n          disabled={otelEnabled}\n          label={labels.columns.state.label}\n          placeholder={columnLabelToPlaceholder(labels.columns.state.label)}\n          tooltip={labels.columns.state.tooltip}\n          value={stateColumn || ''}\n          onChange={onStateColumnChange}\n        />\n        <LabeledInput\n          disabled={otelEnabled}\n          label={labels.columns.instrumentationLibraryName.label}\n          placeholder={columnLabelToPlaceholder(labels.columns.instrumentationLibraryName.label)}\n          tooltip={labels.columns.instrumentationLibraryName.tooltip}\n          value={instrumentationLibraryNameColumn || ''}\n          onChange={onInstrumentationLibraryNameColumnChange}\n        />\n        <LabeledInput\n          disabled={otelEnabled}\n          label={labels.columns.instrumentationLibraryVersion.label}\n          placeholder={columnLabelToPlaceholder(labels.columns.instrumentationLibraryVersion.label)}\n          tooltip={labels.columns.instrumentationLibraryVersion.tooltip}\n          value={instrumentationLibraryVersionColumn || ''}\n          onChange={onInstrumentationLibraryVersionColumnChange}\n        />\n        <Switch\n          disabled={otelEnabled}\n          label={labels.columns.flattenNested.label}\n          tooltip={labels.columns.flattenNested.tooltip}\n          value={flattenNested || false}\n          onChange={onFlattenNestedChange}\n          wide\n        />\n        <LabeledInput\n          disabled={otelEnabled}\n          label={labels.columns.eventsPrefix.label}\n          placeholder={columnLabelToPlaceholder(labels.columns.eventsPrefix.label)}\n          tooltip={labels.columns.eventsPrefix.tooltip}\n          value={traceEventsColumnPrefix || ''}\n          onChange={onEventsColumnPrefixChange}\n        />\n        <LabeledInput\n          disabled={otelEnabled}\n          label={labels.columns.linksPrefix.label}\n          placeholder={columnLabelToPlaceholder(labels.columns.linksPrefix.label)}\n          tooltip={labels.columns.linksPrefix.tooltip}\n          value={traceLinksColumnPrefix || ''}\n          onChange={onLinksColumnPrefixChange}\n        />\n        <LabeledInput\n          label={labels.columns.traceTimestampTableSuffix.label}\n          placeholder={defaultTraceTimestampTableSuffix}\n          tooltip={labels.columns.traceTimestampTableSuffix.tooltip}\n          value={traceTimestampTableSuffix || ''}\n          onChange={onTraceTimestampTableSuffixChange}\n        />\n      </ConfigSubSection>\n      <br/>\n      <ConfigSubSection title={labels.traceIdCorrelation.title} description={labels.traceIdCorrelation.description}>\n        <Switch\n          label={labels.traceIdCorrelation.showTraceLinks.label}\n          tooltip={labels.traceIdCorrelation.showTraceLinks.tooltip}\n          value={showTraceLinks ?? true}\n          onChange={onShowTraceLinksChange}\n          wide\n        />\n      </ConfigSubSection>\n    </ConfigSection>\n  );\n};\n"
  },
  {
    "path": "src/components/experimental/ConfigSection/ConfigSection.test.tsx",
    "content": "import React from 'react';\nimport { screen, render } from '@testing-library/react';\nimport { ConfigSection } from './ConfigSection';\n\ndescribe('<ConfigSection />', () => {\n  it('should render title as <h3>', () => {\n    render(\n      <ConfigSection title=\"Test title\">\n        <div>Content</div>\n      </ConfigSection>\n    );\n\n    expect(screen.getByText('Test title').tagName).toBe('H3');\n  });\n});\n"
  },
  {
    "path": "src/components/experimental/ConfigSection/ConfigSection.tsx",
    "content": "import React from 'react';\nimport { GenericConfigSection, Props as GenericConfigSectionProps } from './GenericConfigSection';\n\ntype Props = Omit<GenericConfigSectionProps, 'kind'>;\n\nexport const ConfigSection = ({ children, ...props }: Props) => {\n  return (\n    <GenericConfigSection {...props} kind=\"section\">\n      {children}\n    </GenericConfigSection>\n  );\n};\n"
  },
  {
    "path": "src/components/experimental/ConfigSection/ConfigSubSection.test.tsx",
    "content": "import React from 'react';\nimport { screen, render } from '@testing-library/react';\nimport { ConfigSubSection } from './ConfigSubSection';\n\ndescribe('<ConfigSubSection />', () => {\n  it('should render title as <h3>', () => {\n    render(\n      <ConfigSubSection title=\"Test title\">\n        <div>Content</div>\n      </ConfigSubSection>\n    );\n\n    expect(screen.getByText('Test title').tagName).toBe('H6');\n  });\n});\n"
  },
  {
    "path": "src/components/experimental/ConfigSection/ConfigSubSection.tsx",
    "content": "import React from 'react';\nimport { GenericConfigSection, Props as GenericConfigSectionProps } from './GenericConfigSection';\n\ntype Props = Omit<GenericConfigSectionProps, 'kind'>;\n\nexport const ConfigSubSection = ({ children, ...props }: Props) => {\n  return (\n    <GenericConfigSection {...props} kind=\"sub-section\">\n      {children}\n    </GenericConfigSection>\n  );\n};\n"
  },
  {
    "path": "src/components/experimental/ConfigSection/DataSourceDescription.test.tsx",
    "content": "import React from 'react';\nimport { DataSourceDescription } from './DataSourceDescription';\nimport { render } from '@testing-library/react';\n\ndescribe('<DataSourceDescription />', () => {\n  it('should render data source name', () => {\n    const dataSourceName = 'Test data source name';\n    const { getByText } = render(\n      <DataSourceDescription dataSourceName={dataSourceName} docsLink=\"https://grafana.com/test-datasource-docs\" />\n    );\n\n    expect(getByText(dataSourceName, { exact: false })).toBeInTheDocument();\n  });\n\n  it('should render docs link', () => {\n    const docsLink = 'https://grafana.com/test-datasource-docs';\n    const { getByText } = render(\n      <DataSourceDescription dataSourceName={'Test data source name'} docsLink={docsLink} />\n    );\n\n    const docsLinkEl = getByText('view the documentation');\n\n    expect(docsLinkEl.getAttribute('href')).toBe(docsLink);\n  });\n\n  it('should render text about required fields by default', () => {\n    const { getByText } = render(\n      <DataSourceDescription\n        dataSourceName={'Test data source name'}\n        docsLink={'https://grafana.com/test-datasource-docs'}\n      />\n    );\n\n    expect(getByText('Fields marked with', { exact: false })).toBeInTheDocument();\n  });\n\n  it('should not render text about required fields when `hasRequiredFields` props is `false`', () => {\n    const { getByText } = render(\n      <DataSourceDescription\n        dataSourceName={'Test data source name'}\n        docsLink={'https://grafana.com/test-datasource-docs'}\n        hasRequiredFields={false}\n      />\n    );\n\n    expect(() => getByText('Fields marked with', { exact: false })).toThrow();\n  });\n\n  it('should render passed `className`', () => {\n    const { container } = render(\n      <DataSourceDescription\n        dataSourceName={'Test data source name'}\n        docsLink={'https://grafana.com/test-datasource-docs'}\n        className=\"test-class-name\"\n      />\n    );\n\n    expect(container.firstChild).toHaveClass('test-class-name');\n  });\n});\n"
  },
  {
    "path": "src/components/experimental/ConfigSection/DataSourceDescription.tsx",
    "content": "import React from 'react';\nimport { cx, css } from '@emotion/css';\nimport { useTheme2 } from '@grafana/ui';\n\ntype Props = {\n  dataSourceName: string;\n  docsLink: string;\n  hasRequiredFields?: boolean;\n  className?: string;\n};\n\nexport const DataSourceDescription = ({ dataSourceName, docsLink, hasRequiredFields = true, className }: Props) => {\n  const theme = useTheme2();\n\n  const styles = {\n    container: css({\n      p: {\n        margin: 0,\n      },\n      'p + p': {\n        marginTop: theme.spacing(2),\n      },\n    }),\n    text: css({\n      ...theme.typography.body,\n      color: theme.colors.text.secondary,\n      a: css({\n        color: theme.colors.text.link,\n        textDecoration: 'underline',\n        '&:hover': {\n          textDecoration: 'none',\n        },\n      }),\n    }),\n  };\n\n  return (\n    <div className={cx(styles.container, className)}>\n      <p className={styles.text}>\n        Before you can use the {dataSourceName} data source, you must configure it below or in the config file. For\n        detailed instructions,{' '}\n        <a href={docsLink} target=\"_blank\" rel=\"noreferrer\">\n          view the documentation\n        </a>\n        .\n      </p>\n      {hasRequiredFields && (\n        <p className={styles.text}>\n          <i>Fields marked with * are required</i>\n        </p>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/experimental/ConfigSection/GenericConfigSection.test.tsx",
    "content": "import React from 'react';\nimport { screen, render } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { GenericConfigSection } from './GenericConfigSection';\n\nlet user = userEvent.setup();\n\ndescribe('<GenericConfigSection />', () => {\n  beforeEach(() => {\n    userEvent.setup();\n  });\n\n  it('should render title', () => {\n    render(\n      <GenericConfigSection title=\"Test title\">\n        <div>Content</div>\n      </GenericConfigSection>\n    );\n\n    expect(screen.getByText('Test title')).toBeInTheDocument();\n  });\n\n  it('should render title as <h3> by default', () => {\n    render(\n      <GenericConfigSection title=\"Test title\">\n        <div>Content</div>\n      </GenericConfigSection>\n    );\n\n    expect(screen.getByText('Test title').tagName).toBe('H3');\n  });\n\n  it('should render title as <h3> when `kind` is `section`', () => {\n    render(\n      <GenericConfigSection title=\"Test title\" kind=\"section\">\n        <div>Content</div>\n      </GenericConfigSection>\n    );\n\n    expect(screen.getByText('Test title').tagName).toBe('H3');\n  });\n\n  it('should render title as <h6> when `kind` is `sub-section`', () => {\n    render(\n      <GenericConfigSection title=\"Test title\" kind=\"sub-section\">\n        <div>Content</div>\n      </GenericConfigSection>\n    );\n\n    expect(screen.getByText('Test title').tagName).toBe('H6');\n  });\n\n  it('should render description', () => {\n    render(\n      <GenericConfigSection title=\"Test title\" description=\"Test description\">\n        <div>Content</div>\n      </GenericConfigSection>\n    );\n\n    expect(screen.getByText('Test description')).toBeInTheDocument();\n  });\n\n  it('should not be collapsible by default', () => {\n    render(\n      <GenericConfigSection title=\"Test title\">\n        <div>Test content</div>\n      </GenericConfigSection>\n    );\n\n    expect(screen.getByText('Test content')).toBeInTheDocument();\n    expect(() => screen.getByLabelText('Expand section Test title')).toThrow();\n    expect(() => screen.getByLabelText('Collapse section Test title')).toThrow();\n  });\n\n  it('should be collapsible with content visible when `isCollapsible` is `true` and `isInitiallyOpen` is not passed', async () => {\n    render(\n      <GenericConfigSection title=\"Test title\" isCollapsible>\n        <div>Test content</div>\n      </GenericConfigSection>\n    );\n\n    expect(screen.getByText('Test content')).toBeInTheDocument();\n\n    await user.click(screen.getByLabelText('Collapse section Test title'));\n\n    expect(() => screen.getByText('Test content')).toThrow();\n  });\n\n  it('should be collapsible with content visible when `isCollapsible` is `true` and `isInitiallyOpen` is `true`', async () => {\n    render(\n      <GenericConfigSection title=\"Test title\" isCollapsible isInitiallyOpen={true}>\n        <div>Test content</div>\n      </GenericConfigSection>\n    );\n\n    expect(screen.getByText('Test content')).toBeInTheDocument();\n\n    await user.click(screen.getByLabelText('Collapse section Test title'));\n\n    expect(() => screen.getByText('Test content')).toThrow();\n  });\n\n  it('should be collapsible with content hidden when `isCollapsible` is `true` and `isInitiallyOpen` is `false`', async () => {\n    render(\n      <GenericConfigSection title=\"Test title\" isCollapsible isInitiallyOpen={false}>\n        <div>Test content</div>\n      </GenericConfigSection>\n    );\n\n    expect(() => screen.getByText('Test content')).toThrow();\n\n    await user.click(screen.getByLabelText('Expand section Test title'));\n\n    expect(screen.getByText('Test content')).toBeInTheDocument();\n  });\n\n  it('should have passed `className`', () => {\n    const { container } = render(\n      <GenericConfigSection title=\"Test title\" className=\"test-class\">\n        <div>Test content</div>\n      </GenericConfigSection>\n    );\n\n    expect(container.firstChild).toHaveClass('test-class');\n  });\n});\n"
  },
  {
    "path": "src/components/experimental/ConfigSection/GenericConfigSection.tsx",
    "content": "import React, { useState, ReactNode } from 'react';\nimport { css } from '@emotion/css';\nimport { useTheme2, IconButton, IconName } from '@grafana/ui';\n\nexport type Props = {\n  title: string;\n  description?: ReactNode;\n  isCollapsible?: boolean;\n  isInitiallyOpen?: boolean;\n  kind?: 'section' | 'sub-section';\n  className?: string;\n  children: ReactNode;\n};\n\nexport const GenericConfigSection = ({\n  children,\n  title,\n  description,\n  isCollapsible = false,\n  isInitiallyOpen = true,\n  kind = 'section',\n  className,\n}: Props) => {\n  const { colors, typography, spacing } = useTheme2();\n  const [isOpen, setIsOpen] = useState(isCollapsible ? isInitiallyOpen : true);\n  const iconName: IconName = isOpen ? 'angle-up' : 'angle-down';\n  const isSubSection = kind === 'sub-section';\n  const collapsibleButtonAriaLabel = `${isOpen ? 'Collapse' : 'Expand'} section ${title}`;\n\n  const styles = {\n    header: css({\n      display: 'flex',\n      justifyContent: 'space-between',\n      alignItems: 'center',\n    }),\n    title: css({\n      margin: 0,\n    }),\n    subtitle: css({\n      margin: 0,\n      fontWeight: typography.fontWeightRegular,\n    }),\n    descriptionText: css({\n      marginTop: spacing(isSubSection ? 0.25 : 0.5),\n      marginBottom: 0,\n      ...typography.bodySmall,\n      color: colors.text.secondary,\n    }),\n    content: css({\n      marginTop: spacing(2),\n    }),\n  };\n\n  return (\n    <div className={className}>\n      <div className={styles.header}>\n        {kind === 'section' ? <h3 className={styles.title}>{title}</h3> : <h6 className={styles.subtitle}>{title}</h6>}\n        {isCollapsible && (\n          <IconButton\n            name={iconName}\n            onClick={() => setIsOpen(!isOpen)}\n            type=\"button\"\n            size=\"xl\"\n            aria-label={collapsibleButtonAriaLabel}\n          />\n        )}\n      </div>\n      {description && <p className={styles.descriptionText}>{description}</p>}\n      {isOpen && <div className={styles.content}>{children}</div>}\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/experimental/ConfigSection/index.ts",
    "content": "export { ConfigSection } from './ConfigSection';\nexport { ConfigSubSection } from './ConfigSubSection';\nexport { DataSourceDescription } from './DataSourceDescription';\n"
  },
  {
    "path": "src/components/queryBuilder/AggregateEditor.test.tsx",
    "content": "import React from 'react';\nimport { fireEvent, render } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { AggregateEditor } from './AggregateEditor';\nimport { selectors } from 'selectors';\nimport { AggregateColumn, AggregateType } from 'types/queryBuilder';\n\ndescribe('AggregateEditor', () => {\n  it('should render with no aggregates', () => {\n    const result = render(<AggregateEditor allColumns={[]} aggregates={[]} onAggregatesChange={() => {}} />);\n    expect(result.container.firstChild).not.toBeNull();\n  });\n\n  it('should render with aggregates', () => {\n    const testAggregate: AggregateColumn = { aggregateType: AggregateType.Count, column: 'foo', alias: 'f' };\n    const result = render(\n      <AggregateEditor allColumns={[]} aggregates={[testAggregate]} onAggregatesChange={() => {}} />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const firstAggregate = result.getByTestId(selectors.components.QueryBuilder.AggregateEditor.itemWrapper);\n    expect(firstAggregate).toBeInTheDocument();\n  });\n\n  it('should call onAggregatesChange when add aggregate button is clicked', async () => {\n    const onAggregatesChange = jest.fn();\n    const result = render(<AggregateEditor allColumns={[]} aggregates={[]} onAggregatesChange={onAggregatesChange} />);\n    expect(result.container.firstChild).not.toBeNull();\n\n    const addButton = result.getByTestId(selectors.components.QueryBuilder.AggregateEditor.addButton);\n    expect(addButton).toBeInTheDocument();\n    await userEvent.click(addButton);\n    expect(onAggregatesChange).toBeCalledTimes(1);\n    expect(onAggregatesChange).toBeCalledWith([expect.anything()]);\n  });\n\n  it('should call onAggregatesChange when remove aggregate button is clicked', async () => {\n    const testAggregate: AggregateColumn = { aggregateType: AggregateType.Count, column: 'foo', alias: 'f' };\n    const onAggregatesChange = jest.fn();\n    const result = render(\n      <AggregateEditor allColumns={[]} aggregates={[testAggregate]} onAggregatesChange={onAggregatesChange} />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const removeButton = result.getByTestId(selectors.components.QueryBuilder.AggregateEditor.itemRemoveButton);\n    expect(removeButton).toBeInTheDocument();\n    await userEvent.click(removeButton);\n    expect(onAggregatesChange).toBeCalledWith([]);\n  });\n\n  it('should call onAggregatesChange when aggregate is updated', async () => {\n    const inputAggregate: AggregateColumn = { aggregateType: AggregateType.Count, column: 'foo', alias: 'f' };\n    const expectedAggregate: AggregateColumn = { aggregateType: AggregateType.Sum, column: 'foo', alias: 'f' };\n    const onAggregatesChange = jest.fn();\n    const result = render(\n      <AggregateEditor allColumns={[]} aggregates={[inputAggregate]} onAggregatesChange={onAggregatesChange} />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const aggregateSelect = result.getAllByRole('combobox')[0];\n    expect(aggregateSelect).toBeInTheDocument();\n    fireEvent.keyDown(aggregateSelect, { key: 'ArrowDown' });\n    fireEvent.keyDown(aggregateSelect, { key: 'ArrowDown' });\n    fireEvent.keyDown(aggregateSelect, { key: 'Enter' });\n    expect(onAggregatesChange).toBeCalledWith([expectedAggregate]);\n  });\n});\n"
  },
  {
    "path": "src/components/queryBuilder/AggregateEditor.tsx",
    "content": "import React, { useState } from 'react';\nimport { SelectableValue } from '@grafana/data';\nimport { InlineFormLabel, Select, Button, Input, HorizontalGroup } from '@grafana/ui';\nimport { AggregateColumn, AggregateType, TableColumn } from 'types/queryBuilder';\nimport labels from 'labels';\nimport { selectors } from 'selectors';\nimport { styles } from 'styles';\n\ninterface AggregateProps {\n  columnOptions: Array<SelectableValue<string>>;\n  index: number;\n  aggregate: AggregateColumn;\n  updateAggregate: (index: number, aggregate: AggregateColumn) => void;\n  removeAggregate: (index: number) => void;\n}\n\nconst allAggregateOptions: Array<SelectableValue<AggregateType>> = [\n  { label: 'Count', value: AggregateType.Count },\n  { label: 'Sum', value: AggregateType.Sum },\n  { label: 'Min', value: AggregateType.Min },\n  { label: 'Max', value: AggregateType.Max },\n  { label: 'Average', value: AggregateType.Average },\n  { label: 'Any', value: AggregateType.Any },\n  // { label: 'Distinct Count', value: AggregateType.Count_Distinct },\n];\n\nconst Aggregate = (props: AggregateProps) => {\n  const { index, aggregate, updateAggregate, removeAggregate } = props;\n  const [isOpen, setIsOpen] = useState(false);\n  const [alias, setAlias] = useState(aggregate.alias || '');\n  const { aliasLabel } = labels.components.AggregatesEditor;\n\n  // Add current value to aggregate functions\n  const aggregateOptions = allAggregateOptions.slice();\n  if (!aggregateOptions.find((a) => a.value === aggregate.aggregateType)) {\n    aggregateOptions.push({ label: aggregate.aggregateType, value: aggregate.aggregateType });\n  }\n\n  // Add current value to column options\n  const columnOptions = props.columnOptions.slice();\n  if (!columnOptions.find((c) => c.value === aggregate.column)) {\n    columnOptions.push({ label: aggregate.column, value: aggregate.column });\n  }\n\n  return (\n    <HorizontalGroup wrap align=\"flex-start\" justify=\"flex-start\">\n      <Select\n        width={20}\n        className={styles.Common.inlineSelect}\n        options={aggregateOptions}\n        value={aggregate.aggregateType}\n        onChange={(e) => updateAggregate(index, { ...aggregate, aggregateType: e.value! })}\n        menuPlacement={'bottom'}\n        allowCustomValue\n      />\n      <Select<string>\n        width={40}\n        className={styles.Common.inlineSelect}\n        options={columnOptions}\n        isOpen={isOpen}\n        onOpenMenu={() => setIsOpen(true)}\n        onCloseMenu={() => setIsOpen(false)}\n        onChange={(e) => updateAggregate(index, { ...aggregate, column: e.value! })}\n        value={aggregate.column}\n        menuPlacement={'bottom'}\n        allowCustomValue\n      />\n      <InlineFormLabel width={2} className=\"query-keyword\">\n        {aliasLabel}\n      </InlineFormLabel>\n      <Input\n        width={20}\n        value={alias}\n        onChange={(e) => setAlias(e.currentTarget.value)}\n        onBlur={(e) => updateAggregate(index, { ...aggregate, alias: e.currentTarget.value })}\n        placeholder=\"alias\"\n      />\n      <Button\n        data-testid={selectors.components.QueryBuilder.AggregateEditor.itemRemoveButton}\n        className={styles.Common.smallBtn}\n        variant=\"destructive\"\n        size=\"sm\"\n        icon=\"trash-alt\"\n        onClick={() => removeAggregate(index)}\n        aria-label=\"aggregate-remove-item\"\n      />\n    </HorizontalGroup>\n  );\n};\n\ninterface AggregateEditorProps {\n  allColumns: readonly TableColumn[];\n  aggregates: AggregateColumn[];\n  onAggregatesChange: (aggregates: AggregateColumn[]) => void;\n}\n\nconst allColumnName = '*';\n\nexport const AggregateEditor = (props: AggregateEditorProps) => {\n  const { allColumns, aggregates, onAggregatesChange } = props;\n  const { label, tooltip, addLabel } = labels.components.AggregatesEditor;\n  const columnOptions: Array<SelectableValue<string>> = allColumns.map((c) => ({\n    label: c.label || c.name,\n    value: c.name,\n  }));\n  columnOptions.push({ label: allColumnName, value: allColumnName });\n\n  const addAggregate = () => {\n    const nextAggregates: AggregateColumn[] = aggregates.slice();\n    nextAggregates.push({ column: '', aggregateType: AggregateType.Count });\n    onAggregatesChange(nextAggregates);\n  };\n  const removeAggregate = (index: number) => {\n    const nextAggregates: AggregateColumn[] = aggregates.slice();\n    nextAggregates.splice(index, 1);\n    onAggregatesChange(nextAggregates);\n  };\n  const updateAggregate = (index: number, aggregatesItem: AggregateColumn) => {\n    const nextAggregates: AggregateColumn[] = aggregates.slice();\n    nextAggregates[index] = aggregatesItem;\n    onAggregatesChange(nextAggregates);\n  };\n\n  const fieldLabel = (\n    <InlineFormLabel\n      width={8}\n      className=\"query-keyword\"\n      data-testid={selectors.components.QueryBuilder.AggregateEditor.sectionLabel}\n      tooltip={tooltip}\n    >\n      {label}\n    </InlineFormLabel>\n  );\n  const fieldSpacer = <div className={`width-8 ${styles.Common.firstLabel}`}></div>;\n\n  return (\n    <>\n      {aggregates.map((aggregate, index) => {\n        const key = `${index}-${aggregate.column}-${aggregate.aggregateType}-${aggregate.alias}`;\n        return (\n          <div\n            className=\"gf-form\"\n            key={key}\n            data-testid={selectors.components.QueryBuilder.AggregateEditor.itemWrapper}\n          >\n            {index === 0 ? fieldLabel : fieldSpacer}\n            <Aggregate\n              columnOptions={columnOptions}\n              index={index}\n              aggregate={aggregate}\n              updateAggregate={updateAggregate}\n              removeAggregate={removeAggregate}\n            />\n          </div>\n        );\n      })}\n\n      <div className=\"gf-form\">\n        {aggregates.length === 0 ? fieldLabel : fieldSpacer}\n        <Button\n          data-testid={selectors.components.QueryBuilder.AggregateEditor.addButton}\n          icon=\"plus-circle\"\n          variant=\"secondary\"\n          size=\"sm\"\n          onClick={addAggregate}\n          className={styles.Common.smallBtn}\n        >\n          {addLabel}\n        </Button>\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "src/components/queryBuilder/ColumnRolesHelp.tsx",
    "content": "import React from 'react';\nimport { Box, Icon, Text, TextLink } from '@grafana/ui';\n\ninterface ColumnRolesHelpProps {\n  text: string;\n  linkText: string;\n  href: string;\n  testIdWrapper: string;\n  testIdLink: string;\n}\n\n/**\n * Small inline note rendered above the Columns section of the query builder\n * views, giving a one-line description of the column-role concept and a link\n * to the in-repo documentation page that enumerates the role → SQL alias\n * mapping.\n */\nexport const ColumnRolesHelp = (props: ColumnRolesHelpProps) => {\n  const { text, linkText, href, testIdWrapper, testIdLink } = props;\n  return (\n    <Box display=\"flex\" alignItems=\"center\" gap={0.5} marginTop={0.5} marginBottom={1} data-testid={testIdWrapper}>\n      <Text variant=\"bodySmall\" color=\"secondary\">\n        <Icon name=\"info-circle\" size=\"sm\" />\n      </Text>\n      <Text variant=\"bodySmall\" color=\"secondary\">\n        {text}{' '}\n        <TextLink href={href} external inline variant=\"bodySmall\" color=\"secondary\" data-testid={testIdLink}>\n          {linkText}\n        </TextLink>\n      </Text>\n    </Box>\n  );\n};\n"
  },
  {
    "path": "src/components/queryBuilder/ColumnSelect.test.tsx",
    "content": "import React from 'react';\nimport { render, fireEvent } from '@testing-library/react';\nimport { ColumnSelect } from './ColumnSelect';\nimport { SelectedColumn, TableColumn } from 'types/queryBuilder';\n\ndescribe('ColumnSelect', () => {\n  const testLabel = 'Label';\n  const testTooltip = 'Tooltip';\n\n  it('should render with empty properties', () => {\n    const result = render(\n      <ColumnSelect\n        allColumns={[]}\n        selectedColumn={undefined}\n        onColumnChange={() => {}}\n        label={testLabel}\n        tooltip={testTooltip}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n  });\n\n  it('should render with valid properties', () => {\n    const allColumns: readonly TableColumn[] = [{ name: 'foo', type: 'string', picklistValues: [] }];\n    const selectedColumn: SelectedColumn = { name: 'foo' };\n    const result = render(\n      <ColumnSelect\n        allColumns={allColumns}\n        selectedColumn={selectedColumn}\n        onColumnChange={() => {}}\n        label={testLabel}\n        tooltip={testTooltip}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n    expect(result.getByText('foo')).not.toBeUndefined();\n  });\n\n  it('should call onColumnChange when a new column is selected', () => {\n    const allColumns: readonly TableColumn[] = [\n      { name: 'one', type: 'string', picklistValues: [] },\n      { name: 'two', type: 'string', picklistValues: [] },\n    ];\n    const onColumnChange = jest.fn();\n    const result = render(\n      <ColumnSelect\n        allColumns={allColumns}\n        selectedColumn={undefined}\n        onColumnChange={onColumnChange}\n        label={testLabel}\n        tooltip={testTooltip}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const multiSelect = result.getByRole('combobox');\n    expect(multiSelect).toBeInTheDocument();\n    fireEvent.keyDown(multiSelect, { key: 'ArrowDown' });\n    fireEvent.keyDown(multiSelect, { key: 'Enter' });\n    expect(onColumnChange).toHaveBeenCalledTimes(1);\n    expect(onColumnChange).toHaveBeenCalledWith(expect.any(Object));\n  });\n});\n"
  },
  {
    "path": "src/components/queryBuilder/ColumnSelect.tsx",
    "content": "import React from 'react';\nimport { SelectableValue } from '@grafana/data';\nimport { InlineFormLabel, Select } from '@grafana/ui';\nimport { ColumnHint, SelectedColumn, TableColumn } from 'types/queryBuilder';\nimport { styles } from 'styles';\n\ninterface ColumnSelectProps {\n  allColumns: readonly TableColumn[];\n  selectedColumn: SelectedColumn | undefined;\n  onColumnChange: (c: SelectedColumn | undefined) => void;\n  columnFilterFn?: (c: TableColumn) => boolean;\n  columnHint?: ColumnHint;\n  label: string;\n  tooltip: string;\n  disabled?: boolean;\n  invalid?: boolean;\n  wide?: boolean;\n  inline?: boolean;\n  clearable?: boolean;\n}\n\nconst defaultFilterFn = () => true;\n\nexport const ColumnSelect = (props: ColumnSelectProps) => {\n  const {\n    allColumns,\n    selectedColumn,\n    onColumnChange,\n    columnFilterFn,\n    columnHint,\n    label,\n    tooltip,\n    disabled,\n    invalid,\n    wide,\n    inline,\n    clearable,\n  } = props;\n  const selectedColumnName = selectedColumn?.name;\n  const columns: Array<SelectableValue<string>> = allColumns\n    .filter(columnFilterFn || defaultFilterFn)\n    .map((c) => ({ label: c.label || c.name, value: c.name }));\n\n  // Select component WILL NOT display the value if it isn't present in the options.\n  let staleOption = false;\n  if (selectedColumn && !columns.find((c) => c.value === selectedColumn.name)) {\n    columns.push({ label: selectedColumn.alias || selectedColumn.name, value: selectedColumn.name });\n    staleOption = true;\n  }\n\n  const onChange = (selected: SelectableValue<string | undefined>) => {\n    if (!selected || !selected.value) {\n      onColumnChange(undefined);\n      return;\n    }\n\n    const column = allColumns.find((c) => c.name === selected!.value)!;\n    const nextColumn: SelectedColumn = {\n      name: column?.name || selected!.value,\n      type: column?.type,\n      hint: columnHint,\n    };\n\n    if (column && column.label !== undefined) {\n      nextColumn.alias = column.label;\n    }\n\n    onColumnChange(nextColumn);\n  };\n\n  const labelStyle = 'query-keyword ' + (inline ? styles.QueryEditor.inlineField : '');\n\n  return (\n    <div className=\"gf-form\">\n      <InlineFormLabel width={wide ? 12 : 8} className={labelStyle} tooltip={tooltip}>\n        {label}\n      </InlineFormLabel>\n      <Select<string | undefined>\n        disabled={disabled}\n        invalid={invalid || staleOption}\n        options={columns}\n        value={selectedColumnName}\n        placeholder={selectedColumnName || undefined}\n        onChange={onChange}\n        width={wide ? 25 : 20}\n        menuPlacement={'bottom'}\n        isClearable={clearable === undefined || clearable}\n        allowCustomValue\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/queryBuilder/ColumnsEditor.test.tsx",
    "content": "import React from 'react';\nimport { fireEvent, render } from '@testing-library/react';\nimport { ColumnsEditor } from './ColumnsEditor';\nimport { TableColumn, SelectedColumn } from 'types/queryBuilder';\nimport { selectors } from 'selectors';\n\ndescribe('ColumnsEditor', () => {\n  const allColumns: readonly TableColumn[] = [\n    { name: 'name', type: 'string', picklistValues: [] },\n    { name: 'dummy', type: 'string', picklistValues: [] },\n  ];\n  const selectedColumns: SelectedColumn[] = [{ name: 'name' }];\n\n  it('should render default value when no options passed', () => {\n    const result = render(<ColumnsEditor allColumns={[]} selectedColumns={[]} onSelectedColumnsChange={() => {}} />);\n    expect(result.container.firstChild).not.toBeNull();\n    expect(result.getByTestId(selectors.components.QueryBuilder.ColumnsEditor.multiSelectWrapper)).toBeInTheDocument();\n  });\n\n  it('should render the correct values when passed', () => {\n    const result = render(\n      <ColumnsEditor allColumns={allColumns} selectedColumns={selectedColumns} onSelectedColumnsChange={() => {}} />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n    expect(result.getByTestId(selectors.components.QueryBuilder.ColumnsEditor.multiSelectWrapper)).toBeInTheDocument();\n\n    const multiSelect = result.getByRole('combobox');\n    expect(multiSelect).toBeInTheDocument();\n    fireEvent.keyDown(multiSelect, { key: 'ArrowDown' });\n    expect(result.getByText('name')).toBeInTheDocument();\n    expect(result.getByText('dummy')).toBeInTheDocument();\n  });\n\n  it('should call onSelectedColumnsChange when a column is selected', () => {\n    const onSelectedColumnsChange = jest.fn();\n    const result = render(\n      <ColumnsEditor\n        allColumns={allColumns}\n        selectedColumns={selectedColumns}\n        onSelectedColumnsChange={onSelectedColumnsChange}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n    expect(result.getByTestId(selectors.components.QueryBuilder.ColumnsEditor.multiSelectWrapper)).toBeInTheDocument();\n\n    const multiSelect = result.getByRole('combobox');\n    expect(multiSelect).toBeInTheDocument();\n    fireEvent.keyDown(multiSelect, { key: 'ArrowDown' });\n    fireEvent.keyDown(multiSelect, { key: 'ArrowDown' });\n    fireEvent.keyDown(multiSelect, { key: 'Enter' });\n\n    expect(onSelectedColumnsChange).toBeCalledTimes(1);\n    expect(onSelectedColumnsChange).toBeCalledWith([expect.any(Object), expect.any(Object)]);\n  });\n\n  it('should call onSelectedColumnsChange when a column is deselected', () => {\n    const onSelectedColumnsChange = jest.fn();\n    const result = render(\n      <ColumnsEditor\n        allColumns={allColumns}\n        selectedColumns={selectedColumns}\n        onSelectedColumnsChange={onSelectedColumnsChange}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n    expect(result.getByTestId(selectors.components.QueryBuilder.ColumnsEditor.multiSelectWrapper)).toBeInTheDocument();\n\n    const removeButton = result.getByTestId('times'); // find by \"x\" symbol\n    fireEvent.click(removeButton);\n    expect(onSelectedColumnsChange).toBeCalledTimes(1);\n    expect(onSelectedColumnsChange).toBeCalledWith([]);\n  });\n\n  it('should close when clicked outside', () => {\n    const onSelectedColumnsChange = jest.fn();\n    const result = render(\n      <ColumnsEditor\n        allColumns={allColumns}\n        selectedColumns={selectedColumns}\n        onSelectedColumnsChange={onSelectedColumnsChange}\n      />\n    );\n    expect(onSelectedColumnsChange).toHaveBeenCalledTimes(0);\n\n    const multiSelect = result.getByRole('combobox');\n    expect(multiSelect).toBeInTheDocument();\n\n    expect(result.queryAllByText('dummy').length).toBe(0); // is popup closed\n    fireEvent.keyDown(multiSelect, { key: 'ArrowDown' });\n    expect(result.getByText('dummy')).toBeInTheDocument(); // is popup open\n    fireEvent.keyDown(multiSelect, { key: 'Esc' });\n    expect(result.queryAllByText('dummy').length).toBe(0); // is popup closed\n    expect(onSelectedColumnsChange).toHaveBeenCalledTimes(0);\n  });\n});\n"
  },
  {
    "path": "src/components/queryBuilder/ColumnsEditor.tsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport { InlineFormLabel, MultiSelect } from '@grafana/ui';\nimport { SelectableValue } from '@grafana/data';\nimport { TableColumn, SelectedColumn } from 'types/queryBuilder';\nimport labels from 'labels';\nimport { selectors } from 'selectors';\nimport { styles } from 'styles';\n\ninterface ColumnsEditorProps {\n  allColumns: readonly TableColumn[];\n  selectedColumns: SelectedColumn[];\n  onSelectedColumnsChange: (selectedColumns: SelectedColumn[]) => void;\n  disabled?: boolean;\n  showAllOption?: boolean;\n}\n\nfunction getCustomColumns(columnNames: string[], allColumns: readonly TableColumn[]): Array<SelectableValue<string>> {\n  const columnNamesSet = new Set(columnNames);\n  return allColumns.filter((c) => columnNamesSet.has(c.name)).map((c) => ({ label: c.label || c.name, value: c.name }));\n}\n\nconst allColumnName = '*';\n\nexport const ColumnsEditor = (props: ColumnsEditorProps) => {\n  const { allColumns, selectedColumns, onSelectedColumnsChange, disabled, showAllOption } = props;\n  const [customColumns, setCustomColumns] = useState<Array<SelectableValue<string>>>([]);\n  const [isOpen, setIsOpen] = useState(false);\n  const allColumnNames = allColumns.map((c) => ({ label: c.label || c.name, value: c.name }));\n  if (showAllOption) {\n    allColumnNames.push({ label: allColumnName, value: allColumnName });\n  }\n  const selectedColumnNames = (selectedColumns || []).map((c) => ({ label: c.alias || c.name, value: c.name }));\n  const { label, tooltip } = labels.components.ColumnsEditor;\n\n  const options = [...allColumnNames, ...customColumns];\n\n  useEffect(() => {\n    if (allColumns.length === 0) {\n      return;\n    }\n\n    const columnNames = selectedColumns.map((c) => c.name);\n    const customColumns = getCustomColumns(columnNames, allColumns);\n    setCustomColumns(customColumns);\n  }, [allColumns, selectedColumns]);\n\n  const onChange = (selected: Array<SelectableValue<string>>): void => {\n    setIsOpen(false);\n    const selectedColumnNames = new Set<string>(selected.map((s) => s.value!));\n    const customColumnNames = new Set<string>(customColumns.map((c) => c.value!));\n    const columnMap = new Map<string, TableColumn>();\n    const currentColumnMap = new Map<string, SelectedColumn>();\n    allColumns.forEach((c) => columnMap.set(c.name, c));\n    selectedColumns.forEach((c) => currentColumnMap.set(c.name, c));\n\n    const excludeAllColumn = selectedColumnNames.size > 1;\n    const nextSelectedColumns: SelectedColumn[] = [];\n    for (let columnName of selectedColumnNames) {\n      if (excludeAllColumn && columnName === allColumnName) {\n        continue;\n      }\n\n      const tableColumn = columnMap.get(columnName);\n      const existingColumn = currentColumnMap.get(columnName);\n\n      if (existingColumn) {\n        nextSelectedColumns.push(existingColumn);\n      } else {\n        nextSelectedColumns.push({\n          name: columnName,\n          type: tableColumn?.type || 'String',\n          custom: customColumnNames.has(columnName),\n          alias: tableColumn?.label || columnName,\n        });\n      }\n    }\n\n    onSelectedColumnsChange(nextSelectedColumns);\n  };\n\n  return (\n    <div className=\"gf-form\">\n      <InlineFormLabel width={8} className=\"query-keyword\" tooltip={tooltip}>\n        {label}\n      </InlineFormLabel>\n      <div\n        data-testid={selectors.components.QueryBuilder.ColumnsEditor.multiSelectWrapper}\n        className={styles.Common.selectWrapper}\n      >\n        <MultiSelect<string>\n          disabled={disabled}\n          options={options}\n          value={selectedColumnNames}\n          isOpen={isOpen}\n          onOpenMenu={() => setIsOpen(true)}\n          onCloseMenu={() => setIsOpen(false)}\n          onChange={onChange}\n          allowCustomValue={true}\n          menuPlacement={'bottom'}\n        />\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/queryBuilder/DatabaseTableSelect.test.tsx",
    "content": "import React from 'react';\nimport { fireEvent, render, waitFor } from '@testing-library/react';\nimport { DatabaseSelect, TableSelect, DatabaseTableSelect } from './DatabaseTableSelect';\nimport { Datasource } from '../../data/CHDatasource';\n\nconst defaultDB = 'default';\nconst testTable = 'samples';\n\ndescribe('DatabaseSelect', () => {\n  it('should render with empty options', async () => {\n    const mockDs = {} as Datasource;\n    mockDs.getDefaultDatabase = jest.fn(() => '');\n    mockDs.fetchDatabases = jest.fn(() => Promise.resolve([]));\n\n    const result = await waitFor(() =>\n      render(<DatabaseSelect datasource={mockDs} database=\"\" onDatabaseChange={() => {}} />)\n    );\n    expect(result.container.firstChild).not.toBeNull();\n  });\n\n  it('should render with valid options', async () => {\n    const mockDs = {} as Datasource;\n    mockDs.getDefaultDatabase = jest.fn(() => defaultDB);\n    mockDs.fetchDatabases = jest.fn(() => Promise.resolve([defaultDB]));\n\n    const result = await waitFor(() =>\n      render(<DatabaseSelect datasource={mockDs} database=\"default\" onDatabaseChange={() => {}} />)\n    );\n    expect(result.container.firstChild).not.toBeNull();\n    expect(result.getByText(defaultDB)).toBeInTheDocument();\n  });\n\n  it('selects a default database when none is provided', async () => {\n    const mockDs = {} as Datasource;\n    mockDs.getDefaultDatabase = jest.fn(() => defaultDB);\n    mockDs.fetchDatabases = jest.fn(() => Promise.resolve([defaultDB]));\n    const onDatabaseChange = jest.fn();\n\n    const result = await waitFor(() =>\n      render(<DatabaseSelect datasource={mockDs} database=\"\" onDatabaseChange={onDatabaseChange} />)\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    expect(onDatabaseChange).toBeCalledTimes(1);\n    expect(onDatabaseChange).toBeCalledWith(defaultDB);\n  });\n\n  it('should call onDatabaseChange when a database is selected', async () => {\n    const mockDs = {} as Datasource;\n    mockDs.getDefaultDatabase = jest.fn(() => defaultDB);\n    mockDs.fetchDatabases = jest.fn(() => Promise.resolve([defaultDB]));\n    const onDatabaseChange = jest.fn();\n\n    const result = await waitFor(() =>\n      render(<DatabaseSelect datasource={mockDs} database=\"other\" onDatabaseChange={onDatabaseChange} />)\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const multiSelect = result.getByRole('combobox');\n    expect(multiSelect).toBeInTheDocument();\n    fireEvent.keyDown(multiSelect, { key: 'ArrowDown' }); // \"other\" db, a custom value\n    fireEvent.keyDown(multiSelect, { key: 'ArrowDown' }); // \"default\" db\n    fireEvent.keyDown(multiSelect, { key: 'Enter' });\n    expect(onDatabaseChange).toBeCalledTimes(1);\n    expect(onDatabaseChange).toBeCalledWith(defaultDB);\n  });\n});\n\ndescribe('TableSelect', () => {\n  it('should render with empty options', async () => {\n    const mockDs = {} as Datasource;\n    mockDs.fetchTables = jest.fn(() => Promise.resolve([]));\n\n    const result = await waitFor(() =>\n      render(<TableSelect datasource={mockDs} database=\"\" table=\"\" onTableChange={() => {}} />)\n    );\n    expect(result.container.firstChild).not.toBeNull();\n  });\n\n  it('should render with valid options', async () => {\n    const mockDs = {} as Datasource;\n    mockDs.fetchTables = jest.fn(() => Promise.resolve([testTable]));\n\n    const result = await waitFor(() =>\n      render(<TableSelect datasource={mockDs} database={defaultDB} table={testTable} onTableChange={() => {}} />)\n    );\n    expect(result.container.firstChild).not.toBeNull();\n    expect(result.getByText(testTable)).toBeInTheDocument();\n  });\n\n  // TODO: this hook is disabled in the component for now\n  // it('selects a default table when none is provided', async () => {\n  //   const mockDs = {} as Datasource;\n  //   mockDs.fetchTables = jest.fn(() => Promise.resolve([testTable]));\n  //   const onTableChange = jest.fn();\n\n  //   const result = await waitFor(() => render(<TableSelect datasource={mockDs} database={defaultDB} table=\"\" onTableChange={onTableChange} />));\n  //   expect(result.container.firstChild).not.toBeNull();\n\n  //   expect(onTableChange).toBeCalledTimes(1);\n  //   expect(onTableChange).toBeCalledWith(testTable);\n  // });\n\n  it('should call onTableChange when a table is selected', async () => {\n    const mockDs = {} as Datasource;\n    mockDs.fetchTables = jest.fn(() => Promise.resolve([testTable]));\n    const onTableChange = jest.fn();\n\n    const result = await waitFor(() =>\n      render(<TableSelect datasource={mockDs} database={defaultDB} table=\"other\" onTableChange={onTableChange} />)\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const multiSelect = result.getByRole('combobox');\n    expect(multiSelect).toBeInTheDocument();\n    fireEvent.keyDown(multiSelect, { key: 'ArrowDown' }); // \"other\" table, a custom value\n    fireEvent.keyDown(multiSelect, { key: 'ArrowDown' }); // test table\n    fireEvent.keyDown(multiSelect, { key: 'Enter' });\n    expect(onTableChange).toBeCalledTimes(1);\n    expect(onTableChange).toBeCalledWith(testTable);\n  });\n});\n\ndescribe('DatabaseTableSelect', () => {\n  it('should render the combined components', async () => {\n    const mockDs = {} as Datasource;\n    mockDs.fetchDatabases = jest.fn(() => Promise.resolve([]));\n    mockDs.fetchTables = jest.fn(() => Promise.resolve([]));\n\n    const result = await waitFor(() =>\n      render(\n        <DatabaseTableSelect\n          datasource={mockDs}\n          database={defaultDB}\n          onDatabaseChange={() => {}}\n          table={testTable}\n          onTableChange={() => {}}\n        />\n      )\n    );\n    expect(result.container.firstChild).not.toBeNull();\n    expect(result.container.firstChild?.childNodes).toHaveLength(2 * 2); // 2 components with a fragment of 2 components\n  });\n});\n"
  },
  {
    "path": "src/components/queryBuilder/DatabaseTableSelect.tsx",
    "content": "import React, { useEffect } from 'react';\nimport { InlineFormLabel, Select } from '@grafana/ui';\nimport { Datasource } from '../../data/CHDatasource';\nimport labels from 'labels';\nimport { styles } from '../../styles';\nimport useTables from 'hooks/useTables';\nimport useDatabases from 'hooks/useDatabases';\n\nexport type DatabaseSelectProps = {\n  datasource: Datasource;\n  database: string;\n  onDatabaseChange: (value: string) => void;\n};\n\nexport const DatabaseSelect = (props: DatabaseSelectProps) => {\n  const { datasource, onDatabaseChange, database } = props;\n  const databases = useDatabases(datasource);\n  const { label, tooltip, empty } = labels.components.DatabaseSelect;\n\n  const options = databases.map((d) => ({ label: d, value: d }));\n  options.push({ label: empty, value: '' }); // Allow a blank value\n\n  // Add selected value to the list if it does not exist.\n  // When loading an existing query, the saved value may no longer be in the list\n  if (database && !databases.includes(database)) {\n    options.push({ label: database, value: database });\n  }\n\n  useEffect(() => {\n    // Auto select default db\n    if (!database) {\n      onDatabaseChange(datasource.getDefaultDatabase());\n    }\n  }, [datasource, database, onDatabaseChange]);\n\n  return (\n    <>\n      <InlineFormLabel width={8} className=\"query-keyword\" tooltip={tooltip}>\n        {label}\n      </InlineFormLabel>\n      <Select\n        className={`width-15 ${styles.Common.inlineSelect}`}\n        options={options}\n        value={database}\n        onChange={(e) => onDatabaseChange(e.value!)}\n        menuPlacement={'bottom'}\n        allowCustomValue\n      ></Select>\n    </>\n  );\n};\n\nexport type TableSelectProps = {\n  datasource: Datasource;\n  database: string;\n  table: string;\n  onTableChange: (value: string) => void;\n};\n\nexport const TableSelect = (props: TableSelectProps) => {\n  const { datasource, onTableChange, database, table } = props;\n  const tables = useTables(datasource, database);\n  const { label, tooltip, empty } = labels.components.TableSelect;\n\n  const options = tables.map((t) => ({ label: t, value: t }));\n  options.push({ label: empty, value: '' }); // Allow a blank value\n\n  // Include saved value in case it's no longer listed\n  if (table && !tables.includes(table)) {\n    options.push({ label: table, value: table });\n  }\n\n  useEffect(() => {\n    // Auto select first/default table\n    if (database && !table && tables.length > 0) {\n      onTableChange(datasource.getDefaultTable() || tables[0]);\n    }\n  }, [database, table, tables, datasource, onTableChange]);\n\n  return (\n    <>\n      <InlineFormLabel width={8} className=\"query-keyword\" tooltip={tooltip}>\n        {label}\n      </InlineFormLabel>\n      <Select\n        className={`width-15 ${styles.Common.inlineSelect}`}\n        options={options}\n        value={table}\n        onChange={(e) => onTableChange(e.value!)}\n        menuPlacement={'bottom'}\n        allowCustomValue\n      ></Select>\n    </>\n  );\n};\n\nexport type DatabaseTableSelectProps = {\n  datasource: Datasource;\n  database: string;\n  onDatabaseChange: (value: string) => void;\n  table: string;\n  onTableChange: (value: string) => void;\n};\n\nexport const DatabaseTableSelect = (props: DatabaseTableSelectProps) => {\n  const { datasource, database, onDatabaseChange, table, onTableChange } = props;\n\n  return (\n    <div className=\"gf-form\">\n      <DatabaseSelect datasource={datasource} database={database} onDatabaseChange={onDatabaseChange} />\n      <TableSelect datasource={datasource} database={database} table={table} onTableChange={onTableChange} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/queryBuilder/DurationUnitSelect.tsx",
    "content": "import React from 'react';\nimport { TimeUnit } from 'types/queryBuilder';\nimport allLabels from 'labels';\nimport { InlineFormLabel, Select } from '@grafana/ui';\nimport { SelectableValue } from '@grafana/data';\nimport { styles } from 'styles';\n\ninterface DurationUnitSelectProps {\n  unit: TimeUnit;\n  onChange: (u: TimeUnit) => void;\n  disabled?: boolean;\n  inline?: boolean;\n}\n\nconst durationUnitOptions: ReadonlyArray<SelectableValue<TimeUnit>> = [\n  { label: TimeUnit.Seconds, value: TimeUnit.Seconds },\n  { label: TimeUnit.Milliseconds, value: TimeUnit.Milliseconds },\n  { label: TimeUnit.Microseconds, value: TimeUnit.Microseconds },\n  { label: TimeUnit.Nanoseconds, value: TimeUnit.Nanoseconds },\n];\n\nexport const DurationUnitSelect = (props: DurationUnitSelectProps) => {\n  const { unit, onChange, disabled, inline } = props;\n  const { label, tooltip } = allLabels.components.TraceQueryBuilder.columns.durationUnit;\n\n  return (\n    <div className=\"gf-form\">\n      <InlineFormLabel\n        width={12}\n        className={`query-keyword ${inline ? styles.QueryEditor.inlineField : ''}`}\n        tooltip={tooltip}\n      >\n        {label}\n      </InlineFormLabel>\n      <Select<TimeUnit>\n        disabled={disabled}\n        options={durationUnitOptions as Array<SelectableValue<TimeUnit>>}\n        value={unit}\n        onChange={(v) => onChange(v.value!)}\n        width={inline ? 25 : 30}\n        menuPlacement={'bottom'}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/queryBuilder/EditorTypeSwitcher.test.tsx",
    "content": "import React from 'react';\nimport { render, waitFor } from '@testing-library/react';\nimport { EditorTypeSwitcher } from './EditorTypeSwitcher';\nimport { CHQuery, CHSqlQuery, EditorType } from 'types/sql';\nimport labels from 'labels';\n\nconst options = {\n  SQLEditor: labels.types.EditorType.sql,\n  QueryBuilder: labels.types.EditorType.builder,\n};\n\ndescribe('EditorTypeSwitcher', () => {\n  it('should render default query', () => {\n    const result = render(\n      <EditorTypeSwitcher\n        query={{ refId: 'A', editorType: EditorType.Builder } as CHQuery}\n        onChange={() => {}}\n        onRunQuery={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n    expect(result.getByLabelText(options.SQLEditor)).not.toBeChecked();\n    expect(result.getByLabelText(options.QueryBuilder)).toBeChecked();\n  });\n\n  it('should render legacy query (query without query type)', () => {\n    const result = render(\n      <EditorTypeSwitcher\n        query={{ refId: 'A', rawSql: 'hello', editorType: EditorType.SQL } as CHSqlQuery}\n        onChange={() => {}}\n        onRunQuery={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n    expect(result.getByLabelText(options.SQLEditor)).toBeChecked();\n    expect(result.getByLabelText(options.QueryBuilder)).not.toBeChecked();\n  });\n\n  it('should render SQL editor', () => {\n    const result = render(\n      <EditorTypeSwitcher\n        query={{\n          pluginVersion: '',\n          refId: 'A',\n          editorType: EditorType.SQL,\n          rawSql: '',\n        }}\n        onChange={() => {}}\n        onRunQuery={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n    expect(result.getByLabelText(options.SQLEditor)).toBeChecked();\n    expect(result.getByLabelText(options.QueryBuilder)).not.toBeChecked();\n  });\n\n  it('should render Query Builder', () => {\n    const result = render(\n      <EditorTypeSwitcher\n        query={\n          {\n            pluginVersion: '',\n            refId: 'A',\n            editorType: EditorType.Builder,\n            rawSql: '',\n          } as CHQuery\n        }\n        onChange={() => {}}\n        onRunQuery={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n    expect(result.getByLabelText(options.SQLEditor)).not.toBeChecked();\n    expect(result.getByLabelText(options.QueryBuilder)).toBeChecked();\n  });\n\n  it('should show cannot convert modal when switching from SQL to Builder and SQL is invalid', () => {\n    const query = {\n      refId: 'A',\n      editorType: EditorType.SQL,\n      rawSql: 'INVALID SQL',\n      queryType: 'table',\n    } as unknown as CHQuery;\n\n    const { getByLabelText, getByText } = render(\n      <EditorTypeSwitcher query={query} onChange={() => {}} onRunQuery={() => {}} />\n    );\n\n    getByLabelText(options.QueryBuilder).click();\n\n    expect(getByText(labels.components.EditorTypeSwitcher.cannotConvert.title)).toBeInTheDocument();\n  });\n\n  it('should show confirm modal when switching from SQL to Builder and SQL is valid', () => {\n    const query = {\n      refId: 'A',\n      editorType: EditorType.SQL,\n      rawSql: 'SELECT * FROM testTable',\n      queryType: 'table',\n    } as unknown as CHQuery;\n\n    const { getByLabelText, getByText } = render(\n      <EditorTypeSwitcher query={query} onChange={() => {}} onRunQuery={() => {}} />\n    );\n\n    getByLabelText(options.QueryBuilder).click();\n\n    expect(getByText(labels.components.EditorTypeSwitcher.switcher.title)).toBeInTheDocument();\n    expect(getByText(labels.components.EditorTypeSwitcher.switcher.body)).toBeInTheDocument();\n  });\n\n  it('should fire onChange after selecting Continue', async () => {\n    const query = {\n      refId: 'A',\n      editorType: EditorType.SQL,\n      rawSql: 'SELECT * FROM testTable',\n      queryType: 'table',\n    } as unknown as CHQuery;\n\n    const onChangeMock = jest.fn();\n\n    const { getByLabelText, getByText } = render(\n      <EditorTypeSwitcher query={query} onChange={onChangeMock} onRunQuery={() => {}} />\n    );\n\n    getByLabelText(options.QueryBuilder).click();\n\n    const continueButton = getByText('Continue');\n    continueButton.click();\n    await waitFor(() => expect(onChangeMock).toHaveBeenCalled());\n  });\n\n  it('should not fire onChange after selecting Cancel', async () => {\n    const query = {\n      refId: 'A',\n      editorType: EditorType.SQL,\n      rawSql: 'SELECT * FROM testTable',\n      queryType: 'table',\n    } as unknown as CHQuery;\n\n    const onChangeMock = jest.fn();\n\n    const { getByLabelText, getByText } = render(\n      <EditorTypeSwitcher query={query} onChange={onChangeMock} onRunQuery={() => {}} />\n    );\n\n    getByLabelText(options.QueryBuilder).click();\n\n    const continueButton = getByText('Cancel');\n    continueButton.click();\n    await waitFor(() => expect(onChangeMock).not.toHaveBeenCalled());\n  });\n});\n"
  },
  {
    "path": "src/components/queryBuilder/EditorTypeSwitcher.tsx",
    "content": "import React, { useState } from 'react';\nimport { SelectableValue } from '@grafana/data';\nimport { RadioButtonGroup, ConfirmModal, InlineFormLabel } from '@grafana/ui';\nimport { getQueryOptionsFromSql } from '../queryBuilder/utils';\nimport { generateSql } from 'data/sqlGenerator';\nimport labels from 'labels';\nimport { EditorType, CHQuery, defaultCHBuilderQuery } from 'types/sql';\nimport { QueryBuilderOptions } from 'types/queryBuilder';\nimport { mapQueryTypeToGrafanaFormat } from 'data/utils';\nimport { Datasource } from 'data/CHDatasource';\n\ninterface CHEditorTypeSwitcherProps {\n  query: CHQuery;\n  onChange: (query: CHQuery) => void;\n  onRunQuery: () => void;\n  datasource?: Datasource;\n}\n\nconst options: Array<SelectableValue<EditorType>> = [\n  { label: labels.types.EditorType.sql, value: EditorType.SQL },\n  { label: labels.types.EditorType.builder, value: EditorType.Builder },\n];\n\n/**\n * Component for switching between the SQL and Query Builder editors.\n */\nexport const EditorTypeSwitcher = (props: CHEditorTypeSwitcherProps) => {\n  const { datasource, query, onChange } = props;\n  const { label, tooltip, switcher, cannotConvert } = labels.components.EditorTypeSwitcher;\n  const editorType: EditorType = query.editorType || EditorType.Builder;\n  const [confirmModalState, setConfirmModalState] = useState<boolean>(false);\n  const [cannotConvertModalState, setCannotConvertModalState] = useState<boolean>(false);\n  const [errorMessage, setErrorMessage] = useState<string>('');\n  const onEditorTypeChange = (editorType: EditorType, confirmed = false) => {\n    // TODO: component state has updated, but not local state.\n    if (query.editorType === EditorType.SQL && editorType === EditorType.Builder && !confirmed) {\n      try {\n        getQueryOptionsFromSql(query.rawSql, query.queryType, datasource);\n        setConfirmModalState(true);\n      } catch (err) {\n        setCannotConvertModalState(true);\n        setErrorMessage((err as Error).message);\n      }\n    } else {\n      let builderOptions: QueryBuilderOptions;\n      switch (query.editorType) {\n        case EditorType.Builder:\n          builderOptions = query.builderOptions;\n          break;\n        case EditorType.SQL:\n          try {\n            builderOptions = getQueryOptionsFromSql(query.rawSql, query.queryType, datasource) as QueryBuilderOptions;\n          } catch (err) {\n            builderOptions = defaultCHBuilderQuery.builderOptions;\n          }\n          break;\n        default:\n          builderOptions = defaultCHBuilderQuery.builderOptions;\n          break;\n      }\n\n      if (editorType === EditorType.SQL) {\n        onChange({\n          ...query,\n          editorType: EditorType.SQL,\n          queryType: builderOptions.queryType,\n          rawSql: generateSql(builderOptions),\n          format: mapQueryTypeToGrafanaFormat(builderOptions.queryType),\n          meta: { builderOptions },\n        });\n      } else if (editorType === EditorType.Builder) {\n        onChange({\n          ...query,\n          editorType: EditorType.Builder,\n          queryType: builderOptions.queryType,\n          rawSql: generateSql(builderOptions),\n          builderOptions,\n        });\n      }\n    }\n  };\n  const onConfirmEditorTypeChange = () => {\n    onEditorTypeChange(EditorType.Builder, true);\n    setConfirmModalState(false);\n    setCannotConvertModalState(false);\n  };\n  return (\n    <span>\n      <InlineFormLabel width={8} className=\"query-keyword\" tooltip={tooltip}>\n        {label}\n      </InlineFormLabel>\n      <RadioButtonGroup options={options} value={editorType} onChange={(e) => onEditorTypeChange(e)} />\n      <ConfirmModal\n        isOpen={confirmModalState}\n        title={switcher.title}\n        body={switcher.body}\n        confirmText={switcher.confirmText}\n        dismissText={switcher.dismissText}\n        icon=\"exclamation-triangle\"\n        onConfirm={onConfirmEditorTypeChange}\n        onDismiss={() => setConfirmModalState(false)}\n      />\n      <ConfirmModal\n        title={cannotConvert.title}\n        body={`${errorMessage}\\n${cannotConvert.message}`}\n        isOpen={cannotConvertModalState}\n        icon=\"exclamation-triangle\"\n        onConfirm={onConfirmEditorTypeChange}\n        confirmText={switcher.confirmText}\n        onDismiss={() => setCannotConvertModalState(false)}\n      />\n    </span>\n  );\n};\n"
  },
  {
    "path": "src/components/queryBuilder/FilterEditor.test.tsx",
    "content": "import React from 'react';\nimport { fireEvent, render } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { defaultNewFilter, FilterEditor, FiltersEditor, FilterValueEditor } from './FilterEditor';\nimport { selectors } from 'selectors';\nimport {\n  BooleanFilter,\n  DateFilter,\n  Filter,\n  FilterOperator,\n  MultiFilter,\n  NumberFilter,\n  StringFilter,\n} from 'types/queryBuilder';\nimport { mockDatasource } from '__mocks__/datasource';\n\ndescribe('FilterEditor', () => {\n  describe('FiltersEditor', () => {\n    it('renders correctly', async () => {\n      const onFiltersChange = jest.fn();\n      const result = render(\n        <FiltersEditor\n          allColumns={[]}\n          filters={[]}\n          onFiltersChange={onFiltersChange}\n          datasource={mockDatasource}\n          database=\"\"\n          table=\"\"\n        />\n      );\n      expect(result.container.firstChild).not.toBeNull();\n      expect(result.getAllByText(selectors.components.QueryEditor.QueryBuilder.WHERE.label).length).toBe(1);\n      expect(result.getByTestId('query-builder-filters-add-button')).toBeInTheDocument();\n      expect(onFiltersChange).toBeCalledTimes(0);\n      await userEvent.click(result.getByTestId('query-builder-filters-add-button'));\n      expect(onFiltersChange).toBeCalledTimes(1);\n      expect(onFiltersChange).toHaveBeenCalledWith([defaultNewFilter]);\n    });\n    it('should render buttons and labels correctly', async () => {\n      const filters: Filter[] = [\n        {\n          filterType: 'custom',\n          condition: 'AND',\n          key: 'StageName',\n          type: 'string',\n          operator: FilterOperator.IsNotNull,\n        },\n        {\n          filterType: 'custom',\n          condition: 'AND',\n          key: 'Type',\n          type: 'string',\n          operator: FilterOperator.IsNotNull,\n        },\n      ];\n      const result = render(\n        <FiltersEditor\n          allColumns={[]}\n          filters={filters}\n          onFiltersChange={() => {}}\n          datasource={mockDatasource}\n          database=\"\"\n          table=\"\"\n        />\n      );\n      expect(result.container.firstChild).not.toBeNull();\n      expect(result.getAllByText(selectors.components.QueryEditor.QueryBuilder.WHERE.label).length).toBe(1);\n      expect(result.queryByTestId('query-builder-filters-add-button')).not.toBeInTheDocument();\n      expect(result.getByTestId('query-builder-filters-inline-add-button')).toBeInTheDocument();\n      expect(result.getAllByTestId('query-builder-filters-inline-add-button').length).toBe(1);\n      expect(result.getAllByTestId('query-builder-filters-remove-button').length).toBe(filters.length);\n    });\n    it('should call the onFiltersChange with correct args', async () => {\n      const filters: Filter[] = [\n        {\n          filterType: 'custom',\n          condition: 'AND',\n          key: 'StageName',\n          type: 'string',\n          operator: FilterOperator.IsNotNull,\n        },\n        {\n          filterType: 'custom',\n          condition: 'AND',\n          key: 'Type',\n          type: 'string',\n          operator: FilterOperator.IsNotNull,\n        },\n      ];\n      const onFiltersChange = jest.fn();\n      const result = render(\n        <FiltersEditor\n          allColumns={[]}\n          filters={filters}\n          onFiltersChange={onFiltersChange}\n          datasource={mockDatasource}\n          database=\"\"\n          table=\"\"\n        />\n      );\n      expect(result.container.firstChild).not.toBeNull();\n      expect(result.getAllByText(selectors.components.QueryEditor.QueryBuilder.WHERE.label).length).toBe(1);\n      expect(result.queryByTestId('query-builder-filters-add-button')).not.toBeInTheDocument();\n      expect(result.getByTestId('query-builder-filters-inline-add-button')).toBeInTheDocument();\n      expect(result.getAllByTestId('query-builder-filters-inline-add-button').length).toBe(1);\n      expect(result.getAllByTestId('query-builder-filters-remove-button').length).toBe(filters.length);\n      await userEvent.click(result.getByTestId('query-builder-filters-inline-add-button'));\n      expect(onFiltersChange).toBeCalledTimes(1);\n      expect(onFiltersChange).toHaveBeenNthCalledWith(1, [...filters, defaultNewFilter]);\n      await userEvent.click(result.getAllByTestId('query-builder-filters-remove-button')[0]);\n      expect(onFiltersChange).toBeCalledTimes(2);\n      expect(onFiltersChange).toHaveBeenNthCalledWith(2, [filters[1]]);\n    });\n  });\n\n  describe('FilterEditor', () => {\n    it('renders correctly', async () => {\n      const result = render(\n        <FilterEditor\n          allColumns={[]}\n          filter={{\n            key: 'foo',\n            operator: FilterOperator.IsNotNull,\n            type: 'boolean',\n            condition: 'AND',\n            filterType: 'custom',\n          }}\n          index={0}\n          onFilterChange={() => {}}\n          removeFilter={() => {}}\n          datasource={mockDatasource}\n          database=\"\"\n          table=\"\"\n        />\n      );\n      expect(result.container.firstChild).not.toBeNull();\n    });\n    it('should have all provided fields in the select', async () => {\n      const result = render(\n        <FilterEditor\n          allColumns={[\n            { name: 'col1', type: 'string', picklistValues: [] },\n            { name: 'col2', type: 'string', picklistValues: [] },\n            { name: 'col3', type: 'string', picklistValues: [] },\n          ]}\n          filter={{\n            key: 'foo',\n            operator: FilterOperator.IsNotNull,\n            type: 'boolean',\n            condition: 'AND',\n            filterType: 'custom',\n          }}\n          index={0}\n          onFilterChange={() => {}}\n          removeFilter={() => {}}\n          datasource={mockDatasource}\n          database=\"\"\n          table=\"\"\n        />\n      );\n\n      // expand the `fieldName` select box\n      await userEvent.type(result.getAllByRole('combobox')[0], '{ArrowDown}');\n\n      expect(result.getByText('col1')).toBeInTheDocument();\n      expect(result.getByText('col2')).toBeInTheDocument();\n      expect(result.getByText('col3')).toBeInTheDocument();\n    });\n    it('should call onFilterChange when user adds correct custom filter for the field with Map type', async () => {\n      const onFilterChange = jest.fn();\n      const result = render(\n        <FilterEditor\n          allColumns={[{ name: 'colName', type: 'Map(String, String)', picklistValues: [] }]}\n          filter={{\n            key: 'foo',\n            type: 'boolean',\n            operator: FilterOperator.IsNotNull,\n            condition: 'AND',\n            filterType: 'custom',\n          }}\n          index={0}\n          onFilterChange={onFilterChange}\n          removeFilter={() => {}}\n          datasource={mockDatasource}\n          database=\"\"\n          table=\"\"\n        />\n      );\n\n      // type into the `fieldName` select box\n      await userEvent.type(result!.getAllByRole('combobox')[0], `colName[['keyName']`);\n      await userEvent.keyboard('{Enter}');\n\n      const expectedFilter: Filter = {\n        key: `colName['keyName']`,\n        type: 'String',\n        operator: FilterOperator.IsNotNull,\n        condition: 'AND',\n        filterType: 'custom',\n      };\n\n      expect(onFilterChange).toHaveBeenCalledWith(0, expectedFilter);\n    });\n\n    it('should render key input for map type', async () => {\n      const onFilterChange = jest.fn();\n      const result = render(\n        <FilterEditor\n          allColumns={[{ name: 'SpanAttributes', type: 'Map(String, String)', picklistValues: [] }]}\n          filter={{\n            key: 'SpanAttributes',\n            type: 'Map(String, String)',\n            value: '',\n            operator: FilterOperator.Equals,\n            condition: 'AND',\n            filterType: 'custom',\n          }}\n          index={0}\n          onFilterChange={onFilterChange}\n          removeFilter={() => {}}\n          datasource={mockDatasource}\n          database=\"\"\n          table=\"\"\n        />\n      );\n\n      // type key into the mapKey input\n      await userEvent.type(result!.getAllByRole('combobox')[1], 'http.status_code');\n      await userEvent.keyboard('{Enter}');\n\n      result.rerender(\n        <FilterEditor\n          allColumns={[{ name: 'SpanAttributes', type: 'Map(String, String)', picklistValues: [] }]}\n          filter={{\n            key: 'SpanAttributes',\n            type: 'Map(String, String)',\n            mapKey: 'http.status_code',\n            value: '',\n            operator: FilterOperator.Equals,\n            condition: 'AND',\n            filterType: 'custom',\n          }}\n          index={0}\n          onFilterChange={onFilterChange}\n          removeFilter={() => {}}\n          datasource={mockDatasource}\n          database=\"\"\n          table=\"\"\n        />\n      );\n\n      // type value into the input\n      await userEvent.type(result!.getByTestId('query-builder-filters-single-string-value-input'), '200');\n      result!.getByTestId('query-builder-filters-single-string-value-input').blur();\n\n      const expectedFilter: Filter = {\n        key: `SpanAttributes`,\n        mapKey: 'http.status_code',\n        value: '200',\n        type: 'Map(String, String)',\n        operator: FilterOperator.Equals,\n        condition: 'AND',\n        filterType: 'custom',\n      };\n\n      expect(onFilterChange).toHaveBeenCalledTimes(2);\n      expect(onFilterChange).toHaveBeenLastCalledWith(0, expectedFilter);\n    });\n  });\n\n  describe('FilterValueEditor', () => {\n    it('should render nothing for null operator', async () => {\n      const result = render(\n        <FilterValueEditor\n          allColumns={[]}\n          filter={{\n            key: 'foo',\n            operator: FilterOperator.IsNotNull,\n            type: 'boolean',\n            condition: 'AND',\n            filterType: 'custom',\n          }}\n          onFilterChange={() => {}}\n        />\n      );\n      expect(result!.container.firstChild).toBeNull();\n    });\n    it('should render nothing for anything operator', async () => {\n      const result = render(\n        <FilterValueEditor\n          allColumns={[]}\n          filter={{\n            key: 'foo',\n            operator: FilterOperator.IsAnything,\n            type: 'String',\n            condition: 'AND',\n            filterType: 'custom',\n          }}\n          onFilterChange={() => {}}\n        />\n      );\n      expect(result!.container.firstChild).toBeNull();\n    });\n    it('should render radio button with value for boolean operator', async () => {\n      const filter: BooleanFilter = {\n        key: 'IsDeleted',\n        operator: FilterOperator.Equals,\n        type: 'boolean',\n        condition: 'AND',\n        value: true,\n        filterType: 'custom',\n      };\n      const onFilterChange = jest.fn();\n      const result = render(<FilterValueEditor allColumns={[]} filter={filter} onFilterChange={onFilterChange} />);\n      expect(result!.container.firstChild).not.toBeNull();\n      expect(result!.getByTestId('query-builder-filters-boolean-value-container')).toBeInTheDocument();\n      expect(result!.getByLabelText('True')).toBeChecked();\n      await userEvent.click(result!.getByLabelText('False'));\n      expect(onFilterChange).toHaveBeenCalledTimes(1);\n      expect(onFilterChange).toHaveBeenNthCalledWith(1, { ...filter, value: false });\n    });\n    it('should render number filter with value for number operator', async () => {\n      const filter: NumberFilter = {\n        filterType: 'custom',\n        key: 'Amount',\n        operator: FilterOperator.GreaterThanOrEqual,\n        type: 'int',\n        condition: 'AND',\n        value: 123,\n      };\n      const onFilterChange = jest.fn();\n      const result = render(<FilterValueEditor allColumns={[]} filter={filter} onFilterChange={onFilterChange} />);\n      expect(result.container.firstChild).not.toBeNull();\n      expect(result.getByTestId('query-builder-filters-number-value-container')).toBeInTheDocument();\n      expect(result.getByTestId('query-builder-filters-number-value-input')).toBeInTheDocument();\n      await userEvent.clear(result.getByTestId('query-builder-filters-number-value-input'));\n      await userEvent.type(result.getByTestId('query-builder-filters-number-value-input'), '300');\n      fireEvent.blur(result.getByTestId('query-builder-filters-number-value-input'));\n      expect(onFilterChange).toHaveBeenCalledTimes(1);\n    });\n    it('should render nothing for date operator with grafana time range', async () => {\n      const filter: DateFilter = {\n        filterType: 'custom',\n        key: 'CreatedDate',\n        operator: FilterOperator.WithInGrafanaTimeRange,\n        type: 'datetime',\n        condition: 'AND',\n      };\n      const onFilterChange = jest.fn();\n      const result = render(<FilterValueEditor allColumns={[]} filter={filter} onFilterChange={onFilterChange} />);\n      expect(result.container.firstChild).toBeNull();\n    });\n    it('should render date filter with value for date operator with time range', async () => {\n      const filter: DateFilter = {\n        filterType: 'custom',\n        key: 'CreatedDate',\n        operator: FilterOperator.Equals,\n        type: 'datetime',\n        condition: 'AND',\n        value: 'now()',\n      };\n      const onFilterChange = jest.fn();\n      const result = render(<FilterValueEditor allColumns={[]} filter={filter} onFilterChange={onFilterChange} />);\n      expect(result.container.firstChild).not.toBeNull();\n      expect(result.getByTestId('query-builder-filters-date-value-container')).toBeInTheDocument();\n      expect(result.getByText('NOW')).toBeInTheDocument();\n    });\n    it('should render select filter for single value picklist', () => {\n      const filter: StringFilter = {\n        filterType: 'custom',\n        key: 'StageName',\n        operator: FilterOperator.Equals,\n        type: 'picklist',\n        condition: 'AND',\n        value: 'Deal won',\n      };\n      const onFilterChange = jest.fn();\n      const result = render(\n        <FilterValueEditor\n          allColumns={[\n            {\n              name: 'StageName',\n              type: 'picklist',\n              picklistValues: [\n                { value: 'Deal won', label: 'Deal Won' },\n                { value: 'Deal lost', label: 'Deal Lost' },\n                { value: 'discovery', label: 'Discovery' },\n              ],\n            },\n          ]}\n          filter={filter}\n          onFilterChange={onFilterChange}\n        />\n      );\n      expect(result.container.firstChild).not.toBeNull();\n      expect(result.getByTestId('query-builder-filters-single-picklist-value-container')).toBeInTheDocument();\n      expect(result.getByText('Deal Won')).toBeInTheDocument();\n      expect(result.queryByText('Discovery')).not.toBeInTheDocument();\n    });\n    it('should render select filter for multi value picklist', () => {\n      const filter: MultiFilter = {\n        filterType: 'custom',\n        key: 'StageName',\n        operator: FilterOperator.In,\n        type: 'picklist',\n        condition: 'AND',\n        value: ['Deal won', 'Deal lost'],\n      };\n      const onFilterChange = jest.fn();\n      const result = render(\n        <FilterValueEditor\n          allColumns={[\n            {\n              name: 'StageName',\n              type: 'picklist',\n              picklistValues: [\n                { value: 'Deal won', label: 'Deal Won' },\n                { value: 'Deal lost', label: 'Deal Lost' },\n                { value: 'discovery', label: 'Discovery' },\n              ],\n            },\n          ]}\n          filter={filter}\n          onFilterChange={onFilterChange}\n        />\n      );\n      expect(result.container.firstChild).not.toBeNull();\n      expect(result.getByTestId('query-builder-filters-multi-picklist-value-container')).toBeInTheDocument();\n      expect(result.getByText('Deal Won')).toBeInTheDocument();\n      expect(result.getByText('Deal Lost')).toBeInTheDocument();\n      expect(result.queryByText('Discovery')).not.toBeInTheDocument();\n    });\n    it('should render input filter for single value string', async () => {\n      const filter: StringFilter = {\n        filterType: 'custom',\n        key: 'Name',\n        operator: FilterOperator.Equals,\n        type: 'string',\n        condition: 'AND',\n        value: 'ABC Corp',\n      };\n      const onFilterChange = jest.fn();\n      const result = render(<FilterValueEditor allColumns={[]} filter={filter} onFilterChange={onFilterChange} />);\n      expect(result.container.firstChild).not.toBeNull();\n      expect(result.getByTestId('query-builder-filters-single-string-value-container')).toBeInTheDocument();\n    });\n    it('should render input filter for multi value string', async () => {\n      const filter: MultiFilter = {\n        filterType: 'custom',\n        key: 'Name',\n        operator: FilterOperator.In,\n        type: 'string',\n        condition: 'AND',\n        value: ['ABC Corp'],\n      };\n      const onFilterChange = jest.fn();\n      const result = render(<FilterValueEditor allColumns={[]} filter={filter} onFilterChange={onFilterChange} />);\n      expect(result.container.firstChild).not.toBeNull();\n      expect(result.getByTestId('query-builder-filters-multi-string-value-container')).toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "src/components/queryBuilder/FilterEditor.tsx",
    "content": "import React, { useState } from 'react';\nimport { SelectableValue } from '@grafana/data';\nimport { Button, HorizontalGroup, InlineFormLabel, Input, MultiSelect, RadioButtonGroup, Select } from '@grafana/ui';\nimport { Filter, FilterOperator, TableColumn, NullFilter } from 'types/queryBuilder';\nimport * as utils from 'components/queryBuilder/utils';\nimport labels from 'labels';\nimport { styles } from 'styles';\nimport { Datasource } from 'data/CHDatasource';\nimport useUniqueMapKeys from 'hooks/useUniqueMapKeys';\n\nconst boolValues: Array<SelectableValue<boolean>> = [\n  { value: true, label: 'True' },\n  { value: false, label: 'False' },\n];\nconst conditions: Array<SelectableValue<'AND' | 'OR'>> = [\n  { value: 'AND', label: 'AND' },\n  { value: 'OR', label: 'OR' },\n];\nconst filterOperators: Array<SelectableValue<FilterOperator>> = [\n  { value: FilterOperator.WithInGrafanaTimeRange, label: 'Within dashboard time range' },\n  { value: FilterOperator.OutsideGrafanaTimeRange, label: 'Outside dashboard time range' },\n  { value: FilterOperator.IsAnything, label: 'IS ANYTHING' },\n  { value: FilterOperator.Equals, label: '=' },\n  { value: FilterOperator.NotEquals, label: '!=' },\n  { value: FilterOperator.LessThan, label: '<' },\n  { value: FilterOperator.LessThanOrEqual, label: '<=' },\n  { value: FilterOperator.GreaterThan, label: '>' },\n  { value: FilterOperator.GreaterThanOrEqual, label: '>=' },\n  { value: FilterOperator.Like, label: 'LIKE' },\n  { value: FilterOperator.NotLike, label: 'NOT LIKE' },\n  { value: FilterOperator.ILike, label: 'ILIKE' },\n  { value: FilterOperator.NotILike, label: 'NOT ILIKE' },\n  { value: FilterOperator.IsEmpty, label: 'IS EMPTY' },\n  { value: FilterOperator.IsNotEmpty, label: 'IS NOT EMPTY' },\n  { value: FilterOperator.In, label: 'IN' },\n  { value: FilterOperator.NotIn, label: 'NOT IN' },\n  { value: FilterOperator.IsNull, label: 'IS NULL' },\n  { value: FilterOperator.IsNotNull, label: 'IS NOT NULL' },\n];\nconst standardTimeOptions: Array<SelectableValue<string>> = [\n  { value: 'today()', label: 'TODAY' },\n  { value: 'yesterday()', label: 'YESTERDAY' },\n  { value: 'now()', label: 'NOW' },\n  { value: 'GRAFANA_START_TIME', label: 'DASHBOARD START TIME' },\n  { value: 'GRAFANA_END_TIME', label: 'DASHBOARD END TIME' },\n];\nexport const defaultNewFilter: NullFilter = {\n  filterType: 'custom',\n  condition: 'AND',\n  key: '',\n  type: '',\n  operator: FilterOperator.IsAnything,\n};\nexport interface PredefinedFilter {\n  restrictToFields?: readonly TableColumn[];\n}\n\nconst FilterValueNumberItem = (props: { value: number; onChange: (value: number) => void }) => {\n  const [value, setValue] = useState(props.value || 0);\n  return (\n    <div data-testid=\"query-builder-filters-number-value-container\">\n      <Input\n        data-testid=\"query-builder-filters-number-value-input\"\n        type=\"number\"\n        value={value}\n        onChange={(e) => setValue(e.currentTarget.valueAsNumber || 0)}\n        onBlur={() => props.onChange(value)}\n      />\n    </div>\n  );\n};\n\nconst FilterValueSingleStringItem = (props: { value: string; onChange: (value: string) => void }) => {\n  return (\n    <div data-testid=\"query-builder-filters-single-string-value-container\">\n      <Input\n        data-testid=\"query-builder-filters-single-string-value-input\"\n        type=\"text\"\n        defaultValue={props.value}\n        width={70}\n        onBlur={(e) => props.onChange(e.currentTarget.value)}\n      />\n    </div>\n  );\n};\n\nconst FilterValueMultiStringItem = (props: { value: string[]; onChange: (value: string[]) => void }) => {\n  const [value, setValue] = useState(props.value || []);\n  return (\n    <div data-testid=\"query-builder-filters-multi-string-value-container\">\n      <Input\n        type=\"text\"\n        value={value.join(',')}\n        placeholder=\"comma separated values\"\n        onChange={(e) => setValue((e.currentTarget.value || '').split(','))}\n        onBlur={() => props.onChange(value)}\n      />\n    </div>\n  );\n};\n\nexport const FilterValueEditor = (props: {\n  allColumns: readonly TableColumn[];\n  filter: Filter;\n  onFilterChange: (filter: Filter) => void;\n}) => {\n  const { filter, onFilterChange, allColumns: fieldsList } = props;\n  const getOptions = () => {\n    const matchedFilter = fieldsList.find((f) => f.name === filter.key);\n    return matchedFilter?.picklistValues || [];\n  };\n  if (utils.isNullFilter(filter)) {\n    return <></>;\n  } else if ([FilterOperator.IsAnything, FilterOperator.IsEmpty, FilterOperator.IsNotEmpty].includes(filter.operator)) {\n    return <></>;\n  } else if (utils.isBooleanFilter(filter)) {\n    const onBoolFilterValueChange = (value: boolean) => {\n      onFilterChange({ ...filter, value });\n    };\n    return (\n      <div data-testid=\"query-builder-filters-boolean-value-container\">\n        <RadioButtonGroup options={boolValues} value={filter.value} onChange={(e) => onBoolFilterValueChange(e!)} />\n      </div>\n    );\n  } else if (utils.isNumberFilter(filter)) {\n    return <FilterValueNumberItem value={filter.value} onChange={(value) => onFilterChange({ ...filter, value })} />;\n  } else if (utils.isDateFilter(filter)) {\n    if (utils.isDateFilterWithOutValue(filter)) {\n      return null;\n    }\n\n    const onDateFilterValueChange = (value: string) => {\n      onFilterChange({ ...filter, value });\n    };\n    const dateOptions = [...standardTimeOptions];\n    if (filter.value && !standardTimeOptions.find((o) => o.value === filter.value)) {\n      dateOptions.push({ label: filter.value, value: filter.value });\n    }\n\n    return (\n      <div data-testid=\"query-builder-filters-date-value-container\">\n        <Select\n          value={filter.value || 'TODAY'}\n          onChange={(e) => onDateFilterValueChange(e.value!)}\n          options={dateOptions}\n          width={40}\n          allowCustomValue\n        />\n      </div>\n    );\n  } else if (utils.isStringFilter(filter)) {\n    const onStringFilterValueChange = (value: string) => {\n      onFilterChange({ ...filter, value });\n    };\n    if (\n      filter.type === 'picklist' &&\n      (filter.operator === FilterOperator.Equals || filter.operator === FilterOperator.NotEquals)\n    ) {\n      return (\n        <div data-testid=\"query-builder-filters-single-picklist-value-container\">\n          <Select value={filter.value} onChange={(e) => onStringFilterValueChange(e.value!)} options={getOptions()} />\n        </div>\n      );\n    }\n\n    return (\n      <FilterValueSingleStringItem\n        value={filter.value}\n        onChange={onStringFilterValueChange}\n        // enforce input re-render when filter changes to avoid stale input value\n        key={filter.value}\n      />\n    );\n  } else if (utils.isMultiFilter(filter)) {\n    const onMultiFilterValueChange = (value: string[]) => {\n      onFilterChange({ ...filter, value });\n    };\n    if (filter.type === 'picklist') {\n      return (\n        <div data-testid=\"query-builder-filters-multi-picklist-value-container\">\n          <MultiSelect\n            value={filter.value}\n            options={getOptions()}\n            onChange={(e) => onMultiFilterValueChange(e.map((v) => v.value!))}\n          />\n        </div>\n      );\n    }\n    return <FilterValueMultiStringItem value={filter.value} onChange={onMultiFilterValueChange} />;\n  } else {\n    return <></>;\n  }\n};\n\nexport const FilterEditor = (props: {\n  allColumns: readonly TableColumn[];\n  index: number;\n  filter: Filter & PredefinedFilter;\n  onFilterChange: (index: number, filter: Filter) => void;\n  removeFilter: (index: number) => void;\n  datasource: Datasource;\n  database: string;\n  table: string;\n}) => {\n  const { index, filter, allColumns: fieldsList, onFilterChange, removeFilter } = props;\n  const [isOpen, setIsOpen] = useState(false);\n  const isMapType = filter.type.startsWith('Map');\n  const isJSONType = filter.type.startsWith('JSON');\n  const mapKeys = useUniqueMapKeys(props.datasource, isMapType ? filter.key : '', props.database, props.table);\n  const mapKeyOptions = mapKeys.map((k) => ({ label: k, value: k }));\n  if (filter.mapKey && !mapKeys.includes(filter.mapKey)) {\n    mapKeyOptions.push({ label: filter.mapKey, value: filter.mapKey });\n  }\n\n  const getFields = () => {\n    const values = (filter.restrictToFields || fieldsList).map((f) => {\n      let label = f.label || f.name;\n      if (f.type.startsWith('Map')) {\n        label += '[]';\n      } else if (f.type.startsWith('JSON')) {\n        label += '{}';\n      }\n\n      return { label, value: f.name };\n    });\n    // Add selected value to the list if it does not exist.\n    if (filter?.key && !values.find((x) => x.value === filter.key)) {\n      values.push({ label: filter.label || filter.key!, value: filter.key! });\n    }\n    return values;\n  };\n  const getFilterOperatorsByType = (type = 'string'): Array<SelectableValue<FilterOperator>> => {\n    if (utils.isBooleanType(type)) {\n      return filterOperators.filter((f) => [FilterOperator.Equals, FilterOperator.NotEquals].includes(f.value!));\n    } else if (utils.isNumberType(type)) {\n      return filterOperators.filter((f) =>\n        [\n          FilterOperator.IsAnything,\n          FilterOperator.IsNull,\n          FilterOperator.IsNotNull,\n          FilterOperator.Equals,\n          FilterOperator.NotEquals,\n          FilterOperator.LessThan,\n          FilterOperator.LessThanOrEqual,\n          FilterOperator.GreaterThan,\n          FilterOperator.GreaterThanOrEqual,\n        ].includes(f.value!)\n      );\n    } else if (utils.isDateType(type)) {\n      return filterOperators.filter((f) =>\n        [\n          FilterOperator.IsAnything,\n          FilterOperator.IsNull,\n          FilterOperator.IsNotNull,\n          FilterOperator.Equals,\n          FilterOperator.NotEquals,\n          FilterOperator.LessThan,\n          FilterOperator.LessThanOrEqual,\n          FilterOperator.GreaterThan,\n          FilterOperator.GreaterThanOrEqual,\n          FilterOperator.WithInGrafanaTimeRange,\n          FilterOperator.OutsideGrafanaTimeRange,\n        ].includes(f.value!)\n      );\n    } else {\n      return filterOperators.filter((f) =>\n        [\n          FilterOperator.IsAnything,\n          FilterOperator.Like,\n          FilterOperator.NotLike,\n          FilterOperator.ILike,\n          FilterOperator.NotILike,\n          FilterOperator.In,\n          FilterOperator.NotIn,\n          FilterOperator.IsNull,\n          FilterOperator.IsNotNull,\n          FilterOperator.Equals,\n          FilterOperator.NotEquals,\n          FilterOperator.IsEmpty,\n          FilterOperator.IsNotEmpty,\n          FilterOperator.LessThan,\n          FilterOperator.LessThanOrEqual,\n          FilterOperator.GreaterThan,\n          FilterOperator.GreaterThanOrEqual,\n        ].includes(f.value!)\n      );\n    }\n  };\n  const onFilterNameChange = (fieldName: string) => {\n    setIsOpen(false);\n    const matchingField = fieldsList.find((f) => f.name === fieldName);\n    const filterData = {\n      key: matchingField?.name || fieldName,\n      type: matchingField?.type || 'String',\n      label: matchingField?.label,\n    };\n\n    let newFilter: Filter & PredefinedFilter;\n    // this is an auto-generated TimeRange filter\n    if (filter.restrictToFields) {\n      newFilter = {\n        filterType: 'custom',\n        key: filterData.key || filter.key,\n        type: 'datetime',\n        condition: filter.condition || 'AND',\n        operator: FilterOperator.WithInGrafanaTimeRange,\n        restrictToFields: filter.restrictToFields,\n        label: filterData.label,\n      };\n    } else if (utils.isBooleanType(filterData.type)) {\n      newFilter = {\n        filterType: 'custom',\n        key: filterData.key,\n        type: 'boolean',\n        condition: filter.condition || 'AND',\n        operator: FilterOperator.Equals,\n        value: false,\n        label: filterData.label,\n      };\n    } else if (utils.isDateType(filterData.type)) {\n      newFilter = {\n        filterType: 'custom',\n        key: filterData.key,\n        type: filterData.type as 'date',\n        condition: filter.condition || 'AND',\n        operator: FilterOperator.Equals,\n        value: 'TODAY',\n        label: filterData.label,\n      };\n    } else {\n      newFilter = {\n        filterType: 'custom',\n        key: filterData.key,\n        type: filterData.type,\n        condition: filter.condition || 'AND',\n        operator: FilterOperator.IsNotNull,\n        label: filterData.label,\n      };\n    }\n    onFilterChange(index, newFilter);\n  };\n  const onFilterMapKeyChange = (mapKey: string) => {\n    const newFilter: Filter = { ...filter };\n    newFilter.mapKey = mapKey;\n    onFilterChange(index, newFilter);\n  };\n  const onFilterOperatorChange = (operator: FilterOperator) => {\n    const newFilter: Filter = { ...filter };\n    newFilter.operator = operator;\n    if (utils.isMultiFilter(newFilter)) {\n      if (!Array.isArray(newFilter.value)) {\n        newFilter.value = [newFilter.value || ''];\n      }\n    }\n    onFilterChange(index, newFilter);\n  };\n  const onFilterConditionChange = (condition: 'AND' | 'OR') => {\n    const newFilter: Filter = { ...filter };\n    newFilter.condition = condition;\n    onFilterChange(index, newFilter);\n  };\n  const onFilterValueChange = (filter: Filter) => {\n    onFilterChange(index, filter);\n  };\n\n  return (\n    <HorizontalGroup wrap align=\"flex-start\" justify=\"flex-start\">\n      {index !== 0 && (\n        <RadioButtonGroup options={conditions} value={filter.condition} onChange={(e) => onFilterConditionChange(e!)} />\n      )}\n      <Select\n        disabled={Boolean(filter.hint)}\n        placeholder={filter.hint ? labels.types.ColumnHint[filter.hint] : undefined}\n        value={filter.key}\n        width={40}\n        className={styles.Common.inlineSelect}\n        options={getFields()}\n        isOpen={isOpen}\n        onOpenMenu={() => setIsOpen(true)}\n        onCloseMenu={() => setIsOpen(false)}\n        onChange={(e) => onFilterNameChange(e.value!)}\n        allowCustomValue\n        menuPlacement={'bottom'}\n      />\n      {(isMapType || isJSONType) && (\n        <Select\n          value={filter.mapKey}\n          placeholder={labels.components.FilterEditor.mapKeyPlaceholder}\n          width={40}\n          className={styles.Common.inlineSelect}\n          options={mapKeyOptions}\n          onChange={(e) => onFilterMapKeyChange(e.value!)}\n          allowCustomValue\n          menuPlacement={'bottom'}\n        />\n      )}\n      <Select\n        value={filter.operator}\n        width={40}\n        className={styles.Common.inlineSelect}\n        options={getFilterOperatorsByType(filter.type)}\n        onChange={(e) => onFilterOperatorChange(e.value!)}\n        menuPlacement={'bottom'}\n      />\n      <FilterValueEditor filter={filter} onFilterChange={onFilterValueChange} allColumns={fieldsList} />\n      <Button\n        data-testid=\"query-builder-filters-remove-button\"\n        icon=\"trash-alt\"\n        variant=\"destructive\"\n        size=\"sm\"\n        className={styles.Common.smallBtn}\n        onClick={() => removeFilter(index)}\n        aria-label=\"query-builder-filters-remove-button\"\n      />\n    </HorizontalGroup>\n  );\n};\n\nexport const FiltersEditor = (props: {\n  allColumns: readonly TableColumn[];\n  filters: Filter[];\n  onFiltersChange: (filters: Filter[]) => void;\n  datasource: Datasource;\n  database: string;\n  table: string;\n}) => {\n  const { filters = [], onFiltersChange, allColumns: fieldsList = [], datasource, database, table } = props;\n  const { label, tooltip, addLabel } = labels.components.FilterEditor;\n  const addFilter = () => {\n    onFiltersChange([...filters, { ...defaultNewFilter }]);\n  };\n  const removeFilter = (index: number) => {\n    const newFilters = [...filters];\n    newFilters.splice(index, 1);\n    onFiltersChange(newFilters);\n  };\n  const onFilterChange = (index: number, filter: Filter) => {\n    const newFilters = [...filters];\n    newFilters[index] = filter;\n    onFiltersChange(newFilters);\n  };\n\n  return (\n    <>\n      {filters.length === 0 && (\n        <div className=\"gf-form\">\n          <InlineFormLabel width={8} className=\"query-keyword\" tooltip={tooltip}>\n            {label}\n          </InlineFormLabel>\n          <Button\n            data-testid=\"query-builder-filters-add-button\"\n            icon=\"plus-circle\"\n            variant=\"secondary\"\n            size=\"sm\"\n            className={styles.Common.smallBtn}\n            onClick={addFilter}\n          >\n            {addLabel}\n          </Button>\n        </div>\n      )}\n      {filters.map((filter, index) => {\n        return (\n          <div className=\"gf-form\" key={index}>\n            {index === 0 ? (\n              <InlineFormLabel width={8} className=\"query-keyword\" tooltip={tooltip}>\n                {label}\n              </InlineFormLabel>\n            ) : (\n              <div className={`width-8 ${styles.Common.firstLabel}`}></div>\n            )}\n            <FilterEditor\n              allColumns={fieldsList}\n              filter={filter}\n              onFilterChange={onFilterChange}\n              removeFilter={removeFilter}\n              index={index}\n              datasource={datasource}\n              database={database}\n              table={table}\n            />\n          </div>\n        );\n      })}\n      {filters.length !== 0 && (\n        <div className=\"gf-form\">\n          <div className={`width-8 ${styles.Common.firstLabel}`}></div>\n          <Button\n            data-testid=\"query-builder-filters-inline-add-button\"\n            icon=\"plus-circle\"\n            variant=\"secondary\"\n            size=\"sm\"\n            className={styles.Common.smallBtn}\n            onClick={addFilter}\n          >\n            {addLabel}\n          </Button>\n        </div>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "src/components/queryBuilder/GroupByEditor.test.tsx",
    "content": "import React from 'react';\nimport { fireEvent, render } from '@testing-library/react';\nimport { GroupByEditor } from './GroupByEditor';\nimport { TableColumn } from 'types/queryBuilder';\n\ndescribe('GroupByEditor', () => {\n  it('should render with empty properties', () => {\n    const result = render(<GroupByEditor allColumns={[]} groupBy={[]} onGroupByChange={() => {}} />);\n    expect(result.container.firstChild).not.toBeNull();\n  });\n\n  it('should render with valid properties', () => {\n    const allColumns: readonly TableColumn[] = [{ name: 'a', type: 'string', picklistValues: [] }];\n    const groupBy: string[] = ['a', 'b'];\n    const result = render(<GroupByEditor allColumns={allColumns} groupBy={groupBy} onGroupByChange={() => {}} />);\n    expect(result.container.firstChild).not.toBeNull();\n  });\n\n  it('should call onGroupByChange when a new column is selected', () => {\n    const allColumns: readonly TableColumn[] = [{ name: 'a', type: 'string', picklistValues: [] }];\n    const groupBy: string[] = ['b'];\n    const onGroupByChange = jest.fn();\n    const result = render(\n      <GroupByEditor allColumns={allColumns} groupBy={groupBy} onGroupByChange={onGroupByChange} />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const multiSelect = result.getByRole('combobox');\n    expect(multiSelect).toBeInTheDocument();\n\n    expect(result.queryAllByText('a').length).toBe(0); // is popup closed\n    fireEvent.keyDown(multiSelect, { key: 'ArrowDown' });\n    expect(result.queryAllByText('a').length).toBe(1); // is popup open\n    fireEvent.keyDown(multiSelect, { key: 'Enter' });\n    expect(result.queryAllByText('a').length).toBe(0); // is popup closed\n    expect(onGroupByChange).toBeCalledTimes(1);\n    expect(onGroupByChange).toBeCalledWith(expect.any(Object));\n  });\n});\n"
  },
  {
    "path": "src/components/queryBuilder/GroupByEditor.tsx",
    "content": "import React, { useState } from 'react';\nimport { InlineFormLabel, MultiSelect } from '@grafana/ui';\nimport { SelectableValue } from '@grafana/data';\nimport { TableColumn } from 'types/queryBuilder';\nimport labels from 'labels';\nimport { styles } from 'styles';\nimport { selectors } from 'selectors';\n\ninterface GroupByEditorProps {\n  allColumns: readonly TableColumn[];\n  groupBy: string[];\n  onGroupByChange: (groupBy: string[]) => void;\n}\n\nexport const GroupByEditor = (props: GroupByEditorProps) => {\n  const { allColumns, groupBy, onGroupByChange } = props;\n  const [isOpen, setIsOpen] = useState(false);\n  const { label, tooltip } = labels.components.GroupByEditor;\n  const options: Array<SelectableValue<string>> = allColumns.map((c) => ({ label: c.name, value: c.name }));\n\n  const onChange = (selection: Array<SelectableValue<string>>) => {\n    setIsOpen(false);\n    onGroupByChange(selection.map((s) => s.value!));\n  };\n\n  return (\n    <div className=\"gf-form\">\n      <InlineFormLabel width={8} className=\"query-keyword\" tooltip={tooltip}>\n        {label}\n      </InlineFormLabel>\n      <div\n        data-testid={selectors.components.QueryBuilder.GroupByEditor.multiSelectWrapper}\n        className={styles.Common.selectWrapper}\n      >\n        <MultiSelect\n          options={options}\n          isOpen={isOpen}\n          onOpenMenu={() => setIsOpen(true)}\n          onCloseMenu={() => setIsOpen(false)}\n          value={groupBy}\n          onChange={onChange}\n          allowCustomValue={true}\n          menuPlacement={'bottom'}\n        />\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/queryBuilder/LimitEditor.test.tsx",
    "content": "import React from 'react';\nimport { render, fireEvent } from '@testing-library/react';\nimport { LimitEditor } from './LimitEditor';\nimport { selectors } from 'selectors';\n\ndescribe('LimitEditor', () => {\n  it('should render', () => {\n    const result = render(<LimitEditor limit={10} onLimitChange={() => {}} />);\n    expect(result.container.firstChild).not.toBeNull();\n  });\n\n  it('should call onLimitChange when limit is changed', () => {\n    const onLimitChange = jest.fn();\n    const result = render(<LimitEditor limit={10} onLimitChange={onLimitChange} />);\n    expect(result.container.firstChild).not.toBeNull();\n\n    const limitInput = result.getByTestId(selectors.components.QueryBuilder.LimitEditor.input);\n    expect(limitInput).toBeInTheDocument();\n    fireEvent.change(limitInput, { target: { value: 5 } });\n    fireEvent.blur(limitInput);\n    expect(limitInput).toHaveValue(5);\n    expect(onLimitChange).toBeCalledTimes(1);\n    expect(onLimitChange).toBeCalledWith(5);\n  });\n});\n"
  },
  {
    "path": "src/components/queryBuilder/LimitEditor.tsx",
    "content": "import React, { useState } from 'react';\nimport { InlineFormLabel, Input } from '@grafana/ui';\nimport labels from 'labels';\nimport { selectors } from 'selectors';\n\ninterface LimitEditorProps {\n  limit: number;\n  onLimitChange: (limit: number) => void;\n}\n\nexport const LimitEditor = (props: LimitEditorProps) => {\n  const [limit, setLimit] = useState<number>(props.limit || 0);\n  const { label, tooltip } = labels.components.LimitEditor;\n\n  return (\n    <div className=\"gf-form\">\n      <InlineFormLabel width={8} className=\"query-keyword\" tooltip={tooltip}>\n        {label}\n      </InlineFormLabel>\n      <Input\n        data-testid={selectors.components.QueryBuilder.LimitEditor.input}\n        width={10}\n        value={limit}\n        type=\"number\"\n        min={0}\n        onChange={(e) => setLimit(e.currentTarget.valueAsNumber)}\n        onBlur={() => props.onLimitChange(limit)}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/queryBuilder/ModeSwitch.test.tsx",
    "content": "import React from 'react';\nimport { fireEvent, render } from '@testing-library/react';\nimport { ModeSwitch } from './ModeSwitch';\n\ndescribe('ModeSwitch', () => {\n  const labelA = 'A';\n  const labelB = 'B';\n  const label = 'test';\n  const tooltip = 'tooltip';\n\n  it('should render', () => {\n    const result = render(\n      <ModeSwitch labelA={labelA} labelB={labelB} value={false} onChange={() => {}} label={label} tooltip={tooltip} />\n    );\n\n    expect(result.container.firstChild).not.toBeNull();\n  });\n\n  it('should call onChange when mode is changed', () => {\n    const onChange = jest.fn();\n    const result = render(\n      <ModeSwitch labelA={labelA} labelB={labelB} value={false} onChange={onChange} label={label} tooltip={tooltip} />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const buttonA = result.getByText(labelA);\n    expect(buttonA).toBeInTheDocument();\n    const buttonB = result.getByText(labelB);\n    expect(buttonB).toBeInTheDocument();\n\n    fireEvent.click(buttonB);\n    expect(onChange).toBeCalledTimes(1);\n    expect(onChange).toBeCalledWith(true);\n\n    result.rerender(\n      <ModeSwitch labelA={labelA} labelB={labelB} value={true} onChange={onChange} label={label} tooltip={tooltip} />\n    );\n\n    fireEvent.click(buttonA);\n    expect(onChange).toBeCalledTimes(2);\n    expect(onChange).toBeCalledWith(false);\n  });\n});\n"
  },
  {
    "path": "src/components/queryBuilder/ModeSwitch.tsx",
    "content": "import React from 'react';\nimport { RadioButtonGroup, InlineFormLabel } from '@grafana/ui';\n\nexport interface ModeSwitchProps {\n  labelA: string;\n  labelB: string;\n  value: boolean;\n  onChange: (value: boolean) => void;\n  label: string;\n  tooltip: string;\n}\n\n/**\n * Component for switching between modes. Boxes are labeled unlike regular Switch.\n */\nexport const ModeSwitch = (props: ModeSwitchProps) => {\n  const { labelA, labelB, value, onChange, label, tooltip } = props;\n\n  const options = [\n    {\n      label: labelA,\n      value: false,\n    },\n    {\n      label: labelB,\n      value: true,\n    },\n  ];\n\n  return (\n    <div className=\"gf-form\">\n      <InlineFormLabel width={8} className=\"query-keyword\" tooltip={tooltip}>\n        {label}\n      </InlineFormLabel>\n      <RadioButtonGroup<boolean> options={options} value={value} onChange={(v) => onChange(v)} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/queryBuilder/OrderByEditor.test.tsx",
    "content": "import React from 'react';\nimport { render } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { OrderByEditor, getOrderByOptions } from './OrderByEditor';\nimport { AggregateType, BuilderMode, OrderByDirection, QueryType, TableColumn } from 'types/queryBuilder';\nimport { SelectableValue } from '@grafana/data';\n\nconst testOptions: Array<SelectableValue<string>> = [\n  { label: 'foo', value: 'foo' },\n  { label: 'bar', value: 'bar' },\n  { label: 'baz', value: 'baz' },\n];\n\ndescribe('OrderByEditor', () => {\n  it('should render null when no fields passed', () => {\n    const result = render(<OrderByEditor orderByOptions={[]} orderBy={[]} onOrderByChange={() => {}} />);\n    expect(result.container.firstChild).toBeNull();\n  });\n  it('should render component when fields passed', () => {\n    const result = render(<OrderByEditor orderByOptions={[testOptions[0]]} orderBy={[]} onOrderByChange={() => {}} />);\n    expect(result.container.firstChild).not.toBeNull();\n  });\n  it('should render default add button when no orderby fields passed', () => {\n    const result = render(<OrderByEditor orderByOptions={[testOptions[0]]} orderBy={[]} onOrderByChange={() => {}} />);\n    expect(result.container.firstChild).not.toBeNull();\n    expect(result.getByTestId('query-builder-orderby-add-button')).toBeInTheDocument();\n    expect(result.queryByTestId('query-builder-orderby-item-wrapper')).not.toBeInTheDocument();\n    expect(result.queryByTestId('query-builder-orderby-remove-button')).not.toBeInTheDocument();\n  });\n  it('should render remove button when at least one orderby fields passed', () => {\n    const result = render(\n      <OrderByEditor\n        orderByOptions={[testOptions[0]]}\n        orderBy={[{ name: 'foo', dir: OrderByDirection.ASC }]}\n        onOrderByChange={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n    expect(result.getByTestId('query-builder-orderby-add-button')).toBeInTheDocument();\n    expect(result.getByTestId('query-builder-orderby-item-wrapper')).toBeInTheDocument();\n    expect(result.getByTestId('query-builder-orderby-remove-button')).toBeInTheDocument();\n  });\n  it('should render add/remove buttons correctly when multiple orderby elements passed', () => {\n    const result = render(\n      <OrderByEditor\n        orderByOptions={[testOptions[0]]}\n        orderBy={[\n          { name: 'foo', dir: OrderByDirection.ASC },\n          { name: 'bar', dir: OrderByDirection.ASC },\n        ]}\n        onOrderByChange={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n    expect(result.queryByTestId('query-builder-orderby-add-button')).toBeInTheDocument();\n    expect(result.getAllByTestId('query-builder-orderby-item-wrapper').length).toBe(2);\n    expect(result.getAllByTestId('query-builder-orderby-remove-button').length).toBe(2);\n  });\n  it('should render label only once', () => {\n    const result = render(\n      <OrderByEditor\n        orderByOptions={[testOptions[0]]}\n        orderBy={[\n          { name: 'foo', dir: OrderByDirection.ASC },\n          { name: 'bar', dir: OrderByDirection.ASC },\n        ]}\n        onOrderByChange={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n    expect(result.getByTestId('query-builder-orderby-item-label')).toBeInTheDocument();\n  });\n  it('should add default item when add button clicked', async () => {\n    const onOrderByChange = jest.fn();\n    const result = render(\n      <OrderByEditor orderByOptions={[testOptions[0]]} orderBy={[]} onOrderByChange={onOrderByChange} />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n    expect(result.getByTestId('query-builder-orderby-add-button')).toBeInTheDocument();\n    expect(result.queryByTestId('query-builder-orderby-item-wrapper')).not.toBeInTheDocument();\n    expect(result.queryByTestId('query-builder-orderby-remove-button')).not.toBeInTheDocument();\n    expect(onOrderByChange).toBeCalledTimes(0);\n    await userEvent.click(result.getByTestId('query-builder-orderby-add-button'));\n    expect(onOrderByChange).toBeCalledTimes(1);\n    expect(onOrderByChange).toBeCalledWith([{ name: 'foo', dir: OrderByDirection.ASC }]);\n  });\n  it('should remove items when remove button clicked', async () => {\n    const onOrderByChange = jest.fn();\n    const result = render(\n      <OrderByEditor\n        orderByOptions={testOptions}\n        orderBy={[\n          { name: 'foo', dir: OrderByDirection.ASC },\n          { name: 'bar', dir: OrderByDirection.ASC },\n        ]}\n        onOrderByChange={onOrderByChange}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n    expect(onOrderByChange).toBeCalledTimes(0);\n    await userEvent.click(result.getAllByTestId('query-builder-orderby-remove-button')[1]);\n    await userEvent.click(result.getAllByTestId('query-builder-orderby-remove-button')[0]);\n    await userEvent.click(result.getAllByTestId('query-builder-orderby-add-button')[0]);\n    expect(onOrderByChange).toBeCalledTimes(3);\n    expect(onOrderByChange).toHaveBeenNthCalledWith(1, [{ name: 'foo', dir: OrderByDirection.ASC }]);\n    expect(onOrderByChange).toHaveBeenNthCalledWith(2, [{ name: 'bar', dir: OrderByDirection.ASC }]);\n    expect(onOrderByChange).toHaveBeenNthCalledWith(3, [\n      { name: 'foo', dir: OrderByDirection.ASC },\n      { name: 'bar', dir: OrderByDirection.ASC },\n      { name: 'foo', dir: OrderByDirection.ASC },\n    ]);\n  });\n});\n\ndescribe('getOrderByOptions', () => {\n  const allColumns: readonly TableColumn[] = [\n    {\n      name: 'field1',\n      type: 'string',\n      picklistValues: [],\n    },\n    {\n      name: 'field2',\n      type: 'string',\n      picklistValues: [],\n    },\n    {\n      name: 'field3',\n      type: 'string',\n      picklistValues: [],\n    },\n    {\n      name: 'field4',\n      type: 'string',\n      picklistValues: [],\n    },\n  ];\n\n  it('should return all columns as options', () => {\n    expect(\n      getOrderByOptions(\n        {\n          database: 'db',\n          table: 'foo',\n          queryType: QueryType.Table,\n          columns: [{ name: 'field1' }, { name: 'field3' }],\n        },\n        allColumns\n      )\n    ).toStrictEqual([\n      {\n        label: 'field1',\n        value: 'field1',\n      },\n      {\n        label: 'field2',\n        value: 'field2',\n      },\n      {\n        label: 'field3',\n        value: 'field3',\n      },\n      {\n        label: 'field4',\n        value: 'field4',\n      },\n    ]);\n  });\n  it('should return only selected columns for aggregate query', () => {\n    expect(\n      getOrderByOptions(\n        {\n          database: 'db',\n          table: 'foo',\n          queryType: QueryType.Table,\n          columns: [{ name: 'field1' }],\n          aggregates: [{ column: 'field2', aggregateType: AggregateType.Max }],\n        },\n        allColumns\n      )\n    ).toStrictEqual([\n      {\n        label: 'field1',\n        value: 'field1',\n      },\n      {\n        label: 'max(field2)',\n        value: 'max(field2)',\n      },\n    ]);\n  });\n  it('should return correct label and value for aggregates with aliases', () => {\n    expect(\n      getOrderByOptions(\n        {\n          database: 'db',\n          table: 'foo',\n          queryType: QueryType.Table,\n          aggregates: [{ column: 'field1', aggregateType: AggregateType.Max, alias: 'a' }],\n        },\n        allColumns\n      )\n    ).toStrictEqual([\n      {\n        label: 'max(field1) as a',\n        value: 'a',\n      },\n    ]);\n  });\n  it('should show options from selected columns, aggregates, and groupBy', () => {\n    expect(\n      getOrderByOptions(\n        {\n          database: 'db',\n          table: 'foo',\n          queryType: QueryType.Table,\n          columns: [{ name: 'field1' }],\n          aggregates: [{ column: 'field2', aggregateType: AggregateType.Max }],\n          groupBy: ['field2'],\n        },\n        allColumns\n      )\n    ).toStrictEqual([\n      {\n        value: 'field1',\n        label: 'field1',\n      },\n      {\n        value: 'max(field2)',\n        label: 'max(field2)',\n      },\n      {\n        value: 'field2',\n        label: 'field2',\n      },\n    ]);\n  });\n  it('aggregated view - two group by and with no aggregates', () => {\n    expect(\n      getOrderByOptions(\n        {\n          database: 'db',\n          table: 'foo',\n          queryType: QueryType.Table,\n          columns: [],\n          aggregates: [],\n          groupBy: ['field3', 'field1'],\n        },\n        allColumns\n      )\n    ).toStrictEqual([\n      {\n        value: 'field1',\n        label: 'field1',\n      },\n      {\n        value: 'field2',\n        label: 'field2',\n      },\n      {\n        value: 'field3',\n        label: 'field3',\n      },\n      {\n        value: 'field4',\n        label: 'field4',\n      },\n    ]);\n  });\n  it('aggregated view - two group by and with two metrics', () => {\n    expect(\n      getOrderByOptions(\n        {\n          database: 'db',\n          table: 'foo',\n          queryType: QueryType.Table,\n          mode: BuilderMode.Aggregate,\n          columns: [],\n          aggregates: [\n            { column: 'field2', aggregateType: AggregateType.Max },\n            { column: 'field1', aggregateType: AggregateType.Sum },\n          ],\n          groupBy: ['field3', 'field1'],\n        },\n        allColumns\n      )\n    ).toStrictEqual([\n      {\n        value: 'max(field2)',\n        label: 'max(field2)',\n      },\n      {\n        value: 'sum(field1)',\n        label: 'sum(field1)',\n      },\n      {\n        value: 'field3',\n        label: 'field3',\n      },\n      {\n        value: 'field1',\n        label: 'field1',\n      },\n    ]);\n  });\n});\n"
  },
  {
    "path": "src/components/queryBuilder/OrderByEditor.tsx",
    "content": "import React from 'react';\nimport { SelectableValue } from '@grafana/data';\nimport { Button, InlineFormLabel, Select } from '@grafana/ui';\nimport { OrderBy, OrderByDirection, QueryBuilderOptions, TableColumn } from 'types/queryBuilder';\nimport allLabels from 'labels';\nimport { styles } from 'styles';\nimport { isAggregateQuery } from 'data/sqlGenerator';\n\ninterface OrderByItemProps {\n  columnOptions: Array<SelectableValue<string>>;\n  index: number;\n  orderByItem: OrderBy;\n  updateOrderByItem: (index: number, orderByItem: OrderBy) => void;\n  removeOrderByItem: (index: number) => void;\n}\n\nconst sortOptions = [\n  { label: 'ASC', value: OrderByDirection.ASC },\n  { label: 'DESC', value: OrderByDirection.DESC },\n];\n\nconst OrderByItem = (props: OrderByItemProps) => {\n  const { columnOptions, index, orderByItem, updateOrderByItem, removeOrderByItem } = props;\n\n  return (\n    <>\n      <Select\n        disabled={Boolean(orderByItem.hint)}\n        placeholder={orderByItem.hint ? allLabels.types.ColumnHint[orderByItem.hint] : undefined}\n        value={orderByItem.hint ? undefined : orderByItem.name}\n        className={styles.Common.inlineSelect}\n        width={36}\n        options={columnOptions}\n        onChange={(e) => updateOrderByItem(index, { ...orderByItem, name: e.value! })}\n        allowCustomValue\n        menuPlacement={'bottom'}\n      />\n      <Select<OrderByDirection>\n        value={orderByItem.dir}\n        className={styles.Common.inlineSelect}\n        width={12}\n        options={sortOptions}\n        onChange={(e) => updateOrderByItem(index, { ...orderByItem, dir: e.value! })}\n        menuPlacement={'bottom'}\n      />\n      <Button\n        data-testid=\"query-builder-orderby-remove-button\"\n        className={styles.Common.smallBtn}\n        variant=\"destructive\"\n        size=\"sm\"\n        icon=\"trash-alt\"\n        onClick={() => removeOrderByItem(index)}\n        aria-label=\"order-by-remove-item\"\n      />\n    </>\n  );\n};\n\ninterface OrderByEditorProps {\n  orderByOptions: Array<SelectableValue<string>>;\n  orderBy: OrderBy[];\n  onOrderByChange: (orderBy: OrderBy[]) => void;\n}\nexport const OrderByEditor = (props: OrderByEditorProps) => {\n  const { orderByOptions, orderBy, onOrderByChange } = props;\n  const { label, tooltip, addLabel } = allLabels.components.OrderByEditor;\n\n  const addOrderByItem = () => {\n    const nextOrderBy: OrderBy[] = orderBy.slice();\n    nextOrderBy.push({ name: orderByOptions[0]?.value!, dir: OrderByDirection.ASC });\n    onOrderByChange(nextOrderBy);\n  };\n  const removeOrderByItem = (index: number) => {\n    const nextOrderBy: OrderBy[] = orderBy.slice();\n    nextOrderBy.splice(index, 1);\n    onOrderByChange(nextOrderBy);\n  };\n  const updateOrderByItem = (index: number, orderByItem: OrderBy) => {\n    const nextOrderBy: OrderBy[] = orderBy.slice();\n    nextOrderBy[index] = orderByItem;\n    onOrderByChange(nextOrderBy);\n  };\n\n  if (orderByOptions.length === 0) {\n    return null;\n  }\n\n  const fieldLabel = (\n    <InlineFormLabel\n      width={8}\n      className=\"query-keyword\"\n      data-testid=\"query-builder-orderby-item-label\"\n      tooltip={tooltip}\n    >\n      {label}\n    </InlineFormLabel>\n  );\n  const fieldSpacer = <div className={`width-8 ${styles.Common.firstLabel}`}></div>;\n\n  return (\n    <>\n      {orderBy.map((orderByItem, index) => {\n        const key = `${index}-${orderByItem.name}-${orderByItem.hint || ''}-${orderByItem.dir}`;\n        return (\n          <div className=\"gf-form\" key={key} data-testid=\"query-builder-orderby-item-wrapper\">\n            {index === 0 ? fieldLabel : fieldSpacer}\n            <OrderByItem\n              columnOptions={orderByOptions}\n              index={index}\n              orderByItem={orderByItem}\n              updateOrderByItem={updateOrderByItem}\n              removeOrderByItem={removeOrderByItem}\n            />\n          </div>\n        );\n      })}\n\n      <div className=\"gf-form\">\n        {orderBy.length === 0 ? fieldLabel : fieldSpacer}\n        <Button\n          data-testid=\"query-builder-orderby-add-button\"\n          icon=\"plus-circle\"\n          variant=\"secondary\"\n          size=\"sm\"\n          onClick={addOrderByItem}\n          className={styles.Common.smallBtn}\n        >\n          {addLabel}\n        </Button>\n      </div>\n    </>\n  );\n};\n\nexport const getOrderByOptions = (\n  builder: QueryBuilderOptions,\n  allColumns: readonly TableColumn[]\n): Array<SelectableValue<string>> => {\n  let allOptions: Array<SelectableValue<string>> = [];\n\n  if (isAggregateQuery(builder)) {\n    builder.columns?.forEach((c) => {\n      allOptions.push({ label: c.alias || c.name, value: c.name });\n    });\n\n    builder.aggregates!.forEach((a) => {\n      let label = `${a.aggregateType}(${a.column})`;\n      let value = label;\n\n      if (a.alias) {\n        label += ` as ${a.alias}`;\n        value = a.alias;\n      }\n\n      allOptions.push({ label, value });\n    });\n\n    if (builder.groupBy && builder.groupBy.length > 0) {\n      builder.groupBy.forEach((g) => allOptions.push({ label: g, value: g }));\n    }\n  } else {\n    allColumns.forEach((c) => allOptions.push({ label: c.label || c.name, value: c.name }));\n  }\n\n  // Add selected value to the list if it does not exist.\n  const allValues = new Set(allOptions.map((o) => o.value));\n  const customValues = builder.orderBy?.filter((o) => !allValues.has(o.name));\n  customValues?.forEach((o) => allOptions.push({ label: o.name, value: o.name }));\n\n  return allOptions;\n};\n"
  },
  {
    "path": "src/components/queryBuilder/OtelVersionSelect.test.tsx",
    "content": "import React from 'react';\nimport { render, fireEvent } from '@testing-library/react';\nimport { OtelVersionSelect } from './OtelVersionSelect';\nimport otel from 'otel';\n\ndescribe('OtelVersionSelect', () => {\n  const testVersion = otel.getLatestVersion();\n  const testVersionName = testVersion.name;\n\n  it('should render with empty properties', () => {\n    const result = render(\n      <OtelVersionSelect enabled={false} onEnabledChange={() => {}} selectedVersion=\"\" onVersionChange={() => {}} />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n  });\n\n  it('should render with valid properties', () => {\n    const result = render(\n      <OtelVersionSelect\n        enabled={false}\n        onEnabledChange={() => {}}\n        selectedVersion={testVersion.version}\n        onVersionChange={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n    expect(result.getByText(testVersionName)).toBeInTheDocument();\n  });\n\n  it('should call onEnabledChange when the switch is enabled', () => {\n    const onEnabledChange = jest.fn();\n    const result = render(\n      <OtelVersionSelect\n        enabled={false}\n        onEnabledChange={onEnabledChange}\n        selectedVersion={testVersion.version}\n        onVersionChange={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const toggle = result.getByRole('checkbox');\n    expect(toggle).toBeInTheDocument();\n    fireEvent.click(toggle);\n    expect(onEnabledChange).toBeCalledTimes(1);\n    expect(onEnabledChange).toBeCalledWith(true);\n  });\n\n  it('should call onEnabledChange when the switch is disabled', () => {\n    const onEnabledChange = jest.fn();\n    const result = render(\n      <OtelVersionSelect\n        enabled={true}\n        onEnabledChange={onEnabledChange}\n        selectedVersion={testVersion.version}\n        onVersionChange={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const toggle = result.getByRole('checkbox');\n    expect(toggle).toBeInTheDocument();\n    fireEvent.click(toggle);\n    expect(onEnabledChange).toBeCalledTimes(1);\n    expect(onEnabledChange).toBeCalledWith(false);\n  });\n\n  it('should call onVersionChange when a new version is selected', () => {\n    const onVersionChange = jest.fn();\n    const result = render(\n      <OtelVersionSelect\n        enabled={true}\n        onEnabledChange={() => {}}\n        selectedVersion={testVersion.version}\n        onVersionChange={onVersionChange}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const select = result.getByRole('combobox');\n    expect(select).toBeInTheDocument();\n    fireEvent.keyDown(select, { key: 'ArrowDown' });\n    fireEvent.keyDown(select, { key: 'Enter' });\n    expect(onVersionChange).toBeCalledTimes(1);\n    expect(onVersionChange).toBeCalledWith(expect.any(String));\n  });\n\n  it('should disable version selection when switch is disabled', () => {\n    const result = render(\n      <OtelVersionSelect\n        enabled={false}\n        onEnabledChange={() => {}}\n        selectedVersion={testVersion.version}\n        onVersionChange={() => {}}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const select = result.getByRole('combobox', { hidden: true });\n    expect(select).toBeDisabled();\n  });\n});\n"
  },
  {
    "path": "src/components/queryBuilder/OtelVersionSelect.tsx",
    "content": "import React, { useEffect } from 'react';\nimport { SelectableValue } from '@grafana/data';\nimport { InlineFormLabel, Select, Switch as GrafanaSwitch, useTheme } from '@grafana/ui';\nimport otel from 'otel';\nimport selectors from 'labels';\n\ninterface OtelVersionSelectProps {\n  enabled: boolean;\n  onEnabledChange: (enabled: boolean) => void;\n  selectedVersion: string;\n  onVersionChange: (version: string) => void;\n  wide?: boolean;\n}\n\nexport const OtelVersionSelect = (props: OtelVersionSelectProps) => {\n  const { enabled, onEnabledChange, selectedVersion, onVersionChange, wide } = props;\n  const { label, tooltip } = selectors.components.OtelVersionSelect;\n  const options: SelectableValue[] = otel.versions.map((v) => ({\n    label: v.name,\n    value: v.version,\n  }));\n\n  useEffect(() => {\n    // Use latest version if not set or doesn't exist (which may happen if config is broken)\n    if (selectedVersion === '' || !otel.getVersion(selectedVersion)) {\n      onVersionChange(otel.getLatestVersion().version);\n    }\n  }, [selectedVersion, onVersionChange]);\n\n  const theme = useTheme();\n  const switchContainerStyle: React.CSSProperties = {\n    padding: `0 ${theme.spacing.sm}`,\n    height: `${theme.spacing.formInputHeight}px`,\n    display: 'flex',\n    alignItems: 'center',\n  };\n\n  return (\n    <div className=\"gf-form\">\n      <InlineFormLabel width={wide ? 12 : 8} className=\"query-keyword\" tooltip={tooltip}>\n        {label}\n      </InlineFormLabel>\n      <div style={switchContainerStyle}>\n        <GrafanaSwitch\n          className=\"gf-form\"\n          value={enabled}\n          onChange={(e) => onEnabledChange(e.currentTarget.checked)}\n          role=\"checkbox\"\n        />\n      </div>\n      <Select\n        disabled={!enabled}\n        options={options}\n        width={20}\n        onChange={(e) => onVersionChange(e.value)}\n        value={selectedVersion}\n        menuPlacement={'bottom'}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/queryBuilder/QueryBuilder.test.tsx",
    "content": "import React from 'react';\nimport { render, screen, waitFor } from '@testing-library/react';\nimport { QueryBuilder } from './QueryBuilder';\nimport { Datasource } from 'data/CHDatasource';\nimport { BuilderMode, QueryType, TimeUnit } from 'types/queryBuilder';\nimport { CoreApp } from '@grafana/data';\n\njest.mock('./views/TableQueryBuilder', () => ({\n  TableQueryBuilder: () => <div data-testid=\"table-component\" />,\n}));\njest.mock('./views/LogsQueryBuilder', () => ({\n  LogsQueryBuilder: () => <div data-testid=\"logs-component\" />,\n}));\njest.mock('./views/TimeSeriesQueryBuilder', () => ({\n  TimeSeriesQueryBuilder: () => <div data-testid=\"time-series-component\" />,\n}));\njest.mock('./views/TraceQueryBuilder', () => ({\n  TraceQueryBuilder: () => <div data-testid=\"trace-component\" />,\n}));\n\ndescribe('QueryBuilder', () => {\n  const setState = jest.fn();\n  const mockDs = { settings: { jsonData: {} } } as Datasource;\n\n  mockDs.fetchDatabases = jest.fn(() => Promise.resolve([]));\n  mockDs.fetchTables = jest.fn((_db?: string) => Promise.resolve([]));\n  mockDs.getDefaultLogsColumns = jest.fn((_db?: string) => new Map());\n  mockDs.getDefaultLogsTable = jest.fn((_db?: string) => '');\n  mockDs.getDefaultLogsDatabase = jest.fn((_db?: string) => '');\n  mockDs.getLogsOtelVersion = jest.fn((_db?: string) => '');\n  mockDs.getDefaultDatabase = jest.fn((_db?: string) => '');\n  mockDs.getDefaultTraceColumns = jest.fn((_db?: string) => new Map());\n  mockDs.shouldSelectLogContextColumns = jest.fn((_db?: string) => false);\n  mockDs.getDefaultTable = jest.fn((_db?: string) => '');\n  mockDs.getDefaultTraceDatabase = jest.fn((_db?: string) => '');\n  mockDs.getDefaultTraceTable = jest.fn((_db?: string) => '');\n  mockDs.getDefaultTraceDurationUnit = jest.fn((_db?: string) => 'ms' as TimeUnit);\n  mockDs.getTraceOtelVersion = jest.fn((_db?: string) => '');\n  mockDs.getDefaultTraceFlattenNested = jest.fn((_db?: string) => false);\n  mockDs.getDefaultTraceEventsColumnPrefix = jest.fn((_db?: string) => '');\n  mockDs.getDefaultTraceLinksColumnPrefix = jest.fn((_db?: string) => '');\n  mockDs.fetchColumns = jest.fn(() => {\n    setState();\n    return Promise.resolve([]);\n  });\n\n  it('renders correctly', async () => {\n    const result = await waitFor(() =>\n      render(\n        <QueryBuilder\n          app={CoreApp.PanelEditor}\n          builderOptions={{\n            queryType: QueryType.Table,\n            mode: BuilderMode.List,\n            database: 'db',\n            table: 'foo',\n            columns: [],\n            filters: [],\n          }}\n          builderOptionsDispatch={() => {}}\n          datasource={mockDs}\n          generatedSql=\"\"\n        />\n      )\n    );\n    expect(result.container.firstChild).not.toBeNull();\n  });\n\n  it('renders TableQueryBuilder when queryType is Table', () => {\n    render(\n      <React.Suspense fallback={<div>Loading...</div>}>\n        <QueryBuilder\n          app={CoreApp.PanelEditor}\n          builderOptions={{\n            queryType: QueryType.Table,\n            mode: BuilderMode.List,\n            database: 'db',\n            table: 'foo',\n            columns: [],\n            filters: [],\n          }}\n          builderOptionsDispatch={() => {}}\n          datasource={mockDs}\n          generatedSql=\"\"\n        />\n      </React.Suspense>\n    );\n    expect(screen.getByTestId('table-component')).toBeInTheDocument();\n  });\n\n  it('renders LogsQueryBuilder when queryType is Logs', async () => {\n    render(\n      <QueryBuilder\n        app={CoreApp.PanelEditor}\n        builderOptions={{\n          queryType: QueryType.Logs,\n          mode: BuilderMode.List,\n          database: 'db',\n          table: 'foo',\n          columns: [],\n          filters: [],\n        }}\n        builderOptionsDispatch={() => {}}\n        datasource={mockDs}\n        generatedSql=\"\"\n      />\n    );\n    expect(screen.getByTestId('logs-component')).toBeInTheDocument();\n  });\n\n  it('renders TimeSeriesQueryBuilder when queryType is TimeSeries', async () => {\n    render(\n      <QueryBuilder\n        app={CoreApp.PanelEditor}\n        builderOptions={{\n          queryType: QueryType.TimeSeries,\n          mode: BuilderMode.List,\n          database: 'db',\n          table: 'foo',\n          columns: [],\n          filters: [],\n        }}\n        builderOptionsDispatch={() => {}}\n        datasource={mockDs}\n        generatedSql=\"\"\n      />\n    );\n    await waitFor(() => {\n      expect(screen.getByTestId('time-series-component')).toBeInTheDocument();\n    });\n  });\n\n  it('renders TraceQueryBuilder when queryType is Traces', async () => {\n    render(\n      <QueryBuilder\n        app={CoreApp.PanelEditor}\n        builderOptions={{\n          queryType: QueryType.Traces,\n          mode: BuilderMode.List,\n          database: 'db',\n          table: 'foo',\n          columns: [],\n          filters: [],\n        }}\n        builderOptionsDispatch={() => {}}\n        datasource={mockDs}\n        generatedSql=\"\"\n      />\n    );\n    await waitFor(() => {\n      expect(screen.getByTestId('trace-component')).toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "src/components/queryBuilder/QueryBuilder.tsx",
    "content": "import React, { useMemo } from 'react';\nimport { Datasource } from 'data/CHDatasource';\nimport { QueryType, QueryBuilderOptions, ColumnHint, StringFilter } from 'types/queryBuilder';\nimport { CoreApp } from '@grafana/data';\nimport { LogsQueryBuilder } from './views/LogsQueryBuilder';\nimport { TimeSeriesQueryBuilder } from './views/TimeSeriesQueryBuilder';\nimport { TableQueryBuilder } from './views/TableQueryBuilder';\nimport { SqlPreview } from './SqlPreview';\nimport { DatabaseTableSelect } from 'components/queryBuilder/DatabaseTableSelect';\nimport { QueryTypeSwitcher } from 'components/queryBuilder/QueryTypeSwitcher';\nimport { styles } from 'styles';\nimport { TraceQueryBuilder } from './views/TraceQueryBuilder';\nimport {\n  BuilderOptionsReducerAction,\n  setBuilderMinimized,\n  setDatabase,\n  setQueryType,\n  setTable,\n} from 'hooks/useBuilderOptionsState';\nimport TraceIdInput from './TraceIdInput';\nimport { Alert, Button, VerticalGroup } from '@grafana/ui';\nimport { Components as allSelectors } from 'selectors';\nimport allLabels from 'labels';\n\ninterface QueryBuilderProps {\n  app: CoreApp | undefined;\n  builderOptions: QueryBuilderOptions;\n  builderOptionsDispatch: React.Dispatch<BuilderOptionsReducerAction>;\n  datasource: Datasource;\n  generatedSql: string;\n}\n\nexport const QueryBuilder = (props: QueryBuilderProps) => {\n  const { datasource, builderOptions, builderOptionsDispatch, generatedSql } = props;\n\n  const onDatabaseChange = (database: string) => builderOptionsDispatch(setDatabase(database));\n  const onTableChange = (table: string) => builderOptionsDispatch(setTable(table));\n  const onQueryTypeChange = (queryType: QueryType) => builderOptionsDispatch(setQueryType(queryType));\n\n  if (builderOptions.meta?.minimized) {\n    return (\n      <MinimizedQueryViewer\n        builderOptions={builderOptions}\n        builderOptionsDispatch={builderOptionsDispatch}\n        datasource={datasource}\n      />\n    );\n  }\n\n  return (\n    <div data-testid=\"query-editor-section-builder\">\n      <div className={'gf-form ' + styles.QueryEditor.queryType}>\n        <DatabaseTableSelect\n          datasource={datasource}\n          database={builderOptions.database}\n          onDatabaseChange={onDatabaseChange}\n          table={builderOptions.table}\n          onTableChange={onTableChange}\n        />\n      </div>\n      <div className={'gf-form ' + styles.QueryEditor.queryType}>\n        <QueryTypeSwitcher queryType={builderOptions.queryType} onChange={onQueryTypeChange} />\n      </div>\n\n      {builderOptions.queryType === QueryType.Table && (\n        <TableQueryBuilder\n          datasource={datasource}\n          builderOptions={builderOptions}\n          builderOptionsDispatch={builderOptionsDispatch}\n        />\n      )}\n      {builderOptions.queryType === QueryType.Logs && (\n        <LogsQueryBuilder\n          datasource={datasource}\n          builderOptions={builderOptions}\n          builderOptionsDispatch={builderOptionsDispatch}\n        />\n      )}\n      {builderOptions.queryType === QueryType.TimeSeries && (\n        <TimeSeriesQueryBuilder\n          datasource={datasource}\n          builderOptions={builderOptions}\n          builderOptionsDispatch={builderOptionsDispatch}\n        />\n      )}\n      {builderOptions.queryType === QueryType.Traces && (\n        <TraceQueryBuilder\n          datasource={datasource}\n          builderOptions={builderOptions}\n          builderOptionsDispatch={builderOptionsDispatch}\n        />\n      )}\n\n      <SqlPreview sql={generatedSql} />\n    </div>\n  );\n};\n\ninterface MinimizedQueryBuilder {\n  builderOptions: QueryBuilderOptions;\n  builderOptionsDispatch: React.Dispatch<BuilderOptionsReducerAction>;\n  datasource: Datasource;\n}\n\nconst MinimizedQueryViewer = (props: MinimizedQueryBuilder) => {\n  const { builderOptions, builderOptionsDispatch, datasource } = props;\n  const defaultColumns = useMemo<Map<ColumnHint, string> | undefined>(() => {\n    if (builderOptions.queryType === QueryType.Logs) {\n      return datasource.getDefaultLogsColumns();\n    } else if (builderOptions.queryType === QueryType.Traces) {\n      return datasource.getDefaultTraceColumns();\n    }\n\n    return undefined;\n  }, [builderOptions.queryType, datasource]);\n  const showConfigWarning = defaultColumns?.size === 0 && builderOptions.columns?.length === 0;\n  const configQueryType =\n    builderOptions.queryType === QueryType.Logs\n      ? 'logs'\n      : builderOptions.queryType === QueryType.Traces\n        ? 'trace'\n        : builderOptions.queryType;\n\n  let traceId;\n  if (\n    builderOptions.queryType === QueryType.Traces &&\n    builderOptions.meta?.isTraceIdMode &&\n    builderOptions.meta.traceId\n  ) {\n    traceId = builderOptions.meta.traceId!;\n  } else if (\n    builderOptions.queryType === QueryType.Logs &&\n    builderOptions.filters?.find((f) => f.hint === ColumnHint.TraceId && 'value' in f)\n  ) {\n    const traceIdFilter = builderOptions.filters?.find(\n      (f) => f.hint === ColumnHint.TraceId && 'value' in f\n    ) as StringFilter;\n    traceId = traceIdFilter.value;\n  }\n\n  return (\n    <div data-testid=\"query-editor-minimized-viewer\">\n      {showConfigWarning && (\n        <Alert title=\"\" severity=\"warning\">\n          <VerticalGroup>\n            <div>\n              {`To enable data linking, enter your default ${configQueryType} configuration in your `}\n              <a\n                style={{ textDecoration: 'underline' }}\n                href={`/connections/datasources/edit/${encodeURIComponent(datasource.uid)}#${builderOptions.queryType}-config`}\n              >\n                ClickHouse Data Source settings\n              </a>\n            </div>\n          </VerticalGroup>\n        </Alert>\n      )}\n      {!traceId && (\n        <Alert title=\"\" severity=\"warning\">\n          <VerticalGroup>\n            <div>Trace ID is empty</div>\n          </VerticalGroup>\n        </Alert>\n      )}\n\n      {traceId && <TraceIdInput traceId={traceId} onChange={() => {}} disabled />}\n\n      <Button\n        data-testid={allSelectors.QueryBuilder.expandBuilderButton}\n        icon=\"plus\"\n        variant=\"secondary\"\n        size=\"md\"\n        onClick={() => builderOptionsDispatch(setBuilderMinimized(false))}\n        className={styles.Common.smallBtn}\n        tooltip={allLabels.components.expandBuilderButton.tooltip}\n      >\n        {allLabels.components.expandBuilderButton.label}\n      </Button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/queryBuilder/QueryTypeSwitcher.test.tsx",
    "content": "import React from 'react';\nimport { fireEvent, render } from '@testing-library/react';\nimport { QueryTypeSwitcher } from './QueryTypeSwitcher';\nimport labels from 'labels';\nimport { QueryType } from 'types/queryBuilder';\n\nconst options = {\n  Table: labels.types.QueryType.table,\n  Logs: labels.types.QueryType.logs,\n  TimeSeries: labels.types.QueryType.timeseries,\n  Traces: labels.types.QueryType.traces,\n};\n\ndescribe('QueryTypeSwitcher', () => {\n  it('should render with default props', () => {\n    const result = render(<QueryTypeSwitcher queryType={QueryType.Table} onChange={() => {}} />);\n    expect(result.container.firstChild).not.toBeNull();\n    expect(result.getByLabelText(options.Table)).toBeChecked();\n    expect(result.getByLabelText(options.Logs)).not.toBeChecked();\n    expect(result.getByLabelText(options.TimeSeries)).not.toBeChecked();\n    expect(result.getByLabelText(options.Traces)).not.toBeChecked();\n  });\n\n  it('should call onChange when a new option is selected', async () => {\n    const onChange = jest.fn();\n    const result = render(<QueryTypeSwitcher queryType={QueryType.Table} onChange={onChange} />);\n    expect(result.container.firstChild).not.toBeNull();\n    const timeSeriesButton = result.getByLabelText(options.TimeSeries);\n    expect(timeSeriesButton).toBeInTheDocument();\n    fireEvent.click(timeSeriesButton);\n    expect(onChange).toBeCalledTimes(1);\n    expect(onChange).toBeCalledWith(QueryType.TimeSeries);\n  });\n});\n"
  },
  {
    "path": "src/components/queryBuilder/QueryTypeSwitcher.tsx",
    "content": "import React from 'react';\nimport { RadioButtonGroup, InlineFormLabel } from '@grafana/ui';\nimport labels from 'labels';\nimport { QueryType } from 'types/queryBuilder';\n\nexport interface QueryTypeSwitcherProps {\n  queryType: QueryType;\n  onChange: (queryType: QueryType) => void;\n  sqlEditor?: boolean;\n}\n\nconst options = [\n  {\n    label: labels.types.QueryType.table,\n    value: QueryType.Table,\n  },\n  {\n    label: labels.types.QueryType.logs,\n    value: QueryType.Logs,\n  },\n  {\n    label: labels.types.QueryType.timeseries,\n    value: QueryType.TimeSeries,\n  },\n  {\n    label: labels.types.QueryType.traces,\n    value: QueryType.Traces,\n  },\n];\n\n/**\n * Component for switching between the different query builder interfaces\n */\nexport const QueryTypeSwitcher = (props: QueryTypeSwitcherProps) => {\n  const { queryType, onChange, sqlEditor } = props;\n  const { label, tooltip, sqlTooltip } = labels.components.QueryTypeSwitcher;\n\n  return (\n    <span>\n      <InlineFormLabel width={8} className=\"query-keyword\" tooltip={sqlEditor ? sqlTooltip : tooltip}>\n        {label}\n      </InlineFormLabel>\n      <RadioButtonGroup options={options} value={queryType} onChange={onChange} />\n    </span>\n  );\n};\n"
  },
  {
    "path": "src/components/queryBuilder/SqlPreview.test.tsx",
    "content": "import React from 'react';\nimport { render } from '@testing-library/react';\nimport { SqlPreview } from './SqlPreview';\n\ndescribe('SqlPreview', () => {\n  it('renders correctly', () => {\n    const result = render(<SqlPreview sql=\"\" />);\n    expect(result.container.firstChild).not.toBeNull();\n  });\n});\n"
  },
  {
    "path": "src/components/queryBuilder/SqlPreview.tsx",
    "content": "import React from 'react';\nimport { InlineFormLabel } from '@grafana/ui';\nimport labels from 'labels';\n\ninterface SqlPreviewProps {\n  sql: string;\n}\n\nexport const SqlPreview = (props: SqlPreviewProps) => {\n  const { sql } = props;\n  const { label, tooltip } = labels.components.SqlPreview;\n\n  return (\n    <div className=\"gf-form\">\n      <InlineFormLabel width={8} className=\"query-keyword\" tooltip={tooltip}>\n        {label}\n      </InlineFormLabel>\n      <pre>{sql}</pre>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/queryBuilder/Switch.test.tsx",
    "content": "import React from 'react';\nimport { render } from '@testing-library/react';\nimport { Switch } from './Switch';\n\ndescribe('Switch', () => {\n  const label = 'test';\n  const tooltip = 'tooltip';\n\n  it('should render', () => {\n    const result = render(<Switch value={false} onChange={() => {}} label={label} tooltip={tooltip} />);\n\n    expect(result.container.firstChild).not.toBeNull();\n  });\n\n  // Commented out as it's broken post npm upgrade - needs investigation\n  // it('should call onChange when mode is changed', () => {\n  //   const onChange = jest.fn();\n  //   const result = render(<Switch value={false} onChange={onChange} label={label} tooltip={tooltip} />);\n  //   expect(result.container.firstChild).not.toBeNull();\n\n  //   const checkbox = result.getByRole('checkbox');\n  //   expect(checkbox).toBeInTheDocument();\n\n  //   fireEvent.click(checkbox);\n  //   expect(onChange).toBeCalledTimes(1);\n  //   expect(onChange).toBeCalledWith(true);\n\n  //   result.rerender(<Switch value={true} onChange={onChange} label={label} tooltip={tooltip} />);\n\n  //   fireEvent.click(checkbox);\n  //   expect(onChange).toBeCalledTimes(2);\n  //   expect(onChange).toBeCalledWith(false);\n  // });\n});\n"
  },
  {
    "path": "src/components/queryBuilder/Switch.tsx",
    "content": "import React from 'react';\nimport { InlineFormLabel, Switch as GrafanaSwitch, useTheme } from '@grafana/ui';\nimport { styles } from 'styles';\n\ninterface SwitchProps {\n  value: boolean;\n  onChange: (value: boolean) => void;\n  label: string;\n  tooltip: string;\n  disabled?: boolean;\n  inline?: boolean;\n  wide?: boolean;\n}\n\nexport const Switch = (props: SwitchProps) => {\n  const { value, onChange, label, tooltip, disabled, inline, wide } = props;\n\n  const theme = useTheme();\n  const switchContainerStyle: React.CSSProperties = {\n    padding: `0 ${theme.spacing.sm}`,\n    height: `${theme.spacing.formInputHeight}px`,\n    display: 'flex',\n    alignItems: 'center',\n  };\n\n  const labelStyle = 'query-keyword ' + (inline ? styles.QueryEditor.inlineField : '');\n\n  return (\n    <div className=\"gf-form\">\n      <InlineFormLabel width={wide ? 12 : 8} className={labelStyle} tooltip={tooltip}>\n        {label}\n      </InlineFormLabel>\n      <div style={switchContainerStyle}>\n        <GrafanaSwitch\n          disabled={disabled}\n          className=\"gf-form\"\n          value={value}\n          onChange={(e) => onChange(e.currentTarget.checked)}\n        />\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/queryBuilder/TraceIdInput.test.tsx",
    "content": "import React from 'react';\nimport { render } from '@testing-library/react';\nimport { selectors } from 'selectors';\nimport TraceIdInput from './TraceIdInput';\nimport userEvent from '@testing-library/user-event';\n\ndescribe('TraceIdInput', () => {\n  it('should render', () => {\n    const result = render(<TraceIdInput traceId=\"\" onChange={() => {}} />);\n    expect(result.container.firstChild).not.toBeNull();\n  });\n\n  it('should call onChange when ID is changed', async () => {\n    const onChange = jest.fn();\n    const result = render(<TraceIdInput traceId=\"\" onChange={onChange} />);\n    expect(result.container.firstChild).not.toBeNull();\n\n    const idInput = result.getByTestId(selectors.components.QueryBuilder.TraceIdInput.input);\n    expect(idInput).toBeInTheDocument();\n    await userEvent.type(idInput, 'test');\n    idInput.blur();\n    expect(idInput).toHaveValue('test');\n    expect(onChange).toHaveBeenCalledTimes(1);\n    expect(onChange).toHaveBeenCalledWith('test');\n  });\n});\n"
  },
  {
    "path": "src/components/queryBuilder/TraceIdInput.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport allLabels from 'labels';\nimport { InlineFormLabel, Input } from '@grafana/ui';\nimport { selectors } from 'selectors';\n\ninterface TraceIdInputProps {\n  traceId: string;\n  onChange: (traceId: string) => void;\n  disabled?: boolean;\n}\n\nconst TraceIdInput = (props: TraceIdInputProps) => {\n  const [inputId, setInputId] = useState<string>('');\n  const { traceId, onChange, disabled } = props;\n  const { label, tooltip } = allLabels.components.TraceQueryBuilder.columns.traceIdFilter;\n\n  useEffect(() => {\n    setInputId(traceId);\n  }, [traceId]);\n\n  return (\n    <div className=\"gf-form\">\n      <InlineFormLabel width={8} className=\"query-keyword\" tooltip={tooltip}>\n        {label}\n      </InlineFormLabel>\n      <Input\n        data-testid={selectors.components.QueryBuilder.TraceIdInput.input}\n        width={40}\n        value={inputId}\n        disabled={disabled}\n        type=\"string\"\n        onChange={(e) => setInputId(e.currentTarget.value)}\n        onBlur={() => onChange(inputId)}\n      />\n    </div>\n  );\n};\n\nexport default TraceIdInput;\n"
  },
  {
    "path": "src/components/queryBuilder/utils.test.ts",
    "content": "import { generateSql } from 'data/sqlGenerator';\nimport { getQueryOptionsFromSql, isDateTimeType, isDateType, isNumberType } from './utils';\nimport {\n  AggregateType,\n  BuilderMode,\n  ColumnHint,\n  DateFilterWithoutValue,\n  FilterOperator,\n  MultiFilter,\n  OrderByDirection,\n  QueryBuilderOptions,\n  QueryType,\n} from 'types/queryBuilder';\nimport { Datasource } from 'data/CHDatasource';\nimport otel from 'otel';\n\ndescribe('isDateType', () => {\n  it('returns true for Date type', () => {\n    expect(isDateType('Date')).toBe(true);\n    expect(isDateType('date')).toBe(true);\n  });\n  it('returns true for Nullable(Date) type', () => {\n    expect(isDateType('Nullable(Date)')).toBe(true);\n  });\n\n  it('returns true for Date32 type', () => {\n    expect(isDateType('Date32')).toBe(true);\n    expect(isDateType('date32')).toBe(true);\n  });\n  it('returns true for Nullable(Date) type', () => {\n    expect(isDateType('Nullable(Date32)')).toBe(true);\n  });\n\n  it('returns true for Datetime type', () => {\n    expect(isDateType('Datetime')).toBe(true);\n    expect(isDateType('datetime')).toBe(true);\n    expect(isDateType(\"DateTime('Asia/Istanbul')\")).toBe(true);\n  });\n  it('returns true for Nullable(Date) type', () => {\n    expect(isDateType(\"Nullable(DateTime('Asia/Istanbul'))\")).toBe(true);\n  });\n\n  it('returns true for Datetime64 type', () => {\n    expect(isDateType('Datetime64(3)')).toBe(true);\n    expect(isDateType('datetime64(3)')).toBe(true);\n    expect(isDateType(\"Datetime64(3, 'Asia/Istanbul')\")).toBe(true);\n  });\n  it('returns true for Nullable(Date) type', () => {\n    expect(isDateType(\"Nullable(Datetime64(3, 'Asia/Istanbul'))\")).toBe(true);\n  });\n\n  it('returns false for other types', () => {\n    expect(isDateType('boolean')).toBe(false);\n    expect(isDateType('Boolean')).toBe(false);\n  });\n});\n\ndescribe('isDateTimeType', () => {\n  it('returns true for DateTime type', () => {\n    expect(isDateTimeType('DateTime')).toBe(true);\n    expect(isDateTimeType('datetime')).toBe(true);\n  });\n  it('returns true for Nullable(DateTime) type', () => {\n    expect(isDateTimeType('Nullable(DateTime)')).toBe(true);\n  });\n  it('returns true for DateTime64 type', () => {\n    expect(isDateTimeType('DateTime64(3)')).toBe(true);\n    expect(isDateTimeType('datetime64(3)')).toBe(true);\n    expect(isDateTimeType(\"Datetime64(3, 'Asia/Istanbul')\")).toBe(true);\n  });\n  it('returns true for Nullable(DateTime64(3)) type', () => {\n    expect(isDateTimeType('Nullable(DateTime64(3))')).toBe(true);\n    expect(isDateTimeType(\"Nullable(DateTime64(3, 'Asia/Istanbul'))\")).toBe(true);\n  });\n  it('returns false for Date type', () => {\n    expect(isDateTimeType('Date')).toBe(false);\n    expect(isDateTimeType('date')).toBe(false);\n    expect(isDateTimeType('Date32')).toBe(false);\n    expect(isDateTimeType('date32')).toBe(false);\n  });\n  it('returns false for Nullable(Date) type', () => {\n    expect(isDateTimeType('Nullable(Date)')).toBe(false);\n    expect(isDateTimeType('Nullable(Date32)')).toBe(false);\n    expect(isDateTimeType('nullable(date)')).toBe(false);\n    expect(isDateTimeType('nullable(date32)')).toBe(false);\n  });\n  it('returns false for other types', () => {\n    expect(isDateTimeType('boolean')).toBe(false);\n    expect(isDateTimeType('String')).toBe(false);\n  });\n});\n\ndescribe('isNumberType', () => {\n  it('returns true for UInt* types', () => {\n    expect(isNumberType('UInt8')).toBe(true);\n    expect(isNumberType('UInt16')).toBe(true);\n    expect(isNumberType('UInt32')).toBe(true);\n    expect(isNumberType('UInt64')).toBe(true);\n    expect(isNumberType('UInt128')).toBe(true);\n    expect(isNumberType('UInt256')).toBe(true);\n  });\n\n  it('returns true for Int* types', () => {\n    expect(isNumberType('Int8')).toBe(true);\n    expect(isNumberType('Int16')).toBe(true);\n    expect(isNumberType('Int32')).toBe(true);\n    expect(isNumberType('Int64')).toBe(true);\n    expect(isNumberType('Int128')).toBe(true);\n    expect(isNumberType('Int256')).toBe(true);\n  });\n\n  it('returns true for Float types', () => {\n    expect(isNumberType('Float32')).toBe(true);\n    expect(isNumberType('Float64')).toBe(true);\n  });\n\n  it('returns true for Decimal types', () => {\n    expect(isNumberType('Decimal(1,2)')).toBe(true);\n    expect(isNumberType('Decimal32(3)')).toBe(true);\n    expect(isNumberType('Decimal64(3)')).toBe(true);\n    expect(isNumberType('Decimal128(3)')).toBe(true);\n    expect(isNumberType('Decimal256(3)')).toBe(true);\n  });\n\n  it('returns false for other types', () => {\n    expect(isNumberType('boolean')).toBe(false);\n    expect(isNumberType('datetime')).toBe(false);\n    expect(isNumberType('Nullable')).toBe(false);\n  });\n});\n\ndescribe('getQueryOptionsFromSql', () => {\n  testCondition('handles a table without a database', 'SELECT name FROM \"foo\"', {\n    queryType: QueryType.Table,\n    mode: BuilderMode.List,\n    database: '',\n    table: 'foo',\n    columns: [{ name: 'name', alias: undefined }],\n    aggregates: [],\n  });\n\n  testCondition('handles a database with a special character', 'SELECT name FROM \"foo-bar\".\"buzz\"', {\n    queryType: QueryType.Table,\n    mode: BuilderMode.List,\n    database: 'foo-bar',\n    table: 'buzz',\n    columns: [{ name: 'name', alias: undefined }],\n    aggregates: [],\n  });\n\n  testCondition('handles a database and a table', 'SELECT name FROM \"db\".\"foo\"', {\n    queryType: QueryType.Table,\n    mode: BuilderMode.List,\n    database: 'db',\n    table: 'foo',\n    columns: [{ name: 'name', alias: undefined }],\n    aggregates: [],\n  });\n\n  testCondition('handles a database and a table with a dot', 'SELECT name FROM \"db\".\"foo.bar\"', {\n    queryType: QueryType.Table,\n    mode: BuilderMode.List,\n    database: 'db',\n    table: 'foo.bar',\n    columns: [{ name: 'name', alias: undefined }],\n    aggregates: [],\n  });\n\n  testCondition('handles 2 columns', 'SELECT field1, field2 FROM \"db\".\"foo\"', {\n    queryType: QueryType.Table,\n    mode: BuilderMode.List,\n    database: 'db',\n    table: 'foo',\n    columns: [\n      { name: 'field1', alias: undefined },\n      { name: 'field2', alias: undefined },\n    ],\n    aggregates: [],\n  });\n\n  testCondition('handles a limit', 'SELECT field1, field2 FROM \"db\".\"foo\" LIMIT 20', {\n    queryType: QueryType.Table,\n    mode: BuilderMode.List,\n    database: 'db',\n    table: 'foo',\n    columns: [\n      { name: 'field1', alias: undefined },\n      { name: 'field2', alias: undefined },\n    ],\n    aggregates: [],\n    limit: 20,\n  });\n\n  testCondition(\n    'handles empty orderBy array',\n    'SELECT field1, field2 FROM \"db\".\"foo\" LIMIT 20',\n    {\n      queryType: QueryType.Table,\n      mode: BuilderMode.List,\n      database: 'db',\n      table: 'foo',\n      columns: [\n        { name: 'field1', alias: undefined },\n        { name: 'field2', alias: undefined },\n      ],\n      aggregates: [],\n      orderBy: [],\n      limit: 20,\n    },\n    false\n  );\n\n  testCondition('handles order by', 'SELECT field1, field2 FROM \"db\".\"foo\" ORDER BY field1 ASC LIMIT 20', {\n    queryType: QueryType.Table,\n    mode: BuilderMode.List,\n    database: 'db',\n    table: 'foo',\n    columns: [\n      { name: 'field1', alias: undefined },\n      { name: 'field2', alias: undefined },\n    ],\n    aggregates: [],\n    orderBy: [{ name: 'field1', dir: OrderByDirection.ASC }],\n    limit: 20,\n  });\n\n  testCondition(\n    'handles no select',\n    'SELECT FROM \"db\"',\n    {\n      queryType: QueryType.Table,\n      mode: BuilderMode.Aggregate,\n      database: 'db',\n      table: '',\n      columns: [],\n      aggregates: [],\n    },\n    false\n  );\n\n  testCondition(\n    'does not escape * field',\n    'SELECT * FROM \"db\"',\n    {\n      queryType: QueryType.Table,\n      mode: BuilderMode.Aggregate,\n      database: 'db',\n      table: '',\n      columns: [{ name: '*' }],\n      aggregates: [],\n      limit: undefined,\n    },\n    false\n  );\n\n  testCondition('handles aggregation function', 'SELECT sum(field1) FROM \"db\".\"foo\"', {\n    queryType: QueryType.Table,\n    mode: BuilderMode.Aggregate,\n    database: 'db',\n    table: 'foo',\n    columns: [],\n    aggregates: [{ column: 'field1', aggregateType: AggregateType.Sum, alias: undefined }],\n    limit: undefined,\n  });\n\n  testCondition('handles aggregation with alias', 'SELECT sum(field1) as total_records FROM \"db\".\"foo\"', {\n    queryType: QueryType.Table,\n    mode: BuilderMode.Aggregate,\n    database: 'db',\n    table: 'foo',\n    columns: [],\n    aggregates: [{ column: 'field1', aggregateType: AggregateType.Sum, alias: 'total_records' }],\n    limit: undefined,\n  });\n\n  testCondition(\n    'handles 2 aggregations',\n    'SELECT sum(field1) as total_records, count(field2) as total_records2 FROM \"db\".\"foo\"',\n    {\n      queryType: QueryType.Table,\n      mode: BuilderMode.Aggregate,\n      table: 'foo',\n      database: 'db',\n      columns: [],\n      aggregates: [\n        { column: 'field1', aggregateType: AggregateType.Sum, alias: 'total_records' },\n        { column: 'field2', aggregateType: AggregateType.Count, alias: 'total_records2' },\n      ],\n      limit: undefined,\n    }\n  );\n\n  testCondition(\n    'handles aggregation with groupBy',\n    'SELECT sum(field1) as total_records, count(field2) as total_records2 FROM \"db\".\"foo\" GROUP BY field3',\n    {\n      database: 'db',\n      table: 'foo',\n      queryType: QueryType.Table,\n      mode: BuilderMode.Aggregate,\n      columns: [],\n      aggregates: [\n        { column: 'field1', aggregateType: AggregateType.Sum, alias: 'total_records' },\n        { column: 'field2', aggregateType: AggregateType.Count, alias: 'total_records2' },\n      ],\n      groupBy: ['field3'],\n    },\n    false\n  );\n\n  testCondition(\n    'handles aggregation with groupBy with columns having group by value',\n    'SELECT field3, sum(field1) as total_records, count(field2) as total_records2 FROM \"db\".\"foo\" GROUP BY field3',\n    {\n      queryType: QueryType.Table,\n      mode: BuilderMode.Aggregate,\n      table: 'foo',\n      database: 'db',\n      columns: [{ name: 'field3' }],\n      aggregates: [\n        { column: 'field1', aggregateType: AggregateType.Sum, alias: 'total_records' },\n        { column: 'field2', aggregateType: AggregateType.Count, alias: 'total_records2' },\n      ],\n      groupBy: ['field3'],\n      limit: undefined,\n    }\n  );\n\n  testCondition(\n    'handles aggregation with group by and order by',\n    'SELECT StageName, Type, count(Id) as count_of, sum(Amount) FROM \"db\".\"foo\" GROUP BY StageName, Type ORDER BY count_of DESC, StageName ASC',\n    {\n      mode: BuilderMode.Aggregate,\n      database: 'db',\n      table: 'foo',\n      queryType: QueryType.Table,\n      columns: [{ name: 'StageName' }, { name: 'Type' }],\n      aggregates: [\n        { column: 'Id', aggregateType: AggregateType.Count, alias: 'count_of' },\n        { column: 'Amount', aggregateType: AggregateType.Sum, alias: undefined },\n      ],\n      groupBy: ['StageName', 'Type'],\n      orderBy: [\n        { name: 'count_of', dir: OrderByDirection.DESC },\n        { name: 'StageName', dir: OrderByDirection.ASC },\n      ],\n    },\n    false\n  );\n\n  testCondition(\n    'handles aggregation with a IN filter',\n    `SELECT count(id) FROM \"db\".\"foo\" WHERE ( stagename IN ('Deal Won', 'Deal Lost') )`,\n    {\n      queryType: QueryType.Table,\n      mode: BuilderMode.Aggregate,\n      database: 'db',\n      table: 'foo',\n      columns: [],\n      aggregates: [{ column: 'id', aggregateType: AggregateType.Count, alias: undefined }],\n      filters: [\n        {\n          key: 'stagename',\n          operator: FilterOperator.In,\n          value: ['Deal Won', 'Deal Lost'],\n          type: 'string',\n        } as MultiFilter,\n      ],\n    }\n  );\n\n  testCondition(\n    'handles aggregation with a NOT IN filter',\n    `SELECT count(id) FROM \"db\".\"foo\" WHERE ( stagename NOT IN ('Deal Won', 'Deal Lost') )`,\n    {\n      queryType: QueryType.Table,\n      mode: BuilderMode.Aggregate,\n      database: 'db',\n      table: 'foo',\n      columns: [],\n      aggregates: [{ column: 'id', aggregateType: AggregateType.Count, alias: undefined }],\n      filters: [\n        {\n          key: 'stagename',\n          operator: FilterOperator.NotIn,\n          value: ['Deal Won', 'Deal Lost'],\n          type: 'string',\n        } as MultiFilter,\n      ],\n      limit: undefined,\n    }\n  );\n\n  testCondition(\n    'handles aggregation with datetime filter',\n    `SELECT count(id) FROM \"db\".\"foo\" WHERE ( createddate >= $__fromTime AND createddate <= $__toTime )`,\n    {\n      queryType: QueryType.Table,\n      mode: BuilderMode.Aggregate,\n      database: 'db',\n      table: 'foo',\n      columns: [],\n      aggregates: [{ column: 'id', aggregateType: AggregateType.Count, alias: undefined }],\n      filters: [\n        {\n          key: 'createddate',\n          operator: FilterOperator.WithInGrafanaTimeRange,\n          type: 'datetime',\n        } as DateFilterWithoutValue,\n      ],\n      limit: undefined,\n    }\n  );\n\n  testCondition(\n    'handles aggregation with date filter',\n    `SELECT count(id) FROM \"db\".\"foo\" WHERE ( NOT ( closedate >= $__fromTime AND closedate <= $__toTime ) )`,\n    {\n      queryType: QueryType.Table,\n      mode: BuilderMode.Aggregate,\n      database: 'db',\n      table: 'foo',\n      columns: [],\n      aggregates: [{ column: 'id', aggregateType: AggregateType.Count, alias: undefined }],\n      filters: [\n        {\n          key: 'closedate',\n          operator: FilterOperator.OutsideGrafanaTimeRange,\n          type: 'datetime',\n        } as DateFilterWithoutValue,\n      ],\n      limit: undefined,\n    }\n  );\n\n  testCondition(\n    'handles timeseries function with \"timeFieldType: DateType\"',\n    'SELECT $__timeInterval(time) as \"time\" FROM \"db\".\"foo\" GROUP BY time',\n    {\n      queryType: QueryType.TimeSeries,\n      mode: BuilderMode.Trend,\n      database: 'db',\n      table: 'foo',\n      columns: [{ name: 'time', type: 'datetime', hint: ColumnHint.Time }],\n      aggregates: [],\n      filters: [],\n    },\n    false\n  );\n\n  testCondition(\n    'handles timeseries function with \"timeFieldType: DateType\" with a filter',\n    'SELECT $__timeInterval(time) as \"time\" FROM \"db\".\"foo\" WHERE ( base IS NOT NULL ) GROUP BY time',\n    {\n      queryType: QueryType.TimeSeries,\n      mode: BuilderMode.Trend,\n      database: 'db',\n      table: 'foo',\n      columns: [{ name: 'time', type: 'datetime', hint: ColumnHint.Time }],\n      aggregates: [],\n      filters: [\n        {\n          condition: 'AND',\n          filterType: 'custom',\n          key: 'base',\n          operator: FilterOperator.IsNotNull,\n          type: 'LowCardinality(String)',\n        },\n      ],\n    },\n    false\n  );\n\n  testCondition(\n    'handles parsing a column with a complex name with spaces and capital characters',\n    'SELECT \"Complex Name\" FROM \"db\".\"foo\"',\n    {\n      queryType: QueryType.Table,\n      mode: BuilderMode.List,\n      database: 'db',\n      table: 'foo',\n      columns: [{ name: 'Complex Name', alias: undefined }],\n      aggregates: [],\n    }\n  );\n\n  it('matches input query type', () => {\n    const sql = 'SELECT test FROM \"db\".\"foo\"';\n    const expectedOptions: QueryBuilderOptions = {\n      queryType: QueryType.Logs,\n      mode: BuilderMode.List,\n      database: 'db',\n      table: 'foo',\n      columns: [{ name: 'test', alias: undefined }],\n      aggregates: [],\n    };\n\n    expect(getQueryOptionsFromSql(sql, QueryType.Logs)).toEqual(expectedOptions);\n  });\n\n  it('matches column hints with Grafana query aliases', () => {\n    const sql = 'SELECT a as body, b as level FROM \"db\".\"foo\"';\n    const expectedOptions: QueryBuilderOptions = {\n      queryType: QueryType.Logs,\n      mode: BuilderMode.List,\n      database: 'db',\n      table: 'foo',\n      columns: [\n        { name: 'a', alias: 'body', hint: ColumnHint.LogMessage },\n        { name: 'b', alias: 'level', hint: ColumnHint.LogLevel },\n      ],\n      aggregates: [],\n      limit: undefined,\n    };\n\n    expect(getQueryOptionsFromSql(sql, QueryType.Logs)).toEqual(expectedOptions);\n  });\n\n  it('matches column hints with OTel log column names', () => {\n    const mockDs = {} as Datasource;\n    mockDs.getDefaultLogsColumns = jest.fn(() => otel.getLatestVersion().logColumnMap);\n\n    const sql = 'SELECT \"Timestamp\", \"SeverityText\" FROM \"db\".\"foo\"';\n    const expectedOptions: QueryBuilderOptions = {\n      queryType: QueryType.Logs,\n      mode: BuilderMode.List,\n      database: 'db',\n      table: 'foo',\n      columns: [\n        { name: 'Timestamp', alias: undefined, hint: ColumnHint.Time },\n        { name: 'SeverityText', alias: undefined, hint: ColumnHint.LogLevel },\n      ],\n      aggregates: [],\n      limit: undefined,\n    };\n\n    expect(getQueryOptionsFromSql(sql, QueryType.Logs, mockDs)).toEqual(expectedOptions);\n  });\n\n  it('matches column hints with datasource log column names', () => {\n    const mockDs = {} as Datasource;\n    mockDs.getDefaultLogsColumns = jest.fn(\n      () =>\n        new Map([\n          [ColumnHint.Time, 'SpecialTimestamp'],\n          [ColumnHint.LogMessage, 'LogBody'],\n        ])\n    );\n\n    const sql = 'SELECT \"SpecialTimestamp\", \"LogBody\" FROM \"db\".\"foo\"';\n    const expectedOptions: QueryBuilderOptions = {\n      queryType: QueryType.Logs,\n      mode: BuilderMode.List,\n      database: 'db',\n      table: 'foo',\n      columns: [\n        { name: 'SpecialTimestamp', alias: undefined, hint: ColumnHint.Time },\n        { name: 'LogBody', alias: undefined, hint: ColumnHint.LogMessage },\n      ],\n      aggregates: [],\n      limit: undefined,\n    };\n\n    expect(getQueryOptionsFromSql(sql, QueryType.Logs, mockDs)).toEqual(expectedOptions);\n  });\n\n  it('matches column hints with OTel trace column names', () => {\n    const mockDs = {} as Datasource;\n    mockDs.getDefaultTraceColumns = jest.fn(() => otel.getLatestVersion().traceColumnMap);\n\n    const sql = 'SELECT \"StartTime\", \"ServiceName\" FROM \"db\".\"foo\"';\n    const expectedOptions: QueryBuilderOptions = {\n      queryType: QueryType.Traces,\n      mode: BuilderMode.List,\n      database: 'db',\n      table: 'foo',\n      columns: [\n        { name: 'StartTime', alias: undefined, hint: ColumnHint.Time },\n        { name: 'ServiceName', alias: undefined, hint: ColumnHint.TraceServiceName },\n      ],\n      aggregates: [],\n      limit: undefined,\n    };\n\n    expect(getQueryOptionsFromSql(sql, QueryType.Traces, mockDs)).toEqual(expectedOptions);\n  });\n\n  it('matches column hints with datasource trace column names', () => {\n    const mockDs = {} as Datasource;\n    mockDs.getDefaultTraceColumns = jest.fn(\n      () =>\n        new Map([\n          [ColumnHint.Time, 'SpecialTimestamp'],\n          [ColumnHint.TraceId, 'CustomTraceID'],\n        ])\n    );\n\n    const sql = 'SELECT \"SpecialTimestamp\", \"CustomTraceID\" FROM \"db\".\"foo\"';\n    const expectedOptions: QueryBuilderOptions = {\n      queryType: QueryType.Traces,\n      mode: BuilderMode.List,\n      database: 'db',\n      table: 'foo',\n      columns: [\n        { name: 'SpecialTimestamp', alias: undefined, hint: ColumnHint.Time },\n        { name: 'CustomTraceID', alias: undefined, hint: ColumnHint.TraceId },\n      ],\n      aggregates: [],\n      limit: undefined,\n    };\n\n    expect(getQueryOptionsFromSql(sql, QueryType.Traces, mockDs)).toEqual(expectedOptions);\n  });\n\n  it('Handles brackets and Grafana macros/variables', () => {\n    const sql = `\n      /* \\${__variable} \\${__variable.key} */\n      SELECT\n        *,\n        \\$__timeInterval(timestamp),\n        '{\"a\": 1, \"b\": { \"c\": 2, \"d\": [1, 2, 3] }}'::json as bracketTest\n      FROM default.table\n      WHERE $__timeFilter(timestamp)\n      AND col != \\${variable}\n      AND col != \\${variable.key}\n      AND col != \\${variable.key:singlequote}\n      AND col != '\\${variable}'\n      AND col != '\\${__variable}'\n      AND col != ('\\${__variable.key}')\n      AND col != \\${variable:singlequote}\n    `;\n\n    const builderOptions = getQueryOptionsFromSql(sql);\n    expect(builderOptions).not.toBeUndefined();\n    expect(typeof builderOptions).not.toBe('string');\n  });\n});\n\nfunction testCondition(name: string, sql: string, builder: QueryBuilderOptions, testQueryOptionsFromSql = true) {\n  it(name, () => {\n    expect(generateSql(builder)).toBe(sql);\n    if (testQueryOptionsFromSql) {\n      expect(getQueryOptionsFromSql(sql)).toEqual(builder);\n    }\n  });\n}\n"
  },
  {
    "path": "src/components/queryBuilder/utils.ts",
    "content": "import {\n  astVisitor,\n  Expr,\n  ExprBinary,\n  ExprCall,\n  ExprInteger,\n  ExprList,\n  ExprRef,\n  ExprString,\n  ExprUnary,\n  FromTable,\n  SelectedColumn,\n} from 'pgsql-ast-parser';\nimport { isString } from 'lodash';\nimport {\n  BooleanFilter,\n  AggregateColumn,\n  AggregateType,\n  BuilderMode,\n  DateFilter,\n  DateFilterWithoutValue,\n  Filter,\n  FilterOperator,\n  MultiFilter,\n  NullFilter,\n  NumberFilter,\n  OrderBy,\n  QueryBuilderOptions,\n  ColumnHint,\n  SelectedColumn as CHSelectedColumn,\n  StringFilter,\n  QueryType,\n} from 'types/queryBuilder';\nimport { sqlToStatement } from 'data/ast';\nimport { getColumnByHint, logColumnHintsToAlias } from 'data/sqlGenerator';\nimport { Datasource } from 'data/CHDatasource';\nimport { tryApplyColumnHints } from 'data/utils';\n\nexport const isBooleanType = (type: string): boolean => {\n  return ['boolean'].includes(type?.toLowerCase());\n};\n\nexport const isNumberType = (type: string): boolean => {\n  return ['int', 'float', 'decimal'].some((t) => type?.toLowerCase().includes(t));\n};\n\nexport const isDateType = (type: string): boolean => {\n  const normalizedName = type?.toLowerCase();\n  return normalizedName?.startsWith('date') || normalizedName?.startsWith('nullable(date');\n};\nexport const isDateTimeType = (type: string): boolean => {\n  const normalizedName = type?.toLowerCase();\n  return normalizedName?.startsWith('datetime') || normalizedName?.startsWith('nullable(datetime');\n};\nexport const isStringType = (type: string): boolean => {\n  return !(isBooleanType(type) || isNumberType(type) || isDateType(type));\n};\nexport const isNullFilter = (filter: Filter): filter is NullFilter => {\n  return [FilterOperator.IsNull, FilterOperator.IsNotNull].includes(filter.operator);\n};\nexport const isBooleanFilter = (filter: Filter): filter is BooleanFilter => {\n  return isBooleanType(filter.type);\n};\nexport const isNumberFilter = (filter: Filter): filter is NumberFilter => {\n  return isNumberType(filter.type);\n};\nexport const isDateFilterWithOutValue = (filter: Filter): filter is DateFilterWithoutValue => {\n  return (\n    isDateType(filter.type) &&\n    [FilterOperator.WithInGrafanaTimeRange, FilterOperator.OutsideGrafanaTimeRange].includes(filter.operator)\n  );\n};\nexport const isDateFilter = (filter: Filter): filter is DateFilter => {\n  return isDateType(filter.type);\n};\nexport const isStringFilter = (filter: Filter): filter is StringFilter => {\n  return isStringType(filter.type) && ![FilterOperator.In, FilterOperator.NotIn].includes(filter.operator);\n};\nexport const isMultiFilter = (filter: Filter): filter is MultiFilter => {\n  return isStringType(filter.type) && [FilterOperator.In, FilterOperator.NotIn].includes(filter.operator);\n};\n\nexport function getQueryOptionsFromSql(\n  sql: string,\n  queryType?: QueryType,\n  datasource?: Datasource\n): QueryBuilderOptions {\n  const ast = sqlToStatement(sql);\n  if (!ast) {\n    throw new Error('The query is not valid SQL.');\n  }\n  if (ast.type !== 'select') {\n    throw new Error('The query is not a select statement.');\n  }\n  if (!ast.from || ast.from.length !== 1) {\n    throw new Error(`The query has too many 'FROM' clauses.`);\n  }\n  if (ast.from[0].type !== 'table') {\n    throw new Error(`The 'FROM' clause is not a table.`);\n  }\n  const fromTable = ast.from[0] as FromTable;\n\n  const columnsAndAggregates = getAggregatesFromAst(ast.columns || null);\n\n  const builderOptions = {\n    database: fromTable.name.schema || '',\n    table: fromTable.name.name || '',\n    queryType: queryType || QueryType.Table,\n    mode: BuilderMode.List,\n    columns: [],\n    aggregates: [],\n  } as QueryBuilderOptions;\n\n  if (columnsAndAggregates.columns.length > 0) {\n    builderOptions.columns = columnsAndAggregates.columns || [];\n  }\n\n  // Reconstruct column hints based off of known column names / aliases\n  if (queryType === QueryType.Logs) {\n    tryApplyColumnHints(builderOptions.columns!, datasource?.getDefaultLogsColumns()); // Try match default log columns\n    tryApplyColumnHints(builderOptions.columns!, logColumnHintsToAlias); // Try match Grafana aliases\n  } else if (queryType === QueryType.Traces) {\n    tryApplyColumnHints(builderOptions.columns!, datasource?.getDefaultTraceColumns());\n  }\n\n  if (columnsAndAggregates.aggregates.length > 0) {\n    builderOptions.mode = BuilderMode.Aggregate;\n    builderOptions.aggregates = columnsAndAggregates.aggregates;\n  }\n\n  const timeColumn = getColumnByHint(builderOptions, ColumnHint.Time);\n  if (!queryType && timeColumn) {\n    builderOptions.queryType = QueryType.TimeSeries;\n    if (builderOptions.aggregates?.length || 0) {\n      builderOptions.mode = BuilderMode.Trend;\n    }\n  }\n\n  if (ast.where) {\n    builderOptions.filters = getFiltersFromAst(ast.where, timeColumn?.name || '');\n  }\n\n  const orderBy = ast.orderBy\n    ?.map<OrderBy>((ob) => {\n      if (ob.by.type !== 'ref') {\n        return {} as OrderBy;\n      }\n      return { name: ob.by.name, dir: ob.order } as OrderBy;\n    })\n    .filter((x) => x.name);\n\n  if (orderBy && orderBy.length > 0) {\n    builderOptions.orderBy = orderBy!;\n  }\n\n  builderOptions.limit = ast.limit?.limit?.type === 'integer' ? ast.limit?.limit.value : undefined;\n\n  const groupBy = ast.groupBy\n    ?.map((gb) => {\n      if (gb.type !== 'ref') {\n        return '';\n      }\n      return gb.name;\n    })\n    .filter((x) => x !== '');\n  if (groupBy && groupBy.length > 0) {\n    builderOptions.groupBy = groupBy;\n  }\n\n  return builderOptions;\n}\n\nfunction getFiltersFromAst(expr: Expr, timeField: string): Filter[] {\n  const filters: Filter[] = [];\n  let i = 0;\n  let notFlag = false;\n  const visitor = astVisitor((map) => ({\n    expr: (e) => {\n      switch (e?.type) {\n        case 'binary':\n          notFlag = getBinaryFilter(e, filters, i, notFlag);\n          map.super().expr(e);\n          break;\n        case 'ref':\n          ({ i, notFlag } = getRefFilter(e, filters, i, notFlag));\n          break;\n        case 'string':\n          i = getStringFilter(filters, i, e);\n          break;\n        case 'integer':\n          i = getIntFilter(filters, i, e);\n          break;\n        case 'unary':\n          notFlag = getUnaryFilter(e, notFlag, i, filters);\n          map.super().expr(e);\n          break;\n        case 'call':\n          i = getCallFilter(e, timeField, filters, i);\n          break;\n        case 'list':\n          i = getListFilter(filters, i, e);\n          break;\n        default:\n          console.error(`${e?.type} is not supported. This is likely a bug.`);\n          break;\n      }\n    },\n  }));\n  visitor.expr(expr);\n  return filters;\n}\n\nfunction getRefFilter(e: ExprRef, filters: Filter[], i: number, notFlag: boolean): { i: number; notFlag: boolean } {\n  if (e.name?.toLowerCase() === '$__fromtime' && filters[i].operator === FilterOperator.GreaterThanOrEqual) {\n    if (notFlag) {\n      filters[i].operator = FilterOperator.OutsideGrafanaTimeRange;\n      notFlag = false;\n    } else {\n      filters[i].operator = FilterOperator.WithInGrafanaTimeRange;\n    }\n    filters[i].type = 'datetime';\n    i++;\n    return { i, notFlag };\n  }\n  if (e.name?.toLowerCase() === '$__totime') {\n    filters.splice(i, 1);\n    return { i, notFlag };\n  }\n  if (!filters[i].key) {\n    filters[i].key = e.name;\n    if (filters[i].operator === FilterOperator.IsNotNull) {\n      i++;\n    }\n    return { i, notFlag };\n  }\n  filters[i] = { ...filters[i], value: [e.name], type: 'string' } as Filter;\n  i++;\n  return { i, notFlag };\n}\n\nfunction getListFilter(filters: Filter[], i: number, e: ExprList): number {\n  filters[i] = {\n    ...filters[i],\n    value: e.expressions.map((x) => {\n      const k = x as ExprString;\n      return k.value;\n    }),\n    type: 'string',\n  } as Filter;\n  i++;\n  return i;\n}\n\nfunction getCallFilter(e: ExprCall, timeField: string, filters: Filter[], i: number): number {\n  const val = `${e.function.name}(${e.args.map<string>((x) => (x as ExprRef).name).join(',')})`;\n  //do not add the timeFilter that is used when using time series and remove the condition\n  if (val === `$__timefilter(${timeField})`) {\n    filters.splice(i, 1);\n    return i;\n  }\n  if (val.startsWith('$__timefilter(')) {\n    filters[i] = {\n      ...filters[i],\n      key: (e.args[0] as ExprRef).name,\n      operator: FilterOperator.WithInGrafanaTimeRange,\n      type: 'datetime',\n    } as Filter;\n    i++;\n    return i;\n  }\n  filters[i] = { ...filters[i], value: val, type: 'datetime' } as Filter;\n  if (!val) {\n    i++;\n  }\n  return i;\n}\n\nfunction getUnaryFilter(e: ExprUnary, notFlag: boolean, i: number, filters: Filter[]): boolean {\n  if (e.op === 'NOT') {\n    return true;\n  }\n  if (i === 0) {\n    filters.unshift({} as Filter);\n  }\n  filters[i].operator = e.op as FilterOperator;\n  return notFlag;\n}\n\nfunction getStringFilter(filters: Filter[], i: number, e: ExprString): number {\n  if (!filters[i].key) {\n    filters[i] = { ...filters[i], key: e.value } as Filter;\n    return i;\n  }\n  filters[i] = { ...filters[i], value: e.value, type: 'string' } as Filter;\n  i++;\n  return i;\n}\n\nfunction getIntFilter(filters: Filter[], i: number, e: ExprInteger): number {\n  if (!filters[i].key) {\n    filters[i] = { ...filters[i], key: e.value.toString() } as Filter;\n    return i;\n  }\n  filters[i] = { ...filters[i], value: e.value, type: 'int' } as Filter;\n  i++;\n  return i;\n}\n\nfunction getBinaryFilter(e: ExprBinary, filters: Filter[], i: number, notFlag: boolean): boolean {\n  if (e.op === 'AND' || e.op === 'OR') {\n    filters.unshift({\n      condition: e.op,\n    } as Filter);\n  } else if (Object.values(FilterOperator).find((x) => e.op === x)) {\n    if (i === 0) {\n      filters.unshift({} as Filter);\n    } else if (!filters[i]) {\n      filters.push({ condition: 'AND' } as Filter);\n    }\n\n    filters[i].operator = e.op as FilterOperator;\n    if (notFlag && filters[i].operator === FilterOperator.Like) {\n      filters[i].operator = FilterOperator.NotLike;\n      notFlag = false;\n    }\n  }\n  return notFlag;\n}\n\nfunction selectCallFunc(s: SelectedColumn): [AggregateColumn | string, string | undefined] {\n  if (s.expr.type !== 'call') {\n    return [{} as AggregateColumn, undefined];\n  }\n  let fields = s.expr.args.map((x) => {\n    if (x.type !== 'ref') {\n      return '';\n    }\n    return x.name;\n  });\n  if (fields.length > 1) {\n    return ['', undefined];\n  }\n  if (Object.values(AggregateType).includes(s.expr.function.name.toLowerCase() as AggregateType)) {\n    return [\n      {\n        aggregateType: s.expr.function.name as AggregateType,\n        column: fields[0],\n        alias: s.alias?.name,\n      } as AggregateColumn,\n      s.alias?.name,\n    ];\n  }\n  return [fields[0], s.alias?.name];\n}\n\nfunction getAggregatesFromAst(selectClauses: SelectedColumn[] | null): {\n  columns: CHSelectedColumn[];\n  aggregates: AggregateColumn[];\n} {\n  if (!selectClauses) {\n    return { columns: [], aggregates: [] };\n  }\n\n  const columns: CHSelectedColumn[] = [];\n  const aggregates: AggregateColumn[] = [];\n\n  for (let s of selectClauses) {\n    switch (s.expr.type) {\n      case 'ref':\n        columns.push({ name: s.expr.name, alias: s.alias?.name });\n        break;\n      case 'call':\n        const [columnOrAggregate, alias] = selectCallFunc(s);\n        if (!columnOrAggregate) {\n          break;\n        }\n\n        if (isString(columnOrAggregate)) {\n          columns.push({ name: columnOrAggregate, type: 'datetime', alias, hint: ColumnHint.Time });\n        } else {\n          aggregates.push(columnOrAggregate);\n        }\n        break;\n    }\n  }\n\n  return { columns, aggregates };\n}\n\nexport const operMap = new Map<string, FilterOperator>([\n  ['equals', FilterOperator.Equals],\n  ['contains', FilterOperator.Like],\n]);\n\nexport function getOper(v: string): FilterOperator {\n  return operMap.get(v) || FilterOperator.Equals;\n}\n"
  },
  {
    "path": "src/components/queryBuilder/views/LogsQueryBuilder.tsx",
    "content": "import React, { useEffect, useMemo, useState } from 'react';\nimport { ColumnsEditor } from '../ColumnsEditor';\nimport { Filter, OrderBy, QueryBuilderOptions, SelectedColumn, ColumnHint } from 'types/queryBuilder';\nimport { ColumnRolesHelp } from '../ColumnRolesHelp';\nimport { ColumnSelect } from '../ColumnSelect';\nimport { OtelVersionSelect } from '../OtelVersionSelect';\nimport { OrderByEditor, getOrderByOptions } from '../OrderByEditor';\nimport { LimitEditor } from '../LimitEditor';\nimport { FiltersEditor } from '../FilterEditor';\nimport allLabels from 'labels';\nimport { getColumnByHint } from 'data/sqlGenerator';\nimport { columnFilterDateTime, columnFilterString } from 'data/columnFilters';\nimport { Datasource } from 'data/CHDatasource';\nimport { useBuilderOptionChanges } from 'hooks/useBuilderOptionChanges';\nimport { Alert, Button, InlineFormLabel, Input, VerticalGroup } from '@grafana/ui';\nimport useColumns from 'hooks/useColumns';\nimport { BuilderOptionsReducerAction, setOptions, setOtelEnabled, setOtelVersion } from 'hooks/useBuilderOptionsState';\nimport useIsNewQuery from 'hooks/useIsNewQuery';\nimport {\n  useDefaultFilters,\n  useDefaultTimeColumn,\n  useLogDefaultsOnMount,\n  useOtelColumns,\n} from './logsQueryBuilderHooks';\nimport { styles } from 'styles';\nimport { Components as allSelectors } from 'selectors';\n\ninterface LogsQueryBuilderProps {\n  datasource: Datasource;\n  builderOptions: QueryBuilderOptions;\n  builderOptionsDispatch: React.Dispatch<BuilderOptionsReducerAction>;\n}\n\ninterface LogsQueryBuilderState {\n  otelEnabled: boolean;\n  otelVersion: string;\n  selectedColumns: SelectedColumn[];\n  timeColumn?: SelectedColumn;\n  logLevelColumn?: SelectedColumn;\n  messageColumn?: SelectedColumn;\n  orderBy: OrderBy[];\n  limit: number;\n  filters: Filter[];\n  logMessageLike: string;\n}\n\nexport const LogsQueryBuilder = (props: LogsQueryBuilderProps) => {\n  const { datasource, builderOptions, builderOptionsDispatch } = props;\n  const labels = allLabels.components.LogsQueryBuilder;\n  const allColumns = useColumns(datasource, builderOptions.database, builderOptions.table);\n  const isNewQuery = useIsNewQuery(builderOptions);\n  const builderState: LogsQueryBuilderState = useMemo(\n    () => ({\n      otelEnabled: builderOptions.meta?.otelEnabled || false,\n      otelVersion: builderOptions.meta?.otelVersion || '',\n      timeColumn: getColumnByHint(builderOptions, ColumnHint.Time),\n      logLevelColumn: getColumnByHint(builderOptions, ColumnHint.LogLevel),\n      messageColumn: getColumnByHint(builderOptions, ColumnHint.LogMessage),\n      selectedColumns:\n        builderOptions.columns?.filter(\n          (c) =>\n            // Only select columns that don't have their own box\n            c.hint !== ColumnHint.Time &&\n            c.hint !== ColumnHint.LogLevel &&\n            c.hint !== ColumnHint.LogMessage\n        ) || [],\n      // liveView: builderOptions.meta?.liveView || false,\n      filters: builderOptions.filters || [],\n      orderBy: builderOptions.orderBy || [],\n      limit: builderOptions.limit || 0,\n      logMessageLike: builderOptions.meta?.logMessageLike || '',\n    }),\n    [builderOptions]\n  );\n  const [showConfigWarning, setConfigWarningOpen] = useState(\n    datasource.getDefaultLogsColumns().size === 0 && builderOptions.columns?.length === 0\n  );\n\n  const onOptionChange = useBuilderOptionChanges<LogsQueryBuilderState>((next) => {\n    const nextColumns = next.selectedColumns.slice();\n    if (next.timeColumn) {\n      nextColumns.push(next.timeColumn);\n    }\n    if (next.logLevelColumn) {\n      nextColumns.push(next.logLevelColumn);\n    }\n    if (next.messageColumn) {\n      nextColumns.push(next.messageColumn);\n    }\n\n    builderOptionsDispatch(\n      setOptions({\n        columns: nextColumns,\n        filters: next.filters,\n        orderBy: next.orderBy,\n        limit: next.limit,\n        meta: {\n          logMessageLike: next.logMessageLike,\n        },\n      })\n    );\n  }, builderState);\n\n  useLogDefaultsOnMount(datasource, isNewQuery, builderOptions, builderOptionsDispatch);\n  useOtelColumns(datasource, allColumns, builderState.otelEnabled, builderState.otelVersion, builderOptionsDispatch);\n  useDefaultTimeColumn(\n    datasource,\n    allColumns,\n    builderOptions.table,\n    builderState.timeColumn,\n    builderState.otelEnabled,\n    builderOptionsDispatch\n  );\n  useDefaultFilters(builderOptions.table, isNewQuery, builderOptionsDispatch);\n\n  const configWarning = showConfigWarning && (\n    <Alert title=\"\" severity=\"warning\" buttonContent=\"Close\" onRemove={() => setConfigWarningOpen(false)}>\n      <VerticalGroup>\n        <div>\n          {'To speed up your query building, enter your default logs configuration in your '}\n          <a\n            style={{ textDecoration: 'underline' }}\n            href={`/connections/datasources/edit/${encodeURIComponent(datasource.uid)}#logs-config`}\n          >\n            ClickHouse Data Source settings\n          </a>\n        </div>\n      </VerticalGroup>\n    </Alert>\n  );\n\n  return (\n    <div>\n      {configWarning}\n      <OtelVersionSelect\n        enabled={builderState.otelEnabled}\n        onEnabledChange={(e) => builderOptionsDispatch(setOtelEnabled(e))}\n        selectedVersion={builderState.otelVersion}\n        onVersionChange={(v) => builderOptionsDispatch(setOtelVersion(v))}\n      />\n      <ColumnRolesHelp\n        text={labels.columnsHelp.text}\n        linkText={labels.columnsHelp.linkText}\n        href={labels.columnsHelp.href}\n        testIdWrapper={allSelectors.QueryBuilder.LogsQueryBuilder.columnRolesHelp}\n        testIdLink={allSelectors.QueryBuilder.LogsQueryBuilder.columnRolesHelpLink}\n      />\n      <ColumnsEditor\n        disabled={builderState.otelEnabled}\n        allColumns={allColumns}\n        selectedColumns={builderState.selectedColumns}\n        onSelectedColumnsChange={onOptionChange('selectedColumns')}\n      />\n      <div className=\"gf-form\">\n        <ColumnSelect\n          disabled={builderState.otelEnabled}\n          allColumns={allColumns}\n          selectedColumn={builderState.timeColumn}\n          invalid={!builderState.timeColumn}\n          onColumnChange={onOptionChange('timeColumn')}\n          columnFilterFn={columnFilterDateTime}\n          columnHint={ColumnHint.Time}\n          label={labels.logTimeColumn.label}\n          tooltip={labels.logTimeColumn.tooltip}\n        />\n        <ColumnSelect\n          disabled={builderState.otelEnabled}\n          allColumns={allColumns}\n          selectedColumn={builderState.logLevelColumn}\n          invalid={!builderState.logLevelColumn}\n          onColumnChange={onOptionChange('logLevelColumn')}\n          columnFilterFn={columnFilterString}\n          columnHint={ColumnHint.LogLevel}\n          label={labels.logLevelColumn.label}\n          tooltip={labels.logLevelColumn.tooltip}\n          inline\n        />\n      </div>\n      <div className=\"gf-form\">\n        <ColumnSelect\n          disabled={builderState.otelEnabled}\n          allColumns={allColumns}\n          selectedColumn={builderState.messageColumn}\n          invalid={!builderState.messageColumn}\n          onColumnChange={onOptionChange('messageColumn')}\n          columnFilterFn={columnFilterString}\n          columnHint={ColumnHint.LogMessage}\n          label={labels.logMessageColumn.label}\n          tooltip={labels.logMessageColumn.tooltip}\n        />\n      </div>\n      <OrderByEditor\n        orderByOptions={getOrderByOptions(builderOptions, allColumns)}\n        orderBy={builderState.orderBy}\n        onOrderByChange={onOptionChange('orderBy')}\n      />\n      <LimitEditor limit={builderState.limit} onLimitChange={onOptionChange('limit')} />\n      <FiltersEditor\n        filters={builderState.filters}\n        onFiltersChange={onOptionChange('filters')}\n        allColumns={allColumns}\n        datasource={datasource}\n        database={builderOptions.database}\n        table={builderOptions.table}\n      />\n      <LogMessageLikeInput logMessageLike={builderState.logMessageLike} onChange={onOptionChange('logMessageLike')} />\n    </div>\n  );\n};\n\ninterface LogMessageLikeInputProps {\n  logMessageLike: string;\n  onChange: (logMessageLike: string) => void;\n}\n\nconst LogMessageLikeInput = (props: LogMessageLikeInputProps) => {\n  const [input, setInput] = useState<string>('');\n  const { logMessageLike, onChange } = props;\n  const { label, tooltip, clearButton } = allLabels.components.LogsQueryBuilder.logMessageFilter;\n\n  useEffect(() => {\n    setInput(logMessageLike);\n  }, [logMessageLike]);\n\n  return (\n    <div className=\"gf-form\">\n      <InlineFormLabel width={8} className=\"query-keyword\" tooltip={tooltip}>\n        {label}\n      </InlineFormLabel>\n      <Input\n        width={200}\n        value={input}\n        type=\"string\"\n        onChange={(e) => setInput(e.currentTarget.value)}\n        onBlur={() => onChange(input)}\n      />\n      {logMessageLike && (\n        <Button\n          data-testid={allSelectors.QueryBuilder.LogsQueryBuilder.LogMessageLikeInput.input}\n          variant=\"secondary\"\n          size=\"md\"\n          onClick={() => onChange('')}\n          className={styles.Common.smallBtn}\n          tooltip={allLabels.components.expandBuilderButton.tooltip}\n        >\n          {clearButton}\n        </Button>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/queryBuilder/views/TableQueryBuilder.tsx",
    "content": "import React, { useMemo } from 'react';\nimport { ColumnsEditor } from '../ColumnsEditor';\nimport { AggregateColumn, BuilderMode, Filter, OrderBy, QueryBuilderOptions, SelectedColumn } from 'types/queryBuilder';\nimport { OrderByEditor, getOrderByOptions } from '../OrderByEditor';\nimport { LimitEditor } from '../LimitEditor';\nimport { FiltersEditor } from '../FilterEditor';\nimport allLabels from 'labels';\nimport { ModeSwitch } from '../ModeSwitch';\nimport { AggregateEditor } from '../AggregateEditor';\nimport { GroupByEditor } from '../GroupByEditor';\nimport { Datasource } from 'data/CHDatasource';\nimport { useBuilderOptionChanges } from 'hooks/useBuilderOptionChanges';\nimport useColumns from 'hooks/useColumns';\nimport { BuilderOptionsReducerAction, setOptions } from 'hooks/useBuilderOptionsState';\n\ninterface TableQueryBuilderProps {\n  datasource: Datasource;\n  builderOptions: QueryBuilderOptions;\n  builderOptionsDispatch: React.Dispatch<BuilderOptionsReducerAction>;\n}\n\ninterface TableQueryBuilderState {\n  isAggregateMode: boolean;\n  selectedColumns: SelectedColumn[];\n  aggregates: AggregateColumn[];\n  groupBy: string[];\n  orderBy: OrderBy[];\n  limit: number;\n  filters: Filter[];\n}\n\nexport const TableQueryBuilder = (props: TableQueryBuilderProps) => {\n  const { datasource, builderOptions, builderOptionsDispatch } = props;\n  const allColumns = useColumns(datasource, builderOptions.database, builderOptions.table);\n  const labels = allLabels.components.TableQueryBuilder;\n  const builderState: TableQueryBuilderState = useMemo(\n    () => ({\n      isAggregateMode: builderOptions.mode === BuilderMode.Aggregate,\n      selectedColumns: builderOptions.columns || [],\n      aggregates: builderOptions.aggregates || [],\n      groupBy: builderOptions.groupBy || [],\n      orderBy: builderOptions.orderBy || [],\n      limit: builderOptions.limit || 0,\n      filters: builderOptions.filters || [],\n    }),\n    [builderOptions]\n  );\n\n  const onOptionChange = useBuilderOptionChanges<TableQueryBuilderState>((next) => {\n    builderOptionsDispatch(\n      setOptions({\n        mode: next.isAggregateMode ? BuilderMode.Aggregate : BuilderMode.List,\n        columns: next.selectedColumns,\n        aggregates: next.aggregates,\n        groupBy: next.groupBy,\n        filters: next.filters,\n        orderBy: next.orderBy,\n        limit: next.limit,\n      })\n    );\n  }, builderState);\n\n  return (\n    <div>\n      <ModeSwitch\n        labelA={labels.simpleQueryModeLabel}\n        labelB={labels.aggregateQueryModeLabel}\n        value={builderState.isAggregateMode}\n        onChange={onOptionChange('isAggregateMode')}\n        label={labels.builderModeLabel}\n        tooltip={labels.builderModeTooltip}\n      />\n\n      <ColumnsEditor\n        allColumns={allColumns}\n        selectedColumns={builderState.selectedColumns}\n        onSelectedColumnsChange={onOptionChange('selectedColumns')}\n        showAllOption\n      />\n\n      {builderState.isAggregateMode && (\n        <>\n          <AggregateEditor\n            allColumns={allColumns}\n            aggregates={builderState.aggregates}\n            onAggregatesChange={onOptionChange('aggregates')}\n          />\n          <GroupByEditor\n            groupBy={builderState.groupBy}\n            onGroupByChange={onOptionChange('groupBy')}\n            allColumns={allColumns}\n          />\n        </>\n      )}\n\n      <OrderByEditor\n        orderByOptions={getOrderByOptions(builderOptions, allColumns)}\n        orderBy={builderState.orderBy}\n        onOrderByChange={onOptionChange('orderBy')}\n      />\n      <LimitEditor limit={builderState.limit} onLimitChange={onOptionChange('limit')} />\n      <FiltersEditor\n        filters={builderState.filters}\n        onFiltersChange={onOptionChange('filters')}\n        allColumns={allColumns}\n        datasource={datasource}\n        database={builderOptions.database}\n        table={builderOptions.table}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/queryBuilder/views/TimeSeriesQueryBuilder.tsx",
    "content": "import React, { useMemo } from 'react';\nimport { ColumnsEditor } from '../ColumnsEditor';\nimport {\n  AggregateColumn,\n  BuilderMode,\n  Filter,\n  OrderBy,\n  QueryBuilderOptions,\n  ColumnHint,\n  SelectedColumn,\n} from 'types/queryBuilder';\nimport { OrderByEditor, getOrderByOptions } from '../OrderByEditor';\nimport { LimitEditor } from '../LimitEditor';\nimport { FiltersEditor } from '../FilterEditor';\nimport allLabels from 'labels';\nimport { ModeSwitch } from '../ModeSwitch';\nimport { AggregateEditor } from '../AggregateEditor';\nimport { GroupByEditor } from '../GroupByEditor';\nimport { ColumnRolesHelp } from '../ColumnRolesHelp';\nimport { ColumnSelect } from '../ColumnSelect';\nimport { Components as allSelectors } from 'selectors';\nimport { getColumnByHint } from 'data/sqlGenerator';\nimport { columnFilterDateTime } from 'data/columnFilters';\nimport { Datasource } from 'data/CHDatasource';\nimport { useBuilderOptionChanges } from 'hooks/useBuilderOptionChanges';\nimport useColumns from 'hooks/useColumns';\nimport { BuilderOptionsReducerAction, setOptions } from 'hooks/useBuilderOptionsState';\nimport { useDefaultFilters, useDefaultTimeColumn } from './timeSeriesQueryBuilderHooks';\nimport useIsNewQuery from 'hooks/useIsNewQuery';\n\ninterface TimeSeriesQueryBuilderProps {\n  datasource: Datasource;\n  builderOptions: QueryBuilderOptions;\n  builderOptionsDispatch: React.Dispatch<BuilderOptionsReducerAction>;\n}\n\ninterface TimeSeriesQueryBuilderState {\n  isAggregateMode: boolean;\n  timeColumn?: SelectedColumn;\n  selectedColumns: SelectedColumn[];\n  aggregates: AggregateColumn[];\n  groupBy: string[];\n  orderBy: OrderBy[];\n  limit: number;\n  filters: Filter[];\n}\n\nexport const TimeSeriesQueryBuilder = (props: TimeSeriesQueryBuilderProps) => {\n  const { datasource, builderOptions, builderOptionsDispatch } = props;\n  const isNewQuery = useIsNewQuery(builderOptions);\n  const allColumns = useColumns(datasource, builderOptions.database, builderOptions.table);\n  const labels = allLabels.components.TimeSeriesQueryBuilder;\n  const builderState: TimeSeriesQueryBuilderState = useMemo(\n    () => ({\n      // TODO: do not depend on \"mode\"\n      isAggregateMode: builderOptions.mode === BuilderMode.Trend,\n      timeColumn: getColumnByHint(builderOptions, ColumnHint.Time),\n      selectedColumns: (builderOptions.columns || []).filter((c) => c.hint !== ColumnHint.Time),\n      aggregates: builderOptions.aggregates || [],\n      groupBy: builderOptions.groupBy || [],\n      orderBy: builderOptions.orderBy || [],\n      limit: builderOptions.limit || 0,\n      filters: builderOptions.filters || [],\n    }),\n    [builderOptions]\n  );\n\n  const onOptionChange = useBuilderOptionChanges<TimeSeriesQueryBuilderState>((next) => {\n    let nextColumns = next.selectedColumns.slice();\n    if (next.isAggregateMode) {\n      nextColumns = [];\n    }\n\n    if (next.timeColumn) {\n      nextColumns.push(next.timeColumn);\n    }\n\n    builderOptionsDispatch(\n      setOptions({\n        mode: next.isAggregateMode ? BuilderMode.Trend : BuilderMode.Aggregate,\n        columns: nextColumns,\n        aggregates: next.isAggregateMode ? next.aggregates : [],\n        groupBy: next.isAggregateMode ? next.groupBy : [],\n        filters: next.filters,\n        orderBy: next.orderBy,\n        limit: next.limit,\n      })\n    );\n  }, builderState);\n\n  useDefaultTimeColumn(allColumns, builderOptions.table, builderState.timeColumn, builderOptionsDispatch);\n  useDefaultFilters(builderOptions.table, isNewQuery, builderOptionsDispatch);\n\n  return (\n    <div>\n      <ModeSwitch\n        labelA={labels.simpleQueryModeLabel}\n        labelB={labels.aggregateQueryModeLabel}\n        value={builderState.isAggregateMode}\n        onChange={onOptionChange('isAggregateMode')}\n        label={labels.builderModeLabel}\n        tooltip={labels.builderModeTooltip}\n      />\n\n      <ColumnRolesHelp\n        text={labels.columnsHelp.text}\n        linkText={labels.columnsHelp.linkText}\n        href={labels.columnsHelp.href}\n        testIdWrapper={allSelectors.QueryBuilder.TimeSeriesQueryBuilder.columnRolesHelp}\n        testIdLink={allSelectors.QueryBuilder.TimeSeriesQueryBuilder.columnRolesHelpLink}\n      />\n\n      <ColumnSelect\n        allColumns={allColumns}\n        selectedColumn={builderState.timeColumn}\n        invalid={!builderState.timeColumn}\n        onColumnChange={onOptionChange('timeColumn')}\n        columnFilterFn={columnFilterDateTime}\n        columnHint={ColumnHint.Time}\n        label={labels.timeColumn.label}\n        tooltip={labels.timeColumn.tooltip}\n        clearable={false}\n      />\n\n      {builderState.isAggregateMode ? (\n        <>\n          <AggregateEditor\n            allColumns={allColumns}\n            aggregates={builderState.aggregates}\n            onAggregatesChange={onOptionChange('aggregates')}\n          />\n          <GroupByEditor\n            groupBy={builderState.groupBy}\n            onGroupByChange={onOptionChange('groupBy')}\n            allColumns={allColumns}\n          />\n        </>\n      ) : (\n        <ColumnsEditor\n          allColumns={allColumns}\n          selectedColumns={builderState.selectedColumns}\n          onSelectedColumnsChange={onOptionChange('selectedColumns')}\n        />\n      )}\n\n      <OrderByEditor\n        orderByOptions={getOrderByOptions(builderOptions, allColumns)}\n        orderBy={builderState.orderBy}\n        onOrderByChange={onOptionChange('orderBy')}\n      />\n      <LimitEditor limit={builderState.limit} onLimitChange={onOptionChange('limit')} />\n      <FiltersEditor\n        filters={builderState.filters}\n        onFiltersChange={onOptionChange('filters')}\n        allColumns={allColumns}\n        datasource={datasource}\n        database={builderOptions.database}\n        table={builderOptions.table}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/queryBuilder/views/TraceQueryBuilder.tsx",
    "content": "import React, { useMemo, useState } from 'react';\nimport { Filter, QueryBuilderOptions, SelectedColumn, ColumnHint, TimeUnit, OrderBy } from 'types/queryBuilder';\nimport { ColumnRolesHelp } from '../ColumnRolesHelp';\nimport { ColumnSelect } from '../ColumnSelect';\nimport { Components as allSelectors } from 'selectors';\nimport { FiltersEditor } from '../FilterEditor';\nimport allLabels from 'labels';\nimport { ModeSwitch } from '../ModeSwitch';\nimport { getColumnByHint } from 'data/sqlGenerator';\nimport { Alert, Collapse, Stack } from '@grafana/ui';\nimport { DurationUnitSelect } from 'components/queryBuilder/DurationUnitSelect';\nimport { Datasource } from 'data/CHDatasource';\nimport { useBuilderOptionChanges } from 'hooks/useBuilderOptionChanges';\nimport useColumns from 'hooks/useColumns';\nimport { BuilderOptionsReducerAction, setOptions, setOtelEnabled, setOtelVersion } from 'hooks/useBuilderOptionsState';\nimport useIsNewQuery from 'hooks/useIsNewQuery';\nimport { OtelVersionSelect } from '../OtelVersionSelect';\nimport { useDefaultFilters, useOtelColumns, useTraceDefaultsOnMount } from './traceQueryBuilderHooks';\nimport TraceIdInput from '../TraceIdInput';\nimport { OrderByEditor, getOrderByOptions } from '../OrderByEditor';\nimport { LimitEditor } from '../LimitEditor';\nimport { LabeledInput } from 'components/configEditor/LabeledInput';\nimport { Switch } from '../Switch';\n\ninterface TraceQueryBuilderProps {\n  datasource: Datasource;\n  builderOptions: QueryBuilderOptions;\n  builderOptionsDispatch: React.Dispatch<BuilderOptionsReducerAction>;\n}\n\ninterface TraceQueryBuilderState {\n  isTraceIdMode: boolean;\n  otelEnabled: boolean;\n  otelVersion: string;\n  traceIdColumn?: SelectedColumn;\n  spanIdColumn?: SelectedColumn;\n  parentSpanIdColumn?: SelectedColumn;\n  serviceNameColumn?: SelectedColumn;\n  operationNameColumn?: SelectedColumn;\n  startTimeColumn?: SelectedColumn;\n  durationTimeColumn?: SelectedColumn;\n  durationUnit: TimeUnit;\n  tagsColumn?: SelectedColumn;\n  serviceTagsColumn?: SelectedColumn;\n  kindColumn?: SelectedColumn;\n  statusCodeColumn?: SelectedColumn;\n  statusMessageColumn?: SelectedColumn;\n  stateColumn?: SelectedColumn;\n  instrumentationLibraryNameColumn?: SelectedColumn;\n  instrumentationLibraryVersionColumn?: SelectedColumn;\n  flattenNested?: boolean;\n  traceEventsColumnPrefix?: string;\n  traceLinksColumnPrefix?: string;\n  traceId: string;\n  orderBy: OrderBy[];\n  limit: number;\n  filters: Filter[];\n}\n\nexport const TraceQueryBuilder = (props: TraceQueryBuilderProps) => {\n  const { datasource, builderOptions, builderOptionsDispatch } = props;\n  const allColumns = useColumns(datasource, builderOptions.database, builderOptions.table);\n  const isNewQuery = useIsNewQuery(builderOptions);\n  const [showConfigWarning, setConfigWarningOpen] = useState(\n    datasource.getDefaultTraceColumns().size === 0 && builderOptions.columns?.length === 0\n  );\n  const [isColumnsOpen, setColumnsOpen] = useState<boolean>(showConfigWarning); // Toggle Columns collapse section\n  const [isFiltersOpen, setFiltersOpen] = useState<boolean>(\n    !(builderOptions.meta?.isTraceIdMode && builderOptions.meta.traceId)\n  ); // Toggle Filters collapse section\n  const labels = allLabels.components.TraceQueryBuilder;\n  const builderState = useMemo<TraceQueryBuilderState>(\n    () => ({\n      isTraceIdMode: builderOptions.meta?.isTraceIdMode || false,\n      otelEnabled: builderOptions.meta?.otelEnabled || false,\n      otelVersion: builderOptions.meta?.otelVersion || '',\n      traceIdColumn: getColumnByHint(builderOptions, ColumnHint.TraceId),\n      spanIdColumn: getColumnByHint(builderOptions, ColumnHint.TraceSpanId),\n      parentSpanIdColumn: getColumnByHint(builderOptions, ColumnHint.TraceParentSpanId),\n      serviceNameColumn: getColumnByHint(builderOptions, ColumnHint.TraceServiceName),\n      operationNameColumn: getColumnByHint(builderOptions, ColumnHint.TraceOperationName),\n      startTimeColumn: getColumnByHint(builderOptions, ColumnHint.Time),\n      durationTimeColumn: getColumnByHint(builderOptions, ColumnHint.TraceDurationTime),\n      durationUnit: builderOptions.meta?.traceDurationUnit || TimeUnit.Nanoseconds,\n      tagsColumn: getColumnByHint(builderOptions, ColumnHint.TraceTags),\n      serviceTagsColumn: getColumnByHint(builderOptions, ColumnHint.TraceServiceTags),\n      kindColumn: getColumnByHint(builderOptions, ColumnHint.TraceKind),\n      statusCodeColumn: getColumnByHint(builderOptions, ColumnHint.TraceStatusCode),\n      statusMessageColumn: getColumnByHint(builderOptions, ColumnHint.TraceStatusMessage),\n      stateColumn: getColumnByHint(builderOptions, ColumnHint.TraceState),\n      instrumentationLibraryNameColumn: getColumnByHint(builderOptions, ColumnHint.TraceInstrumentationLibraryName),\n      instrumentationLibraryVersionColumn: getColumnByHint(\n        builderOptions,\n        ColumnHint.TraceInstrumentationLibraryVersion\n      ),\n      flattenNested: Boolean(builderOptions.meta?.flattenNested),\n      traceEventsColumnPrefix: builderOptions.meta?.traceEventsColumnPrefix || '',\n      traceLinksColumnPrefix: builderOptions.meta?.traceLinksColumnPrefix || '',\n      traceId: builderOptions.meta?.traceId || '',\n      orderBy: builderOptions.orderBy || [],\n      limit: builderOptions.limit || 0,\n      filters: builderOptions.filters || [],\n    }),\n    [builderOptions]\n  );\n\n  const onOptionChange = useBuilderOptionChanges<TraceQueryBuilderState>((next) => {\n    const nextColumns = [\n      next.traceIdColumn,\n      next.spanIdColumn,\n      next.parentSpanIdColumn,\n      next.serviceNameColumn,\n      next.operationNameColumn,\n      next.startTimeColumn,\n      next.durationTimeColumn,\n      next.tagsColumn,\n      next.serviceTagsColumn,\n      next.serviceTagsColumn,\n      next.kindColumn,\n      next.statusCodeColumn,\n      next.statusMessageColumn,\n      next.stateColumn,\n      next.instrumentationLibraryNameColumn,\n      next.instrumentationLibraryVersionColumn,\n    ].filter((c) => c !== undefined) as SelectedColumn[];\n\n    builderOptionsDispatch(\n      setOptions({\n        columns: nextColumns,\n        orderBy: next.orderBy,\n        limit: next.limit,\n        filters: next.filters,\n        meta: {\n          isTraceIdMode: next.isTraceIdMode,\n          traceDurationUnit: next.durationUnit,\n          traceId: next.traceId,\n          flattenNested: next.flattenNested,\n          traceEventsColumnPrefix: next.traceEventsColumnPrefix,\n          traceLinksColumnPrefix: next.traceLinksColumnPrefix,\n        },\n      })\n    );\n  }, builderState);\n\n  useTraceDefaultsOnMount(datasource, isNewQuery, builderOptions, builderOptionsDispatch);\n  useOtelColumns(builderState.otelEnabled, builderState.otelVersion, builderOptionsDispatch);\n  useDefaultFilters(builderOptions.table, builderState.isTraceIdMode, isNewQuery, builderOptionsDispatch);\n\n  const configWarning = showConfigWarning && (\n    <Alert title=\"\" severity=\"warning\" buttonContent=\"Close\" onRemove={() => setConfigWarningOpen(false)}>\n      <Stack>\n        <div>\n          {'To speed up your query building, enter your default trace configuration in your '}\n          <a\n            style={{ textDecoration: 'underline' }}\n            href={`/connections/datasources/edit/${encodeURIComponent(datasource.uid)}#traces-config`}\n          >\n            ClickHouse Data Source settings\n          </a>\n        </div>\n      </Stack>\n    </Alert>\n  );\n\n  return (\n    <div>\n      <ModeSwitch\n        labelA={labels.traceSearchModeLabel}\n        labelB={labels.traceIdModeLabel}\n        value={builderState.isTraceIdMode}\n        onChange={onOptionChange('isTraceIdMode')}\n        label={labels.traceModeLabel}\n        tooltip={labels.traceModeTooltip}\n      />\n\n      <Collapse label={labels.columnsSection} collapsible isOpen={isColumnsOpen} onToggle={setColumnsOpen}>\n        {configWarning}\n        <ColumnRolesHelp\n          text={labels.columnsHelp.text}\n          linkText={labels.columnsHelp.linkText}\n          href={labels.columnsHelp.href}\n          testIdWrapper={allSelectors.QueryBuilder.TraceQueryBuilder.columnRolesHelp}\n          testIdLink={allSelectors.QueryBuilder.TraceQueryBuilder.columnRolesHelpLink}\n        />\n        <OtelVersionSelect\n          enabled={builderState.otelEnabled}\n          onEnabledChange={(e) => builderOptionsDispatch(setOtelEnabled(e))}\n          selectedVersion={builderState.otelVersion}\n          onVersionChange={(v) => builderOptionsDispatch(setOtelVersion(v))}\n          wide\n        />\n        <div className=\"gf-form\">\n          <ColumnSelect\n            disabled={builderState.otelEnabled}\n            allColumns={allColumns}\n            selectedColumn={builderState.traceIdColumn}\n            invalid={!builderState.traceIdColumn}\n            onColumnChange={onOptionChange('traceIdColumn')}\n            columnHint={ColumnHint.TraceId}\n            label={labels.columns.traceId.label}\n            tooltip={labels.columns.traceId.tooltip}\n            wide\n          />\n          <ColumnSelect\n            disabled={builderState.otelEnabled}\n            allColumns={allColumns}\n            selectedColumn={builderState.spanIdColumn}\n            invalid={!builderState.spanIdColumn}\n            onColumnChange={onOptionChange('spanIdColumn')}\n            columnHint={ColumnHint.TraceSpanId}\n            label={labels.columns.spanId.label}\n            tooltip={labels.columns.spanId.tooltip}\n            wide\n            inline\n          />\n        </div>\n        <div className=\"gf-form\">\n          <ColumnSelect\n            disabled={builderState.otelEnabled}\n            allColumns={allColumns}\n            selectedColumn={builderState.parentSpanIdColumn}\n            invalid={!builderState.parentSpanIdColumn}\n            onColumnChange={onOptionChange('parentSpanIdColumn')}\n            columnHint={ColumnHint.TraceParentSpanId}\n            label={labels.columns.parentSpanId.label}\n            tooltip={labels.columns.parentSpanId.tooltip}\n            wide\n          />\n          <ColumnSelect\n            disabled={builderState.otelEnabled}\n            allColumns={allColumns}\n            selectedColumn={builderState.serviceNameColumn}\n            invalid={!builderState.serviceNameColumn}\n            onColumnChange={onOptionChange('serviceNameColumn')}\n            columnHint={ColumnHint.TraceServiceName}\n            label={labels.columns.serviceName.label}\n            tooltip={labels.columns.serviceName.tooltip}\n            wide\n            inline\n          />\n        </div>\n        <div className=\"gf-form\">\n          <ColumnSelect\n            disabled={builderState.otelEnabled}\n            allColumns={allColumns}\n            selectedColumn={builderState.operationNameColumn}\n            invalid={!builderState.operationNameColumn}\n            onColumnChange={onOptionChange('operationNameColumn')}\n            columnHint={ColumnHint.TraceOperationName}\n            label={labels.columns.operationName.label}\n            tooltip={labels.columns.operationName.tooltip}\n            wide\n          />\n          <ColumnSelect\n            disabled={builderState.otelEnabled}\n            allColumns={allColumns}\n            selectedColumn={builderState.startTimeColumn}\n            invalid={!builderState.startTimeColumn}\n            onColumnChange={onOptionChange('startTimeColumn')}\n            columnHint={ColumnHint.Time}\n            label={labels.columns.startTime.label}\n            tooltip={labels.columns.startTime.tooltip}\n            wide\n            inline\n          />\n        </div>\n        <div className=\"gf-form\">\n          <ColumnSelect\n            disabled={builderState.otelEnabled}\n            allColumns={allColumns}\n            selectedColumn={builderState.durationTimeColumn}\n            invalid={!builderState.durationTimeColumn}\n            onColumnChange={onOptionChange('durationTimeColumn')}\n            columnHint={ColumnHint.TraceDurationTime}\n            label={labels.columns.durationTime.label}\n            tooltip={labels.columns.durationTime.tooltip}\n            wide\n          />\n          <DurationUnitSelect\n            disabled={builderState.otelEnabled}\n            unit={builderState.durationUnit}\n            onChange={onOptionChange('durationUnit')}\n            inline\n          />\n        </div>\n        <div className=\"gf-form\">\n          <ColumnSelect\n            disabled={builderState.otelEnabled}\n            allColumns={allColumns}\n            selectedColumn={builderState.tagsColumn}\n            invalid={!builderState.tagsColumn}\n            onColumnChange={onOptionChange('tagsColumn')}\n            columnHint={ColumnHint.TraceTags}\n            label={labels.columns.tags.label}\n            tooltip={labels.columns.tags.tooltip}\n            wide\n          />\n          <ColumnSelect\n            disabled={builderState.otelEnabled}\n            allColumns={allColumns}\n            selectedColumn={builderState.serviceTagsColumn}\n            invalid={!builderState.serviceTagsColumn}\n            onColumnChange={onOptionChange('serviceTagsColumn')}\n            columnHint={ColumnHint.TraceServiceTags}\n            label={labels.columns.serviceTags.label}\n            tooltip={labels.columns.serviceTags.tooltip}\n            wide\n            inline\n          />\n        </div>\n        <div className=\"gf-form\">\n          <ColumnSelect\n            disabled={builderState.otelEnabled}\n            allColumns={allColumns}\n            selectedColumn={builderState.kindColumn}\n            invalid={!builderState.kindColumn}\n            onColumnChange={onOptionChange('kindColumn')}\n            columnHint={ColumnHint.TraceKind}\n            label={labels.columns.kind.label}\n            tooltip={labels.columns.kind.tooltip}\n            wide\n          />\n          <ColumnSelect\n            disabled={builderState.otelEnabled}\n            allColumns={allColumns}\n            selectedColumn={builderState.statusCodeColumn}\n            invalid={!builderState.statusCodeColumn}\n            onColumnChange={onOptionChange('statusCodeColumn')}\n            columnHint={ColumnHint.TraceStatusCode}\n            label={labels.columns.statusCode.label}\n            tooltip={labels.columns.statusCode.tooltip}\n            wide\n            inline\n          />\n        </div>\n        <div className=\"gf-form\">\n          <ColumnSelect\n            disabled={builderState.otelEnabled}\n            allColumns={allColumns}\n            selectedColumn={builderState.statusMessageColumn}\n            invalid={!builderState.statusMessageColumn}\n            onColumnChange={onOptionChange('statusMessageColumn')}\n            columnHint={ColumnHint.TraceStatusMessage}\n            label={labels.columns.statusMessage.label}\n            tooltip={labels.columns.statusMessage.tooltip}\n            wide\n          />\n          <ColumnSelect\n            disabled={builderState.otelEnabled}\n            allColumns={allColumns}\n            selectedColumn={builderState.stateColumn}\n            invalid={!builderState.stateColumn}\n            onColumnChange={onOptionChange('stateColumn')}\n            columnHint={ColumnHint.TraceState}\n            label={labels.columns.state.label}\n            tooltip={labels.columns.state.tooltip}\n            wide\n            inline\n          />\n        </div>\n        <div className=\"gf-form\">\n          <ColumnSelect\n            disabled={builderState.otelEnabled}\n            allColumns={allColumns}\n            selectedColumn={builderState.instrumentationLibraryNameColumn}\n            invalid={!builderState.instrumentationLibraryNameColumn}\n            onColumnChange={onOptionChange('instrumentationLibraryNameColumn')}\n            columnHint={ColumnHint.TraceInstrumentationLibraryName}\n            label={labels.columns.instrumentationLibraryName.label}\n            tooltip={labels.columns.instrumentationLibraryName.tooltip}\n            wide\n          />\n          <ColumnSelect\n            disabled={builderState.otelEnabled}\n            allColumns={allColumns}\n            selectedColumn={builderState.instrumentationLibraryVersionColumn}\n            invalid={!builderState.instrumentationLibraryVersionColumn}\n            onColumnChange={onOptionChange('instrumentationLibraryVersionColumn')}\n            columnHint={ColumnHint.TraceInstrumentationLibraryVersion}\n            label={labels.columns.instrumentationLibraryVersion.label}\n            tooltip={labels.columns.instrumentationLibraryVersion.tooltip}\n            wide\n            inline\n          />\n        </div>\n        <div className=\"gf-form\">\n          <Switch\n            disabled={builderState.otelEnabled}\n            label={labels.columns.flattenNested.label}\n            tooltip={labels.columns.flattenNested.tooltip}\n            value={Boolean(builderState.flattenNested)}\n            onChange={onOptionChange('flattenNested')}\n            wide\n          />\n        </div>\n        <div className=\"gf-form\">\n          <LabeledInput\n            disabled={builderState.otelEnabled}\n            label={labels.columns.eventsPrefix.label}\n            tooltip={labels.columns.eventsPrefix.tooltip}\n            value={builderState.traceEventsColumnPrefix || ''}\n            onChange={onOptionChange('traceEventsColumnPrefix')}\n          />\n        </div>\n        <div className=\"gf-form\">\n          <LabeledInput\n            disabled={builderState.otelEnabled}\n            label={labels.columns.linksPrefix.label}\n            tooltip={labels.columns.linksPrefix.tooltip}\n            value={builderState.traceLinksColumnPrefix || ''}\n            onChange={onOptionChange('traceLinksColumnPrefix')}\n          />\n        </div>\n      </Collapse>\n      <Collapse label={labels.filtersSection} collapsible isOpen={isFiltersOpen} onToggle={setFiltersOpen}>\n        <OrderByEditor\n          orderByOptions={getOrderByOptions(builderOptions, allColumns)}\n          orderBy={builderState.orderBy}\n          onOrderByChange={onOptionChange('orderBy')}\n        />\n        <LimitEditor limit={builderState.limit} onLimitChange={onOptionChange('limit')} />\n        <FiltersEditor\n          allColumns={allColumns}\n          filters={builderState.filters}\n          onFiltersChange={onOptionChange('filters')}\n          datasource={datasource}\n          database={builderOptions.database}\n          table={builderOptions.table}\n        />\n      </Collapse>\n      {builderState.isTraceIdMode && (\n        <TraceIdInput traceId={builderState.traceId} onChange={onOptionChange('traceId')} />\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/queryBuilder/views/logsQueryBuilderHooks.test.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport {\n  useDefaultFilters,\n  useDefaultTimeColumn,\n  useLogDefaultsOnMount,\n  useOtelColumns,\n} from './logsQueryBuilderHooks';\nimport { mockDatasource } from '__mocks__/datasource';\nimport { ColumnHint, QueryBuilderOptions, SelectedColumn, TableColumn } from 'types/queryBuilder';\nimport { setColumnByHint, setOptions } from 'hooks/useBuilderOptionsState';\nimport otel from 'otel';\n\ndescribe('useLogDefaultsOnMount', () => {\n  it('should call builderOptionsDispatch with default log columns', async () => {\n    const builderOptionsDispatch = jest.fn();\n    jest.spyOn(mockDatasource, 'shouldSelectLogContextColumns').mockReturnValue(false);\n    // Should not be included, since shouldSelectLogContextColumns returns false\n    jest.spyOn(mockDatasource, 'getLogContextColumnNames').mockReturnValue(['SampleColumn']);\n    jest.spyOn(mockDatasource, 'getLogsOtelVersion').mockReturnValue(undefined);\n    jest\n      .spyOn(mockDatasource, 'getDefaultLogsColumns')\n      .mockReturnValue(new Map<ColumnHint, string>([[ColumnHint.Time, 'timestamp']]));\n\n    renderHook(() => useLogDefaultsOnMount(mockDatasource, true, {} as QueryBuilderOptions, builderOptionsDispatch));\n\n    const expectedOptions = {\n      database: expect.anything(),\n      table: expect.anything(),\n      columns: [{ name: 'timestamp', hint: ColumnHint.Time }],\n      meta: {\n        otelEnabled: expect.anything(),\n        otelVersion: undefined,\n      },\n    };\n\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(1);\n    expect(builderOptionsDispatch).toHaveBeenCalledWith(expect.objectContaining(setOptions(expectedOptions)));\n  });\n\n  it('should call builderOptionsDispatch with default log columns, including log context columns', async () => {\n    const builderOptionsDispatch = jest.fn();\n    jest.spyOn(mockDatasource, 'shouldSelectLogContextColumns').mockReturnValue(true);\n    // timestamp is included, but also provided as a Log Context column. It should only appear once.\n    jest.spyOn(mockDatasource, 'getLogContextColumnNames').mockReturnValue(['timestamp', 'SampleColumn']);\n    jest.spyOn(mockDatasource, 'getLogsOtelVersion').mockReturnValue(undefined);\n    jest\n      .spyOn(mockDatasource, 'getDefaultLogsColumns')\n      .mockReturnValue(new Map<ColumnHint, string>([[ColumnHint.Time, 'timestamp']]));\n\n    renderHook(() => useLogDefaultsOnMount(mockDatasource, true, {} as QueryBuilderOptions, builderOptionsDispatch));\n\n    const expectedOptions = {\n      database: expect.anything(),\n      table: expect.anything(),\n      columns: [{ name: 'timestamp', hint: ColumnHint.Time }, { name: 'SampleColumn' }],\n      meta: {\n        otelEnabled: expect.anything(),\n        otelVersion: undefined,\n      },\n    };\n\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(1);\n    expect(builderOptionsDispatch).toHaveBeenCalledWith(expect.objectContaining(setOptions(expectedOptions)));\n  });\n\n  it('should not call builderOptionsDispatch after defaults are set', async () => {\n    const builderOptions = {} as QueryBuilderOptions;\n    const builderOptionsDispatch = jest.fn();\n\n    const hook = renderHook(() => useLogDefaultsOnMount(mockDatasource, true, builderOptions, builderOptionsDispatch));\n    hook.rerender();\n\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(1);\n  });\n\n  it('should not call builderOptionsDispatch for existing query', async () => {\n    const isNewQuery = false; // query already exists, is not new\n    const builderOptionsDispatch = jest.fn();\n    renderHook(() =>\n      useLogDefaultsOnMount(mockDatasource, isNewQuery, {} as QueryBuilderOptions, builderOptionsDispatch)\n    );\n\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(0);\n  });\n});\n\ndescribe('useOtelColumns', () => {\n  const testOtelVersion = otel.getLatestVersion();\n\n  it('should not call builderOptionsDispatch if OTEL is already enabled', async () => {\n    jest.spyOn(mockDatasource, 'shouldSelectLogContextColumns').mockReturnValue(false);\n    const builderOptionsDispatch = jest.fn();\n    const allColumns: readonly TableColumn[] = [{ name: 'LogAttributes', type: 'Map(String, String)', picklistValues: [] }];\n\n    renderHook(() => useOtelColumns(mockDatasource, allColumns, true, testOtelVersion.version, builderOptionsDispatch));\n\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(0);\n  });\n\n  it('should not call builderOptionsDispatch if OTEL is disabled', async () => {\n    jest.spyOn(mockDatasource, 'shouldSelectLogContextColumns').mockReturnValue(false);\n    // Should not be included, since shouldSelectLogContextColumns returns false\n    jest.spyOn(mockDatasource, 'getLogContextColumnNames').mockReturnValue(['SampleColumn']);\n    const builderOptionsDispatch = jest.fn();\n    const allColumns: readonly TableColumn[] = [{ name: 'LogAttributes', type: 'Map(String, String)', picklistValues: [] }];\n\n    renderHook(() => useOtelColumns(mockDatasource, allColumns, true, testOtelVersion.version, builderOptionsDispatch));\n\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(0);\n  });\n\n  it('should not call builderOptionsDispatch if allColumns is empty', async () => {\n    jest.spyOn(mockDatasource, 'shouldSelectLogContextColumns').mockReturnValue(false);\n    const builderOptionsDispatch = jest.fn();\n\n    let otelEnabled = false;\n    const hook = renderHook(\n      (enabled) => useOtelColumns(mockDatasource, [], enabled, testOtelVersion.version, builderOptionsDispatch),\n      { initialProps: otelEnabled }\n    );\n    otelEnabled = true;\n    hook.rerender(otelEnabled);\n\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(0);\n  });\n\n  it('should call builderOptionsDispatch with columns when OTEL is toggled on', async () => {\n    jest.spyOn(mockDatasource, 'shouldSelectLogContextColumns').mockReturnValue(false);\n    const builderOptionsDispatch = jest.fn();\n    const allColumns: readonly TableColumn[] = [{ name: 'LogAttributes', type: 'Map(String, String)', picklistValues: [] }];\n\n    let otelEnabled = false;\n    const hook = renderHook(\n      (enabled) => useOtelColumns(mockDatasource, allColumns, enabled, testOtelVersion.version, builderOptionsDispatch),\n      { initialProps: otelEnabled }\n    );\n    otelEnabled = true;\n    hook.rerender(otelEnabled);\n\n    const columns: SelectedColumn[] = [];\n    testOtelVersion.logColumnMap.forEach((v, k) => {\n      columns.push({ name: v, hint: k, type: allColumns.find((c) => c.name === v)?.type });\n    });\n    const expectedOptions = { columns };\n\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(1);\n    expect(builderOptionsDispatch).toHaveBeenCalledWith(expect.objectContaining(setOptions(expectedOptions)));\n  });\n\n  it('should call builderOptionsDispatch with log context columns when auto-select is enabled', async () => {\n    jest.spyOn(mockDatasource, 'shouldSelectLogContextColumns').mockReturnValue(true);\n    // Timestamp is an OTel column, but also provided as a Log Context column. It should only appear once.\n    jest.spyOn(mockDatasource, 'getLogContextColumnNames').mockReturnValue(['Timestamp', 'SampleColumn']);\n    const builderOptionsDispatch = jest.fn();\n    const allColumns: readonly TableColumn[] = [\n      { name: 'LogAttributes', type: 'Map(String, String)', picklistValues: [] },\n      { name: 'SampleColumn', type: 'String', picklistValues: [] },\n    ];\n\n    let otelEnabled = false;\n    const hook = renderHook(\n      (enabled) => useOtelColumns(mockDatasource, allColumns, enabled, testOtelVersion.version, builderOptionsDispatch),\n      { initialProps: otelEnabled }\n    );\n    otelEnabled = true;\n    hook.rerender(otelEnabled);\n\n    const columns: SelectedColumn[] = [];\n    testOtelVersion.logColumnMap.forEach((v, k) => {columns.push({ name: v, hint: k, type: allColumns.find((c) => c.name === v)?.type })});\n    columns.push({ name: 'SampleColumn', type: allColumns.find((c) => c.name === 'SampleColumn')?.type });\n    const expectedOptions = { columns };\n\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(1);\n    expect(builderOptionsDispatch).toHaveBeenCalledWith(expect.objectContaining(setOptions(expectedOptions)));\n  });\n\n  it('should not call builderOptionsDispatch after OTEL columns are set', async () => {\n    jest.spyOn(mockDatasource, 'shouldSelectLogContextColumns').mockReturnValue(false);\n    const builderOptionsDispatch = jest.fn();\n    const allColumns: readonly TableColumn[] = [{ name: 'LogAttributes', type: 'Map(String, String)', picklistValues: [] }];\n\n    let otelEnabled = false; // OTEL is off\n    const hook = renderHook(\n      (enabled) => useOtelColumns(mockDatasource, allColumns, enabled, testOtelVersion.version, builderOptionsDispatch),\n      { initialProps: otelEnabled }\n    );\n    otelEnabled = true;\n    hook.rerender(otelEnabled); // OTEL is on, columns are set\n    hook.rerender(otelEnabled); // OTEL still on, should not set again\n\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(1);\n  });\n});\n\ndescribe('useDefaultTimeColumn', () => {\n  it('should call builderOptionsDispatch when there are no configured defaults', async () => {\n    const builderOptionsDispatch = jest.fn();\n    const tableName = 'logs';\n    const timeColumnName = 'time';\n    jest.spyOn(mockDatasource, 'getDefaultLogsTable').mockReturnValue(undefined);\n    const allColumns: readonly TableColumn[] = [{ name: timeColumnName, type: 'DateTime', picklistValues: [] }];\n    const timeColumn = undefined;\n    const otelEnabled = false;\n\n    renderHook(() =>\n      useDefaultTimeColumn(mockDatasource, allColumns, tableName, timeColumn, otelEnabled, builderOptionsDispatch)\n    );\n\n    const expectedColumn: SelectedColumn = { name: timeColumnName, type: 'DateTime', hint: ColumnHint.Time };\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(1);\n    expect(builderOptionsDispatch).toHaveBeenCalledWith(expect.objectContaining(setColumnByHint(expectedColumn)));\n  });\n\n  it('should not call builderOptionsDispatch when column is already set', async () => {\n    const builderOptionsDispatch = jest.fn();\n    const tableName = 'logs';\n    const timeColumnName = 'time';\n    jest.spyOn(mockDatasource, 'getDefaultLogsTable').mockReturnValue(tableName);\n    jest\n      .spyOn(mockDatasource, 'getDefaultLogsColumns')\n      .mockReturnValue(new Map<ColumnHint, string>([[ColumnHint.Time, timeColumnName]]));\n    const allColumns: readonly TableColumn[] = [{ name: timeColumnName, type: 'DateTime', picklistValues: [] }];\n    const timeColumn: SelectedColumn = { name: timeColumnName, hint: ColumnHint.Time };\n    const otelEnabled = false;\n\n    renderHook(() =>\n      useDefaultTimeColumn(mockDatasource, allColumns, tableName, timeColumn, otelEnabled, builderOptionsDispatch)\n    );\n\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(0);\n  });\n\n  it('should call builderOptionsDispatch when table changes', async () => {\n    const builderOptionsDispatch = jest.fn();\n    const tableName = 'logs';\n    const timeColumnName = 'time';\n    jest.spyOn(mockDatasource, 'getDefaultLogsTable').mockReturnValue(tableName);\n    jest\n      .spyOn(mockDatasource, 'getDefaultLogsColumns')\n      .mockReturnValue(new Map<ColumnHint, string>([[ColumnHint.Time, timeColumnName]]));\n    const allColumns: readonly TableColumn[] = [{ name: timeColumnName, type: 'DateTime', picklistValues: [] }];\n    const timeColumn = undefined;\n    const otelEnabled = false;\n\n    const hook = renderHook(\n      (table) =>\n        useDefaultTimeColumn(mockDatasource, allColumns, table, timeColumn, otelEnabled, builderOptionsDispatch),\n      { initialProps: tableName }\n    );\n    hook.rerender('other_logs');\n\n    const expectedColumn: SelectedColumn = { name: timeColumnName, type: 'DateTime', hint: ColumnHint.Time };\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(1);\n    expect(builderOptionsDispatch).toHaveBeenCalledWith(expect.objectContaining(setColumnByHint(expectedColumn)));\n  });\n});\n\ndescribe('useDefaultFilters', () => {\n  it('should call builderOptionsDispatch when query is new', async () => {\n    const builderOptionsDispatch = jest.fn();\n    const tableName = 'logs';\n    const isNewQuery = true;\n\n    renderHook(() => useDefaultFilters(tableName, isNewQuery, builderOptionsDispatch));\n\n    const expectedOptions = {\n      filters: [expect.anything(), expect.anything()],\n      orderBy: [expect.anything(), expect.anything()],\n    };\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(1);\n    expect(builderOptionsDispatch).toHaveBeenCalledWith(expect.objectContaining(setOptions(expectedOptions)));\n  });\n\n  it('should not call builderOptionsDispatch when query is not new', async () => {\n    const builderOptionsDispatch = jest.fn();\n    const tableName = 'logs';\n    const isNewQuery = false;\n\n    renderHook(() => useDefaultFilters(tableName, isNewQuery, builderOptionsDispatch));\n\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(0);\n  });\n\n  it('should call builderOptionsDispatch when table changes', async () => {\n    const builderOptionsDispatch = jest.fn();\n    const tableName = 'logs';\n    const isNewQuery = false;\n\n    const hook = renderHook((table) => useDefaultFilters(table, isNewQuery, builderOptionsDispatch), {\n      initialProps: tableName,\n    });\n    hook.rerender('other_logs');\n\n    const expectedOptions = {\n      filters: [expect.anything(), expect.anything()],\n      orderBy: [expect.anything(), expect.anything()],\n    };\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(1);\n    expect(builderOptionsDispatch).toHaveBeenCalledWith(expect.objectContaining(setOptions(expectedOptions)));\n  });\n});\n"
  },
  {
    "path": "src/components/queryBuilder/views/logsQueryBuilderHooks.ts",
    "content": "import { Datasource } from 'data/CHDatasource';\nimport { columnFilterDateTime } from 'data/columnFilters';\nimport { BuilderOptionsReducerAction, setColumnByHint, setOptions } from 'hooks/useBuilderOptionsState';\nimport { useEffect, useMemo, useRef } from 'react';\nimport {\n  ColumnHint,\n  DateFilterWithoutValue,\n  Filter,\n  FilterOperator,\n  OrderBy,\n  OrderByDirection,\n  QueryBuilderOptions,\n  SelectedColumn,\n  StringFilter,\n  TableColumn,\n} from 'types/queryBuilder';\nimport otel from 'otel';\n\n/**\n * Loads the default configuration for new queries. (Only runs on new queries)\n */\nexport const useLogDefaultsOnMount = (\n  datasource: Datasource,\n  isNewQuery: boolean,\n  builderOptions: QueryBuilderOptions,\n  builderOptionsDispatch: React.Dispatch<BuilderOptionsReducerAction>\n) => {\n  const didSetDefaults = useRef<boolean>(false);\n  useEffect(() => {\n    if (!isNewQuery || didSetDefaults.current) {\n      return;\n    }\n\n    const defaultDb = datasource.getDefaultLogsDatabase() || datasource.getDefaultDatabase();\n    const defaultTable = datasource.getDefaultLogsTable() || datasource.getDefaultTable();\n    const otelVersion = datasource.getLogsOtelVersion();\n    const defaultColumns = datasource.getDefaultLogsColumns();\n\n    const nextColumns: SelectedColumn[] = [];\n    const includedColumns = new Set<string>();\n    for (let [hint, colName] of defaultColumns) {\n      nextColumns.push({ name: colName, hint });\n      includedColumns.add(colName);\n    }\n\n    if (datasource.shouldSelectLogContextColumns()) {\n      const contextColumnNames = datasource.getLogContextColumnNames();\n\n      for (let columnName of contextColumnNames) {\n        // Excludes columns already added, and maps that contain the selected key (such as \"ResourceAttributes['x']\")\n        if (includedColumns.has(columnName) || includedColumns.has(columnName.split('[')[0])) {\n          continue;\n        }\n\n        nextColumns.push({ name: columnName });\n        includedColumns.add(columnName);\n      }\n    }\n\n    builderOptionsDispatch(\n      setOptions({\n        database: defaultDb,\n        table: defaultTable || builderOptions.table,\n        columns: nextColumns,\n        meta: {\n          otelEnabled: Boolean(otelVersion),\n          otelVersion,\n        },\n      })\n    );\n    didSetDefaults.current = true;\n  }, [\n    builderOptions.columns,\n    builderOptions.orderBy,\n    builderOptions.table,\n    builderOptionsDispatch,\n    datasource,\n    isNewQuery,\n  ]);\n};\n\n/**\n * Sets OTEL Logs columns automatically when OTEL is enabled.\n * Does not run if OTEL is already enabled, only when it's changed.\n */\nexport const useOtelColumns = (\n  datasource: Datasource,\n  allColumns: readonly TableColumn[],\n  otelEnabled: boolean,\n  otelVersion: string,\n  builderOptionsDispatch: React.Dispatch<BuilderOptionsReducerAction>\n) => {\n  const didSetColumns = useRef<boolean>(otelEnabled);\n  if (!otelEnabled) {\n    didSetColumns.current = false;\n  }\n\n  useEffect(() => {\n    if (!otelEnabled || didSetColumns.current || allColumns.length === 0) {\n      return;\n    }\n\n    const otelConfig = otel.getVersion(otelVersion);\n    const logColumnMap = otelConfig?.logColumnMap;\n    if (!logColumnMap) {\n      return;\n    }\n\n    const columns: SelectedColumn[] = [];\n    const includedColumns = new Set<string>();\n    logColumnMap.forEach((name, hint) => {\n      columns.push({ name, hint, type: allColumns.find((c) => c.name === name)?.type });\n      includedColumns.add(name);\n    });\n\n    if (datasource.shouldSelectLogContextColumns()) {\n      const contextColumnNames = datasource.getLogContextColumnNames();\n\n      for (let columnName of contextColumnNames) {\n        // Excludes columns already added, and maps that contain the selected key (such as \"ResourceAttributes['x']\")\n        if (includedColumns.has(columnName) || includedColumns.has(columnName.split('[')[0])) {\n          continue;\n        }\n\n        columns.push({ name: columnName, type: allColumns.find((c) => c.name === columnName)?.type });\n        includedColumns.add(columnName);\n      }\n    }\n\n    builderOptionsDispatch(setOptions({ columns }));\n    didSetColumns.current = true;\n  }, [datasource, allColumns, otelEnabled, otelVersion, builderOptionsDispatch]);\n};\n\n// Finds and selects a default log time column, updates when table changes\nexport const useDefaultTimeColumn = (\n  datasource: Datasource,\n  allColumns: readonly TableColumn[],\n  table: string,\n  timeColumn: SelectedColumn | undefined,\n  otelEnabled: boolean,\n  builderOptionsDispatch: React.Dispatch<BuilderOptionsReducerAction>\n) => {\n  const hasDefaultColumnConfigured = useMemo(\n    () => Boolean(datasource.getDefaultLogsTable()) && datasource.getDefaultLogsColumns().has(ColumnHint.Time),\n    [datasource]\n  );\n  const didSetDefaultTime = useRef<boolean>(Boolean(timeColumn) || hasDefaultColumnConfigured);\n  const lastTable = useRef<string>(table || '');\n  if (table !== lastTable.current) {\n    didSetDefaultTime.current = false;\n  }\n\n  if (Boolean(timeColumn) || otelEnabled) {\n    lastTable.current = table;\n    didSetDefaultTime.current = true;\n  }\n\n  useEffect(() => {\n    if (didSetDefaultTime.current || allColumns.length === 0 || !table) {\n      return;\n    }\n\n    const col = allColumns.filter(columnFilterDateTime)[0];\n    if (!col) {\n      return;\n    }\n\n    const timeColumn: SelectedColumn = {\n      name: col.name,\n      type: col.type,\n      hint: ColumnHint.Time,\n    };\n\n    builderOptionsDispatch(setColumnByHint(timeColumn));\n    lastTable.current = table;\n    didSetDefaultTime.current = true;\n  }, [datasource, allColumns, table, builderOptionsDispatch]);\n};\n\n// Apply default filters/orderBy on table change\nexport const useDefaultFilters = (\n  table: string,\n  isNewQuery: boolean,\n  builderOptionsDispatch: React.Dispatch<BuilderOptionsReducerAction>\n) => {\n  const appliedDefaultFilters = useRef<boolean>(!isNewQuery);\n  const lastTable = useRef<string>(table || '');\n  if (table !== lastTable.current) {\n    appliedDefaultFilters.current = false;\n  }\n\n  useEffect(() => {\n    if (!table || appliedDefaultFilters.current) {\n      return;\n    }\n\n    const defaultFilters: Filter[] = [\n      {\n        type: 'datetime',\n        operator: FilterOperator.WithInGrafanaTimeRange,\n        filterType: 'custom',\n        key: '',\n        hint: ColumnHint.FilterTime,\n        condition: 'AND',\n      } as DateFilterWithoutValue,\n      {\n        type: 'string',\n        operator: FilterOperator.IsAnything,\n        filterType: 'custom',\n        key: '',\n        hint: ColumnHint.LogLevel,\n        condition: 'AND',\n      } as StringFilter,\n    ];\n\n    const defaultOrderBy: OrderBy[] = [\n      { name: '', hint: ColumnHint.FilterTime, dir: OrderByDirection.DESC, default: true },\n      { name: '', hint: ColumnHint.Time, dir: OrderByDirection.DESC, default: true }\n    ];\n\n    lastTable.current = table;\n    appliedDefaultFilters.current = true;\n    builderOptionsDispatch(\n      setOptions({\n        filters: defaultFilters,\n        orderBy: defaultOrderBy,\n      })\n    );\n  }, [table, builderOptionsDispatch]);\n};\n"
  },
  {
    "path": "src/components/queryBuilder/views/timeSeriesQueryBuilderHooks.test.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { useDefaultFilters, useDefaultTimeColumn } from './timeSeriesQueryBuilderHooks';\nimport { ColumnHint, SelectedColumn, TableColumn } from 'types/queryBuilder';\nimport { setColumnByHint, setOptions } from 'hooks/useBuilderOptionsState';\n\ndescribe('useDefaultTimeColumn', () => {\n  it('should call builderOptionsDispatch when time column is unset', async () => {\n    const builderOptionsDispatch = jest.fn();\n    const tableName = 'timeseries';\n    const timeColumnName = 'time';\n    const allColumns: readonly TableColumn[] = [{ name: timeColumnName, type: 'DateTime', picklistValues: [] }];\n    const timeColumn = undefined;\n\n    renderHook(() => useDefaultTimeColumn(allColumns, tableName, timeColumn, builderOptionsDispatch));\n\n    const expectedColumn: SelectedColumn = { name: timeColumnName, type: 'DateTime', hint: ColumnHint.Time };\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(1);\n    expect(builderOptionsDispatch).toHaveBeenCalledWith(expect.objectContaining(setColumnByHint(expectedColumn)));\n  });\n\n  it('should not call builderOptionsDispatch when time column is already set', async () => {\n    const builderOptionsDispatch = jest.fn();\n    const tableName = 'timeseries';\n    const timeColumnName = 'time';\n    const allColumns: readonly TableColumn[] = [{ name: timeColumnName, type: 'DateTime', picklistValues: [] }];\n    const timeColumn: SelectedColumn = { name: timeColumnName, hint: ColumnHint.Time };\n\n    renderHook(() => useDefaultTimeColumn(allColumns, tableName, timeColumn, builderOptionsDispatch));\n\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(0);\n  });\n\n  it('should call builderOptionsDispatch when table changes', async () => {\n    const builderOptionsDispatch = jest.fn();\n    const tableName = 'timeseries';\n    const timeColumnName = 'time';\n    const allColumns: readonly TableColumn[] = [{ name: timeColumnName, type: 'DateTime', picklistValues: [] }];\n    const timeColumn = undefined;\n\n    renderHook((table) => useDefaultTimeColumn(allColumns, table, timeColumn, builderOptionsDispatch), {\n      initialProps: tableName,\n    });\n\n    const expectedColumn: SelectedColumn = { name: timeColumnName, type: 'DateTime', hint: ColumnHint.Time };\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(1);\n    expect(builderOptionsDispatch).toHaveBeenCalledWith(expect.objectContaining(setColumnByHint(expectedColumn)));\n  });\n});\n\ndescribe('useDefaultFilters', () => {\n  it('should call builderOptionsDispatch when query is new', async () => {\n    const builderOptionsDispatch = jest.fn();\n    const tableName = 'timeseries';\n    const isNewQuery = true;\n\n    renderHook(() => useDefaultFilters(tableName, isNewQuery, builderOptionsDispatch));\n\n    const expectedOptions = {\n      filters: [expect.anything()],\n      orderBy: [expect.anything()],\n    };\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(1);\n    expect(builderOptionsDispatch).toHaveBeenCalledWith(expect.objectContaining(setOptions(expectedOptions)));\n  });\n\n  it('should not call builderOptionsDispatch when query is not new', async () => {\n    const builderOptionsDispatch = jest.fn();\n    const tableName = 'timeseries';\n    const isNewQuery = false;\n\n    renderHook(() => useDefaultFilters(tableName, isNewQuery, builderOptionsDispatch));\n\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(0);\n  });\n\n  it('should call builderOptionsDispatch when table changes', async () => {\n    const builderOptionsDispatch = jest.fn();\n    const tableName = 'timeseries';\n    const isNewQuery = false;\n\n    const hook = renderHook((table) => useDefaultFilters(table, isNewQuery, builderOptionsDispatch), {\n      initialProps: tableName,\n    });\n    hook.rerender('other_timeseries');\n\n    const expectedOptions = {\n      filters: [expect.anything()],\n      orderBy: [expect.anything()],\n    };\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(1);\n    expect(builderOptionsDispatch).toHaveBeenCalledWith(expect.objectContaining(setOptions(expectedOptions)));\n  });\n});\n"
  },
  {
    "path": "src/components/queryBuilder/views/timeSeriesQueryBuilderHooks.ts",
    "content": "import { columnFilterDateTime } from 'data/columnFilters';\nimport { BuilderOptionsReducerAction, setColumnByHint, setOptions } from 'hooks/useBuilderOptionsState';\nimport React, { useEffect, useRef } from 'react';\nimport {\n  ColumnHint,\n  DateFilterWithoutValue,\n  Filter,\n  FilterOperator,\n  OrderBy,\n  OrderByDirection,\n  SelectedColumn,\n  TableColumn,\n} from 'types/queryBuilder';\n\n// Finds and selects a default log time column, updates when table changes\nexport const useDefaultTimeColumn = (\n  allColumns: readonly TableColumn[],\n  table: string,\n  timeColumn: SelectedColumn | undefined,\n  builderOptionsDispatch: React.Dispatch<BuilderOptionsReducerAction>\n) => {\n  const didSetDefaultTime = useRef<boolean>(Boolean(timeColumn));\n  const lastTable = useRef<string>(table || '');\n  if (table !== lastTable.current) {\n    didSetDefaultTime.current = false;\n  }\n\n  useEffect(() => {\n    if (didSetDefaultTime.current || allColumns.length === 0 || !table) {\n      return;\n    }\n\n    const col = allColumns.filter(columnFilterDateTime)[0];\n    if (!col) {\n      return;\n    }\n\n    const timeColumn: SelectedColumn = {\n      name: col.name,\n      type: col.type,\n      hint: ColumnHint.Time,\n    };\n\n    builderOptionsDispatch(setColumnByHint(timeColumn));\n    lastTable.current = table;\n    didSetDefaultTime.current = true;\n  }, [allColumns, table, builderOptionsDispatch]);\n};\n\n// Apply default filters on table change\nexport const useDefaultFilters = (\n  table: string,\n  isNewQuery: boolean,\n  builderOptionsDispatch: React.Dispatch<BuilderOptionsReducerAction>\n) => {\n  const appliedDefaultFilters = useRef<boolean>(!isNewQuery);\n  const lastTable = useRef<string>(table || '');\n  if (table !== lastTable.current) {\n    appliedDefaultFilters.current = false;\n  }\n\n  useEffect(() => {\n    if (!table || appliedDefaultFilters.current) {\n      return;\n    }\n\n    const defaultFilters: Filter[] = [\n      {\n        type: 'datetime',\n        operator: FilterOperator.WithInGrafanaTimeRange,\n        filterType: 'custom',\n        key: '',\n        hint: ColumnHint.Time,\n        condition: 'AND',\n      } as DateFilterWithoutValue,\n    ];\n\n    const defaultOrderBy: OrderBy[] = [{ name: '', hint: ColumnHint.Time, dir: OrderByDirection.ASC, default: true }];\n\n    lastTable.current = table;\n    appliedDefaultFilters.current = true;\n    builderOptionsDispatch(\n      setOptions({\n        filters: defaultFilters,\n        orderBy: defaultOrderBy,\n      })\n    );\n  }, [table, builderOptionsDispatch]);\n};\n"
  },
  {
    "path": "src/components/queryBuilder/views/traceQueryBuilderHooks.test.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { useTraceDefaultsOnMount, useOtelColumns, useDefaultFilters } from './traceQueryBuilderHooks';\nimport { mockDatasource } from '__mocks__/datasource';\nimport { ColumnHint, QueryBuilderOptions, SelectedColumn } from 'types/queryBuilder';\nimport { setOptions } from 'hooks/useBuilderOptionsState';\nimport otel from 'otel';\n\ndescribe('useTraceDefaultsOnMount', () => {\n  it('should call builderOptionsDispatch with default trace columns', async () => {\n    const builderOptionsDispatch = jest.fn();\n    jest.spyOn(mockDatasource, 'getTraceOtelVersion').mockReturnValue(undefined);\n    jest\n      .spyOn(mockDatasource, 'getDefaultTraceColumns')\n      .mockReturnValue(new Map<ColumnHint, string>([[ColumnHint.Time, 'timestamp']]));\n\n    renderHook(() => useTraceDefaultsOnMount(mockDatasource, true, {} as QueryBuilderOptions, builderOptionsDispatch));\n\n    const expectedOptions = {\n      database: expect.anything(),\n      table: expect.anything(),\n      columns: [{ name: 'timestamp', hint: ColumnHint.Time }],\n      meta: {\n        otelEnabled: expect.anything(),\n        otelVersion: undefined,\n        traceDurationUnit: expect.anything(),\n        flattenNested: expect.anything(),\n        traceEventsColumnPrefix: expect.anything(),\n        traceLinksColumnPrefix: expect.anything(),\n        traceTimestampTableSuffix: expect.anything(),\n      },\n    };\n\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(1);\n    expect(builderOptionsDispatch).toHaveBeenCalledWith(expect.objectContaining(setOptions(expectedOptions)));\n  });\n\n  it('should not call builderOptionsDispatch after defaults are set', async () => {\n    const builderOptions = {} as QueryBuilderOptions;\n    const builderOptionsDispatch = jest.fn();\n\n    const hook = renderHook(() =>\n      useTraceDefaultsOnMount(mockDatasource, true, builderOptions, builderOptionsDispatch)\n    );\n    hook.rerender();\n\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(1);\n  });\n\n  it('should not call builderOptionsDispatch for existing query', async () => {\n    const isNewQuery = false; // query already exists, is not new\n    const builderOptionsDispatch = jest.fn();\n    renderHook(() =>\n      useTraceDefaultsOnMount(mockDatasource, isNewQuery, {} as QueryBuilderOptions, builderOptionsDispatch)\n    );\n\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(0);\n  });\n});\n\ndescribe('useOtelColumns', () => {\n  const testOtelVersion = otel.getLatestVersion();\n\n  it('should not call builderOptionsDispatch if OTEL is already enabled', async () => {\n    const builderOptionsDispatch = jest.fn();\n    renderHook(() => useOtelColumns(true, testOtelVersion.version, builderOptionsDispatch));\n\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(0);\n  });\n\n  it('should not call builderOptionsDispatch if OTEL is disabled', async () => {\n    const builderOptionsDispatch = jest.fn();\n    renderHook(() => useOtelColumns(true, testOtelVersion.version, builderOptionsDispatch));\n\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(0);\n  });\n\n  it('should call builderOptionsDispatch with columns when OTEL is toggled on', async () => {\n    const builderOptionsDispatch = jest.fn();\n\n    let otelEnabled = false;\n    const hook = renderHook((enabled) => useOtelColumns(enabled, testOtelVersion.version, builderOptionsDispatch), {\n      initialProps: otelEnabled,\n    });\n    otelEnabled = true;\n    hook.rerender(otelEnabled);\n\n    const columns: SelectedColumn[] = [];\n    testOtelVersion.traceColumnMap.forEach((v, k) => columns.push({ name: v, hint: k }));\n    const expectedOptions = {\n      columns,\n      meta: {\n        traceDurationUnit: expect.anything(),\n        flattenNested: expect.anything(),\n        traceEventsColumnPrefix: expect.anything(),\n        traceLinksColumnPrefix: expect.anything(),\n      },\n    };\n\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(1);\n    expect(builderOptionsDispatch).toHaveBeenCalledWith(expect.objectContaining(setOptions(expectedOptions)));\n  });\n\n  it('should not call builderOptionsDispatch after OTEL columns are set', async () => {\n    const builderOptionsDispatch = jest.fn();\n\n    let otelEnabled = false; // OTEL is off\n    const hook = renderHook((enabled) => useOtelColumns(enabled, testOtelVersion.version, builderOptionsDispatch), {\n      initialProps: otelEnabled,\n    });\n    otelEnabled = true;\n    hook.rerender(otelEnabled); // OTEL is on, columns are set\n    hook.rerender(otelEnabled); // OTEL still on, should not set again\n\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(1);\n  });\n});\n\ndescribe('useDefaultFilters', () => {\n  it('should call builderOptionsDispatch when query is new', async () => {\n    const builderOptionsDispatch = jest.fn();\n    const tableName = 'timeseries';\n    const isTraceIdMode = false;\n    const isNewQuery = true;\n\n    renderHook(() => useDefaultFilters(tableName, isTraceIdMode, isNewQuery, builderOptionsDispatch));\n\n    const expectedOptions = {\n      filters: [expect.anything(), expect.anything(), expect.anything(), expect.anything()],\n      orderBy: [expect.anything(), expect.anything()],\n    };\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(1);\n    expect(builderOptionsDispatch).toHaveBeenCalledWith(expect.objectContaining(setOptions(expectedOptions)));\n  });\n\n  it('should not call builderOptionsDispatch when query is not new', async () => {\n    const builderOptionsDispatch = jest.fn();\n    const tableName = 'timeseries';\n    const isTraceIdMode = false;\n    const isNewQuery = false;\n\n    renderHook(() => useDefaultFilters(tableName, isTraceIdMode, isNewQuery, builderOptionsDispatch));\n\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(0);\n  });\n\n  it('should not call builderOptionsDispatch when query is trace ID mode', async () => {\n    const builderOptionsDispatch = jest.fn();\n    const tableName = 'timeseries';\n    const isTraceIdMode = true;\n    const isNewQuery = true;\n\n    renderHook(() => useDefaultFilters(tableName, isTraceIdMode, isNewQuery, builderOptionsDispatch));\n\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(0);\n  });\n\n  it('should call builderOptionsDispatch when table changes', async () => {\n    const builderOptionsDispatch = jest.fn();\n    const tableName = 'timeseries';\n    const isTraceIdMode = false;\n    const isNewQuery = false;\n\n    const hook = renderHook((table) => useDefaultFilters(table, isTraceIdMode, isNewQuery, builderOptionsDispatch), {\n      initialProps: tableName,\n    });\n    hook.rerender('other_timeseries');\n\n    const expectedOptions = {\n      filters: [expect.anything(), expect.anything(), expect.anything(), expect.anything()],\n      orderBy: [expect.anything(), expect.anything()],\n    };\n    expect(builderOptionsDispatch).toHaveBeenCalledTimes(1);\n    expect(builderOptionsDispatch).toHaveBeenCalledWith(expect.objectContaining(setOptions(expectedOptions)));\n  });\n});\n"
  },
  {
    "path": "src/components/queryBuilder/views/traceQueryBuilderHooks.ts",
    "content": "import React, { useEffect, useRef } from 'react';\nimport { Datasource } from 'data/CHDatasource';\nimport otel from 'otel';\nimport {\n  ColumnHint,\n  DateFilterWithoutValue,\n  Filter,\n  FilterOperator,\n  NumberFilter,\n  OrderBy,\n  OrderByDirection,\n  QueryBuilderOptions,\n  SelectedColumn,\n  StringFilter,\n} from 'types/queryBuilder';\nimport { BuilderOptionsReducerAction, setOptions } from 'hooks/useBuilderOptionsState';\n\n/**\n * Loads the default configuration for new queries. (Only runs on new queries)\n */\nexport const useTraceDefaultsOnMount = (\n  datasource: Datasource,\n  isNewQuery: boolean,\n  builderOptions: QueryBuilderOptions,\n  builderOptionsDispatch: React.Dispatch<BuilderOptionsReducerAction>\n) => {\n  const didSetDefaults = useRef<boolean>(false);\n  useEffect(() => {\n    if (!isNewQuery || didSetDefaults.current) {\n      return;\n    }\n\n    const defaultDb = datasource.getDefaultTraceDatabase() || datasource.getDefaultDatabase();\n    const defaultTable = datasource.getDefaultTraceTable() || datasource.getDefaultTable();\n    const defaultDurationUnit = datasource.getDefaultTraceDurationUnit();\n    const otelVersion = datasource.getTraceOtelVersion();\n    const defaultColumns = datasource.getDefaultTraceColumns();\n    const defaultFlattenNested = datasource.getDefaultTraceFlattenNested();\n    const defaultEventsColumnPrefix = datasource.getDefaultTraceEventsColumnPrefix();\n    const defaultLinksColumnPrefix = datasource.getDefaultTraceLinksColumnPrefix();\n    const traceTimestampTableSuffix = datasource.getTraceTimestampTableSuffix();\n\n    const nextColumns: SelectedColumn[] = [];\n    for (let [hint, colName] of defaultColumns) {\n      nextColumns.push({ name: colName, hint });\n    }\n\n    builderOptionsDispatch(\n      setOptions({\n        database: defaultDb,\n        table: defaultTable || builderOptions.table,\n        columns: nextColumns,\n        meta: {\n          otelEnabled: Boolean(otelVersion),\n          otelVersion,\n          traceDurationUnit: defaultDurationUnit,\n          flattenNested: defaultFlattenNested,\n          traceEventsColumnPrefix: defaultEventsColumnPrefix,\n          traceLinksColumnPrefix: defaultLinksColumnPrefix,\n          traceTimestampTableSuffix,\n        },\n      })\n    );\n    didSetDefaults.current = true;\n  }, [\n    builderOptions.columns,\n    builderOptions.orderBy,\n    builderOptions.table,\n    builderOptionsDispatch,\n    datasource,\n    isNewQuery,\n  ]);\n};\n\n/**\n * Sets OTEL Trace columns automatically when OTEL is enabled\n * Does not run if OTEL is already enabled, only when it's changed.\n */\nexport const useOtelColumns = (\n  otelEnabled: boolean,\n  otelVersion: string,\n  builderOptionsDispatch: React.Dispatch<BuilderOptionsReducerAction>\n) => {\n  const didSetColumns = useRef<boolean>(otelEnabled);\n  if (!otelEnabled) {\n    didSetColumns.current = false;\n  }\n\n  useEffect(() => {\n    if (!otelEnabled || didSetColumns.current) {\n      return;\n    }\n\n    const otelConfig = otel.getVersion(otelVersion);\n    const traceColumnMap = otelConfig?.traceColumnMap;\n    if (!traceColumnMap) {\n      return;\n    }\n\n    const columns: SelectedColumn[] = [];\n    traceColumnMap.forEach((name, hint) => {\n      columns.push({ name, hint });\n    });\n\n    builderOptionsDispatch(\n      setOptions({\n        columns,\n        meta: {\n          traceDurationUnit: otelConfig.traceDurationUnit,\n          flattenNested: otelConfig.flattenNested,\n          traceEventsColumnPrefix: otelConfig.traceEventsColumnPrefix,\n          traceLinksColumnPrefix: otelConfig.traceLinksColumnPrefix,\n        },\n      })\n    );\n    didSetColumns.current = true;\n  }, [otelEnabled, otelVersion, builderOptionsDispatch]);\n};\n\n// Apply default filters on table change\nexport const useDefaultFilters = (\n  table: string,\n  isTraceIdMode: boolean,\n  isNewQuery: boolean,\n  builderOptionsDispatch: React.Dispatch<BuilderOptionsReducerAction>\n) => {\n  const appliedDefaultFilters = useRef<boolean>(!isNewQuery);\n  const lastTable = useRef<string>(table || '');\n  if (table !== lastTable.current) {\n    appliedDefaultFilters.current = false;\n  }\n\n  useEffect(() => {\n    if (isTraceIdMode || !table || appliedDefaultFilters.current) {\n      return;\n    }\n\n    const defaultFilters: Filter[] = [\n      {\n        type: 'datetime',\n        operator: FilterOperator.WithInGrafanaTimeRange,\n        filterType: 'custom',\n        key: '',\n        hint: ColumnHint.Time,\n        condition: 'AND',\n      } as DateFilterWithoutValue, // Filter to dashboard time range\n      {\n        type: 'string',\n        operator: FilterOperator.IsEmpty,\n        filterType: 'custom',\n        key: '',\n        hint: ColumnHint.TraceParentSpanId,\n        condition: 'AND',\n        value: '',\n      } as StringFilter, // Only show top level spans\n      {\n        type: 'UInt64',\n        operator: FilterOperator.GreaterThan,\n        filterType: 'custom',\n        key: '',\n        hint: ColumnHint.TraceDurationTime,\n        condition: 'AND',\n        value: 0,\n      } as NumberFilter, // Only show spans where duration > 0\n      {\n        type: 'string',\n        operator: FilterOperator.IsAnything,\n        filterType: 'custom',\n        key: '',\n        hint: ColumnHint.TraceServiceName,\n        condition: 'AND',\n        value: '',\n      } as StringFilter, // Placeholder service name filter for convenience\n    ];\n\n    const defaultOrderBy: OrderBy[] = [\n      { name: '', hint: ColumnHint.Time, dir: OrderByDirection.DESC, default: true },\n      { name: '', hint: ColumnHint.TraceDurationTime, dir: OrderByDirection.DESC, default: true },\n    ];\n\n    lastTable.current = table;\n    appliedDefaultFilters.current = true;\n    builderOptionsDispatch(\n      setOptions({\n        filters: defaultFilters,\n        orderBy: defaultOrderBy,\n      })\n    );\n  }, [table, isTraceIdMode, builderOptionsDispatch]);\n};\n"
  },
  {
    "path": "src/components/sqlProvider.test.ts",
    "content": "import { formatSql, registerSQL } from './sqlProvider';\n\ndescribe('SQL Formatter', () => {\n  it('formats SQL', () => {\n    const input = 'SELECT 1, 2, 3 FROM test LIMIT 1';\n    const expected = 'SELECT\\n 1,\\n 2,\\n 3\\nFROM\\n test\\nLIMIT\\n 1';\n\n    const actual = formatSql(input, 1);\n    expect(actual).toBe(expected);\n  });\n});\n\ndescribe('registerSQL', () => {\n  it('disposes the completion and formatting providers when dispose() is called', () => {\n    const disposables: Array<{ dispose: jest.Mock }> = [];\n    const makeDisposable = () => {\n      const d = { dispose: jest.fn() };\n      disposables.push(d);\n      return d;\n    };\n\n    (window as any).monaco = {\n      languages: {\n        registerCompletionItemProvider: jest.fn(makeDisposable),\n        registerDocumentFormattingEditProvider: jest.fn(makeDisposable),\n      },\n      editor: {} as any,\n    };\n\n    const editor: any = { updateOptions: jest.fn() };\n    const fetcher = jest.fn();\n\n    const registration = registerSQL('sql', editor, fetcher);\n    expect(disposables).toHaveLength(2);\n    disposables.forEach((d) => expect(d.dispose).not.toHaveBeenCalled());\n\n    registration.dispose();\n    disposables.forEach((d) => expect(d.dispose).toHaveBeenCalledTimes(1));\n  });\n\n  it('registers a fresh pair of providers on each call (caller must dispose)', () => {\n    const completionRegister = jest.fn(() => ({ dispose: jest.fn() }));\n    const formattingRegister = jest.fn(() => ({ dispose: jest.fn() }));\n\n    (window as any).monaco = {\n      languages: {\n        registerCompletionItemProvider: completionRegister,\n        registerDocumentFormattingEditProvider: formattingRegister,\n      },\n      editor: {} as any,\n    };\n\n    const editor: any = { updateOptions: jest.fn() };\n\n    registerSQL('sql', editor, jest.fn());\n    registerSQL('sql', editor, jest.fn());\n\n    expect(completionRegister).toHaveBeenCalledTimes(2);\n    expect(formattingRegister).toHaveBeenCalledTimes(2);\n  });\n});\n"
  },
  {
    "path": "src/components/sqlProvider.ts",
    "content": "import { Monaco, MonacoEditor, monacoTypes } from '@grafana/ui';\nimport { format } from 'sql-formatter';\n\ndeclare const monaco: Monaco;\n\ninterface Model {\n  getValueInRange: Function;\n  getWordUntilPosition: Function;\n  getValue: Function;\n  getOffsetAt: Function;\n}\n\ninterface Position {\n  lineNumber: number;\n  column: number;\n}\n\nexport interface Range {\n  startLineNumber: number;\n  endLineNumber: number;\n  startColumn: number;\n  endColumn: number;\n}\n\nexport interface SuggestionResponse {\n  suggestions: monacoTypes.languages.CompletionItem[];\n}\n\nexport interface Suggestion {\n  label: string;\n  kind: number;\n  documentation: string;\n  insertText: string;\n  range: Range;\n  detail?: string;\n  sortText?: string;\n}\n\nexport type Fetcher = {\n  (text: string, range: Range, cursorPosition: number): Promise<SuggestionResponse>;\n};\n\nexport function formatSql(rawSql: string, tabWidth = 4): string {\n  // The default formatter doesn't like the $, so we swap it out\n  const macroPrefix = '$';\n  const swapIdentifier = 'GRAFANA_DOLLAR_TOKEN';\n  const removedVariables = rawSql.replaceAll(macroPrefix, swapIdentifier);\n  const formattedRaw = format(removedVariables, {\n    language: 'postgresql',\n    tabWidth,\n  });\n\n  const formatted = formattedRaw.replaceAll(swapIdentifier, macroPrefix);\n  return formatted;\n}\n\nexport interface SqlRegistration {\n  monacoEditor: typeof monaco.editor;\n  dispose: () => void;\n}\n\nexport function registerSQL(lang: string, editor: MonacoEditor, fetchSuggestions: Fetcher): SqlRegistration {\n  // show options outside query editor\n  editor.updateOptions({ fixedOverflowWidgets: true, scrollBeyondLastLine: false });\n\n  // const registeredLang = monaco.languages.getLanguages().find((l: Lang) => l.id === lang);\n  // if (registeredLang !== undefined) {\n  //   return monaco.editor;\n  // }\n\n  // monaco.languages.register({ id: lang });\n\n  // just extend sql for now so we get syntax highlighting\n  const completionProvider = monaco.languages.registerCompletionItemProvider('sql', {\n    triggerCharacters: [' ', '.', '$'],\n    provideCompletionItems: async (model: Model, position: Position) => {\n      const word = model.getWordUntilPosition(position);\n      const range: Range = {\n        startLineNumber: position.lineNumber,\n        endLineNumber: position.lineNumber,\n        startColumn: word.startColumn,\n        endColumn: word.endColumn,\n      };\n\n      return fetchSuggestions(model.getValue(), range, model.getOffsetAt(position));\n    },\n  });\n\n  const formattingProvider = monaco.languages.registerDocumentFormattingEditProvider('sql', {\n    provideDocumentFormattingEdits(model, options) {\n      return [\n        {\n          range: model.getFullModelRange(),\n          text: formatSql(model.getValue(), options.tabSize),\n        },\n      ];\n    },\n  });\n\n  return {\n    monacoEditor: monaco.editor,\n    dispose: () => {\n      completionProvider.dispose();\n      formattingProvider.dispose();\n    },\n  };\n}\n"
  },
  {
    "path": "src/components/suggestions.test.ts",
    "content": "import { SqlFunction, TableColumn } from 'types/queryBuilder';\nimport { getSuggestions, Schema } from './suggestions';\nimport { Range } from './sqlProvider';\nimport { pluginMacros } from 'ch-parser/pluginMacros';\n\ndescribe('Suggestions', () => {\n  it('matches columns case-insensitively when a prefix is typed', async () => {\n    // User types lowercase \"codefile\" but the column is named \"CodeFile\"\n    const sql = 'SELECT codefile FROM system.query_log';\n    //                          ^ cursor here (position 15, end of \"codefile\")\n    const cursorPosition = 15;\n    const range: Range = {\n      startLineNumber: 0,\n      endLineNumber: 0,\n      startColumn: cursorPosition,\n      endColumn: cursorPosition + 1,\n    };\n\n    const schema: Schema = {\n      databases: async (): Promise<string[]> => ['system'],\n      tables: async (): Promise<string[]> => ['query_log'],\n      columns: async (): Promise<TableColumn[]> => [\n        { label: 'CodeFile', name: 'CodeFile', type: 'String' } as TableColumn,\n        { label: 'EventDate', name: 'EventDate', type: 'DateTime' } as TableColumn,\n      ],\n      functions: async (): Promise<SqlFunction[]> => [],\n      defaultDatabase: 'system',\n    };\n\n    (window as any).monaco = {\n      languages: {\n        CompletionItemKind: { Function: 1, Field: 3, Variable: 4, Class: 5, Module: 8 },\n        CompletionItemInsertTextRule: { InsertAsSnippet: 4 },\n      },\n    };\n\n    const suggestions = await getSuggestions(sql, schema, range, cursorPosition);\n    const labels = suggestions.map((s) => s.label);\n\n    // \"CodeFile\" should appear even though the user typed \"codefile\"\n    expect(labels).toContain('CodeFile');\n    // \"EventDate\" should not appear — it doesn't start with \"codefile\"\n    expect(labels).not.toContain('EventDate');\n  });\n\n  it('dedupes suggestions sharing the same label, kind, and insertText', async () => {\n    const sql = 'SELECT  FROM system.query_log';\n    //                  ^ cursor here (position 7, inside SELECT clause)\n    const cursorPosition = 7;\n    const range: Range = {\n      startLineNumber: 0,\n      endLineNumber: 0,\n      startColumn: cursorPosition,\n      endColumn: cursorPosition + 1,\n    };\n\n    const schema: Schema = {\n      databases: async (): Promise<string[]> => ['system'],\n      tables: async (): Promise<string[]> => ['query_log'],\n      columns: async (): Promise<TableColumn[]> => [\n        { label: 'CodeFile', name: 'CodeFile', type: 'String' } as TableColumn,\n        { label: 'CodeFile', name: 'CodeFile', type: 'String' } as TableColumn,\n        { label: 'EventDate', name: 'EventDate', type: 'DateTime' } as TableColumn,\n      ],\n      functions: async (): Promise<SqlFunction[]> => [\n        { name: 'toDateTime' } as SqlFunction,\n        { name: 'toDateTime' } as SqlFunction,\n      ],\n      defaultDatabase: 'system',\n    };\n\n    (window as any).monaco = {\n      languages: {\n        CompletionItemKind: { Function: 1, Field: 3, Variable: 4, Class: 5, Module: 8, Keyword: 13 },\n        CompletionItemInsertTextRule: { InsertAsSnippet: 4 },\n      },\n    };\n\n    const suggestions = await getSuggestions(sql, schema, range, cursorPosition);\n    const codeFileMatches = suggestions.filter((s) => s.label === 'CodeFile');\n    const toDateTimeMatches = suggestions.filter((s) => s.label === 'toDateTime');\n\n    expect(codeFileMatches).toHaveLength(1);\n    expect(toDateTimeMatches).toHaveLength(1);\n  });\n\n  it('shows suggestions', async () => {\n    const sql = `SELECT number, (SELECT query,  FROM system.query_log LIMIT 1) FROM system.numbers LIMIT 1`;\n    const cursorPosition = 30; //         here ^ after \"query\"\n    const range: Range = {\n      startLineNumber: 0,\n      endLineNumber: 0,\n      startColumn: cursorPosition,\n      endColumn: cursorPosition + 1,\n    };\n\n    const schema: Schema = {\n      databases: async (): Promise<string[]> => ['default', 'system'],\n      tables: async (db?: string): Promise<string[]> => ['numbers', 'query_log'],\n      columns: async (db: string, table: string): Promise<TableColumn[]> => [\n        { label: 'query', type: 'String' } as TableColumn,\n        { label: 'EventDate', type: 'DateTime' } as TableColumn,\n      ],\n      functions: async (): Promise<SqlFunction[]> => [{ name: 'toDateTime' } as SqlFunction],\n      defaultDatabase: 'default',\n    };\n\n    (window as any).monaco = {\n      languages: {\n        CompletionItemKind: {\n          Function: 1,\n          Field: 3,\n          Variable: 4,\n          Class: 5,\n          Module: 8,\n        },\n        CompletionItemInsertTextRule: {\n          InsertAsSnippet: 4,\n        },\n      },\n    };\n\n    const suggestions = await getSuggestions(sql, schema, range, cursorPosition);\n    const suggestionsByLabel = new Map(suggestions.map((s) => [s.label, s]));\n\n    const columnNumber = suggestionsByLabel.get('number');\n    expect(columnNumber).toBeUndefined(); // number is out of scope of the provided subquery\n\n    // Should show all macros\n    for (let macro of pluginMacros) {\n      const macroSuggestion = suggestionsByLabel.get(macro.name);\n      expect(macroSuggestion).not.toBeUndefined();\n    }\n\n    // Should have current columns in context\n    const columnQuery = suggestionsByLabel.get('query');\n    expect(columnQuery).not.toBeUndefined();\n\n    // Should show unused columns from table\n    const columnEventDate = suggestionsByLabel.get('EventDate');\n    expect(columnEventDate).not.toBeUndefined();\n\n    // Should show functions\n    const functionToDateTime = suggestionsByLabel.get('toDateTime');\n    expect(functionToDateTime).not.toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "src/components/suggestions.ts",
    "content": "import { getTemplateSrv } from '@grafana/runtime';\nimport { Monaco, monacoTypes } from '@grafana/ui';\nimport { Range } from './sqlProvider';\nimport { Lexer } from 'ch-parser/lexer';\nimport { keywords, TokenType } from 'ch-parser/types';\nimport { SqlFunction, TableColumn } from 'types/queryBuilder';\nimport { pluginMacros } from 'ch-parser/pluginMacros';\nimport {\n  ClauseType,\n  FromQueryNode,\n  IdentifierQueryNode,\n  parseSelectQueryNode,\n  QueryNodeParser,\n  QueryNodeType,\n  SelectQueryNode,\n} from 'ch-parser/parser';\n\ndeclare const monaco: Monaco;\nexport interface Schema {\n  databases: () => Promise<string[]>;\n  tables: (db?: string) => Promise<string[]>;\n  columns: (db: string, table: string) => Promise<TableColumn[]>;\n  functions: () => Promise<SqlFunction[]>;\n  defaultDatabase?: string;\n}\n\ninterface CursorData {\n  clause: ClauseType;\n  identifiers: string[];\n  database?: string;\n  table?: string;\n  prefix?: string;\n\n  begin: number;\n  end: number;\n}\n\nfunction getCursorInSelectQueryNode(root: SelectQueryNode, cursorPosition: number): CursorData {\n  const cursorData: CursorData = {\n    clause: ClauseType.None,\n    identifiers: [],\n    begin: root.token.begin,\n    end: root.token.end,\n  };\n\n  if (cursorPosition > root.token.end) {\n    cursorData.clause = ClauseType.Select;\n  }\n\n  if (root.from) {\n    cursorData.database = root.from.database;\n    cursorData.table = root.from.table;\n  }\n\n  if (!root.children) {\n    return cursorData;\n  }\n\n  for (const node of root.children) {\n    switch (node.type) {\n      case QueryNodeType.Select:\n        const selectNode = node as SelectQueryNode;\n        const nestedCursorData = getCursorInSelectQueryNode(selectNode, cursorPosition);\n        // +1/-1 to exclude subquery parenthesis\n        if (cursorPosition >= nestedCursorData.begin - 1 && cursorPosition <= nestedCursorData.end + 1) {\n          return nestedCursorData;\n        }\n\n        break;\n      default:\n        cursorData.end = node.token.end;\n        if (node.type === QueryNodeType.From) {\n          const fromNode = node as FromQueryNode;\n          const dbLen = fromNode.database?.length || 0;\n          const separatorLen = dbLen > 0 ? 1 : 0;\n          const tableLen = fromNode.table?.length || 0;\n          const extendedTokenLen = dbLen + separatorLen + tableLen;\n          cursorData.end = fromNode.token.end + extendedTokenLen;\n\n          if ((node as FromQueryNode).prefix) {\n            cursorData.prefix = (node as FromQueryNode).prefix;\n          }\n        }\n\n        if (\n          node.token.type === TokenType.QuotedIdentifier ||\n          node.type === QueryNodeType.Identifier ||\n          (node.token.type === TokenType.BareWord && !node.token.isKeyword())\n        ) {\n          if (node.type === QueryNodeType.Identifier) {\n            cursorData.identifiers.push((node as IdentifierQueryNode).prefix || node.token.text);\n          } else {\n            cursorData.identifiers.push(node.token.text);\n          }\n        }\n\n        if (node.type === QueryNodeType.Identifier && cursorPosition === node.token.end) {\n          cursorData.prefix = (node as IdentifierQueryNode).prefix;\n        }\n\n        if (cursorPosition < node.token.begin) {\n          break;\n        } else if (cursorPosition > node.token.end && node.clause !== ClauseType.None) {\n          cursorData.clause = node.clause;\n        }\n    }\n  }\n\n  return cursorData;\n}\n\nexport async function getSuggestions(\n  text: string,\n  schema: Schema,\n  range: Range,\n  cursorPosition: number\n): Promise<monacoTypes.languages.CompletionItem[]> {\n  const lexer = new Lexer(text);\n  const tokens = [];\n  while (true) {\n    const token = lexer.nextToken();\n    if (token.isEnd()) {\n      break;\n    }\n\n    if (!token.isSignificant()) {\n      continue;\n    }\n\n    tokens.push(token);\n  }\n\n  const parser = new QueryNodeParser(tokens);\n  const selectNode = parseSelectQueryNode(parser);\n\n  if (!selectNode) {\n    return [];\n  }\n\n  const cursorData = getCursorInSelectQueryNode(selectNode, cursorPosition);\n\n  const results = await getSuggestionsFromCursorData(cursorData, schema, range);\n  return dedupeSuggestions(results);\n}\n\nfunction dedupeSuggestions(\n  items: monacoTypes.languages.CompletionItem[]\n): monacoTypes.languages.CompletionItem[] {\n  const seen = new Set<string>();\n  return items.filter((item) => {\n    const label = typeof item.label === 'string' ? item.label : item.label.label;\n    const key = JSON.stringify([label, item.kind, item.insertText]);\n    if (seen.has(key)) {\n      return false;\n    }\n    seen.add(key);\n    return true;\n  });\n}\n\nasync function getSuggestionsFromCursorData(\n  data: CursorData,\n  schema: Schema,\n  range: Range\n): Promise<monacoTypes.languages.CompletionItem[]> {\n  let results: monacoTypes.languages.CompletionItem[] = [];\n\n  if (data.database && (data.database.includes('\"') || data.database.includes('`'))) {\n    data.database = data.database.substring(1, data.database.length - 1);\n  }\n  if (data.table && (data.table.includes('\"') || data.table.includes('`'))) {\n    data.table = data.table.substring(1, data.table.length - 1);\n  }\n\n  const mapping = {\n    [ClauseType.None]: 'keyword',\n    [ClauseType.With]: 'column',\n    [ClauseType.Select]: 'column',\n    [ClauseType.From]: 'database_or_table',\n    [ClauseType.Join]: 'database_or_table',\n    [ClauseType.Where]: 'column',\n    [ClauseType.GroupBy]: 'column',\n    [ClauseType.Having]: 'column',\n    [ClauseType.OrderBy]: 'column',\n    [ClauseType.Limit]: 'keyword',\n    [ClauseType.Identifier]: 'column',\n  };\n\n  if (data.database && !data.table) {\n    mapping[ClauseType.From] = 'table';\n    mapping[ClauseType.Join] = 'table';\n  } else if (data.table && !data.database) {\n    mapping[ClauseType.From] = 'database_or_table';\n    mapping[ClauseType.Join] = 'database_or_table';\n  } else if (data.database && data.table) {\n    mapping[ClauseType.From] = 'table';\n    mapping[ClauseType.Join] = 'table';\n  }\n\n  const contextType = mapping[data.clause];\n\n  const db = data.database || schema.defaultDatabase || 'default';\n  switch (contextType) {\n    case 'database':\n      results = await fetchDatabaseSuggestions(schema, range);\n      break;\n\n    case 'database_or_table':\n      const databases = await fetchDatabaseSuggestions(schema, range, data.prefix);\n      const defaultTables = await fetchTableSuggestions(schema, range, db, data.prefix);\n\n      results = [...databases, ...defaultTables];\n      break;\n\n    case 'table':\n      results = await fetchTableSuggestions(schema, range, db, data.prefix);\n      break;\n\n    case 'column':\n      const macros = await getMacroSuggestions(range, data.prefix);\n      results.push(...macros);\n      const variables = await getVariableSuggestions(range);\n      results.push(...variables);\n\n      results.push({\n        label: 'NULL',\n        insertText: 'NULL',\n        sortText: '!!!NULL',\n        kind: monaco.languages.CompletionItemKind.Keyword,\n        documentation: '',\n        range,\n      });\n\n      const sqlFunctions = await fetchFunctionSuggestions(schema, range);\n      results.push(...sqlFunctions);\n\n      if (data.table) {\n        const database = data.database || schema.defaultDatabase || 'default';\n        const columns = await fetchFieldSuggestions(schema, range, database, data.table, data.prefix);\n        results.push(...columns);\n      }\n\n      break;\n    case 'keyword':\n      results = Array.from(keywords).map((keyword) => ({\n        label: keyword,\n        insertText: keyword,\n        kind: monaco.languages.CompletionItemKind.Keyword,\n        documentation: '',\n        range,\n      }));\n      break;\n  }\n\n  return results;\n}\n\nasync function fetchDatabaseSuggestions(schema: Schema, range: Range, prefix?: string) {\n  const databases = await schema.databases();\n  return databases.map((val) => {\n    let quoteType = '';\n    if (prefix && prefix.startsWith('\"')) {\n      quoteType = '\"';\n    } else if (prefix && prefix.startsWith('`')) {\n      quoteType = '`';\n    }\n    const quoteClosed = val.endsWith(quoteType);\n\n    return {\n      label: val,\n      kind: monaco.languages.CompletionItemKind.Module,\n      detail: 'Database',\n      documentation: 'Database',\n      insertText: quoteType ? `${val}${quoteClosed ? '' : quoteType}` : val,\n      insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,\n      range,\n    };\n  });\n}\n\nasync function fetchTableSuggestions(schema: Schema, range: Range, database: string, prefix?: string) {\n  const tables = await schema.tables(database);\n  return tables.map((val) => {\n    let quoteType = '';\n    if (prefix && prefix.startsWith('\"')) {\n      quoteType = '\"';\n    } else if (prefix && prefix.startsWith('`')) {\n      quoteType = '`';\n    }\n    const quoteClosed = val.endsWith(quoteType);\n\n    return {\n      label: val,\n      kind: monaco.languages.CompletionItemKind.Class,\n      detail: 'Table',\n      documentation: 'Table',\n      insertText: quoteType ? `${val}${quoteClosed ? '' : quoteType}` : val,\n      insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,\n      range,\n    };\n  });\n}\n\nasync function fetchFieldSuggestions(schema: Schema, range: Range, db: string, table: string, prefix?: string) {\n  const columns = await schema.columns(db, table);\n  return columns\n    .map((c) => ({\n      label: c.label!,\n      kind: monaco.languages.CompletionItemKind.Field,\n      sortText: `!!!!${c.label}`,\n      detail: c.type,\n      documentation: c.type,\n      insertText: prefix && prefix.includes('.') ? c.name.substring(prefix?.length || 0) : c.name,\n      range,\n    }))\n    .filter((c) => !prefix || c.label.toLowerCase().startsWith(prefix.toLowerCase()));\n}\n\nasync function fetchFunctionSuggestions(schema: Schema, range: Range) {\n  const sqlFunctions = await schema.functions();\n  return sqlFunctions.map((c) => ({\n    label: c.name,\n    kind: monaco.languages.CompletionItemKind.Function,\n    sortText: `${c.name}`,\n    detail: c.categories || (c.isAggregate && 'Aggregate') || '',\n    documentation: [\n      `Category: ${c.categories || '(none)'}`,\n      `Alias: ${c.aliasTo || '(none)'}`,\n      `Aggregate: ${c.isAggregate}`,\n      `Case insensitive: ${c.caseInsensitive}`,\n      `Origin: ${c.origin}`,\n      `Description: ${c.description || '(none)'}`,\n      `Syntax: ${c.syntax || '(none)'}`,\n      `Arguments: ${c.arguments || '(none)'}`,\n      `Returned value: ${c.returnedValue || '(none)'}`,\n    ].join('\\n'),\n    insertText: `${c.name}(\\${1})`,\n    insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,\n    range,\n  }));\n}\n\nexport function getVariableSuggestions(range: Range) {\n  const templateSrv = getTemplateSrv();\n  if (!templateSrv) {\n    return [];\n  }\n  return templateSrv.getVariables().map((variable) => {\n    const label = `\\${${variable.name}}`;\n    const val = templateSrv.replace(label);\n    return {\n      label,\n      detail: `(Template Variable) ${val}`,\n      kind: monaco.languages.CompletionItemKind.Variable,\n      sortText: `!!!${label}`,\n      documentation: `(Template Variable) ${val}`,\n      insertText: `\\${${variable.name}}`,\n      range,\n    };\n  });\n}\n\nexport function getMacroSuggestions(range: Range, prefix?: string) {\n  return pluginMacros.map((macro) => {\n    const hasPrefix = (prefix || '').includes('$');\n    const nameNoPrefix = macro.name.substring(1);\n\n    return {\n      label: macro.name,\n      detail: `(Plugin Macro) ${macro.columnType || ''}`,\n      kind: macro.isFunction\n        ? monaco.languages.CompletionItemKind.Function\n        : monaco.languages.CompletionItemKind.Variable,\n      sortText: `!!${macro.name.substring(3)}`,\n      documentation: macro.documentation + (macro.example ? '\\nExample output: ' + macro.example : ''),\n      insertText: macro.isFunction\n        ? `${hasPrefix ? nameNoPrefix : macro.name.replaceAll('$', '\\\\$')}(\\${1})`\n        : hasPrefix\n          ? nameNoPrefix\n          : macro.name,\n      insertTextRules: macro.isFunction ? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet : undefined,\n      range,\n    };\n  });\n}\n"
  },
  {
    "path": "src/components/ui/CertificationKey.tsx",
    "content": "import React, { ChangeEvent, MouseEvent, FC } from 'react';\nimport { Input, Button, TextArea, Field } from '@grafana/ui';\n\ninterface Props {\n  label: string;\n  hasCert: boolean;\n  placeholder: string;\n  onChange: (event: ChangeEvent<HTMLTextAreaElement>) => void;\n  onClick: (event: MouseEvent<HTMLButtonElement>) => void;\n}\n\nexport const CertificationKey: FC<Props> = ({ hasCert, label, onChange, onClick, placeholder }) => {\n  return (\n    <Field label={label}>\n      {hasCert ? (\n        <>\n          <Input type=\"text\" disabled value=\"configured\" width={24} />\n          <Button variant=\"secondary\" onClick={onClick} style={{ marginLeft: 4 }}>\n            Reset\n          </Button>\n        </>\n      ) : (\n        <TextArea rows={7} onChange={onChange} placeholder={placeholder} required />\n      )}\n    </Field>\n  );\n};\n"
  },
  {
    "path": "src/dashboards/cluster-analysis.json",
    "content": "{\n  \"__inputs\": [],\n  \"__elements\": [],\n  \"__requires\": [\n    {\n      \"type\": \"panel\",\n      \"id\": \"bargauge\",\n      \"name\": \"Bar gauge\",\n      \"version\": \"\"\n    },\n    {\n      \"type\": \"grafana\",\n      \"id\": \"grafana\",\n      \"name\": \"Grafana\",\n      \"version\": \"9.0.1\"\n    },\n    {\n      \"type\": \"datasource\",\n      \"id\": \"grafana-clickhouse-datasource\",\n      \"name\": \"ClickHouse\",\n      \"version\": \"2.0.0\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"stat\",\n      \"name\": \"Stat\",\n      \"version\": \"\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"table\",\n      \"name\": \"Table\",\n      \"version\": \"\"\n    }\n  ],\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": {\n          \"type\": \"datasource\",\n          \"uid\": \"grafana\"\n        },\n        \"enable\": true,\n        \"hide\": true,\n        \"iconColor\": \"rgba(0, 211, 255, 1)\",\n        \"name\": \"Annotations & Alerts\",\n        \"target\": {\n          \"limit\": 100,\n          \"matchAny\": false,\n          \"tags\": [],\n          \"type\": \"dashboard\"\n        },\n        \"type\": \"dashboard\"\n      }\n    ]\n  },\n  \"editable\": true,\n  \"fiscalYearStartMonth\": 0,\n  \"graphTooltip\": 0,\n  \"id\": null,\n  \"iteration\": 1661857966580,\n  \"links\": [],\n  \"liveNow\": false,\n  \"panels\": [\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"semi-dark-blue\",\n                \"value\": null\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Version\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"unit\",\n                \"value\": \"string\"\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 5,\n        \"w\": 4,\n        \"x\": 0,\n        \"y\": 0\n      },\n      \"id\": 2,\n      \"options\": {\n        \"colorMode\": \"background\",\n        \"graphMode\": \"area\",\n        \"justifyMode\": \"center\",\n        \"orientation\": \"horizontal\",\n        \"reduceOptions\": {\n          \"calcs\": [\"lastNotNull\"],\n          \"fields\": \"/.*/\",\n          \"values\": false\n        },\n        \"textMode\": \"auto\"\n      },\n      \"pluginVersion\": \"9.0.1\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT version()\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"transformations\": [\n        {\n          \"id\": \"convertFieldType\",\n          \"options\": {\n            \"conversions\": [\n              {\n                \"destinationType\": \"string\",\n                \"targetField\": \"Version\"\n              }\n            ],\n            \"fields\": {}\n          }\n        },\n        {\n          \"id\": \"organize\",\n          \"options\": {\n            \"excludeByName\": {},\n            \"indexByName\": {},\n            \"renameByName\": {\n              \"version()\": \"Version\"\n            }\n          }\n        }\n      ],\n      \"type\": \"stat\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"semi-dark-blue\",\n                \"value\": null\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"uptime\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"unit\",\n                \"value\": \"s\"\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 5,\n        \"w\": 4,\n        \"x\": 4,\n        \"y\": 0\n      },\n      \"id\": 4,\n      \"options\": {\n        \"colorMode\": \"background\",\n        \"graphMode\": \"area\",\n        \"justifyMode\": \"center\",\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\"lastNotNull\"],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"textMode\": \"auto\"\n      },\n      \"pluginVersion\": \"9.0.1\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT uptime() as uptime\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"transformations\": [\n        {\n          \"id\": \"organize\",\n          \"options\": {\n            \"excludeByName\": {},\n            \"indexByName\": {},\n            \"renameByName\": {\n              \"uptime\": \"Server uptime\"\n            }\n          }\n        }\n      ],\n      \"type\": \"stat\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"semi-dark-blue\",\n                \"value\": null\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 5,\n        \"w\": 4,\n        \"x\": 8,\n        \"y\": 0\n      },\n      \"id\": 6,\n      \"options\": {\n        \"colorMode\": \"background\",\n        \"graphMode\": \"area\",\n        \"justifyMode\": \"center\",\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\"lastNotNull\"],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"textMode\": \"auto\"\n      },\n      \"pluginVersion\": \"9.0.1\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT count() as \\\"Number of databases\\\" FROM system.databases WHERE name IN (${database:singlequote})\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"type\": \"stat\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"semi-dark-blue\",\n                \"value\": null\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 5,\n        \"w\": 4,\n        \"x\": 12,\n        \"y\": 0\n      },\n      \"id\": 7,\n      \"options\": {\n        \"colorMode\": \"background\",\n        \"graphMode\": \"area\",\n        \"justifyMode\": \"center\",\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\"lastNotNull\"],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"textMode\": \"auto\"\n      },\n      \"pluginVersion\": \"9.0.1\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT count() as \\\"Number of tables\\\" FROM system.tables WHERE database IN (${database:singlequote})\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"type\": \"stat\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"semi-dark-blue\",\n                \"value\": null\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 5,\n        \"w\": 4,\n        \"x\": 16,\n        \"y\": 0\n      },\n      \"id\": 8,\n      \"options\": {\n        \"colorMode\": \"background\",\n        \"graphMode\": \"area\",\n        \"justifyMode\": \"center\",\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\"lastNotNull\"],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"textMode\": \"auto\"\n      },\n      \"pluginVersion\": \"9.0.1\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT sum(total_rows) as \\\"Number of rows\\\" FROM system.tables WHERE database IN (${database:singlequote});\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"type\": \"stat\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"semi-dark-blue\",\n                \"value\": null\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 5,\n        \"w\": 4,\n        \"x\": 20,\n        \"y\": 0\n      },\n      \"id\": 9,\n      \"options\": {\n        \"colorMode\": \"background\",\n        \"graphMode\": \"area\",\n        \"justifyMode\": \"center\",\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\"lastNotNull\"],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"textMode\": \"auto\"\n      },\n      \"pluginVersion\": \"9.0.1\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT count() as \\\"Number of columns\\\" FROM system.columns WHERE database IN (${database:singlequote});\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"type\": \"stat\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"center\",\n            \"displayMode\": \"auto\",\n            \"inspect\": false\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Is local\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"mappings\",\n                \"value\": [\n                  {\n                    \"options\": {\n                      \"0\": {\n                        \"color\": \"light-red\",\n                        \"index\": 0,\n                        \"text\": \"remote\"\n                      },\n                      \"1\": {\n                        \"color\": \"light-green\",\n                        \"index\": 1,\n                        \"text\": \"local\"\n                      }\n                    },\n                    \"type\": \"value\"\n                  }\n                ]\n              },\n              {\n                \"id\": \"custom.displayMode\",\n                \"value\": \"color-background-solid\"\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Errors count\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"color\",\n                \"value\": {\n                  \"mode\": \"continuous-GrYlRd\"\n                }\n              },\n              {\n                \"id\": \"custom.displayMode\",\n                \"value\": \"color-background\"\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Slowdowns count\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"color\",\n                \"value\": {\n                  \"mode\": \"continuous-GrYlRd\"\n                }\n              },\n              {\n                \"id\": \"custom.displayMode\",\n                \"value\": \"color-background\"\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Cluster\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.filterable\",\n                \"value\": true\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Host name\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.filterable\",\n                \"value\": true\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 10,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 5\n      },\n      \"id\": 20,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\"sum\"],\n          \"show\": false\n        },\n        \"showHeader\": true\n      },\n      \"pluginVersion\": \"9.0.1\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT cluster, shard_num, replica_num, host_name, host_address, port, is_local, errors_count, slowdowns_count FROM system.clusters;\\n\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Cluster Overview\",\n      \"transformations\": [\n        {\n          \"id\": \"organize\",\n          \"options\": {\n            \"excludeByName\": {},\n            \"indexByName\": {},\n            \"renameByName\": {\n              \"cluster\": \"Cluster\",\n              \"errors_count\": \"Errors count\",\n              \"host_address\": \"Host address\",\n              \"host_name\": \"Host name\",\n              \"is_local\": \"Is local\",\n              \"port\": \"Port\",\n              \"replica_num\": \"Replicated number\",\n              \"shard_num\": \"Shard number\",\n              \"slowdowns_count\": \"Slowdowns count\"\n            }\n          }\n        }\n      ],\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"percentage\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"progress\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"unit\",\n                \"value\": \"percent\"\n              },\n              {\n                \"id\": \"color\",\n                \"value\": {\n                  \"mode\": \"continuous-RdYlGr\"\n                }\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 7,\n        \"w\": 5,\n        \"x\": 0,\n        \"y\": 15\n      },\n      \"id\": 13,\n      \"options\": {\n        \"displayMode\": \"lcd\",\n        \"minVizHeight\": 10,\n        \"minVizWidth\": 0,\n        \"orientation\": \"horizontal\",\n        \"reduceOptions\": {\n          \"calcs\": [\"lastNotNull\"],\n          \"fields\": \"\",\n          \"values\": true\n        },\n        \"showUnfilled\": true\n      },\n      \"pluginVersion\": \"9.0.1\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT concatAssumeInjective(database, '.', table) as db_table, round(100 * progress, 1) \\\"progress\\\" FROM system.merges WHERE database IN (${database:singlequote}) ORDER BY progress DESC LIMIT 5;\\n\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Merge progress per table\",\n      \"type\": \"bargauge\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"center\",\n            \"displayMode\": \"auto\",\n            \"inspect\": false\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Database\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 78\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Table\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 200\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Elapsed\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 75\n              },\n              {\n                \"id\": \"unit\",\n                \"value\": \"s\"\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Progress\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 82\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Mutation\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 100\n              },\n              {\n                \"id\": \"unit\",\n                \"value\": \"bool\"\n              },\n              {\n                \"id\": \"color\",\n                \"value\": {\n                  \"mode\": \"thresholds\"\n                }\n              },\n              {\n                \"id\": \"thresholds\",\n                \"value\": {\n                  \"mode\": \"absolute\",\n                  \"steps\": [\n                    {\n                      \"color\": \"green\",\n                      \"value\": null\n                    },\n                    {\n                      \"color\": \"red\",\n                      \"value\": 0\n                    },\n                    {\n                      \"color\": \"semi-dark-green\",\n                      \"value\": 1\n                    }\n                  ]\n                }\n              },\n              {\n                \"id\": \"custom.displayMode\",\n                \"value\": \"color-background-solid\"\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Partition id\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 92\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Target path\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 396\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Num parts\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 90\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"total_size_compressed\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 185\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Columns written\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 198\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Total compressed size\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 166\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Compressed size\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 138\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 7,\n        \"w\": 19,\n        \"x\": 5,\n        \"y\": 15\n      },\n      \"id\": 11,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\"sum\"],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"9.0.1\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT concatAssumeInjective(database, '.', table) as db_table, round(elapsed, 1) \\\"elapsed\\\", round(100 * progress, 1) \\\"progress\\\", is_mutation, partition_id, result_part_path, source_part_paths, num_parts, formatReadableSize(total_size_bytes_compressed) \\\"total_size_compressed\\\", formatReadableSize(bytes_read_uncompressed) \\\"read_uncompressed\\\", formatReadableSize(bytes_written_uncompressed) \\\"written_uncompressed\\\", columns_written, formatReadableSize(memory_usage) \\\"memory_usage\\\", thread_id FROM system.merges WHERE database IN (${database:singlequote});\",\n          \"refId\": \"Merges\"\n        }\n      ],\n      \"title\": \"Current merges\",\n      \"transformations\": [\n        {\n          \"id\": \"organize\",\n          \"options\": {\n            \"excludeByName\": {\n              \"partition_id\": true,\n              \"read_uncompressed\": true,\n              \"source_part_paths\": true,\n              \"thread_id\": true,\n              \"written_uncompressed\": true\n            },\n            \"indexByName\": {},\n            \"renameByName\": {\n              \"columns_written\": \"Columns written\",\n              \"database\": \"Database\",\n              \"db_table\": \"Table\",\n              \"elapsed\": \"Elapsed\",\n              \"is_mutation\": \"Mutation\",\n              \"memory_usage\": \"Memory usage\",\n              \"num_parts\": \"Num parts\",\n              \"partition_id\": \"Partition id\",\n              \"progress\": \"Progress\",\n              \"result_part_path\": \"Target path\",\n              \"table\": \"Table\",\n              \"total_size_compressed\": \"Compressed size\"\n            }\n          }\n        }\n      ],\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"percentage\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"parts_remaining\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"min\",\n                \"value\": 0\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 7,\n        \"w\": 5,\n        \"x\": 0,\n        \"y\": 22\n      },\n      \"id\": 14,\n      \"options\": {\n        \"displayMode\": \"lcd\",\n        \"minVizHeight\": 10,\n        \"minVizWidth\": 0,\n        \"orientation\": \"horizontal\",\n        \"reduceOptions\": {\n          \"calcs\": [\"lastNotNull\"],\n          \"fields\": \"\",\n          \"values\": true\n        },\n        \"showUnfilled\": true\n      },\n      \"pluginVersion\": \"9.0.1\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT concatAssumeInjective(database, '.', table, ' - ', mutation_id) as db_table, length(parts_to_do_names) as parts_remaining FROM system.mutations WHERE parts_remaining > 0 AND database IN (${database:singlequote}) ORDER BY parts_remaining DESC;\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Mutations parts remaining\",\n      \"type\": \"bargauge\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"center\",\n            \"displayMode\": \"auto\",\n            \"inspect\": false\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Is completed\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 100\n              },\n              {\n                \"id\": \"unit\",\n                \"value\": \"bool\"\n              },\n              {\n                \"id\": \"color\",\n                \"value\": {\n                  \"mode\": \"thresholds\"\n                }\n              },\n              {\n                \"id\": \"thresholds\",\n                \"value\": {\n                  \"mode\": \"absolute\",\n                  \"steps\": [\n                    {\n                      \"color\": \"green\",\n                      \"value\": null\n                    },\n                    {\n                      \"color\": \"semi-dark-orange\",\n                      \"value\": 0\n                    },\n                    {\n                      \"color\": \"semi-dark-green\",\n                      \"value\": 1\n                    }\n                  ]\n                }\n              },\n              {\n                \"id\": \"custom.displayMode\",\n                \"value\": \"color-background-solid\"\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Database\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 86\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Table\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 194\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Fail time\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 201\n              },\n              {\n                \"id\": \"custom.align\",\n                \"value\": \"center\"\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Result\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 133\n              },\n              {\n                \"id\": \"mappings\",\n                \"value\": [\n                  {\n                    \"options\": {\n                      \"failure\": {\n                        \"color\": \"light-red\",\n                        \"index\": 1,\n                        \"text\": \"failure\"\n                      },\n                      \"success\": {\n                        \"color\": \"semi-dark-green\",\n                        \"index\": 0,\n                        \"text\": \"success\"\n                      }\n                    },\n                    \"type\": \"value\"\n                  }\n                ]\n              },\n              {\n                \"id\": \"custom.displayMode\",\n                \"value\": \"color-background\"\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Reason\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 163\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Command\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 300\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Mutation id\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 157\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Create time\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 194\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 7,\n        \"w\": 19,\n        \"x\": 5,\n        \"y\": 22\n      },\n      \"id\": 15,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\"sum\"],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"9.0.1\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT concatAssumeInjective(database, '.', table) as db_table, mutation_id, command, create_time, parts_to_do_names, is_done, latest_failed_part, if(latest_fail_time = '1970-01-01 00:00:00', 'success', 'failure') as success, if(latest_fail_time = '1970-01-01 00:00:00', '-', CAST(latest_fail_time, 'String')) as fail_time, latest_fail_reason FROM system.mutations WHERE database IN (${database:singlequote}) ORDER BY is_done ASC, create_time DESC LIMIT 10\",\n          \"refId\": \"Merges\"\n        }\n      ],\n      \"title\": \"Current mutations\",\n      \"transformations\": [\n        {\n          \"id\": \"organize\",\n          \"options\": {\n            \"excludeByName\": {\n              \"latest_failed_part\": true,\n              \"partition_id\": true,\n              \"parts_to_do_names\": true,\n              \"read_uncompressed\": true,\n              \"source_part_paths\": true,\n              \"thread_id\": true,\n              \"written_uncompressed\": true\n            },\n            \"indexByName\": {\n              \"command\": 5,\n              \"create_time\": 2,\n              \"db_table\": 0,\n              \"fail_time\": 9,\n              \"is_done\": 3,\n              \"latest_fail_reason\": 6,\n              \"latest_failed_part\": 8,\n              \"mutation_id\": 1,\n              \"parts_to_do_names\": 7,\n              \"success\": 4\n            },\n            \"renameByName\": {\n              \"columns_written\": \"Columns written\",\n              \"command\": \"Command\",\n              \"create_time\": \"Create time\",\n              \"database\": \"Database\",\n              \"db_table\": \"Table\",\n              \"elapsed\": \"Elapsed\",\n              \"fail_time\": \"Fail time\",\n              \"is_done\": \"Is completed\",\n              \"is_mutation\": \"Mutation\",\n              \"latest_fail_reason\": \"Reason\",\n              \"latest_fail_time\": \"Fail time\",\n              \"memory_usage\": \"Memory usage\",\n              \"mutation_id\": \"Mutation id\",\n              \"num_parts\": \"Num parts\",\n              \"partition_id\": \"Partition id\",\n              \"parts_to_do_names\": \"Pending parts\",\n              \"progress\": \"Progress\",\n              \"result_part_path\": \"Target path\",\n              \"success\": \"Result\",\n              \"table\": \"Table\",\n              \"total_size_compressed\": \"Compressed size\"\n            }\n          }\n        },\n        {\n          \"id\": \"convertFieldType\",\n          \"options\": {\n            \"conversions\": [],\n            \"fields\": {}\n          }\n        }\n      ],\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 5,\n        \"x\": 0,\n        \"y\": 29\n      },\n      \"id\": 18,\n      \"options\": {\n        \"displayMode\": \"lcd\",\n        \"minVizHeight\": 10,\n        \"minVizWidth\": 0,\n        \"orientation\": \"horizontal\",\n        \"reduceOptions\": {\n          \"calcs\": [\"lastNotNull\"],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"showUnfilled\": true\n      },\n      \"pluginVersion\": \"9.0.1\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT concatAssumeInjective(database, '.', table) as db_table, queue_size FROM system.replicas WHERE database IN (${database:singlequote}) ORDER BY absolute_delay DESC LIMIT 10\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Replicated tables by delay\",\n      \"transformations\": [\n        {\n          \"id\": \"organize\",\n          \"options\": {\n            \"excludeByName\": {},\n            \"indexByName\": {},\n            \"renameByName\": {\n              \"absolute_delay\": \"Delay\",\n              \"db_table\": \"Table\",\n              \"inserts_in_queue\": \"Inserts in queue\",\n              \"is_leader\": \"Leader\",\n              \"is_readonly\": \"Readonly\",\n              \"merges_in_queue\": \"Merges in queue\",\n              \"queue_size\": \"Queue size\"\n            }\n          }\n        }\n      ],\n      \"type\": \"bargauge\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\",\n            \"inspect\": false\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Table\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 200\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Leader\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 122\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Readonly\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 138\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Delay\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 108\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Queue size\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 113\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 19,\n        \"x\": 5,\n        \"y\": 29\n      },\n      \"id\": 17,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\"sum\"],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"9.0.1\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT concatAssumeInjective(database, '.', table) as db_table, is_leader, is_readonly, absolute_delay, queue_size, inserts_in_queue, merges_in_queue FROM system.replicas WHERE database IN (${database:singlequote}) ORDER BY absolute_delay DESC LIMIT 10\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Replicated tables by delay\",\n      \"transformations\": [\n        {\n          \"id\": \"organize\",\n          \"options\": {\n            \"excludeByName\": {},\n            \"indexByName\": {},\n            \"renameByName\": {\n              \"absolute_delay\": \"Delay\",\n              \"db_table\": \"Table\",\n              \"inserts_in_queue\": \"Inserts in queue\",\n              \"is_leader\": \"Leader\",\n              \"is_readonly\": \"Readonly\",\n              \"merges_in_queue\": \"Merges in queue\",\n              \"queue_size\": \"Queue size\"\n            }\n          }\n        }\n      ],\n      \"type\": \"table\"\n    }\n  ],\n  \"schemaVersion\": 36,\n  \"style\": \"dark\",\n  \"tags\": [],\n  \"templating\": {\n    \"list\": [\n      {\n        \"current\": {},\n        \"hide\": 0,\n        \"includeAll\": false,\n        \"label\": \"ClickHouse instance\",\n        \"multi\": false,\n        \"name\": \"datasource\",\n        \"options\": [],\n        \"query\": \"grafana-clickhouse-datasource\",\n        \"queryValue\": \"\",\n        \"refresh\": 1,\n        \"regex\": \"\",\n        \"skipUrlSync\": false,\n        \"type\": \"datasource\"\n      },\n      {\n        \"current\": {},\n        \"datasource\": {\n          \"type\": \"grafana-clickhouse-datasource\",\n          \"uid\": \"${datasource}\"\n        },\n        \"definition\": \"SELECT name FROM system.databases;\",\n        \"hide\": 0,\n        \"includeAll\": true,\n        \"label\": \"Database\",\n        \"multi\": false,\n        \"name\": \"database\",\n        \"options\": [],\n        \"query\": \"SELECT name FROM system.databases;\",\n        \"refresh\": 1,\n        \"regex\": \"\",\n        \"skipUrlSync\": false,\n        \"sort\": 0,\n        \"type\": \"query\"\n      },\n      {\n        \"datasource\": {\n          \"type\": \"grafana-clickhouse-datasource\",\n          \"uid\": \"y-Ka8y37k\"\n        },\n        \"filters\": [],\n        \"hide\": 0,\n        \"name\": \"filters\",\n        \"skipUrlSync\": false,\n        \"type\": \"adhoc\"\n      }\n    ]\n  },\n  \"time\": {\n    \"from\": \"now-6h\",\n    \"to\": \"now\"\n  },\n  \"timepicker\": {},\n  \"timezone\": \"\",\n  \"title\": \"ClickHouse - Cluster Analysis\",\n  \"uid\": \"_hAsuzBnz\",\n  \"version\": 6,\n  \"weekStart\": \"\"\n}\n"
  },
  {
    "path": "src/dashboards/data-analysis.json",
    "content": "{\n  \"__inputs\": [\n    {\n      \"name\": \"DS_GRAFANA-CLICKHOUSE-DATASOURCE\",\n      \"label\": \"grafana-clickhouse-datasource\",\n      \"description\": \"\",\n      \"type\": \"datasource\",\n      \"pluginId\": \"grafana-clickhouse-datasource\",\n      \"pluginName\": \"ClickHouse\"\n    }\n  ],\n  \"__elements\": {},\n  \"__requires\": [\n    {\n      \"type\": \"panel\",\n      \"id\": \"barchart\",\n      \"name\": \"Bar chart\",\n      \"version\": \"\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"bargauge\",\n      \"name\": \"Bar gauge\",\n      \"version\": \"\"\n    },\n    {\n      \"type\": \"grafana\",\n      \"id\": \"grafana\",\n      \"name\": \"Grafana\",\n      \"version\": \"12.1.1\"\n    },\n    {\n      \"type\": \"datasource\",\n      \"id\": \"grafana-clickhouse-datasource\",\n      \"name\": \"ClickHouse\",\n      \"version\": \"4.10.2\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"stat\",\n      \"name\": \"Stat\",\n      \"version\": \"\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"table\",\n      \"name\": \"Table\",\n      \"version\": \"\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"timeseries\",\n      \"name\": \"Time series\",\n      \"version\": \"\"\n    }\n  ],\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": {\n          \"type\": \"datasource\",\n          \"uid\": \"grafana\"\n        },\n        \"enable\": true,\n        \"hide\": true,\n        \"iconColor\": \"rgba(0, 211, 255, 1)\",\n        \"name\": \"Annotations & Alerts\",\n        \"target\": {\n          \"limit\": 100,\n          \"matchAny\": false,\n          \"tags\": [],\n          \"type\": \"dashboard\"\n        },\n        \"type\": \"dashboard\"\n      }\n    ]\n  },\n  \"editable\": true,\n  \"fiscalYearStartMonth\": 0,\n  \"graphTooltip\": 0,\n  \"id\": null,\n  \"links\": [],\n  \"panels\": [\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"semi-dark-blue\",\n                \"value\": 0\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Version\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"unit\",\n                \"value\": \"string\"\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 3,\n        \"x\": 0,\n        \"y\": 0\n      },\n      \"id\": 4,\n      \"options\": {\n        \"colorMode\": \"background\",\n        \"graphMode\": \"area\",\n        \"justifyMode\": \"center\",\n        \"orientation\": \"horizontal\",\n        \"percentChangeColorMode\": \"standard\",\n        \"reduceOptions\": {\n          \"calcs\": [\"lastNotNull\"],\n          \"fields\": \"/.*/\",\n          \"values\": false\n        },\n        \"showPercentChange\": false,\n        \"textMode\": \"auto\",\n        \"wideLayout\": true\n      },\n      \"pluginVersion\": \"12.1.1\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${DS_GRAFANA-CLICKHOUSE-DATASOURCE}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT version()\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"transformations\": [\n        {\n          \"id\": \"convertFieldType\",\n          \"options\": {\n            \"conversions\": [\n              {\n                \"destinationType\": \"string\",\n                \"targetField\": \"Version\"\n              }\n            ],\n            \"fields\": {}\n          }\n        },\n        {\n          \"id\": \"organize\",\n          \"options\": {\n            \"excludeByName\": {},\n            \"indexByName\": {},\n            \"renameByName\": {\n              \"version()\": \"Version\"\n            }\n          }\n        }\n      ],\n      \"type\": \"stat\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"semi-dark-blue\",\n                \"value\": 0\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"uptime\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"unit\",\n                \"value\": \"s\"\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 3,\n        \"x\": 3,\n        \"y\": 0\n      },\n      \"id\": 2,\n      \"options\": {\n        \"colorMode\": \"background\",\n        \"graphMode\": \"area\",\n        \"justifyMode\": \"center\",\n        \"orientation\": \"auto\",\n        \"percentChangeColorMode\": \"standard\",\n        \"reduceOptions\": {\n          \"calcs\": [\"lastNotNull\"],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"showPercentChange\": false,\n        \"textMode\": \"auto\",\n        \"wideLayout\": true\n      },\n      \"pluginVersion\": \"12.1.1\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${DS_GRAFANA-CLICKHOUSE-DATASOURCE}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT uptime() as uptime\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"transformations\": [\n        {\n          \"id\": \"organize\",\n          \"options\": {\n            \"excludeByName\": {},\n            \"indexByName\": {},\n            \"renameByName\": {\n              \"uptime\": \"Server uptime\"\n            }\n          }\n        }\n      ],\n      \"type\": \"stat\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"light-blue\",\n                \"value\": 0\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 3,\n        \"x\": 6,\n        \"y\": 0\n      },\n      \"id\": 22,\n      \"options\": {\n        \"colorMode\": \"background\",\n        \"graphMode\": \"area\",\n        \"justifyMode\": \"center\",\n        \"orientation\": \"auto\",\n        \"percentChangeColorMode\": \"standard\",\n        \"reduceOptions\": {\n          \"calcs\": [\"lastNotNull\"],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"showPercentChange\": false,\n        \"textMode\": \"value_and_name\",\n        \"wideLayout\": true\n      },\n      \"pluginVersion\": \"12.1.1\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${DS_GRAFANA-CLICKHOUSE-DATASOURCE}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT  sum(total_rows) as \\\"Total rows\\\" FROM system.tables WHERE database IN (${database}) AND name IN (${table})\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"type\": \"stat\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"light-blue\",\n                \"value\": 0\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 3,\n        \"x\": 9,\n        \"y\": 0\n      },\n      \"id\": 23,\n      \"options\": {\n        \"colorMode\": \"background\",\n        \"graphMode\": \"area\",\n        \"justifyMode\": \"center\",\n        \"orientation\": \"auto\",\n        \"percentChangeColorMode\": \"standard\",\n        \"reduceOptions\": {\n          \"calcs\": [\"lastNotNull\"],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"showPercentChange\": false,\n        \"textMode\": \"value_and_name\",\n        \"wideLayout\": true\n      },\n      \"pluginVersion\": \"12.1.1\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${DS_GRAFANA-CLICKHOUSE-DATASOURCE}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT count() as \\\"Total columns\\\" FROM system.columns WHERE database IN (${database}) AND table IN (${table})\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"type\": \"stat\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"cellOptions\": {\n              \"type\": \"auto\"\n            },\n            \"inspect\": false\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"percentage\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": 0\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Used\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"unit\",\n                \"value\": \"percentunit\"\n              },\n              {\n                \"id\": \"custom.cellOptions\",\n                \"value\": {\n                  \"mode\": \"lcd\",\n                  \"type\": \"gauge\"\n                }\n              },\n              {\n                \"id\": \"max\",\n                \"value\": 1\n              },\n              {\n                \"id\": \"min\",\n                \"value\": 0\n              },\n              {\n                \"id\": \"thresholds\",\n                \"value\": {\n                  \"mode\": \"absolute\",\n                  \"steps\": [\n                    {\n                      \"color\": \"green\",\n                      \"value\": 0\n                    },\n                    {\n                      \"color\": \"semi-dark-green\",\n                      \"value\": 0.6\n                    },\n                    {\n                      \"color\": \"#EAB839\",\n                      \"value\": 0.7\n                    },\n                    {\n                      \"color\": \"semi-dark-orange\",\n                      \"value\": 0.75\n                    },\n                    {\n                      \"color\": \"semi-dark-red\",\n                      \"value\": 0.8\n                    }\n                  ]\n                }\n              },\n              {\n                \"id\": \"custom.width\",\n                \"value\": 357\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Name\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 74\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Free space\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 96\n              },\n              {\n                \"id\": \"unit\",\n                \"value\": \"bytes\"\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Path\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 277\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Total space\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 103\n              },\n              {\n                \"id\": \"unit\",\n                \"value\": \"bytes\"\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 0\n      },\n      \"id\": 9,\n      \"options\": {\n        \"cellHeight\": \"sm\",\n        \"footer\": {\n          \"countRows\": false,\n          \"fields\": \"\",\n          \"reducer\": [\"sum\"],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"12.1.1\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${DS_GRAFANA-CLICKHOUSE-DATASOURCE}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 100,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.10.2\",\n          \"queryType\": \"table\",\n          \"rawSql\": \"SELECT\\n    name as Name,\\n    path as Path,\\n    free_space as Free,\\n    total_space as Total,\\n    1 - free_space/total_space as Used\\nFROM system.disks\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Disk usage\",\n      \"transformations\": [\n        {\n          \"id\": \"organize\",\n          \"options\": {\n            \"excludeByName\": {},\n            \"indexByName\": {},\n            \"renameByName\": {\n              \"Free\": \"Free space\",\n              \"Name\": \"\",\n              \"Path\": \"\",\n              \"Total\": \"Total space\",\n              \"Used\": \"\"\n            }\n          }\n        }\n      ],\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"continuous-GrYlRd\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": 0\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 9,\n        \"w\": 8,\n        \"x\": 0,\n        \"y\": 6\n      },\n      \"id\": 25,\n      \"options\": {\n        \"displayMode\": \"lcd\",\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": false\n        },\n        \"maxVizHeight\": 300,\n        \"minVizHeight\": 10,\n        \"minVizWidth\": 0,\n        \"namePlacement\": \"auto\",\n        \"orientation\": \"horizontal\",\n        \"reduceOptions\": {\n          \"calcs\": [],\n          \"fields\": \"\",\n          \"values\": true\n        },\n        \"showUnfilled\": true,\n        \"sizing\": \"auto\",\n        \"valueMode\": \"color\"\n      },\n      \"pluginVersion\": \"12.1.1\",\n      \"targets\": [\n        {\n          \"builderOptions\": {\n            \"fields\": [],\n            \"limit\": 100,\n            \"mode\": \"list\"\n          },\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${DS_GRAFANA-CLICKHOUSE-DATASOURCE}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT concatAssumeInjective(table.database, '.', name) as name,\\n       table_stats.total_rows as total_rows\\nFROM system.tables table\\n         LEFT JOIN ( SELECT table,\\n       database,\\n       sum(rows)                  as total_rows\\nFROM system.parts\\nWHERE table IN (${table}) AND active AND database IN (${database}) \\nGROUP BY table, database\\n ) AS table_stats ON table.name = table_stats.table AND table.database = table_stats.database\\nORDER BY total_rows DESC\\nLIMIT 10\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Top tables by rows\",\n      \"type\": \"bargauge\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"continuous-GrYlRd\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": 0\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 9,\n        \"w\": 7,\n        \"x\": 8,\n        \"y\": 6\n      },\n      \"id\": 26,\n      \"options\": {\n        \"displayMode\": \"lcd\",\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": false\n        },\n        \"maxVizHeight\": 300,\n        \"minVizHeight\": 10,\n        \"minVizWidth\": 0,\n        \"namePlacement\": \"auto\",\n        \"orientation\": \"horizontal\",\n        \"reduceOptions\": {\n          \"calcs\": [],\n          \"fields\": \"\",\n          \"values\": true\n        },\n        \"showUnfilled\": true,\n        \"sizing\": \"auto\",\n        \"valueMode\": \"color\"\n      },\n      \"pluginVersion\": \"12.1.1\",\n      \"targets\": [\n        {\n          \"builderOptions\": {\n            \"fields\": [],\n            \"limit\": 100,\n            \"mode\": \"list\"\n          },\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${DS_GRAFANA-CLICKHOUSE-DATASOURCE}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT concatAssumeInjective(table.database, '.', name) as name,\\n       col_stats.col_count as total_columns\\nFROM system.tables table\\n         LEFT JOIN (SELECT database, table, count() as col_count FROM system.columns  GROUP BY table, database) as col_stats\\n                   ON table.name = col_stats.table AND col_stats.database = table.database\\nWHERE database IN (${database}) AND name != '' AND table IN (${table}) AND name != '' ORDER BY total_columns DESC LIMIT 10;\\n\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Top tables by columns\",\n      \"type\": \"bargauge\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisGridShow\": false,\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"axisSoftMin\": 0,\n            \"fillOpacity\": 61,\n            \"gradientMode\": \"hue\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineWidth\": 1,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": 0\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 9,\n        \"w\": 4,\n        \"x\": 15,\n        \"y\": 6\n      },\n      \"id\": 12,\n      \"options\": {\n        \"barRadius\": 0,\n        \"barWidth\": 1,\n        \"fullHighlight\": false,\n        \"groupWidth\": 0.7,\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"orientation\": \"auto\",\n        \"showValue\": \"auto\",\n        \"stacking\": \"none\",\n        \"text\": {},\n        \"tooltip\": {\n          \"hideZeros\": false,\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        },\n        \"xTickLabelRotation\": 0,\n        \"xTickLabelSpacing\": 0\n      },\n      \"pluginVersion\": \"12.1.1\",\n      \"targets\": [\n        {\n          \"builderOptions\": {\n            \"fields\": [],\n            \"limit\": 100,\n            \"mode\": \"list\"\n          },\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${DS_GRAFANA-CLICKHOUSE-DATASOURCE}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT engine, count() \\\"Number of databases\\\" FROM system.databases WHERE name IN (${database}) GROUP BY engine \",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Database engines\",\n      \"type\": \"barchart\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisGridShow\": false,\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"axisSoftMin\": 0,\n            \"fillOpacity\": 61,\n            \"gradientMode\": \"hue\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineWidth\": 1,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": 0\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 9,\n        \"w\": 5,\n        \"x\": 19,\n        \"y\": 6\n      },\n      \"id\": 11,\n      \"options\": {\n        \"barRadius\": 0,\n        \"barWidth\": 0.97,\n        \"fullHighlight\": false,\n        \"groupWidth\": 0.7,\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"orientation\": \"auto\",\n        \"showValue\": \"auto\",\n        \"stacking\": \"none\",\n        \"tooltip\": {\n          \"hideZeros\": false,\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        },\n        \"xTickLabelRotation\": 0,\n        \"xTickLabelSpacing\": 0\n      },\n      \"pluginVersion\": \"12.1.1\",\n      \"targets\": [\n        {\n          \"builderOptions\": {\n            \"fields\": [],\n            \"limit\": 100,\n            \"mode\": \"list\"\n          },\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${DS_GRAFANA-CLICKHOUSE-DATASOURCE}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT engine, count() \\\"Number of tables\\\" FROM system.tables WHERE database IN (${database}) AND notLike(engine,'System%')  AND name IN (${table}) GROUP BY engine ORDER BY count() DESC\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Table engines\",\n      \"type\": \"barchart\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"custom\": {\n            \"align\": \"auto\",\n            \"cellOptions\": {\n              \"type\": \"auto\"\n            },\n            \"inspect\": false\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"percentage\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": 0\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Engine\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 200\n              },\n              {\n                \"id\": \"custom.filterable\",\n                \"value\": true\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Number of tables\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 128\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Total rows\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 113\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Column count\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 126\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Part count\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 98\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Database\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.filterable\",\n                \"value\": true\n              },\n              {\n                \"id\": \"custom.width\",\n                \"value\": 205\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Partition count\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 143\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Size on disk\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 203\n              },\n              {\n                \"id\": \"unit\",\n                \"value\": \"bytes\"\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Uncompressed size\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"unit\",\n                \"value\": \"bytes\"\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 10,\n        \"w\": 15,\n        \"x\": 0,\n        \"y\": 15\n      },\n      \"id\": 6,\n      \"options\": {\n        \"cellHeight\": \"sm\",\n        \"footer\": {\n          \"countRows\": false,\n          \"fields\": \"\",\n          \"reducer\": [\"sum\"],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": [\n          {\n            \"desc\": true,\n            \"displayName\": \"Uncompressed size\"\n          }\n        ]\n      },\n      \"pluginVersion\": \"12.1.1\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${DS_GRAFANA-CLICKHOUSE-DATASOURCE}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 100,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.10.2\",\n          \"queryType\": \"table\",\n          \"rawSql\": \"SELECT name,\\n       engine,\\n       tables,\\n       partitions,\\n       parts,\\n       bytes_on_disk            \\\"disk_size\\\",\\n       col_count,\\n       total_rows,\\n       data_uncompressed_bytes as \\\"uncompressed_size\\\"\\nFROM system.databases db\\n         LEFT JOIN ( SELECT database,\\n                            uniq(table)                   \\\"tables\\\",\\n                            uniq(table, partition)        \\\"partitions\\\",\\n                            count()                    AS parts,\\n                            sum(bytes_on_disk)            \\\"bytes_on_disk\\\",\\n                            sum(data_uncompressed_bytes) as \\\"data_uncompressed_bytes\\\",\\n                            sum(rows) as total_rows,\\n                            max(col_count) as \\\"col_count\\\"\\n                     FROM system.parts AS parts\\n                               JOIN (SELECT database, count() as col_count\\n                                         FROM system.columns\\n                                         WHERE database IN (${database}) AND table IN (${table})\\n                                         GROUP BY database) as col_stats\\n                                        ON parts.database = col_stats.database\\n                     WHERE database IN (${database}) AND active AND table IN (${table})\\n                     GROUP BY database) AS db_stats ON db.name = db_stats.database\\nWHERE database IN (${database}) AND lower(name) != 'information_schema'\\nORDER BY bytes_on_disk DESC\\nLIMIT 10;\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Database summary\",\n      \"transformations\": [\n        {\n          \"id\": \"organize\",\n          \"options\": {\n            \"excludeByName\": {},\n            \"indexByName\": {\n              \"col_count\": 4,\n              \"disk_size\": 7,\n              \"engine\": 1,\n              \"name\": 0,\n              \"partitions\": 5,\n              \"parts\": 6,\n              \"tables\": 2,\n              \"total_rows\": 3,\n              \"uncompressed_size\": 8\n            },\n            \"renameByName\": {\n              \"col_count\": \"Column count\",\n              \"disk_size\": \"Size on disk\",\n              \"engine\": \"Engine\",\n              \"name\": \"Database\",\n              \"partitions\": \"Partition count\",\n              \"parts\": \"Part count\",\n              \"tables\": \"Number of tables\",\n              \"total_rows\": \"Total rows\",\n              \"uncompressed_size\": \"Uncompressed size\"\n            }\n          }\n        }\n      ],\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"cellOptions\": {\n              \"type\": \"auto\"\n            },\n            \"inspect\": false\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": 0\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Source\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 85\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Type\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 64\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Status\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 71\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 10,\n        \"w\": 9,\n        \"x\": 15,\n        \"y\": 15\n      },\n      \"id\": 14,\n      \"options\": {\n        \"cellHeight\": \"sm\",\n        \"footer\": {\n          \"countRows\": false,\n          \"fields\": \"\",\n          \"reducer\": [\"sum\"],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"12.1.1\",\n      \"targets\": [\n        {\n          \"builderOptions\": {\n            \"fields\": [],\n            \"limit\": 100,\n            \"mode\": \"list\"\n          },\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${DS_GRAFANA-CLICKHOUSE-DATASOURCE}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT source, type, status, count() \\\"count\\\" FROM system.dictionaries GROUP BY source, type, status ORDER BY status DESC, source\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Dictionaries\",\n      \"transformations\": [\n        {\n          \"id\": \"organize\",\n          \"options\": {\n            \"excludeByName\": {},\n            \"indexByName\": {},\n            \"renameByName\": {\n              \"count\": \"Usages\",\n              \"source\": \"Source\",\n              \"status\": \"Status\",\n              \"type\": \"Type\"\n            }\n          }\n        }\n      ],\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"custom\": {\n            \"align\": \"auto\",\n            \"cellOptions\": {\n              \"type\": \"auto\"\n            },\n            \"filterable\": false,\n            \"inspect\": false\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"percentage\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": 0\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Engine\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 86\n              },\n              {\n                \"id\": \"custom.filterable\",\n                \"value\": true\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Total rows\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 116\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Column count\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 156\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Partition count\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 138\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Part count\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 113\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Size on disk\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 145\n              },\n              {\n                \"id\": \"unit\",\n                \"value\": \"bytes\"\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Row Count\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 109\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Database\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 286\n              },\n              {\n                \"id\": \"custom.filterable\",\n                \"value\": true\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Table\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 236\n              },\n              {\n                \"id\": \"custom.filterable\",\n                \"value\": true\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Uncompressed size\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 171\n              },\n              {\n                \"id\": \"unit\",\n                \"value\": \"bytes\"\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 10,\n        \"w\": 15,\n        \"x\": 0,\n        \"y\": 25\n      },\n      \"id\": 7,\n      \"options\": {\n        \"cellHeight\": \"sm\",\n        \"footer\": {\n          \"countRows\": false,\n          \"fields\": \"\",\n          \"reducer\": [\"sum\"],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": [\n          {\n            \"desc\": true,\n            \"displayName\": \"Size on disk\"\n          }\n        ]\n      },\n      \"pluginVersion\": \"12.1.1\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${DS_GRAFANA-CLICKHOUSE-DATASOURCE}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 100,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.10.2\",\n          \"queryType\": \"table\",\n          \"rawSql\": \"SELECT name,\\n       table.database,\\n       engine,\\n       partitions,\\n       parts,\\n       bytes_on_disk            \\\"disk_size\\\",\\n       col_count,\\n       table_stats.total_rows,\\n       data_uncompressed_bytes as \\\"uncompressed_size\\\"\\nFROM system.tables table\\n         LEFT JOIN ( SELECT table,\\n       database,\\n       uniq(table, partition)        \\\"partitions\\\",\\n       count()                    AS parts,\\n       sum(bytes_on_disk)            \\\"bytes_on_disk\\\",\\n       sum(data_uncompressed_bytes) as \\\"data_uncompressed_bytes\\\",\\n       sum(rows)                  as total_rows,\\n                            max(col_count) as col_count\\nFROM system.parts as parts\\n         LEFT JOIN (SELECT database, table, count() as col_count FROM system.columns GROUP BY table, database) as col_stats\\n                   ON parts.table = col_stats.table AND col_stats.database = parts.database\\nWHERE active\\nGROUP BY table, database\\n ) AS table_stats ON table.name = table_stats.table AND table.database = table_stats.database\\nWHERE database IN (${database}) AND lower(name) != 'information_schema' AND table IN (${table})\\nORDER BY bytes_on_disk DESC\\nLIMIT 1000\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Table summary\",\n      \"transformations\": [\n        {\n          \"id\": \"organize\",\n          \"options\": {\n            \"excludeByName\": {},\n            \"indexByName\": {\n              \"col_count\": 8,\n              \"database\": 0,\n              \"disk_size\": 6,\n              \"engine\": 2,\n              \"name\": 1,\n              \"partitions\": 4,\n              \"parts\": 5,\n              \"table_stats.total_rows\": 3,\n              \"uncompressed_size\": 7\n            },\n            \"renameByName\": {\n              \"col_count\": \"Column count\",\n              \"col_stats.col_count\": \"Column count\",\n              \"database\": \"Database\",\n              \"disk_size\": \"Size on disk\",\n              \"engine\": \"Engine\",\n              \"name\": \"Table\",\n              \"partitions\": \"Partition count\",\n              \"parts\": \"Part count\",\n              \"table.database\": \"Database\",\n              \"table_stats.total_rows\": \"Row Count\",\n              \"tables\": \"Number of tables\",\n              \"total_rows\": \"Total rows\",\n              \"uncompressed_size\": \"Uncompressed size\"\n            }\n          }\n        }\n      ],\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"cellOptions\": {\n              \"type\": \"auto\"\n            },\n            \"inspect\": false\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": 0\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Database\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 94\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Table\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 116\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Partition id\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 103\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Disk\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 104\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Reason\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 125\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Min block number\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 141\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Max block number\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 139\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Level\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 89\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Name\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 168\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 10,\n        \"w\": 9,\n        \"x\": 15,\n        \"y\": 25\n      },\n      \"id\": 28,\n      \"options\": {\n        \"cellHeight\": \"sm\",\n        \"footer\": {\n          \"countRows\": false,\n          \"fields\": \"\",\n          \"reducer\": [\"sum\"],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"12.1.1\",\n      \"targets\": [\n        {\n          \"builderOptions\": {\n            \"fields\": [],\n            \"limit\": 100,\n            \"mode\": \"list\"\n          },\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${DS_GRAFANA-CLICKHOUSE-DATASOURCE}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT database, table, partition_id, name, disk, level FROM system.detached_parts WHERE database IN (${database}) AND table IN (${table})\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Detached partitions\",\n      \"transformations\": [\n        {\n          \"id\": \"organize\",\n          \"options\": {\n            \"excludeByName\": {},\n            \"indexByName\": {},\n            \"renameByName\": {\n              \"database\": \"Database\",\n              \"disk\": \"Disk\",\n              \"level\": \"Level\",\n              \"max_block_number\": \"Max block number\",\n              \"min_block_number\": \"Min block number\",\n              \"name\": \"Name\",\n              \"partition_id\": \"Partition id\",\n              \"reason\": \"Reason\",\n              \"table\": \"Table\"\n            }\n          }\n        }\n      ],\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"Rows in part\",\n            \"axisPlacement\": \"auto\",\n            \"axisWidth\": 3,\n            \"barAlignment\": 0,\n            \"barWidthFactor\": 0.6,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 10,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineStyle\": {\n              \"fill\": \"solid\"\n            },\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"always\",\n            \"spanNulls\": true,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": 0\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 11,\n        \"w\": 15,\n        \"x\": 0,\n        \"y\": 35\n      },\n      \"id\": 18,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"right\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"hideZeros\": false,\n          \"mode\": \"multi\",\n          \"sort\": \"none\"\n        }\n      },\n      \"pluginVersion\": \"12.1.1\",\n      \"targets\": [\n        {\n          \"builderOptions\": {\n            \"fields\": [],\n            \"limit\": 100,\n            \"mode\": \"list\"\n          },\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${DS_GRAFANA-CLICKHOUSE-DATASOURCE}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT modification_time as timestamp, concatAssumeInjective(database, '.', table) as table, rows FROM system.parts parts WHERE parts.database IN ($database) AND parts.table IN (${table})  AND $__timeFilter(modification_time) ORDER BY modification_time ASC\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Parts over time with row count\",\n      \"transformations\": [\n        {\n          \"id\": \"organize\",\n          \"options\": {\n            \"excludeByName\": {\n              \"bytes_on_disk\": true,\n              \"name\": true,\n              \"rows\": false\n            },\n            \"indexByName\": {\n              \"rows\": 2,\n              \"table\": 1,\n              \"timestamp\": 0\n            },\n            \"renameByName\": {\n              \"rows\": \"rows in part\"\n            }\n          }\n        },\n        {\n          \"id\": \"prepareTimeSeries\",\n          \"options\": {\n            \"format\": \"many\"\n          }\n        }\n      ],\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"continuous-GrYlRd\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"percentage\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": 0\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 11,\n        \"w\": 9,\n        \"x\": 15,\n        \"y\": 35\n      },\n      \"id\": 16,\n      \"options\": {\n        \"displayMode\": \"gradient\",\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": false\n        },\n        \"maxVizHeight\": 300,\n        \"minVizHeight\": 10,\n        \"minVizWidth\": 0,\n        \"namePlacement\": \"auto\",\n        \"orientation\": \"horizontal\",\n        \"reduceOptions\": {\n          \"calcs\": [\"lastNotNull\"],\n          \"fields\": \"\",\n          \"values\": true\n        },\n        \"showUnfilled\": true,\n        \"sizing\": \"auto\",\n        \"valueMode\": \"color\"\n      },\n      \"pluginVersion\": \"12.1.1\",\n      \"targets\": [\n        {\n          \"builderOptions\": {\n            \"fields\": [],\n            \"limit\": 100,\n            \"mode\": \"list\"\n          },\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${DS_GRAFANA-CLICKHOUSE-DATASOURCE}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT concatAssumeInjective(database, '.', table) as dbTable, count() \\\"partitions\\\", sum(part_count) \\\"parts\\\", max(part_count) \\\"max_parts_per_partition\\\"\\nFROM ( SELECT database, table, count() \\\"part_count\\\"\\n       FROM system.parts\\n       WHERE database IN (${database}) AND active AND table IN (${table})\\n       GROUP BY database, table, partition ) partitions\\nGROUP BY database, table\\nORDER BY max_parts_per_partition DESC\\nLIMIT 10\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Max parts per partition\",\n      \"transformations\": [\n        {\n          \"id\": \"organize\",\n          \"options\": {\n            \"excludeByName\": {\n              \"partitions\": true,\n              \"parts\": true\n            },\n            \"indexByName\": {},\n            \"renameByName\": {}\n          }\n        }\n      ],\n      \"type\": \"bargauge\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"cellOptions\": {\n              \"type\": \"auto\"\n            },\n            \"inspect\": false\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": 0\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Active\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.cellOptions\",\n                \"value\": {\n                  \"mode\": \"basic\",\n                  \"type\": \"color-background\"\n                }\n              },\n              {\n                \"id\": \"mappings\",\n                \"value\": [\n                  {\n                    \"options\": {\n                      \"false\": {\n                        \"color\": \"light-red\",\n                        \"index\": 1\n                      },\n                      \"true\": {\n                        \"color\": \"light-green\",\n                        \"index\": 0\n                      }\n                    },\n                    \"type\": \"value\"\n                  }\n                ]\n              },\n              {\n                \"id\": \"custom.width\",\n                \"value\": 77\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"level\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 69\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"path\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 286\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Database\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 88\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Table\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 111\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Partition Name\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 226\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"disk_name\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 109\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"marks\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 65\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"rows\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 87\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"bytes_on_disk\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 112\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 11,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 46\n      },\n      \"id\": 20,\n      \"options\": {\n        \"cellHeight\": \"sm\",\n        \"footer\": {\n          \"countRows\": false,\n          \"fields\": \"\",\n          \"reducer\": [\"sum\"],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": []\n      },\n      \"pluginVersion\": \"12.1.1\",\n      \"targets\": [\n        {\n          \"builderOptions\": {\n            \"fields\": [],\n            \"limit\": 100,\n            \"mode\": \"list\"\n          },\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${DS_GRAFANA-CLICKHOUSE-DATASOURCE}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT database, table, partition_id, modification_time, name, part_type, active, level, disk_name, path, marks, rows, bytes_on_disk, refcount, min_block_number, max_block_number FROM system.parts WHERE database IN (${database}) AND table IN (${table})  AND modification_time > now() - INTERVAL 3 MINUTE ORDER BY modification_time DESC\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Recent part analysis\",\n      \"transformations\": [\n        {\n          \"id\": \"organize\",\n          \"options\": {\n            \"excludeByName\": {\n              \"is_frozen\": true,\n              \"marks\": false,\n              \"max_date\": true,\n              \"min_date\": true,\n              \"partition_id\": true,\n              \"refcount\": true\n            },\n            \"indexByName\": {},\n            \"renameByName\": {\n              \"active\": \"Active\",\n              \"database\": \"Database\",\n              \"engine\": \"Engine\",\n              \"name\": \"Partition Name\",\n              \"part_type\": \"Partition Type\",\n              \"partition_id\": \"Partition Id\",\n              \"table\": \"Table\"\n            }\n          }\n        },\n        {\n          \"id\": \"convertFieldType\",\n          \"options\": {\n            \"conversions\": [\n              {\n                \"destinationType\": \"boolean\",\n                \"targetField\": \"Active\"\n              }\n            ],\n            \"fields\": {}\n          }\n        }\n      ],\n      \"type\": \"table\"\n    }\n  ],\n  \"schemaVersion\": 41,\n  \"tags\": [],\n  \"templating\": {\n    \"list\": [\n      {\n        \"current\": {},\n        \"includeAll\": false,\n        \"label\": \"ClickHouse instance\",\n        \"name\": \"datasource\",\n        \"options\": [],\n        \"query\": \"grafana-clickhouse-datasource\",\n        \"refresh\": 1,\n        \"regex\": \"\",\n        \"type\": \"datasource\"\n      },\n      {\n        \"current\": {},\n        \"datasource\": {\n          \"type\": \"grafana-clickhouse-datasource\",\n          \"uid\": \"${datasource}\"\n        },\n        \"definition\": \"SELECT name FROM system.databases;\\n\",\n        \"includeAll\": true,\n        \"label\": \"Database\",\n        \"multi\": true,\n        \"name\": \"database\",\n        \"options\": [],\n        \"query\": \"SELECT name FROM system.databases;\\n\",\n        \"refresh\": 1,\n        \"regex\": \"\",\n        \"type\": \"query\"\n      },\n      {\n        \"current\": {},\n        \"datasource\": {\n          \"type\": \"grafana-clickhouse-datasource\",\n          \"uid\": \"${DS_GRAFANA-CLICKHOUSE-DATASOURCE}\"\n        },\n        \"definition\": \"SELECT name FROM system.tables WHERE database IN (${database})\",\n        \"includeAll\": true,\n        \"label\": \"Table\",\n        \"multi\": true,\n        \"name\": \"table\",\n        \"options\": [],\n        \"query\": \"SELECT name FROM system.tables WHERE database IN (${database})\",\n        \"refresh\": 1,\n        \"regex\": \"\",\n        \"type\": \"query\"\n      }\n    ]\n  },\n  \"time\": {\n    \"from\": \"now-24h\",\n    \"to\": \"now\"\n  },\n  \"timepicker\": {},\n  \"timezone\": \"\",\n  \"title\": \"ClickHouse - Data Analysis\",\n  \"uid\": \"-B3tt7a7z\",\n  \"version\": 5,\n  \"weekStart\": \"\"\n}\n"
  },
  {
    "path": "src/dashboards/opentelemetry-clickhouse.json",
    "content": "{\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": {\n          \"type\": \"datasource\",\n          \"uid\": \"grafana\"\n        },\n        \"enable\": true,\n        \"hide\": true,\n        \"iconColor\": \"rgba(0, 211, 255, 1)\",\n        \"name\": \"Annotations & Alerts\",\n        \"target\": {\n          \"limit\": 100,\n          \"matchAny\": false,\n          \"tags\": [],\n          \"type\": \"dashboard\"\n        },\n        \"type\": \"dashboard\"\n      }\n    ]\n  },\n  \"editable\": true,\n  \"fiscalYearStartMonth\": 0,\n  \"graphTooltip\": 0,\n  \"id\": null,\n  \"links\": [],\n  \"liveNow\": false,\n  \"panels\": [\n    {\n      \"collapsed\": false,\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 0\n      },\n      \"id\": 9,\n      \"panels\": [],\n      \"title\": \"Traces\",\n      \"type\": \"row\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"bars\",\n            \"fillOpacity\": 100,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 0,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"normal\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 9,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 1\n      },\n      \"id\": 2,\n      \"maxDataPoints\": 50,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"multi\",\n          \"sort\": \"desc\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"clickhouse-traces\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"aggregates\": [\n                {\n                  \"aggregateType\": \"count\",\n                  \"column\": \"\"\n                }\n              ],\n              \"columns\": [\n                {\n                  \"hint\": \"time\",\n                  \"name\": \"Timestamp\",\n                  \"type\": \"DateTime64(9)\"\n                }\n              ],\n              \"database\": \"default\",\n              \"filters\": [\n                {\n                  \"condition\": \"AND\",\n                  \"filterType\": \"custom\",\n                  \"hint\": \"time\",\n                  \"key\": \"Timestamp\",\n                  \"operator\": \"WITH IN DASHBOARD TIME RANGE\",\n                  \"restrictToFields\": [\n                    {\n                      \"label\": \"Timestamp\",\n                      \"name\": \"Timestamp\",\n                      \"picklistValues\": [],\n                      \"type\": \"DateTime64(9)\"\n                    }\n                  ],\n                  \"type\": \"datetime\"\n                }\n              ],\n              \"groupBy\": [\"ServiceName\"],\n              \"limit\": 10000,\n              \"mode\": \"trend\",\n              \"orderBy\": [],\n              \"queryType\": \"timeseries\",\n              \"table\": \"otel_traces\"\n            }\n          },\n          \"pluginVersion\": \"4.0.6\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT\\r\\n  $__timeInterval(Timestamp) as time,\\r\\n  ServiceName,\\r\\n  count() as ` `\\r\\nFROM otel_traces\\r\\nWHERE\\r\\n  $__conditionalAll(TraceId IN (${trace_id:singlequote}),  $trace_id)\\r\\n  AND $__timeFilter(Timestamp)\\r\\n  AND ServiceName IN (${serviceName:singlequote})\\r\\nGROUP BY ServiceName, time\\r\\nORDER BY time ASC\\r\\nLIMIT 100000\\r\\n\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Traces per Service\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"normal\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          },\n          \"unit\": \"ms\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 9,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 10\n      },\n      \"id\": 7,\n      \"maxDataPoints\": 50,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"multi\",\n          \"sort\": \"desc\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"clickhouse-traces\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"aggregates\": [\n                {\n                  \"aggregateType\": \"count\",\n                  \"column\": \"\"\n                }\n              ],\n              \"columns\": [\n                {\n                  \"hint\": \"time\",\n                  \"name\": \"Timestamp\",\n                  \"type\": \"DateTime64(9)\"\n                }\n              ],\n              \"database\": \"default\",\n              \"filters\": [\n                {\n                  \"condition\": \"AND\",\n                  \"filterType\": \"custom\",\n                  \"hint\": \"time\",\n                  \"key\": \"Timestamp\",\n                  \"operator\": \"WITH IN DASHBOARD TIME RANGE\",\n                  \"restrictToFields\": [\n                    {\n                      \"label\": \"Timestamp\",\n                      \"name\": \"Timestamp\",\n                      \"picklistValues\": [],\n                      \"type\": \"DateTime64(9)\"\n                    }\n                  ],\n                  \"type\": \"datetime\"\n                }\n              ],\n              \"groupBy\": [\"ServiceName\"],\n              \"limit\": 10000,\n              \"mode\": \"trend\",\n              \"orderBy\": [],\n              \"queryType\": \"timeseries\",\n              \"table\": \"otel_traces\"\n            }\n          },\n          \"pluginVersion\": \"4.0.6\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT\\r\\n  $__timeInterval(Timestamp) as time,\\r\\n  ServiceName,\\r\\n  quantile(0.99)(Duration)/1000000 AS p99\\r\\nFROM otel_traces\\r\\nWHERE\\r\\n  $__conditionalAll(TraceId IN (${trace_id:singlequote}),  $trace_id)\\r\\n  AND $__timeFilter(Timestamp)\\r\\n  AND ( Timestamp  >= $__fromTime AND Timestamp <= $__toTime )\\r\\n  AND ServiceName IN (${serviceName:singlequote})\\r\\n  AND ServiceName != 'loadgenerator'\\r\\nGROUP BY time, ServiceName\\r\\nORDER BY time ASC\\r\\nLIMIT 100000\\r\\n\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Service Performance - p99\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"bars\",\n            \"fillOpacity\": 24,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 9,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 19\n      },\n      \"id\": 8,\n      \"maxDataPoints\": 50,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"multi\",\n          \"sort\": \"desc\"\n        }\n      },\n      \"pluginVersion\": \"9.4.1\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"clickhouse-traces\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"aggregates\": [\n                {\n                  \"aggregateType\": \"count\",\n                  \"column\": \"\"\n                }\n              ],\n              \"columns\": [\n                {\n                  \"hint\": \"time\",\n                  \"name\": \"Timestamp\",\n                  \"type\": \"DateTime64(9)\"\n                }\n              ],\n              \"database\": \"default\",\n              \"filters\": [\n                {\n                  \"condition\": \"AND\",\n                  \"filterType\": \"custom\",\n                  \"hint\": \"time\",\n                  \"key\": \"Timestamp\",\n                  \"operator\": \"WITH IN DASHBOARD TIME RANGE\",\n                  \"restrictToFields\": [\n                    {\n                      \"label\": \"Timestamp\",\n                      \"name\": \"Timestamp\",\n                      \"picklistValues\": [],\n                      \"type\": \"DateTime64(9)\"\n                    }\n                  ],\n                  \"type\": \"datetime\"\n                }\n              ],\n              \"groupBy\": [\"ServiceName\"],\n              \"limit\": 10000,\n              \"mode\": \"trend\",\n              \"orderBy\": [],\n              \"queryType\": \"timeseries\",\n              \"table\": \"otel_traces\"\n            }\n          },\n          \"pluginVersion\": \"4.0.6\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT\\r\\n  $__timeInterval(Timestamp) as time,\\r\\n  count(*) as ` `,\\r\\n  ServiceName\\r\\nFROM otel_traces\\r\\nWHERE\\r\\n  $__conditionalAll(TraceId IN (${trace_id:singlequote}),  $trace_id)\\r\\n  AND $__timeFilter(Timestamp)\\r\\n  AND ServiceName IN (${serviceName:singlequote})\\r\\n AND StatusCode IN ('Error', 'STATUS_CODE_ERROR')\\r\\n  AND ServiceName != 'loadgenerator' GROUP BY ServiceName, time\\r\\nORDER BY time ASC\\r\\nLIMIT 100000\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Error rates\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"cellOptions\": {\n              \"type\": \"auto\"\n            },\n            \"inspect\": false\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Trace ID\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 207\n              },\n              {\n                \"id\": \"links\",\n                \"value\": [\n                  {\n                    \"title\": \"__value.raw\",\n                    \"url\": \"/d/8klBUGfVk/otel-traces?${__url_time_range}﻿&﻿${serviceName:queryparam}﻿&var-trace_id=${__value.raw}\"\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Service Name\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 184\n              },\n              {\n                \"id\": \"links\",\n                \"value\": [\n                  {\n                    \"title\": \"__value.raw\",\n                    \"url\": \"/d/8klBUGfVk/otel-traces?${__url_time_range}﻿&﻿${trace_id:queryparam}﻿&var-serviceName=${__value.raw}\"\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Duration\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"unit\",\n                \"value\": \"ms\"\n              },\n              {\n                \"id\": \"custom.cellOptions\",\n                \"value\": {\n                  \"mode\": \"lcd\",\n                  \"type\": \"gauge\",\n                  \"valueDisplayMode\": \"text\"\n                }\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"timestamp\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 216\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Service Tags\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.inspect\",\n                \"value\": true\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"timestamp\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 248\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"timestamp\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 234\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 15,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 28\n      },\n      \"id\": 4,\n      \"options\": {\n        \"cellHeight\": \"sm\",\n        \"footer\": {\n          \"countRows\": false,\n          \"fields\": \"\",\n          \"reducer\": [\"sum\"],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": [\n          {\n            \"desc\": true,\n            \"displayName\": \"Duration\"\n          }\n        ]\n      },\n      \"pluginVersion\": \"11.2.0-72343\",\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"clickhouse-traces\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"default\",\n              \"filters\": [\n                {\n                  \"condition\": \"AND\",\n                  \"filterType\": \"custom\",\n                  \"key\": \"Timestamp\",\n                  \"operator\": \"WITH IN DASHBOARD TIME RANGE\",\n                  \"restrictToFields\": [\n                    {\n                      \"label\": \"Timestamp\",\n                      \"name\": \"Timestamp\",\n                      \"picklistValues\": [],\n                      \"type\": \"DateTime64(9)\"\n                    }\n                  ],\n                  \"type\": \"datetime\"\n                }\n              ],\n              \"limit\": 100,\n              \"mode\": \"list\",\n              \"orderBy\": [],\n              \"queryType\": \"table\",\n              \"table\": \"otel_traces\"\n            }\n          },\n          \"pluginVersion\": \"4.0.6\",\n          \"queryType\": \"table\",\n          \"rawSql\": \"SELECT\\r\\n  min(Timestamp) as timestamp,\\r\\n  TraceId as `Trace ID`,\\r\\n  argMin(ServiceName, Timestamp) as `Service Name`,\\r\\n  divide(max(Duration), 1000000) as Duration\\r\\nFROM otel_traces\\r\\nWHERE\\r\\n  $__conditionalAll(TraceId IN (${trace_id:singlequote}),  $trace_id)\\r\\n  AND ServiceName IN (${serviceName:singlequote})\\r\\n  AND ServiceName != 'loadgenerator'\\r\\n  AND $__timeFilter(Timestamp)\\r\\nGROUP BY TraceId\\r\\nORDER BY Duration DESC\\r\\nLIMIT 100\\r\\n\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Traces\",\n      \"type\": \"table\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"gridPos\": {\n        \"h\": 15,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 28\n      },\n      \"id\": 6,\n      \"targets\": [\n        {\n          \"builderOptions\": {\n            \"columns\": [\n              {\n                \"hint\": \"trace_id\",\n                \"name\": \"TraceId\"\n              },\n              {\n                \"hint\": \"trace_span_id\",\n                \"name\": \"SpanId\"\n              },\n              {\n                \"hint\": \"trace_parent_span_id\",\n                \"name\": \"ParentSpanId\"\n              },\n              {\n                \"hint\": \"trace_service_name\",\n                \"name\": \"ServiceName\"\n              },\n              {\n                \"hint\": \"trace_operation_name\",\n                \"name\": \"SpanName\"\n              },\n              {\n                \"hint\": \"time\",\n                \"name\": \"Timestamp\"\n              },\n              {\n                \"hint\": \"trace_duration_time\",\n                \"name\": \"Duration\"\n              },\n              {\n                \"hint\": \"trace_tags\",\n                \"name\": \"SpanAttributes\"\n              },\n              {\n                \"hint\": \"trace_service_tags\",\n                \"name\": \"ResourceAttributes\"\n              },\n              {\n                \"hint\": \"trace_status_code\",\n                \"name\": \"StatusCode\"\n              }\n            ],\n            \"database\": \"default\",\n            \"filters\": [\n              {\n                \"condition\": \"AND\",\n                \"filterType\": \"custom\",\n                \"hint\": \"time\",\n                \"key\": \"\",\n                \"operator\": \"WITH IN DASHBOARD TIME RANGE\",\n                \"type\": \"datetime\"\n              },\n              {\n                \"condition\": \"AND\",\n                \"filterType\": \"custom\",\n                \"hint\": \"trace_duration_time\",\n                \"key\": \"\",\n                \"operator\": \">\",\n                \"type\": \"UInt64\",\n                \"value\": 0\n              },\n              {\n                \"condition\": \"AND\",\n                \"filterType\": \"custom\",\n                \"hint\": \"trace_service_name\",\n                \"key\": \"\",\n                \"operator\": \"IS ANYTHING\",\n                \"type\": \"string\",\n                \"value\": \"\"\n              }\n            ],\n            \"limit\": 1000,\n            \"meta\": {\n              \"isTraceIdMode\": true,\n              \"otelEnabled\": true,\n              \"otelVersion\": \"latest\",\n              \"traceDurationUnit\": \"nanoseconds\",\n              \"traceId\": \"${trace_id}\"\n            },\n            \"mode\": \"list\",\n            \"orderBy\": [\n              {\n                \"default\": true,\n                \"dir\": \"DESC\",\n                \"hint\": \"time\",\n                \"name\": \"\"\n              },\n              {\n                \"default\": true,\n                \"dir\": \"DESC\",\n                \"hint\": \"trace_duration_time\",\n                \"name\": \"\"\n              }\n            ],\n            \"queryType\": \"traces\",\n            \"table\": \"otel_traces\"\n          },\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"clickhouse-traces\"\n          },\n          \"editorType\": \"builder\",\n          \"format\": 3,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 100,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.0.6\",\n          \"queryType\": \"traces\",\n          \"rawSql\": \"WITH '${trace_id}' as trace_id, (SELECT min(Start) FROM \\\"otel_traces_trace_id_ts\\\" WHERE TraceId = trace_id) as trace_start, (SELECT max(End) + 1 FROM \\\"otel_traces_trace_id_ts\\\" WHERE TraceId = trace_id) as trace_end SELECT \\\"TraceId\\\" as traceID, \\\"SpanId\\\" as spanID, \\\"ParentSpanId\\\" as parentSpanID, \\\"ServiceName\\\" as serviceName, \\\"SpanName\\\" as operationName, \\\"Timestamp\\\" as startTime, multiply(\\\"Duration\\\", 0.000001) as duration, arrayMap(key -> map('key', key, 'value',\\\"SpanAttributes\\\"[key]), mapKeys(\\\"SpanAttributes\\\")) as tags, arrayMap(key -> map('key', key, 'value',\\\"ResourceAttributes\\\"[key]), mapKeys(\\\"ResourceAttributes\\\")) as serviceTags FROM \\\"otel_traces\\\" WHERE traceID = trace_id AND startTime >= trace_start AND startTime <= trace_end AND ( Timestamp >= $__fromTime AND Timestamp <= $__toTime ) AND ( Duration > 0 ) ORDER BY Timestamp DESC, Duration DESC LIMIT 1000\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Trace Details\",\n      \"type\": \"traces\"\n    },\n    {\n      \"collapsed\": false,\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 43\n      },\n      \"id\": 10,\n      \"panels\": [],\n      \"title\": \"Logs\",\n      \"type\": \"row\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 44\n      },\n      \"id\": 11,\n      \"options\": {\n        \"dedupStrategy\": \"none\",\n        \"enableLogDetails\": true,\n        \"prettifyLogMessage\": false,\n        \"showCommonLabels\": false,\n        \"showLabels\": false,\n        \"showTime\": true,\n        \"sortOrder\": \"Descending\",\n        \"wrapLogMessage\": false\n      },\n      \"targets\": [\n        {\n          \"builderOptions\": {\n            \"columns\": [\n              {\n                \"hint\": \"time\",\n                \"name\": \"Timestamp\",\n                \"type\": \"DateTime64(9)\"\n              },\n              {\n                \"hint\": \"log_level\",\n                \"name\": \"SeverityText\",\n                \"type\": \"LowCardinality(String)\"\n              },\n              {\n                \"hint\": \"log_message\",\n                \"name\": \"Body\",\n                \"type\": \"String\"\n              }\n            ],\n            \"database\": \"default\",\n            \"filters\": [],\n            \"limit\": 1000,\n            \"meta\": {\n              \"logMessageLike\": \"\",\n              \"otelVersion\": \"latest\"\n            },\n            \"mode\": \"list\",\n            \"orderBy\": [],\n            \"queryType\": \"logs\",\n            \"table\": \"otel_logs\"\n          },\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"editorType\": \"builder\",\n          \"format\": 2,\n          \"pluginVersion\": \"4.0.6\",\n          \"rawSql\": \"SELECT Timestamp as timestamp, Body as body, SeverityText as level FROM \\\"otel_logs\\\" LIMIT 1000\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Trace Logs\",\n      \"type\": \"logs\"\n    }\n  ],\n  \"refresh\": \"\",\n  \"revision\": 1,\n  \"schemaVersion\": 39,\n  \"tags\": [],\n  \"templating\": {\n    \"list\": [\n      {\n        \"hide\": 0,\n        \"includeAll\": false,\n        \"label\": \"ClickHouse instance\",\n        \"multi\": false,\n        \"name\": \"datasource\",\n        \"options\": [],\n        \"query\": \"grafana-clickhouse-datasource\",\n        \"queryValue\": \"\",\n        \"refresh\": 1,\n        \"regex\": \"\",\n        \"skipUrlSync\": false,\n        \"type\": \"datasource\"\n      },\n      {\n        \"current\": {\n          \"selected\": true,\n          \"text\": [\"All\"],\n          \"value\": [\"$__all\"]\n        },\n        \"datasource\": {\n          \"type\": \"grafana-clickhouse-datasource\",\n          \"uid\": \"${datasource}\"\n        },\n        \"definition\": \"SELECT DISTINCT ServiceName FROM otel_traces\",\n        \"hide\": 0,\n        \"includeAll\": true,\n        \"label\": \"Service Name\",\n        \"multi\": true,\n        \"name\": \"serviceName\",\n        \"options\": [],\n        \"query\": \"SELECT DISTINCT ServiceName FROM otel_traces\",\n        \"refresh\": 1,\n        \"regex\": \"\",\n        \"skipUrlSync\": false,\n        \"sort\": 0,\n        \"type\": \"query\"\n      },\n      {\n        \"allValue\": \"ALL\",\n        \"current\": {\n          \"selected\": false,\n          \"text\": \"All\",\n          \"value\": \"$__all\"\n        },\n        \"datasource\": {\n          \"type\": \"grafana-clickhouse-datasource\",\n          \"uid\": \"${datasource}\"\n        },\n        \"definition\": \"SELECT DISTINCT TraceId FROM otel_traces WHERE ParentSpanId = '' LIMIT 100\",\n        \"hide\": 0,\n        \"includeAll\": true,\n        \"label\": \"Trace Id\",\n        \"multi\": false,\n        \"name\": \"trace_id\",\n        \"options\": [],\n        \"query\": \"SELECT DISTINCT TraceId FROM otel_traces WHERE ParentSpanId = '' LIMIT 100\",\n        \"refresh\": 1,\n        \"regex\": \"\",\n        \"skipUrlSync\": false,\n        \"sort\": 0,\n        \"type\": \"query\"\n      }\n    ]\n  },\n  \"time\": {\n    \"from\": \"now-1h\",\n    \"to\": \"now\"\n  },\n  \"timepicker\": {},\n  \"timezone\": \"\",\n  \"title\": \"Simple ClickHouse OTel Dashboard\",\n  \"uid\": \"8klBUGfVk\",\n  \"version\": 2,\n  \"weekStart\": \"\"\n}\n"
  },
  {
    "path": "src/dashboards/query-analysis.json",
    "content": "{\n  \"__inputs\": [],\n  \"__elements\": [],\n  \"__requires\": [\n    {\n      \"type\": \"grafana\",\n      \"id\": \"grafana\",\n      \"name\": \"Grafana\",\n      \"version\": \"9.0.1\"\n    },\n    {\n      \"type\": \"datasource\",\n      \"id\": \"grafana-clickhouse-datasource\",\n      \"name\": \"ClickHouse\",\n      \"version\": \"2.0.0\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"histogram\",\n      \"name\": \"Histogram\",\n      \"version\": \"\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"piechart\",\n      \"name\": \"Pie chart\",\n      \"version\": \"\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"stat\",\n      \"name\": \"Stat\",\n      \"version\": \"\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"table\",\n      \"name\": \"Table\",\n      \"version\": \"\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"timeseries\",\n      \"name\": \"Time series\",\n      \"version\": \"\"\n    }\n  ],\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": {\n          \"type\": \"datasource\",\n          \"uid\": \"grafana\"\n        },\n        \"enable\": true,\n        \"hide\": true,\n        \"iconColor\": \"rgba(0, 211, 255, 1)\",\n        \"name\": \"Annotations & Alerts\",\n        \"target\": {\n          \"limit\": 100,\n          \"matchAny\": false,\n          \"tags\": [],\n          \"type\": \"dashboard\"\n        },\n        \"type\": \"dashboard\"\n      }\n    ]\n  },\n  \"editable\": true,\n  \"fiscalYearStartMonth\": 0,\n  \"graphTooltip\": 0,\n  \"id\": null,\n  \"iteration\": 1661858001390,\n  \"links\": [],\n  \"liveNow\": false,\n  \"panels\": [\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"semi-dark-blue\",\n                \"value\": null\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 4,\n        \"w\": 5,\n        \"x\": 0,\n        \"y\": 0\n      },\n      \"id\": 10,\n      \"options\": {\n        \"colorMode\": \"background\",\n        \"graphMode\": \"area\",\n        \"justifyMode\": \"auto\",\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\"lastNotNull\"],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"textMode\": \"auto\"\n      },\n      \"pluginVersion\": \"9.0.1\",\n      \"targets\": [\n        {\n          \"builderOptions\": {\n            \"fields\": [],\n            \"limit\": 100,\n            \"mode\": \"list\"\n          },\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT count() as \\\"Total queries\\\" FROM system.query_log WHERE type in ($type) AND initial_user IN ($user) AND query_kind IN ($query_kind) AND $__timeFilter(event_time) \",\n          \"refId\": \"A\"\n        }\n      ],\n      \"type\": \"stat\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"semi-dark-blue\",\n                \"value\": null\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Avg query memory\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"unit\",\n                \"value\": \"decbytes\"\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 4,\n        \"w\": 5,\n        \"x\": 5,\n        \"y\": 0\n      },\n      \"id\": 17,\n      \"options\": {\n        \"colorMode\": \"background\",\n        \"graphMode\": \"area\",\n        \"justifyMode\": \"auto\",\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\"lastNotNull\"],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"textMode\": \"auto\"\n      },\n      \"pluginVersion\": \"9.0.1\",\n      \"targets\": [\n        {\n          \"builderOptions\": {\n            \"fields\": [],\n            \"limit\": 100,\n            \"mode\": \"list\"\n          },\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT avg(memory_usage) as \\\"Avg query memory\\\", $__timeInterval(query_start_time) as time FROM system.query_log WHERE type in ($type) AND initial_user IN ($user) AND query_kind IN ($query_kind) AND $__timeFilter(event_time) GROUP BY time ORDER BY time\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"type\": \"stat\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"fillOpacity\": 80,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineWidth\": 1\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"light-blue\",\n                \"value\": null\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Query time\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"unit\",\n                \"value\": \"ms\"\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 8,\n        \"x\": 10,\n        \"y\": 0\n      },\n      \"id\": 12,\n      \"options\": {\n        \"bucketOffset\": 0,\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        }\n      },\n      \"pluginVersion\": \"8.3.4\",\n      \"targets\": [\n        {\n          \"builderOptions\": {\n            \"fields\": [],\n            \"limit\": 100,\n            \"mode\": \"list\"\n          },\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT query_duration_ms as \\\"Query time\\\" FROM system.query_log WHERE type in ($type) AND initial_user IN ($user) AND query_kind IN ($query_kind) AND $__timeFilter(event_time) LIMIT 1000;\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Query time distribution\",\n      \"type\": \"histogram\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            }\n          },\n          \"mappings\": []\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 6,\n        \"x\": 18,\n        \"y\": 0\n      },\n      \"id\": 8,\n      \"options\": {\n        \"displayLabels\": [],\n        \"legend\": {\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"pieType\": \"pie\",\n        \"reduceOptions\": {\n          \"calcs\": [\"lastNotNull\"],\n          \"fields\": \"\",\n          \"values\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"builderOptions\": {\n            \"fields\": [],\n            \"limit\": 100,\n            \"mode\": \"list\"\n          },\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT initial_user, count() as c FROM system.query_log WHERE type in ($type) AND initial_user IN ($user) AND query_kind IN ($query_kind) GROUP BY initial_user LIMIT 100;\\n\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Top users\",\n      \"type\": \"piechart\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"semi-dark-blue\",\n                \"value\": null\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Avg query time\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"unit\",\n                \"value\": \"dtdurationms\"\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 4,\n        \"w\": 10,\n        \"x\": 0,\n        \"y\": 4\n      },\n      \"id\": 16,\n      \"options\": {\n        \"colorMode\": \"background\",\n        \"graphMode\": \"area\",\n        \"justifyMode\": \"auto\",\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\"lastNotNull\"],\n          \"fields\": \"/^Avg query time$/\",\n          \"values\": false\n        },\n        \"textMode\": \"auto\"\n      },\n      \"pluginVersion\": \"9.0.1\",\n      \"targets\": [\n        {\n          \"builderOptions\": {\n            \"fields\": [],\n            \"limit\": 100,\n            \"mode\": \"list\"\n          },\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT avg(query_duration_ms) as \\\"Avg query time\\\", $__timeInterval(query_start_time) as time FROM system.query_log WHERE type in ($type) AND initial_user IN ($user) AND query_kind IN ($query_kind) AND $__timeFilter(event_time) GROUP BY time ORDER BY time\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"type\": \"stat\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 15,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 1,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"normal\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 11,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 8\n      },\n      \"id\": 3,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"builderOptions\": {\n            \"fields\": [],\n            \"limit\": 100,\n            \"mode\": \"list\"\n          },\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"format\": 2,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT $__timeInterval(query_start_time) as time,\\n       any(normalizeQuery(query)) AS normalized_query,\\n       count() as c\\nFROM system.query_log\\nWHERE type != 'QueryStart'\\n  AND $__timeFilter(event_time)\\n  AND initial_user IN ($user)\\n   AND query_kind IN ($query_kind)\\n  AND normalized_query_hash IN (SELECT normalized_query_hash\\n                                FROM system.query_log\\n                                WHERE type in ($type) AND $__timeFilter(event_time) AND query_kind IN ($query_kind)\\n                                GROUP BY normalized_query_hash\\n                                ORDER BY count() DESC\\n                                LIMIT 5)\\nGROUP BY normalized_query_hash, time\\nORDER BY time\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Top query types over time\",\n      \"transformations\": [\n        {\n          \"id\": \"prepareTimeSeries\",\n          \"options\": {\n            \"format\": \"many\"\n          }\n        }\n      ],\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 10,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 19\n      },\n      \"id\": 6,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"builderOptions\": {\n            \"fields\": [],\n            \"limit\": 100,\n            \"mode\": \"list\"\n          },\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"format\": 2,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT $__timeInterval(query_start_time) as time,\\n       any(normalizeQuery(query)) AS normalized_query,\\n       avg(query_duration_ms) as avg_query_duration\\nFROM system.query_log\\nWHERE type != 'QueryStart'\\n  AND $__timeFilter(event_time)\\n  AND type in ($type) AND initial_user IN ($user) AND query_kind IN ($query_kind)\\n  AND normalized_query_hash IN (SELECT normalized_query_hash\\n                                FROM system.query_log\\n                                WHERE type in ($type) AND initial_user IN ($user) AND query_kind IN ($query_kind) AND $__timeFilter(event_time)\\n                                GROUP BY normalized_query_hash\\n                                ORDER BY avg(query_duration_ms) DESC\\n                                LIMIT 10)\\nGROUP BY normalized_query_hash, time\\nORDER BY time\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Query performance by type over time\",\n      \"transformations\": [\n        {\n          \"id\": \"prepareTimeSeries\",\n          \"options\": {\n            \"format\": \"many\"\n          }\n        }\n      ],\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"bars\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"opacity\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"normal\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 10,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 29\n      },\n      \"id\": 13,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"builderOptions\": {\n            \"fields\": [],\n            \"limit\": 100,\n            \"mode\": \"list\"\n          },\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"format\": 2,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT $__timeInterval(query_start_time) as time, initial_user as user, count() as \\\"number of queries by\\\"\\nFROM system.query_log\\nWHERE query_kind IN ($query_kind) AND type IN ($type) AND $__timeFilter(event_time) AND initial_user IN (\\n  SELECT initial_user\\nFROM system.query_log\\nWHERE query_kind IN ($query_kind) AND type IN ($type) AND $__timeFilter(event_time)\\nGROUP BY initial_user\\nORDER BY count() as c DESC\\nLIMIT 10\\n)\\nGROUP BY initial_user, time\\nORDER BY time;\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Query requests by user\",\n      \"transformations\": [\n        {\n          \"id\": \"prepareTimeSeries\",\n          \"options\": {\n            \"format\": \"many\"\n          }\n        }\n      ],\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"continuous-GrYlRd\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 20,\n            \"gradientMode\": \"scheme\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"smooth\",\n            \"lineWidth\": 3,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\"\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Max Memory Usage\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"unit\",\n                \"value\": \"decbytes\"\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 39\n      },\n      \"id\": 15,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\"\n        }\n      },\n      \"pluginVersion\": \"8.3.4\",\n      \"targets\": [\n        {\n          \"builderOptions\": {\n            \"fields\": [],\n            \"limit\": 100,\n            \"mode\": \"list\"\n          },\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"format\": 2,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT $__timeInterval(query_start_time) as time, \\n      max(memory_usage) as \\\"Max Memory Usage\\\"\\nFROM system.query_log\\nWHERE $__timeFilter(event_time)\\nGROUP BY time\\nORDER BY time DESC\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Memory usage over time\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"Read rows\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 26,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"smooth\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\"\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"read_rows\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"color\",\n                \"value\": {\n                  \"fixedColor\": \"dark-blue\",\n                  \"mode\": \"fixed\"\n                }\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"written_rows\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"color\",\n                \"value\": {\n                  \"fixedColor\": \"dark-red\",\n                  \"mode\": \"fixed\"\n                }\n              },\n              {\n                \"id\": \"custom.axisPlacement\",\n                \"value\": \"right\"\n              },\n              {\n                \"id\": \"custom.axisLabel\",\n                \"value\": \"Written rows\"\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 39\n      },\n      \"id\": 19,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT toStartOfInterval(toDateTime(event_time), INTERVAL 60 second),  sum(read_rows) read_rows, sum(written_rows) written_rows FROM system.query_log WHERE $__timeFilter(event_time) GROUP BY event_time ORDER BY event_time ASC\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Read vs Write Rows\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"thresholds\"\n          },\n          \"custom\": {\n            \"align\": \"auto\",\n            \"displayMode\": \"auto\"\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\"\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"written\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 207\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"type\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 116\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"memory_usage\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 119\n              },\n              {\n                \"id\": \"unit\",\n                \"value\": \"bytes\"\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"result\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 201\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"read\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 123\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"query_kind\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 90\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"query_duration_ms\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 145\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"initial_user\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 104\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"client_hostname\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 246\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"databases\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 114\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"user\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 119\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"client\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 270\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Client host\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 110\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Query id\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 104\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Start time\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 162\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Duration\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 92\n              },\n              {\n                \"id\": \"unit\",\n                \"value\": \"ms\"\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Normalized query\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 885\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Rows weitten\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 150\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Rows read\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 177\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Type\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 84\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Result\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 296\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"User\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 84\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"Rows written\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"custom.width\",\n                \"value\": 197\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 12,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 47\n      },\n      \"id\": 4,\n      \"options\": {\n        \"footer\": {\n          \"fields\": \"\",\n          \"reducer\": [\"sum\"],\n          \"show\": false\n        },\n        \"showHeader\": true,\n        \"sortBy\": [\n          {\n            \"desc\": false,\n            \"displayName\": \"Memory usage\"\n          }\n        ]\n      },\n      \"pluginVersion\": \"8.3.4\",\n      \"targets\": [\n        {\n          \"builderOptions\": {\n            \"fields\": [],\n            \"limit\": 100,\n            \"mode\": \"list\"\n          },\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${datasource}\"\n          },\n          \"format\": 1,\n          \"meta\": {\n            \"builderOptions\": {\n              \"fields\": [],\n              \"limit\": 100,\n              \"mode\": \"list\"\n            }\n          },\n          \"queryType\": \"sql\",\n          \"rawSql\": \"SELECT query_start_time, type, query_duration_ms, initial_user, substring(query_id,1, 8) as query_id, query_kind, normalizeQuery(query) AS normalized_query, concat( toString(read_rows), ' rows / ', formatReadableSize(read_bytes) ) AS read, concat( toString(written_rows), ' rows / ', formatReadableSize(written_bytes) ) AS written, concat( toString(result_rows), ' rows / ', formatReadableSize(result_bytes) ) AS result, memory_usage FROM system.query_log WHERE type in ($type) AND initial_user IN ($user) AND query_kind IN ($query_kind) AND $__timeFilter(event_time) ORDER BY query_duration_ms DESC LIMIT  1000\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Query overview\",\n      \"transformations\": [\n        {\n          \"id\": \"organize\",\n          \"options\": {\n            \"excludeByName\": {},\n            \"indexByName\": {\n              \"initial_user\": 5,\n              \"memory_usage\": 2,\n              \"normalized_query\": 7,\n              \"query_duration_ms\": 4,\n              \"query_id\": 0,\n              \"query_kind\": 6,\n              \"query_start_time\": 3,\n              \"read\": 8,\n              \"result\": 10,\n              \"type\": 1,\n              \"written\": 9\n            },\n            \"renameByName\": {\n              \"c\": \"\",\n              \"client\": \"Client\",\n              \"client_hostname\": \"Client host\",\n              \"databases\": \"Databases\",\n              \"exception\": \"Exception\",\n              \"initial_user\": \"User\",\n              \"memory_usage\": \"Memory usage\",\n              \"normalized_query\": \"Normalized query\",\n              \"query_duration_ms\": \"Duration\",\n              \"query_id\": \"Query id\",\n              \"query_kind\": \"Type\",\n              \"query_start_time\": \"Start time\",\n              \"read\": \"Rows read\",\n              \"result\": \"Result\",\n              \"stack_trace\": \"Stack trace\",\n              \"tables\": \"Tables\",\n              \"type\": \"Status\",\n              \"user\": \"User\",\n              \"written\": \"Rows written\"\n            }\n          }\n        }\n      ],\n      \"type\": \"table\"\n    }\n  ],\n  \"refresh\": false,\n  \"schemaVersion\": 36,\n  \"style\": \"dark\",\n  \"tags\": [],\n  \"templating\": {\n    \"list\": [\n      {\n        \"current\": {},\n        \"hide\": 0,\n        \"includeAll\": false,\n        \"label\": \"ClickHouse instance\",\n        \"multi\": false,\n        \"name\": \"datasource\",\n        \"options\": [],\n        \"query\": \"grafana-clickhouse-datasource\",\n        \"queryValue\": \"\",\n        \"refresh\": 1,\n        \"regex\": \"\",\n        \"skipUrlSync\": false,\n        \"type\": \"datasource\"\n      },\n      {\n        \"current\": {},\n        \"datasource\": {\n          \"type\": \"grafana-clickhouse-datasource\",\n          \"uid\": \"${datasource}\"\n        },\n        \"definition\": \"SELECT DISTINCT(query_kind) as query_kind FROM system.query_log WHERE query_kind != ''\",\n        \"description\": \"\",\n        \"hide\": 0,\n        \"includeAll\": true,\n        \"label\": \"Query kind\",\n        \"multi\": true,\n        \"name\": \"query_kind\",\n        \"options\": [],\n        \"query\": \"SELECT DISTINCT(query_kind) as query_kind FROM system.query_log WHERE query_kind != ''\",\n        \"refresh\": 1,\n        \"regex\": \"\",\n        \"skipUrlSync\": false,\n        \"sort\": 0,\n        \"type\": \"query\"\n      },\n      {\n        \"current\": {},\n        \"datasource\": {\n          \"type\": \"grafana-clickhouse-datasource\",\n          \"uid\": \"${datasource}\"\n        },\n        \"definition\": \"SELECT type FROM system.query_log GROUP BY type\",\n        \"hide\": 0,\n        \"includeAll\": true,\n        \"label\": \"Query status\",\n        \"multi\": true,\n        \"name\": \"type\",\n        \"options\": [],\n        \"query\": \"SELECT type FROM system.query_log GROUP BY type\",\n        \"refresh\": 1,\n        \"regex\": \"\",\n        \"skipUrlSync\": false,\n        \"sort\": 0,\n        \"type\": \"query\"\n      },\n      {\n        \"current\": {},\n        \"datasource\": {\n          \"type\": \"grafana-clickhouse-datasource\",\n          \"uid\": \"${datasource}\"\n        },\n        \"definition\": \"SELECT DISTINCT(initial_user) FROM system.query_log WHERE initial_user != '' LIMIT 100\",\n        \"hide\": 0,\n        \"includeAll\": true,\n        \"label\": \"User\",\n        \"multi\": true,\n        \"name\": \"user\",\n        \"options\": [],\n        \"query\": \"SELECT DISTINCT(initial_user) FROM system.query_log WHERE initial_user != '' LIMIT 100\",\n        \"refresh\": 1,\n        \"regex\": \"\",\n        \"skipUrlSync\": false,\n        \"sort\": 0,\n        \"type\": \"query\"\n      }\n    ]\n  },\n  \"time\": {\n    \"from\": \"now-24h\",\n    \"to\": \"now\"\n  },\n  \"timepicker\": {},\n  \"timezone\": \"\",\n  \"title\": \"ClickHouse - Query Analysis\",\n  \"uid\": \"w5Q2Otank\",\n  \"version\": 1,\n  \"weekStart\": \"\"\n}\n"
  },
  {
    "path": "src/dashboards/system-dashboards.json",
    "content": "{\n  \"__inputs\": [\n    {\n      \"name\": \"the_datasource\",\n      \"label\": \"The Datasource\",\n      \"description\": \"\",\n      \"type\": \"datasource\",\n      \"pluginId\": \"grafana-clickhouse-datasource\",\n      \"pluginName\": \"ClickHouse\"\n    }\n  ],\n  \"__elements\": {},\n  \"__requires\": [\n    {\n      \"type\": \"grafana\",\n      \"id\": \"grafana\",\n      \"name\": \"Grafana\",\n      \"version\": \"11.2.0-pre\"\n    },\n    {\n      \"type\": \"datasource\",\n      \"id\": \"grafana-clickhouse-datasource\",\n      \"name\": \"ClickHouse\",\n      \"version\": \"4.3.2\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"timeseries\",\n      \"name\": \"Time series\",\n      \"version\": \"\"\n    }\n  ],\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": {\n          \"type\": \"grafana\",\n          \"uid\": \"-- Grafana --\"\n        },\n        \"enable\": true,\n        \"hide\": true,\n        \"iconColor\": \"rgba(0, 211, 255, 1)\",\n        \"name\": \"Annotations & Alerts\",\n        \"type\": \"dashboard\"\n      }\n    ]\n  },\n  \"description\": \"Similar to the monitoring dashboard that is built in to ClickHouse.\",\n  \"editable\": true,\n  \"fiscalYearStartMonth\": 0,\n  \"graphTooltip\": 0,\n  \"id\": null,\n  \"links\": [],\n  \"panels\": [\n    {\n      \"collapsed\": false,\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 0\n      },\n      \"id\": null,\n      \"panels\": [],\n      \"title\": \"Overview\",\n      \"type\": \"row\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 0\n      },\n      \"id\": 1,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, avg(ProfileEvent_Query)\\nFROM merge('system', '^metric_log')\\nWHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Queries/second\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 0\n      },\n      \"id\": 2,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, avg(ProfileEvent_OSCPUVirtualTimeMicroseconds) / 1000000\\nFROM merge('system', '^metric_log')\\nWHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"CPU Usage (cores)\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 8\n      },\n      \"id\": 3,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, avg(CurrentMetric_Query)\\nFROM merge('system', '^metric_log')\\nWHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Queries Running\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 8\n      },\n      \"id\": 4,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, avg(CurrentMetric_Merge)\\nFROM merge('system', '^metric_log')\\nWHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Merges Running\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 16\n      },\n      \"id\": 5,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, avg(ProfileEvent_SelectedBytes)\\nFROM merge('system', '^metric_log')\\nWHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Selected Bytes/second\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 16\n      },\n      \"id\": 6,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, avg(ProfileEvent_OSIOWaitMicroseconds) / 1000000\\nFROM merge('system', '^metric_log')\\nWHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"IO Wait\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 24\n      },\n      \"id\": 7,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, avg(ProfileEvent_OSCPUWaitMicroseconds) / 1000000\\nFROM merge('system', '^metric_log')\\nWHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"CPU Wait\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 24\n      },\n      \"id\": 8,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, avg(value)\\nFROM merge('system', '^asynchronous_metric_log')\\nWHERE $__dateFilter(event_date) AND $__timeFilter(event_time) AND metric = 'OSUserTimeNormalized'\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"OS CPU Usage (Userspace)\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 32\n      },\n      \"id\": 9,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, avg(value)\\nFROM merge('system', '^asynchronous_metric_log')\\nWHERE $__dateFilter(event_date) AND $__timeFilter(event_time) AND metric = 'OSSystemTimeNormalized'\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"OS CPU Usage (Kernel)\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 32\n      },\n      \"id\": 10,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, avg(ProfileEvent_OSReadBytes)\\nFROM merge('system', '^metric_log')\\nWHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Read From Disk\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 40\n      },\n      \"id\": 11,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, avg(ProfileEvent_OSReadChars)\\nFROM merge('system', '^metric_log')\\nWHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Read From Filesystem\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 40\n      },\n      \"id\": 12,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, avg(CurrentMetric_MemoryTracking)\\nFROM merge('system', '^metric_log')\\nWHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Memory (tracked)\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 48\n      },\n      \"id\": 13,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, avg(value)\\nFROM merge('system', '^asynchronous_metric_log')\\nWHERE $__dateFilter(event_date) AND $__timeFilter(event_time) AND metric = 'LoadAverage15'\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Load Average (15 minutes)\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 48\n      },\n      \"id\": 14,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, avg(ProfileEvent_SelectedRows)\\nFROM merge('system', '^metric_log')\\nWHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Selected Rows/second\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 56\n      },\n      \"id\": 15,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, avg(ProfileEvent_InsertedRows)\\nFROM merge('system', '^metric_log')\\nWHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Inserted Rows/second\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 56\n      },\n      \"id\": 16,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, avg(value)\\nFROM merge('system', '^asynchronous_metric_log')\\nWHERE $__dateFilter(event_date) AND $__timeFilter(event_time) AND metric = 'TotalPartsOfMergeTreeTables'\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Total MergeTree Parts\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 64\n      },\n      \"id\": 17,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, max(value)\\nFROM merge('system', '^asynchronous_metric_log')\\nWHERE $__dateFilter(event_date) AND $__timeFilter(event_time) AND metric = 'MaxPartCountForPartition'\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Max Parts For Partition\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 64\n      },\n      \"id\": 18,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t,\\n    sum(CurrentMetric_TCPConnection) AS TCP_Connections,\\n    sum(CurrentMetric_MySQLConnection) AS MySQL_Connections,\\n    sum(CurrentMetric_HTTPConnection) AS HTTP_Connections\\nFROM merge('system', '^metric_log')\\nWHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Concurrent network connections\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"collapsed\": false,\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 72\n      },\n      \"id\": null,\n      \"panels\": [],\n      \"title\": \"Cloud overview\",\n      \"type\": \"row\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 72\n      },\n      \"id\": 19,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT \\n  toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t,\\n  avg(metric)\\nFROM (\\n  SELECT event_time, sum(ProfileEvent_Query) AS metric \\n  FROM clusterAllReplicas(default, merge('system', '^metric_log'))\\n  WHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\n  GROUP BY event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Queries/second\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 72\n      },\n      \"id\": 20,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, avg(metric) / 1000000\\nFROM (\\n  SELECT event_time, sum(ProfileEvent_OSCPUVirtualTimeMicroseconds) AS metric \\n  FROM clusterAllReplicas(default, merge('system', '^metric_log'))\\n  WHERE $__dateFilter(event_date) AND $__timeFilter(event_time) GROUP BY event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"CPU Usage (cores)\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 80\n      },\n      \"id\": 21,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT \\n  toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t,\\n  avg(metric)\\nFROM (\\n  SELECT event_time, sum(CurrentMetric_Query) AS metric \\n  FROM clusterAllReplicas(default, merge('system', '^metric_log'))\\n  WHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\n  GROUP BY event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Queries Running\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 80\n      },\n      \"id\": 22,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT \\n  toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t,\\n  avg(metric)\\nFROM (\\n  SELECT event_time, sum(CurrentMetric_Merge) AS metric \\n  FROM clusterAllReplicas(default, merge('system', '^metric_log'))\\n  WHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\n  GROUP BY event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Merges Running\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 88\n      },\n      \"id\": 23,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT \\n  toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t,\\n  avg(metric)\\nFROM (\\n  SELECT event_time, sum(ProfileEvent_SelectedBytes) AS metric \\n  FROM clusterAllReplicas(default, merge('system', '^metric_log'))\\n  WHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\n  GROUP BY event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Selected Bytes/second\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 88\n      },\n      \"id\": 24,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT \\n  toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t,\\n  avg(metric)\\nFROM (\\n  SELECT event_time, sum(ProfileEvent_OSIOWaitMicroseconds) / 1000000 AS metric \\n  FROM clusterAllReplicas(default, merge('system', '^metric_log'))\\n  WHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\n  GROUP BY event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"IO Wait (local fs)\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 96\n      },\n      \"id\": 25,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT \\n  toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t,\\n  avg(metric)\\nFROM (\\n  SELECT event_time, sum(ProfileEvent_ReadBufferFromS3Microseconds) / 1000000 AS metric \\n  FROM clusterAllReplicas(default, merge('system', '^metric_log'))\\n  WHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\n  GROUP BY event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"S3 read wait\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 96\n      },\n      \"id\": 26,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT \\n  toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t,\\n  avg(metric)\\nFROM (\\n  SELECT event_time, sum(ProfileEvent_ReadBufferFromS3RequestsErrors) AS metric \\n  FROM clusterAllReplicas(default, merge('system', '^metric_log'))\\n  WHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\n  GROUP BY event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"S3 read errors/sec\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 104\n      },\n      \"id\": 27,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT \\n  toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t,\\n  avg(metric)\\nFROM (\\n  SELECT event_time, sum(ProfileEvent_OSCPUWaitMicroseconds) / 1000000 AS metric \\n  FROM clusterAllReplicas(default, merge('system', '^metric_log'))\\n  WHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\n  GROUP BY event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"CPU Wait\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 104\n      },\n      \"id\": 28,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, avg(value)\\nFROM clusterAllReplicas(default, merge('system', '^asynchronous_metric_log'))\\nWHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\nAND metric = 'OSUserTimeNormalized'\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"OS CPU Usage (Userspace, normalized)\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 112\n      },\n      \"id\": 29,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, avg(value)\\nFROM clusterAllReplicas(default, merge('system', '^asynchronous_metric_log'))\\nWHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\nAND metric = 'OSSystemTimeNormalized'\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"OS CPU Usage (Kernel, normalized)\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 112\n      },\n      \"id\": 30,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT \\n  toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t,\\n  avg(metric)\\nFROM (\\n  SELECT event_time, sum(ProfileEvent_OSReadBytes) AS metric \\n  FROM clusterAllReplicas(default, merge('system', '^metric_log'))\\n  WHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\n  GROUP BY event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Read From Disk (bytes/sec)\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 120\n      },\n      \"id\": 31,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT \\n  toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t,\\n  avg(metric)\\nFROM (\\n  SELECT event_time, sum(ProfileEvent_OSReadChars) AS metric \\n  FROM clusterAllReplicas(default, merge('system', '^metric_log'))\\n  WHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\n  GROUP BY event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Read From Filesystem (bytes/sec)\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 120\n      },\n      \"id\": 32,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT \\n  toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t,\\n  avg(metric)\\nFROM (\\n  SELECT event_time, sum(CurrentMetric_MemoryTracking) AS metric \\n  FROM clusterAllReplicas(default, merge('system', '^metric_log'))\\n  WHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\n  GROUP BY event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Memory (tracked, bytes)\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 128\n      },\n      \"id\": 33,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, avg(value)\\nFROM (\\n  SELECT event_time, sum(value) AS value\\n  FROM clusterAllReplicas(default, merge('system', '^asynchronous_metric_log'))\\n  WHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\n    AND metric = 'LoadAverage15'\\n  GROUP BY event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Load Average (15 minutes)\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 128\n      },\n      \"id\": 34,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT \\n  toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t,\\n  avg(metric)\\nFROM (\\n  SELECT event_time, sum(ProfileEvent_SelectedRows) AS metric \\n  FROM clusterAllReplicas(default, merge('system', '^metric_log'))\\n  WHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\n  GROUP BY event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Selected Rows/sec\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 136\n      },\n      \"id\": 35,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT \\n  toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t,\\n  avg(metric)\\nFROM (\\n  SELECT event_time, sum(ProfileEvent_InsertedRows) AS metric \\n  FROM clusterAllReplicas(default, merge('system', '^metric_log'))\\n  WHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\n  GROUP BY event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Inserted Rows/sec\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 136\n      },\n      \"id\": 36,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, max(value)\\nFROM clusterAllReplicas(default, merge('system', '^asynchronous_metric_log'))\\nWHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\nAND metric = 'TotalPartsOfMergeTreeTables'\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Total MergeTree Parts\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 144\n      },\n      \"id\": 37,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, max(value)\\nFROM clusterAllReplicas(default, merge('system', '^asynchronous_metric_log'))\\nWHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\nAND metric = 'MaxPartCountForPartition'\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Max Parts For Partition\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 144\n      },\n      \"id\": 38,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT \\n  toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t,\\n  avg(metric)\\nFROM (\\n  SELECT event_time, sum(ProfileEvent_ReadBufferFromS3Bytes) AS metric \\n  FROM clusterAllReplicas(default, merge('system', '^metric_log'))\\n  WHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\n  GROUP BY event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Read From S3 (bytes/sec)\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 152\n      },\n      \"id\": 39,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT \\n  toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t,\\n  avg(metric)\\nFROM (\\n  SELECT event_time, sum(CurrentMetric_FilesystemCacheSize) AS metric \\n  FROM clusterAllReplicas(default, merge('system', '^metric_log'))\\n  WHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\n  GROUP BY event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Filesystem Cache Size\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 152\n      },\n      \"id\": 40,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT \\n  toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t,\\n  avg(metric)\\nFROM (\\n  SELECT event_time, sum(ProfileEvent_DiskS3PutObject + ProfileEvent_DiskS3UploadPart + ProfileEvent_DiskS3CreateMultipartUpload + ProfileEvent_DiskS3CompleteMultipartUpload) AS metric \\n  FROM clusterAllReplicas(default, merge('system', '^metric_log'))\\n  WHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\n  GROUP BY event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Disk S3 write req/sec\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 160\n      },\n      \"id\": 41,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT \\n  toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t,\\n  avg(metric)\\nFROM (\\n  SELECT event_time, sum(ProfileEvent_DiskS3GetObject + ProfileEvent_DiskS3HeadObject + ProfileEvent_DiskS3ListObjects) AS metric \\n  FROM clusterAllReplicas(default, merge('system', '^metric_log'))\\n  WHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\n  GROUP BY event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Disk S3 read req/sec\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 160\n      },\n      \"id\": 42,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT \\n  toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t,\\n  avg(metric)\\nFROM (\\n  SELECT event_time, sum(ProfileEvent_CachedReadBufferReadFromCacheBytes) / (sum(ProfileEvent_CachedReadBufferReadFromCacheBytes) + sum(ProfileEvent_CachedReadBufferReadFromSourceBytes)) AS metric \\n  FROM clusterAllReplicas(default, merge('system', '^metric_log'))\\n  WHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\n  GROUP BY event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"FS cache hit rate\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 168\n      },\n      \"id\": 43,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT \\n  toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t,\\n  avg(metric)\\nFROM (\\n  SELECT event_time, greatest(0, (sum(ProfileEvent_OSReadChars) - sum(ProfileEvent_OSReadBytes)) / (sum(ProfileEvent_OSReadChars) + sum(ProfileEvent_ReadBufferFromS3Bytes))) AS metric \\n  FROM clusterAllReplicas(default, merge('system', '^metric_log'))\\n  WHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\n  GROUP BY event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Page cache hit rate\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 168\n      },\n      \"id\": 44,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, avg(value)\\nFROM (\\n  SELECT event_time, sum(value) AS value\\n  FROM clusterAllReplicas(default, merge('system', '^asynchronous_metric_log'))\\n  WHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\n    AND metric LIKE 'NetworkReceiveBytes%'\\n  GROUP BY event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Network receive bytes/sec\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 176\n      },\n      \"id\": 45,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, avg(value)\\nFROM (\\n  SELECT event_time, sum(value) AS value\\n  FROM clusterAllReplicas(default, merge('system', '^asynchronous_metric_log'))\\n  WHERE $__dateFilter(event_date) AND $__timeFilter(event_time)\\n    AND metric LIKE 'NetworkSendBytes%'\\n  GROUP BY event_time)\\nGROUP BY t\\nORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Network send bytes/sec\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"grafana-clickhouse-datasource\",\n        \"uid\": \"${the_datasource}\"\n      },\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 176\n      },\n      \"id\": 46,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"grafana-clickhouse-datasource\",\n            \"uid\": \"${the_datasource}\"\n          },\n          \"editorType\": \"sql\",\n          \"format\": 0,\n          \"meta\": {\n            \"builderOptions\": {\n              \"columns\": [],\n              \"database\": \"\",\n              \"limit\": 1000,\n              \"mode\": \"list\",\n              \"queryType\": \"table\",\n              \"table\": \"\"\n            }\n          },\n          \"pluginVersion\": \"4.3.2\",\n          \"queryType\": \"timeseries\",\n          \"rawSql\": \"SELECT toStartOfInterval(event_time, INTERVAL $__interval_s SECOND) AS t, max(TCP_Connections), max(MySQL_Connections), max(HTTP_Connections) FROM (SELECT event_time, sum(CurrentMetric_TCPConnection) AS TCP_Connections, sum(CurrentMetric_MySQLConnection) AS MySQL_Connections, sum(CurrentMetric_HTTPConnection) AS HTTP_Connections FROM clusterAllReplicas(default, merge('system', '^metric_log')) WHERE $__dateFilter(event_date) AND $__timeFilter(event_time) GROUP BY event_time) GROUP BY t ORDER BY t WITH FILL STEP $__interval_s SETTINGS skip_unavailable_shards = 1\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Concurrent network connections\",\n      \"type\": \"timeseries\"\n    }\n  ],\n  \"schemaVersion\": 39,\n  \"tags\": [],\n  \"templating\": {\n    \"list\": []\n  },\n  \"time\": {\n    \"from\": \"now-6h\",\n    \"to\": \"now\"\n  },\n  \"timepicker\": {},\n  \"timezone\": \"browser\",\n  \"title\": \"Advanced ClickHouse Monitoring Dashboard\",\n  \"uid\": null,\n  \"version\": 1,\n  \"weekStart\": \"\"\n}\n"
  },
  {
    "path": "src/data/CHDatasource.test.ts",
    "content": "import {\n  arrayToDataFrame,\n  CoreApp,\n  DataQueryRequest,\n  SupplementaryQueryType,\n  TimeRange,\n  toDataFrame,\n  TypedVariableModel,\n} from '@grafana/data';\nimport { DataSourceWithBackend } from '@grafana/runtime';\nimport { DataQuery } from '@grafana/schema';\nimport { mockDatasource } from '__mocks__/datasource';\nimport { cloneDeep } from 'lodash';\nimport { of } from 'rxjs';\nimport { BuilderMode, ColumnHint, FilterOperator, OrderByDirection, QueryBuilderOptions, QueryType } from 'types/queryBuilder';\nimport { CHBuilderQuery, CHQuery, CHSqlQuery, EditorType } from 'types/sql';\nimport { AdHocFilter } from './adHocFilter';\nimport { Datasource } from './CHDatasource';\nimport * as logs from './logs';\n\njest.mock('./logs', () => ({\n  getTimeFieldRoundingClause: jest.fn(),\n  getIntervalInfo: jest.fn(),\n  queryLogsVolume: jest.fn(),\n  TIME_FIELD_ALIAS: jest.requireActual('./logs').TIME_FIELD_ALIAS,\n  DEFAULT_LOGS_ALIAS: jest.requireActual('./logs').DEFAULT_LOGS_ALIAS,\n  LOG_LEVEL_TO_IN_CLAUSE: jest.requireActual('./logs').LOG_LEVEL_TO_IN_CLAUSE,\n}));\n\ninterface InstanceConfig {\n  queryResponse: {} | [];\n}\n\nconst templateSrvMock = { replace: jest.fn(), getVariables: jest.fn(), getAdhocFilters: jest.fn() };\n// noinspection JSUnusedGlobalSymbols\njest.mock('@grafana/runtime', () => ({\n  ...(jest.requireActual('@grafana/runtime') as unknown as object),\n  getTemplateSrv: () => templateSrvMock,\n}));\n\nconst createInstance = ({ queryResponse }: Partial<InstanceConfig> = {}) => {\n  const instance = cloneDeep(mockDatasource);\n  jest.spyOn(instance, 'query').mockImplementation((_request) => of({ data: [toDataFrame(queryResponse ?? [])] }));\n  return instance;\n};\n\ndescribe('ClickHouseDatasource', () => {\n  describe('metricFindQuery', () => {\n    it('fetches values', async () => {\n      const mockedValues = [1, 100];\n      const queryResponse = {\n        fields: [{ name: 'field', type: 'number', values: mockedValues }],\n      };\n      const expectedValues = mockedValues.map((v) => ({ text: v, value: v }));\n      const values = await createInstance({ queryResponse }).metricFindQuery('mock', {});\n      expect(values).toEqual(expectedValues);\n    });\n\n    it('fetches name/value pairs', async () => {\n      const mockedIds = [1, 2];\n      const mockedValues = [100, 200];\n      const queryResponse = {\n        fields: [\n          { name: 'id', type: 'number', values: mockedIds },\n          { name: 'values', type: 'number', values: mockedValues },\n        ],\n      };\n      const expectedValues = mockedValues.map((v, i) => ({ text: v, value: mockedIds[i] }));\n      const values = await createInstance({ queryResponse }).metricFindQuery('mock', {});\n      expect(values).toEqual(expectedValues);\n    });\n  });\n\n  describe('applyTemplateVariables', () => {\n    it('interpolates', async () => {\n      const rawSql = 'foo';\n      const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation(() => rawSql);\n      const query = { rawSql: 'select', editorType: EditorType.SQL } as CHQuery;\n      const val = createInstance({}).applyTemplateVariables(query, {});\n      expect(spyOnReplace).toHaveBeenCalled();\n      expect(val).toEqual({ rawSql, editorType: EditorType.SQL });\n    });\n    it('should handle $__conditionalAll and not replace', async () => {\n      const query = { rawSql: '$__conditionalAll(foo, $fieldVal)', editorType: EditorType.SQL } as CHQuery;\n      const vars = [{ current: { value: `'val1', 'val2'` }, name: 'fieldVal' }] as TypedVariableModel[];\n      const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation((x) => x);\n      const spyOnGetVars = jest.spyOn(templateSrvMock, 'getVariables').mockImplementation(() => vars);\n      const val = createInstance({}).applyTemplateVariables(query, {});\n      expect(spyOnReplace).toHaveBeenCalled();\n      expect(spyOnGetVars).toHaveBeenCalled();\n      expect(val).toEqual({ rawSql: `foo`, editorType: EditorType.SQL });\n    });\n    it('should handle $__conditionalAll and replace', async () => {\n      const query = { rawSql: '$__conditionalAll(foo, $fieldVal)', editorType: EditorType.SQL } as CHQuery;\n      const vars = [{ current: { value: '$__all' }, name: 'fieldVal' }] as TypedVariableModel[];\n      const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation((x) => x);\n      const spyOnGetVars = jest.spyOn(templateSrvMock, 'getVariables').mockImplementation(() => vars);\n      const val = createInstance({}).applyTemplateVariables(query, {});\n      expect(spyOnReplace).toHaveBeenCalled();\n      expect(spyOnGetVars).toHaveBeenCalled();\n      expect(val).toEqual({ rawSql: `1=1`, editorType: EditorType.SQL });\n    });\n\n    it('should apply ad-hoc filters correctly with template variables for table names', async () => {\n      // Setup the query with template variables for table names\n      const query = {\n        rawSql: 'SELECT * FROM ${database}.${table}',\n        editorType: EditorType.SQL,\n      } as CHQuery;\n\n      // Mock the ad-hoc filter\n      const adHocFilter = new AdHocFilter();\n\n      // The resolved table name after template variable substitution\n      const resolvedSql = 'SELECT * FROM test_db.test_table';\n\n      // The expected final SQL with ad-hoc filters applied\n      const sqlWithAdHocFilters = `SELECT * FROM test_db.test_table settings additional_table_filters={'test_db.test_table' : ' column = \\\\'value\\\\' '}`;\n\n      // Mock the template variable resolution\n      const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation(() => resolvedSql);\n      const spyOnGetVars = jest.spyOn(templateSrvMock, 'getVariables').mockImplementation(() => []);\n\n      // Setup ad-hoc filters\n      const adHocFilters = [\n        { key: 'column', operator: '=', value: 'value' },\n        { key: 'column.nested', operator: '=', value: 'value2' }\n      ];\n\n      // Mock getAdhocFilters to return our test filters\n      jest.spyOn(templateSrvMock, 'getAdhocFilters').mockImplementation(() => adHocFilters);\n\n      // Mock adHocFilter.apply to return our expected modified SQL\n      const applyFilterSpy = jest.spyOn(adHocFilter, 'apply').mockImplementation(() => sqlWithAdHocFilters);\n\n      // Create datasource instance with our mocked ad-hoc filter\n      const ds = createInstance({});\n      ds.adHocFilter = adHocFilter;\n\n      // Resolve variables\n      const result = ds.applyTemplateVariables(query, {}, adHocFilters);\n\n      // Verify template variables were resolved before ad-hoc filters were applied\n      expect(spyOnReplace).toHaveBeenCalled();\n      expect(spyOnGetVars).toHaveBeenCalled();\n\n      // Verify that apply was called with the resolved SQL\n      expect(applyFilterSpy).toHaveBeenCalledWith(resolvedSql, adHocFilters, false);\n\n      // Verify that the final query contains the ad-hoc filters\n      expect(result.rawSql).toEqual(sqlWithAdHocFilters);\n    });\n\n    it('should apply ad-hoc filters correctly with JSON and template variables for table names', async () => {\n      // Setup the query with template variables for table names\n      const query = {\n        rawSql: 'SELECT * FROM ${database}.${table}',\n        editorType: EditorType.SQL,\n      } as CHQuery;\n\n      // Mock the ad-hoc filter\n      const adHocFilter = new AdHocFilter();\n\n      // The resolved table name after template variable substitution\n      const resolvedSql = 'SELECT * FROM test_db.test_table';\n\n      // The expected final SQL with ad-hoc filters applied\n      const sqlWithAdHocFilters = `SELECT * FROM test_db.test_table settings additional_table_filters={'test_db.test_table' : ' column = \\\\'value\\\\' '}`;\n\n      // Mock the template variable resolution\n      const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation(() => resolvedSql);\n      const spyOnGetVars = jest.spyOn(templateSrvMock, 'getVariables').mockImplementation(() => [{name: 'clickhouse_adhoc_use_json'}]);\n\n      // Setup ad-hoc filters\n      const adHocFilters = [\n        { key: 'column', operator: '=', value: 'value' },\n        { key: 'column.nested', operator: '=', value: 'value2' }\n      ];\n\n      // Mock getAdhocFilters to return our test filters\n      jest.spyOn(templateSrvMock, 'getAdhocFilters').mockImplementation(() => adHocFilters);\n\n      // Mock adHocFilter.apply to return our expected modified SQL\n      const applyFilterSpy = jest.spyOn(adHocFilter, 'apply').mockImplementation(() => sqlWithAdHocFilters);\n\n      // Create datasource instance with our mocked ad-hoc filter\n      const ds = createInstance({});\n      ds.adHocFilter = adHocFilter;\n\n      // Resolve variables\n      const result = ds.applyTemplateVariables(query, {}, adHocFilters);\n\n      // Verify template variables were resolved before ad-hoc filters were applied\n      expect(spyOnReplace).toHaveBeenCalled();\n      expect(spyOnGetVars).toHaveBeenCalled();\n\n      // Verify that apply was called with the resolved SQL\n      expect(applyFilterSpy).toHaveBeenCalledWith(resolvedSql, adHocFilters, true);\n\n      // Verify that the final query contains the ad-hoc filters\n      expect(result.rawSql).toEqual(sqlWithAdHocFilters);\n    });\n\n\n    it('should expand $__adHocFilters macro with single quotes', async () => {\n      const query = {\n        rawSql: \"SELECT * FROM complex_table settings $__adHocFilters('my_table')\",\n        editorType: EditorType.SQL,\n      } as CHQuery;\n\n      const adHocFilters = [\n        { key: 'key', operator: '=', value: 'val' },\n        { key: 'keyNum', operator: '=', value: '123' },\n      ];\n\n      const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation((x) => x);\n      const spyOnGetVars = jest.spyOn(templateSrvMock, 'getVariables').mockImplementation(() => []);\n\n      const result = createInstance({}).applyTemplateVariables(query, {}, adHocFilters);\n\n      expect(spyOnReplace).toHaveBeenCalled();\n      expect(spyOnGetVars).toHaveBeenCalled();\n      expect(result.rawSql).toEqual(\n        \"SELECT * FROM complex_table settings additional_table_filters={'my_table': ' key = \\\\'val\\\\' AND keyNum = \\\\'123\\\\' '}\"\n      );\n    });\n\n    it('should expand $__adHocFilters macro with double quotes', async () => {\n      const query = {\n        rawSql: 'SELECT * FROM complex_table settings $__adHocFilters(\"my_table\")',\n        editorType: EditorType.SQL,\n      } as CHQuery;\n\n      const adHocFilters = [{ key: 'key', operator: '=', value: 'val' }];\n\n      const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation((x) => x);\n      const spyOnGetVars = jest.spyOn(templateSrvMock, 'getVariables').mockImplementation(() => []);\n\n      const result = createInstance({}).applyTemplateVariables(query, {}, adHocFilters);\n\n      expect(spyOnReplace).toHaveBeenCalled();\n      expect(spyOnGetVars).toHaveBeenCalled();\n      expect(result.rawSql).toEqual(\n        \"SELECT * FROM complex_table settings additional_table_filters={'my_table': ' key = \\\\'val\\\\' '}\"\n      );\n    });\n\n    it('should expand $__adHocFilters macro to empty object when no filters are present', async () => {\n      const query = {\n        rawSql: \"SELECT * FROM complex_table settings $__adHocFilters('my_table')\",\n        editorType: EditorType.SQL,\n      } as CHQuery;\n\n      const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation((x) => x);\n      const spyOnGetVars = jest.spyOn(templateSrvMock, 'getVariables').mockImplementation(() => []);\n\n      const result = createInstance({}).applyTemplateVariables(query, {}, []);\n\n      expect(spyOnReplace).toHaveBeenCalled();\n      expect(spyOnGetVars).toHaveBeenCalled();\n      expect(result.rawSql).toEqual('SELECT * FROM complex_table settings additional_table_filters={}');\n    });\n\n    it('should handle $__adHocFilters macro with spaces', async () => {\n      const query = {\n        rawSql: \"SELECT * FROM complex_table settings $__adHocFilters(  'my_table'  )\",\n        editorType: EditorType.SQL,\n      } as CHQuery;\n\n      const adHocFilters = [{ key: 'key', operator: '=', value: 'val' }];\n\n      const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation((x) => x);\n      const spyOnGetVars = jest.spyOn(templateSrvMock, 'getVariables').mockImplementation(() => []);\n\n      const result = createInstance({}).applyTemplateVariables(query, {}, adHocFilters);\n\n      expect(spyOnReplace).toHaveBeenCalled();\n      expect(spyOnGetVars).toHaveBeenCalled();\n      expect(result.rawSql).toEqual(\n        \"SELECT * FROM complex_table settings additional_table_filters={'my_table': ' key = \\\\'val\\\\' '}\"\n      );\n    });\n\n    it('should expand $__adHocFilters macro with multiple tables', async () => {\n      const query = {\n        rawSql: \"SELECT * FROM complex_table settings $__adHocFilters('table1', 'table2')\",\n        editorType: EditorType.SQL,\n      } as CHQuery;\n\n      const adHocFilters = [\n        { key: 'key', operator: '=', value: 'val' },\n        { key: 'keyNum', operator: '=', value: '123' },\n      ];\n\n      const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation((x) => x);\n      const spyOnGetVars = jest.spyOn(templateSrvMock, 'getVariables').mockImplementation(() => []);\n\n      const result = createInstance({}).applyTemplateVariables(query, {}, adHocFilters);\n\n      expect(spyOnReplace).toHaveBeenCalled();\n      expect(spyOnGetVars).toHaveBeenCalled();\n      expect(result.rawSql).toEqual(\n        \"SELECT * FROM complex_table settings additional_table_filters={'table1': ' key = \\\\'val\\\\' AND keyNum = \\\\'123\\\\' ', 'table2': ' key = \\\\'val\\\\' AND keyNum = \\\\'123\\\\' '}\"\n      );\n    });\n\n    it('should expand $__adHocFilters macro with multiple tables using double quotes', async () => {\n      const query = {\n        rawSql: 'SELECT * FROM complex_table settings $__adHocFilters(\"table1\", \"table2\", \"table3\")',\n        editorType: EditorType.SQL,\n      } as CHQuery;\n\n      const adHocFilters = [{ key: 'key', operator: '=', value: 'val' }];\n\n      const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation((x) => x);\n      const spyOnGetVars = jest.spyOn(templateSrvMock, 'getVariables').mockImplementation(() => []);\n\n      const result = createInstance({}).applyTemplateVariables(query, {}, adHocFilters);\n\n      expect(spyOnReplace).toHaveBeenCalled();\n      expect(spyOnGetVars).toHaveBeenCalled();\n      expect(result.rawSql).toEqual(\n        \"SELECT * FROM complex_table settings additional_table_filters={'table1': ' key = \\\\'val\\\\' ', 'table2': ' key = \\\\'val\\\\' ', 'table3': ' key = \\\\'val\\\\' '}\"\n      );\n    });\n  });\n\n  describe('Tag Keys', () => {\n    it('should Fetch Default Tags When No Second AdHoc Variable', async () => {\n      const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation(() => '$clickhouse_adhoc_query');\n      const ds = cloneDeep(mockDatasource);\n      const frame = arrayToDataFrame([{ name: 'foo', type: 'string', table: 'table' }]);\n      jest.spyOn(ds, 'getDefaultDatabase').mockImplementation(() => undefined!); // Disable default DB\n      const spyOnQuery = jest.spyOn(ds, 'query').mockImplementation((_request) => of({ data: [frame] }));\n\n      const keys = await ds.getTagKeys();\n      expect(spyOnReplace).toHaveBeenCalled();\n      const expected = { rawSql: 'SELECT name, type, table FROM system.columns' };\n\n      expect(spyOnQuery).toHaveBeenCalledWith(\n        expect.objectContaining({ targets: expect.arrayContaining([expect.objectContaining(expected)]) })\n      );\n\n      expect(keys).toEqual([{ text: 'table.foo' }]);\n    });\n\n    it('should Fetch Tags With Default Database', async () => {\n      const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation(() => '$clickhouse_adhoc_query');\n      const frame = arrayToDataFrame([{ name: 'foo', type: 'string', table: 'table' }]);\n      const ds = cloneDeep(mockDatasource);\n      const spyOnQuery = jest.spyOn(ds, 'query').mockImplementation((_request) => of({ data: [frame] }));\n\n      const keys = await ds.getTagKeys();\n      expect(spyOnReplace).toHaveBeenCalled();\n      const expected = { rawSql: \"SELECT name, type, table FROM system.columns WHERE database IN ('foo')\" };\n\n      expect(spyOnQuery).toHaveBeenCalledWith(\n        expect.objectContaining({ targets: expect.arrayContaining([expect.objectContaining(expected)]) })\n      );\n\n      expect(keys).toEqual([{ text: 'table.foo' }]);\n    });\n\n    it('should Fetch Tags From Query', async () => {\n      const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation(() => 'select name from foo');\n      const frame = arrayToDataFrame([{ name: 'foo' }]);\n      const ds = cloneDeep(mockDatasource);\n      const spyOnQuery = jest.spyOn(ds, 'query').mockImplementation((_request) => of({ data: [frame] }));\n\n      const keys = await ds.getTagKeys();\n      expect(spyOnReplace).toHaveBeenCalled();\n      const expected = { rawSql: 'select name from foo' };\n\n      expect(spyOnQuery).toHaveBeenCalledWith(\n        expect.objectContaining({ targets: expect.arrayContaining([expect.objectContaining(expected)]) })\n      );\n\n      expect(keys).toEqual([{ text: 'name' }]);\n    });\n    it('returns no tags when CH version is less than 22.7 ', async () => {\n      const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation(() => 'select name from foo');\n      const frame = arrayToDataFrame([{ version: '21.9.342' }]);\n      const ds = cloneDeep(mockDatasource);\n      ds.adHocFiltersStatus = 2;\n      const spyOnQuery = jest.spyOn(ds, 'query').mockImplementation((_request) => of({ data: [frame] }));\n\n      const keys = await ds.getTagKeys();\n      expect(spyOnReplace).toHaveBeenCalled();\n\n      expect(spyOnQuery).toHaveBeenCalled();\n\n      expect(keys).toEqual({});\n    });\n\n    it('returns tags when CH version is greater than 22.7 ', async () => {\n      const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation(() => 'select name from foo');\n      const frameVer = arrayToDataFrame([{ version: '23.2.212' }]);\n      const frameData = arrayToDataFrame([{ name: 'foo' }]);\n      const ds = cloneDeep(mockDatasource);\n      ds.adHocFiltersStatus = 2;\n      const spyOnQuery = jest.spyOn(ds, 'query').mockImplementation((request) => {\n        return request.targets[0].rawSql === 'SELECT version()' ? of({ data: [frameVer] }) : of({ data: [frameData] });\n      });\n\n      const keys = await ds.getTagKeys();\n      expect(spyOnReplace).toHaveBeenCalled();\n\n      expect(spyOnQuery).toHaveBeenCalled();\n\n      expect(keys).toEqual([{ text: 'name' }]);\n    });\n  });\n\n  describe('Tag Values', () => {\n    it('should Fetch Tag Values from Schema', async () => {\n      const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation(() => '$clickhouse_adhoc_query');\n      const ds = cloneDeep(mockDatasource);\n      ds.settings.jsonData.defaultDatabase = undefined;\n      const frame = arrayToDataFrame([{ bar: 'foo' }]);\n      const spyOnQuery = jest.spyOn(ds, 'query').mockImplementation((_request) => of({ data: [frame] }));\n      const values = await ds.getTagValues({ key: 'foo.bar' });\n      expect(spyOnReplace).toHaveBeenCalled();\n      const expected = { rawSql: 'select distinct bar from foo limit 1000' };\n\n      expect(spyOnQuery).toHaveBeenCalledWith(\n        expect.objectContaining({ targets: expect.arrayContaining([expect.objectContaining(expected)]) })\n      );\n\n      expect(values).toEqual([{ text: 'foo' }]);\n    });\n\n    it('should Fetch Tag Values from Query', async () => {\n      const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation(() => 'select name from bar');\n      const ds = cloneDeep(mockDatasource);\n      const frame = arrayToDataFrame([{ name: 'foo' }]);\n      const spyOnQuery = jest.spyOn(ds, 'query').mockImplementation((_request) => of({ data: [frame] }));\n      const values = await ds.getTagValues({ key: 'name' });\n      expect(spyOnReplace).toHaveBeenCalled();\n      const expected = { rawSql: 'select name from bar' };\n\n      expect(spyOnQuery).toHaveBeenCalledWith(\n        expect.objectContaining({ targets: expect.arrayContaining([expect.objectContaining(expected)]) })\n      );\n\n      expect(values).toEqual([{ text: 'foo' }]);\n    });\n\n    it('should Fetch Tag Values from Schema with . in column name', async () => {\n      const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation(() => '$clickhouse_adhoc_query');\n      const ds = cloneDeep(mockDatasource);\n      ds.settings.jsonData.defaultDatabase = undefined;\n      const frame = arrayToDataFrame([{ ['bar.fizz']: 'foo' }]);\n      const spyOnQuery = jest.spyOn(ds, 'query').mockImplementation((_request) => of({ data: [frame] }));\n      const values = await ds.getTagValues({ key: 'foo.bar.fizz' });\n      expect(spyOnReplace).toHaveBeenCalled();\n      const expected = { rawSql: 'select distinct bar.fizz from foo limit 1000' };\n\n      expect(spyOnQuery).toHaveBeenCalledWith(\n        expect.objectContaining({ targets: expect.arrayContaining([expect.objectContaining(expected)]) })\n      );\n\n      expect(values).toEqual([{ text: 'foo' }]);\n    });\n  });\n\n  describe('Hide Table Name In AdHoc Filters', () => {\n    it('should return only column names when hideTableNameInAdhocFilters is true', async () => {\n      jest.spyOn(templateSrvMock, 'replace').mockImplementation(() => 'foo');\n      const ds = cloneDeep(mockDatasource);\n      ds.settings.jsonData.hideTableNameInAdhocFilters = true;\n      const frame = arrayToDataFrame([{ name: 'foo', type: 'String', table: 'table' }]);\n      jest.spyOn(ds, 'query').mockImplementation((_request) => of({ data: [frame] }));\n\n      const keys = await ds.getTagKeys();\n      expect(keys).toEqual([{ text: 'foo' }]);\n    });\n\n    it('should return table.column when hideTableNameInAdhocFilters is false', async () => {\n      jest.spyOn(templateSrvMock, 'replace').mockImplementation(() => 'foo');\n      const ds = cloneDeep(mockDatasource);\n      ds.settings.jsonData.hideTableNameInAdhocFilters = false;\n      const frame = arrayToDataFrame([{ name: 'foo', type: 'String', table: 'table' }]);\n      jest.spyOn(ds, 'query').mockImplementation((_request) => of({ data: [frame] }));\n\n      const keys = await ds.getTagKeys();\n      expect(keys).toEqual([{ text: 'table.foo' }]);\n    });\n\n    it('should return table.column when hideTableNameInAdhocFilters is undefined (default)', async () => {\n      jest.spyOn(templateSrvMock, 'replace').mockImplementation(() => 'foo');\n      const ds = cloneDeep(mockDatasource);\n      ds.settings.jsonData.hideTableNameInAdhocFilters = undefined;\n      const frame = arrayToDataFrame([{ name: 'foo', type: 'String', table: 'table' }]);\n      jest.spyOn(ds, 'query').mockImplementation((_request) => of({ data: [frame] }));\n\n      const keys = await ds.getTagKeys();\n      expect(keys).toEqual([{ text: 'table.foo' }]);\n    });\n\n    it('should fetch tag values with column name when hideTableNameInAdhocFilters is true', async () => {\n      const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation(() => 'foo');\n      const ds = cloneDeep(mockDatasource);\n      ds.settings.jsonData.hideTableNameInAdhocFilters = true;\n      const frame = arrayToDataFrame([{ bar: 'value1' }, { bar: 'value2' }]);\n      const spyOnQuery = jest.spyOn(ds, 'query').mockImplementation((_request) => of({ data: [frame] }));\n\n      const values = await ds.getTagValues({ key: 'bar' });\n      expect(spyOnReplace).toHaveBeenCalled();\n      const expected = { rawSql: 'select distinct bar from foo limit 1000' };\n\n      expect(spyOnQuery).toHaveBeenCalledWith(\n        expect.objectContaining({ targets: expect.arrayContaining([expect.objectContaining(expected)]) })\n      );\n\n      expect(values).toEqual([{ text: 'value1' }, { text: 'value2' }]);\n    });\n\n    it('should fetch tag values with table.column format when hideTableNameInAdhocFilters is false', async () => {\n      const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation(() => '$clickhouse_adhoc_query');\n      const ds = cloneDeep(mockDatasource);\n      ds.settings.jsonData.defaultDatabase = undefined;\n      ds.settings.jsonData.hideTableNameInAdhocFilters = false;\n      const frame = arrayToDataFrame([{ bar: 'value1' }, { bar: 'value2' }]);\n      const spyOnQuery = jest.spyOn(ds, 'query').mockImplementation((_request) => of({ data: [frame] }));\n\n      const values = await ds.getTagValues({ key: 'foo.bar' });\n      expect(spyOnReplace).toHaveBeenCalled();\n      const expected = { rawSql: 'select distinct bar from foo limit 1000' };\n\n      expect(spyOnQuery).toHaveBeenCalledWith(\n        expect.objectContaining({ targets: expect.arrayContaining([expect.objectContaining(expected)]) })\n      );\n\n      expect(values).toEqual([{ text: 'value1' }, { text: 'value2' }]);\n    });\n\n    it('should handle nested column names with dots when hideTableNameInAdhocFilters is true', async () => {\n      const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation(() => 'foo');\n      const ds = cloneDeep(mockDatasource);\n      ds.settings.jsonData.hideTableNameInAdhocFilters = true;\n      const frame = arrayToDataFrame([{ 'nested.field': 'value1' }, { 'nested.field': 'value2' }]);\n      const spyOnQuery = jest.spyOn(ds, 'query').mockImplementation((_request) => of({ data: [frame] }));\n\n      const values = await ds.getTagValues({ key: 'nested.field' });\n      expect(spyOnReplace).toHaveBeenCalled();\n      const expected = { rawSql: 'select distinct nested.field from foo limit 1000' };\n\n      expect(spyOnQuery).toHaveBeenCalledWith(\n        expect.objectContaining({ targets: expect.arrayContaining([expect.objectContaining(expected)]) })\n      );\n\n      expect(values).toEqual([{ text: 'value1' }, { text: 'value2' }]);\n    });\n\n    it('should handle nested column names with dots when hideTableNameInAdhocFilters is false', async () => {\n      const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation(() => '$clickhouse_adhoc_query');\n      const ds = cloneDeep(mockDatasource);\n      ds.settings.jsonData.defaultDatabase = undefined;\n      ds.settings.jsonData.hideTableNameInAdhocFilters = false;\n      const frame = arrayToDataFrame([{ 'nested.field': 'value1' }, { 'nested.field': 'value2' }]);\n      const spyOnQuery = jest.spyOn(ds, 'query').mockImplementation((_request) => of({ data: [frame] }));\n\n      const values = await ds.getTagValues({ key: 'foo.nested.field' });\n      expect(spyOnReplace).toHaveBeenCalled();\n      const expected = { rawSql: 'select distinct nested.field from foo limit 1000' };\n\n      expect(spyOnQuery).toHaveBeenCalledWith(\n        expect.objectContaining({ targets: expect.arrayContaining([expect.objectContaining(expected)]) })\n      );\n\n      expect(values).toEqual([{ text: 'value1' }, { text: 'value2' }]);\n    });\n  });\n\n  describe('Conditional All', () => {\n    it('should replace $__conditionalAll with 1=1 when all is selected', async () => {\n      const rawSql = 'select stuff from table where $__conditionalAll(fieldVal in ($fieldVal), $fieldVal);';\n      const val = createInstance({}).applyConditionalAll(rawSql, [\n        { name: 'fieldVal', current: { value: '$__all' } } as any,\n      ]);\n      expect(val).toEqual('select stuff from table where 1=1;');\n    });\n    it('should replace $__conditionalAll with arg when anything else is selected', async () => {\n      const rawSql = 'select stuff from table where $__conditionalAll(fieldVal in ($fieldVal), $fieldVal);';\n      const val = createInstance({}).applyConditionalAll(rawSql, [\n        { name: 'fieldVal', current: { value: `'val1', 'val2'` } } as any,\n      ]);\n      expect(val).toEqual(`select stuff from table where fieldVal in ($fieldVal);`);\n    });\n    it('should replace all $__conditionalAll', async () => {\n      const rawSql =\n        'select stuff from table where $__conditionalAll(fieldVal in ($fieldVal), $fieldVal) and $__conditionalAll(fieldVal in ($fieldVal2), $fieldVal2);';\n      const val = createInstance({}).applyConditionalAll(rawSql, [\n        { name: 'fieldVal', current: { value: `'val1', 'val2'` } } as any,\n        { name: 'fieldVal2', current: { value: '$__all' } } as any,\n      ]);\n      expect(val).toEqual(`select stuff from table where fieldVal in ($fieldVal) and 1=1;`);\n    });\n  });\n\n  describe.skip('fetchPathsForJSONColumns', () => {\n    it('sends a correct query when database and table names are provided', async () => {\n      const ds = cloneDeep(mockDatasource);\n      const frame = arrayToDataFrame([\n        JSON.stringify({ keys: 'a.b.c', values: ['Int64'] }),\n        JSON.stringify({ keys: 'a.b.d', values: ['String'] }),\n        JSON.stringify({ keys: 'a.b.e', values: ['Bool'] }),\n      ]);\n      const spyOnQuery = jest.spyOn(ds, 'query').mockImplementation((_request) => of({ data: [frame] }));\n      await ds.fetchPathsForJSONColumns('db_name', 'table_name', 'jsonCol');\n      const expected = {\n        rawSql:\n          'SELECT arrayJoin(distinctJSONPathsAndTypes(jsonCol)) FROM \"db_name\".\"table_name\" SETTINGS max_execution_time=10',\n      };\n\n      expect(spyOnQuery).toHaveBeenCalledWith(\n        expect.objectContaining({ targets: expect.arrayContaining([expect.objectContaining(expected)]) })\n      );\n    });\n\n    it('sends a correct query when only table name is provided', async () => {\n      const ds = cloneDeep(mockDatasource);\n      const frame = arrayToDataFrame([\n        JSON.stringify({ keys: 'a.b.c', values: ['Int64'] }),\n        JSON.stringify({ keys: 'a.b.d', values: ['String'] }),\n        JSON.stringify({ keys: 'a.b.e', values: ['Bool'] }),\n      ]);\n      const spyOnQuery = jest.spyOn(ds, 'query').mockImplementation((_request) => of({ data: [frame] }));\n      await ds.fetchPathsForJSONColumns('', 'table_name', 'jsonCol');\n      const expected = {\n        rawSql: 'SELECT arrayJoin(distinctJSONPathsAndTypes(jsonCol)) FROM \"table_name\" SETTINGS max_execution_time=10',\n      };\n\n      expect(spyOnQuery).toHaveBeenCalledWith(\n        expect.objectContaining({ targets: expect.arrayContaining([expect.objectContaining(expected)]) })\n      );\n    });\n\n    it('sends a correct query when table name contains a dot', async () => {\n      const ds = cloneDeep(mockDatasource);\n      const frame = arrayToDataFrame([\n        JSON.stringify({ keys: 'a.b.c', values: ['Int64'] }),\n        JSON.stringify({ keys: 'a.b.d', values: ['String'] }),\n        JSON.stringify({ keys: 'a.b.e', values: ['Bool'] }),\n      ]);\n      const spyOnQuery = jest.spyOn(ds, 'query').mockImplementation((_request) => of({ data: [frame] }));\n      await ds.fetchPathsForJSONColumns('', 'table.name', 'jsonCol');\n      const expected = {\n        rawSql: 'SELECT arrayJoin(distinctJSONPathsAndTypes(jsonCol)) FROM \"table.name\" SETTINGS max_execution_time=10',\n      };\n\n      expect(spyOnQuery).toHaveBeenCalledWith(\n        expect.objectContaining({ targets: expect.arrayContaining([expect.objectContaining(expected)]) })\n      );\n    });\n\n    it('returns correct json columns', async () => {\n      const ds = cloneDeep(mockDatasource);\n      const frame = arrayToDataFrame([\n        JSON.stringify({ keys: 'a.b.c', values: ['Int64'] }),\n        JSON.stringify({ keys: 'a.b.d', values: ['String'] }),\n        JSON.stringify({ keys: 'a.b.e', values: ['Bool'] }),\n      ]);\n      jest.spyOn(ds, 'query').mockImplementation((_request) => of({ data: [frame] }));\n\n      const jsonColumns = await ds.fetchPathsForJSONColumns('db_name', 'table_name', 'jsonCol');\n      expect(jsonColumns).toMatchObject([\n        { name: 'jsonCol.a.b.c', label: 'jsonCol.a.b.c', type: 'Int64', picklistValues: [] },\n        { name: 'jsonCol.a.b.d', label: 'jsonCol.a.b.d', type: 'String', picklistValues: [] },\n        { name: 'jsonCol.a.b.e', label: 'jsonCol.a.b.e', type: 'Bool', picklistValues: [] },\n      ]);\n    });\n  });\n\n  describe('fetchColumnsFromTable', () => {\n    it('sends a correct query when database and table names are provided', async () => {\n      const ds = cloneDeep(mockDatasource);\n      const frame = arrayToDataFrame([{ name: 'foo', type: 'string', table: 'table' }]);\n      const spyOnQuery = jest.spyOn(ds, 'query').mockImplementation((_request) => of({ data: [frame] }));\n      await ds.fetchColumnsFromTable('db_name', 'table_name');\n      const expected = { rawSql: 'DESC TABLE \"db_name\".\"table_name\"' };\n\n      expect(spyOnQuery).toHaveBeenCalledWith(\n        expect.objectContaining({ targets: expect.arrayContaining([expect.objectContaining(expected)]) })\n      );\n    });\n\n    it('sends a correct query when only table name is provided', async () => {\n      const ds = cloneDeep(mockDatasource);\n      const frame = arrayToDataFrame([{ name: 'foo', type: 'string', table: 'table' }]);\n      const spyOnQuery = jest.spyOn(ds, 'query').mockImplementation((_request) => of({ data: [frame] }));\n      await ds.fetchColumnsFromTable('', 'table_name');\n      const expected = { rawSql: 'DESC TABLE \"table_name\"' };\n\n      expect(spyOnQuery).toHaveBeenCalledWith(\n        expect.objectContaining({ targets: expect.arrayContaining([expect.objectContaining(expected)]) })\n      );\n    });\n\n    it('sends a correct query when table name contains a dot', async () => {\n      const ds = cloneDeep(mockDatasource);\n      const frame = arrayToDataFrame([{ name: 'foo', type: 'string', table: 'table' }]);\n      const spyOnQuery = jest.spyOn(ds, 'query').mockImplementation((_) => of({ data: [frame] }));\n\n      await ds.fetchColumnsFromTable('', 'table.name');\n      const expected = { rawSql: 'DESC TABLE \"table.name\"' };\n\n      expect(spyOnQuery).toHaveBeenCalledWith(\n        expect.objectContaining({ targets: expect.arrayContaining([expect.objectContaining(expected)]) })\n      );\n    });\n  });\n\n  describe('fetchColumnsFromAliasTable', () => {\n    it('sends a correct query when full table name is provided', async () => {\n      const ds = cloneDeep(mockDatasource);\n      const frame = arrayToDataFrame([{ name: 'foo', type: 'string', table: 'table' }]);\n      const spyOnQuery = jest.spyOn(ds, 'query').mockImplementation((_request) => of({ data: [frame] }));\n      await ds.fetchColumnsFromAliasTable('\"db_name\".\"table_name\"');\n      const expected = { rawSql: 'SELECT alias, select, \"type\" FROM \"db_name\".\"table_name\"' };\n\n      expect(spyOnQuery).toHaveBeenCalledWith(\n        expect.objectContaining({ targets: expect.arrayContaining([expect.objectContaining(expected)]) })\n      );\n    });\n  });\n\n  describe('getAliasTable', () => {\n    it('returns the matching table alias', async () => {\n      const ds = cloneDeep(mockDatasource);\n      ds.settings.jsonData.aliasTables = [\n        {\n          targetDatabase: 'db_name',\n          targetTable: 'table_name',\n          aliasDatabase: 'alias_db',\n          aliasTable: 'alias_table',\n        },\n      ];\n      const result = ds.getAliasTable('db_name', 'table_name');\n      const expected = '\"alias_db\".\"alias_table\"';\n\n      expect(result).toBe(expected);\n    });\n\n    it('returns null when no alias matches found', async () => {\n      const ds = cloneDeep(mockDatasource);\n      ds.settings.jsonData.aliasTables = [\n        {\n          targetDatabase: 'db_name',\n          targetTable: 'table_name',\n          aliasDatabase: 'alias_db',\n          aliasTable: 'alias_table',\n        },\n      ];\n      const result = ds.getAliasTable('other_db', 'other_table');\n      expect(result).toBeNull();\n    });\n  });\n\n  describe('filterQuery', () => {\n    it('returns true when hide is not set', () => {\n      expect(mockDatasource.filterQuery({ refId: '1' } as CHQuery)).toBe(true);\n    });\n\n    it('returns true when hide is false', () => {\n      expect(mockDatasource.filterQuery({ refId: '1', hide: false } as CHQuery)).toBe(true);\n    });\n\n    it('returns false when hide is true', () => {\n      expect(mockDatasource.filterQuery({ refId: '1', hide: true } as CHQuery)).toBe(false);\n    });\n  });\n\n  describe('query', () => {\n    it('attaches timezone metadata to targets', async () => {\n      const instance = cloneDeep(mockDatasource);\n      const spy = jest\n        .spyOn(DataSourceWithBackend.prototype, 'query')\n        .mockImplementation((_request) => of({ data: [toDataFrame([])] }));\n      instance.query({\n        targets: [{ refId: '1' }, { refId: '2', hide: false }] as DataQuery[],\n        timezone: 'UTC',\n      } as any);\n\n      expect(spy).toHaveBeenCalledWith({\n        targets: [\n          { refId: '1', meta: { timezone: 'UTC' } },\n          { refId: '2', hide: false, meta: { timezone: 'UTC' } },\n        ],\n        timezone: 'UTC',\n      });\n    });\n  });\n\n  describe('SupplementaryQueriesSupport', () => {\n    const query: CHBuilderQuery = {\n      pluginVersion: '',\n      refId: '42',\n      editorType: EditorType.Builder,\n      rawSql: 'SELECT * FROM system.numbers LIMIT 1',\n      builderOptions: {\n        database: 'default',\n        table: 'logs',\n        queryType: QueryType.Logs,\n        mode: BuilderMode.List,\n        columns: [\n          { name: 'created_at', hint: ColumnHint.Time },\n          { name: 'level', hint: ColumnHint.LogLevel },\n        ],\n      },\n    };\n    const request: DataQueryRequest<CHQuery> = {\n      app: CoreApp.Explore,\n      interval: '1s',\n      intervalMs: 1000,\n      range: {} as TimeRange,\n      requestId: '',\n      scopedVars: {\n        __interval_ms: {\n          text: '',\n          value: 60001,\n        },\n      },\n      startTime: 0,\n      targets: [],\n      timezone: '',\n    };\n\n    let datasource: Datasource;\n    beforeEach(() => {\n      datasource = cloneDeep(mockDatasource);\n    });\n\n    describe('getSupportedSupplementaryQueryTypes', () => {\n      it('should return LogsVolume and LogsSample for empty dsRequest', async () => {\n        const dsRequest = { targets: [{ editorType: EditorType.Builder }] } as DataQueryRequest<CHQuery>;\n        const result = datasource.getSupportedSupplementaryQueryTypes(dsRequest);\n        expect(result).toEqual([SupplementaryQueryType.LogsVolume, SupplementaryQueryType.LogsSample]);\n      });\n\n      it('should return LogsVolume and LogsSample when all targets use Builder editor', async () => {\n        const dsRequest: DataQueryRequest<CHQuery> = {\n          ...request,\n          targets: [\n            {\n              ...query,\n              editorType: EditorType.Builder,\n            },\n          ],\n        };\n        const result = datasource.getSupportedSupplementaryQueryTypes(dsRequest);\n        expect(result).toEqual([SupplementaryQueryType.LogsVolume, SupplementaryQueryType.LogsSample]);\n      });\n\n      it('should return empty array when any target uses SQL editor', async () => {\n        const dsRequest: DataQueryRequest<CHQuery> = {\n          ...request,\n          targets: [\n            {\n              ...query,\n              editorType: EditorType.SQL,\n              queryType: query.builderOptions.queryType,\n            },\n          ],\n        };\n        const result = datasource.getSupportedSupplementaryQueryTypes(dsRequest);\n        expect(result).toEqual([]);\n      });\n    });\n\n    describe('getSupplementaryLogsVolumeQuery', () => {\n      it('should return undefined if any of the conditions are not met', async () => {\n        [QueryType.Table, QueryType.TimeSeries, QueryType.Traces].forEach((queryType) => {\n          expect(\n            datasource.getSupplementaryLogsVolumeQuery(request, {\n              ...query,\n              builderOptions: {\n                ...query.builderOptions,\n                queryType,\n              },\n            })\n          ).toBeUndefined();\n        });\n        [BuilderMode.Aggregate, BuilderMode.Trend].forEach((mode) => {\n          expect(\n            datasource.getSupplementaryLogsVolumeQuery(request, {\n              ...query,\n              builderOptions: {\n                ...query.builderOptions,\n                mode,\n              },\n            })\n          ).toBeUndefined();\n        });\n        expect(\n          datasource.getSupplementaryLogsVolumeQuery(request, {\n            ...query,\n            editorType: EditorType.SQL,\n            queryType: undefined,\n          })\n        ).toBeUndefined();\n        expect(\n          datasource.getSupplementaryLogsVolumeQuery(request, {\n            ...query,\n            builderOptions: {\n              ...query.builderOptions,\n              database: '',\n            },\n          })\n        ).toBeUndefined();\n        expect(\n          datasource.getSupplementaryLogsVolumeQuery(request, {\n            ...query,\n            builderOptions: {\n              ...query.builderOptions,\n              table: '',\n            },\n          })\n        ).toBeUndefined();\n        expect(\n          datasource.getSupplementaryLogsVolumeQuery(request, {\n            ...query,\n            builderOptions: {\n              ...query.builderOptions,\n              columns: query.builderOptions.columns?.filter((c) => c.hint !== ColumnHint.Time),\n            } as QueryBuilderOptions,\n          })\n        ).toBeUndefined();\n      });\n\n      it('should render a basic query if we have no log level field set', async () => {\n        jest\n          .spyOn(logs, 'getTimeFieldRoundingClause')\n          .mockReturnValue('toStartOfInterval(\"created_at\", INTERVAL 1 DAY)');\n        const result = datasource.getSupplementaryLogsVolumeQuery(request, {\n          ...query,\n          builderOptions: {\n            ...query.builderOptions,\n            columns: query.builderOptions.columns?.filter((c) => c.hint !== ColumnHint.LogLevel),\n          } as QueryBuilderOptions,\n        });\n        expect(result?.rawSql).toEqual(\n          'SELECT toStartOfInterval(\"created_at\", INTERVAL 1 DAY) as \"time\", count(*) as logs ' +\n            'FROM \"default\".\"logs\" ' +\n            'GROUP BY time ' +\n            'ORDER BY time ASC'\n        );\n      });\n\n      it('should render a sophisticated log volume query when log level field is set', async () => {\n        jest\n          .spyOn(logs, 'getTimeFieldRoundingClause')\n          .mockReturnValue('toStartOfInterval(\"created_at\", INTERVAL 1 DAY)');\n        const result = datasource.getSupplementaryLogsVolumeQuery(request, query);\n        expect(result?.rawSql).toEqual(\n          `SELECT toStartOfInterval(\"created_at\", INTERVAL 1 DAY) as \"time\", ` +\n            `sum(multiSearchAny(toString(\"level\"), ['critical','fatal','crit','alert','emerg','CRITICAL','FATAL','CRIT','ALERT','EMERG','Critical','Fatal','Crit','Alert','Emerg'])) as critical, ` +\n            `sum(multiSearchAny(toString(\"level\"), ['error','err','eror','ERROR','ERR','EROR','Error','Err','Eror'])) as error, ` +\n            `sum(multiSearchAny(toString(\"level\"), ['warn','warning','WARN','WARNING','Warn','Warning'])) as warn, ` +\n            `sum(multiSearchAny(toString(\"level\"), ['info','information','informational','INFO','INFORMATION','INFORMATIONAL','Info','Information','Informational'])) as info, ` +\n            `sum(multiSearchAny(toString(\"level\"), ['debug','dbug','DEBUG','DBUG','Debug','Dbug'])) as debug, ` +\n            `sum(multiSearchAny(toString(\"level\"), ['trace','TRACE','Trace'])) as trace, ` +\n            `sum(multiSearchAny(toString(\"level\"), ['unknown','UNKNOWN','Unknown'])) as unknown ` +\n            `FROM \"default\".\"logs\" ` +\n            `GROUP BY time ` +\n            `ORDER BY time ASC`\n        );\n      });\n    });\n\n    describe('getSupplementaryLogsSampleQuery', () => {\n      beforeEach(() => {\n        jest.spyOn(datasource, 'getDefaultLogsTable').mockReturnValue('logs');\n        jest.spyOn(datasource, 'getDefaultLogsColumns').mockReturnValue(\n          new Map<ColumnHint, string>([\n            [ColumnHint.Time, 'created_at'],\n            [ColumnHint.LogLevel, 'level'],\n          ])\n        );\n      });\n\n      it('should return undefined if editorType is not Builder', () => {\n        expect(\n          datasource.getSupplementaryLogsSampleQuery({\n            ...query,\n            editorType: EditorType.SQL,\n            queryType: undefined,\n          } as any)\n        ).toBeUndefined();\n      });\n\n      it('should return undefined if database is empty', () => {\n        expect(\n          datasource.getSupplementaryLogsSampleQuery({\n            ...query,\n            builderOptions: { ...query.builderOptions, database: '' },\n          })\n        ).toBeUndefined();\n      });\n\n      it('should return undefined if table does not match default logs table', () => {\n        expect(\n          datasource.getSupplementaryLogsSampleQuery({\n            ...query,\n            builderOptions: { ...query.builderOptions, table: 'other_table' },\n          })\n        ).toBeUndefined();\n      });\n\n      it('should return undefined if no time column is found', () => {\n        expect(\n          datasource.getSupplementaryLogsSampleQuery({\n            ...query,\n            builderOptions: { ...query.builderOptions, columns: [] },\n          })\n        ).toBeUndefined();\n      });\n\n      it('should return a Logs/List query with format 2, limit 100, ordered DESC by time', () => {\n        const result = datasource.getSupplementaryLogsSampleQuery(query);\n        expect(result).toBeDefined();\n        expect(result?.format).toBe(2);\n        expect(result?.editorType).toBe(EditorType.Builder);\n        const bo = (result as CHBuilderQuery).builderOptions;\n        expect(bo.queryType).toBe(QueryType.Logs);\n        expect(bo.mode).toBe(BuilderMode.List);\n        expect(bo.limit).toBe(100);\n        expect(bo.database).toBe(query.builderOptions.database);\n        expect(bo.table).toBe(query.builderOptions.table);\n        // columns come from getDefaultLogsColumns()\n        expect(bo.columns).toEqual([\n          { hint: ColumnHint.Time, name: 'created_at' },\n          { hint: ColumnHint.LogLevel, name: 'level' },\n        ]);\n        expect(bo.orderBy).toEqual([{ name: '', hint: ColumnHint.Time, dir: OrderByDirection.DESC }]);\n      });\n\n      it('should copy and resolve hint-based filters from the original query', () => {\n        const hintedQuery: CHBuilderQuery = {\n          ...query,\n          builderOptions: {\n            ...query.builderOptions,\n            filters: [\n              {\n                hint: ColumnHint.LogLevel,\n                key: '',\n                operator: FilterOperator.Equals,\n                value: 'error',\n                type: 'string',\n                filterType: 'custom',\n                condition: 'AND'\n              },\n            ],\n          },\n        };\n        const result = datasource.getSupplementaryLogsSampleQuery(hintedQuery) as CHBuilderQuery;\n        expect(result).toBeDefined();\n        // hint-based filter key should be resolved to the actual column name\n        expect(result.builderOptions.filters![0].key).toBe('level');\n      });\n\n      it('should work for any Builder query type, not just Logs', () => {\n        const timeSeriesQuery: CHBuilderQuery = {\n          ...query,\n          builderOptions: {\n            ...query.builderOptions,\n            queryType: QueryType.TimeSeries,\n          },\n        };\n        const result = datasource.getSupplementaryLogsSampleQuery(timeSeriesQuery);\n        expect(result).toBeDefined();\n        expect((result as CHBuilderQuery).builderOptions.queryType).toBe(QueryType.Logs);\n      });\n    });\n\n    describe('getSupplementaryRequest', () => {\n      it('should return undefined for LogsSample if no targets produce a supplementary query', () => {\n        jest.spyOn(Datasource.prototype, 'getSupplementaryLogsSampleQuery').mockReturnValue(undefined);\n        expect(\n          datasource.getSupplementaryRequest(SupplementaryQueryType.LogsSample, {\n            targets: [{ refId: 'A', editorType: EditorType.Builder }],\n          } as any)\n        ).toBeUndefined();\n      });\n\n      it('should return a modified request with logs-sample targets', () => {\n        const supplementaryQuery = { rawSql: 'SELECT * FROM logs', refId: '', format: 2 } as CHSqlQuery;\n        jest.spyOn(Datasource.prototype, 'getSupplementaryLogsSampleQuery').mockReturnValue(supplementaryQuery);\n        const result = datasource.getSupplementaryRequest(SupplementaryQueryType.LogsSample, {\n          targets: [{ refId: 'A', editorType: EditorType.Builder }],\n        } as any);\n        expect(result).toMatchObject({\n          hideFromInspector: true,\n          targets: [{ ...supplementaryQuery, refId: 'logs-sample-A' }],\n        });\n      });\n\n      it('should return undefined if there are no supplementary queries for targets', async () => {\n        jest.spyOn(Datasource.prototype, 'getSupplementaryLogsVolumeQuery').mockReturnValue(undefined);\n        jest.spyOn(logs, 'getIntervalInfo').mockReturnValue({ interval: '1d' });\n        expect(\n          datasource.getSupplementaryRequest(SupplementaryQueryType.LogsVolume, {\n            scopedVars: {\n              __interval: {},\n            },\n            targets: ['foo', 'bar'],\n          } as any)\n        ).toBeUndefined();\n      });\n\n      it('should return a modified request with log-volume targets', async () => {\n        const range = ['from', 'to'];\n        const supplementaryQuery = {\n          rawSql: 'supplementaryQuery',\n          refId: '',\n        } as CHSqlQuery;\n        jest.spyOn(Datasource.prototype, 'getSupplementaryLogsVolumeQuery').mockReturnValue(supplementaryQuery);\n        jest.spyOn(logs, 'getIntervalInfo').mockReturnValue({ interval: '1d' });\n        const result = datasource.getSupplementaryRequest(SupplementaryQueryType.LogsVolume, {\n          scopedVars: {\n            __interval: {},\n          },\n          targets: [{ refId: 'A', editorType: EditorType.Builder }],\n          range,\n        } as any);\n        expect(result).toMatchObject({\n          hideFromInspector: true,\n          interval: '1d',\n          scopedVars: {\n            __interval: { text: '1d', value: '1d' },\n          },\n          targets: [{ ...supplementaryQuery, refId: 'log-volume-A' }],\n          range,\n        });\n      });\n    });\n  });\n\n  describe('modifyQuery', () => {\n    const query: CHBuilderQuery = {\n      pluginVersion: '',\n      refId: 'A',\n      editorType: EditorType.Builder,\n      rawSql: '',\n      builderOptions: {\n        database: 'default',\n        table: 'logs',\n        queryType: QueryType.Logs,\n        mode: BuilderMode.List,\n        columns: [{ name: 'LogAttributes', hint: ColumnHint.LogAttributes, type: 'Map(String, String)' }],\n      },\n    };\n\n    let datasource: Datasource;\n    beforeEach(() => {\n      datasource = cloneDeep(mockDatasource);\n    });\n\n    it('should correctly find merged value in log labels field', () => {\n      const frame = {\n        fields: [{ name: 'labels', values: { get: () => ({ ['LogAttributes.service_name']: 'value' }), length: 1 } }],\n      } as any;\n\n      const result = datasource.modifyQuery(query, {\n        type: 'ADD_FILTER',\n        options: { key: 'LogAttributes.service_name', value: 'value' },\n        frame,\n      } as any);\n\n      expect((result as CHBuilderQuery).builderOptions.filters![0].mapKey).toBe('service_name');\n    });\n\n    describe('ADD_FILTER', () => {\n      it('adds an Equals filter for the given field', () => {\n        const result = datasource.modifyQuery(query, {\n          type: 'ADD_FILTER',\n          options: { key: 'level', value: 'info' },\n        } as any) as CHBuilderQuery;\n\n        expect(result.builderOptions.filters).toHaveLength(1);\n        expect(result.builderOptions.filters![0]).toMatchObject({\n          key: 'level',\n          operator: FilterOperator.Equals,\n          value: 'info',\n        });\n      });\n\n      it('replaces an existing Equals filter for the same field', () => {\n        const queryWithFilter: CHBuilderQuery = {\n          ...query,\n          builderOptions: {\n            ...query.builderOptions,\n            filters: [{ condition: 'AND', key: 'level', type: 'string', filterType: 'custom', operator: FilterOperator.Equals, value: 'debug' }],\n          },\n        };\n\n        const result = datasource.modifyQuery(queryWithFilter, {\n          type: 'ADD_FILTER',\n          options: { key: 'level', value: 'info' },\n        } as any) as CHBuilderQuery;\n\n        expect(result.builderOptions.filters).toHaveLength(1);\n        // @ts-expect-error not expecting `NullFilter`\n        expect(result.builderOptions.filters![0].value).toBe('info');\n      });\n\n      it('returns query unchanged when key is missing', () => {\n        const result = datasource.modifyQuery(query, {\n          type: 'ADD_FILTER',\n          options: { value: 'info' },\n        } as any);\n\n        expect(result).toBe(query);\n      });\n    });\n\n    describe('ADD_FILTER_OUT', () => {\n      it('adds a NotEquals filter for the given field', () => {\n        const result = datasource.modifyQuery(query, {\n          type: 'ADD_FILTER_OUT',\n          options: { key: 'level', value: 'error' },\n        } as any) as CHBuilderQuery;\n\n        expect(result.builderOptions.filters).toHaveLength(1);\n        expect(result.builderOptions.filters![0]).toMatchObject({\n          key: 'level',\n          operator: FilterOperator.NotEquals,\n          value: 'error',\n        });\n      });\n\n      it('removes an existing Equals filter for the same field', () => {\n        const queryWithFilter: CHBuilderQuery = {\n          ...query,\n          builderOptions: {\n            ...query.builderOptions,\n            filters: [{ condition: 'AND', key: 'level', type: 'string', filterType: 'custom', operator: FilterOperator.Equals, value: 'info' }],\n          },\n        };\n\n        const result = datasource.modifyQuery(queryWithFilter, {\n          type: 'ADD_FILTER_OUT',\n          options: { key: 'level', value: 'error' },\n        } as any) as CHBuilderQuery;\n\n        expect(result.builderOptions.filters).toHaveLength(1);\n        expect(result.builderOptions.filters![0].operator).toBe(FilterOperator.NotEquals);\n      });\n\n      it('returns query unchanged when key is missing', () => {\n        const result = datasource.modifyQuery(query, {\n          type: 'ADD_FILTER_OUT',\n          options: { value: 'error' },\n        } as any);\n\n        expect(result).toBe(query);\n      });\n\n      it('accumulates multiple NotEquals filters for different values', () => {\n        const queryWithFilter: CHBuilderQuery = {\n          ...query,\n          builderOptions: {\n            ...query.builderOptions,\n            filters: [{ condition: 'AND', key: 'level', type: 'string', filterType: 'custom', operator: FilterOperator.NotEquals, value: 'info' }],\n          },\n        };\n\n        const result = datasource.modifyQuery(queryWithFilter, {\n          type: 'ADD_FILTER_OUT',\n          options: { key: 'level', value: 'error' },\n        } as any) as CHBuilderQuery;\n\n        expect(result.builderOptions.filters).toHaveLength(2);\n        expect(result.builderOptions.filters!.map((f) => ('value' in f ? f.value : null))).toEqual(\n          expect.arrayContaining(['info', 'error'])\n        );\n      });\n\n      it('replaces a NotEquals filter with the exact same value', () => {\n        const queryWithFilter: CHBuilderQuery = {\n          ...query,\n          builderOptions: {\n            ...query.builderOptions,\n            filters: [{ condition: 'AND', key: 'level', type: 'string', filterType: 'custom', operator: FilterOperator.NotEquals, value: 'error' }],\n          },\n        };\n\n        const result = datasource.modifyQuery(queryWithFilter, {\n          type: 'ADD_FILTER_OUT',\n          options: { key: 'level', value: 'error' },\n        } as any) as CHBuilderQuery;\n\n        expect(result.builderOptions.filters).toHaveLength(1);\n        expect(result.builderOptions.filters![0].operator).toBe(FilterOperator.NotEquals);\n        // @ts-expect-error not expecting `NullFilter`\n        expect(result.builderOptions.filters![0].value).toBe('error');\n      });\n    });\n\n    describe('ADD_STRING_FILTER', () => {\n      it('adds an ILike filter using the provided key', () => {\n        const result = datasource.modifyQuery(query, {\n          type: 'ADD_STRING_FILTER',\n          options: { key: 'Body', value: 'error' },\n        } as any) as CHBuilderQuery;\n\n        expect(result.builderOptions.filters).toHaveLength(1);\n        expect(result.builderOptions.filters![0]).toMatchObject({\n          key: 'Body',\n          operator: FilterOperator.ILike,\n          value: 'error',\n        });\n      });\n\n      it('resolves column from LogMessage hint when key is absent', () => {\n        const queryWithLogMessage: CHBuilderQuery = {\n          ...query,\n          builderOptions: {\n            ...query.builderOptions,\n            columns: [{ name: 'Body', hint: ColumnHint.LogMessage }],\n          },\n        };\n\n        const result = datasource.modifyQuery(queryWithLogMessage, {\n          type: 'ADD_STRING_FILTER',\n          options: { value: 'error' },\n        } as any) as CHBuilderQuery;\n\n        expect(result.builderOptions.filters).toHaveLength(1);\n        expect(result.builderOptions.filters![0]).toMatchObject({\n          key: 'Body',\n          operator: FilterOperator.ILike,\n          value: 'error',\n        });\n      });\n\n      it('returns query unchanged when key is absent and no LogMessage column is configured', () => {\n        const result = datasource.modifyQuery(query, {\n          type: 'ADD_STRING_FILTER',\n          options: { value: 'error' },\n        } as any);\n\n        expect(result).toBe(query);\n      });\n    });\n\n    describe('ADD_STRING_FILTER_OUT', () => {\n      it('adds a NotILike filter using the provided key', () => {\n        const result = datasource.modifyQuery(query, {\n          type: 'ADD_STRING_FILTER_OUT',\n          options: { key: 'Body', value: 'error' },\n        } as any) as CHBuilderQuery;\n\n        expect(result.builderOptions.filters).toHaveLength(1);\n        expect(result.builderOptions.filters![0]).toMatchObject({\n          key: 'Body',\n          operator: FilterOperator.NotILike,\n          value: 'error',\n        });\n      });\n\n      it('resolves column from LogMessage hint when key is absent', () => {\n        const queryWithLogMessage: CHBuilderQuery = {\n          ...query,\n          builderOptions: {\n            ...query.builderOptions,\n            columns: [{ name: 'Body', hint: ColumnHint.LogMessage }],\n          },\n        };\n\n        const result = datasource.modifyQuery(queryWithLogMessage, {\n          type: 'ADD_STRING_FILTER_OUT',\n          options: { value: 'error' },\n        } as any) as CHBuilderQuery;\n\n        expect(result.builderOptions.filters).toHaveLength(1);\n        expect(result.builderOptions.filters![0]).toMatchObject({\n          key: 'Body',\n          operator: FilterOperator.NotILike,\n          value: 'error',\n        });\n      });\n\n      it('returns query unchanged when key is absent and no LogMessage column is configured', () => {\n        const result = datasource.modifyQuery(query, {\n          type: 'ADD_STRING_FILTER_OUT',\n          options: { value: 'error' },\n        } as any);\n\n        expect(result).toBe(query);\n      });\n    });\n\n    describe('hint-matched columns via logAliasToColumnHints', () => {\n      it('ADD_FILTER uses hint and empty key when column is resolved via log alias', () => {\n        const queryWithLevel: CHBuilderQuery = {\n          ...query,\n          builderOptions: {\n            ...query.builderOptions,\n            columns: [{ name: 'SeverityText', hint: ColumnHint.LogLevel, type: 'string' }],\n          },\n        };\n\n        const result = datasource.modifyQuery(queryWithLevel, {\n          type: 'ADD_FILTER',\n          options: { key: 'level', value: 'info' },\n        } as any) as CHBuilderQuery;\n\n        expect(result.builderOptions.filters).toHaveLength(1);\n        expect(result.builderOptions.filters![0]).toMatchObject({\n          key: '',\n          hint: ColumnHint.LogLevel,\n          operator: FilterOperator.Equals,\n          value: 'info',\n        });\n      });\n\n      it('ADD_FILTER replaces existing hint-matched filter', () => {\n        const queryWithLevel: CHBuilderQuery = {\n          ...query,\n          builderOptions: {\n            ...query.builderOptions,\n            columns: [{ name: 'SeverityText', hint: ColumnHint.LogLevel, type: 'string' }],\n            filters: [{ condition: 'AND', key: '', hint: ColumnHint.LogLevel, type: 'string', filterType: 'custom', operator: FilterOperator.Equals, value: 'debug' }],\n          },\n        };\n\n        const result = datasource.modifyQuery(queryWithLevel, {\n          type: 'ADD_FILTER',\n          options: { key: 'level', value: 'info' },\n        } as any) as CHBuilderQuery;\n\n        expect(result.builderOptions.filters).toHaveLength(1);\n        // @ts-expect-error not expecting `NullFilter`\n        expect(result.builderOptions.filters![0].value).toBe('info');\n      });\n    });\n\n    describe('OTel map key splitting', () => {\n      it('splits ResourceAttributes key into column + mapKey', () => {\n        const queryWithResource: CHBuilderQuery = {\n          ...query,\n          builderOptions: {\n            ...query.builderOptions,\n            columns: [{ name: 'ResourceAttributes', type: 'Map(String, String)' }],\n          },\n        };\n\n        const result = datasource.modifyQuery(queryWithResource, {\n          type: 'ADD_FILTER',\n          options: { key: 'ResourceAttributes.service.name', value: 'my-service' },\n        } as any) as CHBuilderQuery;\n\n        expect(result.builderOptions.filters![0]).toMatchObject({\n          mapKey: 'service.name',\n          type: 'Map(String, String)',\n          operator: FilterOperator.Equals,\n          value: 'my-service',\n        });\n      });\n\n      it('splits ScopeAttributes key into column + mapKey', () => {\n        const queryWithScope: CHBuilderQuery = {\n          ...query,\n          builderOptions: {\n            ...query.builderOptions,\n            columns: [{ name: 'ScopeAttributes', type: 'Map(String, String)' }],\n          },\n        };\n\n        const result = datasource.modifyQuery(queryWithScope, {\n          type: 'ADD_FILTER',\n          options: { key: 'ScopeAttributes.version', value: '1.0' },\n        } as any) as CHBuilderQuery;\n\n        expect(result.builderOptions.filters![0]).toMatchObject({\n          mapKey: 'version',\n          type: 'Map(String, String)',\n          operator: FilterOperator.Equals,\n          value: '1.0',\n        });\n      });\n\n      it('sets type to JSON for JSON-typed map column', () => {\n        const queryWithJson: CHBuilderQuery = {\n          ...query,\n          builderOptions: {\n            ...query.builderOptions,\n            columns: [{ name: 'LogAttributes', type: 'JSON' }],\n          },\n        };\n\n        const result = datasource.modifyQuery(queryWithJson, {\n          type: 'ADD_FILTER',\n          options: { key: 'LogAttributes.request_id', value: 'abc123' },\n        } as any) as CHBuilderQuery;\n\n        expect(result.builderOptions.filters![0]).toMatchObject({\n          mapKey: 'request_id',\n          type: 'JSON',\n          operator: FilterOperator.Equals,\n          value: 'abc123',\n        });\n      });\n    });\n\n    describe('ADD_STRING_FILTER with LogMessage column alias', () => {\n      it('resolves LogMessage column by alias when name differs', () => {\n        const queryWithAlias: CHBuilderQuery = {\n          ...query,\n          builderOptions: {\n            ...query.builderOptions,\n            columns: [{ name: 'log_body', alias: 'Body', hint: ColumnHint.LogMessage }],\n          },\n        };\n\n        const result = datasource.modifyQuery(queryWithAlias, {\n          type: 'ADD_STRING_FILTER',\n          options: { value: 'error' },\n        } as any) as CHBuilderQuery;\n\n        expect(result.builderOptions.filters).toHaveLength(1);\n        expect(result.builderOptions.filters![0]).toMatchObject({\n          key: 'Body',\n          operator: FilterOperator.ILike,\n          value: 'error',\n        });\n      });\n    });\n\n    it('returns query unchanged for non-Builder editorType', () => {\n      const sqlQuery: CHSqlQuery = { pluginVersion: '', refId: 'A', editorType: EditorType.SQL, rawSql: 'SELECT 1' };\n      const result = datasource.modifyQuery(sqlQuery, { type: 'ADD_FILTER', options: { key: 'level', value: 'info' } } as any);\n      expect(result).toBe(sqlQuery);\n    });\n\n    it('returns query unchanged when value is missing', () => {\n      const result = datasource.modifyQuery(query, { type: 'ADD_FILTER', options: { key: 'level' } } as any);\n      expect(result).toBe(query);\n    });\n  });\n\n  describe('getLogRowContext', () => {\n    const baseBuilderOptions: QueryBuilderOptions = {\n      database: 'default',\n      table: 'logs',\n      queryType: QueryType.Logs,\n      mode: BuilderMode.List,\n      columns: [\n        { name: 'timestamp', type: 'DateTime64(9)', hint: ColumnHint.Time },\n        { name: 'body', type: 'String', hint: ColumnHint.LogMessage },\n      ],\n      filters: [],\n      orderBy: [\n        { name: 'timestamp', hint: ColumnHint.Time, dir: OrderByDirection.DESC },\n        { name: 'offset', dir: OrderByDirection.ASC },\n      ],\n    };\n\n    const baseQuery: CHBuilderQuery = {\n      pluginVersion: '',\n      refId: 'A',\n      editorType: EditorType.Builder,\n      rawSql: '',\n      builderOptions: baseBuilderOptions,\n    };\n\n    const makeRow = () => {\n      const frame = toDataFrame({ fields: [{ name: 'timestamp', values: [1700000000000] }] });\n      return {\n        entryFieldIndex: 0,\n        rowIndex: 0,\n        dataFrame: frame,\n        timeEpochNs: '1700000000000000000',\n        entry: '',\n        hasAnsi: false,\n        hasUnescapedContent: false,\n        labels: {},\n        logLevel: 'info',\n        raw: '',\n        timeFromNow: '',\n        timeEpochMs: 1700000000000,\n        timeLocal: '',\n        timeUtc: '',\n        uid: '',\n      } as any;\n    };\n\n    const contextOptions = {\n      direction: 'BACKWARD' as any,\n      limit: 10,\n    };\n\n    it('preserves secondary ORDER BY entries (e.g. `offset ASC`) as tiebreakers', async () => {\n      const ds = cloneDeep(mockDatasource);\n      // Force a single context column so we don't hit the \"no columns\" guard.\n      jest.spyOn(ds, 'getLogContextColumnsFromLogRow').mockReturnValue([{ name: 'service', value: 'web' }]);\n      const querySpy = jest\n        .spyOn(ds, 'query')\n        .mockImplementation((_req) => of({ data: [toDataFrame([])] }));\n\n      const query = cloneDeep(baseQuery);\n      await ds.getLogRowContext(makeRow(), contextOptions, query);\n\n      expect(querySpy).toHaveBeenCalledTimes(1);\n      const request = querySpy.mock.calls[0][0] as DataQueryRequest<CHQuery>;\n      const sent = request.targets[0] as CHBuilderQuery;\n      const orderBy = sent.builderOptions.orderBy ?? [];\n\n      // Primary entry is the time column (inserted by getLogRowContext).\n      expect(orderBy[0]).toMatchObject({ hint: ColumnHint.Time });\n      // User's secondary entry survives as a tiebreaker.\n      expect(orderBy).toContainEqual(\n        expect.objectContaining({ name: 'offset', dir: OrderByDirection.ASC })\n      );\n      // Original time-column entry is not duplicated.\n      const timeEntries = orderBy.filter(\n        (e) => e.hint === ColumnHint.Time || e.name === 'timestamp'\n      );\n      expect(timeEntries).toHaveLength(1);\n    });\n\n    it('surfaces the underlying ClickHouse error text instead of swallowing it', async () => {\n      const ds = cloneDeep(mockDatasource);\n      jest.spyOn(ds, 'getLogContextColumnsFromLogRow').mockReturnValue([{ name: 'service', value: 'web' }]);\n      jest.spyOn(ds, 'query').mockImplementation((_req) => {\n        // Observable that errors with a ClickHouse-shaped error payload.\n        return new (require('rxjs').Observable)((subscriber: any) => {\n          subscriber.error({ data: { message: 'Code: 47. DB::Exception: Unknown identifier: offset' } });\n        });\n      });\n\n      await expect(ds.getLogRowContext(makeRow(), contextOptions, cloneDeep(baseQuery))).rejects.toThrow(\n        /Unknown identifier: offset/\n      );\n    });\n\n    it('surfaces errors reported on DataQueryResponse.errors[]', async () => {\n      const ds = cloneDeep(mockDatasource);\n      jest.spyOn(ds, 'getLogContextColumnsFromLogRow').mockReturnValue([{ name: 'service', value: 'web' }]);\n      jest.spyOn(ds, 'query').mockImplementation((_req) =>\n        of({\n          data: [],\n          errors: [{ message: 'Code: 62. DB::Exception: Syntax error' } as any],\n        })\n      );\n\n      await expect(ds.getLogRowContext(makeRow(), contextOptions, cloneDeep(baseQuery))).rejects.toThrow(\n        /Syntax error/\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "src/data/CHDatasource.ts",
    "content": "import {\n  AdHocVariableFilter,\n  DataFrame,\n  DataFrameView,\n  DataQueryRequest,\n  DataQueryResponse,\n  DataSourceInstanceSettings,\n  DataSourceWithLogsContextSupport,\n  DataSourceWithQueryModificationSupport,\n  DataSourceWithSupplementaryQueriesSupport,\n  Field,\n  getTimeZone,\n  getTimeZoneInfo,\n  LogRowContextOptions,\n  LogRowContextQueryDirection,\n  LogRowModel,\n  MetricFindValue,\n  QueryFixAction,\n  ScopedVars,\n  SupplementaryQueryOptions,\n  SupplementaryQueryType,\n  TypedVariableModel,\n} from '@grafana/data';\nimport { DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime';\nimport { trackClickhouseHealthCheckFailed } from 'tracking';\nimport LogsContextPanel from 'components/LogsContextPanel';\nimport { cloneDeep, isEmpty, isString } from 'lodash';\nimport otel from 'otel';\nimport { createElement as createReactElement, ReactNode } from 'react';\nimport { firstValueFrom, map, Observable } from 'rxjs';\nimport { CHConfig } from 'types/config';\nimport {\n  AggregateColumn,\n  AggregateType,\n  BuilderMode,\n  ColumnHint,\n  Filter,\n  FilterOperator,\n  OrderByDirection,\n  QueryBuilderOptions,\n  QueryType,\n  SelectedColumn,\n  SqlFunction,\n  TableColumn,\n  TimeUnit,\n} from 'types/queryBuilder';\nimport { CHQuery, EditorType } from 'types/sql';\nimport { pluginVersion } from 'utils/version';\nimport { AdHocFilter } from './adHocFilter';\nimport {\n  DEFAULT_LOGS_ALIAS,\n  getIntervalInfo,\n  getTimeFieldRoundingClause,\n  LOG_LEVEL_TO_IN_CLAUSE,\n  splitLogsVolumeFrames,\n  TIME_FIELD_ALIAS,\n} from './logs';\nimport { generateSql, getColumnByHint, logAliasToColumnHints } from './sqlGenerator';\nimport { labelsFieldName, transformQueryResponseWithTraceAndLogLinks } from './utils';\n\nexport class Datasource\n  extends DataSourceWithBackend<CHQuery, CHConfig>\n  implements\n    DataSourceWithSupplementaryQueriesSupport<CHQuery>,\n    DataSourceWithLogsContextSupport<CHQuery>,\n    DataSourceWithQueryModificationSupport<CHQuery>\n{\n  // This enables default annotation support for 7.2+\n  annotations = {};\n  settings: DataSourceInstanceSettings<CHConfig>;\n  adHocFilter: AdHocFilter;\n  skipAdHocFilter = false; // don't apply adhoc filters to the query\n  adHocFiltersStatus = AdHocFilterStatus.none; // ad hoc filters only work with CH 22.7+\n  adHocCHVerReq = { major: 22, minor: 7 };\n\n  constructor(instanceSettings: DataSourceInstanceSettings<CHConfig>) {\n    super(instanceSettings);\n    this.settings = instanceSettings;\n    this.adHocFilter = new AdHocFilter();\n  }\n\n  static logVolumePrefix = 'log-volume-';\n  static logsSamplePrefix = 'logs-sample-';\n\n  getSupplementaryRequest(\n    type: SupplementaryQueryType,\n    request: DataQueryRequest<CHQuery>\n  ): DataQueryRequest<CHQuery> | undefined {\n    if (!this.getSupportedSupplementaryQueryTypes(request).includes(type)) {\n      return undefined;\n    }\n\n    if (type === SupplementaryQueryType.LogsVolume) {\n      const logsVolumeRequest = cloneDeep(request);\n\n      const intervalInfo = getIntervalInfo(logsVolumeRequest.scopedVars);\n      logsVolumeRequest.interval = intervalInfo.interval;\n      logsVolumeRequest.scopedVars.__interval = { value: intervalInfo.interval, text: intervalInfo.interval };\n      logsVolumeRequest.hideFromInspector = true;\n      if (intervalInfo.intervalMs !== undefined) {\n        logsVolumeRequest.intervalMs = intervalInfo.intervalMs;\n        logsVolumeRequest.scopedVars.__interval_ms = {\n          value: intervalInfo.intervalMs,\n          text: intervalInfo.intervalMs,\n        };\n      }\n\n      const targets: CHQuery[] = [];\n      logsVolumeRequest.targets.forEach((target) => {\n        const supplementaryQuery = this.getSupplementaryLogsVolumeQuery(logsVolumeRequest, target);\n        if (supplementaryQuery !== undefined) {\n          targets.push({ ...supplementaryQuery, refId: `${Datasource.logVolumePrefix}${target.refId}` });\n        }\n      });\n\n      if (!targets.length) {\n        return undefined;\n      }\n\n      return { ...logsVolumeRequest, targets };\n    }\n\n    if (type === SupplementaryQueryType.LogsSample) {\n      const logsSampleRequest = cloneDeep(request);\n      logsSampleRequest.hideFromInspector = true;\n\n      const targets: CHQuery[] = [];\n      logsSampleRequest.targets.forEach((target) => {\n        const supplementaryQuery = this.getSupplementaryLogsSampleQuery(target);\n        if (supplementaryQuery !== undefined) {\n          targets.push({ ...supplementaryQuery, refId: `${Datasource.logsSamplePrefix}${target.refId}` });\n        }\n      });\n\n      if (!targets.length) {\n        return undefined;\n      }\n\n      return { ...logsSampleRequest, targets };\n    }\n\n    return undefined;\n  }\n\n  getSupportedSupplementaryQueryTypes(dsRequest: DataQueryRequest<CHQuery>): SupplementaryQueryType[] {\n    if (dsRequest && dsRequest.targets.some((t) => t.editorType !== EditorType.Builder)) {\n      return [];\n    }\n    return [SupplementaryQueryType.LogsVolume, SupplementaryQueryType.LogsSample];\n  }\n\n  getSupplementaryLogsVolumeQuery(logsVolumeRequest: DataQueryRequest<CHQuery>, query: CHQuery): CHQuery | undefined {\n    if (\n      query.editorType !== EditorType.Builder ||\n      query.builderOptions.queryType !== QueryType.Logs ||\n      query.builderOptions.mode !== BuilderMode.List ||\n      query.builderOptions.database === '' ||\n      query.builderOptions.table === ''\n    ) {\n      return undefined;\n    }\n\n    const timeColumn =\n      getColumnByHint(query.builderOptions, ColumnHint.FilterTime) ||\n      getColumnByHint(query.builderOptions, ColumnHint.Time);\n    if (timeColumn === undefined) {\n      return undefined;\n    }\n\n    const columns: SelectedColumn[] = [];\n    const aggregates: AggregateColumn[] = [];\n    columns.push({\n      name: getTimeFieldRoundingClause(logsVolumeRequest.scopedVars, timeColumn.name),\n      alias: TIME_FIELD_ALIAS,\n      hint: timeColumn.hint!,\n    });\n\n    const logLevelColumn = getColumnByHint(query.builderOptions, ColumnHint.LogLevel);\n    if (logLevelColumn) {\n      // Generates aggregates like\n      // sum(toString(\"log_level\") IN ('dbug', 'debug', 'DBUG', 'DEBUG', 'Dbug', 'Debug')) AS debug\n      const llf = `toString(\"${logLevelColumn.name}\")`;\n      let level: keyof typeof LOG_LEVEL_TO_IN_CLAUSE;\n      for (level in LOG_LEVEL_TO_IN_CLAUSE) {\n        aggregates.push({\n          aggregateType: AggregateType.Sum,\n          column: `multiSearchAny(${llf}, [${LOG_LEVEL_TO_IN_CLAUSE[level]}])`,\n          alias: level,\n        });\n      }\n    } else {\n      // Count all logs if level column isn't selected\n      aggregates.push({\n        aggregateType: AggregateType.Count,\n        column: '*',\n        alias: DEFAULT_LOGS_ALIAS,\n      });\n    }\n\n    const filters = (query.builderOptions.filters?.slice() || []).map((f) => {\n      // In order for a hinted filter to work, the hinted column must be SELECTed OR provide \"key\"\n      // For this histogram query the \"level\" column isn't selected, so we must find the original column name\n      if (f.hint && !f.key) {\n        const originalColumn = getColumnByHint(query.builderOptions, f.hint);\n        f.key = originalColumn?.alias || originalColumn?.name || '';\n      }\n\n      return f;\n    });\n\n    const logVolumeSqlBuilderOptions: QueryBuilderOptions = {\n      database: query.builderOptions.database,\n      table: query.builderOptions.table,\n      queryType: QueryType.TimeSeries,\n      filters,\n      columns,\n      aggregates,\n      orderBy: [{ name: '', hint: timeColumn.hint!, dir: OrderByDirection.ASC }],\n    };\n\n    const logVolumeSupplementaryQuery = generateSql(logVolumeSqlBuilderOptions);\n    return {\n      pluginVersion,\n      editorType: EditorType.Builder,\n      builderOptions: logVolumeSqlBuilderOptions,\n      rawSql: logVolumeSupplementaryQuery,\n      refId: '',\n    };\n  }\n\n  getSupplementaryLogsSampleQuery(query: CHQuery): CHQuery | undefined {\n    if (\n      query.editorType !== EditorType.Builder ||\n      !query.builderOptions.database ||\n      query.builderOptions.table !== this.getDefaultLogsTable()\n    ) {\n      return undefined;\n    }\n\n    const timeColumn =\n      getColumnByHint(query.builderOptions, ColumnHint.FilterTime) ||\n      getColumnByHint(query.builderOptions, ColumnHint.Time);\n\n    if (!timeColumn) {\n      return undefined;\n    }\n\n    const timeHint = timeColumn.hint ?? ColumnHint.Time;\n\n    const filters = (query.builderOptions.filters?.slice() || []).map((f) => {\n      if (f.hint && !f.key) {\n        const originalColumn = getColumnByHint(query.builderOptions, f.hint);\n        f.key = originalColumn?.alias || originalColumn?.name || '';\n      }\n      return { ...f };\n    });\n\n    const defaultColumns = Array.from(this.getDefaultLogsColumns(), ([hint, name]) => ({ hint, name }));\n\n    const columns = defaultColumns.length\n      ? defaultColumns\n      : (query.builderOptions.columns ?? [{ name: timeColumn.name, hint: timeHint }]);\n\n    const logsSampleBuilderOptions: QueryBuilderOptions = {\n      database: query.builderOptions.database,\n      table: query.builderOptions.table,\n      queryType: QueryType.Logs,\n      mode: BuilderMode.List,\n      filters,\n      columns,\n      orderBy: [{ name: '', hint: timeHint, dir: OrderByDirection.DESC }],\n      limit: 100,\n    };\n\n    return {\n      pluginVersion,\n      editorType: EditorType.Builder,\n      builderOptions: logsSampleBuilderOptions,\n      rawSql: generateSql(logsSampleBuilderOptions),\n      refId: '',\n      format: 2, // Logs format\n    };\n  }\n\n  getSupplementaryQuery(_options: SupplementaryQueryOptions, _originalQuery: CHQuery): CHQuery | undefined {\n    return undefined;\n  }\n\n  async metricFindQuery(query: CHQuery | string, options: any) {\n    if (this.adHocFiltersStatus === AdHocFilterStatus.none) {\n      this.adHocFiltersStatus = await this.canUseAdhocFilters();\n    }\n    const chQuery = isString(query) ? { rawSql: query, editorType: EditorType.SQL } : query;\n\n    if (!(chQuery.editorType === EditorType.SQL || chQuery.editorType === EditorType.Builder || !chQuery.editorType)) {\n      return [];\n    }\n\n    if (!chQuery.rawSql) {\n      return [];\n    }\n    const frame = await this.runQuery(chQuery, options);\n    if (frame.fields?.length === 0) {\n      return [];\n    }\n    if (frame?.fields?.length === 1) {\n      return frame?.fields[0]?.values.map((text) => ({ text, value: text }));\n    }\n    // convention - assume the first field is an id field\n    const ids = frame?.fields[0]?.values;\n    return frame?.fields[1]?.values.map((text, i) => ({ text, value: ids.get(i) }));\n  }\n\n  applyTemplateVariables(query: CHQuery, scoped: ScopedVars, filters: AdHocVariableFilter[] = []): CHQuery {\n    let rawQuery = query.rawSql || '';\n    const templateSrv = getTemplateSrv();\n    const templateSrvVariables = templateSrv.getVariables() || [];\n\n    // resolve template variables\n    rawQuery = this.applyConditionalAll(rawQuery, templateSrvVariables);\n    rawQuery = this.replace(rawQuery, scoped) || '';\n\n    if (!this.skipAdHocFilter) {\n      if (this.adHocFiltersStatus === AdHocFilterStatus.disabled && filters.length > 0) {\n        throw new Error(\n          `Unable to apply ad hoc filters. Upgrade ClickHouse to >=${this.adHocCHVerReq.major}.${this.adHocCHVerReq.minor} or remove ad hoc filters for the dashboard.`\n        );\n      }\n\n      const useJSON = Boolean(templateSrvVariables.find((v) => v.name === 'clickhouse_adhoc_use_json'));\n\n      // Check if query contains $__adHocFilters macro\n      const hasMacro = /\\$__adHocFilters\\s*\\(\\s*['\"](.+?)['\"]\\s*\\)/.test(rawQuery);\n\n      // Apply $__adHocFilters macro before automatic filter application\n      rawQuery = this.applyAdHocFiltersMacro(rawQuery, filters, useJSON);\n\n      // Only apply automatic filters if the macro was not used\n      if (!hasMacro) {\n        rawQuery = this.adHocFilter.apply(rawQuery, filters, useJSON);\n      }\n    }\n    this.skipAdHocFilter = false;\n\n    return {\n      ...query,\n      rawSql: rawQuery,\n    };\n  }\n\n  applyConditionalAll(rawQuery: string, templateVars: TypedVariableModel[]): string {\n    if (!rawQuery) {\n      return rawQuery;\n    }\n    const macro = '$__conditionalAll(';\n    let macroIndex = rawQuery.lastIndexOf(macro);\n\n    while (macroIndex !== -1) {\n      const params = this.getMacroArgs(rawQuery, macroIndex + macro.length - 1);\n      if (params.length !== 2) {\n        return rawQuery;\n      }\n      const templateVarParam = params[1].trim();\n      const varRegex = new RegExp(/(?<=\\$\\{)[\\w\\d]+(?=\\})|(?<=\\$)[\\w\\d]+/);\n      const templateVar = varRegex.exec(templateVarParam);\n      let phrase = params[0];\n      if (templateVar) {\n        const key = templateVars.find((x) => x.name === templateVar[0]) as any;\n        let value = key?.current.value.toString();\n        if (value === '' || value === '$__all') {\n          phrase = '1=1';\n        }\n      }\n      rawQuery = rawQuery.replace(`${macro}${params[0]},${params[1]})`, phrase);\n      macroIndex = rawQuery.lastIndexOf(macro);\n    }\n    return rawQuery;\n  }\n\n  applyAdHocFiltersMacro(rawQuery: string, filters: AdHocVariableFilter[], useJSON = false): string {\n    if (!rawQuery) {\n      return rawQuery;\n    }\n\n    // Match $__adHocFilters('table_name') or $__adHocFilters(\"table_name\") or multiple tables\n    const regex = /\\$__adHocFilters\\s*\\(([^)]+)\\)/g;\n\n    return rawQuery.replace(regex, (match, args) => {\n      // Extract all table names from comma-separated quoted strings\n      const tableNameRegex = /['\"]([^'\"]+)['\"]/g;\n      const tableNames: string[] = [];\n      let tableMatch;\n\n      while ((tableMatch = tableNameRegex.exec(args)) !== null) {\n        tableNames.push(tableMatch[1]);\n      }\n\n      if (tableNames.length === 0) {\n        return match; // Return original if no valid table names found\n      }\n\n      const filterStr = this.adHocFilter.buildFilterString(filters, useJSON);\n      if (filterStr === '') {\n        return 'additional_table_filters={}';\n      }\n\n      // Build filter entries for all tables\n      const tableFilters = tableNames.map(tableName => `'${tableName}': '${filterStr}'`).join(', ');\n      return `additional_table_filters={${tableFilters}}`;\n    });\n  }\n\n  getSupportedQueryModifications() {\n    return ['ADD_FILTER', 'ADD_FILTER_OUT', 'ADD_STRING_FILTER', 'ADD_STRING_FILTER_OUT'];\n  }\n\n  // Support filtering by field value in Explore\n  modifyQuery(query: CHQuery, action: QueryFixAction): CHQuery {\n    if (query.editorType !== EditorType.Builder || !action.options || !action.options.value) {\n      return query;\n    }\n\n    let columnName = (() => {\n      const isStringFilterAction = action.type === 'ADD_STRING_FILTER' || action.type === 'ADD_STRING_FILTER_OUT';\n\n      if (isStringFilterAction) {\n        // has no key — resolve the column name from the log message hint.\n        const logMessageColumn = getColumnByHint(query.builderOptions, ColumnHint.LogMessage);\n        return logMessageColumn?.alias || logMessageColumn?.name || action.options.key || '';\n      }\n\n      return action.options.key || '';\n    })();\n\n    if (!columnName) {\n      return query;\n    }\n\n    const actionValue = action.options.value;\n    let mapKey = '';\n\n    // Convert flattened/merged OTel attributes into column+path pair\n    if (['ResourceAttributes', 'ScopeAttributes', 'LogAttributes'].includes(columnName.split('.')[0])) {\n      const prefixIndex = columnName.indexOf('.');\n      mapKey = columnName.substring(prefixIndex + 1);\n      columnName = columnName.substring(0, prefixIndex);\n    }\n\n    // Find selected column by alias/name\n    const lookupByAlias = query.builderOptions.columns?.find((c) => c.alias === columnName); // Check all aliases first,\n    const lookupByName = query.builderOptions.columns?.find((c) => c.name === columnName); // then try matching column name\n    const lookupByLogsAlias = logAliasToColumnHints.has(columnName)\n      ? getColumnByHint(query.builderOptions, logAliasToColumnHints.get(columnName)!)\n      : undefined;\n    const column = lookupByAlias || lookupByName || lookupByLogsAlias;\n    const columnType = column ? column.type || '' : '';\n    const hasMapKey = mapKey !== '';\n\n    let nextFilters: Filter[] = query.builderOptions.filters?.slice() || [];\n    if (action.type === 'ADD_FILTER') {\n      // we need to remove *any other EQ or NE* for the same field,\n      // because we don't want to end up with two filters like `level=info` AND `level=error`\n      nextFilters = nextFilters.filter(\n        (f) =>\n          !(\n            f.type === 'string' &&\n            (column && column.hint && f.hint ? f.hint === column.hint : f.key === columnName) &&\n            (f.operator === FilterOperator.IsAnything ||\n              f.operator === FilterOperator.Equals ||\n              f.operator === FilterOperator.NotEquals)\n          ) &&\n          !(\n            (f.type.startsWith('Map') || f.type.startsWith('JSON')) &&\n            column &&\n            hasMapKey &&\n            f.mapKey === mapKey &&\n            (f.operator === FilterOperator.IsAnything ||\n              f.operator === FilterOperator.Equals ||\n              f.operator === FilterOperator.NotEquals)\n          )\n      );\n\n      nextFilters.push({\n        condition: 'AND',\n        key: column && column.hint ? '' : columnName,\n        hint: column && column.hint ? column.hint : undefined,\n        mapKey: hasMapKey ? mapKey : undefined,\n        type: hasMapKey ? (columnType.startsWith('Map') ? 'Map(String, String)' : 'JSON') : 'String',\n        filterType: 'custom',\n        operator: FilterOperator.Equals,\n        value: actionValue,\n      });\n    } else if (action.type === 'ADD_FILTER_OUT') {\n      // with this we might want to add multiple values as NE filters\n      // for example, `level != info` AND `level != debug`\n      // thus, here we remove only exactly matching NE filters or an existing EQ filter for this field\n      nextFilters = nextFilters.filter(\n        (f) =>\n          !(\n            (f.type === 'string' &&\n              (column && column.hint && f.hint ? f.hint === column.hint : f.key === columnName) &&\n              'value' in f &&\n              f.value === actionValue &&\n              (f.operator === FilterOperator.IsAnything || f.operator === FilterOperator.NotEquals)) ||\n            (f.type === 'string' &&\n              (column && column.hint && f.hint ? f.hint === column.hint : f.key === columnName) &&\n              (f.operator === FilterOperator.IsAnything || f.operator === FilterOperator.Equals)) ||\n            ((f.type.startsWith('Map') || f.type.startsWith('JSON')) &&\n              column &&\n              hasMapKey &&\n              f.mapKey === mapKey &&\n              (f.operator === FilterOperator.IsAnything || f.operator === FilterOperator.Equals))\n          )\n      );\n\n      nextFilters.push({\n        condition: 'AND',\n        key: column && column.hint ? '' : columnName,\n        hint: column && column.hint ? column.hint : undefined,\n        mapKey: hasMapKey ? mapKey : undefined,\n        type: hasMapKey ? (columnType.startsWith('Map') ? 'Map(String, String)' : 'JSON') : 'String',\n        filterType: 'custom',\n        operator: FilterOperator.NotEquals,\n        value: actionValue,\n      });\n    } else if (action.type === 'ADD_STRING_FILTER') {\n      nextFilters.push({\n        condition: 'AND',\n        key: columnName,\n        filterType: 'custom',\n        type: 'string',\n        operator: FilterOperator.ILike,\n        value: actionValue,\n      });\n    } else if (action.type === 'ADD_STRING_FILTER_OUT') {\n      nextFilters.push({\n        condition: 'AND',\n        key: columnName,\n        filterType: 'custom',\n        type: 'string',\n        operator: FilterOperator.NotILike,\n        value: actionValue,\n      });\n    }\n\n    // the query is updated to trigger the URL update and propagation to the panels\n    const nextOptions = { ...query.builderOptions, filters: nextFilters };\n    return {\n      ...query,\n      rawSql: generateSql(nextOptions),\n      builderOptions: nextOptions,\n    };\n  }\n\n  private getMacroArgs(query: string, argsIndex: number): string[] {\n    const args = [] as string[];\n    const re = /\\(|\\)|,/g;\n    let bracketCount = 0;\n    let lastArgEndIndex = 1;\n    let regExpArray: RegExpExecArray | null;\n    const argsSubstr = query.substring(argsIndex, query.length);\n    while ((regExpArray = re.exec(argsSubstr)) !== null) {\n      const foundNode = regExpArray[0];\n      if (foundNode === '(') {\n        bracketCount++;\n      } else if (foundNode === ')') {\n        bracketCount--;\n      }\n      if (foundNode === ',' && bracketCount === 1) {\n        args.push(argsSubstr.substring(lastArgEndIndex, re.lastIndex - 1));\n        lastArgEndIndex = re.lastIndex;\n      }\n      if (bracketCount === 0) {\n        args.push(argsSubstr.substring(lastArgEndIndex, re.lastIndex - 1));\n        return args;\n      }\n    }\n    return [];\n  }\n\n  private replace(value?: string, scopedVars?: ScopedVars) {\n    if (value !== undefined) {\n      return getTemplateSrv().replace(value, scopedVars, this.format);\n    }\n    return value;\n  }\n\n  private format(value: any) {\n    if (Array.isArray(value)) {\n      return `'${value.join(\"','\")}'`;\n    }\n    return value;\n  }\n\n  getDefaultDatabase(): string {\n    return this.settings.jsonData.defaultDatabase || 'default';\n  }\n\n  getDefaultTable(): string | undefined {\n    return this.settings.jsonData.defaultTable;\n  }\n\n  getDefaultLogsDatabase(): string | undefined {\n    return this.settings.jsonData.logs?.defaultDatabase;\n  }\n\n  getDefaultLogsTable(): string | undefined {\n    return this.settings.jsonData.logs?.defaultTable;\n  }\n\n  getDefaultLogsColumns(): Map<ColumnHint, string> {\n    const result = new Map<ColumnHint, string>();\n    const logsConfig = this.settings.jsonData.logs;\n    if (!logsConfig) {\n      return result;\n    }\n\n    const otelEnabled = logsConfig.otelEnabled;\n    const otelVersion = logsConfig.otelVersion;\n\n    const otelConfig = otel.getVersion(otelVersion);\n    if (otelEnabled && otelConfig) {\n      return otelConfig.logColumnMap;\n    }\n\n    logsConfig.filterTimeColumn && result.set(ColumnHint.FilterTime, logsConfig.filterTimeColumn);\n    logsConfig.timeColumn && result.set(ColumnHint.Time, logsConfig.timeColumn);\n    logsConfig.levelColumn && result.set(ColumnHint.LogLevel, logsConfig.levelColumn);\n    logsConfig.messageColumn && result.set(ColumnHint.LogMessage, logsConfig.messageColumn);\n\n    return result;\n  }\n\n  shouldSelectLogContextColumns(): boolean {\n    return this.settings.jsonData.logs?.selectContextColumns || false;\n  }\n\n  getLogContextColumnNames(): string[] {\n    return this.settings.jsonData.logs?.contextColumns?.length ? this.settings.jsonData.logs?.contextColumns : [];\n  }\n\n  /**\n   * Get configured OTEL version for logs. Returns undefined when versioning is disabled/unset.\n   */\n  getLogsOtelVersion(): string | undefined {\n    const logConfig = this.settings.jsonData.logs;\n    return logConfig?.otelEnabled ? logConfig.otelVersion || undefined : undefined;\n  }\n\n  getDefaultTraceDatabase(): string | undefined {\n    return this.settings.jsonData.traces?.defaultDatabase;\n  }\n\n  getDefaultTraceTable(): string | undefined {\n    return this.settings.jsonData.traces?.defaultTable;\n  }\n\n  getDefaultTraceColumns(): Map<ColumnHint, string> {\n    const result = new Map<ColumnHint, string>();\n    const traceConfig = this.settings.jsonData.traces;\n    if (!traceConfig) {\n      return result;\n    }\n\n    const otelEnabled = traceConfig.otelEnabled;\n    const otelVersion = traceConfig.otelVersion;\n\n    const otelConfig = otel.getVersion(otelVersion);\n    if (otelEnabled && otelConfig) {\n      return otelConfig.traceColumnMap;\n    }\n\n    traceConfig.traceIdColumn && result.set(ColumnHint.TraceId, traceConfig.traceIdColumn);\n    traceConfig.spanIdColumn && result.set(ColumnHint.TraceSpanId, traceConfig.spanIdColumn);\n    traceConfig.operationNameColumn && result.set(ColumnHint.TraceOperationName, traceConfig.operationNameColumn);\n    traceConfig.parentSpanIdColumn && result.set(ColumnHint.TraceParentSpanId, traceConfig.parentSpanIdColumn);\n    traceConfig.serviceNameColumn && result.set(ColumnHint.TraceServiceName, traceConfig.serviceNameColumn);\n    traceConfig.durationColumn && result.set(ColumnHint.TraceDurationTime, traceConfig.durationColumn);\n    traceConfig.startTimeColumn && result.set(ColumnHint.Time, traceConfig.startTimeColumn);\n    traceConfig.tagsColumn && result.set(ColumnHint.TraceTags, traceConfig.tagsColumn);\n    traceConfig.serviceTagsColumn && result.set(ColumnHint.TraceServiceTags, traceConfig.serviceTagsColumn);\n    traceConfig.kindColumn && result.set(ColumnHint.TraceKind, traceConfig.kindColumn);\n    traceConfig.statusCodeColumn && result.set(ColumnHint.TraceStatusCode, traceConfig.statusCodeColumn);\n    traceConfig.statusMessageColumn && result.set(ColumnHint.TraceStatusMessage, traceConfig.statusMessageColumn);\n    traceConfig.instrumentationLibraryNameColumn &&\n      result.set(ColumnHint.TraceInstrumentationLibraryName, traceConfig.instrumentationLibraryNameColumn);\n    traceConfig.instrumentationLibraryVersionColumn &&\n      result.set(ColumnHint.TraceInstrumentationLibraryVersion, traceConfig.instrumentationLibraryVersionColumn);\n    traceConfig.stateColumn && result.set(ColumnHint.TraceState, traceConfig.stateColumn);\n\n    return result;\n  }\n\n  /**\n   * Get configured OTEL version for traces. Returns undefined when versioning is disabled/unset.\n   */\n  getTraceOtelVersion(): string | undefined {\n    const traceConfig = this.settings.jsonData.traces;\n    return traceConfig?.otelEnabled ? traceConfig.otelVersion || undefined : undefined;\n  }\n\n  getDefaultTraceDurationUnit(): TimeUnit {\n    return (this.settings.jsonData.traces?.durationUnit as TimeUnit) || TimeUnit.Nanoseconds;\n  }\n\n  getDefaultTraceFlattenNested(): boolean {\n    return this.settings.jsonData.traces?.flattenNested || false;\n  }\n\n  getDefaultTraceEventsColumnPrefix(): string {\n    return this.settings.jsonData.traces?.traceEventsColumnPrefix || 'Events';\n  }\n\n  getDefaultTraceLinksColumnPrefix(): string {\n    return this.settings.jsonData.traces?.traceLinksColumnPrefix || 'Links';\n  }\n\n  /**\n   * Returns the suffix used to locate the companion trace-timestamp index table.\n   * Defaults to the OTel convention (`_trace_id_ts`) when nothing is configured\n   * so the two-step trace ID lookup works out of the box for OTel users and\n   * can be opted into by non-OTel users that follow the same naming convention.\n   */\n  getTraceTimestampTableSuffix(): string {\n    return this.settings.jsonData.traces?.traceTimestampTableSuffix || otel.traceTimestampTableSuffix;\n  }\n\n  /**\n   * Get the TraceId column name from traces configuration\n   * Used when creating logs filter to correlate with trace data\n   */\n  getTracesTraceIdColumn(): string | undefined {\n    const traceConfig = this.settings.jsonData.traces;\n    if (!traceConfig) {\n      return undefined;\n    }\n\n    const otelEnabled = traceConfig.otelEnabled;\n    const otelVersion = traceConfig.otelVersion;\n\n    const otelConfig = otel.getVersion(otelVersion);\n    if (otelEnabled && otelConfig) {\n      return otelConfig.traceColumnMap.get(ColumnHint.TraceId);\n    }\n\n    return traceConfig.traceIdColumn;\n  }\n\n  async fetchDatabases(): Promise<string[]> {\n    return this.fetchData('SHOW DATABASES');\n  }\n\n  async fetchTables(db?: string): Promise<string[]> {\n    const rawSql = db ? `SHOW TABLES FROM \"${db}\"` : 'SHOW TABLES';\n    return this.fetchData(rawSql);\n  }\n\n  /**\n   * Used to populate suggestions in the filter editor for Map columns.\n   *\n   * Samples rows to get a unique set of keys for the map.\n   * May not include ALL keys for a given dataset.\n   *\n   * TODO: This query can be slow/expensive\n   */\n  async fetchUniqueMapKeys(mapColumn: string, db: string, table: string): Promise<string[]> {\n    const rawSql = `SELECT DISTINCT arrayJoin(${mapColumn}.keys) as keys FROM \"${db}\".\"${table}\" LIMIT 1000`;\n    return this.fetchData(rawSql);\n  }\n\n  async fetchEntities() {\n    return this.fetchTables();\n  }\n\n  async fetchFields(database: string, table: string): Promise<string[]> {\n    return this.fetchData(`DESC TABLE \"${database}\".\"${table}\"`);\n  }\n\n  /**\n   * Fetches JSON column suggestions for each specified JSON column.\n   */\n  async fetchPathsForJSONColumns(\n    database: string | undefined,\n    table: string,\n    jsonColumnName: string\n  ): Promise<TableColumn[]> {\n    const prefix = Boolean(database) ? `\"${database}\".` : '';\n    const rawSql = `SELECT arrayJoin(distinctJSONPathsAndTypes(${jsonColumnName})) FROM ${prefix}\"${table}\" SETTINGS max_execution_time=10`;\n    const frame = await this.runQuery({ rawSql });\n    if (frame.fields?.length === 0) {\n      return [];\n    }\n\n    const view = new DataFrameView(frame);\n    const jsonPathsAndTypes: Array<[string, string]> = [];\n    for (let x of view) {\n      if (!x || !x[0]) {\n        continue;\n      }\n\n      const kv = typeof x[0] === 'string' ? JSON.parse(x[0]) : x[0];\n      if (!kv.keys || !kv.values) {\n        continue;\n      }\n\n      jsonPathsAndTypes.push([kv.keys, kv.values]);\n    }\n\n    const columns: TableColumn[] = [];\n    for (let pathAndTypes of jsonPathsAndTypes) {\n      const path = pathAndTypes[0];\n      const types = pathAndTypes[1];\n      if (!path || !types || types.length === 0) {\n        continue;\n      }\n\n      columns.push({\n        name: `${jsonColumnName}.${path}`,\n        label: `${jsonColumnName}.${path}`,\n        type: types[0],\n        picklistValues: [],\n      });\n    }\n\n    return columns;\n  }\n\n  /**\n   * Fetches column suggestions from the table schema.\n   */\n  async fetchColumnsFromTable(database: string | undefined, table: string): Promise<TableColumn[]> {\n    const prefix = Boolean(database) ? `\"${database}\".` : '';\n    const rawSql = `DESC TABLE ${prefix}\"${table}\"`;\n    const frame = await this.runQuery({ rawSql });\n    if (frame.fields?.length === 0) {\n      return [];\n    }\n    const view = new DataFrameView(frame);\n    const columns: TableColumn[] = view.map((item) => ({\n      name: item[0],\n      type: item[1],\n      label: item[0],\n      picklistValues: [],\n    }));\n\n    return columns;\n\n    // TODO: wait for JSON function perf improvements\n    // const results = await Promise.all(\n    //   columns\n    //     .filter((c) => c.type.startsWith('JSON'))\n    //     .map((c) => this.fetchPathsForJSONColumns(database, table, c.name))\n    // );\n    // return [...columns, ...results.flat()];\n  }\n\n  /**\n   * Fetches SQL functions from server.\n   */\n  async fetchSqlFunctions(): Promise<SqlFunction[]> {\n    const rawSql = `\n      SELECT\n        name, is_aggregate, case_insensitive, alias_to, origin, description,\n        syntax, arguments, returned_value, examples, categories\n      FROM system.functions\n      LIMIT 10000\n    `;\n    const frame = await this.runQuery({ rawSql });\n    if (frame.fields?.length === 0) {\n      return [];\n    }\n    const view = new DataFrameView(frame);\n    const sqlFunctions: SqlFunction[] = view.map((item) => ({\n      name: String(item[0]),\n      isAggregate: Boolean(item[1]),\n      caseInsensitive: Boolean(item[2]),\n      aliasTo: String(item[3]),\n      origin: String(item[4]),\n      description: String(item[5]),\n      syntax: String(item[6]),\n      arguments: String(item[7]),\n      returnedValue: String(item[8]),\n      examples: String(item[9]),\n      categories: String(item[10]),\n    }));\n\n    return sqlFunctions;\n  }\n\n  /**\n   * Fetches column suggestions from an alias definition table.\n   */\n  async fetchColumnsFromAliasTable(fullTableName: string): Promise<TableColumn[]> {\n    const rawSql = `SELECT alias, select, \"type\" FROM ${fullTableName}`;\n    const frame = await this.runQuery({ rawSql });\n    if (frame.fields?.length === 0) {\n      return [];\n    }\n    const view = new DataFrameView(frame);\n    return view.map((item) => ({\n      name: item[1],\n      type: item[2],\n      label: item[0],\n      picklistValues: [],\n    }));\n  }\n\n  getAliasTable(targetDatabase: string | undefined, targetTable: string): string | null {\n    const aliasEntries = this.settings?.jsonData?.aliasTables || [];\n    const matchedEntry =\n      aliasEntries.find((e) => {\n        const matchDatabase = !e.targetDatabase || e.targetDatabase === targetDatabase;\n        const matchTable = e.targetTable === targetTable;\n        return matchDatabase && matchTable;\n      }) || null;\n\n    if (matchedEntry === null) {\n      return null;\n    }\n\n    const aliasDatabase = matchedEntry.aliasDatabase || targetDatabase || null;\n    const aliasTable = matchedEntry.aliasTable;\n    const prefix = Boolean(aliasDatabase) ? `\"${aliasDatabase}\".` : '';\n    return `${prefix}\"${aliasTable}\"`;\n  }\n\n  async fetchColumns(database: string | undefined, table: string): Promise<TableColumn[]> {\n    const fullAliasTableName = this.getAliasTable(database, table);\n    if (fullAliasTableName !== null) {\n      return this.fetchColumnsFromAliasTable(fullAliasTableName);\n    }\n\n    return this.fetchColumnsFromTable(database, table);\n  }\n\n  private async fetchData(rawSql: string) {\n    const frame = await this.runQuery({ rawSql });\n    return this.values(frame);\n  }\n\n  private getTimezone(request: DataQueryRequest<CHQuery>): string | undefined {\n    // timezone specified in the time picker\n    if (request.timezone && request.timezone !== 'browser') {\n      return request.timezone;\n    }\n    // fall back to the local timezone\n    const localTimezoneInfo = getTimeZoneInfo(getTimeZone(), Date.now());\n    return localTimezoneInfo?.ianaName;\n  }\n\n  filterQuery(query: CHQuery): boolean {\n    return !query.hide;\n  }\n\n  query(request: DataQueryRequest<CHQuery>): Observable<DataQueryResponse> {\n    const targets = request.targets\n      // attach timezone information\n      .map((t) => {\n        return {\n          ...t,\n          meta: {\n            ...t?.meta,\n            timezone: this.getTimezone(request),\n          },\n        };\n      });\n\n    const hasLogsVolumeTargets = targets.some((t) => t.refId?.startsWith(Datasource.logVolumePrefix));\n\n    return super\n      .query({\n        ...request,\n        targets,\n      })\n      .pipe(\n        map((res: DataQueryResponse) => {\n          const transformed = transformQueryResponseWithTraceAndLogLinks(this, request, res);\n          if (hasLogsVolumeTargets) {\n            return { ...transformed, data: splitLogsVolumeFrames(transformed.data, Datasource.logVolumePrefix) };\n          }\n          return transformed;\n        })\n      );\n  }\n\n  private runQuery(request: Partial<CHQuery>, options?: any): Promise<DataFrame> {\n    return new Promise((resolve) => {\n      const req = {\n        targets: [{ ...request, refId: String(Math.random()) }],\n        range: options ? options.range : (getTemplateSrv() as any).timeRange,\n      } as DataQueryRequest<CHQuery>;\n      this.query(req).subscribe((res: DataQueryResponse) => {\n        resolve(res.data[0] || { fields: [] });\n      });\n    });\n  }\n\n  private values(frame: DataFrame) {\n    if (frame.fields?.length === 0) {\n      return [];\n    }\n    return frame?.fields[0]?.values.map((text) => text);\n  }\n\n  async getTagKeys(): Promise<MetricFindValue[]> {\n    if (this.adHocFiltersStatus === AdHocFilterStatus.disabled || this.adHocFiltersStatus === AdHocFilterStatus.none) {\n      this.adHocFiltersStatus = await this.canUseAdhocFilters();\n      if (this.adHocFiltersStatus === AdHocFilterStatus.disabled) {\n        return {} as MetricFindValue[];\n      }\n    }\n    const { type, frame } = await this.fetchTags();\n    if (type === TagType.query) {\n      return frame.fields.map((f) => ({ text: f.name }));\n    }\n    const view = new DataFrameView(frame);\n    const hideTableName = this.settings.jsonData.hideTableNameInAdhocFilters || false;\n    return view.map((item) => ({\n      text: hideTableName ? item[0] : `${item[2]}.${item[0]}`,\n    }));\n  }\n\n  async getTagValues({ key }: any): Promise<MetricFindValue[]> {\n    const { type } = this.getTagSource();\n    this.skipAdHocFilter = true;\n    if (type === TagType.query) {\n      return this.fetchTagValuesFromQuery(key);\n    }\n    return this.fetchTagValuesFromSchema(key);\n  }\n\n  private fieldValuesToMetricFindValues(field: Field): MetricFindValue[] {\n    // Convert to string to avoid https://github.com/grafana/grafana/issues/12209\n    return field.values\n      .filter((value) => value !== null)\n      .map((value) => {\n        return { text: String(value) };\n      });\n  }\n\n  private async fetchTagValuesFromSchema(key: string): Promise<MetricFindValue[]> {\n    const { from } = this.getTagSource();\n    const hideTableName = this.settings.jsonData.hideTableNameInAdhocFilters || false;\n\n    let col: string;\n    let source: string;\n\n    if (hideTableName && from) {\n      // When hideTableNameInAdhocFilters is true, key is just the column name (e.g., 'bar')\n      col = key;\n      source = from;\n    } else {\n      // When hideTableNameInAdhocFilters is false, key is 'table.column' format (e.g., 'foo.bar')\n      const [table, ...colParts] = key.split('.');\n      col = colParts.join('.');\n      source = from?.includes('.') ? `${from.split('.')[0]}.${table}` : table;\n    }\n\n    const rawSql = `select distinct ${col} from ${source} limit 1000`;\n    const frame = await this.runQuery({ rawSql });\n    if (frame.fields?.length === 0) {\n      return [];\n    }\n    const field = frame.fields[0];\n    return this.fieldValuesToMetricFindValues(field);\n  }\n\n  private async fetchTagValuesFromQuery(key: string): Promise<MetricFindValue[]> {\n    const tagSource = this.getTagSource();\n\n    // Check if the query contains the $__adhoc_column macro\n    if (tagSource.source && tagSource.source.includes('$__adhoc_column')) {\n      // Replace the macro with the actual column name\n      const queryWithColumn = tagSource.source.replace(/\\$__adhoc_column/g, key);\n      this.skipAdHocFilter = true;\n      const frame = await this.runQuery({ rawSql: queryWithColumn });\n\n      if (frame.fields?.length === 0) {\n        return [];\n      }\n\n      const field = frame.fields[0];\n      return this.fieldValuesToMetricFindValues(field);\n    }\n\n    // Fallback to the original behavior\n    const { frame } = await this.fetchTags();\n    const field = frame.fields.find((f) => f.name === key);\n    if (field) {\n      return this.fieldValuesToMetricFindValues(field);\n    }\n    return [];\n  }\n\n  private async fetchTags(): Promise<Tags> {\n    const tagSource = this.getTagSource();\n    this.skipAdHocFilter = true;\n\n    if (tagSource.source === undefined) {\n      const rawSql = 'SELECT name, type, table FROM system.columns';\n      const results = await this.runQuery({ rawSql });\n      return { type: TagType.schema, frame: results };\n    }\n\n    if (tagSource.type === TagType.query) {\n      // Check if the query contains the $__adhoc_column macro\n      if (tagSource.source.includes('$__adhoc_column')) {\n        // Extract table name from the query and get column list from system.columns\n        const tableName = this.extractTableNameFromQuery(tagSource.source);\n        if (tableName) {\n          this.adHocFilter.setTargetTableFromQuery(tagSource.source.replace(/\\$__adhoc_column/g, '*'));\n\n          // Parse database.table format\n          const parts = tableName.split('.');\n          let query: string;\n          if (parts.length === 2) {\n            const [db, table] = parts;\n            query = `SELECT name, type, table FROM system.columns WHERE database = '${db}' AND table = '${table}'`;\n          } else {\n            query = `SELECT name, type, table FROM system.columns WHERE table = '${tableName}'`;\n          }\n          const results = await this.runQuery({ rawSql: query });\n          return { type: TagType.schema, frame: results };\n        }\n      } else {\n        this.adHocFilter.setTargetTableFromQuery(tagSource.source);\n      }\n    }\n\n    const results = await this.runQuery({ rawSql: tagSource.source });\n    return { type: tagSource.type, frame: results };\n  }\n\n  private extractTableNameFromQuery(query: string): string | null {\n    // Try to extract table name from FROM clause\n    // Supports formats: FROM table, FROM database.table, FROM \"database\".\"table\"\n    const fromMatch = query.match(/FROM\\s+(?:\"?(\\w+)\"?\\.)?\"?(\\w+)\"?/i);\n    if (fromMatch) {\n      const database = fromMatch[1];\n      const table = fromMatch[2];\n      return database ? `${database}.${table}` : table;\n    }\n    return null;\n  }\n\n  private getTagSource() {\n    // @todo https://github.com/grafana/grafana/issues/13109\n    const ADHOC_VAR = '$clickhouse_adhoc_query';\n    const defaultDatabase = this.getDefaultDatabase();\n    let source = getTemplateSrv().replace(ADHOC_VAR);\n    if (source === ADHOC_VAR && isEmpty(defaultDatabase)) {\n      return { type: TagType.schema, source: undefined };\n    }\n    source = source === ADHOC_VAR ? defaultDatabase! : source;\n    if (source.toLowerCase().startsWith('select')) {\n      return { type: TagType.query, source };\n    }\n    if (!source.includes('.')) {\n      const sql = `SELECT name, type, table FROM system.columns WHERE database IN ('${source}')`;\n      return { type: TagType.schema, source: sql, from: source };\n    }\n    const [db, table] = source.split('.');\n    const sql = `SELECT name, type, table FROM system.columns WHERE database IN ('${db}') AND table = '${table}'`;\n    return { type: TagType.schema, source: sql, from: source };\n  }\n\n  // Returns true if ClickHouse's version is greater than or equal to 22.7\n  // 22.7 added 'settings additional_table_filters' which is used for ad hoc filters\n  private async canUseAdhocFilters(): Promise<AdHocFilterStatus> {\n    this.skipAdHocFilter = true;\n    const data = await this.fetchData(`SELECT version()`);\n    try {\n      const verString = (data[0] as unknown as string).split('.');\n      const ver = { major: Number.parseInt(verString[0], 10), minor: Number.parseInt(verString[1], 10) };\n      return ver.major > this.adHocCHVerReq.major ||\n        (ver.major === this.adHocCHVerReq.major && ver.minor >= this.adHocCHVerReq.minor)\n        ? AdHocFilterStatus.enabled\n        : AdHocFilterStatus.disabled;\n    } catch (err) {\n      console.error(`Unable to parse ClickHouse version: ${err}`);\n      throw err;\n    }\n  }\n\n  // interface DataSourceWithLogsContextSupport\n  getLogContextColumnsFromLogRow(row: LogRowModel): LogContextColumn[] {\n    const contextColumnNames = this.getLogContextColumnNames();\n    const contextColumns: LogContextColumn[] = [];\n\n    for (let columnName of contextColumnNames) {\n      const isMapKey = columnName.includes(\"['\") && columnName.includes(\"']\");\n      let mapName = '';\n      let keyName = '';\n      if (isMapKey) {\n        mapName = columnName.substring(0, columnName.indexOf('['));\n        keyName = columnName.substring(columnName.indexOf(\"['\") + 2, columnName.lastIndexOf(\"']\"));\n      }\n\n      const field = row.dataFrame.fields.find(\n        (f) =>\n          // exact column name match\n          f.name === columnName ||\n          (isMapKey &&\n            // entire map was selected\n            (f.name === mapName ||\n              // single key was selected from map\n              f.name === `arrayElement(${mapName}, '${keyName}')` ||\n              f.name === 'labels'))\n      );\n      if (!field) {\n        continue;\n      }\n\n      let value = field.values.get(row.rowIndex);\n      if (value && field.type === 'other' && isMapKey) {\n        // Extract merged Resource/Log Attributes from \"labels\"\n        if (field.name === labelsFieldName) {\n          value = value[`${mapName}.${keyName}`];\n        } else {\n          value = value[keyName];\n        }\n      }\n\n      if (!value) {\n        continue;\n      }\n\n      let contextColumnName: string;\n      if (isMapKey) {\n        contextColumnName = `${mapName}['${keyName}']`;\n      } else {\n        contextColumnName = columnName;\n      }\n\n      contextColumns.push({\n        name: contextColumnName,\n        value,\n      });\n    }\n\n    return contextColumns;\n  }\n\n  /**\n   * Runs a query based on a single log row and a direction (forward/backward)\n   *\n   * Will remove all filters and ORDER BYs, and will re-add them based on the configured context columns.\n   * Context columns are used to narrow down to a single logging unit as defined by your logging infrastructure.\n   * Typically this will be a single service, or container/pod in docker/k8s.\n   *\n   * If no context columns can be matched from the selected data frame, then the query is not run.\n   */\n  async getLogRowContext(\n    row: LogRowModel,\n    options?: LogRowContextOptions,\n    query?: CHQuery | undefined,\n    cacheFilters?: boolean\n  ): Promise<DataQueryResponse> {\n    if (!query) {\n      throw new Error('Missing query for log context');\n    } else if (!options || !options.direction || options.limit === undefined) {\n      throw new Error('Missing log context options for query');\n    } else if (query.editorType === EditorType.SQL || !query.builderOptions) {\n      throw new Error('Log context feature only works for builder queries');\n    }\n\n    const contextQuery = cloneDeep(query);\n    contextQuery.refId = '';\n    const builderOptions = contextQuery.builderOptions;\n    builderOptions.limit = options.limit;\n\n    const timeColumn =\n      getColumnByHint(builderOptions, ColumnHint.FilterTime) || getColumnByHint(builderOptions, ColumnHint.Time);\n    if (!timeColumn) {\n      throw new Error('Missing time column for log context');\n    }\n\n    // Preserve the user's secondary ORDER BY (e.g. `offset ASC` alongside\n    // `timestamp DESC`) so rows with identical timestamps keep their\n    // stable order in the log-context view. The time column is forced to\n    // the front because context pagination uses it; the user's remaining\n    // ORDER BY entries ride along as tiebreakers. Drop any existing entry\n    // that targets the same time column to avoid duplicating it. See #1293.\n    const originalOrderBy = builderOptions.orderBy ?? [];\n    builderOptions.orderBy = [];\n    builderOptions.orderBy.push({\n      name: '',\n      hint: timeColumn.hint!,\n      dir: options.direction === LogRowContextQueryDirection.Forward ? OrderByDirection.ASC : OrderByDirection.DESC,\n    });\n    for (const entry of originalOrderBy) {\n      const targetsTimeColumn =\n        entry.hint === ColumnHint.Time ||\n        entry.hint === ColumnHint.FilterTime ||\n        (!!entry.name && entry.name === timeColumn.name);\n      if (targetsTimeColumn) {\n        continue;\n      }\n      builderOptions.orderBy.push(entry);\n    }\n\n    builderOptions.filters = [];\n    builderOptions.filters.push({\n      operator:\n        options.direction === LogRowContextQueryDirection.Forward\n          ? FilterOperator.GreaterThanOrEqual\n          : FilterOperator.LessThanOrEqual,\n      filterType: 'custom',\n      hint: timeColumn.hint!,\n      key: '',\n      value: `fromUnixTimestamp64Nano(${row.timeEpochNs})`,\n      type: 'datetime',\n      condition: 'AND',\n    });\n\n    const contextColumns = this.getLogContextColumnsFromLogRow(row);\n    if (contextColumns.length < 1) {\n      throw new Error('Unable to match any log context columns');\n    }\n\n    const contextColumnFilters: Filter[] = contextColumns.map((c) => ({\n      operator: FilterOperator.Equals,\n      filterType: 'custom',\n      key: c.name,\n      value: c.value,\n      type: 'string',\n      condition: 'AND',\n    }));\n    builderOptions.filters.push(...contextColumnFilters);\n\n    contextQuery.rawSql = generateSql(builderOptions);\n    const req = {\n      targets: [contextQuery],\n    } as DataQueryRequest<CHQuery>;\n\n    // Surface the underlying ClickHouse error instead of letting Grafana\n    // wrap it in the generic \"Error loading more logs\" banner. The observable\n    // returned by `this.query(req)` can reject with a structured error or emit\n    // a `DataQueryResponse` with a populated `errors`/`error` field. In both\n    // cases we want the original server message to reach the user. See #1362.\n    let response: DataQueryResponse;\n    try {\n      response = await firstValueFrom(this.query(req));\n    } catch (err) {\n      const detail = this.extractQueryErrorMessage(err);\n      throw new Error(detail ? `Log context query failed: ${detail}` : 'Log context query failed');\n    }\n\n    const responseError = response?.errors?.find((e) => !!e?.message)?.message;\n    if (responseError) {\n      throw new Error(`Log context query failed: ${responseError}`);\n    }\n\n    return response;\n  }\n\n  private extractQueryErrorMessage(err: unknown): string | undefined {\n    if (!err) {\n      return undefined;\n    }\n    if (typeof err === 'string') {\n      return err;\n    }\n    if (typeof err === 'object') {\n      const anyErr = err as { data?: { message?: string }; message?: string; statusText?: string };\n      return anyErr.data?.message || anyErr.message || anyErr.statusText;\n    }\n    return undefined;\n  }\n\n  /**\n   * Unused + deprecated but required by interface, log context button is always visible now\n   * https://github.com/grafana/grafana/issues/66819\n   */\n  showContextToggle(row?: LogRowModel): boolean {\n    return true;\n  }\n\n  /**\n   * Returns a React component that is displayed in the top portion of the log context panel\n   */\n  getLogRowContextUi(\n    row: LogRowModel,\n    runContextQuery?: (() => void) | undefined,\n    query?: CHQuery | undefined\n  ): ReactNode {\n    const contextColumns = this.getLogContextColumnsFromLogRow(row);\n    return createReactElement(LogsContextPanel, { columns: contextColumns, datasourceUid: this.uid });\n  }\n\n  async testDatasource(): Promise<{ status: string; message: string }> {\n    const result = await this.callHealthCheck();\n    if (result.status !== 'OK') {\n      const category = parseConnectionErrorCategory(result.message);\n      trackClickhouseHealthCheckFailed({\n        error_category: category,\n        protocol: this.settings.jsonData.protocol ?? 'native',\n      });\n      const detail = result.message.replace(/^\\[\\w+\\]\\s*/, '');\n      const hint = getConnectionErrorHint(category, detail);\n      const label = category === 'tls' ? 'TLS' : category.charAt(0).toUpperCase() + category.slice(1);\n      return {\n        status: 'error',\n        message: hint ? `${label} error [${detail}]: ${hint}` : result.message,\n      };\n    }\n    return { status: 'success', message: result.message };\n  }\n}\n\n// parseConnectionErrorCategory extracts the error category embedded by the backend in\n// health check failure messages of the form \"[category] original error message\".\nfunction parseConnectionErrorCategory(message: string): string {\n  const match = message?.match(/^\\[(\\w+)\\]/);\n  return match ? match[1] : 'unknown';\n}\n\nconst CONNECTION_ERROR_HINTS: Record<string, string> = {\n  auth: 'Verify your credentials and that the user has the required permissions in ClickHouse.',\n  network:\n    'Check that the host and port are correct and that the ClickHouse server is reachable from the machine running Grafana.',\n  tls: 'Verify your TLS certificate configuration. If using a self-signed certificate, ensure the CA certificate is configured.',\n  timeout:\n    'Check that the ClickHouse server is reachable and consider increasing the dial timeout in the connection settings.',\n  config: 'Check that all required fields are correctly filled in.',\n};\n\nfunction getConnectionErrorHint(category: string, detail: string): string | undefined {\n  if (category === 'tls' && detail.includes('first record does not look like a TLS handshake')) {\n    return 'The server does not appear to be using TLS. Try disabling the secure connection toggle.';\n  }\n  return CONNECTION_ERROR_HINTS[category];\n}\n\nenum TagType {\n  query,\n  schema,\n}\n\nenum AdHocFilterStatus {\n  none = 0,\n  enabled,\n  disabled,\n}\n\ninterface Tags {\n  type?: TagType;\n  frame: DataFrame;\n}\n\nexport interface LogContextColumn {\n  name: string;\n  value: string;\n}\n"
  },
  {
    "path": "src/data/adHocFilter.test.ts",
    "content": "import { AdHocVariableFilter } from '@grafana/data';\nimport { AdHocFilter } from './adHocFilter';\n\ndescribe('AdHocManager', () => {\n  it('apply ad hoc filter with no inner query and existing WHERE', () => {\n    const ahm = new AdHocFilter();\n    ahm.setTargetTableFromQuery('SELECT * FROM foo');\n    const val = ahm.apply('SELECT stuff FROM foo WHERE col = test', [\n      { key: 'key', operator: '=', value: 'val' },\n      { key: 'keyNum', operator: '=', value: '123' },\n    ] as AdHocVariableFilter[]);\n    expect(val).toEqual(\n      `SELECT stuff FROM foo WHERE col = test settings additional_table_filters={'foo' : ' key = \\\\'val\\\\' AND keyNum = \\\\'123\\\\' '}`\n    );\n  });\n  it('apply ad hoc filter with no inner query and no existing WHERE', () => {\n    const ahm = new AdHocFilter();\n    ahm.setTargetTableFromQuery('SELECT * FROM foo');\n    const val = ahm.apply('SELECT stuff FROM foo', [\n      { key: 'key', operator: '=', value: 'val' },\n      { key: 'keyNum', operator: '=', value: '123' },\n    ] as AdHocVariableFilter[]);\n    expect(val).toEqual(\n      `SELECT stuff FROM foo settings additional_table_filters={'foo' : ' key = \\\\'val\\\\' AND keyNum = \\\\'123\\\\' '}`\n    );\n  });\n  it('apply ad hoc filter with an inner query without existing WHERE', () => {\n    const ahm = new AdHocFilter();\n    ahm.setTargetTableFromQuery('SELECT * FROM foo');\n    const val = ahm.apply(`SELECT stuff FROM (SELECT * FROM foo) as r , bar GROUP BY s ORDER BY s`, [\n      { key: 'key', operator: '=', value: 'val' },\n      { key: 'keyNum', operator: '=', value: '123' },\n    ] as AdHocVariableFilter[]);\n    expect(val).toEqual(\n      `SELECT stuff FROM (SELECT * FROM foo) as r , bar GROUP BY s ORDER BY s settings additional_table_filters={'foo' : ' key = \\\\'val\\\\' AND keyNum = \\\\'123\\\\' '}`\n    );\n  });\n  it('apply ad hoc filter with an inner from query with existing WHERE', () => {\n    const ahm = new AdHocFilter();\n    ahm.setTargetTableFromQuery('SELECT * FROM foo');\n    const val = ahm.apply(`SELECT stuff FROM (SELECT * FROM foo WHERE col = test) as r GROUP BY s ORDER BY s`, [\n      { key: 'key', operator: '=', value: 'val' },\n      { key: 'keyNum', operator: '=', value: '123' },\n    ] as AdHocVariableFilter[]);\n    expect(val).toEqual(\n      `SELECT stuff FROM (SELECT * FROM foo WHERE col = test) as r GROUP BY s ORDER BY s settings additional_table_filters={'foo' : ' key = \\\\'val\\\\' AND keyNum = \\\\'123\\\\' '}`\n    );\n  });\n  it('apply ad hoc filter with an inner where query with existing WHERE', () => {\n    const ahm = new AdHocFilter();\n    ahm.setTargetTableFromQuery('SELECT * FROM foo');\n    const val = ahm.apply(\n      `SELECT * FROM foo WHERE (name = stuff) AND (name IN ( SELECT * FROM foo WHERE (field = 'hello') GROUP BY name ORDER BY count() DESC LIMIT 10 )) GROUP BY name , time ORDER BY time`,\n      [{ key: 'key', operator: '=', value: 'val' }] as AdHocVariableFilter[]\n    );\n    expect(val).toEqual(\n      `SELECT * FROM foo WHERE (name = stuff) AND (name IN ( SELECT * FROM foo WHERE (field = 'hello') GROUP BY name ORDER BY count() DESC LIMIT 10 )) GROUP BY name , time ORDER BY time settings additional_table_filters={'foo' : ' key = \\\\'val\\\\' '}`\n    );\n  });\n  it('does not apply ad hoc filter when the target table is not in the query', () => {\n    const ahm = new AdHocFilter();\n    ahm.setTargetTableFromQuery('SELECT * FROM bar');\n    const val = ahm.apply('select stuff FROM foo', [\n      { key: 'key', operator: '=', value: 'val' },\n    ] as AdHocVariableFilter[]);\n    expect(val).toEqual('select stuff FROM foo');\n  });\n  it('apply ad hoc filter when the ad hoc options are from a query with a from inline query', () => {\n    const ahm = new AdHocFilter();\n    ahm.setTargetTableFromQuery('SELECT * FROM (select * FROM foo) bar');\n    const val = ahm.apply('select stuff FROM foo', [\n      { key: 'key', operator: '=', value: 'val' },\n    ] as AdHocVariableFilter[]);\n    expect(val).toEqual(`select stuff FROM foo settings additional_table_filters={'foo' : ' key = \\\\'val\\\\' '}`);\n  });\n  it('apply ad hoc filter when the ad hoc options are from a query with a where inline query', () => {\n    const ahm = new AdHocFilter();\n    ahm.setTargetTableFromQuery(\n      'SELECT * FROM foo where stuff = stuff and (repo in (select * FROM foo)) order by stuff'\n    );\n    const val = ahm.apply('select stuff FROM foo', [\n      { key: 'key', operator: '=', value: 'val' },\n    ] as AdHocVariableFilter[]);\n    expect(val).toEqual(`select stuff FROM foo settings additional_table_filters={'foo' : ' key = \\\\'val\\\\' '}`);\n  });\n  it('apply ad hoc filter to complex join statement', () => {\n    const ahm = new AdHocFilter();\n    ahm.setTargetTableFromQuery(\n      'SELECT * FROM foo where stuff = stuff and (repo in (select * FROM foo)) order by stuff'\n    );\n    const val = ahm.apply(\n      `SELECT number, letter FROM foo AS x INNER JOIN (SELECT number FROM system.numbers LIMIT 5) AS inner_numbers ON inner_numbers.number = x.number ARRAY JOIN ['a', 'b'] AS letter LIMIT 5`,\n      [{ key: 'key', operator: '=', value: 'val' }] as AdHocVariableFilter[]\n    );\n    expect(val).toEqual(\n      `SELECT number, letter FROM foo AS x INNER JOIN (SELECT number FROM system.numbers LIMIT 5) AS inner_numbers ON inner_numbers.number = x.number ARRAY JOIN ['a', 'b'] AS letter LIMIT 5 settings additional_table_filters={'foo' : ' key = \\\\'val\\\\' '}`\n    );\n  });\n  it('throws an error when the adhoc filter select cannot be parsed', () => {\n    const ahm = new AdHocFilter();\n    expect(function () {\n      ahm.setTargetTableFromQuery('select not sql');\n    }).toThrow(new Error('Failed to get table from adhoc query.'));\n  });\n  it('apply ad hoc filter with same table casing', () => {\n    const ahm = new AdHocFilter();\n    ahm.setTargetTableFromQuery('SELECT * FROM fooTable');\n    const val = ahm.apply('SELECT stuff FROM fooTable', [\n      { key: 'key', operator: '=', value: 'val' },\n    ] as AdHocVariableFilter[]);\n    expect(val).toEqual(\n      `SELECT stuff FROM fooTable settings additional_table_filters={'fooTable' : ' key = \\\\'val\\\\' '}`\n    );\n  });\n  it('apply ad hoc filter with default schema', () => {\n    const ahm = new AdHocFilter();\n    ahm.setTargetTableFromQuery('SELECT * FROM default.foo');\n    const val = ahm.apply('SELECT stuff FROM default.foo', [\n      { key: 'key', operator: '=', value: 'val' },\n    ] as AdHocVariableFilter[]);\n    expect(val).toEqual(\n      `SELECT stuff FROM default.foo settings additional_table_filters={'default.foo' : ' key = \\\\'val\\\\' '}`\n    );\n  });\n  it('apply ad hoc filter and does not include the table reference in the selected fields of the function', () => {\n    const ahm = new AdHocFilter();\n    ahm.setTargetTableFromQuery('SELECT * FROM foo');\n    const val = ahm.apply('SELECT foo.stuff FROM foo', [\n      { key: 'foo.key', operator: '=', value: 'val' },\n    ] as AdHocVariableFilter[]);\n    expect(val).toEqual(`SELECT foo.stuff FROM foo settings additional_table_filters={'foo' : ' key = \\\\'val\\\\' '}`);\n  });\n\n  it('apply ad hoc filter converts \"=~\" to \"REGEXP\"', () => {\n    const ahm = new AdHocFilter();\n    ahm.setTargetTableFromQuery('SELECT * FROM foo');\n    const val = ahm.apply('SELECT stuff FROM foo WHERE col = test', [\n      { key: 'key', operator: '=~', value: 'val' },\n    ] as AdHocVariableFilter[]);\n    expect(val).toEqual(\n      `SELECT stuff FROM foo WHERE col = test settings additional_table_filters={'foo' : ' key REGEXP \\\\'val\\\\' '}`\n    );\n  });\n\n  it('apply ad hoc filter converts \"!~\" to \"NOT REGEXP\"', () => {\n    const ahm = new AdHocFilter();\n    ahm.setTargetTableFromQuery('SELECT * FROM foo');\n    const val = ahm.apply('SELECT stuff FROM foo WHERE col = test', [\n      { key: 'key', operator: '!~', value: 'val' },\n    ] as AdHocVariableFilter[]);\n    expect(val).toEqual(\n      `SELECT stuff FROM foo WHERE col = test settings additional_table_filters={'foo' : ' key NOT REGEXP \\\\'val\\\\' '}`\n    );\n  });\n\n  it('apply ad hoc filter IN operator with string values', () => {\n    const ahm = new AdHocFilter();\n    ahm.setTargetTableFromQuery('SELECT * FROM foo');\n    const val = ahm.apply('SELECT stuff FROM foo WHERE col = test', [\n      { key: 'key', operator: 'IN', value: \"('val1', 'val2')\" },\n    ] as AdHocVariableFilter[]);\n    expect(val).toEqual(\n      `SELECT stuff FROM foo WHERE col = test settings additional_table_filters={'foo' : ' key IN (\\\\'val1\\\\', \\\\'val2\\\\') '}`\n    );\n  });\n\n  it('apply ad hoc filter IN operator without parentheses', () => {\n    const ahm = new AdHocFilter();\n    ahm.setTargetTableFromQuery('SELECT * FROM foo');\n    const val = ahm.apply('SELECT stuff FROM foo WHERE col = test', [\n      { key: 'key', operator: 'IN', value: \"'val1', 'val2'\" },\n    ] as AdHocVariableFilter[]);\n    expect(val).toEqual(\n      `SELECT stuff FROM foo WHERE col = test settings additional_table_filters={'foo' : ' key IN (\\\\'val1\\\\', \\\\'val2\\\\') '}`\n    );\n  });\n\n  it('apply ad hoc filter IN operator with integer values', () => {\n    const ahm = new AdHocFilter();\n    ahm.setTargetTableFromQuery('SELECT * FROM foo');\n    const val = ahm.apply('SELECT stuff FROM foo WHERE col = test', [\n      { key: 'key', operator: 'IN', value: '(1, 2, 3)' },\n    ] as AdHocVariableFilter[]);\n    expect(val).toEqual(\n      `SELECT stuff FROM foo WHERE col = test settings additional_table_filters={'foo' : ' key IN (1, 2, 3) '}`\n    );\n  });\n\n  it('does not apply an adhoc filter without \"operator\"', () => {\n    const ahm = new AdHocFilter();\n    ahm.setTargetTableFromQuery('SELECT * FROM foo');\n    const val = ahm.apply('SELECT foo.stuff FROM foo', [\n      // @ts-expect-error\n      { key: 'foo.key', operator: undefined, value: 'val' },\n    ]);\n    expect(val).toEqual(`SELECT foo.stuff FROM foo`);\n  });\n\n  it('does not apply an adhoc filter without \"value\"', () => {\n    const ahm = new AdHocFilter();\n    ahm.setTargetTableFromQuery('SELECT * FROM foo');\n    const val = ahm.apply('SELECT foo.stuff FROM foo', [\n      // @ts-expect-error\n      { key: 'foo.key', operator: '=', value: undefined },\n    ]);\n    expect(val).toEqual(`SELECT foo.stuff FROM foo`);\n  });\n\n  it('does not apply an adhoc filter without \"key\"', () => {\n    const ahm = new AdHocFilter();\n    ahm.setTargetTableFromQuery('SELECT * FROM foo');\n    const val = ahm.apply('SELECT foo.stuff FROM foo', [\n      // @ts-expect-error\n      { key: undefined, operator: '=', value: 'val' },\n    ]);\n    expect(val).toEqual(`SELECT foo.stuff FROM foo`);\n  });\n\n  it('log a malformed filter', () => {\n    const warn = jest.spyOn(console, 'warn');\n    const value = { key: 'foo.key', operator: '=', value: undefined };\n    const ahm = new AdHocFilter();\n    ahm.setTargetTableFromQuery('SELECT * FROM foo');\n    ahm.apply('SELECT foo.stuff FROM foo', [\n      // @ts-expect-error\n      value,\n    ]);\n    expect(warn).toHaveBeenCalledTimes(1);\n    expect(warn).toHaveBeenCalledWith('Invalid adhoc filter will be ignored:', value);\n  });\n\n  it('apply ad hoc filter with no set table', () => {\n    const ahm = new AdHocFilter();\n    const val = ahm.apply('SELECT stuff FROM foo', [\n      { key: 'key', operator: '=', value: 'val' },\n    ] as AdHocVariableFilter[]);\n    expect(val).toEqual(`SELECT stuff FROM foo settings additional_table_filters={'foo' : ' key = \\\\'val\\\\' '}`);\n  });\n\n  it('converts arrayElement with single quotes', () => {\n    const ahm = new AdHocFilter();\n    const result = ahm.apply('SELECT * FROM foo', [\n      { key: \"arrayElement(ResourceAttributes, 'cloud.region')\", operator: '=', value: 'test' },\n    ] as AdHocVariableFilter[]);\n    expect(result).toContain(\"ResourceAttributes[\\\\'cloud.region\\\\']\");\n  });\n\n  it('converts Map column filter to proper filter syntax', () => {\n    const ahm = new AdHocFilter();\n    const result = ahm.apply('SELECT * FROM foo', [\n      { key: \"ResourceAttributes.cloud.region\", operator: '=', value: 'test' },\n    ] as AdHocVariableFilter[], false);\n    expect(result).toContain(\"ResourceAttributes[\\\\\\'cloud.region\\\\\\']\");\n  });\n  it('converts JSON column filter to proper filter syntax', () => {\n    const ahm = new AdHocFilter();\n    const result = ahm.apply('SELECT * FROM foo', [\n      { key: \"ResourceAttributes.cloud.region'\", operator: '=', value: 'test' },\n    ] as AdHocVariableFilter[], true);\n    expect(result).toContain('ResourceAttributes.cloud.region');\n  });\n\n  describe('buildFilterString', () => {\n    it('builds filter string with single filter', () => {\n      const ahm = new AdHocFilter();\n      const result = ahm.buildFilterString([{ key: 'key', operator: '=', value: 'val' }] as AdHocVariableFilter[]);\n      expect(result).toEqual(\" key = \\\\'val\\\\' \");\n    });\n\n    it('builds filter string with multiple filters', () => {\n      const ahm = new AdHocFilter();\n      const result = ahm.buildFilterString([\n        { key: 'key', operator: '=', value: 'val' },\n        { key: 'keyNum', operator: '=', value: '123' },\n      ] as AdHocVariableFilter[]);\n      expect(result).toEqual(\" key = \\\\'val\\\\' AND keyNum = \\\\'123\\\\' \");\n    });\n\n    it('returns empty string with no filters', () => {\n      const ahm = new AdHocFilter();\n      const result = ahm.buildFilterString([]);\n      expect(result).toEqual('');\n    });\n\n    it('builds filter string with regex operators', () => {\n      const ahm = new AdHocFilter();\n      const result = ahm.buildFilterString([{ key: 'key', operator: '=~', value: 'val' }] as AdHocVariableFilter[]);\n      expect(result).toEqual(\" key REGEXP \\\\'val\\\\' \");\n    });\n\n    it('builds filter string with negated regex operator', () => {\n      const ahm = new AdHocFilter();\n      const result = ahm.buildFilterString([{ key: 'key', operator: '!~', value: 'val' }] as AdHocVariableFilter[]);\n      expect(result).toEqual(\" key NOT REGEXP \\\\'val\\\\' \");\n    });\n\n    it('builds filter string with IN operator', () => {\n      const ahm = new AdHocFilter();\n      const result = ahm.buildFilterString([\n        { key: 'key', operator: 'IN', value: \"'val1', 'val2'\" },\n      ] as AdHocVariableFilter[]);\n      expect(result).toEqual(\" key IN (\\\\'val1\\\\', \\\\'val2\\\\') \");\n    });\n\n    it('ignores invalid filters', () => {\n      const ahm = new AdHocFilter();\n      const result = ahm.buildFilterString([\n        { key: 'key', operator: '=', value: 'val' },\n        { key: '', operator: '=', value: 'val' } as any,\n        { key: 'key2', operator: '=', value: 'val2' },\n      ] as AdHocVariableFilter[]);\n      expect(result).toEqual(\" key = \\\\'val\\\\' AND key2 = \\\\'val2\\\\' \");\n    });\n  });\n  it('should apply ad hoc filter with . in column name', () => {\n    const ahm = new AdHocFilter();\n    const val = ahm.apply('SELECT stuff FROM foo', [\n      { key: 'TABLE.key.key2', operator: '=', value: 'val' },\n    ] as AdHocVariableFilter[]);\n    expect(val).toEqual(`SELECT stuff FROM foo settings additional_table_filters={'foo' : ' key.key2 = \\\\'val\\\\' '}`);\n  });\n});\n"
  },
  {
    "path": "src/data/adHocFilter.ts",
    "content": "import { AdHocVariableFilter } from '@grafana/data';\nimport { getTable } from './ast';\n\nexport class AdHocFilter {\n  private _targetTable = '';\n\n  setTargetTableFromQuery(query: string) {\n    this._targetTable = getTable(query);\n    if (this._targetTable === '') {\n      throw new Error('Failed to get table from adhoc query.');\n    }\n  }\n\n  buildFilterString(adHocFilters: AdHocVariableFilter[], useJSON = false): string {\n    if (!adHocFilters || adHocFilters.length === 0) {\n      return '';\n    }\n\n    const validFilters = adHocFilters.filter((filter: AdHocVariableFilter) => {\n      const valid = isValid(filter);\n      if (!valid) {\n        console.warn('Invalid adhoc filter will be ignored:', filter);\n      }\n      return valid;\n    });\n\n    const filters = validFilters\n      .map((f, i) => {\n        const key = escapeKey(f.key, useJSON);\n        const value = escapeValueBasedOnOperator(f.value, f.operator);\n        const condition = i !== validFilters.length - 1 ? (f.condition ? f.condition : 'AND') : '';\n        const operator = convertOperatorToClickHouseOperator(f.operator);\n        return ` ${key} ${operator} ${value} ${condition}`;\n      })\n      .join('');\n\n    return filters;\n  }\n\n  apply(sql: string, adHocFilters: AdHocVariableFilter[], useJSON = false): string {\n    if (sql === '' || !adHocFilters || adHocFilters.length === 0) {\n      return sql;\n    }\n\n    // sql can contain a query with double quotes around the database and table name, e.g. \"default\".\"table\", so we remove those\n    if (this._targetTable !== '' && !sql.replace(/\"/g, '').match(new RegExp(`.*\\\\b${this._targetTable}\\\\b.*`, 'gi'))) {\n      return sql;\n    }\n\n    if (this._targetTable === '') {\n      this._targetTable = getTable(sql);\n    }\n\n    if (this._targetTable === '') {\n      return sql;\n    }\n\n    const filters = this.buildFilterString(adHocFilters, useJSON);\n\n    if (filters === '') {\n      return sql;\n    }\n    // Semicolons are not required and cause problems when building the SQL\n    sql = sql.replace(';', '');\n    return `${sql} settings additional_table_filters={'${this._targetTable}' : '${filters}'}`;\n  }\n}\n\nfunction isValid(filter: AdHocVariableFilter): boolean {\n  return filter.key !== undefined && filter.key !== '' && filter.operator !== undefined && filter.value !== undefined;\n}\n\nfunction escapeKey(s: string, isJSON = false): string {\n  if (['ResourceAttributes', 'ScopeAttributes', 'LogAttributes'].includes(s.split('.')[0])) {\n    if (isJSON) {\n      return s;\n    }\n\n    // Map syntax\n    const parts = s.split('.');\n    const prefix = parts.shift();\n\n    return `${prefix}[\\\\'${parts.join('.')}\\\\']`;\n  }\n\n  // Convert arrayElement syntax to bracket notation\n  if (s.startsWith('arrayElement(') && s.endsWith(')')) {\n    const match = s.match(/arrayElement\\((.*?),\\s*['\"](.*?)['\"]\\)/);\n    if (match) {\n      const [_, array, key] = match;\n      return `${array}[\\\\'${key}\\\\']`;\n    }\n  }\n  return s.includes('.') ? s.split('.').slice(1).join('.') : s;\n}\n\nfunction escapeValueBasedOnOperator(s: string, operator: string): string {\n  if (operator === 'IN') {\n    // Allow list of values without parentheses\n    if (s.length > 2 && s[0] !== '(' && s[s.length - 1] !== ')') {\n      s = `(${s})`;\n    }\n    return s.replace(/'/g, \"\\\\'\");\n  } else {\n    return `\\\\'${s}\\\\'`;\n  }\n}\n\nfunction convertOperatorToClickHouseOperator(operator: string): string {\n  // Grafana's \"Matches regex\" (=~) and \"Does not match regex\" (!~) are regex\n  // operators, so map them to ClickHouse's REGEXP. Using ILIKE here produced\n  // semantically wrong filters and prevented index usage for indexed columns\n  // (see grafana/clickhouse-datasource#1443).\n  if (operator === '=~') {\n    return 'REGEXP';\n  }\n  if (operator === '!~') {\n    return 'NOT REGEXP';\n  }\n  return operator;\n}\n"
  },
  {
    "path": "src/data/ast.test.ts",
    "content": "import { getFields, sqlToStatement } from './ast';\nimport { toSql } from 'pgsql-ast-parser';\n\ndescribe('ast', () => {\n  describe('getFields', () => {\n    it('return 1 expression if statement does not have an alias', () => {\n      const stm = getFields(`select foo from bar`);\n      expect(stm.length).toBe(1);\n    });\n  });\n  describe('sqlToStatement', () => {\n    it('settings parse correctly', () => {\n      const sql = 'SELECT count(*) FROM foo SETTINGS setting1=stuff setting2=stuff';\n      const stm = sqlToStatement(sql);\n      // this is formatted like this to match how pgsql generates its sql\n      expect(toSql.statement(stm)).toEqual('SELECT (count (*) )  FROM foo');\n    });\n\n    // https://github.com/grafana/clickhouse-datasource/issues/714\n    it('does not error when brackets/macros/variables are present', () => {\n      const errLog = jest.spyOn(console, 'error');\n      const sql = `\n        /* \\${__variable} \\${__variable.key} */\n        SELECT\n          *,\n          \\$__timeInterval(timestamp),\n          '{\"a\": 1, \"b\": { \"c\": 2, \"d\": [1, 2, 3] }}'::json as bracketTest\n        FROM default.table\n        WHERE $__timeFilter(timestamp)\n        AND col != \\${variable}\n        AND col != \\${variable.key}\n        AND col != \\${variable.key:singlequote}\n        AND col != '\\${variable}'\n        AND col != '\\${__variable}'\n        AND col != ('\\${__variable.key}')\n        AND col != \\${variable:singlequote}\n      `;\n\n      const stm = sqlToStatement(sql);\n      const astSql = toSql.statement(stm);\n      expect(errLog).toHaveBeenCalledTimes(0);\n      expect(stm).not.toEqual({});\n      expect(astSql).not.toBeFalsy();\n    });\n  });\n});\n"
  },
  {
    "path": "src/data/ast.ts",
    "content": "import { parseFirst, Statement, SelectFromStatement, astMapper, toSql, ExprRef } from 'pgsql-ast-parser';\n\ninterface ReplacePart {\n  startIndex: number;\n  name: string;\n  replacementName: string;\n}\ntype ReplaceParts = ReplacePart[];\n\nfunction getReplacementKey(isVariable: boolean) {\n  const prefix = isVariable ? 'v' : 'f';\n  return prefix + (Math.random() + 1).toString(36).substring(7);\n}\n\n/**\n * Replaces macro functions and keywords such as $__timeFilter() and \"default\"\n */\nfunction replaceMacroFunctions(sql: string): [ReplaceParts, string] {\n  const replaceFuncs: ReplaceParts = [];\n  // default is a keyword in this grammar, but it can be used in CH\n  const keywordRegex = /(\\$__|\\$|default|settings)/gi;\n  let regExpArray: RegExpExecArray | null;\n  while ((regExpArray = keywordRegex.exec(sql)) !== null) {\n    replaceFuncs.push({ startIndex: regExpArray.index, name: regExpArray[0], replacementName: '' });\n  }\n\n  // need to process in reverse so starting positions aren't affected by replacing other things\n  for (let i = replaceFuncs.length - 1; i >= 0; i--) {\n    const si = replaceFuncs[i].startIndex;\n    const replacementName = getReplacementKey(false);\n    replaceFuncs[i].replacementName = replacementName;\n    // settings do not parse and we do not need information from them so we will remove them\n    if (replaceFuncs[i].name.toLowerCase() === 'settings') {\n      sql = sql.substring(0, si);\n      continue;\n    }\n    sql = sql.substring(0, si) + replacementName + sql.substring(si + replaceFuncs[i].name.length);\n  }\n\n  return [replaceFuncs, sql];\n}\n\n/**\n * Replaces Grafana variables such as ${var} ${var.key} ${var.key:singlequote}\n * https://grafana.com/docs/grafana/latest/dashboards/variables\n */\nfunction replaceMacroVariables(sql: string): [ReplaceParts, string] {\n  const replaceVariables: ReplaceParts = [];\n  const variableRegex = /\\${[a-zA-Z0-9_:.\\w]+}/g;\n\n  let regExpArray: RegExpExecArray | null;\n  while ((regExpArray = variableRegex.exec(sql)) !== null) {\n    replaceVariables.push({ startIndex: regExpArray.index, name: regExpArray[0], replacementName: '' });\n  }\n\n  // need to process in reverse so starting positions aren't affected by replacing other things\n  for (let i = replaceVariables.length - 1; i >= 0; i--) {\n    const si = replaceVariables[i].startIndex;\n    const replacementName = getReplacementKey(true);\n    replaceVariables[i].replacementName = replacementName;\n    sql = sql.substring(0, si) + replacementName + sql.substring(si + replaceVariables[i].name.length);\n  }\n\n  return [replaceVariables, sql];\n}\n\n// TODO: support query parameters: https://clickhouse.com/docs/en/interfaces/cli#cli-queries-with-parameters\n\nexport function sqlToStatement(rawSql: string): Statement {\n  const [replaceVars, variableSql] = replaceMacroVariables(rawSql);\n  const [replaceFuncs, sql] = replaceMacroFunctions(variableSql);\n  const replaceParts = replaceVars.concat(replaceFuncs);\n\n  let ast: Statement;\n  try {\n    ast = parseFirst(sql);\n  } catch (err) {\n    console.error(`Failed to parse SQL statement into an AST: ${err}`);\n    return {} as Statement;\n  }\n\n  const mapper = astMapper((map) => ({\n    tableRef: (t) => {\n      const rfs = replaceParts.find((x) => x.replacementName === t.schema);\n      if (rfs) {\n        return { ...t, schema: t.schema?.replace(rfs.replacementName, rfs.name) };\n      }\n      const rft = replaceParts.find((x) => x.replacementName === t.name);\n      if (rft) {\n        return { ...t, name: t.name.replace(rft.replacementName, rft.name) };\n      }\n      return map.super().tableRef(t);\n    },\n    ref: (r) => {\n      const rf = replaceParts.find((x) => r.name.startsWith(x.replacementName));\n      if (rf) {\n        const d = r.name.replace(rf.replacementName, rf.name);\n        return { ...r, name: d };\n      }\n      return map.super().ref(r);\n    },\n    expr: (e) => {\n      if (!e || e.type !== 'string') {\n        return map.super().expr(e);\n      }\n\n      const rf = replaceParts.find((x) => e.value.startsWith(x.replacementName));\n      if (rf) {\n        const d = e.value.replace(rf.replacementName, rf.name);\n        return { ...e, value: d };\n      }\n\n      return map.super().expr(e);\n    },\n    call: (c) => {\n      const rf = replaceParts.find((x) => c.function.name.startsWith(x.replacementName));\n      if (rf) {\n        return { ...c, function: { ...c.function, name: c.function.name.replace(rf.replacementName, rf.name) } };\n      }\n      return map.super().call(c);\n    },\n  }));\n  return mapper.statement(ast)!;\n}\n\nexport function getTable(sql: string): string {\n  const stm = sqlToStatement(sql);\n  if (stm.type !== 'select' || !stm.from?.length || stm.from?.length <= 0) {\n    return '';\n  }\n  switch (stm.from![0].type) {\n    case 'table': {\n      const table = stm.from![0];\n      const tableName = `${table.name.schema ? `${table.name.schema}.` : ''}${table.name.name}`;\n      // clickhouse table names are case-sensitive and pgsql parser removes casing,\n      // so we need to get the casing from the raw sql\n      const s = new RegExp(`\\\\b${tableName}\\\\b`, 'gi').exec(sql);\n      return s ? s[0] : tableName;\n    }\n    case 'statement': {\n      const table = stm.from![0];\n      return getTable(toSql.statement(table.statement));\n    }\n  }\n  return '';\n}\n\nexport function getFields(sql: string): string[] {\n  const stm = sqlToStatement(sql) as SelectFromStatement;\n  if (stm.type !== 'select' || !stm.columns?.length || stm.columns?.length <= 0) {\n    return [];\n  }\n\n  return stm.columns.map((x) => {\n    const exprName = (x.expr as ExprRef).name;\n\n    if (x.alias !== undefined) {\n      return `${exprName} as ${x.alias?.name}`;\n    } else {\n      return `${exprName}`;\n    }\n  });\n}\n"
  },
  {
    "path": "src/data/columnFilters.test.ts",
    "content": "import { SelectedColumn } from 'types/queryBuilder';\nimport { columnFilterDateTime, columnFilterOr, columnFilterString } from './columnFilters';\n\ndescribe('columnFilterDateTime', () => {\n  it.each<{ col: SelectedColumn; expected: boolean }>([\n    { col: { name: 't', type: 'Date' }, expected: true },\n    { col: { name: 't', type: 'DateTime' }, expected: true },\n    { col: { name: 't', type: 'Nullable(DateTime)' }, expected: true },\n    { col: { name: 't', type: 'DateTime64' }, expected: true },\n    { col: { name: 't', type: 'DateTime64(9)' }, expected: true },\n    { col: { name: 't', type: 'date' }, expected: true },\n    { col: { name: 't', type: 'datEtIME' }, expected: true },\n\n    { col: { name: 't', type: 'String' }, expected: false },\n    { col: { name: 't', type: 'Int64' }, expected: false },\n    { col: { name: 't', type: 'Dat' }, expected: false },\n    { col: { name: 't', type: 'DaTme' }, expected: false },\n    { col: { name: 't', type: 'nullaBLE(DaTme)' }, expected: false },\n  ])('returns $expected for case $# (\"$col.type\")', ({ col, expected }) => {\n    expect(columnFilterDateTime(col)).toBe(expected);\n  });\n});\n\ndescribe('columnFilterString', () => {\n  it.each<{ col: SelectedColumn; expected: boolean }>([\n    { col: { name: 't', type: 'String' }, expected: true },\n    { col: { name: 't', type: 'LowCardinality(String)' }, expected: true },\n    { col: { name: 't', type: 'LowCardinality(Nullable(String))' }, expected: true },\n    { col: { name: 't', type: 'newFeature(nullable(string))' }, expected: true },\n    { col: { name: 't', type: 'string' }, expected: true },\n\n    { col: { name: 't', type: 'Int64' }, expected: false },\n    { col: { name: 't', type: 'str' }, expected: false },\n    { col: { name: 't', type: 'Date' }, expected: false },\n    { col: { name: 't', type: 'DateTime' }, expected: false },\n  ])('returns $expected for case $# (\"$col.type\")', ({ col, expected }) => {\n    expect(columnFilterString(col)).toBe(expected);\n  });\n});\n\ndescribe('columnFilterOr', () => {\n  it('matches no filters using logical OR operator', () => {\n    const col: SelectedColumn = { name: 't', type: 'invalid' };\n    expect(columnFilterOr(col, columnFilterString, columnFilterDateTime)).toBe(false);\n  });\n\n  it('compares multiple filters using logical OR operator, matching first', () => {\n    const col: SelectedColumn = { name: 't', type: 'String' };\n    expect(columnFilterOr(col, columnFilterString, columnFilterDateTime)).toBe(true);\n  });\n\n  it('compares multiple filters using logical OR operator, matching last', () => {\n    const col: SelectedColumn = { name: 't', type: 'String' };\n    expect(columnFilterOr(col, columnFilterDateTime, columnFilterString)).toBe(true);\n  });\n});\n"
  },
  {
    "path": "src/data/columnFilters.ts",
    "content": "import { SelectedColumn } from 'types/queryBuilder';\n\nexport const columnFilterDateTime = (s: SelectedColumn): boolean => (s.type || '').toLowerCase().includes('date');\nexport const columnFilterString = (s: SelectedColumn): boolean =>\n  (s.type || '').toLowerCase().includes('string') || (s.type || '').toLowerCase().includes('enum');\nexport const columnFilterOr = (\n  s: SelectedColumn,\n  ...filterFuncs: ReadonlyArray<(s: SelectedColumn) => boolean>\n): boolean => {\n  for (let filterFn of filterFuncs) {\n    if (filterFn(s)) {\n      return true;\n    }\n  }\n\n  return false;\n};\n"
  },
  {
    "path": "src/data/logs.test.ts",
    "content": "import { FieldType } from '@grafana/data';\nimport { getIntervalInfo, getTimeFieldRoundingClause, LOG_LEVEL_TO_IN_CLAUSE, splitLogsVolumeFrames } from './logs';\n\ndescribe('logs', () => {\n\n  describe('getIntervalInfo', () => {\n    it('should return the default value when no interval info is provided', async () => {\n      expect(getIntervalInfo({})).toEqual({ interval: '$__interval' });\n    });\n    it('should do buckets per day when the provided interval greater than an hour', async () => {\n      expect(\n        getIntervalInfo({\n          __interval_ms: {\n            text: '',\n            value: 60 * 60 * 1000 + 1,\n          },\n        })\n      ).toEqual({ interval: '1d', intervalMs: 24 * 60 * 60 * 1000 });\n    });\n    it('should do buckets per hour when the provided interval greater than a minute', async () => {\n      expect(\n        getIntervalInfo({\n          __interval_ms: {\n            text: '',\n            value: 60 * 1000 + 1,\n          },\n        })\n      ).toEqual({ interval: '1h', intervalMs: 60 * 60 * 1000 });\n    });\n    it('should do buckets per minute when the provided interval greater than a second', async () => {\n      expect(\n        getIntervalInfo({\n          __interval_ms: {\n            text: '',\n            value: 1001,\n          },\n        })\n      ).toEqual({ interval: '1m', intervalMs: 60 * 1000 });\n    });\n    it('should do buckets per second', async () => {\n      expect(\n        getIntervalInfo({\n          __interval_ms: {\n            text: '',\n            value: 1,\n          },\n        })\n      ).toEqual({ interval: '1s', intervalMs: 1000 });\n    });\n  });\n\n  describe('getTimeFieldRoundingClause', () => {\n    it('should fall back to DAY grouping when no interval info is provided', async () => {\n      expect(getTimeFieldRoundingClause({}, 'created_at')).toEqual('toStartOfInterval(\"created_at\", INTERVAL 1 DAY)');\n    });\n    it('should do buckets per day when the provided interval greater than an hour', async () => {\n      expect(\n        getTimeFieldRoundingClause(\n          {\n            __interval_ms: {\n              text: '',\n              value: 60 * 60 * 1000 + 1,\n            },\n          },\n          'created_at'\n        )\n      ).toEqual('toStartOfInterval(\"created_at\", INTERVAL 1 DAY)');\n    });\n    it('should do buckets per hour when the provided interval greater than a minute', async () => {\n      expect(\n        getTimeFieldRoundingClause(\n          {\n            __interval_ms: {\n              text: '',\n              value: 60 * 1000 + 1,\n            },\n          },\n          'created_at'\n        )\n      ).toEqual('toStartOfInterval(\"created_at\", INTERVAL 1 HOUR)');\n    });\n    it('should do buckets per minute when the provided interval greater than a second', async () => {\n      expect(\n        getTimeFieldRoundingClause(\n          {\n            __interval_ms: {\n              text: '',\n              value: 1001,\n            },\n          },\n          'created_at'\n        )\n      ).toEqual('toStartOfInterval(\"created_at\", INTERVAL 1 MINUTE)');\n    });\n    it('should do buckets per second', async () => {\n      expect(\n        getTimeFieldRoundingClause(\n          {\n            __interval_ms: {\n              text: '',\n              value: 1,\n            },\n          },\n          'created_at'\n        )\n      ).toEqual('toStartOfInterval(\"created_at\", INTERVAL 1 SECOND)');\n    });\n  });\n\n  describe('splitLogsVolumeFrames', () => {\n    const prefix = 'log-volume-';\n    const times = [1000, 2000, 3000];\n    const makeFrame = (refId: string, fields: Array<{ name: string; values: number[] }>) => ({\n      refId,\n      length: times.length,\n      fields: fields.map(({ name, values }) => ({ name, type: FieldType.number, values, config: {} })),\n    });\n\n    it('passes through frames that do not match the prefix', () => {\n      const frame = makeFrame('other-query', [{ name: 'time', values: times }, { name: 'logs', values: [1, 2, 3] }]);\n      expect(splitLogsVolumeFrames([frame], prefix)).toEqual([frame]);\n    });\n\n    it('passes through a matching frame with no time field', () => {\n      const frame = makeFrame(`${prefix}1`, [{ name: 'logs', values: [1, 2, 3] }]);\n      expect(splitLogsVolumeFrames([frame], prefix)).toEqual([frame]);\n    });\n\n    it('passes through a matching frame with no level fields', () => {\n      const frame = makeFrame(`${prefix}1`, [{ name: 'time', values: times }]);\n      expect(splitLogsVolumeFrames([frame], prefix)).toEqual([frame]);\n    });\n\n    it('splits a single-level frame and labels it \"logs\"', () => {\n      const frame = {\n        refId: `${prefix}1`,\n        length: times.length,\n        fields: [\n          { name: 'time', type: FieldType.number, values: times, config: {} },\n          { name: 'logs', type: FieldType.number, values: [1, 2, 3], config: {} },\n        ],\n      };\n      const result = splitLogsVolumeFrames([frame], prefix);\n      expect(result).toHaveLength(1);\n      expect(result[0]).toEqual({\n        refId: `${prefix}1`,\n        length: times.length,\n        fields: [\n          { name: 'Time', type: FieldType.time, values: times, config: {} },\n          { name: 'Value', type: FieldType.number, values: [1, 2, 3], labels: { level: 'logs' }, config: {} },\n        ],\n      });\n    });\n\n    it('splits a multi-level frame into one frame per level using field names as labels', () => {\n      const frame = {\n        refId: `${prefix}1`,\n        length: times.length,\n        fields: [\n          { name: 'time', type: FieldType.number, values: times, config: {} },\n          { name: 'error', type: FieldType.number, values: [1, 2, 3], config: {} },\n          { name: 'info', type: FieldType.number, values: [4, 5, 6], config: {} },\n        ],\n      };\n      const result = splitLogsVolumeFrames([frame], prefix);\n      expect(result).toHaveLength(2);\n      expect(result[0].fields[1].labels).toEqual({ level: 'error' });\n      expect(result[1].fields[1].labels).toEqual({ level: 'info' });\n    });\n\n    it('preserves non-volume frames alongside split volume frames', () => {\n      const nonVolume = makeFrame('other', [{ name: 'time', values: times }, { name: 'val', values: [7, 8, 9] }]);\n      const volume = {\n        refId: `${prefix}1`,\n        length: times.length,\n        fields: [\n          { name: 'time', type: FieldType.number, values: times, config: {} },\n          { name: 'logs', type: FieldType.number, values: [1, 2, 3], config: {} },\n        ],\n      };\n      const result = splitLogsVolumeFrames([nonVolume, volume], prefix);\n      expect(result).toHaveLength(2);\n      expect(result[0]).toEqual(nonVolume);\n      expect(result[1].refId).toBe(`${prefix}1`);\n    });\n  });\n\n  describe('LOG_LEVEL_TO_IN_CLAUSE', () => {\n    it('should generate correct IN clauses', async () => {\n      expect(LOG_LEVEL_TO_IN_CLAUSE).toEqual({\n        critical:\n          \"'critical','fatal','crit','alert','emerg','CRITICAL','FATAL','CRIT','ALERT','EMERG','Critical','Fatal','Crit','Alert','Emerg'\",\n        debug: \"'debug','dbug','DEBUG','DBUG','Debug','Dbug'\",\n        error: \"'error','err','eror','ERROR','ERR','EROR','Error','Err','Eror'\",\n        info: \"'info','information','informational','INFO','INFORMATION','INFORMATIONAL','Info','Information','Informational'\",\n        trace: \"'trace','TRACE','Trace'\",\n        unknown: \"'unknown','UNKNOWN','Unknown'\",\n        warn: \"'warn','warning','WARN','WARNING','Warn','Warning'\",\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "src/data/logs.ts",
    "content": "import { DataFrame, FieldType, ScopedVars } from '@grafana/data';\nimport { partition } from 'lodash';\n\nconst MILLISECOND = 1;\nconst SECOND = 1000 * MILLISECOND;\nconst MINUTE = 60 * SECOND;\nconst HOUR = 60 * MINUTE;\nconst DAY = 24 * HOUR;\n\nexport function getIntervalInfo(scopedVars: ScopedVars): { interval: string; intervalMs?: number } {\n  if (scopedVars.__interval_ms) {\n    let intervalMs: number = scopedVars.__interval_ms.value;\n    let interval;\n    if (intervalMs > HOUR) {\n      intervalMs = DAY;\n      interval = '1d';\n    } else if (intervalMs > MINUTE) {\n      intervalMs = HOUR;\n      interval = '1h';\n    } else if (intervalMs > SECOND) {\n      intervalMs = MINUTE;\n      interval = '1m';\n    } else {\n      intervalMs = SECOND;\n      interval = '1s';\n    }\n\n    return { interval, intervalMs };\n  } else {\n    return { interval: '$__interval' };\n  }\n}\n\nexport function getTimeFieldRoundingClause(scopedVars: ScopedVars, timeField: string): string {\n  // NB: slight discrepancy with getIntervalInfo here\n  // it returns { interval: '$__interval' } when the interval from the ScopedVars is undefined,\n  // but we fall back to DAY here\n  let interval = 'DAY';\n  if (scopedVars.__interval_ms) {\n    let intervalMs: number = scopedVars.__interval_ms.value;\n    if (intervalMs > HOUR) {\n      interval = 'DAY';\n    } else if (intervalMs > MINUTE) {\n      interval = 'HOUR';\n    } else if (intervalMs > SECOND) {\n      interval = 'MINUTE';\n    } else {\n      interval = 'SECOND';\n    }\n  }\n  return `toStartOfInterval(\"${timeField}\", INTERVAL 1 ${interval})`;\n}\n\nexport const TIME_FIELD_ALIAS = 'time';\nexport const DEFAULT_LOGS_ALIAS = 'logs';\n\n/**\n * Mapping of canonical log levels to corresponding IN clauses\n * with all possible lower, upper and capital case values for this level\n *\n * For example: trace -> IN ('trace', 'TRACE', 'Trace')\n *\n * @see {LogLevel} for reference values\n */\ntype LogLevelToInClause = Record<'critical' | 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'unknown', string>;\nexport const LOG_LEVEL_TO_IN_CLAUSE: LogLevelToInClause = (() => {\n  const levels = {\n    critical: ['critical', 'fatal', 'crit', 'alert', 'emerg'],\n    error: ['error', 'err', 'eror'],\n    warn: ['warn', 'warning'],\n    info: ['info', 'information', 'informational'],\n    debug: ['debug', 'dbug'],\n    trace: ['trace'],\n    unknown: ['unknown'],\n  };\n  return (Object.keys(levels) as Array<keyof typeof levels>).reduce((allLevels, level) => {\n    allLevels[level] = `${[\n      ...levels[level].map((l) => `'${l}'`),\n      ...levels[level].map((l) => `'${l.toUpperCase()}'`),\n      ...levels[level].map((l) => `'${l.charAt(0).toUpperCase() + l.slice(1)}'`),\n    ].join(',')}`;\n    return allLevels;\n  }, {} as LogLevelToInClause);\n})();\n\nexport function splitLogsVolumeFrames(data: DataFrame[], logVolumePrefix: string): DataFrame[] {\n  const result: DataFrame[] = [];\n\n  for (const frame of data) {\n    if (!frame.refId?.startsWith(logVolumePrefix)) {\n      result.push(frame);\n      continue;\n    }\n\n    const [timeFields, levelFields] = partition(frame.fields, (f) => f.name === TIME_FIELD_ALIAS);\n    const timeField = timeFields[0];\n    if (!timeField || levelFields.length === 0) {\n      result.push(frame);\n      continue;\n    }\n\n    const oneLevelDetected = levelFields.length === 1 && levelFields[0].name === DEFAULT_LOGS_ALIAS;\n    for (const levelField of levelFields) {\n      const levelName = oneLevelDetected ? 'logs' : levelField.name;\n      result.push({\n        refId: frame.refId,\n        length: timeField.values.length,\n        fields: [\n          { name: 'Time', type: FieldType.time, values: timeField.values, config: {} },\n          { name: 'Value', type: FieldType.number, values: levelField.values, labels: { level: levelName }, config: {} },\n        ],\n      });\n    }\n  }\n  return result;\n}\n\nexport const allLogLevels = [\n  'critical',\n  'fatal',\n  'crit',\n  'alert',\n  'emerg',\n  'error',\n  'err',\n  'eror',\n  'warn',\n  'warning',\n  'info',\n  'information',\n  'informational',\n  'debug',\n  'dbug',\n  'trace',\n  'unknown',\n];\n"
  },
  {
    "path": "src/data/migration.test.ts",
    "content": "import { CHBuilderQuery, CHQuery, CHSqlQuery, EditorType } from 'types/sql';\nimport { migrateCHQuery } from './migration';\nimport { pluginVersion } from 'utils/version';\nimport {\n  AggregateType,\n  BuilderMode,\n  ColumnHint,\n  Filter,\n  FilterOperator,\n  OrderByDirection,\n  QueryBuilderOptions,\n  QueryType,\n} from 'types/queryBuilder';\nimport { mapQueryTypeToGrafanaFormat } from './utils';\n\ndescribe('Query Editor Version Migration', () => {\n  it('does not apply migration for empty query', () => {\n    const query = {} as CHQuery;\n\n    const migratedQuery = migrateCHQuery(query);\n    expect(migratedQuery).not.toBeUndefined();\n    expect(migratedQuery).toEqual(query);\n  });\n\n  it('does not apply migration for default grafana query', () => {\n    const defaultGrafanaQuery = {\n      datasource: 'test-ds',\n      refId: 'A',\n    } as unknown as CHQuery;\n\n    const migratedQuery = migrateCHQuery(defaultGrafanaQuery);\n    expect(migratedQuery).not.toBeUndefined();\n    expect(migratedQuery).toEqual(defaultGrafanaQuery);\n  });\n\n  it('does not apply migration to latest query schema', () => {\n    const latestQuery: CHBuilderQuery = {\n      pluginVersion,\n      editorType: EditorType.Builder,\n      builderOptions: {\n        database: 'default',\n        table: 'test',\n        queryType: QueryType.Table,\n        mode: BuilderMode.List,\n        columns: [\n          { name: 'a', type: 'String' },\n          { name: 'b', type: 'String' },\n        ],\n        aggregates: [{ aggregateType: AggregateType.Count, column: '*', alias: 'c' }],\n        filters: [\n          {\n            type: 'String',\n            operator: FilterOperator.Equals,\n            filterType: 'custom',\n            key: 'b',\n            condition: 'AND',\n            value: 'test',\n          },\n        ],\n        groupBy: ['a'],\n        orderBy: [{ name: 'a', dir: OrderByDirection.ASC }],\n        limit: 250,\n        meta: {\n          otelEnabled: false,\n          otelVersion: 'test',\n        },\n      },\n      rawSql: 'sql',\n      refId: 'A',\n    };\n\n    const migratedQuery = migrateCHQuery(latestQuery);\n    expect(migratedQuery).toBe(latestQuery);\n    expect(migratedQuery).toEqual(latestQuery);\n  });\n\n  it('apply migration for v3 builder query', () => {\n    const v3Query = {\n      refId: 'A',\n      datasource: {\n        type: 'ch-ds',\n        uid: 'test-uid',\n      },\n      key: 'test-key',\n      queryType: 'builder',\n      rawSql: 'SELECT 1',\n      builderOptions: {\n        mode: 'list',\n        fields: ['created_at', 'level', 'event'],\n        limit: 50,\n        database: 'default',\n        table: 'logs',\n        filters: [\n          {\n            operator: 'WITH IN DASHBOARD TIME RANGE',\n            filterType: 'custom',\n            key: 'created_at',\n            type: 'datetime',\n            condition: 'AND',\n            restrictToFields: [\n              {\n                name: 'created_at',\n                type: 'DateTime',\n                label: 'created_at',\n                picklistValues: [],\n              },\n            ],\n          },\n          {\n            filterType: 'custom',\n            key: 'event',\n            type: 'String',\n            condition: 'AND',\n            operator: 'IS NOT NULL',\n          },\n        ],\n        metrics: [{ field: 'level', aggregation: 'count', alias: 'c' }],\n        groupBy: ['c'],\n        orderBy: [{ name: 'created_at', dir: 'DESC' }],\n      },\n      format: 1,\n      selectedFormat: 1,\n      meta: {\n        timezone: 'tz',\n      },\n    } as unknown as CHQuery;\n\n    const latestQuery: CHBuilderQuery = {\n      pluginVersion,\n      editorType: EditorType.Builder,\n      refId: 'A',\n      datasource: {\n        type: 'ch-ds',\n        uid: 'test-uid',\n      },\n      key: 'test-key',\n      builderOptions: {\n        database: 'default',\n        table: 'logs',\n        queryType: QueryType.Table,\n        mode: BuilderMode.List,\n        columns: [{ name: 'created_at' }, { name: 'level' }, { name: 'event' }],\n        filters: [\n          {\n            operator: FilterOperator.WithInGrafanaTimeRange,\n            filterType: 'custom',\n            key: 'created_at',\n            type: 'datetime',\n            condition: 'AND',\n            restrictToFields: [\n              {\n                name: 'created_at',\n                type: 'DateTime',\n                label: 'created_at',\n                picklistValues: [],\n              },\n            ],\n          } as Filter,\n          {\n            filterType: 'custom',\n            key: 'event',\n            type: 'String',\n            condition: 'AND',\n            operator: FilterOperator.IsNotNull,\n          },\n        ],\n        aggregates: [{ aggregateType: AggregateType.Count, column: 'level', alias: 'c' }],\n        groupBy: ['c'],\n        orderBy: [{ name: 'created_at', dir: OrderByDirection.DESC }],\n        limit: 50,\n      },\n      rawSql: 'SELECT 1',\n      format: mapQueryTypeToGrafanaFormat(QueryType.Table),\n      meta: {\n        timezone: 'tz',\n      },\n    };\n\n    const migratedQuery = migrateCHQuery(v3Query);\n    expect(migratedQuery).toEqual(latestQuery);\n  });\n\n  it('apply migration for v3 sql query', () => {\n    const v3Query = {\n      refId: 'A',\n      datasource: {\n        type: 'ch-ds',\n        uid: 'test-uid',\n      },\n      key: 'test-key',\n      queryType: 'sql',\n      rawSql: 'SELECT 1',\n      meta: {\n        timezone: 'tz',\n        builderOptions: {\n          fields: ['created_at', 'level', 'event'],\n        },\n      },\n      format: 1,\n      selectedFormat: 1,\n      expand: true,\n    } as unknown as CHQuery;\n\n    const latestQuery: CHSqlQuery = {\n      pluginVersion,\n      editorType: EditorType.SQL,\n      refId: 'A',\n      datasource: {\n        type: 'ch-ds',\n        uid: 'test-uid',\n      },\n      key: 'test-key',\n      rawSql: 'SELECT 1',\n      queryType: QueryType.Table,\n      format: mapQueryTypeToGrafanaFormat(QueryType.Table),\n      expand: true,\n      meta: {\n        timezone: 'tz',\n        builderOptions: {\n          database: '',\n          table: '',\n          queryType: QueryType.Table,\n          columns: [{ name: 'created_at' }, { name: 'level' }, { name: 'event' }],\n        } as QueryBuilderOptions,\n      },\n    };\n\n    const migratedQuery = migrateCHQuery(v3Query);\n    expect(migratedQuery).toEqual(latestQuery);\n  });\n\n  it('apply migration for partial v3 query', () => {\n    const v3Query = {\n      queryType: 'builder',\n      builderOptions: {\n        mode: 'list',\n      },\n      rawSql: '',\n    } as unknown as CHQuery;\n\n    const latestQuery: CHBuilderQuery = {\n      pluginVersion,\n      editorType: EditorType.Builder,\n      builderOptions: {\n        database: '',\n        table: '',\n        queryType: QueryType.Table,\n        mode: BuilderMode.List,\n        columns: [],\n      },\n      rawSql: '',\n      refId: '',\n    };\n\n    const migratedQuery = migrateCHQuery(v3Query);\n    expect(migratedQuery).toEqual(latestQuery);\n  });\n\n  it('v3 migration maps hinted columns', () => {\n    const v3Query = {\n      queryType: 'builder',\n      builderOptions: {\n        timeField: 'timestamp',\n        timeFieldType: 'DateTime',\n        logLevelField: 'level',\n      },\n      rawSql: '',\n    } as unknown as CHQuery;\n\n    const latestQuery: CHBuilderQuery = {\n      pluginVersion,\n      editorType: EditorType.Builder,\n      builderOptions: {\n        database: '',\n        table: '',\n        queryType: QueryType.TimeSeries, // TimeSeries because v3 timeField is present\n        columns: [\n          { name: 'timestamp', type: 'DateTime', hint: ColumnHint.Time },\n          { name: 'level', hint: ColumnHint.LogLevel },\n        ],\n      },\n      format: undefined,\n      rawSql: '',\n      refId: '',\n    };\n\n    const migratedQuery = migrateCHQuery(v3Query);\n    expect(migratedQuery).toEqual(latestQuery);\n  });\n\n  it('v3 migration detects QueryType.TimeSeries', () => {\n    const v3Query = {\n      queryType: 'builder',\n      builderOptions: {\n        timeField: 'timestamp',\n        timeFieldType: 'DateTime',\n      },\n      rawSql: '',\n    } as unknown as CHQuery;\n\n    const latestQuery: CHBuilderQuery = {\n      pluginVersion,\n      editorType: EditorType.Builder,\n      builderOptions: {\n        database: '',\n        table: '',\n        queryType: QueryType.TimeSeries,\n        columns: [{ name: 'timestamp', type: 'DateTime', hint: ColumnHint.Time }],\n      },\n      format: undefined,\n      rawSql: '',\n      refId: '',\n    };\n\n    const migratedQuery = migrateCHQuery(v3Query);\n    expect(migratedQuery).toEqual(latestQuery);\n  });\n\n  it('v3 migration detects QueryType.Logs', () => {\n    const v3Query = {\n      queryType: 'builder',\n      builderOptions: {\n        logLevelField: 'level',\n      },\n      rawSql: '',\n    } as unknown as CHQuery;\n\n    const latestQuery: CHBuilderQuery = {\n      pluginVersion,\n      editorType: EditorType.Builder,\n      builderOptions: {\n        database: '',\n        table: '',\n        queryType: QueryType.Logs,\n        columns: [{ name: 'level', hint: ColumnHint.LogLevel }],\n      },\n      format: undefined,\n      rawSql: '',\n      refId: '',\n    };\n\n    const migratedQuery = migrateCHQuery(v3Query);\n    expect(migratedQuery).toEqual(latestQuery);\n  });\n});\n"
  },
  {
    "path": "src/data/migration.ts",
    "content": "import { ColumnHint, Filter, QueryBuilderOptions, QueryType, SelectedColumn } from 'types/queryBuilder';\nimport { CHBuilderQuery, CHQuery, CHSqlQuery, EditorType } from 'types/sql';\nimport { isVersionGtOrEq, pluginVersion } from 'utils/version';\nimport { mapGrafanaFormatToQueryType } from './utils';\n\nexport type AnyCHQuery = Partial<CHQuery> & { [k: string]: any };\nexport type AnyQueryBuilderOptions = Partial<QueryBuilderOptions> & { [k: string]: any };\n\n/**\n * Takes a CHQuery and transforms it to the latest interface.\n */\nexport const migrateCHQuery = (savedQuery: CHQuery): CHQuery => {\n  const isGrafanaDefaultQuery = savedQuery.rawSql === undefined;\n  if (isGrafanaDefaultQuery) {\n    return savedQuery;\n  }\n\n  if (isV3CHQuery(savedQuery)) {\n    return migrateV3CHQuery(savedQuery);\n  }\n\n  return savedQuery;\n};\n\n/**\n * Takes v3 CHQuery and returns a version compatible with the latest editor.\n */\nconst migrateV3CHQuery = (savedQuery: AnyCHQuery): CHQuery => {\n  // Builder Query\n  if (savedQuery['queryType'] === 'builder') {\n    const builderQuery: CHBuilderQuery = {\n      ...savedQuery,\n      pluginVersion,\n      editorType: EditorType.Builder,\n      builderOptions: migrateV3QueryBuilderOptions(savedQuery['builderOptions'] || {}),\n      rawSql: savedQuery.rawSql || '',\n      refId: savedQuery.refId || '',\n      format: savedQuery.format,\n    };\n\n    if (savedQuery?.meta?.timezone) {\n      builderQuery.meta = {\n        timezone: savedQuery.meta.timezone,\n      };\n    }\n\n    // delete unwanted properties from v3\n    delete (builderQuery as any)['queryType'];\n    delete (builderQuery as any)['selectedFormat'];\n\n    return builderQuery;\n  }\n\n  // Raw SQL Query\n  const rawSqlQuery: CHSqlQuery = {\n    ...savedQuery,\n    pluginVersion,\n    editorType: EditorType.SQL,\n    rawSql: savedQuery.rawSql || '',\n    refId: savedQuery.refId || '',\n    format: savedQuery.format,\n    queryType: mapGrafanaFormatToQueryType(savedQuery.format),\n    meta: {},\n  };\n\n  if (savedQuery.expand) {\n    rawSqlQuery.expand = savedQuery.expand;\n  }\n\n  if (savedQuery.meta) {\n    const meta = savedQuery.meta as any;\n    if (meta.timezone) {\n      rawSqlQuery.meta!.timezone = meta.timezone;\n    }\n\n    if (meta.builderOptions) {\n      // When changing from builder to raw editor, the builder options are saved and also require migration\n      rawSqlQuery.meta!.builderOptions = migrateV3QueryBuilderOptions(meta.builderOptions);\n    }\n  }\n\n  // delete unwanted properties from v3\n  delete (rawSqlQuery as any)['builderOptions'];\n  delete (rawSqlQuery as any)['selectedFormat'];\n\n  return rawSqlQuery;\n};\n\n/**\n * Takes v3 options and returns a version compatible with the latest builder.\n */\nconst migrateV3QueryBuilderOptions = (savedOptions: AnyQueryBuilderOptions): QueryBuilderOptions => {\n  const mapped: QueryBuilderOptions = {\n    database: savedOptions.database || '',\n    table: savedOptions.table || '',\n    queryType: getV3QueryType(savedOptions),\n    columns: [],\n  };\n\n  if (savedOptions.mode) {\n    mapped.mode = savedOptions.mode;\n  }\n\n  if (savedOptions['fields'] || Array.isArray(savedOptions['fields'])) {\n    const oldColumns: string[] = savedOptions['fields'];\n    mapped.columns = oldColumns.map((name: string) => ({ name }));\n  }\n\n  const timeField: string = savedOptions['timeField'];\n  const timeFieldType: string = savedOptions['timeFieldType'];\n  if (timeField) {\n    const timeColumn: SelectedColumn = {\n      name: timeField,\n      type: timeFieldType,\n      hint: ColumnHint.Time,\n    };\n\n    mapped.columns!.push(timeColumn);\n  }\n\n  const logLevelField: string = savedOptions['logLevelField'];\n  if (logLevelField) {\n    const logLevelColumn: SelectedColumn = {\n      name: logLevelField,\n      hint: ColumnHint.LogLevel,\n    };\n\n    mapped.columns!.push(logLevelColumn);\n  }\n\n  if (savedOptions['metrics'] || Array.isArray(savedOptions['metrics'])) {\n    const oldAggregates: any[] = savedOptions['metrics'];\n    mapped.aggregates = oldAggregates.map((agg) => ({\n      aggregateType: agg['aggregation'],\n      column: agg['field'],\n      alias: agg['alias'],\n    }));\n  }\n\n  if (savedOptions.filters || Array.isArray(savedOptions.filters)) {\n    const oldFilters: Filter[] = savedOptions.filters;\n\n    mapped.filters = oldFilters.map((filter: Filter) => {\n      const result: Filter = {\n        ...filter,\n      };\n\n      if (filter.key === timeField) {\n        result.hint = ColumnHint.Time;\n      } else if (filter.key === logLevelField) {\n        result.hint = ColumnHint.LogLevel;\n      }\n\n      return result;\n    });\n  }\n\n  if (savedOptions.groupBy || Array.isArray(savedOptions.groupBy)) {\n    mapped.groupBy = savedOptions.groupBy;\n  }\n\n  if (savedOptions.orderBy || Array.isArray(savedOptions.orderBy)) {\n    mapped.orderBy = savedOptions.orderBy;\n  }\n\n  if (savedOptions.limit !== undefined && savedOptions.limit >= 0) {\n    mapped.limit = savedOptions.limit;\n  }\n\n  return mapped;\n};\n\n/**\n * Checks if CHQuery is from <= v3 options.\n */\nconst isV3CHQuery = (savedQuery: AnyCHQuery): boolean => {\n  // pluginVersion was added in v4\n  const oldPluginVersion = !savedQuery['pluginVersion'] || !isVersionGtOrEq(savedQuery.pluginVersion, '4.0.0');\n  const oldQueryType = savedQuery['queryType'] === 'sql' || savedQuery['queryType'] === 'builder';\n  return oldPluginVersion || oldQueryType;\n};\n\n/**\n * Takes v3 options and returns the optimal QueryType. Defaults to QueryType.Table.\n */\nconst getV3QueryType = (savedOptions: AnyQueryBuilderOptions): QueryType => {\n  if (savedOptions['timeField']) {\n    return QueryType.TimeSeries;\n  } else if (savedOptions['logLevelField']) {\n    return QueryType.Logs;\n  }\n\n  return QueryType.Table;\n};\n"
  },
  {
    "path": "src/data/sqlGenerator.test.ts",
    "content": "import {\n  AggregateType,\n  BuilderMode,\n  ColumnHint,\n  FilterOperator,\n  OrderByDirection,\n  QueryBuilderOptions,\n  QueryType,\n  SelectedColumn,\n  TimeUnit,\n} from 'types/queryBuilder';\nimport {\n  _testExports,\n  generateSql,\n  getColumnByHint,\n  getColumnIndexByHint,\n  getColumnsByHints,\n  isAggregateQuery,\n} from './sqlGenerator';\n\ndescribe('SQL Generator', () => {\n  it('generates simple table query', () => {\n    const opts: QueryBuilderOptions = {\n      database: 'default',\n      table: 'sample',\n      queryType: QueryType.Table,\n      columns: [\n        { name: 'a', type: 'UInt64' },\n        { name: 'b', type: 'String' },\n        { name: 'c', type: 'String' },\n      ],\n      limit: 1000,\n      filters: [\n        {\n          filterType: 'custom',\n          key: 'b',\n          type: 'String',\n          condition: 'AND',\n          operator: FilterOperator.IsNotNull,\n        },\n      ],\n      orderBy: [],\n    };\n\n    const expectedSqlParts = ['SELECT a, b, c FROM \"default\".\"sample\"', 'WHERE ( b IS NOT NULL ) LIMIT 1000'];\n\n    const sql = generateSql(opts);\n    expect(sql).toEqual(expectedSqlParts.join(' '));\n  });\n\n  it('generates aggregate table query', () => {\n    const opts: QueryBuilderOptions = {\n      database: 'default',\n      table: 'sample',\n      queryType: QueryType.Table,\n      mode: BuilderMode.Aggregate,\n      columns: [\n        { name: 'a', type: 'DateTime' },\n        { name: 'b', type: 'String' },\n        { name: 'c', type: 'String' },\n      ],\n      aggregates: [{ aggregateType: AggregateType.Count, column: '*', alias: 'd' }],\n      limit: 1000,\n      filters: [\n        {\n          filterType: 'custom',\n          key: 'b',\n          type: 'String',\n          condition: 'AND',\n          operator: FilterOperator.IsNotNull,\n        },\n      ],\n      groupBy: ['a'],\n      orderBy: [],\n    };\n\n    const expectedSqlParts = [\n      'SELECT a, b, c, count(*) as d FROM \"default\".\"sample\"',\n      'WHERE ( b IS NOT NULL ) GROUP BY a LIMIT 1000',\n    ];\n\n    const sql = generateSql(opts);\n    expect(sql).toEqual(expectedSqlParts.join(' '));\n  });\n\n  it('generates logs query', () => {\n    const opts: QueryBuilderOptions = {\n      database: 'default',\n      table: 'logs',\n      queryType: QueryType.Logs,\n      columns: [\n        { name: 'log_ts', type: 'DateTime', hint: ColumnHint.Time },\n        { name: 'log_level', type: 'String', hint: ColumnHint.LogLevel },\n        { name: 'log_body', type: 'String', hint: ColumnHint.LogMessage },\n      ],\n      limit: 1000,\n      filters: [\n        {\n          filterType: 'custom',\n          type: 'datetime',\n          key: '',\n          condition: 'AND',\n          hint: ColumnHint.Time,\n          operator: FilterOperator.WithInGrafanaTimeRange,\n        },\n        {\n          filterType: 'custom',\n          type: 'String',\n          key: '',\n          value: 'error',\n          condition: 'AND',\n          hint: ColumnHint.LogLevel,\n          operator: FilterOperator.Equals,\n        },\n      ],\n      orderBy: [{ name: '', hint: ColumnHint.Time, dir: OrderByDirection.DESC }],\n    };\n\n    const expectedSqlParts = [\n      'SELECT log_ts as \"timestamp\", log_body as \"body\", log_level as \"level\"',\n      'FROM \"default\".\"logs\"',\n      'WHERE ( timestamp >= $__fromTime AND timestamp <= $__toTime )',\n      \"AND ( level = 'error' )\",\n      'ORDER BY timestamp DESC LIMIT 1000',\n    ];\n\n    const sql = generateSql(opts);\n    expect(sql).toEqual(expectedSqlParts.join(' '));\n  });\n\n  it('generates simple time series query', () => {\n    const opts: QueryBuilderOptions = {\n      database: 'default',\n      table: 'time_data',\n      queryType: QueryType.TimeSeries,\n      columns: [\n        { name: 'time_field', type: 'DateTime', hint: ColumnHint.Time },\n        { name: 'number_field', type: 'UInt64' },\n      ],\n      limit: 100,\n      filters: [\n        {\n          filterType: 'custom',\n          key: 'number_field',\n          type: 'UInt64',\n          condition: 'AND',\n          operator: FilterOperator.GreaterThan,\n          value: 0,\n        },\n      ],\n      orderBy: [{ name: '', hint: ColumnHint.Time, dir: OrderByDirection.ASC }],\n    };\n    const expectedSqlParts = [\n      'SELECT time_field as \"time\", number_field',\n      'FROM \"default\".\"time_data\" WHERE ( number_field > 0 )',\n      'ORDER BY time ASC LIMIT 100',\n    ];\n\n    const sql = generateSql(opts);\n    expect(sql).toEqual(expectedSqlParts.join(' '));\n  });\n\n  it('generates aggregate time series query', () => {\n    const opts: QueryBuilderOptions = {\n      database: 'default',\n      table: 'time_data',\n      queryType: QueryType.TimeSeries,\n      columns: [\n        { name: 'time_field', type: 'DateTime', hint: ColumnHint.Time },\n        { name: 'number_field', type: 'UInt64' },\n      ],\n      limit: 100,\n      aggregates: [{ aggregateType: AggregateType.Sum, column: 'number_field', alias: 'total' }],\n      filters: [\n        {\n          filterType: 'custom',\n          key: 'number_field',\n          type: 'UInt64',\n          condition: 'AND',\n          operator: FilterOperator.GreaterThan,\n          value: 0,\n        },\n      ],\n      orderBy: [{ name: '', hint: ColumnHint.Time, dir: OrderByDirection.ASC }],\n    };\n    const expectedSqlParts = [\n      'SELECT time_field as \"time\", number_field, sum(number_field) as total',\n      'FROM \"default\".\"time_data\" WHERE ( number_field > 0 )',\n      'GROUP BY time ORDER BY time ASC LIMIT 100',\n    ];\n\n    const sql = generateSql(opts);\n    expect(sql).toEqual(expectedSqlParts.join(' '));\n  });\n\n  it('generates trace ID query without OTel enabled', () => {\n    const opts: QueryBuilderOptions = {\n      database: 'default',\n      table: 'otel_traces',\n      queryType: QueryType.Traces,\n      columns: [\n        { name: 'TraceId', type: 'String', hint: ColumnHint.TraceId },\n        { name: 'SpanId', type: 'String', hint: ColumnHint.TraceSpanId },\n        { name: 'ParentSpanId', type: 'String', hint: ColumnHint.TraceParentSpanId },\n        { name: 'ServiceName', type: 'LowCardinality(String)', hint: ColumnHint.TraceServiceName },\n        { name: 'SpanName', type: 'LowCardinality(String)', hint: ColumnHint.TraceOperationName },\n        { name: 'Timestamp', type: 'DateTime64(9)', hint: ColumnHint.Time },\n        { name: 'Duration', type: 'Int64', hint: ColumnHint.TraceDurationTime },\n        { name: 'SpanAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceTags },\n        { name: 'ResourceAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceServiceTags },\n        { name: 'StatusCode', type: 'LowCardinality(String)', hint: ColumnHint.TraceStatusCode },\n      ],\n      filters: [],\n      meta: {\n        minimized: true,\n        otelEnabled: false,\n        otelVersion: 'latest',\n        traceDurationUnit: TimeUnit.Nanoseconds,\n        isTraceIdMode: true,\n        traceId: 'abcdefg',\n      },\n      limit: 1000,\n      orderBy: [],\n    };\n    const expectedSqlParts = [\n      'SELECT \"TraceId\" as traceID, \"SpanId\" as spanID, \"ParentSpanId\" as parentSpanID,',\n      '\"ServiceName\" as serviceName, \"SpanName\" as operationName, multiply(toUnixTimestamp64Nano(\"Timestamp\"), 0.000001) as startTime,',\n      'multiply(\"Duration\", 0.000001) as duration,',\n      `arrayMap(key -> map('key', key, 'value',\"SpanAttributes\"[key]),`,\n      `mapKeys(\"SpanAttributes\")) as tags,`,\n      `arrayMap(key -> map('key', key, 'value',\"ResourceAttributes\"[key]), mapKeys(\"ResourceAttributes\")) as serviceTags,`,\n      `if(\"StatusCode\" IN ('Error', 'STATUS_CODE_ERROR'), 2, 0) as statusCode`,\n      `FROM \"default\".\"otel_traces\" WHERE traceID = 'abcdefg'`,\n    ];\n\n    const sql = generateSql(opts);\n    expect(sql).toEqual(expectedSqlParts.join(' '));\n  });\n\n  it('generates trace ID query with additional fields, flatten nested disabled', () => {\n    const opts: QueryBuilderOptions = {\n      database: 'default',\n      table: 'otel_traces',\n      queryType: QueryType.Traces,\n      columns: [\n        { name: 'TraceId', type: 'String', hint: ColumnHint.TraceId },\n        { name: 'SpanId', type: 'String', hint: ColumnHint.TraceSpanId },\n        { name: 'ParentSpanId', type: 'String', hint: ColumnHint.TraceParentSpanId },\n        { name: 'ServiceName', type: 'LowCardinality(String)', hint: ColumnHint.TraceServiceName },\n        { name: 'SpanName', type: 'LowCardinality(String)', hint: ColumnHint.TraceOperationName },\n        { name: 'Timestamp', type: 'DateTime64(9)', hint: ColumnHint.Time },\n        { name: 'Duration', type: 'Int64', hint: ColumnHint.TraceDurationTime },\n        { name: 'SpanAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceTags },\n        { name: 'ResourceAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceServiceTags },\n        { name: 'StatusCode', type: 'LowCardinality(String)', hint: ColumnHint.TraceStatusCode },\n        { name: 'Kind', type: 'String', hint: ColumnHint.TraceKind },\n        { name: 'StatusMessage', type: 'String', hint: ColumnHint.TraceStatusMessage },\n        { name: 'InstrumentationLibraryName', type: 'String', hint: ColumnHint.TraceInstrumentationLibraryName },\n        { name: 'InstrumentationLibraryVersion', type: 'String', hint: ColumnHint.TraceInstrumentationLibraryVersion },\n        { name: 'TraceState', type: 'String', hint: ColumnHint.TraceState },\n      ],\n      filters: [],\n      meta: {\n        minimized: true,\n        otelEnabled: true,\n        otelVersion: 'latest',\n        traceDurationUnit: TimeUnit.Nanoseconds,\n        isTraceIdMode: true,\n        traceId: 'abcdefg',\n        flattenNested: false,\n        traceEventsColumnPrefix: 'Events',\n        traceLinksColumnPrefix: 'Links',\n        hasTraceTimestampTable: true,\n      },\n      limit: 1000,\n      orderBy: [],\n    };\n\n    const expectedSqlParts = [\n      `WITH 'abcdefg' as trace_id, (SELECT min(Start) FROM \"default\".\"otel_traces_trace_id_ts\" WHERE TraceId = trace_id) as trace_start,`,\n      `(SELECT max(End) + 1 FROM \"default\".\"otel_traces_trace_id_ts\" WHERE TraceId = trace_id) as trace_end`,\n      'SELECT \"TraceId\" as traceID, \"SpanId\" as spanID, \"ParentSpanId\" as parentSpanID,',\n      '\"ServiceName\" as serviceName, \"SpanName\" as operationName, multiply(toUnixTimestamp64Nano(\"Timestamp\"), 0.000001) as startTime,',\n      'multiply(\"Duration\", 0.000001) as duration,',\n      `arrayMap(key -> map('key', key, 'value',\"SpanAttributes\"[key]),`,\n      `mapKeys(\"SpanAttributes\")) as tags,`,\n      `arrayMap(key -> map('key', key, 'value',\"ResourceAttributes\"[key]), mapKeys(\"ResourceAttributes\")) as serviceTags,`,\n      `if(\"StatusCode\" IN ('Error', 'STATUS_CODE_ERROR'), 2, 0) as statusCode,`,\n      `arrayMap((name, timestamp, attributes) -> tuple(name, toString(toUnixTimestamp64Milli(timestamp)), arrayMap( key -> map('key', key, 'value', attributes[key]), mapKeys(attributes)))::Tuple(name String, timestamp String, fields Array(Map(String, String))), \"Events\".Name, \"Events\".Timestamp, \"Events\".Attributes) AS logs,`,\n      `arrayMap((traceID, spanID, attributes) -> tuple(traceID, spanID, arrayMap(key -> map('key', key, 'value', attributes[key]), mapKeys(attributes)))::Tuple(traceID String, spanID String, tags Array(Map(String, String))), \"Links\".TraceId, \"Links\".SpanId, \"Links\".Attributes) AS references,`,\n      '\"Kind\" as kind,',\n      '\"StatusMessage\" as statusMessage,',\n      '\"InstrumentationLibraryName\" as instrumentationLibraryName,',\n      '\"InstrumentationLibraryVersion\" as instrumentationLibraryVersion,',\n      '\"TraceState\" as traceState',\n      `FROM \"default\".\"otel_traces\" WHERE traceID = trace_id AND \"Timestamp\" >= trace_start AND \"Timestamp\" <= trace_end`,\n    ];\n\n    const sql = generateSql(opts);\n    expect(sql).toEqual(expectedSqlParts.join(' '));\n  });\n\n  it('generates trace ID query with additional fields, flatten nested enabled', () => {\n    const opts: QueryBuilderOptions = {\n      database: 'default',\n      table: 'otel_traces',\n      queryType: QueryType.Traces,\n      columns: [\n        { name: 'TraceId', type: 'String', hint: ColumnHint.TraceId },\n        { name: 'SpanId', type: 'String', hint: ColumnHint.TraceSpanId },\n        { name: 'ParentSpanId', type: 'String', hint: ColumnHint.TraceParentSpanId },\n        { name: 'ServiceName', type: 'LowCardinality(String)', hint: ColumnHint.TraceServiceName },\n        { name: 'SpanName', type: 'LowCardinality(String)', hint: ColumnHint.TraceOperationName },\n        { name: 'Timestamp', type: 'DateTime64(9)', hint: ColumnHint.Time },\n        { name: 'Duration', type: 'Int64', hint: ColumnHint.TraceDurationTime },\n        { name: 'SpanAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceTags },\n        { name: 'ResourceAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceServiceTags },\n        { name: 'StatusCode', type: 'LowCardinality(String)', hint: ColumnHint.TraceStatusCode },\n        { name: 'Kind', type: 'String', hint: ColumnHint.TraceKind },\n        { name: 'StatusMessage', type: 'String', hint: ColumnHint.TraceStatusMessage },\n        { name: 'InstrumentationLibraryName', type: 'String', hint: ColumnHint.TraceInstrumentationLibraryName },\n        { name: 'InstrumentationLibraryVersion', type: 'String', hint: ColumnHint.TraceInstrumentationLibraryVersion },\n        { name: 'TraceState', type: 'String', hint: ColumnHint.TraceState },\n      ],\n      filters: [],\n      meta: {\n        minimized: true,\n        otelEnabled: true,\n        otelVersion: 'latest',\n        traceDurationUnit: TimeUnit.Nanoseconds,\n        isTraceIdMode: true,\n        traceId: 'abcdefg',\n        flattenNested: true,\n        traceEventsColumnPrefix: 'Events',\n        traceLinksColumnPrefix: 'Links',\n        hasTraceTimestampTable: true,\n      },\n      limit: 1000,\n      orderBy: [],\n    };\n\n    const expectedSqlParts = [\n      `WITH 'abcdefg' as trace_id, (SELECT min(Start) FROM \"default\".\"otel_traces_trace_id_ts\" WHERE TraceId = trace_id) as trace_start,`,\n      `(SELECT max(End) + 1 FROM \"default\".\"otel_traces_trace_id_ts\" WHERE TraceId = trace_id) as trace_end`,\n      'SELECT \"TraceId\" as traceID, \"SpanId\" as spanID, \"ParentSpanId\" as parentSpanID,',\n      '\"ServiceName\" as serviceName, \"SpanName\" as operationName, multiply(toUnixTimestamp64Nano(\"Timestamp\"), 0.000001) as startTime,',\n      'multiply(\"Duration\", 0.000001) as duration,',\n      `arrayMap(key -> map('key', key, 'value',\"SpanAttributes\"[key]),`,\n      `mapKeys(\"SpanAttributes\")) as tags,`,\n      `arrayMap(key -> map('key', key, 'value',\"ResourceAttributes\"[key]), mapKeys(\"ResourceAttributes\")) as serviceTags,`,\n      `if(\"StatusCode\" IN ('Error', 'STATUS_CODE_ERROR'), 2, 0) as statusCode,`,\n      `arrayMap(event -> tuple(multiply(toFloat64(event.Timestamp), 1000), arrayConcat(arrayMap(key -> map('key', key, 'value', event.Attributes[key]), mapKeys(event.Attributes)), [map('key', 'message', 'value', event.Name)]))::Tuple(timestamp Float64, fields Array(Map(String, String))), \"Events\") as logs,`,\n      `arrayMap(link -> tuple(link.TraceId, link.SpanId, arrayMap(key -> map('key', key, 'value', link.Attributes[key]), mapKeys(link.Attributes)))::Tuple(traceID String, spanID String, tags Array(Map(String, String))), \"Links\") AS references,`,\n      '\"Kind\" as kind,',\n      '\"StatusMessage\" as statusMessage,',\n      '\"InstrumentationLibraryName\" as instrumentationLibraryName,',\n      '\"InstrumentationLibraryVersion\" as instrumentationLibraryVersion,',\n      '\"TraceState\" as traceState',\n      `FROM \"default\".\"otel_traces\" WHERE traceID = trace_id AND \"Timestamp\" >= trace_start AND \"Timestamp\" <= trace_end`,\n    ];\n\n    const sql = generateSql(opts);\n    expect(sql).toEqual(expectedSqlParts.join(' '));\n  });\n\n  it('generates trace ID query with OTel enabled', () => {\n    const opts: QueryBuilderOptions = {\n      database: 'default',\n      table: 'otel_traces',\n      queryType: QueryType.Traces,\n      columns: [\n        { name: 'TraceId', type: 'String', hint: ColumnHint.TraceId },\n        { name: 'SpanId', type: 'String', hint: ColumnHint.TraceSpanId },\n        { name: 'ParentSpanId', type: 'String', hint: ColumnHint.TraceParentSpanId },\n        { name: 'ServiceName', type: 'LowCardinality(String)', hint: ColumnHint.TraceServiceName },\n        { name: 'SpanName', type: 'LowCardinality(String)', hint: ColumnHint.TraceOperationName },\n        { name: 'Timestamp', type: 'DateTime64(9)', hint: ColumnHint.Time },\n        { name: 'Duration', type: 'Int64', hint: ColumnHint.TraceDurationTime },\n        { name: 'SpanAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceTags },\n        { name: 'ResourceAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceServiceTags },\n        { name: 'StatusCode', type: 'LowCardinality(String)', hint: ColumnHint.TraceStatusCode },\n      ],\n      filters: [],\n      meta: {\n        minimized: true,\n        otelEnabled: true,\n        otelVersion: 'latest',\n        traceDurationUnit: TimeUnit.Nanoseconds,\n        isTraceIdMode: true,\n        traceId: 'abcdefg',\n        hasTraceTimestampTable: true,\n      },\n      limit: 1000,\n      orderBy: [],\n    };\n    const expectedSqlParts = [\n      `WITH 'abcdefg' as trace_id, (SELECT min(Start) FROM \"default\".\"otel_traces_trace_id_ts\" WHERE TraceId = trace_id) as trace_start,`,\n      `(SELECT max(End) + 1 FROM \"default\".\"otel_traces_trace_id_ts\" WHERE TraceId = trace_id) as trace_end`,\n      'SELECT \"TraceId\" as traceID, \"SpanId\" as spanID, \"ParentSpanId\" as parentSpanID,',\n      '\"ServiceName\" as serviceName, \"SpanName\" as operationName, multiply(toUnixTimestamp64Nano(\"Timestamp\"), 0.000001) as startTime,',\n      'multiply(\"Duration\", 0.000001) as duration,',\n      `arrayMap(key -> map('key', key, 'value',\"SpanAttributes\"[key]),`,\n      `mapKeys(\"SpanAttributes\")) as tags,`,\n      `arrayMap(key -> map('key', key, 'value',\"ResourceAttributes\"[key]), mapKeys(\"ResourceAttributes\")) as serviceTags,`,\n      `if(\"StatusCode\" IN ('Error', 'STATUS_CODE_ERROR'), 2, 0) as statusCode`,\n      `FROM \"default\".\"otel_traces\" WHERE traceID = trace_id AND \"Timestamp\" >= trace_start AND \"Timestamp\" <= trace_end`,\n    ];\n\n    const sql = generateSql(opts);\n    expect(sql).toEqual(expectedSqlParts.join(' '));\n  });\n\n  it('generates an OTel trace ID query without the time range optimization when the trace timestamp table does not exist ', () => {\n    const opts: QueryBuilderOptions = {\n      database: 'default',\n      table: 'otel_traces',\n      queryType: QueryType.Traces,\n      columns: [\n        { name: 'TraceId', type: 'String', hint: ColumnHint.TraceId },\n        { name: 'SpanId', type: 'String', hint: ColumnHint.TraceSpanId },\n        { name: 'ParentSpanId', type: 'String', hint: ColumnHint.TraceParentSpanId },\n        { name: 'ServiceName', type: 'LowCardinality(String)', hint: ColumnHint.TraceServiceName },\n        { name: 'SpanName', type: 'LowCardinality(String)', hint: ColumnHint.TraceOperationName },\n        { name: 'Timestamp', type: 'DateTime64(9)', hint: ColumnHint.Time },\n        { name: 'Duration', type: 'Int64', hint: ColumnHint.TraceDurationTime },\n        { name: 'SpanAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceTags },\n        { name: 'ResourceAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceServiceTags },\n        { name: 'StatusCode', type: 'LowCardinality(String)', hint: ColumnHint.TraceStatusCode },\n      ],\n      filters: [],\n      meta: {\n        minimized: true,\n        otelEnabled: true,\n        otelVersion: 'latest',\n        traceDurationUnit: TimeUnit.Nanoseconds,\n        isTraceIdMode: true,\n        traceId: 'abcdefg',\n        hasTraceTimestampTable: false, // trace timestamp table does not exist\n      },\n      limit: 1000,\n      orderBy: [],\n    };\n    const expectedSqlParts = [\n      'SELECT \"TraceId\" as traceID, \"SpanId\" as spanID, \"ParentSpanId\" as parentSpanID,',\n      '\"ServiceName\" as serviceName, \"SpanName\" as operationName, multiply(toUnixTimestamp64Nano(\"Timestamp\"), 0.000001) as startTime,',\n      'multiply(\"Duration\", 0.000001) as duration,',\n      `arrayMap(key -> map('key', key, 'value',\"SpanAttributes\"[key]),`,\n      `mapKeys(\"SpanAttributes\")) as tags,`,\n      `arrayMap(key -> map('key', key, 'value',\"ResourceAttributes\"[key]), mapKeys(\"ResourceAttributes\")) as serviceTags,`,\n      `if(\"StatusCode\" IN ('Error', 'STATUS_CODE_ERROR'), 2, 0) as statusCode`,\n      `FROM \"default\".\"otel_traces\" WHERE traceID = 'abcdefg'`,\n    ];\n\n    const sql = generateSql(opts);\n    expect(sql).toEqual(expectedSqlParts.join(' '));\n  });\n\n  it('regression #1541: trace ID query does not apply LIMIT (spans must not be truncated)', () => {\n    const opts: QueryBuilderOptions = {\n      database: 'default',\n      table: 'otel_traces',\n      queryType: QueryType.Traces,\n      columns: [\n        { name: 'TraceId', type: 'String', hint: ColumnHint.TraceId },\n        { name: 'SpanId', type: 'String', hint: ColumnHint.TraceSpanId },\n        { name: 'Timestamp', type: 'DateTime64(9)', hint: ColumnHint.Time },\n      ],\n      filters: [],\n      meta: {\n        minimized: true,\n        otelEnabled: false,\n        otelVersion: 'latest',\n        isTraceIdMode: true,\n        traceId: 'abcdefg',\n      },\n      // Inherited from the trace-search builder; this is the bug: spans of\n      // the selected trace must not be capped at this number.\n      limit: 3,\n      orderBy: [],\n    };\n    const sql = generateSql(opts);\n    expect(sql).not.toMatch(/\\bLIMIT\\b/);\n  });\n\n  it('generates an optimized trace ID query without OTel when a trace timestamp table exists', () => {\n    const opts: QueryBuilderOptions = {\n      database: 'default',\n      table: 'custom_traces',\n      queryType: QueryType.Traces,\n      columns: [\n        { name: 'TraceId', type: 'String', hint: ColumnHint.TraceId },\n        { name: 'SpanId', type: 'String', hint: ColumnHint.TraceSpanId },\n        { name: 'ParentSpanId', type: 'String', hint: ColumnHint.TraceParentSpanId },\n        { name: 'ServiceName', type: 'LowCardinality(String)', hint: ColumnHint.TraceServiceName },\n        { name: 'SpanName', type: 'LowCardinality(String)', hint: ColumnHint.TraceOperationName },\n        { name: 'Timestamp', type: 'DateTime64(9)', hint: ColumnHint.Time },\n        { name: 'Duration', type: 'Int64', hint: ColumnHint.TraceDurationTime },\n        { name: 'SpanAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceTags },\n        { name: 'ResourceAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceServiceTags },\n        { name: 'StatusCode', type: 'LowCardinality(String)', hint: ColumnHint.TraceStatusCode },\n      ],\n      filters: [],\n      meta: {\n        minimized: true,\n        otelEnabled: false,\n        otelVersion: undefined,\n        traceDurationUnit: TimeUnit.Nanoseconds,\n        isTraceIdMode: true,\n        traceId: 'abcdefg',\n        hasTraceTimestampTable: true,\n      },\n      limit: 1000,\n      orderBy: [],\n    };\n    const expectedSqlParts = [\n      `WITH 'abcdefg' as trace_id, (SELECT min(Start) FROM \"default\".\"custom_traces_trace_id_ts\" WHERE TraceId = trace_id) as trace_start,`,\n      `(SELECT max(End) + 1 FROM \"default\".\"custom_traces_trace_id_ts\" WHERE TraceId = trace_id) as trace_end`,\n      'SELECT \"TraceId\" as traceID, \"SpanId\" as spanID, \"ParentSpanId\" as parentSpanID,',\n      '\"ServiceName\" as serviceName, \"SpanName\" as operationName, multiply(toUnixTimestamp64Nano(\"Timestamp\"), 0.000001) as startTime,',\n      'multiply(\"Duration\", 0.000001) as duration,',\n      `arrayMap(key -> map('key', key, 'value',\"SpanAttributes\"[key]),`,\n      `mapKeys(\"SpanAttributes\")) as tags,`,\n      `arrayMap(key -> map('key', key, 'value',\"ResourceAttributes\"[key]), mapKeys(\"ResourceAttributes\")) as serviceTags,`,\n      `if(\"StatusCode\" IN ('Error', 'STATUS_CODE_ERROR'), 2, 0) as statusCode`,\n      `FROM \"default\".\"custom_traces\" WHERE traceID = trace_id AND \"Timestamp\" >= trace_start AND \"Timestamp\" <= trace_end`,\n    ];\n\n    const sql = generateSql(opts);\n    expect(sql).toEqual(expectedSqlParts.join(' '));\n  });\n\n  it('honours a configured traceTimestampTableSuffix in the optimized trace ID query', () => {\n    const opts: QueryBuilderOptions = {\n      database: 'default',\n      table: 'custom_traces',\n      queryType: QueryType.Traces,\n      columns: [\n        { name: 'TraceId', type: 'String', hint: ColumnHint.TraceId },\n        { name: 'SpanId', type: 'String', hint: ColumnHint.TraceSpanId },\n        { name: 'ParentSpanId', type: 'String', hint: ColumnHint.TraceParentSpanId },\n        { name: 'ServiceName', type: 'LowCardinality(String)', hint: ColumnHint.TraceServiceName },\n        { name: 'SpanName', type: 'LowCardinality(String)', hint: ColumnHint.TraceOperationName },\n        { name: 'Timestamp', type: 'DateTime64(9)', hint: ColumnHint.Time },\n        { name: 'Duration', type: 'Int64', hint: ColumnHint.TraceDurationTime },\n        { name: 'SpanAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceTags },\n        { name: 'ResourceAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceServiceTags },\n        { name: 'StatusCode', type: 'LowCardinality(String)', hint: ColumnHint.TraceStatusCode },\n      ],\n      filters: [],\n      meta: {\n        minimized: true,\n        otelEnabled: false,\n        otelVersion: undefined,\n        traceDurationUnit: TimeUnit.Nanoseconds,\n        isTraceIdMode: true,\n        traceId: 'abcdefg',\n        hasTraceTimestampTable: true,\n        traceTimestampTableSuffix: '_ts_index',\n      },\n      limit: 1000,\n      orderBy: [],\n    };\n    const sql = generateSql(opts);\n\n    expect(sql).toContain('FROM \"default\".\"custom_traces_ts_index\"');\n    expect(sql).not.toContain('custom_traces_trace_id_ts');\n    expect(sql).toContain(`WITH 'abcdefg' as trace_id`);\n    expect(sql).toContain('\"Timestamp\" >= trace_start');\n  });\n\n  it('generates trace search query', () => {\n    const opts: QueryBuilderOptions = {\n      database: 'default',\n      table: 'otel_traces',\n      queryType: QueryType.Traces,\n      columns: [\n        { name: 'TraceId', type: 'String', hint: ColumnHint.TraceId },\n        { name: 'SpanId', type: 'String', hint: ColumnHint.TraceSpanId },\n        { name: 'ParentSpanId', type: 'String', hint: ColumnHint.TraceParentSpanId },\n        { name: 'ServiceName', type: 'LowCardinality(String)', hint: ColumnHint.TraceServiceName },\n        { name: 'SpanName', type: 'LowCardinality(String)', hint: ColumnHint.TraceOperationName },\n        { name: 'Timestamp', type: 'DateTime64(9)', hint: ColumnHint.Time },\n        { name: 'Duration', type: 'Int64', hint: ColumnHint.TraceDurationTime },\n        { name: 'SpanAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceTags },\n        { name: 'ResourceAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceServiceTags },\n      ],\n      filters: [\n        {\n          condition: 'AND',\n          filterType: 'custom',\n          hint: ColumnHint.Time,\n          key: '',\n          operator: FilterOperator.WithInGrafanaTimeRange,\n          type: 'datetime',\n        },\n        {\n          condition: 'AND',\n          filterType: 'custom',\n          hint: ColumnHint.TraceParentSpanId,\n          key: '',\n          operator: FilterOperator.IsEmpty,\n          type: 'string',\n          value: '',\n        },\n        {\n          condition: 'AND',\n          filterType: 'custom',\n          hint: ColumnHint.TraceDurationTime,\n          key: '',\n          operator: FilterOperator.GreaterThan,\n          type: 'UInt64',\n          value: 0,\n        },\n        {\n          condition: 'AND',\n          filterType: 'custom',\n          hint: ColumnHint.TraceServiceName,\n          key: '',\n          operator: FilterOperator.IsAnything,\n          type: 'string',\n          value: '',\n        },\n      ],\n      meta: {\n        otelEnabled: true,\n        otelVersion: 'latest',\n        traceDurationUnit: TimeUnit.Nanoseconds,\n      },\n      limit: 1000,\n      orderBy: [\n        { name: '', hint: ColumnHint.Time, dir: OrderByDirection.DESC },\n        { name: '', hint: ColumnHint.TraceDurationTime, dir: OrderByDirection.DESC },\n      ],\n    };\n    const expectedSqlParts = [\n      'SELECT \"TraceId\" as traceID, \"ServiceName\" as serviceName, \"SpanName\" as operationName,',\n      '\"Timestamp\" as startTime, multiply(\"Duration\", 0.000001) as duration',\n      'FROM \"default\".\"otel_traces\" WHERE ( Timestamp >= $__fromTime AND Timestamp <= $__toTime )',\n      \"AND ( ParentSpanId = '' ) AND ( Duration > 0 ) ORDER BY Timestamp DESC, Duration DESC LIMIT 1000\",\n    ];\n\n    const sql = generateSql(opts);\n    expect(sql).toEqual(expectedSqlParts.join(' '));\n  });\n\n  it('generates table query with column names containing colons', () => {\n    const opts: QueryBuilderOptions = {\n      database: 'default',\n      table: 'verifications',\n      queryType: QueryType.Table,\n      columns: [\n        { name: 'verification:id', type: 'String' },\n        { name: 'my:name', type: 'String' },\n        { name: 'regular_column', type: 'String' },\n      ],\n      limit: 1000,\n      filters: [],\n      orderBy: [],\n    };\n\n    const expectedSqlParts = [\n      'SELECT \"verification:id\", \"my:name\", regular_column',\n      'FROM \"default\".\"verifications\" LIMIT 1000',\n    ];\n\n    const sql = generateSql(opts);\n    expect(sql).toEqual(expectedSqlParts.join(' '));\n  });\n});\n\ndescribe('isAggregateQuery', () => {\n  it('returns true for aggregate query', () => {\n    const builderOptions = {\n      aggregates: [{ column: 'foo', aggregateType: AggregateType.Count }],\n    } as QueryBuilderOptions;\n    expect(isAggregateQuery(builderOptions)).toEqual(true);\n  });\n  it('returns false for query without aggregates', () => {\n    const builderOptions = {} as QueryBuilderOptions;\n    expect(isAggregateQuery(builderOptions)).toEqual(false);\n  });\n});\n\ndescribe('getColumnByHint', () => {\n  it('returns a selected column when present', () => {\n    const testColumn = { name: 'time', type: 'datetime', hint: ColumnHint.Time };\n    const builderOptions = { columns: [testColumn] } as QueryBuilderOptions;\n    expect(getColumnByHint(builderOptions, ColumnHint.Time)).toMatchObject(testColumn);\n  });\n  it('returns a undefined when column not present', () => {\n    const testColumn = { name: 'time', type: 'datetime' };\n    const builderOptions = { columns: [testColumn] } as QueryBuilderOptions;\n    expect(getColumnByHint(builderOptions, ColumnHint.Time)).toBeUndefined();\n  });\n});\n\ndescribe('getColumnIndexByHint', () => {\n  it('returns a selected column index when present', () => {\n    const testColumns = [{}, { name: 'time', type: 'datetime', hint: ColumnHint.Time }];\n    const builderOptions = { columns: testColumns } as QueryBuilderOptions;\n    expect(getColumnIndexByHint(builderOptions, ColumnHint.Time)).toEqual(1);\n  });\n  it('returns -1 when column not present', () => {\n    const testColumn = { name: 'time', type: 'datetime' };\n    const builderOptions = { columns: [testColumn] } as QueryBuilderOptions;\n    expect(getColumnIndexByHint(builderOptions, ColumnHint.Time)).toEqual(-1);\n  });\n});\n\ndescribe('getColumnsByHints', () => {\n  it('returns selected columns when present', () => {\n    const testColumns = [\n      { name: 'time', type: 'DateTime', hint: ColumnHint.Time },\n      { name: 'level', type: 'String', hint: ColumnHint.LogLevel },\n    ];\n    const builderOptions = { columns: testColumns } as QueryBuilderOptions;\n    expect(getColumnsByHints(builderOptions, [ColumnHint.Time, ColumnHint.LogLevel])).toHaveLength(2);\n  });\n  it('returns empty array when columns not present', () => {\n    const testColumn = { name: 'time', type: 'datetime' };\n    const builderOptions = { columns: [testColumn] } as QueryBuilderOptions;\n    expect(getColumnsByHints(builderOptions, [ColumnHint.Time])).toHaveLength(0);\n  });\n});\n\ndescribe('getColumnIdentifier', () => {\n  const cases: Array<{ input: SelectedColumn; expected: string }> = [\n    { input: { name: '' }, expected: `` },\n    { input: { name: ' ' }, expected: `\" \"` },\n    { input: { name: 'test' }, expected: `test` },\n    { input: { name: 'test with space' }, expected: `\"test with space\"` },\n    { input: { name: 'test with alias', alias: 'a' }, expected: `\"test with alias\" as \"a\"` },\n    { input: { name: 'test_with_alias', alias: 'b' }, expected: `test_with_alias as \"b\"` },\n    { input: { name: '\"test\" as a', alias: '' }, expected: `\"test\" as a` },\n    { input: { name: 'verification:id' }, expected: `\"verification:id\"` },\n    { input: { name: 'my:name' }, expected: `\"my:name\"` },\n    { input: { name: 'namespace:field:value' }, expected: `\"namespace:field:value\"` },\n    { input: { name: 'verification:id', alias: 'vid' }, expected: `\"verification:id\" as \"vid\"` },\n  ];\n\n  it.each(cases)('returns correct identifier (case %#)', (c) => {\n    expect(_testExports.getColumnIdentifier(c.input)).toEqual(c.expected);\n  });\n});\n\ndescribe('getTableIdentifier', () => {\n  const cases: Array<{ input: { database: string; table: string }; expected: string }> = [\n    { input: { database: '', table: '' }, expected: '' },\n    { input: { database: 'database', table: '' }, expected: '\"database\"' },\n    { input: { database: 'database', table: 'table' }, expected: '\"database\".\"table\"' },\n    { input: { database: '', table: 'table' }, expected: '\"table\"' },\n  ];\n\n  it.each(cases)('returns correct identifier (case %#)', (c) => {\n    expect(_testExports.getTableIdentifier(c.input.database, c.input.table)).toEqual(c.expected);\n  });\n});\n\ndescribe('escapeIdentifier', () => {\n  const cases: Array<{ input: string; expected: string }> = [\n    { input: '', expected: '' },\n    { input: ' ', expected: `\" \"` },\n    { input: 'x', expected: `\"x\"` },\n    { input: 'x x x', expected: `\"x x x\"` },\n    { input: undefined as any as string, expected: `` },\n  ];\n\n  it.each(cases)('returns escaped identifier (case %#)', (c) => {\n    expect(_testExports.escapeIdentifier(c.input)).toEqual(c.expected);\n  });\n});\n\ndescribe('escapeValue', () => {\n  const cases: Array<{ input: string; expected: string }> = [\n    { input: ``, expected: `''` },\n    { input: ` `, expected: `' '` },\n    { input: `$variable`, expected: `$variable` },\n    { input: `\\${variable}`, expected: `\\${variable}` },\n    { input: `\\${variable:singlequote}`, expected: `\\${variable:singlequote}` },\n    { input: `\\${variable.key}`, expected: `\\${variable.key}` },\n    { input: `\\${variable.key:singlequote}`, expected: `\\${variable.key:singlequote}` },\n    { input: `count(column)`, expected: `count(column)` },\n    { input: `'custom expression'`, expected: `'custom expression'` },\n    { input: `plain text`, expected: `'plain text'` },\n    { input: `text`, expected: `'text'` },\n    { input: `\"column\"`, expected: `\"column\"` },\n    { input: `invalid(`, expected: `invalid(` },\n    { input: `invalid)`, expected: `invalid)` },\n    { input: `$()'\" `, expected: `$()'\" ` },\n  ];\n\n  it.each(cases)('returns escaped value (case %#)', (c) => {\n    expect(_testExports.escapeValue(c.input)).toEqual(c.expected);\n  });\n});\n\ndescribe('concatQueryParts', () => {\n  it('concats query parts', () => {\n    const parts = [\n      'SELECT',\n      '', // empty strings should be ignored\n      ' ', // spaces allowed\n      '*',\n      'FROM',\n      'test',\n    ];\n    const sql = _testExports.concatQueryParts(parts);\n    const expectedSql = 'SELECT   * FROM test'; // 3 spaces expected before *\n    expect(sql).toEqual(expectedSql);\n  });\n});\n\ndescribe('getOrderBy', () => {\n  it('returns empty order By', () => {\n    const options = {} as QueryBuilderOptions;\n    const sql = _testExports.getOrderBy(options);\n    const expectedSql = '';\n    expect(sql).toEqual(expectedSql);\n  });\n\n  it('returns regular order By', () => {\n    const options = {\n      orderBy: [\n        { name: 'normal', dir: OrderByDirection.ASC },\n        { name: 'order', dir: OrderByDirection.DESC },\n      ],\n    } as QueryBuilderOptions;\n    const sql = _testExports.getOrderBy(options);\n    const expectedSql = 'normal ASC, order DESC';\n    expect(sql).toEqual(expectedSql);\n  });\n\n  it('returns hinted order By', () => {\n    const options = {\n      columns: [{ name: 'hinted', hint: ColumnHint.Time }],\n      orderBy: [\n        { name: '', hint: ColumnHint.Time, dir: OrderByDirection.ASC },\n        { name: 'normal', dir: OrderByDirection.ASC },\n        { name: 'order', dir: OrderByDirection.DESC },\n      ],\n    } as QueryBuilderOptions;\n    const sql = _testExports.getOrderBy(options);\n    const expectedSql = 'hinted ASC, normal ASC, order DESC';\n    expect(sql).toEqual(expectedSql);\n  });\n\n  describe('when hintsToGroup is set', () => {\n    it('groups orderBy columns by hintsToGroup', () => {\n      const options = {\n        columns: [\n          { name: 'TimestampTime', hint: ColumnHint.FilterTime },\n          { name: 'Timestamp', hint: ColumnHint.Time },\n          { name: 'SeverityText', hint: ColumnHint.LogLevel },\n        ],\n        orderBy: [\n          { name: '', hint: ColumnHint.FilterTime, dir: OrderByDirection.DESC },\n          { name: '', hint: ColumnHint.LogLevel, dir: OrderByDirection.ASC },\n          { name: '', hint: ColumnHint.Time, dir: OrderByDirection.DESC },\n        ],\n      } as QueryBuilderOptions;\n      const hintsToGroup = new Set([ColumnHint.FilterTime, ColumnHint.Time]);\n      const sql = _testExports.getOrderBy(options, hintsToGroup);\n      const expectedSql = '(TimestampTime, Timestamp) DESC, SeverityText ASC';\n      expect(sql).toEqual(expectedSql);\n    });\n\n    it('does not wrap single grouped column in parentheses', () => {\n      const options = {\n        columns: [\n          { name: 'TimestampTime', hint: ColumnHint.FilterTime },\n          { name: 'Timestamp', hint: ColumnHint.Time },\n          { name: 'SeverityText', hint: ColumnHint.LogLevel },\n        ],\n        orderBy: [\n          { name: '', hint: ColumnHint.FilterTime, dir: OrderByDirection.DESC },\n          { name: '', hint: ColumnHint.LogLevel, dir: OrderByDirection.ASC },\n        ],\n      } as QueryBuilderOptions;\n      const hintsToGroup = new Set([ColumnHint.FilterTime, ColumnHint.Time]);\n      const sql = _testExports.getOrderBy(options, hintsToGroup);\n      const expectedSql = 'TimestampTime DESC, SeverityText ASC';\n      expect(sql).toEqual(expectedSql);\n    });\n\n    it('uses direction of first grouped item for the whole group', () => {\n      const options = {\n        columns: [\n          { name: 'TimestampTime', hint: ColumnHint.FilterTime },\n          { name: 'Timestamp', hint: ColumnHint.Time },\n        ],\n        orderBy: [\n          { name: '', hint: ColumnHint.FilterTime, dir: OrderByDirection.DESC },\n          { name: '', hint: ColumnHint.Time, dir: OrderByDirection.ASC },\n        ],\n      } as QueryBuilderOptions;\n      const hintsToGroup = new Set([ColumnHint.FilterTime, ColumnHint.Time]);\n      const sql = _testExports.getOrderBy(options, hintsToGroup);\n      const expectedSql = '(TimestampTime, Timestamp) DESC';\n      expect(sql).toEqual(expectedSql);\n    });\n\n    it('inserts hint group at index of first grouped column when mixed with non-grouped columns', () => {\n      const options = {\n        columns: [\n          { name: 'SeverityText', hint: ColumnHint.LogLevel },\n          { name: 'TimestampTime', hint: ColumnHint.FilterTime },\n          { name: 'Timestamp', hint: ColumnHint.Time },\n        ],\n        orderBy: [\n          { name: '', hint: ColumnHint.LogLevel, dir: OrderByDirection.ASC },\n          { name: '', hint: ColumnHint.FilterTime, dir: OrderByDirection.DESC },\n          { name: '', hint: ColumnHint.Time, dir: OrderByDirection.DESC },\n        ],\n      } as QueryBuilderOptions;\n      const hintsToGroup = new Set([ColumnHint.FilterTime, ColumnHint.Time]);\n      const sql = _testExports.getOrderBy(options, hintsToGroup);\n      const expectedSql = 'SeverityText ASC, (TimestampTime, Timestamp) DESC';\n      expect(sql).toEqual(expectedSql);\n    });\n  });\n});\n\ndescribe('getLimit', () => {\n  const cases: Array<{ input: number | undefined; expected: string }> = [\n    { input: undefined, expected: '' },\n    { input: -1, expected: '' },\n    { input: 0, expected: '' },\n    { input: 1, expected: 'LIMIT 1' },\n    { input: 100, expected: 'LIMIT 100' },\n    { input: 1000, expected: 'LIMIT 1000' },\n  ];\n\n  it.each(cases)('returns correct LIMIT clause (case %#)', (c) => {\n    expect(_testExports.getLimit(c.input)).toEqual(c.expected);\n  });\n});\n\ndescribe('is*Type', () => {\n  it.each<{ input: string; expected: boolean }>([\n    { input: 'String', expected: true },\n    { input: 'Nullable(String)', expected: true },\n    { input: 'LowCardinality(Nullable(String))', expected: true },\n    { input: 'FixedString(1)', expected: true },\n    { input: 'LowCardinality(Nullable(FixedString(1)))', expected: true },\n    { input: 'LowCardinality(FixedString(1))', expected: true },\n    { input: 'Nullable(FixedString(1))', expected: true },\n    { input: 'Array(String)', expected: false },\n  ])('$input isStringType $expected', (c) => {\n    expect(_testExports.isStringType(c.input)).toEqual(c.expected);\n  });\n});\n\ndescribe('getFilters', () => {\n  it('returns empty filter array', () => {\n    const options = {} as QueryBuilderOptions;\n    const sql = _testExports.getFilters(options);\n    const expectedSql = '';\n    expect(sql).toEqual(expectedSql);\n  });\n\n  it('returns correct IN clause for escaped and unescaped values', () => {\n    const options = {\n      filters: [\n        {\n          condition: 'AND',\n          filterType: 'custom',\n          key: 'col',\n          operator: FilterOperator.In,\n          type: 'string',\n          value: '1, (2), 3, some string, \\'another string\\', someFunction(123), \"column reference\"'.split(','),\n        },\n      ],\n    } as QueryBuilderOptions;\n    const sql = _testExports.getFilters(options);\n    const expectedSql = `( col IN ('1', (2), '3', 'some string', 'another string', someFunction(123), \"column reference\") )`;\n    expect(sql).toEqual(expectedSql);\n  });\n\n  it('extracts Map value type for mapKey filter with Map(String, String)', () => {\n    const options = {\n      filters: [\n        {\n          condition: 'AND',\n          filterType: 'custom',\n          key: 'ResourceAttributes',\n          mapKey: 'service.name',\n          operator: FilterOperator.Equals,\n          type: 'Map(String, String)',\n          value: 'my-service',\n        },\n      ],\n    } as QueryBuilderOptions;\n    const sql = _testExports.getFilters(options);\n    expect(sql).toEqual(`( ResourceAttributes['service.name'] = 'my-service' )`);\n  });\n\n  it('extracts Map value type for mapKey filter with Map(String, UInt64)', () => {\n    const options = {\n      filters: [\n        {\n          condition: 'AND',\n          filterType: 'custom',\n          key: 'NumericMap',\n          mapKey: 'count',\n          operator: FilterOperator.Equals,\n          type: 'Map(String, UInt64)',\n          value: 42,\n        },\n      ],\n    } as QueryBuilderOptions;\n    const sql = _testExports.getFilters(options);\n    expect(sql).toEqual(`( NumericMap['count'] = 42 )`);\n  });\n\n  it('extracts Map value type for mapKey filter with Map(LowCardinality(String), String)', () => {\n    const options = {\n      filters: [\n        {\n          condition: 'AND',\n          filterType: 'custom',\n          key: 'SpanAttributes',\n          mapKey: 'http.method',\n          operator: FilterOperator.Like,\n          type: 'Map(LowCardinality(String), String)',\n          value: 'GET',\n        },\n      ],\n    } as QueryBuilderOptions;\n    const sql = _testExports.getFilters(options);\n    expect(sql).toEqual(`( SpanAttributes['http.method'] LIKE '%GET%' )`);\n  });\n\n  it('extracts Map value type for mapKey filter with Map(LowCardinality(String), UInt64)', () => {\n    const options = {\n      filters: [\n        {\n          condition: 'AND',\n          filterType: 'custom',\n          key: 'NumericAttrs',\n          mapKey: 'retry_count',\n          operator: FilterOperator.Equals,\n          type: 'Map(LowCardinality(String), UInt64)',\n          value: 3,\n        },\n      ],\n    } as QueryBuilderOptions;\n    const sql = _testExports.getFilters(options);\n    expect(sql).toEqual(`( NumericAttrs['retry_count'] = 3 )`);\n  });\n\n  it('returns complex filter array', () => {\n    const options = {\n      columns: [{ name: 'hinted', hint: ColumnHint.Time }],\n      filters: [\n        {\n          condition: 'AND',\n          filterType: 'custom',\n          hint: ColumnHint.Time,\n          key: '',\n          operator: FilterOperator.WithInGrafanaTimeRange,\n          type: 'datetime',\n        },\n        {\n          condition: 'AND',\n          filterType: 'custom',\n          key: 'text',\n          operator: FilterOperator.IsEmpty,\n          type: 'string',\n          value: '',\n        },\n        {\n          condition: 'AND',\n          filterType: 'custom',\n          key: 'volume',\n          operator: FilterOperator.GreaterThan,\n          type: 'UInt64',\n          value: 0,\n        },\n        {\n          condition: 'AND',\n          filterType: 'custom',\n          key: 'should_be_excluded_from_filters',\n          operator: FilterOperator.IsAnything,\n          type: 'string',\n          value: '',\n        },\n      ],\n    } as QueryBuilderOptions;\n    const sql = _testExports.getFilters(options);\n    const expectedSql = \"( hinted >= $__fromTime AND hinted <= $__toTime ) AND ( text = '' ) AND ( volume > 0 )\";\n    expect(sql).toEqual(expectedSql);\n  });\n});\n"
  },
  {
    "path": "src/data/sqlGenerator.ts",
    "content": "import {\n  BooleanFilter,\n  BuilderMode,\n  ColumnHint,\n  DateFilterWithValue,\n  FilterOperator,\n  MultiFilter,\n  NumberFilter,\n  OrderByDirection,\n  QueryBuilderOptions,\n  QueryType,\n  SelectedColumn,\n  StringFilter,\n  TimeUnit,\n} from 'types/queryBuilder';\nimport otel from 'otel';\n\n/**\n * Generates a SQL string for the given QueryBuilderOptions\n */\nexport const generateSql = (options: QueryBuilderOptions): string => {\n  const hasTraceIdFilter = options.meta?.isTraceIdMode && options.meta?.traceId;\n  if (options.queryType === QueryType.Traces && hasTraceIdFilter) {\n    return generateTraceIdQuery(options);\n  } else if (options.queryType === QueryType.Traces) {\n    return generateTraceSearchQuery(options);\n  } else if (options.queryType === QueryType.Logs) {\n    return generateLogsQuery(options);\n  } else if (options.queryType === QueryType.TimeSeries && options.mode !== BuilderMode.Trend) {\n    return generateSimpleTimeSeriesQuery(options);\n  } else if (options.queryType === QueryType.TimeSeries && options.mode === BuilderMode.Trend) {\n    return generateAggregateTimeSeriesQuery(options);\n  } else if (options.queryType === QueryType.Table) {\n    return generateTableQuery(options);\n  }\n\n  return '';\n};\n\n/**\n * Generates trace search query.\n */\nconst generateTraceSearchQuery = (options: QueryBuilderOptions): string => {\n  const { database, table } = options;\n\n  const queryParts: string[] = [];\n\n  // TODO: these columns could be a map or some other convenience function\n  const selectParts: string[] = [];\n  const traceId = getColumnByHint(options, ColumnHint.TraceId);\n  if (traceId !== undefined) {\n    selectParts.push(`${escapeIdentifier(traceId.name)} as traceID`);\n  }\n\n  const traceServiceName = getColumnByHint(options, ColumnHint.TraceServiceName);\n  if (traceServiceName !== undefined) {\n    selectParts.push(`${escapeIdentifier(traceServiceName.name)} as serviceName`);\n  }\n\n  const traceOperationName = getColumnByHint(options, ColumnHint.TraceOperationName);\n  if (traceOperationName !== undefined) {\n    selectParts.push(`${escapeIdentifier(traceOperationName.name)} as operationName`);\n  }\n\n  const traceStartTime = getColumnByHint(options, ColumnHint.Time);\n  if (traceStartTime !== undefined) {\n    selectParts.push(`${escapeIdentifier(traceStartTime.name)} as startTime`);\n  }\n\n  const traceDurationTime = getColumnByHint(options, ColumnHint.TraceDurationTime);\n  if (traceDurationTime !== undefined) {\n    const timeUnit = options.meta?.traceDurationUnit;\n    selectParts.push(getTraceDurationSelectSql(escapeIdentifier(traceDurationTime.name), timeUnit));\n  }\n\n  const selectPartsSql = selectParts.join(', ');\n\n  queryParts.push('SELECT');\n  queryParts.push(selectPartsSql);\n  queryParts.push('FROM');\n  queryParts.push(getTableIdentifier(database, table));\n\n  const filterParts = getFilters(options);\n  if (filterParts) {\n    queryParts.push('WHERE');\n    queryParts.push(filterParts);\n  }\n\n  const orderBy = getOrderBy(options);\n  if (orderBy) {\n    queryParts.push('ORDER BY');\n    queryParts.push(orderBy);\n  }\n\n  const limit = getLimit(options.limit);\n  if (limit !== '') {\n    queryParts.push(limit);\n  }\n\n  return concatQueryParts(queryParts);\n};\n\n/**\n * Generates trace query with columns that fit Grafana's Trace panel\n * Column aliases follow this structure:\n * https://grafana.com/docs/grafana/latest/explore/trace-integration/#data-frame-structure\n */\nconst generateTraceIdQuery = (options: QueryBuilderOptions): string => {\n  const { database, table } = options;\n\n  const queryParts: string[] = [];\n\n  // TODO: these columns could be a map or some other convenience function\n  const selectParts: string[] = [];\n  const traceId = getColumnByHint(options, ColumnHint.TraceId);\n  if (traceId !== undefined) {\n    selectParts.push(`${escapeIdentifier(traceId.name)} as traceID`);\n  }\n\n  const traceSpanId = getColumnByHint(options, ColumnHint.TraceSpanId);\n  if (traceSpanId !== undefined) {\n    selectParts.push(`${escapeIdentifier(traceSpanId.name)} as spanID`);\n  }\n\n  const traceParentSpanId = getColumnByHint(options, ColumnHint.TraceParentSpanId);\n  if (traceParentSpanId !== undefined) {\n    selectParts.push(`${escapeIdentifier(traceParentSpanId.name)} as parentSpanID`);\n  }\n\n  const traceServiceName = getColumnByHint(options, ColumnHint.TraceServiceName);\n  if (traceServiceName !== undefined) {\n    selectParts.push(`${escapeIdentifier(traceServiceName.name)} as serviceName`);\n  }\n\n  const traceOperationName = getColumnByHint(options, ColumnHint.TraceOperationName);\n  if (traceOperationName !== undefined) {\n    selectParts.push(`${escapeIdentifier(traceOperationName.name)} as operationName`);\n  }\n\n  const traceStartTime = getColumnByHint(options, ColumnHint.Time);\n  if (traceStartTime !== undefined) {\n    selectParts.push(`${convertTimeFieldToMilliseconds(escapeIdentifier(traceStartTime.name))} as startTime`);\n  }\n\n  const traceDurationTime = getColumnByHint(options, ColumnHint.TraceDurationTime);\n  if (traceDurationTime !== undefined) {\n    const timeUnit = options.meta?.traceDurationUnit;\n    selectParts.push(getTraceDurationSelectSql(escapeIdentifier(traceDurationTime.name), timeUnit));\n  }\n\n  // TODO: for tags and serviceTags, consider the column type. They might not require mapping, they could already be JSON.\n  const traceTags = getColumnByHint(options, ColumnHint.TraceTags);\n  if (traceTags !== undefined) {\n    selectParts.push(\n      `arrayMap(key -> map('key', key, 'value',${escapeIdentifier(traceTags.name)}[key]), mapKeys(${escapeIdentifier(traceTags.name)})) as tags`\n    );\n  }\n\n  const traceServiceTags = getColumnByHint(options, ColumnHint.TraceServiceTags);\n  if (traceServiceTags !== undefined) {\n    selectParts.push(\n      `arrayMap(key -> map('key', key, 'value',${escapeIdentifier(traceServiceTags.name)}[key]), mapKeys(${escapeIdentifier(traceServiceTags.name)})) as serviceTags`\n    );\n  }\n\n  const traceStatusCode = getColumnByHint(options, ColumnHint.TraceStatusCode);\n  if (traceStatusCode !== undefined) {\n    selectParts.push(\n      `if(${escapeIdentifier(traceStatusCode.name)} IN ('Error', 'STATUS_CODE_ERROR'), 2, 0) as statusCode`\n    );\n  }\n\n  const flattenNested = Boolean(options.meta?.flattenNested);\n\n  const traceEventsPrefix = options.meta?.traceEventsColumnPrefix || '';\n  if (traceEventsPrefix !== '') {\n    if (flattenNested) {\n      selectParts.push(\n        [\n          `arrayMap(event -> tuple(multiply(toFloat64(event.Timestamp), 1000),`,\n          `arrayConcat(arrayMap(key -> map('key', key, 'value', event.Attributes[key]),`,\n          `mapKeys(event.Attributes)), [map('key', 'message', 'value', event.Name)]))::Tuple(timestamp Float64, fields Array(Map(String, String))),`,\n          `${escapeIdentifier(traceEventsPrefix)}) as logs`,\n        ].join(' ')\n      );\n    } else {\n      selectParts.push(\n        [\n          `arrayMap((name, timestamp, attributes) -> tuple(name, toString(toUnixTimestamp64Milli(timestamp)),`,\n          `arrayMap( key -> map('key', key, 'value', attributes[key]),`,\n          `mapKeys(attributes)))::Tuple(name String, timestamp String, fields Array(Map(String, String))),`,\n          `${escapeIdentifier(traceEventsPrefix)}.Name, ${escapeIdentifier(traceEventsPrefix)}.Timestamp,`,\n          `${escapeIdentifier(traceEventsPrefix)}.Attributes) AS logs`,\n        ].join(' ')\n      );\n    }\n  }\n\n  const traceLinksPrefix = options.meta?.traceLinksColumnPrefix || '';\n  if (traceLinksPrefix !== '') {\n    if (flattenNested) {\n      selectParts.push(\n        [\n          `arrayMap(link -> tuple(link.TraceId, link.SpanId, arrayMap(key -> map('key', key, 'value', link.Attributes[key]),`,\n          `mapKeys(link.Attributes)))::Tuple(traceID String, spanID String, tags Array(Map(String, String))),`,\n          `${escapeIdentifier(traceLinksPrefix)}) AS references`,\n        ].join(' ')\n      );\n    } else {\n      selectParts.push(\n        [\n          `arrayMap((traceID, spanID, attributes) -> tuple(traceID, spanID, arrayMap(key -> map('key', key, 'value', attributes[key]),`,\n          `mapKeys(attributes)))::Tuple(traceID String, spanID String, tags Array(Map(String, String))),`,\n          `${escapeIdentifier(traceLinksPrefix)}.TraceId, ${escapeIdentifier(traceLinksPrefix)}.SpanId,`,\n          `${escapeIdentifier(traceLinksPrefix)}.Attributes) AS references`,\n        ].join(' ')\n      );\n    }\n  }\n\n  const traceKind = getColumnByHint(options, ColumnHint.TraceKind);\n  if (traceKind !== undefined) {\n    selectParts.push(`${escapeIdentifier(traceKind.name)} as kind`);\n  }\n\n  const traceStatusMessage = getColumnByHint(options, ColumnHint.TraceStatusMessage);\n  if (traceStatusMessage !== undefined) {\n    selectParts.push(`${escapeIdentifier(traceStatusMessage.name)} as statusMessage`);\n  }\n\n  const traceInstrumentationLibraryName = getColumnByHint(options, ColumnHint.TraceInstrumentationLibraryName);\n  if (traceInstrumentationLibraryName !== undefined) {\n    selectParts.push(`${escapeIdentifier(traceInstrumentationLibraryName.name)} as instrumentationLibraryName`);\n  }\n\n  const traceInstrumentationLibraryVersion = getColumnByHint(options, ColumnHint.TraceInstrumentationLibraryVersion);\n  if (traceInstrumentationLibraryVersion !== undefined) {\n    selectParts.push(`${escapeIdentifier(traceInstrumentationLibraryVersion.name)} as instrumentationLibraryVersion`);\n  }\n\n  const traceState = getColumnByHint(options, ColumnHint.TraceState);\n  if (traceState !== undefined) {\n    selectParts.push(`${escapeIdentifier(traceState.name)} as traceState`);\n  }\n\n  const selectPartsSql = selectParts.join(', ');\n\n  // Optimize trace ID filtering when a companion timestamp index table is available.\n  // The optimization is gated purely on that capability — OTel is not required, so\n  // any schema following the `<table>_trace_id_ts` convention (or a user-configured\n  // suffix) benefits from the narrowed time range.\n  const hasTraceTimestampTable = options.meta?.hasTraceTimestampTable;\n  const hasTraceIdFilter = options.meta?.isTraceIdMode && options.meta?.traceId;\n  const applyTraceIdOptimization =\n    hasTraceTimestampTable && hasTraceIdFilter && traceStartTime !== undefined;\n  if (applyTraceIdOptimization) {\n    const traceId = options.meta!.traceId;\n    const suffix = options.meta?.traceTimestampTableSuffix || otel.traceTimestampTableSuffix;\n    const timestampTable = getTableIdentifier(database, table + suffix);\n    queryParts.push('WITH');\n    queryParts.push(`'${traceId}' as trace_id,`);\n    queryParts.push(`(SELECT min(Start) FROM ${timestampTable} WHERE TraceId = trace_id) as trace_start,`);\n    queryParts.push(`(SELECT max(End) + 1 FROM ${timestampTable} WHERE TraceId = trace_id) as trace_end`);\n  }\n\n  queryParts.push('SELECT');\n  queryParts.push(selectPartsSql);\n  queryParts.push('FROM');\n  queryParts.push(getTableIdentifier(database, table));\n\n  const filterParts = getFilters(options);\n\n  if (hasTraceIdFilter || filterParts) {\n    queryParts.push('WHERE');\n  }\n\n  if (applyTraceIdOptimization) {\n    queryParts.push('traceID = trace_id');\n    queryParts.push('AND');\n    queryParts.push(`${escapeIdentifier(traceStartTime.name)} >= trace_start`);\n    queryParts.push('AND');\n    queryParts.push(`${escapeIdentifier(traceStartTime.name)} <= trace_end`);\n  } else if (hasTraceIdFilter) {\n    const traceId = options.meta!.traceId;\n    queryParts.push(`traceID = '${traceId}'`);\n  }\n\n  if (filterParts) {\n    if (hasTraceIdFilter) {\n      queryParts.push('AND');\n    }\n\n    queryParts.push(filterParts);\n  }\n\n  const orderBy = getOrderBy(options);\n  if (orderBy) {\n    queryParts.push('ORDER BY');\n    queryParts.push(orderBy);\n  }\n\n  // Intentionally no LIMIT: this query is only reached in single-trace mode\n  // (`generateSql` routes here when `isTraceIdMode && traceId` are both set),\n  // and the WHERE clause already narrows to one trace ID. Applying the list's\n  // LIMIT here cuts off spans in the trace waterfall — see #1541.\n\n  return concatQueryParts(queryParts);\n};\n\n/**\n * Generates logs query with columns that fit Grafana's Logs panel\n * Column aliases follow this structure:\n * https://grafana.com/developers/plugin-tools/tutorials/build-a-logs-data-source-plugin#logs-data-frame-format\n *\n * note: column order seems to matter as well as alias name\n */\nconst generateLogsQuery = (_options: QueryBuilderOptions): string => {\n  // Copy columns so column aliases can be safely mutated\n  const options = { ..._options, columns: _options.columns?.map((c) => ({ ...c })) };\n  const { database, table } = options;\n\n  const queryParts: string[] = [];\n\n  // TODO: these columns could be a map or some other convenience function\n  const selectParts: string[] = [];\n  const logTime = getColumnByHint(options, ColumnHint.Time) || getColumnByHint(options, ColumnHint.FilterTime);\n  if (logTime !== undefined) {\n    // Must be first column in list.\n    logTime.alias = logColumnHintsToAlias.get(logTime.hint!);\n    selectParts.push(getColumnIdentifier(logTime));\n  }\n\n  const logMessage = getColumnByHint(options, ColumnHint.LogMessage);\n  if (logMessage !== undefined) {\n    // Must be second column in list.\n    logMessage.alias = logColumnHintsToAlias.get(ColumnHint.LogMessage);\n    selectParts.push(getColumnIdentifier(logMessage));\n  }\n\n  const logLevel = getColumnByHint(options, ColumnHint.LogLevel);\n  if (logLevel !== undefined) {\n    // TODO: \"severity\" should be a number, but \"level\" can be a string? Perhaps we can check the column type here?\n    logLevel.alias = logColumnHintsToAlias.get(ColumnHint.LogLevel);\n    selectParts.push(getColumnIdentifier(logLevel));\n  }\n\n  const traceId = getColumnByHint(options, ColumnHint.TraceId);\n  if (traceId !== undefined) {\n    traceId.alias = logColumnHintsToAlias.get(ColumnHint.TraceId);\n    selectParts.push(getColumnIdentifier(traceId));\n  }\n\n  const resourceAttributes = getColumnByHint(options, ColumnHint.ResourceAttributes);\n  if (resourceAttributes !== undefined) {\n    resourceAttributes.alias = logColumnHintsToAlias.get(ColumnHint.ResourceAttributes);\n    selectParts.push(getColumnIdentifier(resourceAttributes));\n  }\n\n  const scopeAttributes = getColumnByHint(options, ColumnHint.ScopeAttributes);\n  if (scopeAttributes !== undefined) {\n    scopeAttributes.alias = logColumnHintsToAlias.get(ColumnHint.ScopeAttributes);\n    selectParts.push(getColumnIdentifier(scopeAttributes));\n  }\n\n  const logAttributes = getColumnByHint(options, ColumnHint.LogAttributes);\n  if (logAttributes !== undefined) {\n    logAttributes.alias = logColumnHintsToAlias.get(ColumnHint.LogAttributes);\n    selectParts.push(getColumnIdentifier(logAttributes));\n  }\n\n  options.columns\n    ?.filter((c) => c.hint === undefined) // remove specialized columns\n    .forEach((c) => selectParts.push(getColumnIdentifier(c)));\n\n  const selectPartsSql = selectParts.join(', ');\n\n  queryParts.push('SELECT');\n  queryParts.push(selectPartsSql);\n  queryParts.push('FROM');\n  queryParts.push(getTableIdentifier(database, table));\n\n  const filterParts = getFilters(options);\n  const hasLogMessageFilter = logMessage && options.meta?.logMessageLike;\n\n  if (filterParts || hasLogMessageFilter) {\n    queryParts.push('WHERE');\n  }\n\n  if (filterParts) {\n    queryParts.push(filterParts);\n  }\n\n  if (hasLogMessageFilter) {\n    if (filterParts) {\n      queryParts.push('AND');\n    }\n\n    queryParts.push(`(${logMessage.alias || logMessage.name} LIKE '%${options.meta!.logMessageLike}%')`);\n  }\n\n  const hintsToGroup = new Set([ColumnHint.FilterTime, ColumnHint.Time]);\n  const orderBy = getOrderBy(options, hintsToGroup);\n  if (orderBy) {\n    queryParts.push('ORDER BY');\n    queryParts.push(orderBy);\n  }\n\n  const limit = getLimit(options.limit);\n  if (limit !== '') {\n    queryParts.push(limit);\n  }\n\n  return concatQueryParts(queryParts);\n};\n\n/**\n * Generates a simple time series query. Includes user selected columns.\n */\nconst generateSimpleTimeSeriesQuery = (_options: QueryBuilderOptions): string => {\n  // Copy columns so column aliases can be safely mutated\n  const options = { ..._options, columns: _options.columns?.map((c) => ({ ...c })) };\n  const { database, table } = options;\n\n  const queryParts: string[] = [];\n\n  const selectParts: string[] = [];\n  const selectNames = new Set<string>();\n  const timeColumn = getColumnByHint(options, ColumnHint.Time) || getColumnByHint(options, ColumnHint.FilterTime);\n  if (timeColumn !== undefined) {\n    timeColumn.alias = 'time';\n    selectParts.push(getColumnIdentifier(timeColumn));\n    selectNames.add(timeColumn.alias);\n  }\n\n  const columnsExcludingTimeColumn = options.columns?.filter((c) => c.hint !== ColumnHint.Time && c.hint !== ColumnHint.FilterTime);\n  columnsExcludingTimeColumn?.forEach((c) => {\n    selectParts.push(getColumnIdentifier(c));\n    selectNames.add(c.alias || c.name);\n  });\n\n  const aggregateSelectParts: string[] = [];\n  options.aggregates?.forEach((agg) => {\n    const alias = agg.alias ? ` as ${agg.alias.replace(/ /g, '_')}` : '';\n    const name = `${agg.aggregateType}(${agg.column})`;\n    aggregateSelectParts.push(`${name}${alias}`);\n    selectNames.add(alias ? alias.substring(4) : name);\n  });\n\n  options.groupBy?.forEach((g) => {\n    if (selectNames.has(g)) {\n      // don't add if already selected\n      return;\n    }\n\n    selectParts.push(g);\n  });\n\n  // (v3) aggregate selections go AFTER group by\n  aggregateSelectParts.forEach((a) => selectParts.push(a));\n\n  const selectPartsSql = selectParts.join(', ');\n\n  queryParts.push('SELECT');\n  queryParts.push(selectPartsSql);\n  queryParts.push('FROM');\n  queryParts.push(getTableIdentifier(database, table));\n\n  const filterParts = getFilters(options);\n  if (filterParts) {\n    queryParts.push('WHERE');\n    queryParts.push(filterParts);\n  }\n\n  const hasAggregates = options.aggregates?.length || 0 > 0;\n  const hasGroupBy = options.groupBy?.length || 0 > 0;\n  if (hasAggregates || hasGroupBy) {\n    queryParts.push('GROUP BY');\n  }\n\n  if ((options.groupBy?.length || 0) > 0) {\n    const groupByTime = timeColumn !== undefined ? `, ${timeColumn.alias}` : '';\n    queryParts.push(`${options.groupBy!.join(', ')}${groupByTime}`);\n  } else if (hasAggregates && timeColumn) {\n    queryParts.push(timeColumn.alias!);\n  }\n\n  const orderBy = getOrderBy(options);\n  if (orderBy) {\n    queryParts.push('ORDER BY');\n    queryParts.push(orderBy);\n  }\n\n  const limit = getLimit(options.limit);\n  if (limit !== '') {\n    queryParts.push(limit);\n  }\n\n  return concatQueryParts(queryParts);\n};\n\n/**\n * Generates an aggregate time series query.\n */\nconst generateAggregateTimeSeriesQuery = (_options: QueryBuilderOptions): string => {\n  // Copy columns so column aliases can be safely mutated\n  const options = { ..._options, columns: _options.columns?.map((c) => ({ ...c })) };\n  const { database, table } = options;\n\n  const queryParts: string[] = [];\n  const selectParts: string[] = [];\n\n  const timeColumn = getColumnByHint(options, ColumnHint.Time) || getColumnByHint(options, ColumnHint.FilterTime);\n  if (timeColumn !== undefined) {\n    timeColumn.name = `$__timeInterval(${timeColumn.name})`;\n    timeColumn.alias = 'time';\n    selectParts.push(getColumnIdentifier(timeColumn));\n  }\n\n  options.groupBy?.forEach((g) => selectParts.push(g));\n\n  options.aggregates?.forEach((agg) => {\n    const alias = agg.alias ? ` as ${agg.alias.replace(/ /g, '_')}` : '';\n    const name = `${agg.aggregateType}(${agg.column})`;\n    selectParts.push(`${name}${alias}`);\n  });\n\n  const selectPartsSql = selectParts.join(', ');\n\n  queryParts.push('SELECT');\n  queryParts.push(selectPartsSql);\n  queryParts.push('FROM');\n  queryParts.push(getTableIdentifier(database, table));\n\n  const filterParts = getFilters(options);\n  if (filterParts) {\n    queryParts.push('WHERE');\n    queryParts.push(filterParts);\n  }\n\n  queryParts.push('GROUP BY');\n  if ((options.groupBy?.length || 0) > 0) {\n    const groupByTime = timeColumn !== undefined ? `, ${timeColumn.alias}` : '';\n    queryParts.push(`${options.groupBy!.join(', ')}${groupByTime}`);\n  } else if (timeColumn) {\n    queryParts.push(timeColumn.alias!);\n  }\n\n  const orderBy = getOrderBy(options);\n  if (orderBy) {\n    queryParts.push('ORDER BY');\n    queryParts.push(orderBy);\n  }\n\n  const limit = getLimit(options.limit);\n  if (limit !== '') {\n    queryParts.push(limit);\n  }\n\n  return concatQueryParts(queryParts);\n};\n\n/**\n * Generates a table query.\n */\nconst generateTableQuery = (options: QueryBuilderOptions): string => {\n  const { database, table } = options;\n  const isAggregateMode = options.mode === BuilderMode.Aggregate;\n\n  const queryParts: string[] = [];\n  const selectParts: string[] = [];\n  const selectNames = new Set<string>();\n\n  options.columns?.forEach((c) => {\n    selectParts.push(getColumnIdentifier(c));\n    selectNames.add(c.alias || c.name);\n  });\n\n  if (isAggregateMode) {\n    options.aggregates?.forEach((agg) => {\n      const alias = agg.alias ? ` as ${agg.alias.replace(/ /g, '_')}` : '';\n      const name = `${agg.aggregateType}(${agg.column})`;\n      selectParts.push(`${name}${alias}`);\n      selectNames.add(alias ? alias.substring(4) : name);\n    });\n\n    options.groupBy?.forEach((g) => {\n      if (selectNames.has(g)) {\n        // don't add if already selected\n        return;\n      }\n\n      // user must manually select groupBys, for flexibility\n      // selectParts.push(g)\n    });\n  }\n\n  const selectPartsSql = selectParts.join(', ');\n\n  queryParts.push('SELECT');\n  queryParts.push(selectPartsSql);\n  queryParts.push('FROM');\n  queryParts.push(getTableIdentifier(database, table));\n\n  const filterParts = getFilters(options);\n  if (filterParts) {\n    queryParts.push('WHERE');\n    queryParts.push(filterParts);\n  }\n\n  if (isAggregateMode && (options.groupBy?.length || 0) > 0) {\n    queryParts.push('GROUP BY');\n    queryParts.push(options.groupBy!.join(', '));\n  }\n\n  const orderBy = getOrderBy(options);\n  if (orderBy) {\n    queryParts.push('ORDER BY');\n    queryParts.push(orderBy);\n  }\n\n  const limit = getLimit(options.limit);\n  if (limit !== '') {\n    queryParts.push(limit);\n  }\n\n  return concatQueryParts(queryParts);\n};\n\nexport const isAggregateQuery = (builder: QueryBuilderOptions): boolean => (builder.aggregates?.length || 0) > 0;\nexport const getColumnByHint = (options: QueryBuilderOptions, hint: ColumnHint): SelectedColumn | undefined =>\n  options.columns?.find((c) => c.hint === hint);\nexport const getColumnIndexByHint = (options: QueryBuilderOptions, hint: ColumnHint): number =>\n  (options.columns || []).findIndex((c) => c.hint === hint);\nexport const getColumnsByHints = (\n  options: QueryBuilderOptions,\n  hints: readonly ColumnHint[]\n): readonly SelectedColumn[] => {\n  const columns = [];\n\n  for (let hint of hints) {\n    const col = getColumnByHint(options, hint);\n    if (col !== undefined) {\n      columns.push(col);\n    }\n  }\n\n  return columns;\n};\n\nconst getColumnIdentifier = (col: SelectedColumn): string => {\n  let colName = col.name;\n\n  // allow for functions like count()\n  if (\n    colName.includes('(') ||\n    colName.includes(')') ||\n    colName.includes('\"') ||\n    colName.includes('\"') ||\n    colName.includes(' as ')\n  ) {\n    colName = col.name;\n  } else if (colName.includes(' ') || colName.includes(':')) {\n    colName = escapeIdentifier(col.name);\n  }\n\n  if (col.alias && col.alias !== col.name && escapeIdentifier(col.alias) !== colName) {\n    return `${colName} as \"${col.alias}\"`;\n  }\n\n  return colName;\n};\n\nconst getTableIdentifier = (database: string, table: string): string => {\n  const sep = !database || !table ? '' : '.';\n  return `${escapeIdentifier(database)}${sep}${escapeIdentifier(table)}`;\n};\n\nconst escapeIdentifier = (id: string): string => {\n  return id ? `\"${id}\"` : '';\n};\n\nconst escapeValue = (value: string): string => {\n  if (value.includes('$') || value.includes('(') || value.includes(')') || value.includes(\"'\") || value.includes('\"')) {\n    return value;\n  }\n\n  return `'${value}'`;\n};\n\n/**\n * Returns the SELECT column for trace duration.\n * Time unit is used to convert the value to milliseconds, as is required by Grafana's Trace panel.\n */\nconst getTraceDurationSelectSql = (columnIdentifier: string, timeUnit?: TimeUnit): string => {\n  const alias = 'duration';\n  switch (timeUnit) {\n    case TimeUnit.Seconds:\n      return `multiply(${columnIdentifier}, 1000) as ${alias}`;\n    case TimeUnit.Milliseconds:\n      return `${columnIdentifier} as ${alias}`;\n    case TimeUnit.Microseconds:\n      return `multiply(${columnIdentifier}, 0.001) as ${alias}`;\n    case TimeUnit.Nanoseconds:\n      return `multiply(${columnIdentifier}, 0.000001) as ${alias}`;\n    default:\n      return `${columnIdentifier} as ${alias}`;\n  }\n};\n\n/** Returns the input time field converted to a Unix timestamp in nanoseconds and then adjusted to milliseconds. */\nconst convertTimeFieldToMilliseconds = (columnIdentifier: string) =>\n  `multiply(toUnixTimestamp64Nano(${columnIdentifier}), 0.000001)`;\n\n/**\n * Concatenates query parts with no empty spaces.\n */\nconst concatQueryParts = (parts: readonly string[]): string => {\n  let query = '';\n  for (let i = 0; i < parts.length; i++) {\n    const p = parts[i];\n    if (!p) {\n      continue;\n    }\n\n    query += p;\n\n    if (i !== parts.length - 1) {\n      query += ' ';\n    }\n  }\n\n  return query;\n};\n\n/**\n * Returns the order by list, excluding the \"ORDER BY\" keyword.\n * If `hintsToGroup` is specified, orderBy columns with hints found in the Set will be grouped together.\n * e.g. when `hintsToGroup` equals Set([ColumnHint.FilterTime, ColumnHint.Time]),\n * result will be: \"(TimestampTime, Timestamp) DESC, SeverityText ASC\",\n * instead of: \"TimestampTime DESC, Timestamp DESC, SeverityText ASC\"\n */\nconst getOrderBy = (options: QueryBuilderOptions, hintsToGroup?: Set<ColumnHint>): string => {\n  const orderByParts: string[] = [];\n\n  const hintGroup: { columns: string[]; dir?: OrderByDirection; insertIndex?: number } = { columns: [] };\n\n  if ((options.orderBy?.length || 0) > 0) {\n    options.orderBy?.forEach((o) => {\n      let colName = o.name;\n\n      const hintedColumn = o.hint && getColumnByHint(options, o.hint);\n\n      if (hintedColumn) {\n        colName = hintedColumn.alias || hintedColumn.name;\n      }\n\n      if (!colName) {\n        return;\n      }\n\n      const inHintGroup = o.hint && hintsToGroup?.has(o.hint);\n\n      if (inHintGroup) {\n        if (hintGroup.insertIndex === undefined) {\n          // remember index of first column to be grouped, use that index for the whole group\n          hintGroup.insertIndex = orderByParts.length;\n        }\n\n        hintGroup.columns.push(colName);\n\n        if (!hintGroup.dir) {\n          // use the direction of first grouped item for the whole group\n          hintGroup.dir = o.dir;\n        }\n      } else {\n        orderByParts.push(`${colName} ${o.dir}`);\n      }\n    });\n  }\n\n  if (hintGroup.columns.length > 0 && hintGroup.dir && hintGroup.insertIndex !== undefined) {\n    let hintGroupColumnsJoined = hintGroup.columns.join(', ');\n\n    if (hintGroup.columns.length > 1) {\n      // only wrap in () if there's more than one column in the group\n      hintGroupColumnsJoined = `(${hintGroupColumnsJoined})`;\n    }\n\n    orderByParts.splice(hintGroup.insertIndex, 0, `${hintGroupColumnsJoined} ${hintGroup.dir}`);\n  }\n\n  return orderByParts.join(', ');\n};\n\n/**\n * Returns the limit clause including the \"LIMIT\" keyword\n */\nconst getLimit = (limit?: number | undefined): string => {\n  limit = Math.max(0, limit || 0);\n  if (limit > 0) {\n    return 'LIMIT ' + limit;\n  }\n\n  return '';\n};\n\n/**\n * Returns the filters in the WHERE clause, excluding the \"WHERE\" keyword\n */\nconst getFilters = (options: QueryBuilderOptions): string => {\n  const filters = options.filters || [];\n  const builtFilters: string[] = [];\n\n  for (const filter of filters) {\n    if (filter.operator === FilterOperator.IsAnything) {\n      continue;\n    }\n\n    const filterParts: string[] = [];\n\n    let column = filter.key;\n    let type = filter.type || '';\n    let hintedColumn = filter.hint && getColumnByHint(options, filter.hint);\n\n    // Fall back to Time/FilterTime if column not found\n    if (filter.hint === ColumnHint.Time && !hintedColumn) {\n      hintedColumn = getColumnByHint(options, ColumnHint.FilterTime);\n    } else if (filter.hint === ColumnHint.FilterTime && !hintedColumn) {\n      hintedColumn = getColumnByHint(options, ColumnHint.Time);\n    }\n\n    if (hintedColumn) {\n      column = hintedColumn.alias || hintedColumn.name;\n      type = hintedColumn.type || type;\n    }\n\n    if (!column) {\n      continue;\n    }\n\n    if (filter.mapKey && type.startsWith('Map')) {\n      column += `['${filter.mapKey}']`;\n      // Extract the value type from Map(KeyType, ValueType)\n      const valueType = type.match(/Map\\(\\s*.+\\s*,\\s*(.+)\\s*\\)/)?.[1]?.trim() || 'String';\n      type = valueType;\n    } else if (filter.mapKey && type.startsWith('JSON')) {\n      const escapedJSONPaths = filter.mapKey\n        .split('.')\n        .map((p) => `\\`${p}\\``)\n        .join('.');\n      column += `.${escapedJSONPaths}`;\n    }\n\n    filterParts.push(column);\n\n    let operator: string = filter.operator;\n    let negate = false;\n    if (filter.operator === FilterOperator.IsEmpty || filter.operator === FilterOperator.IsNotEmpty) {\n      operator = '';\n    } else if (filter.operator === FilterOperator.NotLike) {\n      operator = 'LIKE';\n      negate = true;\n    } else if (filter.operator === FilterOperator.NotILike) {\n      operator = 'ILIKE';\n      negate = true;\n    } else if (filter.operator === FilterOperator.OutsideGrafanaTimeRange) {\n      operator = '';\n      negate = true;\n    } else if (filter.operator === FilterOperator.WithInGrafanaTimeRange) {\n      operator = '';\n    }\n\n    if (operator) {\n      filterParts.push(operator);\n    }\n\n    if (isNullFilter(filter.operator)) {\n      // empty\n    } else if (filter.operator === FilterOperator.IsEmpty) {\n      filterParts.push(`= ''`);\n    } else if (filter.operator === FilterOperator.IsNotEmpty) {\n      filterParts.push(`!= ''`);\n    } else if (isBooleanFilter(type)) {\n      filterParts.push(String((filter as BooleanFilter).value));\n    } else if (isNumberFilter(type)) {\n      filterParts.push(String((filter as NumberFilter).value || '0'));\n    } else if (isDateFilter(type)) {\n      if (isDateFilterWithoutValue(type, filter.operator)) {\n        if (isDateType(type)) {\n          filterParts.push('>=', '$__fromTime', 'AND', column, '<=', '$__toTime');\n        }\n      } else {\n        switch ((filter as DateFilterWithValue).value) {\n          case 'GRAFANA_START_TIME':\n            if (isDateType(type)) {\n              filterParts.push('$__fromTime');\n            }\n            break;\n          case 'GRAFANA_END_TIME':\n            if (isDateType(type)) {\n              filterParts.push('$__toTime');\n            }\n            break;\n          default:\n            filterParts.push(escapeValue(String((filter as DateFilterWithValue).value || 'TODAY')));\n        }\n      }\n    } else if (isStringFilter(type, filter.operator)) {\n      if (\n        filter.operator === FilterOperator.Like ||\n        filter.operator === FilterOperator.NotLike ||\n        filter.operator === FilterOperator.ILike ||\n        filter.operator === FilterOperator.NotILike\n      ) {\n        filterParts.push(`'%${filter.value || ''}%'`);\n      } else {\n        filterParts.push(escapeValue((filter as StringFilter).value || ''));\n      }\n    } else if (isMultiFilter(type, filter.operator)) {\n      filterParts.push(`(${(filter as MultiFilter).value?.map((v) => escapeValue(v.trim())).join(', ')})`);\n    } else {\n      filterParts.push(escapeValue((filter as StringFilter).value || ''));\n    }\n\n    if (negate) {\n      filterParts.unshift('NOT', '(');\n      filterParts.push(')');\n    }\n\n    filterParts.unshift('(');\n    if (builtFilters.length > 0) {\n      filterParts.unshift(filter.condition);\n    }\n    filterParts.push(')');\n\n    const builtFilter = concatQueryParts(filterParts);\n    builtFilters.push(builtFilter);\n  }\n\n  return concatQueryParts(builtFilters);\n};\n\nconst stripTypeModifiers = (type: string): string => {\n  return type\n    .toLowerCase()\n    .replace(/\\(/g, '')\n    .replace(/\\)/g, '')\n    .replace(/nullable/g, '')\n    .replace(/lowcardinality/g, '');\n};\nconst isBooleanType = (type: string): boolean => type?.toLowerCase().startsWith('boolean');\nconst numberTypes = ['int', 'float', 'decimal'];\nconst isNumberType = (type: string): boolean => numberTypes.some((t) => type?.toLowerCase().includes(t));\nconst isDateType = (type: string): boolean =>\n  type?.toLowerCase().startsWith('date') || type?.toLowerCase().startsWith('nullable(date');\n// const isDateTimeType = (type: string): boolean => type?.toLowerCase().startsWith('datetime') || type?.toLowerCase().startsWith('nullable(datetime');\nconst isStringType = (type: string): boolean => {\n  type = stripTypeModifiers(type.toLowerCase());\n  return (\n    (type === 'string' || type.startsWith('fixedstring')) &&\n    !(isBooleanType(type) || isNumberType(type) || isDateType(type))\n  );\n};\nconst isNullFilter = (operator: FilterOperator): boolean =>\n  operator === FilterOperator.IsNull || operator === FilterOperator.IsNotNull;\nconst isBooleanFilter = (type: string): boolean => isBooleanType(type);\nconst isNumberFilter = (type: string): boolean => isNumberType(type);\nconst isDateFilterWithoutValue = (type: string, operator: FilterOperator): boolean =>\n  isDateType(type) &&\n  (operator === FilterOperator.WithInGrafanaTimeRange || operator === FilterOperator.OutsideGrafanaTimeRange);\nconst isDateFilter = (type: string): boolean => isDateType(type);\nconst isStringFilter = (type: string, operator: FilterOperator): boolean =>\n  isStringType(type) && !(operator === FilterOperator.In || operator === FilterOperator.NotIn);\nconst isMultiFilter = (type: string, operator: FilterOperator): boolean =>\n  isStringType(type) && (operator === FilterOperator.In || operator === FilterOperator.NotIn);\n\n/**\n * When filtering in the logs panel in explore view, we need a way to\n * map from the SQL generator's aliases back to the original column hints\n * so that filters can be added properly.\n */\nconst logAliasToColumnHintsEntries: ReadonlyArray<[string, ColumnHint]> = [\n  ['timestamp', ColumnHint.FilterTime],\n  ['timestamp', ColumnHint.Time], // duplicate key, last value is kept\n  ['body', ColumnHint.LogMessage],\n  ['level', ColumnHint.LogLevel],\n  ['traceID', ColumnHint.TraceId],\n];\nexport const logAliasToColumnHints: Map<string, ColumnHint> = new Map(logAliasToColumnHintsEntries);\nexport const logColumnHintsToAlias: Map<ColumnHint, string> = new Map(\n  logAliasToColumnHintsEntries.map((e) => [e[1], e[0]])\n);\n\nexport const _testExports = {\n  getColumnIdentifier,\n  getTableIdentifier,\n  escapeIdentifier,\n  escapeValue,\n  concatQueryParts,\n  getOrderBy,\n  getLimit,\n  getFilters,\n  isStringType,\n};\n"
  },
  {
    "path": "src/data/utils.test.ts",
    "content": "import { ColumnHint, QueryBuilderOptions, QueryType, TimeUnit } from 'types/queryBuilder';\nimport {\n  applyTraceSearchFieldConfig,\n  columnLabelToPlaceholder,\n  dataFrameHasLogLabelWithName,\n  isBuilderOptionsRunnable,\n  labelsFieldName,\n  transformQueryResponseWithTraceAndLogLinks,\n  tryApplyColumnHints,\n} from './utils';\nimport { newMockDatasource } from '__mocks__/datasource';\nimport { CoreApp, DataFrame, DataQueryRequest, DataQueryResponse, Field, FieldType } from '@grafana/data';\nimport { CHBuilderQuery, CHQuery, EditorType } from 'types/sql';\nimport { Datasource } from './CHDatasource';\nimport otel from 'otel';\n\ndescribe('isBuilderOptionsRunnable', () => {\n  it('should return false for empty builder options', () => {\n    const opts: QueryBuilderOptions = {\n      database: 'default',\n      table: 'test',\n      queryType: QueryType.Table,\n    };\n\n    const runnable = isBuilderOptionsRunnable(opts);\n    expect(runnable).toBe(false);\n  });\n\n  it('should return true for valid builder options', () => {\n    const opts: QueryBuilderOptions = {\n      database: 'default',\n      table: 'test',\n      queryType: QueryType.Table,\n      columns: [{ name: 'valid_column' }],\n    };\n\n    const runnable = isBuilderOptionsRunnable(opts);\n    expect(runnable).toBe(true);\n  });\n});\n\ndescribe('tryApplyColumnHints', () => {\n  it('does not apply hints when queryType and hint map are not provided', () => {\n    const columns = [\n      { name: 'a', alias: undefined, hint: undefined },\n      { name: 'b', alias: undefined, hint: undefined },\n    ];\n\n    tryApplyColumnHints(columns);\n\n    expect(columns[0].hint).toBeUndefined();\n    expect(columns[1].hint).toBeUndefined();\n  });\n\n  it('applies time hint to columns that contain \"time\"', () => {\n    const columns = [\n      { name: 'Timestamp', alias: undefined, hint: undefined },\n      { name: 'log_timestamp', alias: undefined, hint: undefined },\n    ];\n\n    tryApplyColumnHints(columns);\n\n    expect(columns[0].hint).toEqual(ColumnHint.Time);\n    expect(columns[1].hint).toEqual(ColumnHint.Time);\n  });\n\n  it('does not apply hints to column with existing hint', () => {\n    const columns = [{ name: 'time', alias: undefined, hint: ColumnHint.TraceServiceName }];\n\n    tryApplyColumnHints(columns);\n\n    expect(columns[0].hint).toEqual(ColumnHint.TraceServiceName);\n  });\n\n  it('applies hints by column name according to hint map, ignoring case', () => {\n    const columns = [\n      { name: 'Super_Custom_Timestamp', alias: undefined, hint: undefined },\n      { name: 'LogLevel', alias: undefined, hint: undefined },\n    ];\n    const hintMap: Map<ColumnHint, string> = new Map([\n      [ColumnHint.Time, 'super_custom_timestamp'],\n      [ColumnHint.LogLevel, 'LogLevel'],\n    ]);\n\n    tryApplyColumnHints(columns, hintMap);\n\n    expect(columns[0].hint).toEqual(ColumnHint.Time);\n    expect(columns[1].hint).toEqual(ColumnHint.LogLevel);\n  });\n\n  it('applies hints by column alias according to hint map, ignoring case', () => {\n    const columns = [\n      { name: 'other name', alias: 'Super_Custom_Timestamp', hint: undefined },\n      { name: 'other name', alias: 'LogLevel', hint: undefined },\n    ];\n    const hintMap: Map<ColumnHint, string> = new Map([\n      [ColumnHint.Time, 'super_custom_timestamp'],\n      [ColumnHint.LogLevel, 'LogLevel'],\n    ]);\n\n    tryApplyColumnHints(columns, hintMap);\n\n    expect(columns[0].hint).toEqual(ColumnHint.Time);\n    expect(columns[1].hint).toEqual(ColumnHint.LogLevel);\n  });\n});\n\ndescribe('columnLabelToPlaceholder', () => {\n  it('converts to lowercase and removes multiple spaces', () => {\n    const expected = 'expected_test_output';\n    const actual = columnLabelToPlaceholder('Expected TEST output');\n    expect(actual).toEqual(expected);\n  });\n});\n\ndescribe('applyTraceSearchFieldConfig', () => {\n  const buildTraceSearchRequestResponse = (\n    fields: Field[],\n    builderOptions: Partial<QueryBuilderOptions> = {}\n  ): [DataQueryRequest<CHQuery>, DataQueryResponse] => {\n    const inputQuery: CHBuilderQuery = {\n      refId: 'A',\n      editorType: EditorType.Builder,\n      builderOptions: {\n        database: 'default',\n        table: 'otel_traces',\n        queryType: QueryType.Traces,\n        ...builderOptions,\n      },\n      pluginVersion: '',\n      rawSql: '',\n    };\n\n    const request: DataQueryRequest<CHQuery> = {\n      requestId: '',\n      interval: '',\n      intervalMs: 0,\n      range: {} as any,\n      scopedVars: {} as any,\n      targets: [inputQuery],\n      timezone: '',\n      app: CoreApp.Explore,\n      startTime: 0,\n    };\n\n    const data: DataFrame[] = [{\n      fields,\n      length: 1,\n      refId: 'A',\n    }];\n    const response: DataQueryResponse = { data };\n\n    return [request, response];\n  };\n\n  it('applies field configs to trace search result fields', () => {\n    const fields: Field[] = [\n      { name: 'traceID', type: FieldType.string, config: {}, values: [] },\n      { name: 'serviceName', type: FieldType.string, config: {}, values: [] },\n      { name: 'operationName', type: FieldType.string, config: {}, values: [] },\n      { name: 'startTime', type: FieldType.time, config: {}, values: [] },\n      { name: 'duration', type: FieldType.number, config: {}, values: [] },\n    ];\n\n    const [request, response] = buildTraceSearchRequestResponse(fields);\n    applyTraceSearchFieldConfig(request, response);\n\n    expect(response.data[0].fields[4].config.unit).toBe('ms');\n    expect(response.data[0].fields[4].config.displayName).toBe('Duration');\n    expect(response.data[0].fields[0].config.displayName).toBe('Trace ID');\n    expect(response.data[0].fields[1].config.displayName).toBe('Service Name');\n    expect(response.data[0].fields[2].config.displayName).toBe('Operation Name');\n    expect(response.data[0].fields[3].config.displayName).toBe('Start Time');\n  });\n\n  it('does not apply field configs to trace ID mode queries', () => {\n    const fields: Field[] = [\n      { name: 'duration', type: FieldType.number, config: {}, values: [] },\n    ];\n\n    const [request, response] = buildTraceSearchRequestResponse(fields, {\n      meta: { isTraceIdMode: true, traceId: 'abc123' },\n    });\n    applyTraceSearchFieldConfig(request, response);\n\n    expect(response.data[0].fields[0].config.unit).toBeUndefined();\n  });\n\n  it('does not apply field configs to non-trace queries', () => {\n    const fields: Field[] = [\n      { name: 'duration', type: FieldType.number, config: {}, values: [] },\n    ];\n\n    const [request, response] = buildTraceSearchRequestResponse(fields, {\n      queryType: QueryType.Table,\n    });\n    applyTraceSearchFieldConfig(request, response);\n\n    expect(response.data[0].fields[0].config.unit).toBeUndefined();\n  });\n\n  it('preserves existing field config properties', () => {\n    const fields: Field[] = [\n      { name: 'duration', type: FieldType.number, config: { decimals: 2 }, values: [] },\n    ];\n\n    const [request, response] = buildTraceSearchRequestResponse(fields);\n    applyTraceSearchFieldConfig(request, response);\n\n    expect(response.data[0].fields[0].config.unit).toBe('ms');\n    expect(response.data[0].fields[0].config.decimals).toBe(2);\n  });\n\n  it('does not modify fields that have no matching config', () => {\n    const fields: Field[] = [\n      { name: 'customColumn', type: FieldType.string, config: {}, values: [] },\n    ];\n\n    const [request, response] = buildTraceSearchRequestResponse(fields);\n    applyTraceSearchFieldConfig(request, response);\n\n    expect(response.data[0].fields[0].config).toEqual({});\n  });\n});\n\ndescribe('transformQueryResponseWithTraceAndLogLinks', () => {\n  const buildTestRequestResponse = (\n    builderOptions: Partial<QueryBuilderOptions>\n  ): [DataQueryRequest<CHQuery>, DataQueryResponse] => {\n    const inputQuery: CHBuilderQuery = {\n      refId: 'A',\n      editorType: EditorType.Builder,\n      builderOptions: {\n        database: '',\n        table: '',\n        queryType: QueryType.Traces,\n        ...builderOptions,\n      },\n      pluginVersion: '',\n      rawSql: '',\n    };\n\n    const request: DataQueryRequest<CHQuery> = {\n      requestId: '',\n      interval: '',\n      intervalMs: 0,\n      range: {} as any,\n      scopedVars: {} as any,\n      targets: [inputQuery],\n      timezone: '',\n      app: CoreApp.Explore,\n      startTime: 0,\n    };\n\n    const field: Field = {\n      name: 'traceID',\n      type: FieldType.string,\n      config: {},\n      values: [],\n    };\n    const data: DataFrame[] = [\n      {\n        fields: [field],\n        length: 1,\n        refId: 'A',\n      },\n    ];\n    const response: DataQueryResponse = { data };\n\n    return [request, response];\n  };\n\n  it('inserts links into trace query. Copy trace columns, default log columns.', async () => {\n    const mockDatasource = newMockDatasource();\n    const getDefaultTraceDatabase = jest.spyOn(mockDatasource, 'getDefaultTraceDatabase');\n    const getDefaultTraceTable = jest.spyOn(mockDatasource, 'getDefaultTraceTable');\n    const getDefaultTraceColumns = jest.spyOn(mockDatasource, 'getDefaultTraceColumns');\n    const getDefaultLogsDatabase = jest.spyOn(mockDatasource, 'getDefaultLogsDatabase');\n    const getDefaultLogsTable = jest.spyOn(mockDatasource, 'getDefaultLogsTable');\n    const getDefaultLogsColumns = jest.spyOn(mockDatasource, 'getDefaultLogsColumns');\n\n    const builderOptions: Partial<QueryBuilderOptions> = {\n      queryType: QueryType.Traces,\n      columns: [{ name: 'a' }],\n    };\n\n    const [request, response] = buildTestRequestResponse(builderOptions);\n    const out = transformQueryResponseWithTraceAndLogLinks(mockDatasource, request, response);\n\n    const links = out?.data[0]?.fields[0]?.config?.links;\n    expect(links).not.toBeUndefined();\n    expect(links).toHaveLength(2);\n    expect(getDefaultTraceDatabase).not.toHaveBeenCalled();\n    expect(getDefaultTraceTable).not.toHaveBeenCalled();\n    expect(getDefaultTraceColumns).not.toHaveBeenCalled();\n    expect(getDefaultLogsDatabase).toHaveBeenCalled();\n    expect(getDefaultLogsTable).toHaveBeenCalled();\n    expect(getDefaultLogsColumns).toHaveBeenCalled();\n  });\n\n  it('inserts links into logs query. Copy logs columns, default trace columns.', async () => {\n    const mockDatasource = newMockDatasource();\n    const getDefaultTraceDatabase = jest.spyOn(mockDatasource, 'getDefaultTraceDatabase');\n    const getDefaultTraceTable = jest.spyOn(mockDatasource, 'getDefaultTraceTable');\n    const getDefaultTraceColumns = jest.spyOn(mockDatasource, 'getDefaultTraceColumns');\n    const getDefaultLogsDatabase = jest.spyOn(mockDatasource, 'getDefaultLogsDatabase');\n    const getDefaultLogsTable = jest.spyOn(mockDatasource, 'getDefaultLogsTable');\n    const getDefaultLogsColumns = jest.spyOn(mockDatasource, 'getDefaultLogsColumns');\n    const getDefaultTraceEventsColumnPrefix = jest.spyOn(mockDatasource, 'getDefaultTraceEventsColumnPrefix');\n    const getDefaultTraceLinksColumnPrefix = jest.spyOn(mockDatasource, 'getDefaultTraceLinksColumnPrefix');\n\n    const builderOptions: Partial<QueryBuilderOptions> = {\n      queryType: QueryType.Logs,\n    };\n\n    const [request, response] = buildTestRequestResponse(builderOptions);\n    const out = transformQueryResponseWithTraceAndLogLinks(mockDatasource, request, response);\n\n    const links = out?.data[0]?.fields[0]?.config?.links;\n    expect(links).not.toBeUndefined();\n    expect(links).toHaveLength(2);\n    expect(getDefaultTraceDatabase).toHaveBeenCalled();\n    expect(getDefaultTraceTable).toHaveBeenCalled();\n    expect(getDefaultTraceColumns).toHaveBeenCalled();\n    expect(getDefaultLogsDatabase).not.toHaveBeenCalled();\n    expect(getDefaultLogsTable).not.toHaveBeenCalled();\n    // getDefaultLogsColumns is now called to get traceIdColumnName for correlation\n    expect(getDefaultLogsColumns).toHaveBeenCalled();\n    expect(getDefaultTraceEventsColumnPrefix).toHaveBeenCalled();\n    expect(getDefaultTraceLinksColumnPrefix).toHaveBeenCalled();\n  });\n\n  it('includes TraceId filter in View logs link query using configured column', async () => {\n    const mockDatasource = newMockDatasource();\n    // Mock that TraceId is configured\n    jest.spyOn(mockDatasource, 'getDefaultLogsColumns').mockReturnValue(new Map([[ColumnHint.TraceId, 'TraceId']]));\n\n    const builderOptions: Partial<QueryBuilderOptions> = {\n      queryType: QueryType.Traces,\n      columns: [{ name: 'a' }],\n    };\n\n    const [request, response] = buildTestRequestResponse(builderOptions);\n    const out = transformQueryResponseWithTraceAndLogLinks(mockDatasource, request, response);\n\n    const links = out?.data[0]?.fields[0]?.config?.links;\n    const viewLogsLink = links?.find((link: any) => link.title === 'View logs');\n\n    const logsQuery = viewLogsLink?.internal?.query as CHBuilderQuery;\n    expect(logsQuery.builderOptions.columns).toBeDefined();\n\n    // TraceId column should be in the columns array\n    const traceIdColumn = logsQuery.builderOptions.columns?.find((c) => c.hint === ColumnHint.TraceId);\n    expect(traceIdColumn).toBeDefined();\n    expect(traceIdColumn?.name).toBe('TraceId');\n\n    // Filter should have the TraceId hint and column name as key\n    const traceIdFilter = logsQuery.builderOptions.filters?.find((f) => (f as any).hint === ColumnHint.TraceId) as any;\n    expect(traceIdFilter).toBeDefined();\n    expect(traceIdFilter.key).toBe('TraceId');\n  });\n\n  describe('trace ID link rawSql pre-generation', () => {\n    const newOtelMockDatasource = (): Datasource => {\n      const ds = newMockDatasource();\n      (ds as any).settings = {\n        ...((ds as any).settings || {}),\n        jsonData: {\n          ...(((ds as any).settings || {}).jsonData || {}),\n          defaultDatabase: 'otel',\n          traces: {\n            defaultDatabase: 'otel',\n            defaultTable: 'otel_traces',\n            otelEnabled: true,\n            otelVersion: 'latest',\n            durationUnit: TimeUnit.Nanoseconds,\n          },\n        },\n      };\n      return ds;\n    };\n\n    it('trace→trace link has pre-generated rawSql with _trace_id_ts optimization', () => {\n      const mockDatasource = newOtelMockDatasource();\n      const otelConfig = otel.getVersion('latest')!;\n      const columns = Array.from(otelConfig.traceColumnMap, ([hint, name]) => ({ name, hint }));\n\n      const builderOptions: Partial<QueryBuilderOptions> = {\n        database: 'otel',\n        table: 'otel_traces',\n        queryType: QueryType.Traces,\n        columns,\n        meta: {\n          otelEnabled: true,\n          otelVersion: 'latest',\n          traceDurationUnit: TimeUnit.Nanoseconds,\n          hasTraceTimestampTable: true,\n        },\n      };\n\n      const [request, response] = buildTestRequestResponse(builderOptions);\n      const out = transformQueryResponseWithTraceAndLogLinks(mockDatasource, request, response);\n\n      const links = out?.data[0]?.fields[0]?.config?.links;\n      const viewTraceLink = links?.find((link: any) => link.title === 'View trace');\n      const traceQuery = viewTraceLink?.internal?.query as CHBuilderQuery;\n\n      expect(traceQuery.rawSql).not.toBe('');\n      expect(traceQuery.rawSql).toContain('otel_traces_trace_id_ts');\n      expect(traceQuery.rawSql).toContain('trace_start');\n      expect(traceQuery.rawSql).toContain('trace_end');\n      expect(traceQuery.builderOptions.meta?.hasTraceTimestampTable).toBe(true);\n    });\n\n    it('logs→trace link with OTel sets hasTraceTimestampTable and generates optimized rawSql', () => {\n      const mockDatasource = newOtelMockDatasource();\n\n      const builderOptions: Partial<QueryBuilderOptions> = {\n        queryType: QueryType.Logs,\n      };\n\n      const [request, response] = buildTestRequestResponse(builderOptions);\n      const out = transformQueryResponseWithTraceAndLogLinks(mockDatasource, request, response);\n\n      const links = out?.data[0]?.fields[0]?.config?.links;\n      const viewTraceLink = links?.find((link: any) => link.title === 'View trace');\n      const traceQuery = viewTraceLink?.internal?.query as CHBuilderQuery;\n\n      expect(traceQuery.builderOptions.meta?.otelEnabled).toBe(true);\n      expect(traceQuery.builderOptions.meta?.hasTraceTimestampTable).toBe(true);\n      expect(traceQuery.rawSql).not.toBe('');\n      expect(traceQuery.rawSql).toContain('otel_traces_trace_id_ts');\n      expect(traceQuery.rawSql).toContain('trace_start');\n      expect(traceQuery.rawSql).toContain('trace_end');\n    });\n\n    it('logs→trace link without OTel generates rawSql without _trace_id_ts optimization', () => {\n      const mockDatasource = newMockDatasource();\n\n      const builderOptions: Partial<QueryBuilderOptions> = {\n        queryType: QueryType.Logs,\n      };\n\n      const [request, response] = buildTestRequestResponse(builderOptions);\n      const out = transformQueryResponseWithTraceAndLogLinks(mockDatasource, request, response);\n\n      const links = out?.data[0]?.fields[0]?.config?.links;\n      const viewTraceLink = links?.find((link: any) => link.title === 'View trace');\n      const traceQuery = viewTraceLink?.internal?.query as CHBuilderQuery;\n\n      expect(traceQuery.rawSql).not.toBe('');\n      expect(traceQuery.rawSql).not.toContain('trace_id_ts');\n      expect(traceQuery.builderOptions.meta?.hasTraceTimestampTable).toBeFalsy();\n    });\n\n    it('trace→trace link without hasTraceTimestampTable generates rawSql without optimization', () => {\n      const mockDatasource = newOtelMockDatasource();\n      const otelConfig = otel.getVersion('latest')!;\n      const columns = Array.from(otelConfig.traceColumnMap, ([hint, name]) => ({ name, hint }));\n\n      const builderOptions: Partial<QueryBuilderOptions> = {\n        database: 'otel',\n        table: 'otel_traces',\n        queryType: QueryType.Traces,\n        columns,\n        meta: {\n          otelEnabled: true,\n          otelVersion: 'latest',\n          traceDurationUnit: TimeUnit.Nanoseconds,\n          hasTraceTimestampTable: false,\n        },\n      };\n\n      const [request, response] = buildTestRequestResponse(builderOptions);\n      const out = transformQueryResponseWithTraceAndLogLinks(mockDatasource, request, response);\n\n      const links = out?.data[0]?.fields[0]?.config?.links;\n      const viewTraceLink = links?.find((link: any) => link.title === 'View trace');\n      const traceQuery = viewTraceLink?.internal?.query as CHBuilderQuery;\n\n      expect(traceQuery.rawSql).not.toBe('');\n      expect(traceQuery.rawSql).not.toContain('trace_id_ts');\n      expect(traceQuery.builderOptions.meta?.hasTraceTimestampTable).toBe(false);\n    });\n  });\n\n  it('does not inject \"View trace\" link when showTraceLinks is false', async () => {\n    const mockDatasource = newMockDatasource();\n    mockDatasource.settings.jsonData.traces = { showTraceLinks: false };\n\n    const builderOptions: Partial<QueryBuilderOptions> = {\n      queryType: QueryType.Traces,\n      columns: [{ name: 'a' }],\n    };\n\n    const [request, response] = buildTestRequestResponse(builderOptions);\n    const out = transformQueryResponseWithTraceAndLogLinks(mockDatasource, request, response);\n\n    const links = out?.data[0]?.fields[0]?.config?.links;\n    expect(links).toBeDefined();\n    expect(links?.find((link: any) => link.title === 'View trace')).toBeUndefined();\n    expect(links?.find((link: any) => link.title === 'View logs')).toBeDefined();\n  });\n\n  it('does not inject \"View logs\" link when showLogLinks is false', async () => {\n    const mockDatasource = newMockDatasource();\n    mockDatasource.settings.jsonData.logs = { showLogLinks: false };\n\n    const builderOptions: Partial<QueryBuilderOptions> = {\n      queryType: QueryType.Traces,\n      columns: [{ name: 'a' }],\n    };\n\n    const [request, response] = buildTestRequestResponse(builderOptions);\n    const out = transformQueryResponseWithTraceAndLogLinks(mockDatasource, request, response);\n\n    const links = out?.data[0]?.fields[0]?.config?.links;\n    expect(links).toBeDefined();\n    expect(links?.find((link: any) => link.title === 'View trace')).toBeDefined();\n    expect(links?.find((link: any) => link.title === 'View logs')).toBeUndefined();\n  });\n});\n\ndescribe('dataFrameHasLogLabelWithName', () => {\n  it('should return false for undefined dataframe', () => {\n    expect(dataFrameHasLogLabelWithName(undefined, 'testLabel')).toBe(false);\n  });\n\n  it('should return false for dataframe with no fields', () => {\n    const frame: DataFrame = { fields: [] } as any as DataFrame;\n    expect(dataFrameHasLogLabelWithName(frame, 'testLabel')).toBe(false);\n  });\n\n  it('should return false when log labels field is not present', () => {\n    const frame: DataFrame = {\n      fields: [{ name: 'otherField', values: { get: jest.fn(), length: 1 } }],\n    } as any as DataFrame;\n    expect(dataFrameHasLogLabelWithName(frame, 'testLabel')).toBe(false);\n  });\n\n  it('should return false when log labels field has no values', () => {\n    const frame: DataFrame = {\n      fields: [{ name: labelsFieldName, values: { get: jest.fn(), length: 0 } }],\n    } as any as DataFrame;\n    expect(dataFrameHasLogLabelWithName(frame, 'testLabel')).toBe(false);\n  });\n\n  it('should return false when log labels field value is null', () => {\n    const frame: DataFrame = {\n      fields: [{ name: labelsFieldName, values: { get: () => null, length: 1 } }],\n    } as any as DataFrame;\n    expect(dataFrameHasLogLabelWithName(frame, 'testLabel')).toBe(false);\n  });\n\n  it('should return true when log label with given name exists', () => {\n    const frame: DataFrame = {\n      fields: [\n        {\n          name: labelsFieldName,\n          values: { get: () => ({ testLabel: 'value', otherLabel: 'otherValue' }), length: 1 },\n        },\n      ],\n    } as any as DataFrame;\n    expect(dataFrameHasLogLabelWithName(frame, 'testLabel')).toBe(true);\n  });\n\n  it('should return false when log label with given name does not exist', () => {\n    const frame: DataFrame = {\n      fields: [\n        {\n          name: labelsFieldName,\n          values: { get: () => ({ otherLabel: 'value' }), length: 1 },\n        },\n      ],\n    } as any as DataFrame;\n    expect(dataFrameHasLogLabelWithName(frame, 'testLabel')).toBe(false);\n  });\n});\n"
  },
  {
    "path": "src/data/utils.ts",
    "content": "import { CoreApp, DataFrame, DataQueryRequest, DataQueryResponse, FieldConfig } from '@grafana/data';\nimport {\n  ColumnHint,\n  FilterOperator,\n  OrderByDirection,\n  QueryBuilderOptions,\n  QueryType,\n  SelectedColumn,\n  StringFilter,\n} from 'types/queryBuilder';\nimport { CHBuilderQuery, CHQuery, EditorType } from 'types/sql';\nimport { Datasource } from './CHDatasource';\nimport { pluginVersion } from 'utils/version';\nimport { generateSql } from './sqlGenerator';\nimport otel from 'otel';\n\n/**\n * Returns true if the builder options contain enough information to start showing a query\n */\nexport const isBuilderOptionsRunnable = (builderOptions: QueryBuilderOptions): boolean => {\n  return (\n    (builderOptions.columns?.length || 0) > 0 ||\n    (builderOptions.filters?.length || 0) > 0 ||\n    (builderOptions.orderBy?.length || 0) > 0 ||\n    (builderOptions.aggregates?.length || 0) > 0 ||\n    (builderOptions.groupBy?.length || 0) > 0\n  );\n};\n\n/**\n * Converts QueryBuilderOptions to Grafana format\n * src: https://github.com/grafana/sqlds/blob/main/query.go#L20\n */\nexport const mapQueryBuilderOptionsToGrafanaFormat = (t?: QueryBuilderOptions): number => {\n  switch (t?.queryType) {\n    case QueryType.Table:\n      return 1;\n    case QueryType.Logs:\n      return 2;\n    case QueryType.TimeSeries:\n      return 0;\n    case QueryType.Traces:\n      return t.meta?.isTraceIdMode ? 3 : 1;\n    default:\n      return 1 << 8; // an unused u32, defaults to timeseries/graph on plugin backend.\n  }\n};\n\n/**\n * Converts QueryType to Grafana format\n * src: https://github.com/grafana/sqlds/blob/main/query.go#L20\n */\nexport const mapQueryTypeToGrafanaFormat = (t?: QueryType): number => {\n  switch (t) {\n    case QueryType.Table:\n      return 1;\n    case QueryType.Logs:\n      return 2;\n    case QueryType.TimeSeries:\n      return 0;\n    case QueryType.Traces:\n      return 3;\n    default:\n      return 1 << 8; // an unused u32, defaults to timeseries/graph on plugin backend.\n  }\n};\n\n/**\n * Converts Grafana format to builder QueryType\n * src: https://github.com/grafana/sqlds/blob/main/query.go#L20\n */\nexport const mapGrafanaFormatToQueryType = (f?: number): QueryType => {\n  switch (f) {\n    case 0:\n      return QueryType.TimeSeries;\n    case 1:\n      return QueryType.Table;\n    case 2:\n      return QueryType.Logs;\n    case 3:\n      return QueryType.Traces;\n    default:\n      return QueryType.Table;\n  }\n};\n\n/**\n * Manipulates column array in-place to include column hints, loosely matched by the provided column hint map.\n */\nexport const tryApplyColumnHints = (columns: SelectedColumn[], hintsToColumns?: Map<ColumnHint, string>) => {\n  const columnsToHints: Map<string, ColumnHint> = new Map();\n  if (hintsToColumns) {\n    hintsToColumns.forEach((name, hint) => {\n      columnsToHints.set(name.toLowerCase().trim(), hint);\n    });\n  }\n\n  for (const column of columns) {\n    if (column.hint) {\n      continue;\n    }\n\n    const name = column.name.toLowerCase().trim();\n    const alias = column.alias?.toLowerCase().trim() || '';\n\n    const hint = columnsToHints.get(name) || columnsToHints.get(alias);\n    if (hint) {\n      column.hint = hint;\n      continue;\n    }\n\n    if (name.includes('time')) {\n      column.hint = ColumnHint.Time;\n    }\n  }\n};\n\n/**\n * Converts label into sql-style column name.\n * Example: \"Test Column\" -> \"test_column\"\n */\nexport const columnLabelToPlaceholder = (label: string) => label.toLowerCase().replace(/ /g, '_');\n\n/**\n * Field config map for trace search result columns.\n * Maps column name (lowercase) to Grafana FieldConfig for better default display.\n */\nconst traceSearchFieldConfigs: Record<string, FieldConfig> = {\n  duration: {\n    unit: 'ms',\n    displayName: 'Duration',\n  },\n  starttime: {\n    displayName: 'Start Time',\n  },\n  servicename: {\n    displayName: 'Service Name',\n  },\n  operationname: {\n    displayName: 'Operation Name',\n  },\n  traceid: {\n    displayName: 'Trace ID',\n  },\n};\n\n/**\n * Applies field configs to trace search result frames for better default display.\n * Trace search results are table-format frames from trace queries (non-traceIdMode).\n */\nexport const applyTraceSearchFieldConfig = (req: DataQueryRequest<CHQuery>, res: DataQueryResponse): void => {\n  res.data.forEach((frame: DataFrame) => {\n    const originalQuery = req.targets.find((t) => t.refId === frame.refId) as CHBuilderQuery;\n    if (!originalQuery) {\n      return;\n    }\n\n    const isTraceSearch = originalQuery.editorType === EditorType.Builder &&\n      originalQuery.builderOptions.queryType === QueryType.Traces &&\n      !originalQuery.builderOptions.meta?.isTraceIdMode;\n\n    if (!isTraceSearch) {\n      return;\n    }\n\n    frame.fields.forEach((field) => {\n      const fieldConfig = traceSearchFieldConfigs[field.name.toLowerCase()];\n      if (fieldConfig) {\n        field.config = {\n          ...field.config,\n          ...fieldConfig,\n        };\n      }\n    });\n  });\n};\n\n/**\n * Mutates the DataQueryResponse to include trace/log links on the traceID field.\n * The link will open a second query editor in split view\n * on the explore page with the selected trace ID.\n *\n * Requires defaults to be configured when crossing query types.\n */\nexport const transformQueryResponseWithTraceAndLogLinks = (\n  datasource: Datasource,\n  req: DataQueryRequest<CHQuery>,\n  res: DataQueryResponse\n): DataQueryResponse => {\n  applyTraceSearchFieldConfig(req, res);\n\n  res.data.forEach((frame: DataFrame) => {\n    const originalQuery = req.targets.find((t) => t.refId === frame.refId) as CHBuilderQuery;\n    if (!originalQuery) {\n      return;\n    }\n\n    const traceField = frame.fields.find(\n      (field) => field.name.toLowerCase() === 'traceid' || field.name.toLowerCase() === 'trace_id'\n    );\n    if (!traceField) {\n      return;\n    }\n\n    // Get the configured TraceId column name for use in both trace and logs queries\n    const defaultLogsColumns = datasource.getDefaultLogsColumns();\n    // Use traces config traceIdColumn if available, otherwise fallback to logs default\n    const traceIdColumnName =\n      datasource.getTracesTraceIdColumn() || defaultLogsColumns.get(ColumnHint.TraceId) || 'TraceId';\n\n    const traceIdQuery: CHBuilderQuery = {\n      datasource: datasource,\n      editorType: EditorType.Builder,\n      rawSql: '',\n      builderOptions: {} as QueryBuilderOptions,\n      pluginVersion,\n      refId: 'Trace ID',\n    };\n\n    const traceTimestampTableSuffix = datasource.getTraceTimestampTableSuffix();\n\n    if (\n      originalQuery.editorType === EditorType.Builder &&\n      originalQuery.builderOptions.queryType === QueryType.Traces\n    ) {\n      // Copy fields directly from trace search\n\n      traceIdQuery.builderOptions = {\n        ...originalQuery.builderOptions,\n        filters: [], // Clear filters and orderBy since it's an exact ID lookup\n        orderBy: [],\n        meta: {\n          ...originalQuery.builderOptions.meta,\n          minimized: true,\n          isTraceIdMode: true,\n          traceId: '${__value.raw}',\n          traceTimestampTableSuffix:\n            originalQuery.builderOptions.meta?.traceTimestampTableSuffix || traceTimestampTableSuffix,\n        },\n      };\n    } else {\n      // Create new query based on trace defaults\n\n      const otelVersion = datasource.getTraceOtelVersion();\n      const otelConfig = otel.getVersion(otelVersion);\n      const traceEventsColumnPrefix = datasource.getDefaultTraceEventsColumnPrefix();\n      const traceLinksColumnPrefix = datasource.getDefaultTraceLinksColumnPrefix();\n      const options: QueryBuilderOptions = {\n        database:\n          datasource.getDefaultTraceDatabase() ||\n          traceIdQuery.builderOptions.database ||\n          datasource.getDefaultDatabase(),\n        table: datasource.getDefaultTraceTable() || datasource.getDefaultTable() || traceIdQuery.builderOptions.table,\n        queryType: QueryType.Traces,\n        columns: [],\n        filters: [],\n        orderBy: [],\n        meta: {\n          minimized: true,\n          isTraceIdMode: true,\n          traceId: '${__value.raw}',\n          traceDurationUnit: datasource.getDefaultTraceDurationUnit(),\n          otelEnabled: Boolean(otelVersion),\n          otelVersion: otelVersion,\n          traceEventsColumnPrefix: traceEventsColumnPrefix,\n          traceLinksColumnPrefix: traceLinksColumnPrefix,\n          hasTraceTimestampTable: Boolean(otelVersion),\n          traceTimestampTableSuffix,\n        },\n      };\n\n      if (otelConfig?.traceColumnMap) {\n        options.columns = Array.from(otelConfig.traceColumnMap, ([hint, name]) => ({ name, hint }));\n      } else {\n        const defaultColumns = datasource.getDefaultTraceColumns();\n        for (let [hint, colName] of defaultColumns) {\n          options.columns!.push({ name: colName, hint });\n        }\n      }\n\n      traceIdQuery.builderOptions = options;\n    }\n\n    // Pre-generate rawSql so the query executes immediately when the link is opened.\n    // Trace ID queries don't contain $__fromTime/$__toTime time macros, so they're\n    // safe to include (unlike trace search queries which would break data link detection).\n    traceIdQuery.rawSql = generateSql(traceIdQuery.builderOptions);\n\n    const traceLogsQuery: CHBuilderQuery = {\n      datasource: datasource,\n      editorType: EditorType.Builder,\n      rawSql: '',\n      builderOptions: {} as QueryBuilderOptions,\n      pluginVersion,\n      refId: 'Trace Logs',\n    };\n\n    if (originalQuery.editorType === EditorType.Builder && originalQuery.builderOptions.queryType === QueryType.Logs) {\n      // Copy fields directly from log search\n      traceLogsQuery.builderOptions = {\n        ...originalQuery.builderOptions,\n        filters: [\n          {\n            type: 'string',\n            operator: FilterOperator.Equals,\n            filterType: 'custom',\n            key: traceIdColumnName,\n            hint: ColumnHint.TraceId,\n            condition: 'AND',\n            value: '${__value.raw}',\n          } as StringFilter,\n        ],\n        orderBy: [{ name: '', hint: ColumnHint.Time, dir: OrderByDirection.ASC }],\n        meta: {\n          ...originalQuery.builderOptions.meta,\n          minimized: true,\n        },\n      };\n    } else {\n      // Create new query based on log defaults\n\n      const otelVersion = datasource.getLogsOtelVersion();\n      const options: QueryBuilderOptions = {\n        database:\n          datasource.getDefaultLogsDatabase() ||\n          traceLogsQuery.builderOptions.database ||\n          datasource.getDefaultDatabase(),\n        table: datasource.getDefaultLogsTable() || datasource.getDefaultTable() || traceLogsQuery.builderOptions.table,\n        queryType: QueryType.Logs,\n        columns: [],\n        orderBy: [{ name: '', hint: ColumnHint.Time, dir: OrderByDirection.ASC }],\n        filters: [\n          {\n            type: 'string',\n            operator: FilterOperator.Equals,\n            filterType: 'custom',\n            key: traceIdColumnName,\n            hint: ColumnHint.TraceId,\n            condition: 'AND',\n            value: '${__value.raw}',\n          } as StringFilter,\n        ],\n        meta: {\n          minimized: true,\n          otelEnabled: Boolean(otelVersion),\n          otelVersion: otelVersion,\n        },\n      };\n\n      for (let [hint, colName] of defaultLogsColumns) {\n        options.columns!.push({ name: colName, hint });\n      }\n\n      // Ensure TraceId column is in the array so filter can find it via hint lookup\n      if (!options.columns!.find((c) => c.hint === ColumnHint.TraceId)) {\n        options.columns!.push({ name: traceIdColumnName, hint: ColumnHint.TraceId });\n      }\n\n      traceLogsQuery.builderOptions = options;\n    }\n\n    // Generate rawSql for Dashboard mode to preserve query through serialization\n    const openInNewWindow = req.app !== CoreApp.Explore;\n    if (openInNewWindow) {\n      traceLogsQuery.rawSql = generateSql(traceLogsQuery.builderOptions || {});\n    } else {\n      traceLogsQuery.rawSql = '';\n    }\n    traceField.config.links = [];\n    if (datasource.settings.jsonData.traces?.showTraceLinks !== false) {\n      traceField.config.links!.push({\n        title: 'View trace',\n        targetBlank: openInNewWindow,\n        url: '',\n        internal: {\n          query: traceIdQuery,\n          datasourceUid: traceIdQuery.datasource?.uid!,\n          datasourceName: traceIdQuery.datasource?.type!,\n          panelsState: {\n            trace: {\n              spanId: '${__value.raw}',\n            },\n          },\n        },\n      });\n    }\n    if (datasource.settings.jsonData.logs?.showLogLinks !== false) {\n      traceField.config.links!.push({\n        title: 'View logs',\n        targetBlank: openInNewWindow,\n        url: '',\n        internal: {\n          query: traceLogsQuery,\n          datasourceUid: traceLogsQuery.datasource?.uid!,\n          datasourceName: traceLogsQuery.datasource?.type!,\n        },\n      });\n    }\n  });\n\n  return res;\n};\n\n// The name of the dataframe field containing labels\nexport const labelsFieldName = 'labels';\n\n/**\n * Returns true if the dataframe contains a log label that matches the provided name.\n *\n * This function exists for the logs panel, when clicking \"filter for value\" on a single log row.\n * A dataframe will be provided for that single row, and we need to check the labels object to see if it\n * contains a field with that name. If it does then we can create a filter using the labels column hint.\n */\nexport const dataFrameHasLogLabelWithName = (frame: DataFrame | undefined, name: string): boolean => {\n  if (!frame || !frame.fields || frame.fields.length === 0) {\n    return false;\n  }\n\n  const field = frame.fields.find((f) => f.name === labelsFieldName);\n  if (!field || !field.values || field.values.length < 1 || !field.values.get(0)) {\n    return false;\n  }\n\n  const labels = (field.values.get(0) || {}) as object;\n  const labelKeys = Object.keys(labels);\n\n  return labelKeys.includes(name);\n};\n"
  },
  {
    "path": "src/data/validate.test.ts",
    "content": "import { validate } from './validate';\n\ndescribe('Validate', () => {\n  describe('valid SQL', () => {\n    it('handles a basic SELECT', () => {\n      expect(validate('SELECT foo FROM bar').valid).toBe(true);\n    });\n\n    it('handles ClickHouse FINAL keyword', () => {\n      expect(validate('SELECT * FROM table FINAL').valid).toBe(true);\n    });\n\n    it('handles ClickHouse PREWHERE', () => {\n      expect(validate('SELECT * FROM t PREWHERE x > 1 WHERE y > 2').valid).toBe(true);\n    });\n\n    it('handles ClickHouse ARRAY JOIN', () => {\n      expect(validate('SELECT * FROM t ARRAY JOIN arr').valid).toBe(true);\n    });\n\n    it('handles ClickHouse SETTINGS', () => {\n      expect(validate(\"SELECT * FROM t SETTINGS max_rows_to_read = 1000\").valid).toBe(true);\n    });\n\n    it('handles ClickHouse GLOBAL IN', () => {\n      expect(validate('SELECT * FROM t WHERE id GLOBAL IN (SELECT id FROM t2)').valid).toBe(true);\n    });\n\n    it('handles ClickHouse ASOF JOIN', () => {\n      expect(validate('SELECT * FROM t1 ASOF JOIN t2 ON t1.id = t2.id').valid).toBe(true);\n    });\n\n    it('handles ClickHouse :: cast operator', () => {\n      expect(validate(\"SELECT '2024-01-01'::DateTime FROM t\").valid).toBe(true);\n    });\n\n    it('handles Grafana $__timeFilter macro', () => {\n      expect(validate('SELECT * FROM t WHERE $__timeFilter(timestamp)').valid).toBe(true);\n    });\n\n    it('handles Grafana $__interval macro', () => {\n      expect(validate('SELECT toStartOfInterval(ts, INTERVAL $__interval second) FROM t').valid).toBe(true);\n    });\n\n    it('handles Grafana ${variable} template variables', () => {\n      expect(validate('SELECT * FROM t WHERE service = ${service}').valid).toBe(true);\n    });\n\n    it('handles single-line comments', () => {\n      expect(validate('SELECT * FROM t -- this is a comment').valid).toBe(true);\n    });\n\n    it('handles block comments', () => {\n      expect(validate('SELECT /* comment */ * FROM t').valid).toBe(true);\n    });\n\n    it('handles hex numbers', () => {\n      expect(validate('SELECT 0xFF FROM t').valid).toBe(true);\n    });\n\n    it('handles Unicode smart single quotes as string literals', () => {\n      // U+2018 … U+2019 around \"hello\"\n      expect(validate('SELECT \\u2018hello\\u2019 FROM t').valid).toBe(true);\n    });\n\n    it('handles Unicode smart double quotes as identifiers', () => {\n      // U+201C … U+201D around \"name\"\n      expect(validate('SELECT \\u201Cname\\u201D FROM t').valid).toBe(true);\n    });\n\n    it('handles Unicode minus sign (U+2212) as a minus operator', () => {\n      expect(validate('SELECT 1 \\u2212 2 FROM t').valid).toBe(true);\n    });\n\n    it('handles heredoc with word-char tag', () => {\n      expect(validate(\"SELECT $foo$ anything goes 'here' $foo$ FROM t\").valid).toBe(true);\n    });\n\n    it('handles heredoc with empty tag', () => {\n      expect(validate('SELECT $$ hello world $$ FROM t').valid).toBe(true);\n    });\n  });\n\n  describe('invalid SQL', () => {\n    it('catches an unclosed single-quoted string', () => {\n      const v = validate(\"SELECT * FROM t WHERE name = 'unclosed\");\n      expect(v.valid).toBe(false);\n      expect(v.error?.message).toBe('Single quoted string is not closed');\n    });\n\n    it('catches an unclosed double-quoted identifier', () => {\n      const v = validate('SELECT \"unclosed FROM t');\n      expect(v.valid).toBe(false);\n      expect(v.error?.message).toBe('Double quoted string is not closed');\n    });\n\n    it('catches an unclosed backtick identifier', () => {\n      const v = validate('SELECT `unclosed FROM t');\n      expect(v.valid).toBe(false);\n      expect(v.error?.message).toBe('Back quoted string is not closed');\n    });\n\n    it('catches an unclosed block comment', () => {\n      const v = validate('SELECT * FROM t /* unclosed comment');\n      expect(v.valid).toBe(false);\n      expect(v.error?.message).toBe('Multiline comment is not closed');\n    });\n\n    it('catches a stray exclamation mark', () => {\n      const v = validate('SELECT * FROM t WHERE x ! 1');\n      expect(v.valid).toBe(false);\n      expect(v.error?.message).toBe('Exclamation mark can only occur in != operator');\n    });\n\n    it('reports the correct line number for an error on line 2', () => {\n      const sql = 'SELECT *\\nFROM t WHERE name = \\'unclosed';\n      const v = validate(sql);\n      expect(v.valid).toBe(false);\n      expect(v.error?.startLine).toBe(2);\n    });\n\n    it('reports the correct column for an error', () => {\n      const sql = \"SELECT 'unclosed\";\n      const v = validate(sql);\n      expect(v.valid).toBe(false);\n      expect(v.error?.startCol).toBe(8); // quote starts at col 8\n    });\n\n    it('catches an unclosed Unicode smart single quote', () => {\n      const v = validate('SELECT \\u2018unclosed FROM t');\n      expect(v.valid).toBe(false);\n      expect(v.error?.message).toBe('Single quoted string is not closed');\n    });\n\n    it('catches an unclosed Unicode smart double quote', () => {\n      const v = validate('SELECT \\u201Cunclosed FROM t');\n      expect(v.valid).toBe(false);\n      expect(v.error?.message).toBe('Double quoted string is not closed');\n    });\n  });\n});\n"
  },
  {
    "path": "src/data/validate.ts",
    "content": "import { Lexer } from 'ch-parser/lexer';\nimport { getErrorTokenDescription } from 'ch-parser/types';\n\nexport interface Error {\n  startLine: number;\n  endLine: number;\n  startCol: number;\n  endCol: number;\n  message: string;\n  expected: string;\n}\n\nexport interface Validation {\n  valid: boolean;\n  error?: Error;\n}\n\nfunction offsetToLineCol(sql: string, offset: number): { line: number; col: number } {\n  const lines = sql.substring(0, offset).split('\\n');\n  return {\n    line: lines.length,\n    col: lines[lines.length - 1].length + 1,\n  };\n}\n\nexport function validate(sql: string): Validation {\n  const lexer = new Lexer(sql);\n  while (true) {\n    const token = lexer.nextToken();\n    if (token.isEnd()) {\n      break;\n    }\n    if (token.isError()) {\n      const start = offsetToLineCol(sql, token.begin);\n      const end = offsetToLineCol(sql, token.end);\n      const description = getErrorTokenDescription(token.type);\n      return {\n        valid: false,\n        error: {\n          startLine: start.line,\n          endLine: end.line,\n          startCol: start.col,\n          endCol: end.col,\n          message: description,\n          expected: description,\n        },\n      };\n    }\n  }\n  return { valid: true };\n}\n"
  },
  {
    "path": "src/hooks/useBuilderOptionChanges.test.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { useBuilderOptionChanges } from './useBuilderOptionChanges';\n\ninterface TestData {\n  x: number;\n  y: number;\n}\n\ndescribe('useBuilderOptionChanges', () => {\n  it('calls onChange with merged object', async () => {\n    const onChange = jest.fn();\n    const prevState: TestData = {\n      x: 1,\n      y: 2,\n    };\n    const hook = renderHook(() => useBuilderOptionChanges<TestData>(onChange, prevState));\n    const applyChanges = hook.result.current;\n\n    expect(applyChanges).not.toBeUndefined();\n    applyChanges('y')(3);\n\n    expect(onChange).toHaveBeenCalledTimes(1);\n    expect(onChange).toHaveBeenCalledWith({ x: 1, y: 3 });\n  });\n});\n"
  },
  {
    "path": "src/hooks/useBuilderOptionChanges.ts",
    "content": "import React from 'react';\n\ntype onOptionChangeFn<T> = (key: keyof T) => (nextValue: React.SetStateAction<any>) => void;\n\n/**\n * Returns a function that can apply changes with an object or a specific key in an object. When called\n * will run another function with the changes applied.\n *\n * Does not deep clone the object. This is used for top level fields on the QueryBuilderOptions type.\n *\n * @param onChange a function that receives the updated state from the change function\n * @param prevState the current (previous) state object\n * @returns a function used to apply changes to individual fields\n */\nexport function useBuilderOptionChanges<T>(onChange: (nextState: T) => void, prevState: T): onOptionChangeFn<T> {\n  return (key: keyof T) => (nextValue: React.SetStateAction<any>) => {\n    const nextState: T = {\n      ...prevState,\n      [key]: nextValue,\n    };\n\n    onChange(nextState);\n  };\n}\n"
  },
  {
    "path": "src/hooks/useBuilderOptionsState.test.ts",
    "content": "import { ColumnHint, QueryType } from 'types/queryBuilder';\nimport {\n  setAllOptions,\n  setBuilderMinimized,\n  setColumnByHint,\n  setDatabase,\n  setOptions,\n  setOtelEnabled,\n  setOtelVersion,\n  setQueryType,\n  setTable,\n  testFuncs,\n} from './useBuilderOptionsState';\nconst { reducer, buildInitialState } = testFuncs;\n\ndescribe('reducer', () => {\n  it('applies SetOptions action', async () => {\n    const prevState = buildInitialState();\n    const action = setOptions({\n      limit: 100,\n      // Include meta to verify deep merge\n      meta: {\n        otelEnabled: true,\n      },\n    });\n\n    const nextState = reducer(prevState, action);\n    expect(nextState.limit).toEqual(100);\n    expect(nextState.meta?.otelEnabled).toEqual(true);\n  });\n  it('applies SetAllOptions action', async () => {\n    const prevState = buildInitialState({\n      limit: 100,\n    });\n    const action = setAllOptions({\n      database: 'default',\n      table: 'test',\n      queryType: QueryType.Table,\n    });\n\n    const nextState = reducer(prevState, action);\n    // SetAllOptions will overwrite with defaults\n    expect(nextState.limit).not.toEqual(100);\n  });\n  it('run SetQueryType action with no changes', async () => {\n    const prevState = buildInitialState({\n      queryType: QueryType.TimeSeries,\n    });\n    const action = setQueryType(QueryType.TimeSeries);\n\n    const nextState = reducer(prevState, action);\n    expect(nextState.queryType).toEqual(QueryType.TimeSeries);\n  });\n  it('applies SetQueryType to reset settings but preserve db/table', async () => {\n    const prevState = buildInitialState({\n      database: 'prev_db',\n      table: 'prev_table',\n      queryType: QueryType.Table,\n      groupBy: ['will', 'be', 'reset'],\n    });\n    const action = setQueryType(QueryType.Logs);\n\n    const nextState = reducer(prevState, action);\n    expect(nextState.database).toEqual('prev_db');\n    expect(nextState.table).toEqual('prev_table');\n    expect(nextState.queryType).toEqual(QueryType.Logs);\n    expect(nextState.groupBy).toBeFalsy();\n  });\n  it('applies SetDatabase to reset settings but preserve query type', async () => {\n    const prevState = buildInitialState({\n      database: 'prev_db',\n      table: 'prev_table',\n      queryType: QueryType.Logs,\n      groupBy: ['will', 'be', 'reset'],\n    });\n    const action = setDatabase('next_db');\n\n    const nextState = reducer(prevState, action);\n    expect(nextState.database).toEqual('next_db');\n    expect(nextState.table).toEqual('');\n    expect(nextState.queryType).toEqual(QueryType.Logs);\n    expect(nextState.groupBy).toBeFalsy();\n  });\n  it('applies SetTable to reset settings but preserve db/queryType', async () => {\n    const prevState = buildInitialState({\n      database: 'prev_db',\n      table: 'prev_table',\n      queryType: QueryType.Logs,\n      groupBy: ['will', 'be', 'reset'],\n    });\n    const action = setTable('next_table');\n\n    const nextState = reducer(prevState, action);\n    expect(nextState.database).toEqual('prev_db');\n    expect(nextState.table).toEqual('next_table');\n    expect(nextState.queryType).toEqual(QueryType.Logs);\n    expect(nextState.groupBy).toBeFalsy();\n  });\n  it('applies SetOtelEnabled action', async () => {\n    const prevState = buildInitialState({\n      limit: 50,\n    });\n    const action = setOtelEnabled(true);\n\n    const nextState = reducer(prevState, action);\n    expect(nextState.limit).toEqual(50);\n    expect(nextState.meta?.otelEnabled).toEqual(true);\n  });\n  it('applies SetOtelVersion action', async () => {\n    const prevState = buildInitialState({\n      limit: 50,\n    });\n    const action = setOtelVersion('0.0.1');\n\n    const nextState = reducer(prevState, action);\n    expect(nextState.limit).toEqual(50);\n    expect(nextState.meta?.otelVersion).toEqual('0.0.1');\n  });\n  it('applies SetColumnByHint action, overwrites existing column', async () => {\n    const prevState = buildInitialState({\n      columns: [{ name: 'prev_timestamp', hint: ColumnHint.Time }, { name: 'a' }, { name: 'b' }, { name: 'c' }],\n    });\n    const action = setColumnByHint({ name: 'next_timestamp', hint: ColumnHint.Time });\n\n    const nextState = reducer(prevState, action);\n    expect(nextState.columns).toHaveLength(4);\n    expect(nextState.columns![0].name).toEqual('a');\n    expect(nextState.columns![1].name).toEqual('b');\n    expect(nextState.columns![2].name).toEqual('c');\n    // Updated column is filtered and pushed to end of array\n    expect(nextState.columns![3].name).toEqual('next_timestamp');\n  });\n  it('applies SetBuilderMinimized action', async () => {\n    const prevState = buildInitialState();\n    const action = setBuilderMinimized(true);\n\n    const nextState = reducer(prevState, action);\n    expect(nextState.meta?.minimized).toBe(true);\n  });\n});\n\ndescribe('buildInitialState', () => {\n  it('builds initial state using defaults', async () => {\n    const state = buildInitialState();\n    expect(state).not.toBeUndefined();\n    expect(state.database).toEqual('');\n    expect(state.table).toEqual('');\n    expect(state.queryType).toEqual(QueryType.Table);\n  });\n\n  it('builds initial state and merge saved state', async () => {\n    const state = buildInitialState({\n      table: 'saved_table',\n      limit: 50,\n      meta: {\n        otelEnabled: true,\n      },\n    });\n    expect(state).not.toBeUndefined();\n    expect(state.database).toEqual('');\n    expect(state.table).toEqual('saved_table');\n    expect(state.limit).toEqual(50);\n    expect(state.queryType).toEqual(QueryType.Table);\n    expect(state.meta?.otelEnabled).toEqual(true);\n  });\n});\n"
  },
  {
    "path": "src/hooks/useBuilderOptionsState.ts",
    "content": "import { Reducer, useReducer } from 'react';\nimport { QueryBuilderOptions, QueryType, SelectedColumn } from 'types/queryBuilder';\nimport { defaultCHBuilderQuery } from 'types/sql';\n\nenum BuilderOptionsActionType {\n  SetOptions = 'set_options',\n  SetAllOptions = 'set_all_options',\n  SetQueryType = 'set_query_type',\n  SetDatabase = 'set_database',\n  SetTable = 'set_table',\n  SetOtelEnabled = 'set_otel_enabled',\n  SetOtelVersion = 'set_otel_version',\n  SetColumnByHint = 'set_column_by_hint',\n  SetBuilderMinimized = 'set_builder_minimized',\n}\n\ntype QueryBuilderOptionsReducerAction = {\n  type: BuilderOptionsActionType;\n  payload: Partial<QueryBuilderOptions>;\n};\n\ntype GenericReducerAction = {\n  type: BuilderOptionsActionType;\n  payload: any;\n};\n\nexport type BuilderOptionsReducerAction = QueryBuilderOptionsReducerAction | GenericReducerAction;\n\nconst createAction = (\n  type: BuilderOptionsActionType,\n  payload: Partial<QueryBuilderOptions>\n): BuilderOptionsReducerAction => ({ type, payload });\nconst createGenericAction = (type: BuilderOptionsActionType, payload: any): GenericReducerAction => ({ type, payload });\nexport const setOptions = (options: Partial<QueryBuilderOptions>): BuilderOptionsReducerAction =>\n  createAction(BuilderOptionsActionType.SetOptions, options);\nexport const setAllOptions = (options: QueryBuilderOptions): BuilderOptionsReducerAction =>\n  createAction(BuilderOptionsActionType.SetAllOptions, options);\nexport const setQueryType = (queryType: QueryType): BuilderOptionsReducerAction =>\n  createAction(BuilderOptionsActionType.SetQueryType, { queryType });\nexport const setDatabase = (database: string): BuilderOptionsReducerAction =>\n  createAction(BuilderOptionsActionType.SetDatabase, { database });\nexport const setTable = (table: string): BuilderOptionsReducerAction =>\n  createAction(BuilderOptionsActionType.SetTable, { table });\nexport const setOtelEnabled = (otelEnabled: boolean): BuilderOptionsReducerAction =>\n  createAction(BuilderOptionsActionType.SetOtelEnabled, { meta: { otelEnabled } });\nexport const setOtelVersion = (otelVersion: string): BuilderOptionsReducerAction =>\n  createAction(BuilderOptionsActionType.SetOtelVersion, { meta: { otelVersion } });\nexport const setColumnByHint = (column: SelectedColumn): GenericReducerAction =>\n  createGenericAction(BuilderOptionsActionType.SetColumnByHint, { column });\nexport const setBuilderMinimized = (minimized: boolean): GenericReducerAction =>\n  createGenericAction(BuilderOptionsActionType.SetBuilderMinimized, { minimized });\n\nconst reducer = (state: QueryBuilderOptions, action: BuilderOptionsReducerAction): QueryBuilderOptions => {\n  const actionFn = actions.get(action.type);\n  if (!actionFn) {\n    throw Error('missing function for BuilderOptionsActionType: ' + action.type);\n  }\n\n  const nextState = actionFn(state, action);\n  // console.log('ACTION:', action.type, 'PAYLOAD:', action.payload, 'PREV STATE:', state, 'NEXT STATE:', nextState);\n  return nextState;\n};\n\n/**\n * A mapping between action type and reducer function, used in reducer to apply action changes.\n */\nconst actions = new Map<BuilderOptionsActionType, Reducer<QueryBuilderOptions, BuilderOptionsReducerAction>>([\n  [\n    BuilderOptionsActionType.SetOptions,\n    (state: QueryBuilderOptions, action: BuilderOptionsReducerAction): QueryBuilderOptions => {\n      // A catch-all action for applying option changes.\n      const nextOptions = action.payload as Partial<QueryBuilderOptions>;\n      return mergeBuilderOptionsState(state, nextOptions);\n    },\n  ],\n  [\n    BuilderOptionsActionType.SetAllOptions,\n    (state: QueryBuilderOptions, action: BuilderOptionsReducerAction): QueryBuilderOptions => {\n      // Resets existing state with provided options.\n      const nextOptions = action.payload as Partial<QueryBuilderOptions>;\n      return buildInitialState(nextOptions);\n    },\n  ],\n  [\n    BuilderOptionsActionType.SetQueryType,\n    (state: QueryBuilderOptions, action: BuilderOptionsReducerAction): QueryBuilderOptions => {\n      // If switching query type, reset the editor.\n      const nextQueryType = action.payload.queryType;\n      if (state.queryType !== nextQueryType) {\n        return buildInitialState({\n          database: state.database,\n          table: state.table,\n          queryType: nextQueryType,\n        });\n      }\n\n      return state;\n    },\n  ],\n  [\n    BuilderOptionsActionType.SetDatabase,\n    (state: QueryBuilderOptions, action: BuilderOptionsReducerAction): QueryBuilderOptions => {\n      // Clear table and reset editor when database changes\n      return buildInitialState({\n        database: action.payload.database,\n        table: '',\n        queryType: state.queryType,\n      });\n    },\n  ],\n  [\n    BuilderOptionsActionType.SetTable,\n    (state: QueryBuilderOptions, action: BuilderOptionsReducerAction): QueryBuilderOptions => {\n      // Reset editor when table changes\n      return buildInitialState({\n        database: state.database,\n        table: action.payload.table,\n        queryType: state.queryType,\n      });\n    },\n  ],\n  [\n    BuilderOptionsActionType.SetOtelEnabled,\n    (state: QueryBuilderOptions, action: BuilderOptionsReducerAction): QueryBuilderOptions => {\n      return mergeBuilderOptionsState(state, {\n        meta: {\n          otelEnabled: Boolean(action.payload.meta?.otelEnabled),\n        },\n      });\n    },\n  ],\n  [\n    BuilderOptionsActionType.SetOtelVersion,\n    (state: QueryBuilderOptions, action: BuilderOptionsReducerAction): QueryBuilderOptions => {\n      return mergeBuilderOptionsState(state, {\n        meta: {\n          otelVersion: action.payload.meta?.otelVersion,\n        },\n      });\n    },\n  ],\n  [\n    BuilderOptionsActionType.SetColumnByHint,\n    (state: QueryBuilderOptions, action: GenericReducerAction): QueryBuilderOptions => {\n      const col = action.payload.column as SelectedColumn;\n      const nextColumns = (state.columns || []).filter((c) => c.hint !== col.hint);\n      nextColumns.push(col);\n\n      return mergeBuilderOptionsState(state, {\n        columns: nextColumns,\n      });\n    },\n  ],\n  [\n    BuilderOptionsActionType.SetBuilderMinimized,\n    (state: QueryBuilderOptions, action: GenericReducerAction): QueryBuilderOptions => {\n      const minimized = action.payload.minimized as boolean;\n      return mergeBuilderOptionsState(state, {\n        meta: { minimized },\n      });\n    },\n  ],\n]);\n\nconst buildInitialState = (savedOptions?: Partial<QueryBuilderOptions>): QueryBuilderOptions => {\n  const defaultOptions = defaultCHBuilderQuery.builderOptions;\n  const initialState = {\n    ...defaultOptions,\n    ...savedOptions,\n    meta: {\n      ...defaultOptions.meta,\n      ...savedOptions?.meta,\n    },\n  };\n\n  return initialState;\n};\n\nconst mergeBuilderOptionsState = (\n  prevState: QueryBuilderOptions,\n  nextState: Partial<QueryBuilderOptions>\n): QueryBuilderOptions => {\n  return {\n    ...prevState,\n    ...nextState,\n    meta: {\n      ...prevState.meta,\n      ...nextState.meta,\n    },\n  };\n};\n\nexport const useBuilderOptionsState = (\n  savedOptions: QueryBuilderOptions\n): [QueryBuilderOptions, React.Dispatch<BuilderOptionsReducerAction>] => {\n  const [state, dispatch] = useReducer(reducer, savedOptions, buildInitialState);\n  return [state, dispatch];\n};\n\nexport const testFuncs = {\n  reducer,\n  buildInitialState,\n};\n"
  },
  {
    "path": "src/hooks/useColumns.test.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { act } from 'react';\nimport { Datasource } from 'data/CHDatasource';\nimport useColumns from './useColumns';\nimport { TableColumn } from 'types/queryBuilder';\n\ndescribe('useColumns', () => {\n  it('should return empty array if datasource is invalid', async () => {\n    let result: { current: readonly TableColumn[] };\n    await act(async () => {\n      const r = renderHook(() => useColumns(undefined!, 'db', 'table'));\n      result = r.result;\n    });\n\n    expect(result!.current).toHaveLength(0);\n  });\n\n  it('should return empty array if database string is empty', async () => {\n    const mockDs = {} as Datasource;\n    mockDs.fetchColumns = jest.fn((db: string, table: string) => Promise.resolve([]));\n    let result: { current: readonly TableColumn[] };\n    await act(async () => {\n      const r = renderHook(() => useColumns(mockDs, '', 'table'));\n      result = r.result;\n    });\n\n    expect(result!.current).toHaveLength(0);\n  });\n\n  it('should return empty array if table string is empty', async () => {\n    const mockDs = {} as Datasource;\n    mockDs.fetchColumns = jest.fn((db: string, table: string) => Promise.resolve([]));\n    let result: { current: readonly TableColumn[] };\n    await act(async () => {\n      const r = renderHook(() => useColumns(mockDs, 'db', ''));\n      result = r.result;\n    });\n\n    expect(result!.current).toHaveLength(0);\n  });\n\n  it('should fetch table columns', async () => {\n    const mockDs = {} as Datasource;\n    mockDs.fetchColumns = jest.fn((db: string, table: string) =>\n      Promise.resolve([\n        { name: 'a', type: 'string', picklistValues: [] },\n        { name: 'b', type: 'string', picklistValues: [] },\n        // { name: '*' } (an \"all\" column is added by the hook)\n      ])\n    );\n\n    let result: { current: readonly TableColumn[] };\n    await act(async () => {\n      const r = renderHook(() => useColumns(mockDs, 'db', 'table'));\n      result = r.result;\n    });\n\n    expect(result!.current).toHaveLength(2);\n  });\n});\n"
  },
  {
    "path": "src/hooks/useColumns.ts",
    "content": "import { useState, useEffect, useRef } from 'react';\nimport { TableColumn } from 'types/queryBuilder';\nimport { Datasource } from 'data/CHDatasource';\n\nexport default (datasource: Datasource, database: string, table: string): readonly TableColumn[] => {\n  const [columns, setColumns] = useState<readonly TableColumn[]>([]);\n\n  useEffect(() => {\n    if (!datasource || !database || !table) {\n      return;\n    }\n\n    let ignore = false;\n    datasource\n      .fetchColumns(database, table)\n      .then((columns) => {\n        if (ignore) {\n          return;\n        }\n        setColumns(columns);\n      })\n      .catch((ex: any) => {\n        console.error(ex);\n      });\n\n    return () => {\n      ignore = true;\n    };\n  }, [datasource, database, table]);\n\n  // Immediately return empty array on change so columns aren't stale\n  const lastDbTable = useRef<string>('');\n  const dbTable = database + table;\n  if (dbTable !== lastDbTable.current) {\n    lastDbTable.current = dbTable;\n    setColumns([]);\n    return [];\n  }\n\n  return columns;\n};\n"
  },
  {
    "path": "src/hooks/useDatabases.test.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { act } from 'react';\nimport { Datasource } from 'data/CHDatasource';\nimport useDatabases from './useDatabases';\n\ndescribe('useDatabases', () => {\n  it('should return empty array if invalid datasource is provided', async () => {\n    let result: { current: readonly string[] };\n    await act(async () => {\n      const r = renderHook(() => useDatabases(undefined!));\n      result = r.result;\n    });\n\n    expect(result!.current).toHaveLength(0);\n  });\n\n  it('should fetch databases', async () => {\n    const mockDs = {} as Datasource;\n    mockDs.fetchDatabases = jest.fn(() => Promise.resolve(['a', 'b']));\n\n    let result: { current: readonly string[] };\n    await act(async () => {\n      const r = renderHook(() => useDatabases(mockDs));\n      result = r.result;\n    });\n\n    expect(result!.current).toHaveLength(2);\n  });\n});\n"
  },
  {
    "path": "src/hooks/useDatabases.ts",
    "content": "import { useState, useEffect } from 'react';\nimport { Datasource } from 'data/CHDatasource';\n\nexport default (datasource: Datasource): readonly string[] => {\n  const [databases, setDatabases] = useState<string[]>([]);\n\n  useEffect(() => {\n    if (!datasource) {\n      return;\n    }\n\n    datasource\n      .fetchDatabases()\n      .then((databases) => setDatabases(databases))\n      .catch((ex: any) => {\n        console.error('Failed to fetch databases', ex);\n      });\n  }, [datasource]);\n\n  return databases;\n};\n"
  },
  {
    "path": "src/hooks/useIsNewQuery.test.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport useIsNewQuery from './useIsNewQuery';\nimport { QueryBuilderOptions, QueryType } from 'types/queryBuilder';\n\ndescribe('useIsNewQuery', () => {\n  const newQueryOpts: QueryBuilderOptions = {\n    database: 'default',\n    table: 'test',\n    queryType: QueryType.Table,\n  };\n\n  const existingQueryOpts: QueryBuilderOptions = {\n    database: 'default',\n    table: 'test',\n    queryType: QueryType.Table,\n    columns: [{ name: 'valid_column' }],\n  };\n\n  it('should return true when new query is provided', async () => {\n    const hook = renderHook(() => useIsNewQuery(newQueryOpts));\n    expect(hook.result.current).toBe(true);\n  });\n\n  it('should return false when existing query is provided', async () => {\n    const hook = renderHook(() => useIsNewQuery(existingQueryOpts));\n    expect(hook.result.current).toBe(false);\n  });\n\n  it('should continue to return true when new query is updated', async () => {\n    const hook = renderHook((opts) => useIsNewQuery(opts), { initialProps: newQueryOpts });\n    const firstResult = hook.result.current;\n    hook.rerender(existingQueryOpts);\n    const secondResult = hook.result.current;\n\n    expect(firstResult).toBe(true);\n    expect(secondResult).toBe(true);\n  });\n\n  it('should continue to return false when existing query is updated', async () => {\n    const hook = renderHook((opts) => useIsNewQuery(opts), { initialProps: existingQueryOpts });\n    const firstResult = hook.result.current;\n    hook.rerender(existingQueryOpts);\n    const secondResult = hook.result.current;\n\n    expect(firstResult).toBe(false);\n    expect(secondResult).toBe(false);\n  });\n});\n"
  },
  {
    "path": "src/hooks/useIsNewQuery.ts",
    "content": "import { isBuilderOptionsRunnable } from 'data/utils';\nimport { useRef } from 'react';\nimport { QueryBuilderOptions } from 'types/queryBuilder';\n\n/**\n * Returns true if the initial builderOptions represent a new query.\n * Returns false if the query was loaded from a saved URL or dashboard.\n *\n * Does not update on re-renders\n */\nexport default (builderOptions: QueryBuilderOptions): boolean => {\n  const isNewQuery = useRef<boolean>(!isBuilderOptionsRunnable(builderOptions));\n  return isNewQuery.current;\n};\n"
  },
  {
    "path": "src/hooks/useSchemaSuggestionsProvider.ts",
    "content": "import { Schema } from 'components/suggestions';\nimport { Datasource } from 'data/CHDatasource';\nimport { useRef } from 'react';\nimport { SqlFunction, TableColumn } from 'types/queryBuilder';\n\nexport interface SchemaCache {\n  functions: SqlFunction[] | null;\n  databases: string[] | null;\n  tables: Map<string, string[]>;\n  columns: Map<string, TableColumn[]>;\n}\n\nfunction defaultSchemaCache(): SchemaCache {\n  return {\n    functions: null,\n    databases: null,\n    tables: new Map<string, string[]>(),\n    columns: new Map<string, TableColumn[]>(),\n  };\n}\n\n/**\n * Provides an interface for the auto-complete to read schema data from.\n * This data is cached since the auto-complete is always looking for schema data.\n *\n * Sometimes another CH datasource's suggestions will show up.\n * There's no way to detect this (tried using datasource.uid), it could be monaco caching suggestions since it does show a mix\n */\nexport function useSchemaSuggestionsProvider(datasource: Datasource): Schema {\n  const cache = useRef<SchemaCache>(defaultSchemaCache());\n\n  async function fetchFunctions() {\n    if (cache.current.functions === null) {\n      cache.current.functions = await datasource.fetchSqlFunctions();\n    }\n\n    return cache.current.functions;\n  }\n\n  async function fetchDatabases() {\n    if (cache.current.databases === null) {\n      cache.current.databases = await datasource.fetchDatabases();\n    }\n\n    return cache.current.databases;\n  }\n\n  async function fetchTables(db?: string) {\n    if (db === undefined) {\n      db = '';\n    }\n\n    if (!cache.current.tables.has(db)) {\n      const tables = await datasource.fetchTables(db);\n      cache.current.tables.set(db, tables);\n\n      return tables;\n    }\n\n    return cache.current.tables.get(db)!;\n  }\n\n  async function fetchColumns(db: string, table: string) {\n    const key = `${db || ''}.${table || ''}`;\n\n    if (!cache.current.columns.has(key)) {\n      const columns = await datasource.fetchColumnsFromTable(db, table);\n      cache.current.columns.set(key, columns);\n\n      return columns;\n    }\n\n    return cache.current.columns.get(key)!;\n  }\n\n  return {\n    functions: fetchFunctions,\n    databases: fetchDatabases,\n    tables: fetchTables,\n    columns: fetchColumns,\n    defaultDatabase: datasource.getDefaultDatabase(),\n  };\n}\n"
  },
  {
    "path": "src/hooks/useTables.test.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { act } from 'react';\nimport { Datasource } from 'data/CHDatasource';\nimport useTables from './useTables';\n\ndescribe('useTables', () => {\n  it('should return empty array if invalid datasource is provided', async () => {\n    let result: { current: readonly string[] };\n    await act(async () => {\n      const r = renderHook(() => useTables(undefined!, 'db'));\n      result = r.result;\n    });\n\n    expect(result!.current).toHaveLength(0);\n  });\n\n  it('should return empty array if empty database string is provided', async () => {\n    const mockDs = {} as Datasource;\n    mockDs.fetchTables = jest.fn((db: string) => Promise.resolve(['a', 'b']));\n\n    let result: { current: readonly string[] };\n    await act(async () => {\n      const r = renderHook(() => useTables(mockDs, ''));\n      result = r.result;\n    });\n\n    expect(result!.current).toHaveLength(0);\n  });\n\n  it('should fetch tables', async () => {\n    const mockDs = {} as Datasource;\n    mockDs.fetchTables = jest.fn((db: string) => Promise.resolve(['a', 'b']));\n\n    let result: { current: readonly string[] };\n    await act(async () => {\n      const r = renderHook(() => useTables(mockDs, 'db'));\n      result = r.result;\n    });\n\n    expect(result!.current).toHaveLength(2);\n  });\n});\n"
  },
  {
    "path": "src/hooks/useTables.ts",
    "content": "import { useState, useEffect, useRef } from 'react';\nimport { Datasource } from 'data/CHDatasource';\n\nexport default (datasource: Datasource, database: string): readonly string[] => {\n  const [tables, setTables] = useState<string[]>([]);\n\n  useEffect(() => {\n    if (!datasource || !database) {\n      return;\n    }\n\n    let ignore = false;\n    datasource\n      .fetchTables(database)\n      .then((tables) => {\n        if (ignore) {\n          return;\n        }\n        setTables(tables);\n      })\n      .catch((ex: any) => {\n        console.error('Failed to fetch tables for database:', database, ex);\n      });\n\n    return () => {\n      ignore = true;\n    };\n  }, [datasource, database]);\n\n  // Immediately return empty array on change so tables aren't stale\n  const lastDatabase = useRef<string>('');\n  if (database !== lastDatabase.current) {\n    lastDatabase.current = database;\n    setTables([]);\n    return [];\n  }\n\n  return tables;\n};\n"
  },
  {
    "path": "src/hooks/useUniqueMapKeys.test.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { act } from 'react';\nimport { Datasource } from 'data/CHDatasource';\nimport useUniqueMapKeys from './useUniqueMapKeys';\n\ndescribe('useUniqueMapKeys', () => {\n  it('should return empty array if invalid datasource is provided', async () => {\n    let result: { current: readonly string[] };\n    await act(async () => {\n      const r = renderHook(() => useUniqueMapKeys(undefined!, 'col', 'db', 'table'));\n      result = r.result;\n    });\n\n    expect(result!.current).toHaveLength(0);\n  });\n\n  it('should return empty array if empty column string is provided', async () => {\n    const mockDs = {} as Datasource;\n\n    let result: { current: readonly string[] };\n    await act(async () => {\n      const r = renderHook(() => useUniqueMapKeys(mockDs, '', 'db', 'table'));\n      result = r.result;\n    });\n\n    expect(result!.current).toHaveLength(0);\n  });\n\n  it('should return empty array if empty database string is provided', async () => {\n    const mockDs = {} as Datasource;\n\n    let result: { current: readonly string[] };\n    await act(async () => {\n      const r = renderHook(() => useUniqueMapKeys(mockDs, 'col', '', 'table'));\n      result = r.result;\n    });\n\n    expect(result!.current).toHaveLength(0);\n  });\n\n  it('should return empty array if empty table string is provided', async () => {\n    const mockDs = {} as Datasource;\n\n    let result: { current: readonly string[] };\n    await act(async () => {\n      const r = renderHook(() => useUniqueMapKeys(mockDs, 'col', 'db', ''));\n      result = r.result;\n    });\n\n    expect(result!.current).toHaveLength(0);\n  });\n\n  it('should fetch unique map keys', async () => {\n    const mockDs = {} as Datasource;\n    mockDs.fetchUniqueMapKeys = jest.fn((col: string, db: string, table: string) => Promise.resolve(['a', 'b']));\n\n    let result: { current: readonly string[] };\n    await act(async () => {\n      const r = renderHook(() => useUniqueMapKeys(mockDs, 'col', 'db', 'table'));\n      result = r.result;\n    });\n\n    expect(result!.current).toHaveLength(2);\n  });\n});\n"
  },
  {
    "path": "src/hooks/useUniqueMapKeys.ts",
    "content": "import { useState, useEffect, useRef } from 'react';\nimport { Datasource } from 'data/CHDatasource';\n\nexport default (datasource: Datasource, mapColumn: string, database: string, table: string): readonly string[] => {\n  const [keys, setKeys] = useState<string[]>([]);\n\n  useEffect(() => {\n    if (!datasource || !mapColumn || !database || !table) {\n      return;\n    }\n\n    let ignore = false;\n    datasource\n      .fetchUniqueMapKeys(mapColumn, database, table)\n      .then((tables) => {\n        if (ignore) {\n          return;\n        }\n        setKeys(tables);\n      })\n      .catch((ex: any) => {\n        console.error('Failed to fetch map keys for column:', mapColumn, database, table, ex);\n      });\n\n    return () => {\n      ignore = true;\n    };\n  }, [datasource, mapColumn, database, table]);\n\n  // Immediately return empty array on change so keys aren't stale\n  const lastDatabase = useRef<string>('');\n  const lastTable = useRef<string>('');\n  if (database !== lastDatabase.current || table !== lastTable.current) {\n    lastDatabase.current = database;\n    lastTable.current = table;\n    setKeys([]);\n    return [];\n  }\n\n  return keys;\n};\n"
  },
  {
    "path": "src/labels.ts",
    "content": "import { ColumnHint } from 'types/queryBuilder';\n\nexport default {\n  components: {\n    Config: {\n      ConfigEditor: {\n        serverAddress: {\n          label: 'Server address',\n          placeholder: 'Server address',\n          tooltip: 'ClickHouse host address',\n          error: 'Server address required',\n        },\n        serverPort: {\n          label: 'Server port',\n          insecureNativePort: '9000',\n          insecureHttpPort: '8123',\n          secureNativePort: '9440',\n          secureHttpPort: '8443',\n          tooltip: 'ClickHouse server port',\n          error: 'Port is required',\n        },\n        path: {\n          label: 'HTTP URL Path',\n          tooltip: 'Additional URL path for HTTP requests',\n          placeholder: 'additional-path',\n        },\n        protocol: {\n          label: 'Protocol',\n          tooltip: 'Native or HTTP for server protocol',\n        },\n        username: {\n          label: 'Username',\n          placeholder: 'default',\n          tooltip: 'ClickHouse username',\n        },\n        password: {\n          label: 'Password',\n          placeholder: 'password',\n          tooltip: 'ClickHouse password',\n        },\n        tlsSkipVerify: {\n          label: 'Skip TLS Verify',\n          tooltip: 'Skip TLS Verify',\n        },\n        tlsClientAuth: {\n          label: 'TLS Client Auth',\n          tooltip: 'TLS Client Auth',\n        },\n        tlsAuthWithCACert: {\n          label: 'With CA Cert',\n          tooltip: 'Needed for verifying self-signed TLS Certs',\n        },\n        tlsCACert: {\n          label: 'CA Cert',\n          placeholder: 'CA Cert. Begins with -----BEGIN CERTIFICATE-----',\n        },\n        tlsClientCert: {\n          label: 'Client Cert',\n          placeholder: 'Client Cert. Begins with -----BEGIN CERTIFICATE-----',\n        },\n        tlsClientKey: {\n          label: 'Client Key',\n          placeholder: 'Client Key. Begins with -----BEGIN RSA PRIVATE KEY-----',\n        },\n        secure: {\n          label: 'Secure Connection',\n          tooltip: 'Toggle on if the connection is secure',\n        },\n        secureSocksProxy: {\n          label: 'Enable Secure Socks Proxy',\n          tooltip: 'Enable proxying the datasource connection through the secure socks proxy to a different network.',\n        },\n        enableRowLimit: {\n          label: 'Enable row limit',\n          testid: 'data-testid enable-row-limit-switch',\n          tooltip:\n            'Enable using the Grafana row limit setting to limit the number of rows returned from Clickhouse. Ensure the appropriate permissions are set for your user. Only supported for Grafana >= 11.0.0. Defaults to false.',\n        },\n        hideTableNameInAdhocFilters: {\n          label: 'Hide table name in ad hoc filters',\n          testid: 'data-testid hide-table-name-in-adhoc-filters-switch',\n          tooltip:\n            'Show only column names in ad hoc filter keys instead of the full \"table.column\" format. This simplifies the filter interface when working with schemas that have many tables. Defaults to false.',\n        },\n      },\n      HttpHeadersConfig: {\n        title: 'HTTP Headers',\n        label: 'Custom HTTP Headers',\n        description: 'Add Custom HTTP headers when querying the database',\n        headerNameLabel: 'Header Name',\n        headerNamePlaceholder: 'X-Custom-Header',\n        insecureHeaderValueLabel: 'Header Value',\n        secureHeaderValueLabel: 'Secure Header Value',\n        secureLabel: 'Secure',\n        addHeaderLabel: 'Add Header',\n        forwardGrafanaHeaders: {\n          label: 'Forward Grafana HTTP Headers',\n          tooltip: 'Forward Grafana HTTP Headers to datasource.',\n        },\n      },\n      AliasTableConfig: {\n        title: 'Column Alias Tables',\n        descriptionParts: [\n          'Provide alias tables with a',\n          '(`alias` String, `select` String, `type` String)',\n          'schema to use as a source for column selection.',\n        ],\n        addTableLabel: 'Add Table',\n        targetDatabaseLabel: 'Target Database',\n        targetDatabasePlaceholder: '(optional)',\n        targetTableLabel: 'Target Table',\n        aliasDatabaseLabel: 'Alias Database',\n        aliasDatabasePlaceholder: '(optional)',\n        aliasTableLabel: 'Alias Table',\n      },\n\n      DefaultDatabaseTableConfig: {\n        title: 'Default DB and table',\n        database: {\n          label: 'Default database',\n          description: 'the default database used by the query builder',\n          name: 'defaultDatabase',\n          placeholder: 'default',\n        },\n        table: {\n          label: 'Default table',\n          description: 'the default table used by the query builder',\n          name: 'defaultTable',\n          placeholder: 'table',\n        },\n      },\n      QuerySettingsConfig: {\n        title: 'Query settings',\n        connMaxLifetime: {\n          label: 'Connection Max Lifetime (minutes)',\n          name: 'connMaxLifetime',\n          placeholder: '5',\n          tooltip: 'Maximum lifetime of a connection in minutes',\n        },\n        dialTimeout: {\n          label: 'Dial Timeout (seconds)',\n          name: 'dialTimeout',\n          placeholder: '10',\n          tooltip: 'Timeout in seconds for connection',\n        },\n        maxIdleConns: {\n          label: 'Max Idle Connections',\n          name: 'maxIdleConns',\n          placeholder: '25',\n          tooltip: 'Maximum number of idle connections',\n        },\n        maxOpenConns: {\n          label: 'Max Open Connections',\n          name: 'maxOpenConns',\n          placeholder: '50',\n          tooltip: 'Maximum number of open connections',\n        },\n        queryTimeout: {\n          label: 'Query Timeout (seconds)',\n          name: 'queryTimeout',\n          placeholder: '60',\n          tooltip: 'Timeout in seconds for read queries',\n        },\n        validateSql: {\n          label: 'Validate SQL',\n          tooltip: 'Validate SQL in the editor.',\n        },\n      },\n      TracesConfig: {\n        title: 'Traces configuration',\n        description: '(Optional) Default settings for trace queries',\n        defaultDatabase: {\n          label: 'Default trace database',\n          description: 'the default database used by the trace query builder',\n          name: 'defaultDatabase',\n          placeholder: 'default',\n        },\n        defaultTable: {\n          label: 'Default trace table',\n          description: 'the default table used by the trace query builder',\n          name: 'defaultTable',\n        },\n        columns: {\n          title: 'Default columns',\n          description: 'Default columns for trace queries. Leave empty to disable.',\n\n          traceId: {\n            label: 'Trace ID column',\n            tooltip: 'Column for the trace ID',\n          },\n          spanId: {\n            label: 'Span ID column',\n            tooltip: 'Column for the span ID',\n          },\n          parentSpanId: {\n            label: 'Parent Span ID column',\n            tooltip: 'Column for the parent span ID',\n          },\n          serviceName: {\n            label: 'Service Name column',\n            tooltip: 'Column for the service name',\n          },\n          operationName: {\n            label: 'Operation Name column',\n            tooltip: 'Column for the operation name',\n          },\n          startTime: {\n            label: 'Start Time column',\n            tooltip: 'Column for the start time',\n          },\n          durationTime: {\n            label: 'Duration Time column',\n            tooltip: 'Column for the duration time',\n          },\n          tags: {\n            label: 'Tags column',\n            tooltip: 'Column for the trace tags',\n          },\n          serviceTags: {\n            label: 'Service Tags column',\n            tooltip: 'Column for the service tags',\n          },\n          flattenNested: {\n            label: 'Use Flatten Nested',\n            tooltip: 'Enable if your traces table was created with flatten_nested=1',\n          },\n          eventsPrefix: {\n            label: 'Events prefix',\n            tooltip: 'Prefix for the events column (Events.Timestamp, Events.Name, etc.)',\n          },\n          linksPrefix: {\n            label: 'Links prefix',\n            tooltip: 'Prefix for the trace references column (Links.TraceId, Links.TraceState, etc.)',\n          },\n          traceTimestampTableSuffix: {\n            label: 'Trace timestamp table suffix',\n            tooltip:\n              'Suffix appended to the traces table name to locate a companion index keyed by TraceId with Start/End columns. When such a table exists, trace ID lookups narrow the main query to a small time window instead of scanning the whole table. Leave blank to use the OTel default (_trace_id_ts).',\n          },\n          kind: {\n            label: 'Kind column',\n            tooltip: 'Column for the trace kind',\n          },\n          statusCode: {\n            label: 'Status Code column',\n            tooltip: 'Column for the trace status code',\n          },\n          statusMessage: {\n            label: 'Status Message column',\n            tooltip: 'Column for the trace status message',\n          },\n          instrumentationLibraryName: {\n            label: 'Library Name column',\n            tooltip: 'Column for the instrumentation library name',\n          },\n          instrumentationLibraryVersion: {\n            label: 'Library Version column',\n            tooltip: 'Column for the instrumentation library version',\n          },\n          state: {\n            label: 'State column',\n            tooltip: 'Column for the trace state',\n          },\n        },\n        traceIdCorrelation: {\n          title: 'Trace ID correlation',\n          description: 'Options for showing links to correlated data.',\n\n          showTraceLinks: {\n            label: 'Show \"View trace\" links',\n            tooltip: 'Show \"View trace\" links on trace_id/traceid fields.',\n          },\n        },\n      },\n      LogsConfig: {\n        title: 'Logs configuration',\n        description: '(Optional) default settings for log queries',\n        defaultDatabase: {\n          label: 'Default log database',\n          description: 'the default database used by the logs query builder',\n          name: 'defaultDatabase',\n          placeholder: 'default',\n        },\n        defaultTable: {\n          label: 'Default log table',\n          description: 'the default table used by the logs query builder',\n          name: 'defaultTable',\n        },\n        columns: {\n          title: 'Default columns',\n          description: 'Default columns for log queries. Leave empty to disable.',\n\n          filterTime: {\n            label: 'Filter Time column',\n            tooltip: 'A lower precision column for filtering logs by timestamp',\n          },\n          time: {\n            label: 'Time column',\n            tooltip: 'Column for the log timestamp, used for high precision sorting',\n          },\n          level: {\n            label: 'Log Level column',\n            tooltip: 'Column for the log level',\n          },\n          message: {\n            label: 'Log Message column',\n            tooltip: 'Column for log message',\n          },\n        },\n        traceIdCorrelation: {\n          title: 'Trace ID correlation',\n          description: 'Options for showing links to correlated data.',\n\n          showLogLinks: {\n            label: 'Show \"View logs\" links',\n            tooltip: 'Show \"View logs\" links on trace_id/traceid fields.',\n          },\n        },\n        contextColumns: {\n          title: 'Context columns',\n          description:\n            'These columns are used to narrow down a single log row to its original service/container/pod source. At least one is required for the log context feature to work.',\n\n          selectContextColumns: {\n            label: 'Auto-Select Columns',\n            tooltip: 'When enabled, will always include context columns in log queries',\n          },\n          columns: {\n            label: 'Context Columns',\n            tooltip: \"Comma separated list of column names to use for identifying a log's source\",\n            placeholder: 'Column name (enter key to add)',\n          },\n        },\n      },\n    },\n    EditorTypeSwitcher: {\n      label: 'Editor Type',\n      tooltip: 'Switches between the raw SQL Editor and the Query Builder.',\n      switcher: {\n        title: 'Are you sure?',\n        body: 'Queries that are too complex for the Query Builder will be altered.',\n        confirmText: 'Continue',\n        dismissText: 'Cancel',\n      },\n      cannotConvert: {\n        title: 'Cannot convert',\n        message: 'Do you want to delete your current query and use the query builder?',\n        confirmText: 'Yes',\n      },\n    },\n    expandBuilderButton: {\n      label: 'Show full query',\n      tooltip: 'Shows the full query builder',\n    },\n    QueryTypeSwitcher: {\n      label: 'Query Type',\n      tooltip: 'Sets the layout for the query builder',\n      sqlTooltip: 'Sets the panel type for explore view',\n    },\n    DatabaseSelect: {\n      label: 'Database',\n      tooltip: 'ClickHouse database to query from',\n      empty: '<select database>',\n    },\n    TableSelect: {\n      label: 'Table',\n      tooltip: 'ClickHouse table to query from',\n      empty: '<select table>',\n    },\n    ColumnsEditor: {\n      label: 'Columns',\n      tooltip: 'A list of columns to include in the query',\n    },\n    OtelVersionSelect: {\n      label: 'Use OTel',\n      tooltip: 'Enables Open Telemetry schema versioning',\n    },\n    LimitEditor: {\n      label: 'Limit',\n      tooltip: 'Limits the number of rows returned by the query',\n    },\n    SqlPreview: {\n      label: 'SQL Preview',\n      tooltip: 'Preview of the generated SQL. You can safely switch to SQL Editor to customize the generated query',\n    },\n    AggregatesEditor: {\n      label: 'Aggregates',\n      tooltip: 'Aggregate functions to use',\n      aliasLabel: 'as',\n      aliasTooltip: 'alias for this aggregate function',\n      addLabel: 'Aggregate',\n    },\n    OrderByEditor: {\n      label: 'Order By',\n      tooltip: 'Order by column',\n      addLabel: 'Order By',\n    },\n    FilterEditor: {\n      label: 'Filters',\n      tooltip: `List of filters`,\n      addLabel: 'Filter',\n      mapKeyPlaceholder: 'map key',\n    },\n    GroupByEditor: {\n      label: 'Group By',\n      tooltip: 'Group the results by specific column',\n    },\n    LogsQueryBuilder: {\n      columnsHelp: {\n        text: 'Map your table columns to the roles the logs panel expects. Each column is aliased in the generated SQL.',\n        linkText: 'Learn about column roles',\n        href: 'https://grafana.com/docs/plugins/grafana-clickhouse-datasource/latest/query-editor/#column-roles',\n      },\n      logTimeColumn: {\n        label: 'Time',\n        tooltip:\n          'Primary log timestamp. Aliased to `timestamp` in the generated SQL. Common names: Timestamp, timestamp, event_time, @timestamp, created_at. OTel: Timestamp.',\n      },\n      logLevelColumn: {\n        label: 'Log Level',\n        tooltip:\n          'Log severity. Aliased to `level` in the generated SQL. Common names: level, severity, severity_text, log_level. OTel: SeverityText.',\n      },\n      logMessageColumn: {\n        label: 'Message',\n        tooltip:\n          'Log message body. Aliased to `body` in the generated SQL. Common names: message, msg, body, log_message. OTel: Body.',\n      },\n      liveView: {\n        label: 'Live View',\n        tooltip: 'Enable to update logs in real time',\n      },\n      logMessageFilter: {\n        label: 'Message Filter',\n        tooltip: 'Applies a LIKE filter to the log message body',\n        clearButton: 'Clear',\n      },\n      logLevelFilter: {\n        label: 'Level Filter',\n        tooltip: 'Applies a filter to the log level',\n      },\n    },\n    TimeSeriesQueryBuilder: {\n      simpleQueryModeLabel: 'Simple',\n      aggregateQueryModeLabel: 'Aggregate',\n      builderModeLabel: 'Builder Mode',\n      builderModeTooltip: 'Switches the query builder between the simple and aggregate modes',\n      columnsHelp: {\n        text: 'The Time column is required — it anchors the series to the panel time range. Other selected columns become value series.',\n        linkText: 'Learn about column roles',\n        href: 'https://grafana.com/docs/plugins/grafana-clickhouse-datasource/latest/query-editor/#column-roles',\n      },\n      timeColumn: {\n        label: 'Time',\n        tooltip:\n          'Timestamp used to order and bucket the series. Must be a DateTime/DateTime64 column. Common names: time, timestamp, event_time. OTel: Timestamp.',\n      },\n    },\n    TableQueryBuilder: {\n      simpleQueryModeLabel: 'Simple',\n      aggregateQueryModeLabel: 'Aggregate',\n      builderModeLabel: 'Builder Mode',\n      builderModeTooltip: 'Switches the query builder between the simple and aggregate modes',\n    },\n    TraceQueryBuilder: {\n      traceIdModeLabel: 'Trace ID',\n      traceSearchModeLabel: 'Trace Search',\n      traceModeLabel: 'Trace Mode',\n      traceModeTooltip: 'Switches between trace ID and trace search mode',\n      columnsSection: 'Columns',\n      filtersSection: 'Filters',\n      columnsHelp: {\n        text: 'Map your table columns to the roles the traces panel expects. Each column is aliased in the generated SQL.',\n        linkText: 'Learn about column roles',\n        href: 'https://grafana.com/docs/plugins/grafana-clickhouse-datasource/latest/query-editor/#column-roles',\n      },\n\n      columns: {\n        traceId: {\n          label: 'Trace ID Column',\n          tooltip:\n            'Identifier shared by all spans in a trace. Aliased to `traceID` in the generated SQL. Common names: trace_id, traceId. OTel: TraceId.',\n        },\n        spanId: {\n          label: 'Span ID Column',\n          tooltip:\n            'Identifier for an individual span. Aliased to `spanID` in the generated SQL. Common names: span_id, spanId. OTel: SpanId.',\n        },\n        parentSpanId: {\n          label: 'Parent Span ID Column',\n          tooltip:\n            'Parent span reference, empty for root spans. Aliased to `parentSpanID` in the generated SQL. Common names: parent_span_id, parentSpanId. OTel: ParentSpanId.',\n        },\n        serviceName: {\n          label: 'Service Name Column',\n          tooltip:\n            'Name of the service that emitted the span. Aliased to `serviceName` in the generated SQL. Common names: service, service_name. OTel: ServiceName.',\n        },\n        operationName: {\n          label: 'Operation Name Column',\n          tooltip:\n            'Name of the operation or endpoint. Aliased to `operationName` in the generated SQL. Common names: operation, operation_name, span_name. OTel: SpanName.',\n        },\n        startTime: {\n          label: 'Start Time Column',\n          tooltip:\n            'Span start time. Used to filter by the panel time range. Must be a DateTime/DateTime64 column. Common names: start_time, timestamp. OTel: Timestamp.',\n        },\n        durationTime: {\n          label: 'Duration Time Column',\n          tooltip:\n            'Span duration. Set the unit field to match your column (ns, ms, s, ...). Common names: duration, duration_ns, duration_ms. OTel: Duration.',\n        },\n        durationUnit: {\n          label: 'Duration Unit',\n          tooltip:\n            'Unit used by your Duration column. OTel stores nanoseconds; other schemas often use milliseconds or seconds.',\n        },\n        tags: {\n          label: 'Tags Column',\n          tooltip:\n            'Span attributes, typically a Map. Aliased to `tags` in the generated SQL. Common names: tags, attributes. OTel: SpanAttributes.',\n        },\n        serviceTags: {\n          label: 'Service Tags Column',\n          tooltip:\n            'Resource-level attributes, typically a Map. Aliased to `serviceTags` in the generated SQL. Common names: resource, resource_attributes. OTel: ResourceAttributes.',\n        },\n        flattenNested: {\n          label: 'Use Flatten Nested',\n          tooltip: 'Enable if your traces table was created with flatten_nested=1',\n        },\n        eventsPrefix: {\n          label: 'Events Prefix',\n          tooltip: 'Prefix for the events column (OTel default: `Events`)',\n        },\n        linksPrefix: {\n          label: 'Links Prefix',\n          tooltip: 'Prefix for the trace references column (OTel default: `Links`)',\n        },\n        kind: {\n          label: 'Kind Column',\n          tooltip:\n            'Kind of span (SERVER, CLIENT, PRODUCER, CONSUMER, INTERNAL). Common names: kind, span_kind. OTel: SpanKind.',\n        },\n        statusCode: {\n          label: 'Status Code Column',\n          tooltip:\n            'Span status code (Ok, Error, Unset). Aliased to `statusCode` in the generated SQL. OTel: StatusCode.',\n        },\n        statusMessage: {\n          label: 'Status Message Column',\n          tooltip:\n            'Human-readable status description for the span. Common names: status_message. OTel: StatusMessage.',\n        },\n        instrumentationLibraryName: {\n          label: 'Library Name Column',\n          tooltip:\n            'Name of the instrumentation library (Optional). OTel: ScopeName or InstrumentationLibraryName.',\n        },\n        instrumentationLibraryVersion: {\n          label: 'Library Version Column',\n          tooltip:\n            'Version of the instrumentation library (Optional). OTel: ScopeVersion or InstrumentationLibraryVersion.',\n        },\n        state: {\n          label: 'State Column',\n          tooltip: 'W3C trace state baggage passed alongside the trace. OTel: TraceState.',\n        },\n        traceIdFilter: {\n          label: 'Trace ID',\n          tooltip: 'filter by a specific trace ID',\n        },\n      },\n    },\n  },\n  types: {\n    EditorType: {\n      sql: 'SQL Editor',\n      builder: 'Query Builder',\n    },\n    QueryType: {\n      table: 'Table',\n      logs: 'Logs',\n      timeseries: 'Time Series',\n      traces: 'Traces',\n    },\n    ColumnHint: {\n      [ColumnHint.FilterTime]: 'Filter Time',\n      [ColumnHint.Time]: 'Time',\n\n      [ColumnHint.ResourceAttributes]: 'Resource Attributes',\n      [ColumnHint.ScopeAttributes]: 'Scope Attributes',\n      [ColumnHint.LogAttributes]: 'Log Attributes',\n\n      [ColumnHint.LogLevel]: 'Level',\n      [ColumnHint.LogMessage]: 'Message',\n\n      [ColumnHint.TraceId]: 'Trace ID',\n      [ColumnHint.TraceSpanId]: 'Span ID',\n      [ColumnHint.TraceParentSpanId]: 'Parent Span ID',\n      [ColumnHint.TraceServiceName]: 'Service Name',\n      [ColumnHint.TraceOperationName]: 'Operation Name',\n      [ColumnHint.TraceDurationTime]: 'Duration Time',\n      [ColumnHint.TraceTags]: 'Tags',\n      [ColumnHint.TraceServiceTags]: 'Service Tags',\n      [ColumnHint.TraceStatusCode]: 'Status Code',\n      [ColumnHint.TraceKind]: 'Kind',\n      [ColumnHint.TraceStatusMessage]: 'Status Message',\n      [ColumnHint.TraceInstrumentationLibraryName]: 'Instrumentation Library Name',\n      [ColumnHint.TraceInstrumentationLibraryVersion]: 'Instrumentation Library Version',\n      [ColumnHint.TraceState]: 'State',\n    },\n  },\n};\n"
  },
  {
    "path": "src/module.ts",
    "content": "import { DataSourcePlugin, DashboardLoadedEvent, FeatureToggles } from '@grafana/data';\nimport { Datasource } from './data/CHDatasource';\nimport { ConfigEditor as ConfigEditorV1 } from './views/CHConfigEditor';\nimport { ConfigEditor as ConfigEditorV2 } from './views/config-v2/CHConfigEditor';\nimport { CHQueryEditor } from './views/CHQueryEditor';\nimport { CHConfig } from 'types/config';\nimport { CHQuery } from 'types/sql';\nimport { config, getAppEvents } from '@grafana/runtime';\nimport { analyzeQueries, trackClickhouseDashboardLoaded } from 'tracking';\nimport pluginJson from './plugin.json';\nimport clickhouseVersion from '../package.json';\n\n// ConfigEditorV2 is the new design for the ClickHouse configuration page\nconst configEditor = config.featureToggles['newClickhouseConfigPageDesign' as keyof FeatureToggles]\n  ? ConfigEditorV2\n  : ConfigEditorV1;\n\nexport const plugin = new DataSourcePlugin<Datasource, CHQuery, CHConfig>(Datasource)\n  .setConfigEditor(configEditor)\n  .setQueryEditor(CHQueryEditor);\n\n// Track dashboard loads to RudderStack\ngetAppEvents().subscribe<DashboardLoadedEvent<CHQuery>>(\n  DashboardLoadedEvent,\n  ({ payload: { dashboardId, orgId, grafanaVersion, queries } }) => {\n    const clickhouseQueries = queries[pluginJson.id]?.filter((q) => !q.hide);\n    if (!clickhouseQueries?.length) {\n      return;\n    }\n\n    trackClickhouseDashboardLoaded({\n      clickhouse_plugin_version: clickhouseVersion.version,\n      grafana_version: grafanaVersion,\n      dashboard_id: dashboardId,\n      org_id: orgId,\n      ...analyzeQueries(clickhouseQueries),\n    });\n  }\n);\n"
  },
  {
    "path": "src/otel.ts",
    "content": "import { ColumnHint, TimeUnit } from 'types/queryBuilder';\n\nexport const defaultLogsTable = 'otel_logs';\nexport const defaultTraceTable = 'otel_traces';\n\nexport const traceTimestampTableSuffix = '_trace_id_ts';\n\nexport interface OtelVersion {\n  name: string;\n  version: string;\n  specUrl?: string;\n  logsTable: string;\n  logColumnMap: Map<ColumnHint, string>;\n  logLevels: string[];\n  traceTable: string;\n  traceColumnMap: Map<ColumnHint, string>;\n  traceDurationUnit: TimeUnit.Nanoseconds;\n  flattenNested: boolean;\n  traceEventsColumnPrefix: string;\n  traceLinksColumnPrefix: string;\n}\n\nconst otel129: OtelVersion = {\n  name: '1.2.9',\n  version: '1.29.0',\n  specUrl: 'https://opentelemetry.io/docs/specs/otel',\n  logsTable: defaultLogsTable,\n  logColumnMap: new Map<ColumnHint, string>([\n    [ColumnHint.FilterTime, 'TimestampTime'],\n    [ColumnHint.Time, 'Timestamp'],\n    [ColumnHint.LogMessage, 'Body'],\n    [ColumnHint.LogLevel, 'SeverityText'],\n    [ColumnHint.TraceId, 'TraceId'],\n    [ColumnHint.ResourceAttributes, 'ResourceAttributes'],\n    [ColumnHint.ScopeAttributes, 'ScopeAttributes'],\n    [ColumnHint.LogAttributes, 'LogAttributes'],\n  ]),\n  logLevels: ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'],\n  traceTable: defaultTraceTable,\n  traceColumnMap: new Map<ColumnHint, string>([\n    [ColumnHint.Time, 'Timestamp'],\n    [ColumnHint.TraceId, 'TraceId'],\n    [ColumnHint.TraceSpanId, 'SpanId'],\n    [ColumnHint.TraceParentSpanId, 'ParentSpanId'],\n    [ColumnHint.TraceServiceName, 'ServiceName'],\n    [ColumnHint.TraceOperationName, 'SpanName'],\n    [ColumnHint.TraceDurationTime, 'Duration'],\n    [ColumnHint.TraceTags, 'SpanAttributes'],\n    [ColumnHint.TraceServiceTags, 'ResourceAttributes'],\n    [ColumnHint.TraceStatusCode, 'StatusCode'],\n    [ColumnHint.TraceKind, 'SpanKind'],\n    [ColumnHint.TraceStatusMessage, 'StatusMessage'],\n    [ColumnHint.TraceState, 'TraceState'],\n  ]),\n  flattenNested: false,\n  traceDurationUnit: TimeUnit.Nanoseconds,\n  traceEventsColumnPrefix: 'Events',\n  traceLinksColumnPrefix: 'Links',\n};\n\nexport const versions: readonly OtelVersion[] = [\n  // When selected, will always keep OTEL config up to date as new versions are added\n  { ...otel129, name: `latest (${otel129.name})`, version: 'latest' },\n  otel129,\n];\n\nexport const getLatestVersion = (): OtelVersion => versions[0];\nexport const getVersion = (version: string | undefined): OtelVersion | undefined => {\n  if (!version) {\n    return;\n  }\n\n  return versions.find((v) => v.version === version);\n};\n\nexport default {\n  traceTimestampTableSuffix,\n  versions,\n  getLatestVersion,\n  getVersion,\n};\n"
  },
  {
    "path": "src/plugin.json",
    "content": "{\n  \"$schema\": \"https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json\",\n  \"type\": \"datasource\",\n  \"name\": \"ClickHouse\",\n  \"id\": \"grafana-clickhouse-datasource\",\n  \"metrics\": true,\n  \"backend\": true,\n  \"logs\": true,\n  \"tracing\": true,\n  \"alerting\": true,\n  \"annotations\": true,\n  \"executable\": \"gpx_clickhouse\",\n  \"includes\": [\n    {\n      \"type\": \"dashboard\",\n      \"name\": \"Query Analysis\",\n      \"path\": \"dashboards/query-analysis.json\"\n    },\n    {\n      \"type\": \"dashboard\",\n      \"name\": \"Data Analysis\",\n      \"path\": \"dashboards/data-analysis.json\"\n    },\n    {\n      \"type\": \"dashboard\",\n      \"name\": \"Cluster Analysis\",\n      \"path\": \"dashboards/cluster-analysis.json\"\n    },\n    {\n      \"type\": \"dashboard\",\n      \"name\": \"ClickHouse OTel Dashboard\",\n      \"path\": \"dashboards/opentelemetry-clickhouse.json\"\n    },\n    {\n      \"type\": \"dashboard\",\n      \"name\": \"ClickHouse System Dashboards\",\n      \"path\": \"dashboards/system-dashboards.json\"\n    }\n  ],\n  \"category\": \"sql\",\n  \"info\": {\n    \"description\": \"ClickHouse datasource plugin for Grafana\",\n    \"author\": {\n      \"name\": \"Grafana Labs\"\n    },\n    \"keywords\": [\"Simple\"],\n    \"logos\": {\n      \"small\": \"img/logo.svg\",\n      \"large\": \"img/logo.svg\"\n    },\n    \"links\": [\n      {\n        \"name\": \"Website\",\n        \"url\": \"https://github.com/grafana/clickhouse-datasource\"\n      },\n      {\n        \"name\": \"License\",\n        \"url\": \"https://github.com/grafana/clickhouse-datasource/blob/master/LICENSE\"\n      }\n    ],\n    \"screenshots\": [],\n    \"version\": \"%VERSION%\",\n    \"updated\": \"%TODAY%\"\n  },\n  \"dependencies\": {\n    \"grafanaDependency\": \">=11.6.0\",\n    \"plugins\": []\n  }\n}\n"
  },
  {
    "path": "src/selectors.ts",
    "content": "import { E2ESelectors } from '@grafana/e2e-selectors';\nexport const Components = {\n  QueryEditor: {\n    CodeEditor: {\n      input: () => '.monaco-editor textarea',\n      container: 'data-testid-code-editor-container',\n      Expand: 'data-testid-code-editor-expand-button',\n    },\n    Format: {\n      label: 'Format',\n      tooltip: 'Query Type',\n      options: {\n        AUTO: 'Auto',\n        TABLE: 'Table',\n        TIME_SERIES: 'Time Series',\n        LOGS: 'Logs',\n        TRACE: 'Trace',\n      },\n    },\n    Types: {\n      label: 'Query Type',\n      tooltip: 'Query Type',\n      options: {\n        SQLEditor: 'SQL Editor',\n        QueryBuilder: 'Query Builder',\n      },\n      switcher: {\n        title: 'Are you sure?',\n        body: 'Queries that are too complex for the Query Builder will be altered.',\n        confirmText: 'Continue',\n        dismissText: 'Cancel',\n      },\n      cannotConvert: {\n        title: 'Cannot convert',\n        confirmText: 'Yes',\n      },\n    },\n    QueryBuilder: {\n      TYPES: {\n        label: 'Show as',\n        tooltip: 'Show as',\n        options: {\n          LIST: 'Table',\n          AGGREGATE: 'Aggregate',\n          TREND: 'Time Series',\n        },\n      },\n      DATABASE: {\n        label: 'Database',\n        tooltip: 'ClickHouse database to query from',\n      },\n      FROM: {\n        label: 'Table',\n        tooltip: 'ClickHouse table to query from',\n      },\n      SELECT: {\n        label: 'Fields',\n        tooltipTable: 'List of fields to show',\n        tooltipAggregate: `List of metrics to show. Use any of the given aggregation along with the field`,\n        ALIAS: {\n          label: 'as',\n          tooltip: 'alias',\n        },\n        AddLabel: 'Field',\n        RemoveLabel: '',\n      },\n      AGGREGATES: {\n        label: 'Aggregates',\n        tooltipTable: 'Aggregate functions to use',\n        tooltipAggregate: `Aggregate functions to use`,\n        ALIAS: {\n          label: 'as',\n          tooltip: 'alias',\n        },\n        AddLabel: 'Aggregate',\n        RemoveLabel: '',\n      },\n      WHERE: {\n        label: 'Filters',\n        tooltip: `List of filters`,\n        AddLabel: 'Filter',\n        RemoveLabel: '',\n      },\n      GROUP_BY: {\n        label: 'Group by',\n        tooltip: 'Group the results by specific field',\n      },\n      ORDER_BY: {\n        label: 'Order by',\n        tooltip: 'Order by field',\n        AddLabel: 'Order by',\n        RemoveLabel: '',\n      },\n      LIMIT: {\n        label: 'Limit',\n        tooltip: 'Number of records/results to show.',\n      },\n      TIME_FIELD: {\n        label: 'Time field',\n        tooltip: 'Select the time field for trending over time',\n      },\n      LOGS_VOLUME_TIME_FIELD: {\n        label: 'Time field',\n        tooltip: 'Select the time field for logs volume histogram. If not selected, the histogram will not be shown',\n      },\n      LOG_LEVEL_FIELD: {\n        label: 'Log level field',\n        tooltip: 'Select the field to extract log level information from',\n      },\n      PREVIEW: {\n        label: 'SQL Preview',\n        tooltip: 'SQL Preview. You can safely switch to SQL Editor to customize the generated query',\n      },\n    },\n  },\n  Config: {\n    HttpHeaderConfig: {\n      headerEditor: 'config__http-header-config__header-editor',\n      addHeaderButton: 'config__http-header-config__add-header-button',\n      removeHeaderButton: 'config__http-header-config__remove-header-button',\n      headerSecureSwitch: 'config__http-header-config__header-secure-switch',\n      headerNameInput: 'config__http-header-config__header-name-input',\n      headerValueInput: 'config__http-header-config__header-value-input',\n      forwardGrafanaHeadersSwitch: 'config__http-header-config__forward-grafana-headers-switch',\n    },\n    AliasTableConfig: {\n      aliasEditor: 'config__alias-table-config__alias-editor',\n      addEntryButton: 'config__alias-table-config__add-entry-button',\n      removeEntryButton: 'config__alias-table-config__remove-entry-button',\n      targetDatabaseInput: 'config__alias-table-config__target-database-input',\n      targetTableInput: 'config__alias-table-config__target-table-input',\n      aliasDatabaseInput: 'config__alias-table-config__alias-database-input',\n      aliasTableInput: 'config__alias-table-config__alias-table-input',\n    },\n  },\n  LogsContextPanel: {\n    alert: 'logs-context-panel__alert',\n    LogsContextKey: 'logs-context-panel__logs-context-key',\n  },\n  QueryBuilder: {\n    expandBuilderButton: 'query-builder__expand-builder-button',\n    LogsQueryBuilder: {\n      LogMessageLikeInput: {\n        input: 'query-builder__logs-query-builder__log-message-like-input__input',\n      },\n      columnRolesHelp: 'query-builder__logs-query-builder__column-roles-help',\n      columnRolesHelpLink: 'query-builder__logs-query-builder__column-roles-help-link',\n    },\n    TraceQueryBuilder: {\n      columnRolesHelp: 'query-builder__trace-query-builder__column-roles-help',\n      columnRolesHelpLink: 'query-builder__trace-query-builder__column-roles-help-link',\n    },\n    TimeSeriesQueryBuilder: {\n      columnRolesHelp: 'query-builder__time-series-query-builder__column-roles-help',\n      columnRolesHelpLink: 'query-builder__time-series-query-builder__column-roles-help-link',\n    },\n    AggregateEditor: {\n      sectionLabel: 'query-builder__aggregate-editor__section-label',\n      itemWrapper: 'query-builder__aggregate-editor__item-wrapper',\n      itemRemoveButton: 'query-builder__aggregate-editor-item-remove-button',\n      addButton: 'query-builder__aggregate-editor__add-button',\n    },\n    ColumnsEditor: {\n      multiSelectWrapper: 'query-builder__columns-editor__multi-select-wrapper',\n    },\n    GroupByEditor: {\n      multiSelectWrapper: 'query-builder__group-by__multi-select-wrapper',\n    },\n    LimitEditor: {\n      input: 'query-builder__limit-editor__input',\n    },\n    TraceIdInput: {\n      input: 'query-builder__trace-id-input__input',\n    },\n  },\n};\nexport const selectors: { components: E2ESelectors<typeof Components> } = {\n  components: Components,\n};\n"
  },
  {
    "path": "src/styles.ts",
    "content": "import { css } from '@emotion/css';\n\nexport const styles = {\n  Common: {\n    check: css`\n      margin-top: 5px;\n    `,\n    wrapper: css`\n      position: relative;\n      width: 100%;\n    `,\n    smallBtn: css`\n      margin-top: 5px;\n      margin-inline: 5px;\n    `,\n    selectWrapper: css`\n      width: 100%;\n    `,\n    inlineSelect: css`\n      margin-right: 5px;\n    `,\n    firstLabel: css`\n      margin-right: 5px;\n    `,\n    expand: css`\n      position: absolute;\n      top: 2px;\n      left: 6px;\n      z-index: 100;\n      color: gray;\n    `,\n  },\n  ConfigEditor: {\n    container: css`\n      justify-content: space-between;\n      h5 {\n        line-height: 34px;\n        margin-bottom: 5px;\n      }\n      button {\n        margin-right: 5px;\n      }\n    `,\n    wide: css`\n      width: 75%;\n    `,\n    subHeader: css`\n      padding: 5px 0 5px 0;\n    `,\n  },\n  QueryEditor: {\n    queryType: css`\n      justify-content: space-between;\n      span {\n        display: flex;\n      }\n    `,\n    inlineField: css`\n      margin-left: 7px;\n    `,\n  },\n  FormatSelector: {\n    formatSelector: css`\n      display: flex;\n    `,\n  },\n  VariablesEditor: {},\n};\n"
  },
  {
    "path": "src/test/setupTests.ts",
    "content": "import '@testing-library/jest-dom';\nimport '@testing-library/jest-dom/extend-expect';\n\nprocess.on('unhandledRejection', (err) => {\n  console.warn(err);\n});\n"
  },
  {
    "path": "src/tracking.test.ts",
    "content": "import { ClickhouseCounters, analyzeQueries } from 'tracking';\nimport { CHBuilderQuery, CHQuery, CHSqlQuery, EditorType } from 'types/sql';\nimport { QueryType, BuilderMode } from 'types/queryBuilder';\n\ninterface AnalyzeQueriesTestCase {\n  description: string;\n  queries: CHQuery[];\n  expectedCounters: ClickhouseCounters;\n}\n\ndescribe('analyzeQueries', () => {\n  const baseQuery: Partial<CHQuery> = {\n    pluginVersion: '',\n    rawSql: '',\n    refId: '',\n  };\n\n  const emptyCounters: ClickhouseCounters = {\n    sql_queries: 0,\n    sql_query_type_table: 0,\n    sql_query_type_logs: 0,\n    sql_query_type_timeseries: 0,\n    sql_query_type_traces: 0,\n\n    builder_queries: 0,\n    builder_query_type_table: 0,\n    builder_query_type_table_simple: 0,\n    builder_query_type_table_aggregate: 0,\n    builder_query_type_logs: 0,\n    builder_query_type_timeseries: 0,\n    builder_query_type_timeseries_simple: 0,\n    builder_query_type_timeseries_aggregate: 0,\n    builder_query_type_traces: 0,\n    builder_query_type_traces_search: 0,\n    builder_query_type_traces_id: 0,\n    builder_minimized_queries: 0,\n    builder_otel_queries: 0,\n  };\n\n  const cases: AnalyzeQueriesTestCase[] = [\n    {\n      description: 'should count 1 sql query with no query types',\n      queries: [\n        {\n          ...baseQuery,\n          editorType: EditorType.SQL,\n        } as CHSqlQuery,\n      ],\n      expectedCounters: {\n        ...emptyCounters,\n        sql_queries: 1,\n      },\n    },\n    {\n      description: 'should count 1 sql query with Table type',\n      queries: [\n        {\n          ...baseQuery,\n          editorType: EditorType.SQL,\n          queryType: QueryType.Table,\n        } as CHSqlQuery,\n      ],\n      expectedCounters: {\n        ...emptyCounters,\n        sql_queries: 1,\n        sql_query_type_table: 1,\n      },\n    },\n    {\n      description: 'should count 1 sql query with Logs type',\n      queries: [\n        {\n          ...baseQuery,\n          editorType: EditorType.SQL,\n          queryType: QueryType.Logs,\n        } as CHSqlQuery,\n      ],\n      expectedCounters: {\n        ...emptyCounters,\n        sql_queries: 1,\n        sql_query_type_logs: 1,\n      },\n    },\n    {\n      description: 'should count 1 sql query with TimeSeries type',\n      queries: [\n        {\n          ...baseQuery,\n          editorType: EditorType.SQL,\n          queryType: QueryType.TimeSeries,\n        } as CHSqlQuery,\n      ],\n      expectedCounters: {\n        ...emptyCounters,\n        sql_queries: 1,\n        sql_query_type_timeseries: 1,\n      },\n    },\n    {\n      description: 'should count 1 sql query with Traces type',\n      queries: [\n        {\n          ...baseQuery,\n          editorType: EditorType.SQL,\n          queryType: QueryType.Traces,\n        } as CHSqlQuery,\n      ],\n      expectedCounters: {\n        ...emptyCounters,\n        sql_queries: 1,\n        sql_query_type_traces: 1,\n      },\n    },\n\n    {\n      description: 'should count 1 builder query with no builderOptions',\n      queries: [\n        {\n          ...baseQuery,\n          editorType: EditorType.Builder,\n        } as CHBuilderQuery,\n      ],\n      expectedCounters: {\n        ...emptyCounters,\n        builder_queries: 1,\n      },\n    },\n    {\n      description: 'should count 1 builder query with empty builderOptions',\n      queries: [\n        {\n          ...baseQuery,\n          editorType: EditorType.Builder,\n          builderOptions: {},\n        } as CHBuilderQuery,\n      ],\n      expectedCounters: {\n        ...emptyCounters,\n        builder_queries: 1,\n      },\n    },\n    {\n      description: 'should count 1 builder query with Table type, no mode',\n      queries: [\n        {\n          ...baseQuery,\n          editorType: EditorType.Builder,\n          builderOptions: {\n            queryType: QueryType.Table,\n          },\n        } as CHBuilderQuery,\n      ],\n      expectedCounters: {\n        ...emptyCounters,\n        builder_queries: 1,\n        builder_query_type_table: 1,\n        builder_query_type_table_simple: 1, // Table defaults to simple\n      },\n    },\n    {\n      description: 'should count 1 builder query with Table type, simple mode',\n      queries: [\n        {\n          ...baseQuery,\n          editorType: EditorType.Builder,\n          builderOptions: {\n            queryType: QueryType.Table,\n            mode: BuilderMode.List,\n          },\n        } as CHBuilderQuery,\n      ],\n      expectedCounters: {\n        ...emptyCounters,\n        builder_queries: 1,\n        builder_query_type_table: 1,\n        builder_query_type_table_simple: 1,\n      },\n    },\n    {\n      description: 'should count 1 builder query with Table type, aggregate mode',\n      queries: [\n        {\n          ...baseQuery,\n          editorType: EditorType.Builder,\n          builderOptions: {\n            queryType: QueryType.Table,\n            mode: BuilderMode.Aggregate,\n          },\n        } as CHBuilderQuery,\n      ],\n      expectedCounters: {\n        ...emptyCounters,\n        builder_queries: 1,\n        builder_query_type_table: 1,\n        builder_query_type_table_aggregate: 1,\n      },\n    },\n    {\n      description: 'should count 1 builder query with Logs type',\n      queries: [\n        {\n          ...baseQuery,\n          editorType: EditorType.Builder,\n          builderOptions: {\n            queryType: QueryType.Logs,\n          },\n        } as CHBuilderQuery,\n      ],\n      expectedCounters: {\n        ...emptyCounters,\n        builder_queries: 1,\n        builder_query_type_logs: 1,\n      },\n    },\n    {\n      description: 'should count 1 builder query with TimeSeries type, no mode',\n      queries: [\n        {\n          ...baseQuery,\n          editorType: EditorType.Builder,\n          builderOptions: {\n            queryType: QueryType.TimeSeries,\n          },\n        } as CHBuilderQuery,\n      ],\n      expectedCounters: {\n        ...emptyCounters,\n        builder_queries: 1,\n        builder_query_type_timeseries: 1,\n        builder_query_type_timeseries_simple: 1, // TimeSeries defaults to simple\n      },\n    },\n    {\n      description: 'should count 1 builder query with TimeSeries type, simple mode',\n      queries: [\n        {\n          ...baseQuery,\n          editorType: EditorType.Builder,\n          builderOptions: {\n            queryType: QueryType.TimeSeries,\n            mode: BuilderMode.Aggregate,\n          },\n        } as CHBuilderQuery,\n      ],\n      expectedCounters: {\n        ...emptyCounters,\n        builder_queries: 1,\n        builder_query_type_timeseries: 1,\n        builder_query_type_timeseries_simple: 1,\n      },\n    },\n    {\n      description: 'should count 1 builder query with TimeSeries type, aggregate mode',\n      queries: [\n        {\n          ...baseQuery,\n          editorType: EditorType.Builder,\n          builderOptions: {\n            queryType: QueryType.TimeSeries,\n            mode: BuilderMode.Trend,\n          },\n        } as CHBuilderQuery,\n      ],\n      expectedCounters: {\n        ...emptyCounters,\n        builder_queries: 1,\n        builder_query_type_timeseries: 1,\n        builder_query_type_timeseries_aggregate: 1,\n      },\n    },\n    {\n      description: 'should count 1 builder query with Traces type, no mode',\n      queries: [\n        {\n          ...baseQuery,\n          editorType: EditorType.Builder,\n          builderOptions: {\n            queryType: QueryType.Traces,\n          },\n        } as CHBuilderQuery,\n      ],\n      expectedCounters: {\n        ...emptyCounters,\n        builder_queries: 1,\n        builder_query_type_traces: 1,\n        builder_query_type_traces_search: 1, // Traces defaults to search mode\n      },\n    },\n    {\n      description: 'should count 1 builder query with Traces type, trace ID mode',\n      queries: [\n        {\n          ...baseQuery,\n          editorType: EditorType.Builder,\n          builderOptions: {\n            queryType: QueryType.Traces,\n            meta: {\n              isTraceIdMode: true,\n            },\n          },\n        } as CHBuilderQuery,\n      ],\n      expectedCounters: {\n        ...emptyCounters,\n        builder_queries: 1,\n        builder_query_type_traces: 1,\n        builder_query_type_traces_id: 1,\n      },\n    },\n    {\n      description: 'should count 1 builder query with Traces type, trace search mode',\n      queries: [\n        {\n          ...baseQuery,\n          editorType: EditorType.Builder,\n          builderOptions: {\n            queryType: QueryType.Traces,\n            meta: {\n              isTraceIdMode: false,\n            },\n          },\n        } as CHBuilderQuery,\n      ],\n      expectedCounters: {\n        ...emptyCounters,\n        builder_queries: 1,\n        builder_query_type_traces: 1,\n        builder_query_type_traces_search: 1,\n      },\n    },\n    {\n      description: 'should count 1 minimized query',\n      queries: [\n        {\n          ...baseQuery,\n          editorType: EditorType.Builder,\n          builderOptions: {\n            queryType: QueryType.Table,\n            meta: {\n              minimized: true,\n            },\n          },\n        } as CHBuilderQuery,\n      ],\n      expectedCounters: {\n        ...emptyCounters,\n        builder_queries: 1,\n        builder_query_type_table: 1,\n        builder_query_type_table_simple: 1,\n        builder_minimized_queries: 1,\n      },\n    },\n    {\n      description: 'should count 1 otel query',\n      queries: [\n        {\n          ...baseQuery,\n          editorType: EditorType.Builder,\n          builderOptions: {\n            queryType: QueryType.Table,\n            meta: {\n              otelEnabled: true,\n            },\n          },\n        } as CHBuilderQuery,\n      ],\n      expectedCounters: {\n        ...emptyCounters,\n        builder_queries: 1,\n        builder_query_type_table: 1,\n        builder_query_type_table_simple: 1,\n        builder_otel_queries: 1,\n      },\n    },\n    {\n      description: 'should count 3 queries, mixed types',\n      queries: [\n        {\n          ...baseQuery,\n          editorType: EditorType.SQL,\n        } as CHSqlQuery,\n        {\n          ...baseQuery,\n          editorType: EditorType.Builder,\n          builderOptions: {\n            queryType: QueryType.Table,\n          },\n        } as CHBuilderQuery,\n        {\n          ...baseQuery,\n          editorType: EditorType.SQL,\n        } as CHSqlQuery,\n      ],\n      expectedCounters: {\n        ...emptyCounters,\n        sql_queries: 2,\n        builder_queries: 1,\n        builder_query_type_table: 1,\n        builder_query_type_table_simple: 1,\n      },\n    },\n    {\n      description: 'should count 0 queries',\n      queries: [],\n      expectedCounters: { ...emptyCounters },\n    },\n  ];\n\n  it.each(cases)('$description', (c) => {\n    expect(analyzeQueries(c.queries)).toMatchObject(c.expectedCounters);\n  });\n});\n"
  },
  {
    "path": "src/tracking.ts",
    "content": "import { reportInteraction } from '@grafana/runtime';\nimport { CHQuery, EditorType } from 'types/sql';\nimport { QueryType, BuilderMode } from 'types/queryBuilder';\n\nexport const trackClickhouseDashboardLoaded = (props: ClickhouseDashboardLoadedProps) => {\n  reportInteraction('grafana_ds_clickhouse_dashboard_loaded', props);\n};\n\nexport const trackClickhouseHealthCheckFailed = (props: { error_category: string; protocol: string }) => {\n  reportInteraction('grafana_ds_clickhouse_healthcheck_failed', {\n    error_category: props.error_category,\n    protocol: props.protocol,\n  });\n};\n\nexport type ClickhouseCounters = {\n  sql_queries: number;\n  sql_query_type_table: number;\n  sql_query_type_logs: number;\n  sql_query_type_timeseries: number;\n  sql_query_type_traces: number;\n\n  builder_queries: number;\n  builder_query_type_table: number;\n  builder_query_type_table_simple: number;\n  builder_query_type_table_aggregate: number;\n  builder_query_type_logs: number;\n  builder_query_type_timeseries: number;\n  builder_query_type_timeseries_simple: number;\n  builder_query_type_timeseries_aggregate: number;\n  builder_query_type_traces: number;\n  builder_query_type_traces_search: number;\n  builder_query_type_traces_id: number;\n  builder_minimized_queries: number;\n  builder_otel_queries: number;\n};\n\nexport interface ClickhouseDashboardLoadedProps extends ClickhouseCounters {\n  clickhouse_plugin_version?: string;\n  grafana_version?: string;\n  dashboard_id: string;\n  org_id?: number;\n  [key: string]: any;\n}\n\nexport const analyzeQueries = (queries: CHQuery[]): ClickhouseCounters => {\n  const c: ClickhouseCounters = {\n    sql_queries: 0,\n    sql_query_type_table: 0,\n    sql_query_type_logs: 0,\n    sql_query_type_timeseries: 0,\n    sql_query_type_traces: 0,\n\n    builder_queries: 0,\n    builder_query_type_table: 0,\n    builder_query_type_table_simple: 0,\n    builder_query_type_table_aggregate: 0,\n    builder_query_type_logs: 0,\n    builder_query_type_timeseries: 0,\n    builder_query_type_timeseries_simple: 0,\n    builder_query_type_timeseries_aggregate: 0,\n    builder_query_type_traces: 0,\n    builder_query_type_traces_search: 0,\n    builder_query_type_traces_id: 0,\n    builder_minimized_queries: 0,\n    builder_otel_queries: 0,\n  };\n\n  queries.forEach((q) => {\n    if (q.editorType === EditorType.SQL) {\n      c.sql_queries++;\n\n      if (q.queryType === QueryType.Table) {\n        c.sql_query_type_table++;\n      } else if (q.queryType === QueryType.Logs) {\n        c.sql_query_type_logs++;\n      } else if (q.queryType === QueryType.TimeSeries) {\n        c.sql_query_type_timeseries++;\n      } else if (q.queryType === QueryType.Traces) {\n        c.sql_query_type_traces++;\n      }\n    } else if (q.editorType === EditorType.Builder) {\n      c.builder_queries++;\n\n      if (!q.builderOptions) {\n        return;\n      }\n\n      if (q.builderOptions.queryType === QueryType.Table) {\n        c.builder_query_type_table++;\n\n        if (q.builderOptions.mode === BuilderMode.Aggregate) {\n          c.builder_query_type_table_aggregate++;\n        } else {\n          c.builder_query_type_table_simple++;\n        }\n      } else if (q.builderOptions.queryType === QueryType.Logs) {\n        c.builder_query_type_logs++;\n      } else if (q.builderOptions.queryType === QueryType.TimeSeries) {\n        c.builder_query_type_timeseries++;\n\n        if (q.builderOptions.mode === BuilderMode.Trend) {\n          c.builder_query_type_timeseries_aggregate++;\n        } else {\n          c.builder_query_type_timeseries_simple++;\n        }\n      } else if (q.builderOptions.queryType === QueryType.Traces) {\n        c.builder_query_type_traces++;\n\n        if (q.builderOptions.meta?.isTraceIdMode) {\n          c.builder_query_type_traces_id++;\n        } else {\n          c.builder_query_type_traces_search++;\n        }\n      }\n\n      if (q.builderOptions.meta?.minimized) {\n        c.builder_minimized_queries++;\n      }\n\n      if (q.builderOptions.meta?.otelEnabled) {\n        c.builder_otel_queries++;\n      }\n    }\n  });\n\n  return c;\n};\n"
  },
  {
    "path": "src/types/config.ts",
    "content": "import { DataSourceJsonData, KeyValue } from '@grafana/data';\nimport otel, { defaultLogsTable, defaultTraceTable } from 'otel';\nimport { TimeUnit } from './queryBuilder';\n\nexport interface CHConfig extends DataSourceJsonData {\n  /**\n   * The version of the plugin this config was saved with\n   */\n  version: string;\n\n  host: string;\n  port: number;\n  protocol: Protocol;\n  secure?: boolean;\n  path?: string;\n\n  tlsSkipVerify?: boolean;\n  tlsAuth?: boolean;\n  tlsAuthWithCACert?: boolean;\n\n  username: string;\n\n  defaultDatabase?: string;\n  defaultTable?: string;\n\n  connMaxLifetime?: string;\n  dialTimeout?: string;\n  maxIdleConns?: string;\n  maxOpenConns?: string;\n  queryTimeout?: string;\n  validateSql?: boolean;\n\n  logs?: CHLogsConfig;\n  traces?: CHTracesConfig;\n\n  aliasTables?: AliasTableEntry[];\n\n  httpHeaders?: CHHttpHeader[];\n  forwardGrafanaHeaders?: boolean;\n\n  customSettings?: CHCustomSetting[];\n  enableSecureSocksProxy?: boolean;\n  enableRowLimit?: boolean;\n\n  hideTableNameInAdhocFilters?: boolean;\n\n  pdcInjected?: boolean;\n}\n\ninterface CHSecureConfigProperties {\n  password?: string;\n\n  tlsCACert?: string;\n  tlsClientCert?: string;\n  tlsClientKey?: string;\n}\nexport type CHSecureConfig = CHSecureConfigProperties | KeyValue<string>;\n\nexport interface CHHttpHeader {\n  name: string;\n  value: string;\n  secure: boolean;\n}\n\nexport interface CHCustomSetting {\n  setting: string;\n  value: string;\n}\n\nexport interface CHLogsConfig {\n  defaultDatabase?: string;\n  defaultTable?: string;\n\n  otelEnabled?: boolean;\n  otelVersion?: string;\n\n  filterTimeColumn?: string;\n  timeColumn?: string;\n  levelColumn?: string;\n  messageColumn?: string;\n\n  selectContextColumns?: boolean;\n  contextColumns?: string[];\n  showLogLinks?: boolean;\n}\n\nexport interface CHTracesConfig {\n  defaultDatabase?: string;\n  defaultTable?: string;\n\n  otelEnabled?: boolean;\n  otelVersion?: string;\n\n  traceIdColumn?: string;\n  spanIdColumn?: string;\n  operationNameColumn?: string;\n  parentSpanIdColumn?: string;\n  serviceNameColumn?: string;\n  durationColumn?: string;\n  durationUnit?: string;\n  startTimeColumn?: string;\n  tagsColumn?: string;\n  serviceTagsColumn?: string;\n  kindColumn?: string;\n  statusCodeColumn?: string;\n  statusMessageColumn?: string;\n  stateColumn?: string;\n  instrumentationLibraryNameColumn?: string;\n  instrumentationLibraryVersionColumn?: string;\n\n  flattenNested?: boolean;\n  traceEventsColumnPrefix?: string;\n  traceLinksColumnPrefix?: string;\n  showTraceLinks?: boolean;\n\n  /**\n   * Suffix appended to the traces table name to locate a companion trace-timestamp\n   * index table (e.g. `<table>_trace_id_ts`). When such a table exists, trace ID\n   * queries run a two-step lookup that narrows the main query's time range,\n   * avoiding a full scan. Defaults to `_trace_id_ts` (the OTel convention).\n   */\n  traceTimestampTableSuffix?: string;\n}\n\nexport interface AliasTableEntry {\n  targetDatabase: string;\n  targetTable: string;\n  aliasDatabase: string;\n  aliasTable: string;\n}\n\nexport enum Protocol {\n  Native = 'native',\n  Http = 'http',\n}\n\nexport const defaultCHAdditionalSettingsConfig: Partial<CHConfig> = {\n  logs: {\n    defaultTable: defaultLogsTable,\n    otelVersion: otel.getLatestVersion().version,\n    selectContextColumns: true,\n    contextColumns: [],\n  },\n  traces: {\n    defaultTable: defaultTraceTable,\n    otelVersion: otel.getLatestVersion().version,\n    durationUnit: TimeUnit.Nanoseconds,\n  },\n};\n"
  },
  {
    "path": "src/types/queryBuilder.ts",
    "content": "export interface FieldLabel {\n  label: string;\n  tooltip: string;\n}\n\nexport enum BuilderMode {\n  List = 'list',\n  Aggregate = 'aggregate',\n  Trend = 'trend',\n}\n\n/**\n * QueryType determines the display/query format.\n */\nexport enum QueryType {\n  Table = 'table',\n  Logs = 'logs',\n  TimeSeries = 'timeseries',\n  Traces = 'traces',\n}\n\nexport interface QueryBuilderOptions {\n  database: string;\n  table: string;\n  queryType: QueryType;\n\n  mode?: BuilderMode; // TODO: no longer required?\n\n  columns?: SelectedColumn[];\n  aggregates?: AggregateColumn[];\n  filters?: Filter[];\n  groupBy?: string[];\n  orderBy?: OrderBy[];\n  limit?: number;\n\n  /**\n   * Contains metadata for editor-specific use cases.\n   */\n  meta?: {\n    /**\n     * When enabled, will hide most/all of the query builder options.\n     *\n     * Intended to be used for trace ID lookups where we only care to show the visualization panel\n     */\n    minimized?: boolean;\n\n    // Logs\n    liveView?: boolean;\n    logMessageLike?: string;\n\n    // Trace\n    traceDurationUnit?: TimeUnit;\n    /**\n     * true for trace ID mode, false for trace search mode\n     */\n    isTraceIdMode?: boolean;\n    traceId?: string;\n    /**\n     * true when the trace timestamp optimization table is detected\n     */\n    hasTraceTimestampTable?: boolean;\n    /**\n     * Suffix used to locate the companion trace-timestamp index table\n     * (e.g. `_trace_id_ts`). When omitted, generators fall back to the\n     * built-in OTel default.\n     */\n    traceTimestampTableSuffix?: string;\n\n    /**\n     * True if \"Nested\" column types should be treated as if they\n     * were created with flatten_nested=1. Applies to trace Events and Links columns.\n     */\n    flattenNested?: boolean;\n    traceEventsColumnPrefix?: string;\n    traceLinksColumnPrefix?: string;\n\n    // Logs & Traces\n    otelEnabled?: boolean;\n    otelVersion?: string;\n  };\n}\n\nexport enum AggregateType {\n  Sum = 'sum',\n  Average = 'avg',\n  Min = 'min',\n  Max = 'max',\n  Count = 'count',\n  Any = 'any',\n  // Count_Distinct = 'count_distinct',\n}\n\nexport type AggregateColumn = {\n  aggregateType: AggregateType;\n  column: string;\n  alias?: string;\n};\n\nexport interface Field {\n  name: string;\n  type: string;\n  rel: string;\n  label: string;\n  ref: string[];\n}\n\nexport interface FullEntity {\n  name: string;\n  label: string;\n  custom: boolean;\n  queryable: boolean;\n}\n\ninterface TableColumnPickListItem {\n  label: string;\n  value: string;\n}\n\n/**\n * Represents a column retrieved from ClickHouse\n */\nexport interface TableColumn {\n  name: string;\n  type: string;\n  picklistValues: TableColumnPickListItem[];\n  label?: string;\n  filterable?: boolean;\n  sortable?: boolean;\n  groupable?: boolean;\n  aggregatable?: boolean;\n}\n\nexport interface SqlFunction {\n  name: string;\n  isAggregate: boolean;\n  caseInsensitive: boolean;\n  aliasTo: string;\n  origin: string;\n  description: string;\n  syntax: string;\n  arguments: string;\n  returnedValue: string;\n  examples: string;\n  categories: string;\n}\n\n/**\n * Some columns are used to enable certain features.\n * This enum defines the different use cases that a column may be used for in the query generator.\n * For example, \"Time\" would be used to identify the primary time column for a time series.\n */\nexport enum ColumnHint {\n  /**\n   * An optional lower precision timestamp used for filtering logs.\n   */\n  FilterTime = 'filter_time',\n  Time = 'time',\n\n  ResourceAttributes = 'resource_attributes',\n  ScopeAttributes = 'scope_attributes',\n  LogAttributes = 'log_attributes',\n\n  LogLevel = 'log_level',\n  LogMessage = 'log_message',\n\n  TraceId = 'trace_id',\n  TraceSpanId = 'trace_span_id',\n  TraceParentSpanId = 'trace_parent_span_id',\n  TraceServiceName = 'trace_service_name',\n  TraceOperationName = 'trace_operation_name',\n  TraceDurationTime = 'trace_duration_time',\n  TraceTags = 'trace_tags',\n  TraceServiceTags = 'trace_service_tags',\n  TraceStatusCode = 'trace_status_code',\n  TraceKind = 'trace_kind',\n  TraceStatusMessage = 'trace_status_message',\n  TraceInstrumentationLibraryName = 'instrumentation_library_name',\n  TraceInstrumentationLibraryVersion = 'instrumentation_library_version',\n  TraceState = 'trace_state',\n}\n\n/**\n * TimeUnit determines a unit of time.\n */\nexport enum TimeUnit {\n  Seconds = 'seconds',\n  Milliseconds = 'milliseconds',\n  Microseconds = 'microseconds',\n  Nanoseconds = 'nanoseconds',\n}\n\n/**\n * Represents a column selection, including metadata for the query generator to use.\n */\nexport interface SelectedColumn {\n  name: string;\n  type?: string;\n  alias?: string;\n  custom?: boolean;\n  hint?: ColumnHint;\n}\n\nexport enum OrderByDirection {\n  ASC = 'ASC',\n  DESC = 'DESC',\n}\n\nexport interface OrderBy {\n  name: string;\n  dir: OrderByDirection;\n  /**\n   * true if this orderBy was configured to be present by default\n   */\n  default?: boolean;\n\n  /**\n   * If provided, SQL generator will ignore \"name\" and instead\n   * find the intended column by the hint\n   */\n  hint?: ColumnHint;\n}\n\nexport enum FilterOperator {\n  /**\n   * A placeholder filter that gets excluded from SQL generation\n   */\n  IsAnything = 'IS ANYTHING',\n\n  /**\n   * Compares to an empty string\n   */\n  IsEmpty = 'IS EMPTY',\n  IsNotEmpty = 'IS NOT EMPTY',\n\n  IsNull = 'IS NULL',\n  IsNotNull = 'IS NOT NULL',\n  Equals = '=',\n  NotEquals = '!=',\n  LessThan = '<',\n  LessThanOrEqual = '<=',\n  GreaterThan = '>',\n  GreaterThanOrEqual = '>=',\n  Like = 'LIKE',\n  NotLike = 'NOT LIKE',\n  ILike = 'ILIKE',\n  NotILike = 'NOT ILIKE',\n  In = 'IN',\n  NotIn = 'NOT IN',\n  WithInGrafanaTimeRange = 'WITH IN DASHBOARD TIME RANGE',\n  OutsideGrafanaTimeRange = 'OUTSIDE DASHBOARD TIME RANGE',\n}\n\nexport interface CommonFilterProps {\n  filterType: 'custom';\n  /**\n   * Column name\n   */\n  key: string;\n  /**\n   * key used when using a map type: exampleMap['mapKey']\n   */\n  mapKey?: string;\n  type: string;\n  condition: 'AND' | 'OR';\n\n  /**\n   * Used to uniquely identify a dynamically added filter\n   * For example, might be set to 'timeRange' for the default added time range filter.\n   */\n  id?: string;\n  /**\n   * If provided, SQL generator will ignore \"key\" and instead\n   * find the intended column by the hint.\n   *\n   * Note that the column MUST be present in the selected columns array in order\n   * for the filter to be applied unless key is also provided.\n   */\n  hint?: ColumnHint;\n\n  /**\n   * Display label for filter\n   */\n  label?: string;\n}\n\nexport interface NullFilter extends CommonFilterProps {\n  operator: FilterOperator.IsAnything | FilterOperator.IsNull | FilterOperator.IsNotNull;\n}\n\nexport interface BooleanFilter extends CommonFilterProps {\n  type: 'boolean';\n  operator: FilterOperator.IsAnything | FilterOperator.Equals | FilterOperator.NotEquals;\n  value: boolean;\n}\n\nexport interface StringFilter extends CommonFilterProps {\n  operator:\n    | FilterOperator.IsAnything\n    | FilterOperator.IsEmpty\n    | FilterOperator.IsNotEmpty\n    | FilterOperator.Equals\n    | FilterOperator.NotEquals\n    | FilterOperator.Like\n    | FilterOperator.NotLike\n    | FilterOperator.ILike\n    | FilterOperator.NotILike;\n  value: string;\n}\n\nexport interface NumberFilter extends CommonFilterProps {\n  operator:\n    | FilterOperator.IsAnything\n    | FilterOperator.Equals\n    | FilterOperator.NotEquals\n    | FilterOperator.LessThan\n    | FilterOperator.LessThanOrEqual\n    | FilterOperator.GreaterThan\n    | FilterOperator.GreaterThanOrEqual;\n  value: number;\n}\n\nexport interface DateFilterWithValue extends CommonFilterProps {\n  type: 'datetime' | 'date';\n  operator:\n    | FilterOperator.IsAnything\n    | FilterOperator.Equals\n    | FilterOperator.NotEquals\n    | FilterOperator.LessThan\n    | FilterOperator.LessThanOrEqual\n    | FilterOperator.GreaterThan\n    | FilterOperator.GreaterThanOrEqual;\n  value: string;\n}\n\nexport interface DateFilterWithoutValue extends CommonFilterProps {\n  type: 'datetime' | 'date';\n  operator: FilterOperator.IsAnything | FilterOperator.WithInGrafanaTimeRange | FilterOperator.OutsideGrafanaTimeRange;\n}\n\nexport type DateFilter = DateFilterWithValue | DateFilterWithoutValue;\n\nexport interface MultiFilter extends CommonFilterProps {\n  operator: FilterOperator.IsAnything | FilterOperator.In | FilterOperator.NotIn;\n  value: string[];\n}\n\nexport type Filter = NullFilter | BooleanFilter | NumberFilter | DateFilter | StringFilter | MultiFilter;\n"
  },
  {
    "path": "src/types/sql.ts",
    "content": "import { DataQuery } from '@grafana/schema';\nimport { BuilderMode, QueryType, QueryBuilderOptions } from './queryBuilder';\n\n/**\n * EditorType determines the query editor type.\n */\nexport enum EditorType {\n  SQL = 'sql',\n  Builder = 'builder',\n}\n\nexport interface CHQueryBase extends DataQuery {\n  pluginVersion: string;\n  editorType: EditorType;\n  rawSql: string;\n\n  /**\n   * REQUIRED by backend for auto selecting preferredVisualizationType.\n   * Only used in explore view.\n   * src: https://github.com/grafana/sqlds/blob/dda2dc0a54b128961fc9f7885baabf555f3ddfdc/query.go#L36\n   */\n  format?: number;\n}\n\nexport interface CHSqlQuery extends CHQueryBase {\n  editorType: EditorType.SQL;\n  queryType?: QueryType; // only used in explore view\n  meta?: {\n    timezone?: string;\n    // meta fields to be used just for building builder options when migrating back to EditorType.Builder\n    builderOptions?: QueryBuilderOptions;\n  };\n  expand?: boolean;\n}\n\nexport interface CHBuilderQuery extends CHQueryBase {\n  editorType: EditorType.Builder;\n  builderOptions: QueryBuilderOptions;\n  meta?: {\n    timezone?: string;\n  };\n}\n\nexport type CHQuery = CHSqlQuery | CHBuilderQuery;\n\n// TODO: these aren't really types\nexport const defaultEditorType: EditorType = EditorType.Builder;\nexport const defaultCHBuilderQuery: Omit<CHBuilderQuery, 'refId'> = {\n  pluginVersion: '',\n  editorType: EditorType.Builder,\n  rawSql: '',\n  builderOptions: {\n    database: '',\n    table: '',\n    queryType: QueryType.Table,\n    mode: BuilderMode.List,\n    columns: [],\n    meta: {},\n    limit: 1000,\n  },\n};\nexport const defaultCHSqlQuery: Omit<CHSqlQuery, 'refId'> = {\n  pluginVersion: '',\n  editorType: EditorType.SQL,\n  rawSql: '',\n  expand: false,\n};\n"
  },
  {
    "path": "src/utils/version.test.ts",
    "content": "import { SemVersion, isVersionGtOrEq } from './version';\n\ndescribe('SemVersion', () => {\n  let version = '1.0.0-alpha.1';\n\n  describe('parsing', () => {\n    it('should parse version properly', () => {\n      const semver = new SemVersion(version);\n      expect(semver.major).toBe(1);\n      expect(semver.minor).toBe(0);\n      expect(semver.patch).toBe(0);\n      expect(semver.meta).toBe('alpha.1');\n    });\n  });\n\n  describe('comparing', () => {\n    beforeEach(() => {\n      version = '3.4.5';\n    });\n\n    it('should detect greater version properly', () => {\n      const semver = new SemVersion(version);\n      const cases = [\n        { value: '3.4.5', expected: true },\n        { value: '3.4.4', expected: true },\n        { value: '3.4.6', expected: false },\n        { value: '4', expected: false },\n        { value: '3.5', expected: false },\n      ];\n      cases.forEach((testCase) => {\n        expect(semver.isGtOrEq(testCase.value)).toBe(testCase.expected);\n      });\n    });\n  });\n\n  describe('isVersionGtOrEq', () => {\n    it('should compare versions properly (a >= b)', () => {\n      const cases = [\n        { values: ['3.4.5', '3.4.5'], expected: true },\n        { values: ['3.4.5', '3.4.4'], expected: true },\n        { values: ['3.4.5', '3.4.6'], expected: false },\n        { values: ['3.4', '3.4.0'], expected: true },\n        { values: ['3', '3.0.0'], expected: true },\n        { values: ['3.1.1-beta1', '3.1'], expected: true },\n        { values: ['3.4.5', '4'], expected: false },\n        { values: ['3.4.5', '3.5'], expected: false },\n        { values: ['6.0.0', '5.2.0'], expected: true },\n        { values: ['3.1.1', '4.0.0-beta'], expected: false },\n      ];\n      cases.forEach((testCase) => {\n        expect(isVersionGtOrEq(testCase.values[0], testCase.values[1])).toBe(testCase.expected);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "src/utils/version.ts",
    "content": "import pluginPackage from '../../package.json';\nimport { isNumber } from 'lodash';\n\nexport const pluginVersion = pluginPackage.version;\n\nconst versionPattern = /^(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?(?:-([0-9A-Za-z\\.]+))?/;\n\nexport class SemVersion {\n  major: number;\n  minor: number;\n  patch: number;\n  meta: string;\n\n  constructor(version: string) {\n    this.major = 0;\n    this.minor = 0;\n    this.patch = 0;\n    this.meta = '';\n\n    const match = versionPattern.exec(version);\n    if (match) {\n      this.major = Number(match[1]);\n      this.minor = Number(match[2] || 0);\n      this.patch = Number(match[3] || 0);\n      this.meta = match[4];\n    }\n  }\n\n  isGtOrEq(version: string): boolean {\n    const compared = new SemVersion(version);\n\n    for (let i = 0; i < this.comparable.length; ++i) {\n      if (this.comparable[i] > compared.comparable[i]) {\n        return true;\n      }\n      if (this.comparable[i] < compared.comparable[i]) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  isValid(): boolean {\n    return isNumber(this.major);\n  }\n\n  get comparable() {\n    return [this.major, this.minor, this.patch];\n  }\n}\n\nexport function isVersionGtOrEq(a: string, b: string): boolean {\n  const aSemver = new SemVersion(a);\n  return aSemver.isGtOrEq(b);\n}\n"
  },
  {
    "path": "src/views/CHConfigEditor.test.tsx",
    "content": "import React from 'react';\nimport { render, screen } from '@testing-library/react';\nimport { ConfigEditor } from './CHConfigEditor';\nimport { mockConfigEditorProps } from '__mocks__/ConfigEditor';\nimport '@testing-library/jest-dom';\nimport { Protocol } from 'types/config';\nimport allLabels from 'labels';\n\njest.mock('@grafana/runtime', () => {\n  const original = jest.requireActual('@grafana/runtime');\n  return {\n    ...original,\n    config: { buildInfo: { version: '10.0.0' }, secureSocksDSProxyEnabled: true },\n  };\n});\n\ndescribe('ConfigEditor', () => {\n  const labels = allLabels.components.Config.ConfigEditor;\n\n  it('new editor', () => {\n    render(<ConfigEditor {...mockConfigEditorProps()} />);\n    expect(screen.getByPlaceholderText(labels.serverAddress.placeholder)).toBeInTheDocument();\n    expect(screen.getByPlaceholderText(labels.serverPort.insecureHttpPort)).toBeInTheDocument();\n    expect(screen.getByPlaceholderText(labels.username.placeholder)).toBeInTheDocument();\n    expect(screen.getByPlaceholderText(labels.password.placeholder)).toBeInTheDocument();\n    expect(screen.getByPlaceholderText(labels.path.placeholder)).toBeInTheDocument();\n  });\n  it('with password', async () => {\n    render(\n      <ConfigEditor\n        {...mockConfigEditorProps()}\n        options={{\n          ...mockConfigEditorProps().options,\n          secureJsonData: { password: 'foo' },\n          secureJsonFields: { password: true },\n        }}\n      />\n    );\n    expect(screen.getByPlaceholderText(labels.serverAddress.placeholder)).toBeInTheDocument();\n    expect(screen.getByPlaceholderText(labels.serverPort.insecureHttpPort)).toBeInTheDocument();\n    expect(screen.getByPlaceholderText(labels.username.placeholder)).toBeInTheDocument();\n    const a = screen.getByText('Reset');\n    expect(a).toBeInTheDocument();\n  });\n  it('with path', async () => {\n    const path = 'custom-path';\n    render(\n      <ConfigEditor\n        {...mockConfigEditorProps()}\n        options={{\n          ...mockConfigEditorProps().options,\n          jsonData: { ...mockConfigEditorProps().options.jsonData, path, protocol: Protocol.Http },\n        }}\n      />\n    );\n    expect(screen.queryByPlaceholderText(labels.path.placeholder)).toHaveValue(path);\n  });\n  it('with secure connection', async () => {\n    render(\n      <ConfigEditor\n        {...mockConfigEditorProps()}\n        options={{\n          ...mockConfigEditorProps().options,\n          jsonData: { ...mockConfigEditorProps().options.jsonData, secure: true },\n        }}\n      />\n    );\n    expect(screen.queryByPlaceholderText(labels.serverPort.secureHttpPort)).toBeInTheDocument();\n  });\n  it('with protocol', async () => {\n    render(\n      <ConfigEditor\n        {...mockConfigEditorProps()}\n        options={{\n          ...mockConfigEditorProps().options,\n          jsonData: { ...mockConfigEditorProps().options.jsonData, protocol: Protocol.Http },\n        }}\n      />\n    );\n    expect(screen.getAllByLabelText('HTTP').pop()).toBeInTheDocument();\n    expect(screen.getAllByLabelText('HTTP').pop()).toBeChecked();\n  });\n  it('without tlsCACert', async () => {\n    render(<ConfigEditor {...mockConfigEditorProps()} />);\n    expect(screen.queryByPlaceholderText(labels.tlsCACert.placeholder)).not.toBeInTheDocument();\n  });\n  it('with tlsCACert', async () => {\n    render(\n      <ConfigEditor\n        {...mockConfigEditorProps()}\n        options={{\n          ...mockConfigEditorProps().options,\n          jsonData: { ...mockConfigEditorProps().options.jsonData, tlsAuthWithCACert: true },\n        }}\n      />\n    );\n    expect(screen.getByPlaceholderText(labels.tlsCACert.placeholder)).toBeInTheDocument();\n  });\n  it('without tlsAuth', async () => {\n    render(<ConfigEditor {...mockConfigEditorProps()} />);\n    expect(screen.queryByPlaceholderText(labels.tlsClientCert.placeholder)).not.toBeInTheDocument();\n    expect(screen.queryByPlaceholderText(labels.tlsClientKey.placeholder)).not.toBeInTheDocument();\n  });\n  it('with tlsAuth', async () => {\n    render(\n      <ConfigEditor\n        {...mockConfigEditorProps()}\n        options={{\n          ...mockConfigEditorProps().options,\n          jsonData: { ...mockConfigEditorProps().options.jsonData, tlsAuth: true },\n        }}\n      />\n    );\n    expect(screen.getByPlaceholderText(labels.tlsClientCert.placeholder)).toBeInTheDocument();\n    expect(screen.getByPlaceholderText(labels.tlsClientKey.placeholder)).toBeInTheDocument();\n  });\n  it('with additional properties', async () => {\n    const jsonDataOverrides = {\n      defaultDatabase: 'default',\n      queryTimeout: '100',\n      dialTimeout: '100',\n      validateSql: true,\n      customSettings: [{ setting: 'test-setting', value: 'test-value' }],\n      forwardGrafanaHeaders: true,\n      enableRowLimit: true,\n    };\n    render(<ConfigEditor {...mockConfigEditorProps(jsonDataOverrides)} />);\n    expect(screen.getByText(labels.secureSocksProxy.label)).toBeInTheDocument();\n    expect(screen.getByDisplayValue(jsonDataOverrides.customSettings[0].setting)).toBeInTheDocument();\n    expect(screen.getByDisplayValue(jsonDataOverrides.customSettings[0].value)).toBeInTheDocument();\n    expect(screen.getByText(labels.enableRowLimit.label)).toBeInTheDocument();\n    expect(screen.getByTestId(labels.enableRowLimit.testid)).toBeChecked();\n  });\n});\n"
  },
  {
    "path": "src/views/CHConfigEditor.tsx",
    "content": "import React, { ChangeEvent, useEffect, useMemo, useState } from 'react';\nimport {\n  DataSourcePluginOptionsEditorProps,\n  onUpdateDatasourceJsonDataOption,\n  onUpdateDatasourceSecureJsonDataOption,\n} from '@grafana/data';\nimport { RadioButtonGroup, Switch, Input, SecretInput, Button, Field, Alert, Stack } from '@grafana/ui';\nimport { CertificationKey } from '../components/ui/CertificationKey';\nimport {\n  CHConfig,\n  CHCustomSetting,\n  CHSecureConfig,\n  CHLogsConfig,\n  Protocol,\n  CHTracesConfig,\n  AliasTableEntry,\n} from 'types/config';\nimport { gte as versionGte } from 'semver';\nimport { ConfigSection, ConfigSubSection, DataSourceDescription } from 'components/experimental/ConfigSection';\nimport { config } from '@grafana/runtime';\nimport { Divider } from 'components/Divider';\nimport { TimeUnit } from 'types/queryBuilder';\nimport { DefaultDatabaseTableConfig } from 'components/configEditor/DefaultDatabaseTableConfig';\nimport { QuerySettingsConfig } from 'components/configEditor/QuerySettingsConfig';\nimport { LogsConfig } from 'components/configEditor/LogsConfig';\nimport { TracesConfig } from 'components/configEditor/TracesConfig';\nimport { HttpHeadersConfig } from 'components/configEditor/HttpHeadersConfig';\nimport allLabels from '../labels';\nimport { createValidationAPI, onHttpHeadersChange, useConfigDefaults } from './CHConfigEditorHooks';\nimport { AliasTableConfig } from '../components/configEditor/AliasTableConfig';\nimport * as trackingV1 from './trackingV1';\n\nexport interface ConfigEditorProps extends DataSourcePluginOptionsEditorProps<CHConfig, CHSecureConfig> {}\n\nexport const ConfigEditor: React.FC<ConfigEditorProps> = (props) => {\n  const { options, onOptionsChange } = props;\n  const { jsonData, secureJsonFields } = options;\n\n  const validationEnabled = (config.featureToggles as Record<string, boolean | undefined> | undefined)?.[\n    'clickHouseConfigValidation'\n  ];\n  const validation = useMemo(\n    () => (validationEnabled ? (props.validation ?? createValidationAPI()) : undefined),\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [props.validation]\n  );\n  const labels = allLabels.components.Config.ConfigEditor;\n  const secureJsonData = (options.secureJsonData || {}) as CHSecureConfig;\n  const hasTLSCACert = secureJsonFields && secureJsonFields.tlsCACert;\n  const hasTLSClientCert = secureJsonFields && secureJsonFields.tlsClientCert;\n  const hasTLSClientKey = secureJsonFields && secureJsonFields.tlsClientKey;\n  const protocolOptions = [\n    { label: 'Native', value: Protocol.Native },\n    { label: 'HTTP', value: Protocol.Http },\n  ];\n\n  useConfigDefaults(options, onOptionsChange);\n\n  // Register a validator for required fields. The validator runs when\n  // validation.validate() is called by Grafana before saving — if it returns\n  // false, the save and backend health check are both blocked.\n  //\n  // We also eagerly call clearError in the effect body so that inline errors\n  // disappear as soon as the user fills in a field, rather than waiting for\n  // the next save attempt.\n  useEffect(() => {\n    if (!validation) {\n      return;\n    }\n    if (jsonData.host) {\n      validation.clearError('host');\n    }\n    if (jsonData.port) {\n      validation.clearError('port');\n    }\n\n    return validation.registerValidation(() => {\n      let valid = true;\n      if (!jsonData.host) {\n        validation.setError('host', labels.serverAddress.error);\n        valid = false;\n      } else {\n        validation.clearError('host');\n      }\n      if (!jsonData.port) {\n        validation.setError('port', labels.serverPort.error);\n        valid = false;\n      } else {\n        validation.clearError('port');\n      }\n      return valid;\n    });\n  }, [jsonData.host, jsonData.port, validation, labels.serverAddress.error, labels.serverPort.error]);\n\n  const fieldErrors = validation?.getErrors() ?? {};\n\n  const onPortChange = (port: string) => {\n    onOptionsChange({\n      ...options,\n      jsonData: {\n        ...options.jsonData,\n        port: +port,\n      },\n    });\n  };\n  const onTLSSettingsChange = (\n    key: keyof Pick<CHConfig, 'tlsSkipVerify' | 'tlsAuth' | 'tlsAuthWithCACert'>,\n    value: boolean\n  ) => {\n    onOptionsChange({\n      ...options,\n      jsonData: {\n        ...options.jsonData,\n        [key]: value,\n      },\n    });\n  };\n  const onSwitchToggle = (\n    key: keyof Pick<\n      CHConfig,\n      | 'secure'\n      | 'validateSql'\n      | 'enableSecureSocksProxy'\n      | 'forwardGrafanaHeaders'\n      | 'enableRowLimit'\n      | 'hideTableNameInAdhocFilters'\n    >,\n    value: boolean\n  ) => {\n    onOptionsChange({\n      ...options,\n      jsonData: {\n        ...options.jsonData,\n        [key]: value,\n      },\n    });\n  };\n\n  const onProtocolToggle = (protocol: Protocol) => {\n    onOptionsChange({\n      ...options,\n      jsonData: {\n        ...options.jsonData,\n        protocol: protocol,\n      },\n    });\n  };\n\n  const onCertificateChangeFactory = (key: keyof Omit<CHSecureConfig, 'password'>, value: string) => {\n    onOptionsChange({\n      ...options,\n      secureJsonData: {\n        ...secureJsonData,\n        [key]: value,\n      },\n    });\n  };\n  const onResetClickFactory = (key: keyof Omit<CHSecureConfig, 'password'>) => {\n    onOptionsChange({\n      ...options,\n      secureJsonFields: {\n        ...secureJsonFields,\n        [key]: false,\n      },\n      secureJsonData: {\n        ...secureJsonData,\n        [key]: '',\n      },\n    });\n  };\n  const onResetPassword = () => {\n    onOptionsChange({\n      ...options,\n      secureJsonFields: {\n        ...options.secureJsonFields,\n        password: false,\n      },\n      secureJsonData: {\n        ...options.secureJsonData,\n        password: '',\n      },\n    });\n  };\n  const onCustomSettingsChange = (customSettings: CHCustomSetting[]) => {\n    onOptionsChange({\n      ...options,\n      jsonData: {\n        ...options.jsonData,\n        customSettings: customSettings.filter((s) => !!s.setting && !!s.value),\n      },\n    });\n  };\n  const onLogsConfigChange = (key: keyof CHLogsConfig, value: string | boolean | string[]) => {\n    onOptionsChange({\n      ...options,\n      jsonData: {\n        ...options.jsonData,\n        logs: {\n          ...options.jsonData.logs,\n          [key]: value,\n        },\n      },\n    });\n  };\n  const onTracesConfigChange = (key: keyof CHTracesConfig, value: string | boolean) => {\n    onOptionsChange({\n      ...options,\n      jsonData: {\n        ...options.jsonData,\n        traces: {\n          ...options.jsonData.traces,\n          durationUnit: options.jsonData.traces?.durationUnit || TimeUnit.Nanoseconds,\n          [key]: value,\n        },\n      },\n    });\n  };\n  const onAliasTableConfigChange = (aliasTables: AliasTableEntry[]) => {\n    // track events when both a target table and alias table has a value\n    if (aliasTables.length > 0 && aliasTables[0].targetTable && aliasTables[0].aliasTable) {\n      trackingV1.trackClickhouseConfigV1ColumnAliasTableAdded();\n    }\n    onOptionsChange({\n      ...options,\n      jsonData: {\n        ...options.jsonData,\n        aliasTables,\n      },\n    });\n  };\n\n  const [customSettings, setCustomSettings] = useState(jsonData.customSettings || []);\n\n  const hasAdditionalSettings = Boolean(\n    window.location.hash || // if trying to link to section on page, open all settings (React breaks this?)\n    options.jsonData.defaultDatabase ||\n    options.jsonData.defaultTable ||\n    options.jsonData.dialTimeout ||\n    options.jsonData.queryTimeout ||\n    options.jsonData.validateSql ||\n    options.jsonData.enableSecureSocksProxy ||\n    options.jsonData.customSettings ||\n    options.jsonData.logs ||\n    options.jsonData.traces\n  );\n\n  const defaultPort = jsonData.secure\n    ? jsonData.protocol === Protocol.Native\n      ? labels.serverPort.secureNativePort\n      : labels.serverPort.secureHttpPort\n    : jsonData.protocol === Protocol.Native\n      ? labels.serverPort.insecureNativePort\n      : labels.serverPort.insecureHttpPort;\n  const portDescription = `${labels.serverPort.tooltip} (default for ${jsonData.secure ? 'secure' : ''} ${jsonData.protocol}: ${defaultPort})`;\n\n  const uidWarning = !options.uid && (\n    <Alert title=\"\" severity=\"warning\" buttonContent=\"Close\">\n      <Stack>\n        <div>\n          {'This datasource is missing the'}\n          <code>uid</code>\n          {'field in its configuration. If your datasource is '}\n          <a\n            style={{ textDecoration: 'underline' }}\n            href=\"https://grafana.com/docs/grafana/latest/administration/provisioning/#data-sources\"\n            target=\"_blank\"\n            rel=\"noreferrer\"\n          >\n            provisioned via YAML\n          </a>\n          {', please verify the UID is set. This is required to enable data linking between logs and traces.'}\n        </div>\n      </Stack>\n    </Alert>\n  );\n\n  return (\n    <>\n      {uidWarning}\n      <DataSourceDescription\n        dataSourceName=\"Clickhouse\"\n        docsLink=\"https://grafana.com/grafana/plugins/grafana-clickhouse-datasource/\"\n        hasRequiredFields\n      />\n      <Divider />\n      <ConfigSection title=\"Server\">\n        <Field\n          required\n          label={labels.serverAddress.label}\n          description={labels.serverAddress.tooltip}\n          invalid={!!fieldErrors.host}\n          error={fieldErrors.host}\n        >\n          <Input\n            name=\"host\"\n            width={80}\n            value={jsonData.host || ''}\n            onChange={(e) => onUpdateDatasourceJsonDataOption(props, 'host')(e)}\n            label={labels.serverAddress.label}\n            aria-label={labels.serverAddress.label}\n            placeholder={labels.serverAddress.placeholder}\n            onBlur={trackingV1.trackClickhouseConfigV1HostInput}\n          />\n        </Field>\n        <Field\n          required\n          label={labels.serverPort.label}\n          description={portDescription}\n          invalid={!!fieldErrors.port}\n          error={fieldErrors.port}\n        >\n          <Input\n            name=\"port\"\n            width={40}\n            type=\"number\"\n            value={jsonData.port || ''}\n            onChange={(e) => onPortChange(e.currentTarget.value)}\n            label={labels.serverPort.label}\n            aria-label={labels.serverPort.label}\n            placeholder={defaultPort}\n            onBlur={(e) => trackingV1.trackClickhouseConfigV1PortInput({ port: e.currentTarget.value })}\n          />\n        </Field>\n\n        <Field label={labels.protocol.label} description={labels.protocol.tooltip}>\n          <RadioButtonGroup<Protocol>\n            options={protocolOptions}\n            disabledOptions={[]}\n            value={jsonData.protocol || Protocol.Native}\n            onChange={(e) => {\n              trackingV1.trackClickhouseConfigV1NativeHttpToggleClicked({ nativeHttpToggle: e });\n              onProtocolToggle(e!);\n            }}\n          />\n        </Field>\n        <Field label={labels.secure.label} description={labels.secure.tooltip}>\n          <Switch\n            id=\"secure\"\n            className=\"gf-form\"\n            value={jsonData.secure || false}\n            onChange={(e) => {\n              trackingV1.trackClickhouseConfigV1SecureConnectionToggleClicked({\n                secureConnection: e.currentTarget.checked,\n              });\n              onSwitchToggle('secure', e.currentTarget.checked);\n            }}\n          />\n        </Field>\n\n        {jsonData.protocol === Protocol.Http && (\n          <Field label={labels.path.label} description={labels.path.tooltip}>\n            <Input\n              value={jsonData.path || ''}\n              name=\"path\"\n              width={80}\n              onChange={onUpdateDatasourceJsonDataOption(props, 'path')}\n              label={labels.path.label}\n              aria-label={labels.path.label}\n              placeholder={labels.path.placeholder}\n            />\n          </Field>\n        )}\n      </ConfigSection>\n\n      {jsonData.protocol === Protocol.Http && (\n        <HttpHeadersConfig\n          headers={options.jsonData.httpHeaders}\n          forwardGrafanaHeaders={options.jsonData.forwardGrafanaHeaders}\n          secureFields={options.secureJsonFields}\n          onHttpHeadersChange={(headers) => onHttpHeadersChange(headers, options, onOptionsChange)}\n          onForwardGrafanaHeadersChange={(forwardGrafanaHeaders) =>\n            onSwitchToggle('forwardGrafanaHeaders', forwardGrafanaHeaders)\n          }\n        />\n      )}\n\n      <Divider />\n      <ConfigSection title=\"TLS / SSL Settings\">\n        <Field label={labels.tlsSkipVerify.label} description={labels.tlsSkipVerify.tooltip}>\n          <Switch\n            className=\"gf-form\"\n            value={jsonData.tlsSkipVerify || false}\n            onChange={(e) => {\n              trackingV1.trackClickhouseConfigV1SkipTLSVerifyToggleClicked({\n                skipTlsVerifyToggle: e.currentTarget.checked,\n              });\n              onTLSSettingsChange('tlsSkipVerify', e.currentTarget.checked);\n            }}\n          />\n        </Field>\n        <Field label={labels.tlsClientAuth.label} description={labels.tlsClientAuth.tooltip}>\n          <Switch\n            className=\"gf-form\"\n            value={jsonData.tlsAuth || false}\n            onChange={(e) => {\n              trackingV1.trackClickhouseConfigV1TLSClientAuthToggleClicked({\n                clientAuthToggle: e.currentTarget.checked,\n              });\n              onTLSSettingsChange('tlsAuth', e.currentTarget.checked);\n            }}\n          />\n        </Field>\n        <Field label={labels.tlsAuthWithCACert.label} description={labels.tlsAuthWithCACert.tooltip}>\n          <Switch\n            className=\"gf-form\"\n            value={jsonData.tlsAuthWithCACert || false}\n            onChange={(e) => {\n              trackingV1.trackClickhouseConfigV1WithCACertToggleClicked({ caCertToggle: e.currentTarget.checked });\n              onTLSSettingsChange('tlsAuthWithCACert', e.currentTarget.checked);\n            }}\n          />\n        </Field>\n        {jsonData.tlsAuthWithCACert && (\n          <CertificationKey\n            hasCert={!!hasTLSCACert}\n            onChange={(e) => onCertificateChangeFactory('tlsCACert', e.currentTarget.value)}\n            placeholder={labels.tlsCACert.placeholder}\n            label={labels.tlsCACert.label}\n            onClick={() => onResetClickFactory('tlsCACert')}\n          />\n        )}\n        {jsonData.tlsAuth && (\n          <>\n            <CertificationKey\n              hasCert={!!hasTLSClientCert}\n              onChange={(e) => onCertificateChangeFactory('tlsClientCert', e.currentTarget.value)}\n              placeholder={labels.tlsClientCert.placeholder}\n              label={labels.tlsClientCert.label}\n              onClick={() => onResetClickFactory('tlsClientCert')}\n            />\n            <CertificationKey\n              hasCert={!!hasTLSClientKey}\n              placeholder={labels.tlsClientKey.placeholder}\n              label={labels.tlsClientKey.label}\n              onChange={(e) => onCertificateChangeFactory('tlsClientKey', e.currentTarget.value)}\n              onClick={() => onResetClickFactory('tlsClientKey')}\n            />\n          </>\n        )}\n      </ConfigSection>\n\n      <Divider />\n      <ConfigSection title=\"Credentials\">\n        <Field label={labels.username.label} description={labels.username.tooltip}>\n          <Input\n            name=\"user\"\n            width={40}\n            value={jsonData.username || ''}\n            onChange={onUpdateDatasourceJsonDataOption(props, 'username')}\n            label={labels.username.label}\n            aria-label={labels.username.label}\n            placeholder={labels.username.placeholder}\n          />\n        </Field>\n        <Field label={labels.password.label} description={labels.password.tooltip}>\n          <SecretInput\n            name=\"pwd\"\n            width={40}\n            label={labels.password.label}\n            aria-label={labels.password.label}\n            placeholder={labels.password.placeholder}\n            value={secureJsonData.password || ''}\n            isConfigured={(secureJsonFields && secureJsonFields.password) as boolean}\n            onReset={onResetPassword}\n            onChange={onUpdateDatasourceSecureJsonDataOption(props, 'password')}\n          />\n        </Field>\n      </ConfigSection>\n\n      <Divider />\n      <ConfigSection\n        title=\"Additional settings\"\n        description=\"Additional settings are optional settings that can be configured for more control over your data source. This includes the default database, dial and query timeouts, SQL validation, and custom ClickHouse settings.\"\n        isCollapsible\n        isInitiallyOpen={hasAdditionalSettings}\n      >\n        <Divider />\n        <DefaultDatabaseTableConfig\n          defaultDatabase={jsonData.defaultDatabase}\n          defaultTable={jsonData.defaultTable}\n          onDefaultDatabaseChange={(e) => {\n            trackingV1.trackClickhouseConfigV1DefaultDbInput();\n            onUpdateDatasourceJsonDataOption(props, 'defaultDatabase')(e);\n          }}\n          onDefaultTableChange={(e) => {\n            trackingV1.trackClickhouseConfigV1DefaultTableInput();\n            onUpdateDatasourceJsonDataOption(props, 'defaultTable')(e);\n          }}\n        />\n\n        <Divider />\n        <QuerySettingsConfig\n          connMaxLifetime={jsonData.connMaxLifetime}\n          dialTimeout={jsonData.dialTimeout}\n          maxIdleConns={jsonData.maxIdleConns}\n          maxOpenConns={jsonData.maxOpenConns}\n          queryTimeout={jsonData.queryTimeout}\n          validateSql={jsonData.validateSql}\n          onDialTimeoutChange={(e) => {\n            trackingV1.trackClickhouseConfigV1QuerySettings({ dialTimeout: Number(e.currentTarget.value) });\n            onUpdateDatasourceJsonDataOption(props, 'dialTimeout')(e);\n          }}\n          onQueryTimeoutChange={(e) => {\n            trackingV1.trackClickhouseConfigV1QuerySettings({ queryTimeout: Number(e.currentTarget.value) });\n            onUpdateDatasourceJsonDataOption(props, 'queryTimeout')(e);\n          }}\n          onConnMaxLifetimeChange={(e) => {\n            trackingV1.trackClickhouseConfigV1QuerySettings({ connMaxLifetime: Number(e.currentTarget.value) });\n            onUpdateDatasourceJsonDataOption(props, 'connMaxLifetime')(e);\n          }}\n          onConnMaxIdleConnsChange={(e) => {\n            trackingV1.trackClickhouseConfigV1QuerySettings({ maxIdleConns: Number(e.currentTarget.value) });\n            onUpdateDatasourceJsonDataOption(props, 'maxIdleConns')(e);\n          }}\n          onConnMaxOpenConnsChange={(e) => {\n            trackingV1.trackClickhouseConfigV1QuerySettings({ maxOpenConns: Number(e.currentTarget.value) });\n            onUpdateDatasourceJsonDataOption(props, 'maxOpenConns')(e);\n          }}\n          onValidateSqlChange={(e) => {\n            trackingV1.trackClickhouseConfigV1QuerySettings({ validateSql: e.currentTarget.checked });\n            onSwitchToggle('validateSql', e.currentTarget.checked);\n          }}\n        />\n\n        <Divider />\n        <LogsConfig\n          logsConfig={jsonData.logs}\n          onDefaultDatabaseChange={(db) => {\n            trackingV1.trackClickhouseConfigV1LogsConfig({ defaultDatabase: db });\n            onLogsConfigChange('defaultDatabase', db);\n          }}\n          onDefaultTableChange={(table) => {\n            trackingV1.trackClickhouseConfigV1LogsConfig({ defaultTable: table });\n            onLogsConfigChange('defaultTable', table);\n          }}\n          onOtelEnabledChange={(v) => {\n            trackingV1.trackClickhouseConfigV1LogsConfig({ otelEnabled: v });\n            onLogsConfigChange('otelEnabled', v);\n          }}\n          onOtelVersionChange={(v) => {\n            trackingV1.trackClickhouseConfigV1LogsConfig({ version: v });\n            onLogsConfigChange('otelVersion', v);\n          }}\n          onFilterTimeColumnChange={(c) => {\n            trackingV1.trackClickhouseConfigV1LogsConfig({ filterTimeColumn: c });\n            onLogsConfigChange('filterTimeColumn', c);\n          }}\n          onTimeColumnChange={(c) => {\n            trackingV1.trackClickhouseConfigV1LogsConfig({ timeColumn: c });\n            onLogsConfigChange('timeColumn', c);\n          }}\n          onLevelColumnChange={(c) => {\n            trackingV1.trackClickhouseConfigV1LogsConfig({ levelColumn: c });\n            onLogsConfigChange('levelColumn', c);\n          }}\n          onMessageColumnChange={(c) => {\n            trackingV1.trackClickhouseConfigV1LogsConfig({ messageColumn: c });\n            onLogsConfigChange('messageColumn', c);\n          }}\n          onSelectContextColumnsChange={(c) => {\n            trackingV1.trackClickhouseConfigV1LogsConfig({ selectContextColumns: c });\n            onLogsConfigChange('selectContextColumns', c);\n          }}\n          onContextColumnsChange={(c) => {\n            trackingV1.trackClickhouseConfigV1LogsConfig({ contextColumns: c });\n            onLogsConfigChange('contextColumns', c);\n          }}\n          onShowLogLinksChange={(v) => {\n            onLogsConfigChange('showLogLinks', v);\n          }}\n        />\n\n        <Divider />\n        <TracesConfig\n          tracesConfig={jsonData.traces}\n          onDefaultDatabaseChange={(db) => {\n            trackingV1.trackClickhouseConfigV1TracesConfig({ defaultDatabase: db });\n            onTracesConfigChange('defaultDatabase', db);\n          }}\n          onDefaultTableChange={(table) => {\n            trackingV1.trackClickhouseConfigV1TracesConfig({ defaultTable: table });\n            onTracesConfigChange('defaultTable', table);\n          }}\n          onOtelEnabledChange={(v) => {\n            trackingV1.trackClickhouseConfigV1TracesConfig({ otelEnabled: v });\n            onTracesConfigChange('otelEnabled', v);\n          }}\n          onOtelVersionChange={(v) => {\n            trackingV1.trackClickhouseConfigV1TracesConfig({ version: v });\n            onTracesConfigChange('otelVersion', v);\n          }}\n          onTraceIdColumnChange={(c) => {\n            trackingV1.trackClickhouseConfigV1TracesConfig({ traceIdColumn: c });\n            onTracesConfigChange('traceIdColumn', c);\n          }}\n          onSpanIdColumnChange={(c) => {\n            trackingV1.trackClickhouseConfigV1TracesConfig({ spanIdColumn: c });\n            onTracesConfigChange('spanIdColumn', c);\n          }}\n          onOperationNameColumnChange={(c) => {\n            trackingV1.trackClickhouseConfigV1TracesConfig({ operationNameColumn: c });\n            onTracesConfigChange('operationNameColumn', c);\n          }}\n          onParentSpanIdColumnChange={(c) => {\n            trackingV1.trackClickhouseConfigV1TracesConfig({ parentSpanIdColumn: c });\n            onTracesConfigChange('parentSpanIdColumn', c);\n          }}\n          onServiceNameColumnChange={(c) => {\n            trackingV1.trackClickhouseConfigV1TracesConfig({ serviceNameColumn: c });\n            onTracesConfigChange('serviceNameColumn', c);\n          }}\n          onDurationColumnChange={(c) => {\n            trackingV1.trackClickhouseConfigV1TracesConfig({ durationColumn: c });\n            onTracesConfigChange('durationColumn', c);\n          }}\n          onDurationUnitChange={(c) => {\n            trackingV1.trackClickhouseConfigV1TracesConfig({ durationUnit: c });\n            onTracesConfigChange('durationUnit', c);\n          }}\n          onStartTimeColumnChange={(c) => {\n            trackingV1.trackClickhouseConfigV1TracesConfig({ startTimeColumn: c });\n            onTracesConfigChange('startTimeColumn', c);\n          }}\n          onTagsColumnChange={(c) => {\n            trackingV1.trackClickhouseConfigV1TracesConfig({ tagsColumn: c });\n            onTracesConfigChange('tagsColumn', c);\n          }}\n          onServiceTagsColumnChange={(c) => {\n            trackingV1.trackClickhouseConfigV1TracesConfig({ serviceTagsColumn: c });\n            onTracesConfigChange('serviceTagsColumn', c);\n          }}\n          onKindColumnChange={(c) => {\n            trackingV1.trackClickhouseConfigV1TracesConfig({ kindColumn: c });\n            onTracesConfigChange('kindColumn', c);\n          }}\n          onStatusCodeColumnChange={(c) => {\n            trackingV1.trackClickhouseConfigV1TracesConfig({ statusCodeColumn: c });\n            onTracesConfigChange('statusCodeColumn', c);\n          }}\n          onStatusMessageColumnChange={(c) => {\n            trackingV1.trackClickhouseConfigV1TracesConfig({ statusMessageColumn: c });\n            onTracesConfigChange('statusMessageColumn', c);\n          }}\n          onStateColumnChange={(c) => {\n            trackingV1.trackClickhouseConfigV1TracesConfig({ stateColumn: c });\n            onTracesConfigChange('stateColumn', c);\n          }}\n          onInstrumentationLibraryNameColumnChange={(c) => {\n            trackingV1.trackClickhouseConfigV1TracesConfig({ instrumentationLibraryNameColumn: c });\n            onTracesConfigChange('instrumentationLibraryNameColumn', c);\n          }}\n          onInstrumentationLibraryVersionColumnChange={(c) => {\n            trackingV1.trackClickhouseConfigV1TracesConfig({ instrumentationLibraryVersionColumn: c });\n            onTracesConfigChange('instrumentationLibraryVersionColumn', c);\n          }}\n          onFlattenNestedChange={(c) => {\n            trackingV1.trackClickhouseConfigV1TracesConfig({ flattenNested: c });\n            onTracesConfigChange('flattenNested', c);\n          }}\n          onEventsColumnPrefixChange={(c) => {\n            trackingV1.trackClickhouseConfigV1TracesConfig({ traceEventsColumnPrefix: c });\n            onTracesConfigChange('traceEventsColumnPrefix', c);\n          }}\n          onLinksColumnPrefixChange={(c) => {\n            trackingV1.trackClickhouseConfigV1TracesConfig({ traceLinksColumnPrefix: c });\n            onTracesConfigChange('traceLinksColumnPrefix', c);\n          }}\n          onShowTraceLinksChange={(v) => {\n            onTracesConfigChange('showTraceLinks', v);\n          }}\n          onTraceTimestampTableSuffixChange={(c) => {\n            trackingV1.trackClickhouseConfigV1TracesConfig({ traceTimestampTableSuffix: c });\n            onTracesConfigChange('traceTimestampTableSuffix', c);\n          }}\n        />\n\n        <Divider />\n        <AliasTableConfig aliasTables={jsonData.aliasTables} onAliasTablesChange={onAliasTableConfigChange} />\n        <Divider />\n        <Field label={labels.enableRowLimit.label} description={labels.enableRowLimit.tooltip}>\n          <Switch\n            className=\"gf-form\"\n            value={jsonData.enableRowLimit || false}\n            data-testid={labels.enableRowLimit.testid}\n            onChange={(e) => {\n              trackingV1.trackClickhouseConfigV1EnableRowLimitToggle({ rowLimitEnabled: e.currentTarget.checked });\n              onSwitchToggle('enableRowLimit', e.currentTarget.checked);\n            }}\n          />\n        </Field>\n        <Field\n          label={labels.hideTableNameInAdhocFilters.label}\n          description={labels.hideTableNameInAdhocFilters.tooltip}\n        >\n          <Switch\n            className=\"gf-form\"\n            value={jsonData.hideTableNameInAdhocFilters || false}\n            data-testid={labels.hideTableNameInAdhocFilters.testid}\n            onChange={(e) => {\n              onSwitchToggle('hideTableNameInAdhocFilters', e.currentTarget.checked);\n            }}\n          />\n        </Field>\n        {config.secureSocksDSProxyEnabled && versionGte(config.buildInfo.version, '10.0.0') && (\n          <Field label={labels.secureSocksProxy.label} description={labels.secureSocksProxy.tooltip}>\n            <Switch\n              className=\"gf-form\"\n              value={jsonData.enableSecureSocksProxy || false}\n              onChange={(e) => onSwitchToggle('enableSecureSocksProxy', e.currentTarget.checked)}\n            />\n          </Field>\n        )}\n        <ConfigSubSection title=\"Custom Settings\">\n          {customSettings.map(({ setting, value }, i) => {\n            return (\n              <Stack key={i} direction=\"row\">\n                <Field label={`Setting`} aria-label={`Setting`}>\n                  <Input\n                    value={setting}\n                    placeholder={'Setting'}\n                    onChange={(changeEvent: ChangeEvent<HTMLInputElement>) => {\n                      let newSettings = customSettings.concat();\n                      newSettings[i] = { setting: changeEvent.target.value, value };\n                      setCustomSettings(newSettings);\n                    }}\n                    onBlur={() => {\n                      trackingV1.trackClickhouseConfigV1CustomSettingAdded();\n                      onCustomSettingsChange(customSettings);\n                    }}\n                  ></Input>\n                </Field>\n                <Field label={'Value'} aria-label={`Value`}>\n                  <Input\n                    value={value}\n                    placeholder={'Value'}\n                    onChange={(changeEvent: ChangeEvent<HTMLInputElement>) => {\n                      let newSettings = customSettings.concat();\n                      newSettings[i] = { setting, value: changeEvent.target.value };\n                      setCustomSettings(newSettings);\n                    }}\n                    onBlur={() => {\n                      onCustomSettingsChange(customSettings);\n                    }}\n                  ></Input>\n                </Field>\n              </Stack>\n            );\n          })}\n          <Button\n            variant=\"secondary\"\n            icon=\"plus\"\n            type=\"button\"\n            onClick={() => {\n              setCustomSettings([...customSettings, { setting: '', value: '' }]);\n            }}\n          >\n            Add custom setting\n          </Button>\n        </ConfigSubSection>\n      </ConfigSection>\n    </>\n  );\n};\n"
  },
  {
    "path": "src/views/CHConfigEditorHooks.test.ts",
    "content": "import { DataSourceSettings } from '@grafana/data';\nimport { renderHook } from '@testing-library/react';\nimport { CHConfig, CHHttpHeader, CHSecureConfig, defaultCHAdditionalSettingsConfig, Protocol } from 'types/config';\nimport { onHttpHeadersChange, useConfigDefaults } from './CHConfigEditorHooks';\nimport { pluginVersion } from 'utils/version';\n\ndescribe('onHttpHeadersChange', () => {\n  it('should properly sort headers into secure/plain config fields', async () => {\n    const onOptionsChange = jest.fn();\n    const headers: CHHttpHeader[] = [\n      {\n        name: 'X-Existing-Auth-Header',\n        value: '',\n        secure: true,\n      },\n      {\n        name: 'X-Existing-Header',\n        value: 'existing value',\n        secure: false,\n      },\n      {\n        name: 'Authorization',\n        value: 'secret1234',\n        secure: true,\n      },\n      {\n        name: 'X-Custom-Header',\n        value: 'plain text value',\n        secure: false,\n      },\n    ];\n    const opts = {\n      jsonData: {\n        httpHeaders: [\n          {\n            name: 'X-Existing-Auth-Header',\n            value: '',\n            secure: true,\n          },\n          {\n            name: 'X-Existing-Header',\n            value: 'existing value',\n            secure: false,\n          },\n        ],\n      },\n      secureJsonFields: {\n        'secureHttpHeaders.X-Existing-Auth-Header': true,\n      },\n    } as any as DataSourceSettings<CHConfig, CHSecureConfig>;\n\n    onHttpHeadersChange(headers, opts, onOptionsChange);\n\n    const expectedOptions = {\n      jsonData: {\n        httpHeaders: [\n          {\n            name: 'X-Existing-Auth-Header',\n            value: '',\n            secure: true,\n          },\n          {\n            name: 'X-Existing-Header',\n            value: 'existing value',\n            secure: false,\n          },\n          {\n            name: 'Authorization',\n            value: '',\n            secure: true,\n          },\n          {\n            name: 'X-Custom-Header',\n            value: 'plain text value',\n            secure: false,\n          },\n        ],\n      },\n      secureJsonFields: {\n        'secureHttpHeaders.X-Existing-Auth-Header': true,\n        'secureHttpHeaders.Authorization': true,\n      },\n      secureJsonData: {\n        'secureHttpHeaders.Authorization': 'secret1234',\n      },\n    };\n    expect(onOptionsChange).toHaveBeenCalledTimes(1);\n    expect(onOptionsChange).toHaveBeenCalledWith(expect.objectContaining(expectedOptions));\n  });\n});\n\ndescribe('useConfigDefaults', () => {\n  const expectedDefaults: Partial<CHConfig> = {\n    version: pluginVersion,\n    protocol: Protocol.Native,\n    logs: {\n      defaultTable: defaultCHAdditionalSettingsConfig.logs?.defaultTable,\n      selectContextColumns: true,\n      contextColumns: [],\n    },\n    traces: {\n      defaultTable: defaultCHAdditionalSettingsConfig.traces?.defaultTable,\n    },\n  };\n\n  it('should rename v3 fields to latest config names', async () => {\n    const onOptionsChange = jest.fn();\n    const options = {\n      jsonData: {\n        server: 'address',\n        timeout: '8',\n      },\n    } as any as DataSourceSettings<CHConfig>;\n\n    renderHook((opts) => useConfigDefaults(opts, onOptionsChange), { initialProps: options });\n\n    const expectedOptions = {\n      jsonData: {\n        ...expectedDefaults,\n        host: 'address',\n        dialTimeout: '8',\n      },\n    };\n    expect(onOptionsChange).toHaveBeenCalledTimes(1);\n    expect(onOptionsChange).toHaveBeenCalledWith(expect.objectContaining(expectedOptions));\n  });\n\n  it('should rename v3 fields to latest config names if only server name is present', async () => {\n    const onOptionsChange = jest.fn();\n    const options = {\n      jsonData: {\n        server: 'address',\n      },\n    } as any as DataSourceSettings<CHConfig>;\n\n    renderHook((opts) => useConfigDefaults(opts, onOptionsChange), { initialProps: options });\n\n    const expectedOptions = {\n      jsonData: {\n        ...expectedDefaults,\n        host: 'address',\n      },\n    };\n    expect(onOptionsChange).toHaveBeenCalledTimes(1);\n    expect(onOptionsChange).toHaveBeenCalledWith(expect.objectContaining(expectedOptions));\n  });\n\n  it('should remove v3 fields without overwriting latest config names', async () => {\n    const onOptionsChange = jest.fn();\n    const options = {\n      jsonData: {\n        server: 'old',\n        host: 'new',\n        timeout: '6',\n        dialTimeout: '8',\n      },\n    } as any as DataSourceSettings<CHConfig>;\n\n    renderHook((opts) => useConfigDefaults(opts, onOptionsChange), { initialProps: options });\n\n    const expectedOptions = {\n      jsonData: {\n        ...expectedDefaults,\n        host: 'new',\n        dialTimeout: '8',\n      },\n    };\n    expect(onOptionsChange).toHaveBeenCalledTimes(1);\n    expect(onOptionsChange).toHaveBeenCalledWith(expect.objectContaining(expectedOptions));\n  });\n\n  it('should add plugin version', async () => {\n    const onOptionsChange = jest.fn();\n    const options = {\n      jsonData: {\n        protocol: Protocol.Native,\n      },\n    } as any as DataSourceSettings<CHConfig>;\n\n    renderHook((opts) => useConfigDefaults(opts, onOptionsChange), { initialProps: options });\n\n    const expectedOptions = {\n      jsonData: {\n        ...expectedDefaults,\n        version: pluginVersion,\n        protocol: Protocol.Native,\n      },\n    };\n    expect(onOptionsChange).toHaveBeenCalledTimes(1);\n    expect(onOptionsChange).toHaveBeenCalledWith(expect.objectContaining(expectedOptions));\n  });\n\n  it('should overwrite plugin version', async () => {\n    const onOptionsChange = jest.fn();\n    const options = {\n      jsonData: {\n        version: '3.0.0',\n        protocol: Protocol.Native,\n      },\n    } as any as DataSourceSettings<CHConfig>;\n\n    renderHook((opts) => useConfigDefaults(opts, onOptionsChange), { initialProps: options });\n\n    const expectedOptions = {\n      jsonData: {\n        ...expectedDefaults,\n        version: pluginVersion,\n        protocol: Protocol.Native,\n      },\n    };\n    expect(onOptionsChange).toHaveBeenCalledTimes(1);\n    expect(onOptionsChange).toHaveBeenCalledWith(expect.objectContaining(expectedOptions));\n  });\n\n  it('should not overwrite existing settings', async () => {\n    const onOptionsChange = jest.fn();\n    const options = {\n      jsonData: {\n        host: 'existing',\n        dialTimeout: 20,\n        protocol: Protocol.Http,\n        logs: {\n          defaultTable: 'not_default_logs',\n        },\n        traces: {\n          defaultTable: '', // empty\n        },\n      },\n    } as any as DataSourceSettings<CHConfig>;\n\n    renderHook((opts) => useConfigDefaults(opts, onOptionsChange), { initialProps: options });\n\n    const expectedOptions = {\n      jsonData: {\n        ...expectedDefaults,\n        host: 'existing',\n        dialTimeout: 20,\n        protocol: Protocol.Http,\n        logs: {\n          defaultTable: 'not_default_logs',\n        },\n        traces: {\n          defaultTable: '',\n        },\n      },\n    };\n    expect(onOptionsChange).toHaveBeenCalledTimes(1);\n    expect(onOptionsChange).toHaveBeenCalledWith(expect.objectContaining(expectedOptions));\n  });\n\n  it('should apply defaults for unset config fields', async () => {\n    const onOptionsChange = jest.fn();\n    const options = {\n      jsonData: {},\n    } as any as DataSourceSettings<CHConfig>;\n\n    renderHook((opts) => useConfigDefaults(opts, onOptionsChange), { initialProps: options });\n\n    const expectedOptions = {\n      jsonData: { ...expectedDefaults },\n    };\n    expect(onOptionsChange).toHaveBeenCalledTimes(1);\n    expect(onOptionsChange).toHaveBeenCalledWith(expect.objectContaining(expectedOptions));\n  });\n\n  it('should not call onOptionsChange after defaults are already set', async () => {\n    const onOptionsChange = jest.fn();\n    const options = {\n      jsonData: {},\n    } as any as DataSourceSettings<CHConfig>;\n\n    const hook = renderHook((opts) => useConfigDefaults(opts, onOptionsChange), { initialProps: options });\n    hook.rerender();\n\n    expect(onOptionsChange).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "src/views/CHConfigEditorHooks.ts",
    "content": "import { DataSourceSettings, KeyValue } from '@grafana/data';\nimport { useEffect, useRef } from 'react';\nimport { CHConfig, CHHttpHeader, CHSecureConfig, defaultCHAdditionalSettingsConfig, Protocol } from 'types/config';\nimport { pluginVersion } from 'utils/version';\n\n/**\n * Mirrors the DataSourceConfigValidationAPI interface from @grafana/data.\n * Defined locally until this plugin updates its @grafana/data peer dependency\n * to a version that includes DataSourceConfigValidationAPI in its exports.\n */\nexport interface ValidationAPI {\n  registerValidation: (validator: () => Promise<boolean> | boolean) => () => void;\n  validate: () => Promise<boolean>;\n  isValid: () => boolean;\n  getErrors: () => Record<string, string>;\n  setError: (field: string, message: string) => void;\n  clearError: (field: string) => void;\n}\n\n/**\n * Handles saving HTTP headers to Grafana config.\n *\n * All header keys go to the unsecure config.\n * If the header is marked as secure, its value goes to the\n * secure json config where it is hidden.\n */\nexport const onHttpHeadersChange = (\n  headers: CHHttpHeader[],\n  options: DataSourceSettings<CHConfig, CHSecureConfig>,\n  onOptionsChange: (opts: DataSourceSettings<CHConfig, CHSecureConfig>) => void\n) => {\n  const httpHeaders: CHHttpHeader[] = [];\n  const secureHttpHeaderKeys: KeyValue<boolean> = {};\n  const secureHttpHeaderValues: KeyValue<string> = {};\n\n  for (let header of headers) {\n    if (!header.name) {\n      continue;\n    }\n\n    if (header.secure) {\n      const key = `secureHttpHeaders.${header.name}`;\n      secureHttpHeaderKeys[key] = true;\n\n      if (header.value) {\n        secureHttpHeaderValues[key] = header.value;\n        header.value = '';\n      }\n    }\n\n    httpHeaders.push(header);\n  }\n\n  const currentSecureJsonFields: KeyValue<boolean> = { ...options.secureJsonFields };\n  for (let key in currentSecureJsonFields) {\n    if (!secureHttpHeaderKeys[key] && key.startsWith('secureHttpHeaders.')) {\n      // Remove key from secureJsonData when it is no longer present in header config\n      secureHttpHeaderKeys[key] = false;\n      secureHttpHeaderValues[key] = '';\n    }\n  }\n\n  onOptionsChange({\n    ...options,\n    jsonData: {\n      ...options.jsonData,\n      httpHeaders,\n    },\n    secureJsonFields: {\n      ...options.secureJsonFields,\n      ...secureHttpHeaderKeys,\n    },\n    secureJsonData: {\n      ...options.secureJsonData,\n      ...secureHttpHeaderValues,\n    },\n  });\n};\n\n/**\n * Applies default settings and migrations to config options.\n */\nexport const useConfigDefaults = (\n  options: DataSourceSettings<CHConfig>,\n  onOptionsChange: (opts: DataSourceSettings<CHConfig>) => void\n) => {\n  const appliedDefaults = useRef<boolean>(false);\n  useEffect(() => {\n    if (appliedDefaults.current) {\n      return;\n    }\n\n    const jsonData = { ...options.jsonData };\n    jsonData.version = pluginVersion; // Always overwrite version\n\n    // v3 Migration\n\n    const v3ServerField = (jsonData as any)['server'];\n    if (v3ServerField && !jsonData.host) {\n      jsonData.host = v3ServerField;\n    }\n    delete (jsonData as any)['server'];\n\n    const v3TimeoutField = (jsonData as any)['timeout'];\n    if (v3TimeoutField && !jsonData.dialTimeout) {\n      jsonData.dialTimeout = v3TimeoutField;\n    }\n    delete (jsonData as any)['timeout'];\n\n    // Defaults\n\n    if (!jsonData.protocol) {\n      jsonData.protocol = Protocol.Native;\n    }\n\n    if (!jsonData.logs || jsonData.logs.defaultTable === undefined) {\n      jsonData.logs = {\n        ...jsonData.logs,\n        defaultTable: defaultCHAdditionalSettingsConfig.logs?.defaultTable,\n        selectContextColumns: true,\n        contextColumns: [],\n      };\n    }\n\n    if (!jsonData.traces || jsonData.traces.defaultTable === undefined) {\n      jsonData.traces = {\n        ...jsonData.traces,\n        defaultTable: defaultCHAdditionalSettingsConfig.traces?.defaultTable,\n      };\n    }\n\n    onOptionsChange({\n      ...options,\n      jsonData,\n    });\n    appliedDefaults.current = true;\n  }, [options, onOptionsChange]);\n};\n\n/**\n * Factory that creates a local DataSourceConfigValidationAPI instance.\n *\n * Used when Grafana core does not yet pass props.validation down to the config\n * editor. Config editors should prefer props.validation when present and fall\n * back to a memoised instance created by this factory:\n *\n *   const validationAPI = useMemo(() => props.validation ?? createValidationAPI(), [props.validation]);\n *\n * Validators registered via registerValidation are run in order when\n * validate() is called. setError / clearError let components push field-level\n * errors imperatively (e.g. on blur or after an async check).\n */\nexport const createValidationAPI = (): ValidationAPI => {\n  const validators = new Set<() => Promise<boolean> | boolean>();\n  const errors: Record<string, string> = {};\n\n  return {\n    registerValidation(validator: () => Promise<boolean> | boolean): () => void {\n      validators.add(validator);\n      return () => validators.delete(validator);\n    },\n\n    async validate(): Promise<boolean> {\n      const results = await Promise.all(Array.from(validators).map((v) => Promise.resolve(v())));\n      return results.every(Boolean);\n    },\n\n    isValid(): boolean {\n      return Object.keys(errors).length === 0;\n    },\n\n    getErrors(): Record<string, string> {\n      return errors;\n    },\n\n    setError(field: string, message: string): void {\n      errors[field] = message;\n    },\n\n    clearError(field: string): void {\n      delete errors[field];\n    },\n  };\n};\n"
  },
  {
    "path": "src/views/CHQueryEditor.test.tsx",
    "content": "import React from 'react';\nimport { render, screen } from '@testing-library/react';\nimport '@testing-library/jest-dom';\nimport { CHQueryEditor } from './CHQueryEditor';\nimport * as ui from '@grafana/ui';\nimport { mockDatasource } from '__mocks__/datasource';\nimport { EditorType } from 'types/sql';\nimport { QueryType } from 'types/queryBuilder';\n\njest.mock('@grafana/ui', () => ({\n  ...jest.requireActual<typeof ui>('@grafana/ui'),\n  CodeEditor: function CodeEditor({ onEditorDidMount, value }: { onEditorDidMount: any; value: string }) {\n    onEditorDidMount = () => {\n      return {\n        getValue: () => {\n          return value;\n        },\n      };\n    };\n    return <div data-testid=\"code-editor\">{`${value}`}</div>;\n  },\n}));\n\ndescribe('Query Editor', () => {\n  it('Should display sql in the editor', () => {\n    const rawSql = 'foo';\n    render(\n      <CHQueryEditor\n        query={{ pluginVersion: '', rawSql, refId: 'A', editorType: EditorType.SQL }}\n        onChange={jest.fn()}\n        onRunQuery={jest.fn()}\n        datasource={mockDatasource}\n      />\n    );\n    expect(screen.queryByText(rawSql)).toBeInTheDocument();\n  });\n\n  it('Should render QueryBuilder when editorType is Builder', () => {\n    render(\n      <CHQueryEditor\n        query={{\n          pluginVersion: '',\n          rawSql: 'SELECT * FROM table',\n          refId: 'A',\n          editorType: EditorType.Builder,\n          builderOptions: {\n            database: '',\n            table: '',\n            queryType: QueryType.Table,\n          },\n        }}\n        onChange={jest.fn()}\n        onRunQuery={jest.fn()}\n        datasource={mockDatasource}\n      />\n    );\n    // QueryBuilder does not have a test id, but we can check for generatedSql text\n    expect(screen.getByText('SELECT * FROM table')).toBeInTheDocument();\n  });\n\n  it('Should not sync builder options when editorType remains SQL', () => {\n    const builderOptions = {\n      database: 'db2',\n      table: 'table2',\n      queryType: QueryType.Table,\n    };\n\n    const query = {\n      pluginVersion: '',\n      rawSql: 'SELECT * FROM table2',\n      refId: 'A',\n      editorType: EditorType.SQL,\n      builderOptions,\n    };\n\n    const onChange = jest.fn();\n\n    render(<CHQueryEditor query={query} onChange={onChange} onRunQuery={jest.fn()} datasource={mockDatasource} />);\n\n    // onChange should not be called since editorType is SQL\n    expect(onChange).not.toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "src/views/CHQueryEditor.tsx",
    "content": "import React, { useEffect, useMemo, useRef } from 'react';\nimport { QueryEditorProps } from '@grafana/data';\nimport { Datasource } from 'data/CHDatasource';\nimport { EditorTypeSwitcher } from 'components/queryBuilder/EditorTypeSwitcher';\nimport { styles } from 'styles';\nimport { Button } from '@grafana/ui';\nimport { CHBuilderQuery, CHQuery, EditorType } from 'types/sql';\nimport { CHConfig } from 'types/config';\nimport { QueryBuilder } from 'components/queryBuilder/QueryBuilder';\nimport { generateSql } from 'data/sqlGenerator';\nimport { SqlEditor } from 'components/SqlEditor';\nimport { isBuilderOptionsRunnable, mapQueryBuilderOptionsToGrafanaFormat } from 'data/utils';\nimport { setAllOptions, setOptions, useBuilderOptionsState } from 'hooks/useBuilderOptionsState';\nimport { pluginVersion } from 'utils/version';\nimport { migrateCHQuery } from 'data/migration';\nimport useTables from 'hooks/useTables';\n\nexport type CHQueryEditorProps = QueryEditorProps<Datasource, CHQuery, CHConfig>;\n\n/**\n * Top level query editor component\n */\nexport const CHQueryEditor = (props: CHQueryEditorProps) => {\n  const { datasource, query: savedQuery, onRunQuery } = props;\n  const query = migrateCHQuery(savedQuery);\n\n  return (\n    <>\n      <div className={'gf-form ' + styles.QueryEditor.queryType}>\n        <EditorTypeSwitcher {...props} query={query} datasource={datasource} />\n        <Button onClick={() => onRunQuery()}>Run Query</Button>\n      </div>\n      <CHEditorByType {...props} query={query} />\n    </>\n  );\n};\n\nconst CHEditorByType = (props: CHQueryEditorProps) => {\n  const { query, onChange, app } = props;\n  const [builderOptions, builderOptionsDispatch] = useBuilderOptionsState((query as CHBuilderQuery).builderOptions);\n\n  /**\n   * Grafana will sometimes replace the builder options directly, so we need to sync in both directions.\n   * For example, selecting an entry from the query history will cause the local state to fall out of sync.\n   * The \"key\" property is present on these historical entries.\n   */\n  const queryKey = query.key || '';\n  const lastKey = useRef<string>(queryKey);\n  if (queryKey !== lastKey.current && query.editorType === EditorType.Builder) {\n    builderOptionsDispatch(setAllOptions((query as CHBuilderQuery).builderOptions || {}));\n    lastKey.current = queryKey;\n  }\n\n  /**\n   * Sync builder options when switching from SQL Editor to Query Builder\n   */\n  const lastEditorType = useRef<EditorType | undefined>(undefined);\n  if (query.editorType !== lastEditorType.current && query.editorType === EditorType.Builder) {\n    builderOptionsDispatch(setAllOptions((query as CHBuilderQuery).builderOptions || {}));\n  }\n  lastEditorType.current = query.editorType;\n\n  // Prevent trying to run empty query on load\n  const shouldSkipChanges = useRef<boolean>(true);\n  if (isBuilderOptionsRunnable(builderOptions)) {\n    shouldSkipChanges.current = false;\n  }\n\n  // Resolve hasTraceTimestampTable for any trace ID query — not only OTel ones.\n  // Running this at the CHEditorByType level means the check fires even when\n  // the builder is minimized via a logs→trace deep-link. The companion-table\n  // suffix is configurable on the datasource (defaults to the OTel convention)\n  // so non-OTel schemas can opt in to the two-step trace ID lookup.\n  const traceTimestampTableSuffix =\n    builderOptions.meta?.traceTimestampTableSuffix || props.datasource.getTraceTimestampTableSuffix();\n  const needsTraceTableCheck = Boolean(builderOptions.meta?.isTraceIdMode);\n  const traceDb = needsTraceTableCheck ? builderOptions.database : '';\n  const traceTables = useTables(props.datasource, traceDb);\n  const hasTraceTimestampTable = useMemo(\n    () => traceTables.some((t) => t === builderOptions.table + traceTimestampTableSuffix),\n    [builderOptions.table, traceTables, traceTimestampTableSuffix]\n  );\n\n  useEffect(() => {\n    if (!needsTraceTableCheck || traceTables.length === 0) {\n      return;\n    }\n\n    if (hasTraceTimestampTable !== builderOptions.meta?.hasTraceTimestampTable) {\n      builderOptionsDispatch(\n        setOptions({\n          meta: { hasTraceTimestampTable },\n        })\n      );\n    }\n  }, [\n    needsTraceTableCheck,\n    traceTables,\n    hasTraceTimestampTable,\n    builderOptions.meta?.hasTraceTimestampTable,\n    builderOptionsDispatch,\n  ]);\n\n  useEffect(() => {\n    if (shouldSkipChanges.current || query.editorType === EditorType.SQL) {\n      return;\n    }\n\n    onChange({\n      ...query,\n      pluginVersion,\n      editorType: EditorType.Builder,\n      rawSql: generateSql(builderOptions),\n      builderOptions,\n      format: mapQueryBuilderOptionsToGrafanaFormat(builderOptions),\n    });\n\n    // TODO: fix dependency warning with \"useEffectEvent\" once added to stable version of react\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [builderOptions]);\n\n  if (query.editorType === EditorType.SQL) {\n    return (\n      <div data-testid=\"query-editor-section-sql\">\n        <SqlEditor {...props} />\n      </div>\n    );\n  }\n\n  return (\n    <QueryBuilder\n      datasource={props.datasource}\n      builderOptions={builderOptions}\n      builderOptionsDispatch={builderOptionsDispatch}\n      generatedSql={query.rawSql}\n      app={app}\n    />\n  );\n};\n"
  },
  {
    "path": "src/views/config-v2/AdditionalSettingsSection.tsx",
    "content": "import { ConfigSubSection } from 'components/experimental/ConfigSection';\nimport allLabels from './labelsV2';\nimport React, { ChangeEvent, useMemo, useState } from 'react';\nimport {\n  DataSourcePluginOptionsEditorProps,\n  onUpdateDatasourceJsonDataOption,\n  onUpdateDatasourceJsonDataOptionChecked,\n} from '@grafana/data';\nimport {\n  AliasTableEntry,\n  CHConfig,\n  CHCustomSetting,\n  CHLogsConfig,\n  CHSecureConfig,\n  CHTracesConfig,\n  defaultCHAdditionalSettingsConfig,\n} from 'types/config';\nimport { AliasTableConfig } from 'components/configEditor/AliasTableConfig';\nimport { DefaultDatabaseTableConfig } from 'components/configEditor/DefaultDatabaseTableConfig';\nimport { LogsConfig } from 'components/configEditor/LogsConfig';\nimport { QuerySettingsConfig } from 'components/configEditor/QuerySettingsConfig';\nimport { TracesConfig } from 'components/configEditor/TracesConfig';\nimport { config } from '@grafana/runtime';\nimport { TimeUnit } from 'types/queryBuilder';\nimport { useConfigDefaults } from 'views/CHConfigEditorHooks';\nimport { gte as versionGte } from 'semver';\nimport {\n  Field,\n  Divider,\n  Stack,\n  Input,\n  Button,\n  Switch,\n  Box,\n  CollapsableSection,\n  Text,\n  Badge,\n  useStyles2,\n} from '@grafana/ui';\nimport { CONFIG_SECTION_HEADERS, CONTAINER_MIN_WIDTH } from './constants';\nimport {\n  trackClickhouseConfigV2CustomSettingClicked,\n  trackClickhouseConfigV2DefaultDbInput,\n  trackClickhouseConfigV2DefaultTableInput,\n  trackClickhouseConfigV2EnableRowLimitToggle,\n  trackClickhouseConfigV2LogsConfig,\n  trackClickhouseConfigV2QuerySettings,\n  trackClickhouseConfigV2TracesConfig,\n} from './tracking';\nimport { css } from '@emotion/css';\nimport { isEqual } from 'es-toolkit/compat';\n\nexport interface Props extends DataSourcePluginOptionsEditorProps<CHConfig, CHSecureConfig> {}\n\nexport const AdditionalSettingsSection = (props: Props) => {\n  const { options, onOptionsChange } = props;\n  const { jsonData } = options;\n  const labels = allLabels.components.Config.ConfigEditor;\n  const styles = useStyles2(getStyles);\n\n  useConfigDefaults(options, onOptionsChange);\n\n  const [customSettings, setCustomSettings] = useState(jsonData.customSettings || []);\n\n  const onLogsConfigChange = (key: keyof CHLogsConfig, value: string | boolean | string[]) => {\n    onOptionsChange({\n      ...options,\n      jsonData: {\n        ...options.jsonData,\n        logs: {\n          ...(options.jsonData.logs || {}),\n          [key]: value,\n        },\n      },\n    });\n  };\n\n  const onUpdateLogsConfig = (key: keyof CHLogsConfig, value: string | boolean | string[]) => {\n    trackClickhouseConfigV2LogsConfig({ [key]: value });\n    onLogsConfigChange(key, value);\n  };\n\n  const onTracesConfigChange = (key: keyof CHTracesConfig, value: string | boolean) => {\n    onOptionsChange({\n      ...options,\n      jsonData: {\n        ...options.jsonData,\n        traces: {\n          ...options.jsonData.traces,\n          durationUnit: options.jsonData.traces?.durationUnit || TimeUnit.Nanoseconds,\n          [key]: value,\n        },\n      },\n    });\n  };\n\n  const onUpdateTracesConfig = (key: keyof CHTracesConfig, value: string | boolean) => {\n    trackClickhouseConfigV2TracesConfig({ [key]: value });\n    onTracesConfigChange(key, value);\n  };\n\n  const onAliasTableConfigChange = (aliasTables: AliasTableEntry[]) => {\n    onOptionsChange({\n      ...options,\n      jsonData: {\n        ...options.jsonData,\n        aliasTables,\n      },\n    });\n  };\n\n  const onCustomSettingsChange = (customSettings: CHCustomSetting[]) => {\n    onOptionsChange({\n      ...options,\n      jsonData: {\n        ...options.jsonData,\n        customSettings: customSettings.filter((s) => !!s.setting && !!s.value),\n      },\n    });\n  };\n  const shouldBeOpen = useMemo(() => {\n    const defaultLogs = defaultCHAdditionalSettingsConfig.logs;\n    const defaultTraces = defaultCHAdditionalSettingsConfig.traces;\n    const logs = jsonData.logs ?? defaultLogs;\n    const traces = jsonData.traces ?? defaultTraces;\n\n    return (\n      !!jsonData.defaultDatabase ||\n      !!jsonData.defaultTable ||\n      !!jsonData.connMaxLifetime ||\n      !!jsonData.dialTimeout ||\n      !!jsonData.maxIdleConns ||\n      !!jsonData.maxOpenConns ||\n      !!jsonData.queryTimeout ||\n      !!jsonData.validateSql ||\n      !isEqual(logs, defaultLogs) ||\n      !isEqual(traces, defaultTraces) ||\n      (jsonData.aliasTables?.length ?? 0) > 0 ||\n      !!jsonData.enableRowLimit ||\n      !!jsonData.enableSecureSocksProxy ||\n      customSettings.length > 0\n    );\n  }, [jsonData, customSettings]);\n\n  return (\n    <Box\n      borderStyle=\"solid\"\n      borderColor=\"weak\"\n      padding={2}\n      marginBottom={4}\n      id={`${CONFIG_SECTION_HEADERS[3].id}`}\n      minWidth={CONTAINER_MIN_WIDTH}\n    >\n      <CollapsableSection\n        label={\n          <>\n            <Text variant=\"h3\">{CONFIG_SECTION_HEADERS[3].label}</Text>\n            <Badge text=\"optional\" color=\"darkgrey\" className={styles.badge} />\n          </>\n        }\n        isOpen={!!shouldBeOpen}\n      >\n        <DefaultDatabaseTableConfig\n          defaultDatabase={jsonData.defaultDatabase}\n          defaultTable={jsonData.defaultTable}\n          onDefaultDatabaseChange={(e) => {\n            trackClickhouseConfigV2DefaultDbInput();\n            onUpdateDatasourceJsonDataOption(props, 'defaultDatabase')(e);\n          }}\n          onDefaultTableChange={(e) => {\n            trackClickhouseConfigV2DefaultTableInput();\n            onUpdateDatasourceJsonDataOption(props, 'defaultTable')(e);\n          }}\n        />\n        <Divider />\n        <QuerySettingsConfig\n          connMaxLifetime={jsonData.connMaxLifetime}\n          dialTimeout={jsonData.dialTimeout}\n          maxIdleConns={jsonData.maxIdleConns}\n          maxOpenConns={jsonData.maxOpenConns}\n          queryTimeout={jsonData.queryTimeout}\n          validateSql={jsonData.validateSql}\n          onDialTimeoutChange={(e) => {\n            trackClickhouseConfigV2QuerySettings({ dialTimeout: Number(e.currentTarget.value) });\n            onUpdateDatasourceJsonDataOption(props, 'dialTimeout')(e);\n          }}\n          onQueryTimeoutChange={(e) => {\n            trackClickhouseConfigV2QuerySettings({ queryTimeout: Number(e.currentTarget.value) });\n            onUpdateDatasourceJsonDataOption(props, 'queryTimeout')(e);\n          }}\n          onConnMaxLifetimeChange={(e) => {\n            trackClickhouseConfigV2QuerySettings({ connMaxLifetime: Number(e.currentTarget.value) });\n            onUpdateDatasourceJsonDataOption(props, 'connMaxLifetime')(e);\n          }}\n          onConnMaxIdleConnsChange={(e) => {\n            trackClickhouseConfigV2QuerySettings({ maxIdleConns: Number(e.currentTarget.value) });\n            onUpdateDatasourceJsonDataOption(props, 'maxIdleConns')(e);\n          }}\n          onConnMaxOpenConnsChange={(e) => {\n            trackClickhouseConfigV2QuerySettings({ maxOpenConns: Number(e.currentTarget.value) });\n            onUpdateDatasourceJsonDataOption(props, 'maxOpenConns')(e);\n          }}\n          onValidateSqlChange={(e) => {\n            trackClickhouseConfigV2QuerySettings({ validateSql: e.currentTarget.checked });\n            onUpdateDatasourceJsonDataOptionChecked(props, 'validateSql')(e);\n          }}\n        />\n        <Divider />\n        <LogsConfig\n          logsConfig={jsonData.logs}\n          onDefaultDatabaseChange={(db) => onUpdateLogsConfig('defaultDatabase', db)}\n          onDefaultTableChange={(table) => onUpdateLogsConfig('defaultTable', table)}\n          onOtelEnabledChange={(v) => onUpdateLogsConfig('otelEnabled', v)}\n          onOtelVersionChange={(v) => onUpdateLogsConfig('otelVersion', v)}\n          onFilterTimeColumnChange={(c) => onUpdateLogsConfig('filterTimeColumn', c)}\n          onTimeColumnChange={(c) => onUpdateLogsConfig('timeColumn', c)}\n          onLevelColumnChange={(c) => onUpdateLogsConfig('levelColumn', c)}\n          onMessageColumnChange={(c) => onUpdateLogsConfig('messageColumn', c)}\n          onSelectContextColumnsChange={(c) => onUpdateLogsConfig('selectContextColumns', c)}\n          onContextColumnsChange={(c) => onUpdateLogsConfig('contextColumns', c)}\n          onShowLogLinksChange={(v) => onUpdateLogsConfig('showLogLinks', v)}\n        />\n\n        <Divider />\n        <TracesConfig\n          tracesConfig={jsonData.traces}\n          onDefaultDatabaseChange={(db) => onUpdateTracesConfig('defaultDatabase', db)}\n          onDefaultTableChange={(table) => onUpdateTracesConfig('defaultTable', table)}\n          onOtelEnabledChange={(v) => onUpdateTracesConfig('otelEnabled', v)}\n          onOtelVersionChange={(v) => onUpdateTracesConfig('otelVersion', v)}\n          onTraceIdColumnChange={(c) => onUpdateTracesConfig('traceIdColumn', c)}\n          onSpanIdColumnChange={(c) => onUpdateTracesConfig('spanIdColumn', c)}\n          onOperationNameColumnChange={(c) => onUpdateTracesConfig('operationNameColumn', c)}\n          onParentSpanIdColumnChange={(c) => onUpdateTracesConfig('parentSpanIdColumn', c)}\n          onServiceNameColumnChange={(c) => onUpdateTracesConfig('serviceNameColumn', c)}\n          onDurationColumnChange={(c) => onUpdateTracesConfig('durationColumn', c)}\n          onDurationUnitChange={(c) => onUpdateTracesConfig('durationUnit', c)}\n          onStartTimeColumnChange={(c) => onUpdateTracesConfig('startTimeColumn', c)}\n          onTagsColumnChange={(c) => onUpdateTracesConfig('tagsColumn', c)}\n          onServiceTagsColumnChange={(c) => onUpdateTracesConfig('serviceTagsColumn', c)}\n          onKindColumnChange={(c) => onUpdateTracesConfig('kindColumn', c)}\n          onStatusCodeColumnChange={(c) => onUpdateTracesConfig('statusCodeColumn', c)}\n          onStatusMessageColumnChange={(c) => onUpdateTracesConfig('statusMessageColumn', c)}\n          onStateColumnChange={(c) => onUpdateTracesConfig('stateColumn', c)}\n          onInstrumentationLibraryNameColumnChange={(c) => onUpdateTracesConfig('instrumentationLibraryNameColumn', c)}\n          onInstrumentationLibraryVersionColumnChange={(c) =>\n            onUpdateTracesConfig('instrumentationLibraryVersionColumn', c)\n          }\n          onFlattenNestedChange={(c) => onUpdateTracesConfig('flattenNested', c)}\n          onEventsColumnPrefixChange={(c) => onUpdateTracesConfig('traceEventsColumnPrefix', c)}\n          onLinksColumnPrefixChange={(c) => onUpdateTracesConfig('traceLinksColumnPrefix', c)}\n          onShowTraceLinksChange={(v) => onUpdateTracesConfig('showTraceLinks', v)}\n          onTraceTimestampTableSuffixChange={(c) => onUpdateTracesConfig('traceTimestampTableSuffix', c)}\n        />\n        <Divider />\n        <AliasTableConfig aliasTables={jsonData.aliasTables} onAliasTablesChange={onAliasTableConfigChange} />\n        <Divider />\n        <Field label={labels.enableRowLimit.label} description={labels.enableRowLimit.tooltip}>\n          <Switch\n            value={jsonData.enableRowLimit || false}\n            data-testid={labels.enableRowLimit.testid}\n            onChange={(e) => {\n              trackClickhouseConfigV2EnableRowLimitToggle({ rowLimitEnabled: e.currentTarget.checked });\n              onUpdateDatasourceJsonDataOptionChecked(props, 'enableRowLimit')(e);\n            }}\n          />\n        </Field>\n        {config.secureSocksDSProxyEnabled && versionGte(config.buildInfo.version, '10.0.0') && (\n          <Field label={labels.secureSocksProxy.label} description={labels.secureSocksProxy.tooltip}>\n            <Switch\n              value={jsonData.enableSecureSocksProxy || false}\n              onChange={(e) => onUpdateDatasourceJsonDataOptionChecked(props, 'enableSecureSocksProxy')(e)}\n            />\n          </Field>\n        )}\n        <ConfigSubSection title=\"Custom Settings\">\n          {customSettings.map(({ setting, value }, i) => {\n            return (\n              <Stack key={i} direction=\"row\">\n                <Field label={`Setting`} aria-label={`Setting`}>\n                  <Input\n                    value={setting}\n                    placeholder={'Setting'}\n                    onChange={(changeEvent: ChangeEvent<HTMLInputElement>) => {\n                      let newSettings = customSettings.concat();\n                      newSettings[i] = { setting: changeEvent.target.value, value };\n                      setCustomSettings(newSettings);\n                    }}\n                    onBlur={() => onCustomSettingsChange(customSettings)}\n                  ></Input>\n                </Field>\n                <Field label={'Value'} aria-label={`Value`}>\n                  <Input\n                    value={value}\n                    placeholder={'Value'}\n                    onChange={(changeEvent: ChangeEvent<HTMLInputElement>) => {\n                      let newSettings = customSettings.concat();\n                      newSettings[i] = { setting, value: changeEvent.target.value };\n                      setCustomSettings(newSettings);\n                    }}\n                    onBlur={() => {\n                      onCustomSettingsChange(customSettings);\n                    }}\n                  ></Input>\n                </Field>\n              </Stack>\n            );\n          })}\n          <Button\n            variant=\"secondary\"\n            icon=\"plus\"\n            type=\"button\"\n            onClick={() => {\n              trackClickhouseConfigV2CustomSettingClicked();\n              setCustomSettings([...customSettings, { setting: '', value: '' }]);\n            }}\n          >\n            Add custom setting\n          </Button>\n        </ConfigSubSection>\n      </CollapsableSection>\n    </Box>\n  );\n};\n\nconst getStyles = () => ({\n  badge: css({\n    marginLeft: 'auto',\n  }),\n});\n"
  },
  {
    "path": "src/views/config-v2/AliasTableConfigV2.test.tsx",
    "content": "import React from 'react';\nimport { render, fireEvent } from '@testing-library/react';\nimport { AliasTableConfigV2 } from './AliasTableConfigV2';\nimport { selectors as allSelectors } from 'selectors';\nimport { AliasTableEntry } from 'types/config';\n\ndescribe('AliasTableConfig', () => {\n  const selectors = allSelectors.components.Config.AliasTableConfig;\n\n  it('should render', () => {\n    const result = render(<AliasTableConfigV2 aliasTables={[]} onAliasTablesChange={() => {}} />);\n    expect(result.container.firstChild).not.toBeNull();\n  });\n\n  it('should not call onAliasTablesChange when entry is added', () => {\n    const onAliasTablesChange = jest.fn();\n    const result = render(<AliasTableConfigV2 aliasTables={[]} onAliasTablesChange={onAliasTablesChange} />);\n    expect(result.container.firstChild).not.toBeNull();\n\n    const addEntryButton = result.getByTestId(selectors.addEntryButton);\n    expect(addEntryButton).toBeInTheDocument();\n    fireEvent.click(addEntryButton);\n\n    expect(onAliasTablesChange).toHaveBeenCalledTimes(0);\n  });\n\n  it('should call onAliasTablesChange when entry is updated', () => {\n    const onAliasTablesChange = jest.fn();\n    const result = render(<AliasTableConfigV2 aliasTables={[]} onAliasTablesChange={onAliasTablesChange} />);\n    expect(result.container.firstChild).not.toBeNull();\n\n    const addEntryButton = result.getByTestId(selectors.addEntryButton);\n    expect(addEntryButton).toBeInTheDocument();\n    fireEvent.click(addEntryButton);\n\n    const aliasEditor = result.getByTestId(selectors.aliasEditor);\n    expect(aliasEditor).toBeInTheDocument();\n\n    const targetDatabaseInput = result.getByTestId(selectors.targetDatabaseInput);\n    expect(targetDatabaseInput).toBeInTheDocument();\n    fireEvent.change(targetDatabaseInput, { target: { value: 'default ' } }); // with space in name\n    fireEvent.blur(targetDatabaseInput);\n    expect(targetDatabaseInput).toHaveValue('default ');\n    expect(onAliasTablesChange).toHaveBeenCalledTimes(1);\n\n    const targetTableInput = result.getByTestId(selectors.targetTableInput);\n    expect(targetTableInput).toBeInTheDocument();\n    fireEvent.change(targetTableInput, { target: { value: 'query_log' } });\n    fireEvent.blur(targetTableInput);\n    expect(targetTableInput).toHaveValue('query_log');\n    expect(onAliasTablesChange).toHaveBeenCalledTimes(2);\n\n    const aliasDatabaseInput = result.getByTestId(selectors.aliasDatabaseInput);\n    expect(aliasDatabaseInput).toBeInTheDocument();\n    fireEvent.change(aliasDatabaseInput, { target: { value: 'default_aliases ' } }); // with space in name\n    fireEvent.blur(aliasDatabaseInput);\n    expect(aliasDatabaseInput).toHaveValue('default_aliases ');\n    expect(onAliasTablesChange).toHaveBeenCalledTimes(3);\n\n    const aliasTableInput = result.getByTestId(selectors.aliasTableInput);\n    expect(aliasTableInput).toBeInTheDocument();\n    fireEvent.change(aliasTableInput, { target: { value: 'query_log_aliases' } });\n    fireEvent.blur(aliasTableInput);\n    expect(aliasTableInput).toHaveValue('query_log_aliases');\n    expect(onAliasTablesChange).toHaveBeenCalledTimes(4);\n\n    const expected: AliasTableEntry[] = [\n      {\n        targetDatabase: 'default', // without space in name\n        targetTable: 'query_log',\n        aliasDatabase: 'default_aliases', // without space in name\n        aliasTable: 'query_log_aliases',\n      },\n    ];\n    expect(onAliasTablesChange).toHaveBeenCalledWith(expect.objectContaining(expected));\n  });\n\n  it('should call onAliasTablesChange when entry is removed', () => {\n    const onAliasTablesChange = jest.fn();\n    const result = render(\n      <AliasTableConfigV2\n        aliasTables={[\n          {\n            targetDatabase: '',\n            targetTable: 'query_log',\n            aliasDatabase: '',\n            aliasTable: 'query_log_aliases',\n          },\n          {\n            targetDatabase: '',\n            targetTable: 'query_log2',\n            aliasDatabase: '',\n            aliasTable: 'query_log2_aliases',\n          },\n        ]}\n        onAliasTablesChange={onAliasTablesChange}\n      />\n    );\n    expect(result.container.firstChild).not.toBeNull();\n\n    const removeEntryButton = result.getAllByTestId(selectors.removeEntryButton)[0]; // Get 1st\n    expect(removeEntryButton).toBeInTheDocument();\n    fireEvent.click(removeEntryButton);\n\n    const expected: AliasTableEntry[] = [\n      {\n        targetDatabase: '',\n        targetTable: 'query_log2',\n        aliasDatabase: '',\n        aliasTable: 'query_log2_aliases',\n      },\n    ];\n    expect(onAliasTablesChange).toHaveBeenCalledTimes(1);\n    expect(onAliasTablesChange).toHaveBeenCalledWith(expect.objectContaining(expected));\n  });\n});\n"
  },
  {
    "path": "src/views/config-v2/AliasTableConfigV2.tsx",
    "content": "import React, { ChangeEvent, useState } from 'react';\nimport { ConfigSection } from 'components/experimental/ConfigSection';\nimport { Input, Field, Stack, Button } from '@grafana/ui';\nimport { AliasTableEntry } from 'types/config';\nimport allLabels from './labelsV2';\nimport { styles } from 'styles';\nimport { selectors as allSelectors } from 'selectors';\nimport { trackClickhouseConfigV2ColumnAliasTableAdded } from 'views/config-v2/tracking';\n\ninterface AliasTablesConfigProps {\n  aliasTables?: AliasTableEntry[];\n  onAliasTablesChange: (v: AliasTableEntry[]) => void;\n}\n\nexport const AliasTableConfigV2 = (props: AliasTablesConfigProps) => {\n  const { onAliasTablesChange } = props;\n  const [entries, setEntries] = useState<AliasTableEntry[]>(props.aliasTables || []);\n  const labels = allLabels.components.Config.AliasTableConfig;\n  const selectors = allSelectors.components.Config.AliasTableConfig;\n\n  const entryToUniqueKey = (entry: AliasTableEntry) =>\n    `\"${entry.targetDatabase}\".\"${entry.targetTable}\":\"${entry.aliasDatabase}\".\"${entry.aliasTable}\"`;\n  const removeDuplicateEntries = (entries: AliasTableEntry[]): AliasTableEntry[] => {\n    const duplicateKeys = new Set();\n    return entries.filter((entry) => {\n      const key = entryToUniqueKey(entry);\n      if (duplicateKeys.has(key)) {\n        return false;\n      }\n\n      duplicateKeys.add(key);\n      return true;\n    });\n  };\n\n  const addEntry = () => {\n    setEntries(\n      removeDuplicateEntries([\n        ...entries,\n        {\n          targetDatabase: '',\n          targetTable: '',\n          aliasDatabase: '',\n          aliasTable: '',\n        },\n      ])\n    );\n  };\n  const removeEntry = (index: number) => {\n    let nextEntries: AliasTableEntry[] = entries.slice();\n    nextEntries.splice(index, 1);\n    nextEntries = removeDuplicateEntries(nextEntries);\n    setEntries(nextEntries);\n    onAliasTablesChange(nextEntries);\n  };\n  const updateEntry = (index: number, entry: AliasTableEntry) => {\n    let nextEntries: AliasTableEntry[] = entries.slice();\n    entry.targetDatabase = entry.targetDatabase.trim();\n    entry.targetTable = entry.targetTable.trim();\n    entry.aliasDatabase = entry.aliasDatabase.trim();\n    entry.aliasTable = entry.aliasTable.trim();\n    nextEntries[index] = entry;\n\n    nextEntries = removeDuplicateEntries(nextEntries);\n    setEntries(nextEntries);\n    onAliasTablesChange(nextEntries);\n  };\n\n  return (\n    <ConfigSection title={labels.title}>\n      <div>\n        <span>{labels.descriptionParts[0]}</span>\n        <code>{labels.descriptionParts[1]}</code>\n        <span>{labels.descriptionParts[2]}</span>\n      </div>\n      <br />\n\n      {entries.map((entry, index) => (\n        <AliasTableEditor\n          key={entryToUniqueKey(entry)}\n          targetDatabase={entry.targetDatabase}\n          targetTable={entry.targetTable}\n          aliasDatabase={entry.aliasDatabase}\n          aliasTable={entry.aliasTable}\n          onEntryChange={(e) => updateEntry(index, e)}\n          onRemove={() => removeEntry(index)}\n        />\n      ))}\n      <Button\n        data-testid={selectors.addEntryButton}\n        icon=\"plus-circle\"\n        variant=\"secondary\"\n        size=\"sm\"\n        onClick={() => {\n          addEntry();\n          trackClickhouseConfigV2ColumnAliasTableAdded();\n        }}\n        className={styles.Common.smallBtn}\n      >\n        {labels.addTableLabel}\n      </Button>\n    </ConfigSection>\n  );\n};\n\ninterface AliasTableEditorProps {\n  targetDatabase: string;\n  targetTable: string;\n  aliasDatabase: string;\n  aliasTable: string;\n  onEntryChange: (v: AliasTableEntry) => void;\n  onRemove?: () => void;\n}\n\nconst AliasTableEditor = (props: AliasTableEditorProps) => {\n  const { onEntryChange, onRemove } = props;\n  const [targetDatabase, setTargetDatabase] = useState<string>(props.targetDatabase);\n  const [targetTable, setTargetTable] = useState<string>(props.targetTable);\n  const [aliasDatabase, setAliasDatabase] = useState<string>(props.aliasDatabase);\n  const [aliasTable, setAliasTable] = useState<string>(props.aliasTable);\n  const labels = allLabels.components.Config.AliasTableConfig;\n  const selectors = allSelectors.components.Config.AliasTableConfig;\n\n  const onUpdate = () => {\n    onEntryChange({ targetDatabase, targetTable, aliasDatabase, aliasTable });\n  };\n\n  return (\n    <div data-testid={selectors.aliasEditor}>\n      <Stack>\n        <Field label={labels.targetDatabaseLabel} aria-label={labels.targetDatabaseLabel}>\n          <Input\n            data-testid={selectors.targetDatabaseInput}\n            value={targetDatabase}\n            placeholder={labels.targetDatabasePlaceholder}\n            onChange={(e: ChangeEvent<HTMLInputElement>) => setTargetDatabase(e.target.value)}\n            onBlur={() => onUpdate()}\n          />\n        </Field>\n        <Field label={labels.targetTableLabel} aria-label={labels.targetTableLabel}>\n          <Input\n            data-testid={selectors.targetTableInput}\n            value={targetTable}\n            placeholder={labels.targetTableLabel}\n            onChange={(e: ChangeEvent<HTMLInputElement>) => setTargetTable(e.target.value)}\n            onBlur={() => onUpdate()}\n          />\n        </Field>\n        <Field label={labels.aliasDatabaseLabel} aria-label={labels.aliasDatabaseLabel}>\n          <Input\n            data-testid={selectors.aliasDatabaseInput}\n            value={aliasDatabase}\n            placeholder={labels.aliasDatabasePlaceholder}\n            onChange={(e: ChangeEvent<HTMLInputElement>) => setAliasDatabase(e.target.value)}\n            onBlur={() => onUpdate()}\n          />\n        </Field>\n        <Field label={labels.aliasTableLabel} aria-label={labels.aliasTableLabel}>\n          <Input\n            data-testid={selectors.aliasTableInput}\n            value={aliasTable}\n            placeholder={labels.aliasTableLabel}\n            onChange={(e: ChangeEvent<HTMLInputElement>) => setAliasTable(e.target.value)}\n            onBlur={() => onUpdate()}\n          />\n        </Field>\n        {onRemove && (\n          <Button\n            data-testid={selectors.removeEntryButton}\n            className={styles.Common.smallBtn}\n            variant=\"destructive\"\n            size=\"sm\"\n            icon=\"trash-alt\"\n            onClick={onRemove}\n            aria-label=\"alias-remove-entry\"\n          />\n        )}\n      </Stack>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/views/config-v2/CHConfigEditor.test.tsx",
    "content": "import React from 'react';\nimport '@testing-library/jest-dom';\n\nimport { render, screen } from '@testing-library/react';\n\nimport { ConfigEditor } from './CHConfigEditor';\nimport { createTestProps } from './helpers';\n\njest.mock('./LeftSidebar', () => ({\n  LeftSidebar: () => <div data-testid=\"left-sidebar\" />,\n}));\n\njest.mock('./ServerAndEncryptionSection', () => ({\n  ServerAndEncryptionSection: () => <div data-testid=\"server-encryption-section\" />,\n}));\n\ndescribe('ConfigEditor', () => {\n  const defaultProps = createTestProps({\n    options: {\n      jsonData: {},\n      secureJsonData: {},\n      secureJsonFields: {},\n    },\n    mocks: {\n      onOptionsChange: jest.fn(),\n    },\n  });\n\n  it('renders the LeftSideBar, ServerAndEncryptionSection, and HttpProtocolSettingsSection', () => {\n    render(<ConfigEditor {...defaultProps} />);\n\n    expect(screen.getByTestId('left-sidebar')).toBeInTheDocument();\n    expect(screen.getByTestId('server-encryption-section')).toBeInTheDocument();\n  });\n\n  it.skip('shows the informational alert', () => {\n    render(<ConfigEditor {...defaultProps} />);\n    expect(screen.getByText(/You are viewing a new design/i)).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "src/views/config-v2/CHConfigEditor.tsx",
    "content": "import React, { useMemo } from 'react';\nimport { DataSourcePluginOptionsEditorProps, GrafanaTheme2 } from '@grafana/data';\nimport { Alert, Box, Stack, Text, TextLink, useStyles2 } from '@grafana/ui';\nimport { config } from '@grafana/runtime';\nimport { CHConfig, CHSecureConfig } from 'types/config';\nimport { ServerAndEncryptionSection } from './ServerAndEncryptionSection';\nimport { css } from '@emotion/css';\nimport { LeftSidebar } from './LeftSidebar';\nimport { CONTAINER_MIN_WIDTH } from './constants';\nimport { trackClickhouseConfigV2FeedbackButtonClicked } from './tracking';\nimport { AdditionalSettingsSection } from './AdditionalSettingsSection';\nimport { DatabaseCredentialsSection } from './DatabaseCredentialsSection';\nimport { TLSSSLSettingsSection } from './TLSSSLSettingsSection';\nimport { createValidationAPI } from '../CHConfigEditorHooks';\n\nexport interface ConfigEditorProps extends DataSourcePluginOptionsEditorProps<CHConfig, CHSecureConfig> {}\n\nexport const ConfigEditor: React.FC<ConfigEditorProps> = (props) => {\n  const { options, onOptionsChange } = props;\n  const styles = useStyles2(getStyles);\n  const validationEnabled = (config.featureToggles as Record<string, boolean | undefined> | undefined)?.[\n    'clickHouseConfigValidation'\n  ];\n  const validation = useMemo(\n    () => (validationEnabled ? (props.validation ?? createValidationAPI()) : undefined),\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [props.validation]\n  );\n\n  return (\n    <Stack justifyContent=\"space-between\">\n      <div className={`${styles.hideOnSmallScreen} ${styles.leftSticky}`}>\n        <Box width=\"100%\" flex=\"1 1 auto\">\n          <LeftSidebar pdcInjected={Boolean(options?.jsonData?.pdcInjected)} />\n        </Box>\n      </div>\n      <Box width=\"60%\" flex=\"1 1 auto\" minWidth={CONTAINER_MIN_WIDTH}>\n        <div className={styles.requiredFields}>\n          <Alert\n            severity=\"info\"\n            title=\"You are viewing a new design for the ClickHouse configuration settings.\"\n            className={styles.alertHeight}\n          >\n            <>\n              <TextLink\n                href=\"https://docs.google.com/forms/d/e/1FAIpQLSd5YOYxfW-CU0tQnfFA08fkymGlmZ8XcFRMniE5lScIsmdt5w/viewform\"\n                external\n                onClick={trackClickhouseConfigV2FeedbackButtonClicked}\n              >\n                Share your thoughts\n              </TextLink>{' '}\n              to help us make it even better.\n            </>\n          </Alert>\n          <Text variant=\"bodySmall\" color=\"secondary\">\n            Fields marked with * are required\n          </Text>\n        </div>\n        <ServerAndEncryptionSection onOptionsChange={onOptionsChange} options={options} validation={validation} />\n        <DatabaseCredentialsSection onOptionsChange={onOptionsChange} options={options} validation={validation} />\n        <TLSSSLSettingsSection onOptionsChange={onOptionsChange} options={options} />\n        <AdditionalSettingsSection onOptionsChange={onOptionsChange} options={options} />\n      </Box>\n      <Box width=\"20%\" flex=\"0 0 20%\">\n        {/* TODO: Right sidebar */}\n      </Box>\n    </Stack>\n  );\n};\n\nconst getStyles = (theme: GrafanaTheme2) => ({\n  hideOnSmallScreen: css({\n    width: '250px',\n    flex: '0 0 250px',\n    [theme.breakpoints.down('sm')]: {\n      display: 'none',\n    },\n  }),\n  leftSticky: css({\n    position: 'sticky',\n    top: '100px',\n    alignSelf: 'flex-start',\n    maxHeight: 'calc(100vh - 100px)',\n    overflow: 'hidden',\n  }),\n  requiredFields: css({\n    marginBottom: theme.spacing(2),\n  }),\n  alertHeight: css({\n    height: '100px',\n  }),\n});\n"
  },
  {
    "path": "src/views/config-v2/DatabaseCredentialsSection.test.tsx",
    "content": "import React from 'react';\nimport { act, render, screen, fireEvent } from '@testing-library/react';\n\nimport { DatabaseCredentialsSection } from './DatabaseCredentialsSection';\nimport { createMockValidation, createTestProps } from './helpers';\n\ndescribe('DatabaseCredentialsSection', () => {\n  const onOptionsChangeMock = jest.fn();\n  let consoleSpy: jest.SpyInstance;\n\n  const defaultProps = createTestProps({\n    options: {\n      jsonData: {\n        username: '',\n      },\n      secureJsonData: {},\n      secureJsonFields: {},\n    },\n    mocks: {\n      onOptionsChange: onOptionsChangeMock,\n    },\n  });\n\n  beforeEach(() => {\n    // Mock console.error to suppress React act() warnings\n    consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});\n    jest.clearAllMocks();\n  });\n\n  afterEach(() => {\n    consoleSpy.mockRestore();\n  });\n\n  it('renders username and password fields', () => {\n    render(<DatabaseCredentialsSection {...defaultProps} />);\n\n    expect(screen.getByLabelText(/username/i)).toBeInTheDocument();\n    expect(screen.getByLabelText(/password/i)).toBeInTheDocument();\n  });\n\n  it('calls onOptionsChange when username is changed', () => {\n    render(<DatabaseCredentialsSection {...defaultProps} />);\n\n    const usernameInput = screen.getByLabelText(/username/i);\n    fireEvent.change(usernameInput, { target: { value: 'alice' } });\n\n    expect(onOptionsChangeMock).toHaveBeenCalled();\n    const lastArgs = onOptionsChangeMock.mock.lastCall?.[0];\n    expect(lastArgs.jsonData?.username).toBe('alice');\n  });\n\n  it('calls onOptionsChange when password is changed', () => {\n    render(<DatabaseCredentialsSection {...defaultProps} />);\n\n    const passwordInput = screen.getByLabelText(/password/i);\n    fireEvent.change(passwordInput, { target: { value: 'secret' } });\n\n    expect(onOptionsChangeMock).toHaveBeenCalled();\n\n    const lastArgs = onOptionsChangeMock.mock.lastCall?.[0];\n    expect(lastArgs.secureJsonData?.password).toBe('secret');\n  });\n\n  describe('validation', () => {\n    const emptyProps = createTestProps({\n      options: {\n        jsonData: { username: '' },\n        secureJsonData: {},\n        secureJsonFields: {},\n      },\n      mocks: { onOptionsChange: jest.fn() },\n    });\n\n    const filledProps = createTestProps({\n      options: {\n        jsonData: { username: 'default' },\n        secureJsonData: {},\n        secureJsonFields: {},\n      },\n      mocks: { onOptionsChange: jest.fn() },\n    });\n\n    it('shows inline error for username when validator is called with empty value', async () => {\n      const validation = createMockValidation();\n      render(<DatabaseCredentialsSection {...emptyProps} validation={validation} />);\n\n      await act(async () => {\n        validation.runValidator();\n      });\n\n      expect(screen.getByText('Username is required')).toBeInTheDocument();\n    });\n\n    it('shows no errors when all fields are filled', async () => {\n      const validation = createMockValidation();\n      render(<DatabaseCredentialsSection {...filledProps} validation={validation} />);\n\n      await act(async () => {\n        validation.runValidator();\n      });\n\n      expect(screen.queryByText('Username is required')).not.toBeInTheDocument();\n    });\n  });\n\n  it('resets password when Reset is clicked (isConfigured=true)', () => {\n    const configuredProps = createTestProps({\n      options: {\n        jsonData: {\n          username: 'bob',\n        },\n        secureJsonData: {\n          password: 'configured',\n        },\n        secureJsonFields: {\n          password: true,\n        },\n      },\n      mocks: {\n        onOptionsChange: onOptionsChangeMock,\n      },\n    });\n\n    render(<DatabaseCredentialsSection {...configuredProps} />);\n\n    const resetButton = screen.getByRole('button', { name: /reset/i });\n    fireEvent.click(resetButton);\n\n    expect(onOptionsChangeMock).toHaveBeenCalled();\n\n    const lastArgs = onOptionsChangeMock.mock.lastCall?.[0];\n    expect(lastArgs.secureJsonFields?.password).toBe(false);\n    expect(lastArgs.secureJsonData?.password).toBe('');\n  });\n});\n"
  },
  {
    "path": "src/views/config-v2/DatabaseCredentialsSection.tsx",
    "content": "import { Box, CollapsableSection, Field, Input, SecretInput, Text, TextLink, useStyles2 } from '@grafana/ui';\nimport React, { useEffect, useState } from 'react';\nimport { CONFIG_SECTION_HEADERS, CONTAINER_MIN_WIDTH } from './constants';\nimport {\n  DataSourcePluginOptionsEditorProps,\n  onUpdateDatasourceJsonDataOption,\n  onUpdateDatasourceSecureJsonDataOption,\n} from '@grafana/data';\nimport allLabels from './labelsV2';\nimport { CHConfig, CHSecureConfig } from 'types/config';\nimport { css } from '@emotion/css';\nimport {\n  trackClickhouseConfigV2DatabaseCredentialsPasswordInput,\n  trackClickhouseConfigV2DatabaseCredentialsUserInput,\n} from './tracking';\nimport { ValidationAPI } from '../CHConfigEditorHooks';\n\nexport interface Props extends DataSourcePluginOptionsEditorProps<CHConfig, CHSecureConfig> {\n  validation?: ValidationAPI;\n}\n\nexport const DatabaseCredentialsSection = (props: Props) => {\n  const { options, onOptionsChange, validation } = props;\n  const { jsonData, secureJsonFields } = options;\n  const secureJsonData = (options.secureJsonData || {}) as CHSecureConfig;\n  const labels = allLabels.components.Config.ConfigEditor;\n  const styles = useStyles2(getStyles);\n\n  const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});\n\n  useEffect(() => {\n    if (!validation) {\n      return;\n    }\n    if (jsonData.username) {\n      setFieldErrors((prev) => { const next = { ...prev }; delete next.username; return next; });\n      validation.clearError('username');\n    }\n    return validation.registerValidation(() => {\n      const errors: Record<string, string> = {};\n      if (!jsonData.username) {\n        errors.username = labels.username.error;\n      }\n      setFieldErrors(errors);\n      Object.entries(errors).forEach(([field, msg]) => validation.setError(field, msg));\n      return Object.keys(errors).length === 0;\n    });\n  }, [jsonData.username, validation, labels.username.error]);\n\n  const onResetPassword = () => {\n    onOptionsChange({\n      ...options,\n      secureJsonFields: {\n        ...options.secureJsonFields,\n        password: false,\n      },\n      secureJsonData: {\n        ...options.secureJsonData,\n        password: '',\n      },\n    });\n  };\n\n  return (\n    <Box\n      borderStyle=\"solid\"\n      borderColor=\"weak\"\n      padding={2}\n      marginBottom={4}\n      id={`${CONFIG_SECTION_HEADERS[1].id}`}\n      minWidth={CONTAINER_MIN_WIDTH}\n    >\n      <CollapsableSection\n        label={<Text variant=\"h3\">{CONFIG_SECTION_HEADERS[1].label}</Text>}\n        isOpen={!!CONFIG_SECTION_HEADERS[1].isOpen}\n      >\n        <div className={styles.credentialsSection}>\n          <Field\n            label={labels.username.label}\n            description={\n              <>\n                {labels.username.tooltip}{' '}\n                <TextLink\n                  variant=\"bodySmall\"\n                  href=\"https://clickhouse.com/docs/en/operations/settings/permissions-for-queries#readonly\"\n                  external\n                >\n                  a read-only user\n                </TextLink>\n              </>\n            }\n            required\n            invalid={!!fieldErrors.username}\n            error={fieldErrors.username}\n          >\n            <Input\n              name=\"user\"\n              value={jsonData.username || ''}\n              onChange={onUpdateDatasourceJsonDataOption(props, 'username')}\n              label={labels.username.label}\n              aria-label={labels.username.label}\n              placeholder={labels.username.placeholder}\n              onBlur={(e) => {\n                trackClickhouseConfigV2DatabaseCredentialsUserInput();\n                if (!e.currentTarget.value) {\n                  setFieldErrors((prev) => ({ ...prev, username: labels.username.error }));\n                  validation?.setError('username', labels.username.error);\n                }\n              }}\n            />\n          </Field>\n          <Field\n            label={labels.password.label}\n            description={<div className={styles.passwordDescription}>{labels.password.tooltip}</div>}\n          >\n            <SecretInput\n              name=\"pwd\"\n              label={labels.password.label}\n              aria-label={labels.password.label}\n              placeholder={labels.password.placeholder}\n              value={secureJsonData.password || ''}\n              isConfigured={(secureJsonFields && secureJsonFields.password) as boolean}\n              onReset={onResetPassword}\n              onChange={onUpdateDatasourceSecureJsonDataOption(props, 'password')}\n              onBlur={trackClickhouseConfigV2DatabaseCredentialsPasswordInput}\n            />\n          </Field>\n        </div>\n      </CollapsableSection>\n    </Box>\n  );\n};\n\nconst getStyles = () => ({\n  passwordDescription: css({\n    marginTop: '5px',\n  }),\n  credentialsSection: css({\n    display: 'flex',\n    flexWrap: 'wrap',\n    gap: '8px',\n\n    '& > div': {\n      flex: '1 1 300px',\n      minWidth: 0,\n    },\n  }),\n});\n"
  },
  {
    "path": "src/views/config-v2/HttpHeadersConfigV2.test.tsx",
    "content": "import React from 'react';\nimport { render, screen, fireEvent } from '@testing-library/react';\nimport { HttpHeadersConfigV2 } from './HttpHeadersConfigV2';\nimport { selectors } from 'selectors';\nimport { Protocol } from 'types/config';\nimport { createTestProps } from './helpers';\n\ndescribe('HttpHeadersConfigV2', () => {\n  const onHttpHeadersChange = jest.fn();\n  const onOptionsChangeMock = jest.fn();\n  let consoleSpy: jest.SpyInstance;\n\n  const defaultProps = createTestProps({\n    options: {\n      jsonData: {\n        host: '',\n        secure: false,\n        protocol: Protocol.Native,\n        port: undefined,\n        pdcInjected: false,\n      },\n      secureJsonData: {},\n      secureJsonFields: {},\n    },\n    mocks: {\n      onOptionsChange: onOptionsChangeMock,\n    },\n  });\n\n  const renderWith = (overrides?: Partial<React.ComponentProps<typeof HttpHeadersConfigV2>>) => {\n    const props: React.ComponentProps<typeof HttpHeadersConfigV2> = {\n      ...defaultProps,\n      headers: [],\n      forwardGrafanaHeaders: false,\n      secureFields: {},\n      onHttpHeadersChange,\n      ...(overrides || {}),\n    };\n    return render(<HttpHeadersConfigV2 {...props} />);\n  };\n\n  beforeEach(() => {\n    // Mock console.error to suppress React act() warnings\n    consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});\n    jest.clearAllMocks();\n  });\n\n  afterEach(() => {\n    consoleSpy.mockRestore();\n  });\n\n  it('renders top label, Add header button, and forward checkbox', () => {\n    renderWith();\n\n    expect(screen.getByText(/custom http headers/i)).toBeInTheDocument();\n    expect(screen.getByRole('button', { name: /add header/i })).toBeInTheDocument();\n    expect(screen.getByLabelText(/forward grafana http headers to data source/i)).toBeInTheDocument();\n  });\n\n  it('adds a new header editor when Add header is clicked', () => {\n    renderWith();\n\n    const before = screen.queryAllByTestId(selectors.components.Config.HttpHeaderConfig.headerEditor).length;\n    fireEvent.click(screen.getByTestId(selectors.components.Config.HttpHeaderConfig.addHeaderButton));\n    const after = screen.getAllByTestId(selectors.components.Config.HttpHeaderConfig.headerEditor).length;\n\n    expect(after).toBe(before + 1);\n    expect(onHttpHeadersChange).not.toHaveBeenCalled();\n  });\n\n  it('renders any initial headers passed in', () => {\n    renderWith({\n      headers: [\n        { name: 'X-Auth', value: 'abc', secure: false },\n        { name: 'Foo', value: 'bar', secure: true },\n      ],\n    });\n\n    const editors = screen.getAllByTestId(selectors.components.Config.HttpHeaderConfig.headerEditor);\n    expect(editors.length).toBe(2);\n    expect(screen.getAllByTestId(selectors.components.Config.HttpHeaderConfig.headerNameInput)[0]).toHaveValue(\n      'X-Auth'\n    );\n    expect(screen.getAllByTestId(selectors.components.Config.HttpHeaderConfig.headerNameInput)[1]).toHaveValue('Foo');\n  });\n\n  it('removes a header and calls onHttpHeadersChange when Remove is clicked', () => {\n    renderWith({\n      headers: [\n        { name: 'A', value: '1', secure: false },\n        { name: 'B', value: '2', secure: false },\n      ],\n    });\n\n    const before = screen.getAllByTestId(selectors.components.Config.HttpHeaderConfig.headerEditor).length;\n    const removeButtons = screen.getAllByTestId('trash-alt');\n    fireEvent.click(removeButtons[0]);\n\n    expect(onHttpHeadersChange).toHaveBeenCalled();\n    const next = onHttpHeadersChange.mock.lastCall?.[0];\n    expect(next.length).toBe(before - 1);\n    expect(next.find((h: any) => h.name === 'A')).toBeUndefined();\n  });\n\n  it('toggles \"Forward Grafana headers\" and calls onForwardGrafanaHeadersChange', () => {\n    renderWith({ forwardGrafanaHeaders: false });\n\n    const forwardCb = screen.getByLabelText(/forward grafana http headers to data source/i) as HTMLInputElement;\n    fireEvent.click(forwardCb);\n\n    expect(onOptionsChangeMock).toHaveBeenCalled();\n    expect(onOptionsChangeMock).toHaveBeenLastCalledWith(\n      expect.objectContaining({\n        jsonData: expect.objectContaining({ forwardGrafanaHeaders: true }),\n      })\n    );\n  });\n\n  describe('HttpHeadersConfigV2', () => {\n    const onHttpHeadersChange = jest.fn();\n    const onOptionsChangeMock = jest.fn();\n    let consoleSpy: jest.SpyInstance;\n\n    const defaultProps = createTestProps({\n      options: {\n        jsonData: {\n          host: '',\n          secure: false,\n          protocol: Protocol.Native,\n          port: undefined,\n          pdcInjected: false,\n        },\n        secureJsonData: {},\n        secureJsonFields: {},\n      },\n      mocks: {\n        onOptionsChange: onOptionsChangeMock,\n      },\n    });\n\n    const renderWith = (overrides?: Partial<React.ComponentProps<typeof HttpHeadersConfigV2>>) => {\n      const props: React.ComponentProps<typeof HttpHeadersConfigV2> = {\n        ...defaultProps,\n        headers: [],\n        forwardGrafanaHeaders: false,\n        secureFields: {},\n        onHttpHeadersChange,\n        ...(overrides || {}),\n      };\n      return render(<HttpHeadersConfigV2 {...props} />);\n    };\n\n    beforeEach(() => {\n      // Mock console.error to suppress React act() warnings\n      consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});\n      jest.clearAllMocks();\n    });\n\n    afterEach(() => {\n      consoleSpy.mockRestore();\n    });\n\n    it('renders top label, Add header button, and forward checkbox', () => {\n      renderWith();\n\n      expect(screen.getByText(/custom http headers/i)).toBeInTheDocument();\n      expect(screen.getByRole('button', { name: /add header/i })).toBeInTheDocument();\n      expect(screen.getByLabelText(/forward grafana http headers to data source/i)).toBeInTheDocument();\n    });\n\n    it('adds a new header editor when Add header is clicked', () => {\n      renderWith();\n\n      const before = screen.queryAllByTestId(selectors.components.Config.HttpHeaderConfig.headerEditor).length;\n      fireEvent.click(screen.getByTestId(selectors.components.Config.HttpHeaderConfig.addHeaderButton));\n      const after = screen.getAllByTestId(selectors.components.Config.HttpHeaderConfig.headerEditor).length;\n\n      expect(after).toBe(before + 1);\n      expect(onHttpHeadersChange).not.toHaveBeenCalled();\n    });\n\n    it('renders any initial headers passed in', () => {\n      renderWith({\n        headers: [\n          { name: 'X-Auth', value: 'abc', secure: false },\n          { name: 'Foo', value: 'bar', secure: true },\n        ],\n      });\n\n      const editors = screen.getAllByTestId(selectors.components.Config.HttpHeaderConfig.headerEditor);\n      expect(editors.length).toBe(2);\n      expect(screen.getAllByTestId(selectors.components.Config.HttpHeaderConfig.headerNameInput)[0]).toHaveValue(\n        'X-Auth'\n      );\n      expect(screen.getAllByTestId(selectors.components.Config.HttpHeaderConfig.headerNameInput)[1]).toHaveValue('Foo');\n    });\n\n    it('removes a header and calls onHttpHeadersChange when Remove is clicked', () => {\n      renderWith({\n        headers: [\n          { name: 'A', value: '1', secure: false },\n          { name: 'B', value: '2', secure: false },\n        ],\n      });\n\n      const before = screen.getAllByTestId(selectors.components.Config.HttpHeaderConfig.headerEditor).length;\n      const removeButtons = screen.getAllByTestId('trash-alt');\n      fireEvent.click(removeButtons[0]);\n\n      expect(onHttpHeadersChange).toHaveBeenCalled();\n      const next = onHttpHeadersChange.mock.lastCall?.[0];\n      expect(next.length).toBe(before - 1);\n      expect(next.find((h: any) => h.name === 'A')).toBeUndefined();\n    });\n\n    it('toggles \"Forward Grafana headers\" and updated forwardGrafanaHeaders value to true', () => {\n      renderWith({ forwardGrafanaHeaders: false });\n\n      const forwardCb = screen.getByLabelText(/forward grafana http headers to data source/i) as HTMLInputElement;\n      fireEvent.click(forwardCb);\n\n      expect(onOptionsChangeMock).toHaveBeenCalled();\n      expect(onOptionsChangeMock).toHaveBeenLastCalledWith(\n        expect.objectContaining({\n          jsonData: expect.objectContaining({ forwardGrafanaHeaders: true }),\n        })\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "src/views/config-v2/HttpHeadersConfigV2.tsx",
    "content": "import React, { ChangeEvent, useMemo, useState } from 'react';\nimport { Input, Field, SecretInput, Button, Stack, Checkbox, Box } from '@grafana/ui';\nimport { CHConfig, CHHttpHeader, CHSecureConfig } from 'types/config';\nimport allLabels from './labelsV2';\nimport { styles } from 'styles';\nimport { selectors as allSelectors } from 'selectors';\nimport { DataSourcePluginOptionsEditorProps, KeyValue, onUpdateDatasourceJsonDataOptionChecked } from '@grafana/data';\n\ninterface HttpHeadersConfigProps extends DataSourcePluginOptionsEditorProps<CHConfig, CHSecureConfig> {\n  headers?: CHHttpHeader[];\n  forwardGrafanaHeaders?: boolean;\n  secureFields: KeyValue<boolean>;\n  onHttpHeadersChange: (v: CHHttpHeader[]) => void;\n}\n\nexport const HttpHeadersConfigV2 = (props: HttpHeadersConfigProps) => {\n  const { secureFields, onHttpHeadersChange } = props;\n  const configuredSecureHeaders = useConfiguredSecureHttpHeaders(secureFields);\n  const [headers, setHeaders] = useState<CHHttpHeader[]>(props.headers || []);\n  const [forwardGrafanaHeaders, setForwardGrafanaHeaders] = useState<boolean>(props.forwardGrafanaHeaders || false);\n  const labels = allLabels.components.Config.HttpHeadersConfig;\n  const selectors = allSelectors.components.Config.HttpHeaderConfig;\n\n  const addHeader = () => setHeaders([...headers, { name: '', value: '', secure: false }]);\n\n  const removeHeader = (index: number) => {\n    const nextHeaders: CHHttpHeader[] = [...headers.slice(0, index), ...headers.slice(index + 1)];\n    setHeaders(nextHeaders);\n    onHttpHeadersChange(nextHeaders);\n  };\n\n  const updateHeader = (index: number, header: CHHttpHeader) => {\n    const nextHeaders: CHHttpHeader[] = [\n      ...headers.slice(0, index),\n      { ...header, name: header.name.trim() },\n      ...headers.slice(index + 1),\n    ];\n    setHeaders(nextHeaders);\n    onHttpHeadersChange(nextHeaders);\n  };\n\n  const updateForwardGrafanaHeaders = (e: React.SyntheticEvent<HTMLInputElement, Event>) => {\n    setForwardGrafanaHeaders(e.currentTarget.checked);\n    onUpdateDatasourceJsonDataOptionChecked(props, 'forwardGrafanaHeaders')(e);\n  };\n\n  return (\n    <div style={{ marginLeft: '30px' }}>\n      <Field label={labels.label}>\n        <>\n          {headers.map((header, index) => (\n            <HttpHeaderEditorV2\n              key={header.name + index}\n              name={header.name}\n              value={header.value}\n              secure={header.secure}\n              isSecureConfigured={configuredSecureHeaders.has(header.name)}\n              onHeaderChange={(h) => updateHeader(index, h)}\n              onRemove={() => removeHeader(index)}\n            />\n          ))}\n          <Button\n            data-testid={selectors.addHeaderButton}\n            icon=\"plus\"\n            variant=\"secondary\"\n            size=\"sm\"\n            onClick={addHeader}\n            className={styles.Common.smallBtn}\n          >\n            {labels.addHeaderLabel}\n          </Button>\n        </>\n      </Field>\n\n      <Checkbox\n        label={labels.forwardGrafanaHeaders.label}\n        checked={forwardGrafanaHeaders}\n        onChange={(e) => updateForwardGrafanaHeaders(e)}\n      />\n    </div>\n  );\n};\n\ninterface HttpHeaderEditorProps {\n  name: string;\n  value: string;\n  secure: boolean;\n  isSecureConfigured: boolean;\n  onHeaderChange: (v: CHHttpHeader) => void;\n  onRemove?: () => void;\n}\n\nconst HttpHeaderEditorV2 = (props: HttpHeaderEditorProps) => {\n  const { onHeaderChange, onRemove } = props;\n  const [name, setName] = useState<string>(props.name);\n  const [value, setValue] = useState<string>(props.value);\n  const [secure, setSecure] = useState<boolean>(props.secure);\n  const [isSecureConfigured, setSecureConfigured] = useState<boolean>(props.isSecureConfigured);\n\n  const labels = allLabels.components.Config.HttpHeadersConfig;\n  const selectors = allSelectors.components.Config.HttpHeaderConfig;\n\n  const onUpdate = () => {\n    onHeaderChange({ name, value, secure });\n  };\n\n  const headerValueLabel = secure ? labels.secureHeaderValueLabel : labels.insecureHeaderValueLabel;\n\n  return (\n    <div data-testid={selectors.headerEditor} style={{ marginTop: '10px' }}>\n      <Stack direction=\"row\" alignItems=\"center\" gap={1}>\n        <Field label={labels.headerNameLabel} aria-label={labels.headerNameLabel}>\n          <Input\n            data-testid={selectors.headerNameInput}\n            value={name}\n            disabled={isSecureConfigured}\n            placeholder={labels.headerNamePlaceholder}\n            onChange={(e: ChangeEvent<HTMLInputElement>) => setName(e.target.value)}\n            onBlur={onUpdate}\n          />\n        </Field>\n\n        <Box flex=\"1 1 auto\">\n          <Field label={headerValueLabel} aria-label={headerValueLabel}>\n            {secure ? (\n              <SecretInput\n                data-testid={selectors.headerValueInput}\n                placeholder={labels.secureHeaderValueLabel}\n                value={value}\n                isConfigured={isSecureConfigured}\n                onReset={() => setSecureConfigured(false)}\n                onChange={(e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value)}\n                onBlur={onUpdate}\n              />\n            ) : (\n              <Input\n                data-testid={selectors.headerValueInput}\n                value={value}\n                placeholder={labels.insecureHeaderValueLabel}\n                onChange={(e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value)}\n                onBlur={onUpdate}\n              />\n            )}\n          </Field>\n        </Box>\n\n        {!isSecureConfigured && (\n          <Checkbox\n            label={labels.secureLabel}\n            checked={secure}\n            onChange={(e) => setSecure(e.currentTarget.checked)}\n            data-testid={selectors.forwardGrafanaHeadersSwitch}\n          />\n        )}\n\n        {onRemove && (\n          <Button\n            data-testid={selectors.removeHeaderButton}\n            className={styles.Common.smallBtn}\n            variant=\"destructive\"\n            size=\"sm\"\n            icon=\"trash-alt\"\n            onClick={onRemove}\n            aria-label=\"http-header-remove\"\n          />\n        )}\n      </Stack>\n    </div>\n  );\n};\n\n/**\n * Returns a Set of all secured headers that are configured\n */\nexport const useConfiguredSecureHttpHeaders = (secureJsonFields: KeyValue<boolean>): Set<string> => {\n  return useMemo(() => {\n    const secureHeaders = new Set<string>();\n    for (const key in secureJsonFields) {\n      if (key.startsWith('secureHttpHeaders.') && secureJsonFields[key]) {\n        secureHeaders.add(key.substring(key.indexOf('.') + 1));\n      }\n    }\n    return secureHeaders;\n  }, [secureJsonFields]);\n};\n"
  },
  {
    "path": "src/views/config-v2/HttpProtocolSettingsSection.test.tsx",
    "content": "import React from 'react';\nimport { render, screen, fireEvent } from '@testing-library/react';\n\nimport { HttpProtocolSettingsSection } from './HttpProtocolSettingsSection';\nimport { createTestProps } from './helpers';\nimport { Protocol } from 'types/config';\n\ndescribe('HttpProtocolSettingsSection', () => {\n  const onOptionsChangeMock = jest.fn();\n  let consoleSpy: jest.SpyInstance;\n\n  const defaultProps = createTestProps({\n    options: {\n      jsonData: {\n        protocol: Protocol.Http,\n        path: '/',\n        httpHeaders: [],\n        forwardGrafanaHeaders: false,\n      },\n      secureJsonData: {},\n      secureJsonFields: {},\n    },\n    mocks: {\n      onOptionsChange: onOptionsChangeMock,\n    },\n  });\n\n  beforeEach(() => {\n    // Mock console.error to suppress React act() warnings\n    consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});\n    jest.clearAllMocks();\n  });\n\n  afterEach(() => {\n    consoleSpy.mockRestore();\n  });\n\n  it('renders nothing when protocol is not HTTP', () => {\n    render(\n      <HttpProtocolSettingsSection\n        {...defaultProps}\n        options={{\n          ...defaultProps.options,\n          jsonData: { ...defaultProps.options.jsonData, protocol: Protocol.Native },\n        }}\n      />\n    );\n\n    expect(screen.queryByLabelText(/path/i)).toBeNull();\n    expect(screen.queryByRole('button', { name: /optional http settings/i })).toBeNull();\n  });\n\n  it('calls onOptionsChange when HTTP path is changed', () => {\n    render(<HttpProtocolSettingsSection {...defaultProps} />);\n\n    const pathInput = screen.getByLabelText(/path/i);\n    fireEvent.change(pathInput, { target: { value: '/api' } });\n\n    expect(onOptionsChangeMock).toHaveBeenCalled();\n  });\n\n  it('toggles Optional HTTP settings open/closed via the button (icon changes)', () => {\n    render(<HttpProtocolSettingsSection {...defaultProps} />);\n\n    expect(screen.getByTestId('angle-right')).toBeInTheDocument();\n\n    fireEvent.click(screen.getByRole('button', { name: /optional http settings/i }));\n\n    expect(screen.getByTestId('angle-down')).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "src/views/config-v2/HttpProtocolSettingsSection.tsx",
    "content": "import React, { useState } from 'react';\nimport { DataSourcePluginOptionsEditorProps, onUpdateDatasourceJsonDataOption } from '@grafana/data';\nimport { Field, Input, Button, useTheme2 } from '@grafana/ui';\nimport allLabels from './labelsV2';\nimport { CHConfig, CHSecureConfig, Protocol } from 'types/config';\nimport { css } from '@emotion/css';\nimport { onHttpHeadersChange } from 'views/CHConfigEditorHooks';\nimport { HttpHeadersConfigV2 } from './HttpHeadersConfigV2';\n\nexport interface HttpProtocolSettingsSectionProps\n  extends DataSourcePluginOptionsEditorProps<CHConfig, CHSecureConfig> {}\n\nexport const HttpProtocolSettingsSection = (props: HttpProtocolSettingsSectionProps) => {\n  const { options, onOptionsChange } = props;\n  const { jsonData, secureJsonFields } = options;\n  const labels = allLabels.components.Config.ConfigEditor;\n\n  const [optionalHttpIsOpen, setOptionalHttpIsOpen] = useState(\n    Boolean(jsonData.httpHeaders?.length || jsonData.forwardGrafanaHeaders)\n  );\n\n  const theme = useTheme2();\n  const styles = {\n    httpSettingsSection: css({ marginTop: theme.spacing(2) }),\n    httpSettingsButton: css({ marginBottom: theme.spacing(2) }),\n  };\n\n  return jsonData.protocol === Protocol.Http ? (\n    <div className={styles.httpSettingsSection}>\n      <Field label={labels.path.label} description={labels.path.tooltip}>\n        <Input\n          value={jsonData.path ?? '/'}\n          name=\"path\"\n          onChange={onUpdateDatasourceJsonDataOption(props, 'path')}\n          aria-label={labels.path.label}\n          placeholder={labels.path.placeholder}\n        />\n      </Field>\n      <Button\n        icon={optionalHttpIsOpen ? 'angle-down' : 'angle-right'}\n        size=\"sm\"\n        variant=\"secondary\"\n        onClick={() => setOptionalHttpIsOpen(!optionalHttpIsOpen)}\n        className={styles.httpSettingsButton}\n      >\n        Optional HTTP settings\n      </Button>\n      {optionalHttpIsOpen && (\n        <HttpHeadersConfigV2\n          options={options}\n          onOptionsChange={onOptionsChange}\n          headers={jsonData.httpHeaders}\n          forwardGrafanaHeaders={jsonData.forwardGrafanaHeaders}\n          secureFields={secureJsonFields}\n          onHttpHeadersChange={(headers) => onHttpHeadersChange(headers, options, onOptionsChange)}\n        />\n      )}\n    </div>\n  ) : null;\n};\n"
  },
  {
    "path": "src/views/config-v2/LeftSidebar.test.tsx",
    "content": "import React from 'react';\nimport { render, screen, fireEvent } from '@testing-library/react';\n\nimport { LeftSidebar } from './LeftSidebar';\nimport { CONFIG_SECTION_HEADERS, CONFIG_SECTION_HEADERS_WITH_PDC } from './constants';\n\ndescribe('LeftSidebar', () => {\n  let consoleSpy: jest.SpyInstance;\n\n  beforeEach(() => {\n    // Mock console.error to suppress React act() warnings\n    consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});\n    jest.clearAllMocks();\n  });\n\n  afterEach(() => {\n    consoleSpy.mockRestore();\n  });\n\n  it('renders all default headers when pdcInjected is false', () => {\n    render(<LeftSidebar pdcInjected={false} />);\n\n    CONFIG_SECTION_HEADERS.forEach((header) => {\n      expect(screen.getByTestId(`${header.label}-sidebar`)).toBeInTheDocument();\n      expect(screen.getByText(header.label)).toBeInTheDocument();\n    });\n  });\n\n  it('renders all PDC headers when pdcInjected is true', () => {\n    render(<LeftSidebar pdcInjected={true} />);\n\n    CONFIG_SECTION_HEADERS_WITH_PDC.forEach((header) => {\n      expect(screen.getByTestId(`${header.label}-sidebar`)).toBeInTheDocument();\n      expect(screen.getByText(header.label)).toBeInTheDocument();\n    });\n  });\n\n  it('shows optional text for optional headers', () => {\n    render(<LeftSidebar pdcInjected={false} />);\n\n    CONFIG_SECTION_HEADERS.filter((h) => h.isOptional).forEach((header) => {\n      expect(screen.getByTestId(`${header.label}-sidebar`).querySelector('div')).toHaveTextContent(/optional/i);\n    });\n  });\n\n  it('scrolls to the target with offset when link is clicked', () => {\n    render(<LeftSidebar pdcInjected={false} />);\n    const firstHeader = CONFIG_SECTION_HEADERS[0];\n\n    const target = document.createElement('div');\n    target.id = firstHeader.id;\n    target.getBoundingClientRect = jest.fn(() => ({ top: 200 }) as any);\n    document.body.appendChild(target);\n\n    Object.defineProperty(window, 'scrollY', { value: 100, writable: true });\n    const scrollToSpy = jest.spyOn(window, 'scrollTo').mockImplementation(() => {});\n\n    fireEvent.click(screen.getByText(firstHeader.label));\n\n    expect(scrollToSpy).toHaveBeenCalledWith({ top: 240, behavior: 'smooth' });\n\n    scrollToSpy.mockRestore();\n    document.body.removeChild(target);\n  });\n});\n"
  },
  {
    "path": "src/views/config-v2/LeftSidebar.tsx",
    "content": "import React from 'react';\nimport { Box, Icon, LinkButton, Space, Stack, Text, useStyles2 } from '@grafana/ui';\nimport { CONFIG_SECTION_HEADERS, CONFIG_SECTION_HEADERS_WITH_PDC } from './constants';\nimport { css } from '@emotion/css';\ninterface LeftSidebarProps {\n  pdcInjected: boolean;\n}\n\nexport const LeftSidebar = ({ pdcInjected }: LeftSidebarProps) => {\n  const headers = pdcInjected ? CONFIG_SECTION_HEADERS_WITH_PDC : CONFIG_SECTION_HEADERS;\n  const styles = useStyles2(getStyles);\n\n  return (\n    <Stack>\n      <Box flex={1} marginY={1}>\n        <Text element=\"h4\">Connect data source</Text>\n        <Box paddingTop={2}>\n          {headers.map((header, index) => (\n            <div key={index} data-testid={`${header.label}-sidebar`}>\n              <Icon name=\"circle\" size=\"xs\" />\n              <LinkButton\n                style={header.isOptional ? { padding: '5px 15px', height: '50px', width: '225px' } : {}}\n                variant=\"secondary\"\n                fill=\"text\"\n                onClick={(e) => {\n                  e.preventDefault();\n                  const target = document.getElementById(header.id);\n                  if (target) {\n                    const top = target.getBoundingClientRect().top + window.scrollY - 60;\n                    window.scrollTo({ top, behavior: 'smooth' });\n                  }\n                }}\n              >\n                <div className={styles.sidebarText}>\n                  <div className={styles.sidebarLabel}>{header.label}</div>\n                  {header.isOptional && (\n                    <div className={styles.sidebarOptional}>\n                      <Text color=\"secondary\" variant=\"bodySmall\">\n                        optional\n                      </Text>\n                    </div>\n                  )}\n                </div>\n              </LinkButton>\n              <Space v={1} />\n            </div>\n          ))}\n        </Box>\n      </Box>\n    </Stack>\n  );\n};\n\nconst getStyles = () => ({\n  inlineField: css({\n    display: 'flex',\n    alignItems: 'center',\n    justifyContent: 'center',\n  }),\n  sidebarText: css({\n    display: 'flex',\n    flexDirection: 'column',\n  }),\n  sidebarLabel: css({\n    display: 'flex',\n    alignItems: 'center',\n    justifyContent: 'center',\n    marginBottom: 0,\n    lineHeight: 1,\n  }),\n  sidebarOptional: css({\n    marginTop: 0,\n    marginBottom: 0,\n    lineHeight: 1,\n  }),\n});\n"
  },
  {
    "path": "src/views/config-v2/ServerAndEncryptionSection.test.tsx",
    "content": "import React from 'react';\nimport { act, render, screen, fireEvent } from '@testing-library/react';\n\nimport { ServerAndEncryptionSection } from './ServerAndEncryptionSection';\nimport { createMockValidation, createTestProps } from './helpers';\nimport { Protocol } from 'types/config';\n\ndescribe('ServerAndEncryptionSection', () => {\n  const onOptionsChangeMock = jest.fn();\n  let consoleSpy: jest.SpyInstance;\n\n  const defaultProps = createTestProps({\n    options: {\n      jsonData: {\n        host: '',\n        secure: false,\n        protocol: Protocol.Native,\n        port: undefined,\n        pdcInjected: false,\n      },\n      secureJsonData: {},\n      secureJsonFields: {},\n    },\n    mocks: {\n      onOptionsChange: onOptionsChangeMock,\n    },\n  });\n\n  beforeEach(() => {\n    // Mock console.error to suppress React act() warnings\n    consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});\n    jest.clearAllMocks();\n  });\n\n  afterEach(() => {\n    consoleSpy.mockRestore();\n  });\n\n  it('calls onOptionsChange when server host address is changed', () => {\n    render(<ServerAndEncryptionSection {...defaultProps} />);\n\n    const input = screen.getByTestId('clickhouse-v2-config-host-input');\n    fireEvent.change(input, { target: { value: 'clickhouse-example.com' } });\n\n    expect(onOptionsChangeMock).toHaveBeenCalled();\n  });\n\n  it('updates protocol value on radio button toggle', () => {\n    render(<ServerAndEncryptionSection {...defaultProps} />);\n\n    const httpOption = screen.getByRole('radio', { name: /http/i });\n    fireEvent.click(httpOption);\n\n    expect(onOptionsChangeMock).toHaveBeenCalled();\n    expect(onOptionsChangeMock.mock.lastCall[0].jsonData.protocol).toBe(Protocol.Http);\n  });\n\n  it('renders HTTPS label instead of HTTP when secure is enabled', () => {\n    const props = createTestProps({\n      options: {\n        jsonData: {\n          host: '',\n          secure: true,\n          protocol: Protocol.Http,\n          port: undefined,\n          pdcInjected: false,\n        },\n        secureJsonData: {},\n        secureJsonFields: {},\n      },\n      mocks: {\n        onOptionsChange: onOptionsChangeMock,\n      },\n    });\n\n    render(<ServerAndEncryptionSection {...props} />);\n\n    expect(screen.getByRole('radio', { name: /https/i })).toBeInTheDocument();\n  });\n\n  it('renders HTTPS secure port description', () => {\n    const props = createTestProps({\n      options: {\n        jsonData: {\n          host: '',\n          secure: true,\n          protocol: Protocol.Http,\n          port: undefined,\n          pdcInjected: false,\n        },\n        secureJsonData: {},\n        secureJsonFields: {},\n      },\n      mocks: {\n        onOptionsChange: onOptionsChangeMock,\n      },\n    });\n\n    render(<ServerAndEncryptionSection {...props} />);\n\n    expect(screen.getByText(/default for HTTPS: 8443/i)).toBeInTheDocument();\n  });\n\n  it('calls onOptionsChange when server port is changed', () => {\n    render(<ServerAndEncryptionSection {...defaultProps} />);\n\n    const input = screen.getByTestId('clickhouse-v2-config-port-input');\n    fireEvent.change(input, { target: { value: 9000 } });\n\n    expect(onOptionsChangeMock).toHaveBeenCalled();\n  });\n\n  it('updates secure state on switch toggle', async () => {\n    render(<ServerAndEncryptionSection {...defaultProps} />);\n\n    const secureSwitch = screen.getByRole('checkbox', { name: /secure connection/i });\n    await fireEvent.click(secureSwitch);\n\n    expect(onOptionsChangeMock).toHaveBeenLastCalledWith(\n      expect.objectContaining({\n        jsonData: expect.objectContaining({ secure: true }),\n      })\n    );\n  });\n\n  describe('validation', () => {\n    const emptyProps = createTestProps({\n      options: {\n        jsonData: { host: '', port: undefined, protocol: Protocol.Native, secure: false },\n        secureJsonData: {},\n        secureJsonFields: {},\n      },\n      mocks: { onOptionsChange: jest.fn() },\n    });\n\n    const filledProps = createTestProps({\n      options: {\n        jsonData: { host: 'clickhouse-server', port: 9000, protocol: Protocol.Native, secure: false },\n        secureJsonData: {},\n        secureJsonFields: {},\n      },\n      mocks: { onOptionsChange: jest.fn() },\n    });\n\n    it('shows inline errors for host and port when validator is called with empty values', async () => {\n      const validation = createMockValidation();\n      render(<ServerAndEncryptionSection {...emptyProps} validation={validation} />);\n\n      await act(async () => {\n        validation.runValidator();\n      });\n\n      expect(screen.getByText('Server address required')).toBeInTheDocument();\n      expect(screen.getByText('Port is required')).toBeInTheDocument();\n    });\n\n    it('shows no errors when all fields are filled', async () => {\n      const validation = createMockValidation();\n      render(<ServerAndEncryptionSection {...filledProps} validation={validation} />);\n\n      await act(async () => {\n        validation.runValidator();\n      });\n\n      expect(screen.queryByText('Server address required')).not.toBeInTheDocument();\n      expect(screen.queryByText('Port is required')).not.toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "src/views/config-v2/ServerAndEncryptionSection.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport {\n  DataSourcePluginOptionsEditorProps,\n  GrafanaTheme2,\n  onUpdateDatasourceJsonDataOption,\n  onUpdateDatasourceJsonDataOptionChecked,\n} from '@grafana/data';\nimport { ValidationAPI } from '../CHConfigEditorHooks';\nimport {\n  Box,\n  CollapsableSection,\n  TextLink,\n  Field,\n  Input,\n  Toggletip,\n  IconButton,\n  RadioButtonGroup,\n  Text,\n  useStyles2,\n  Checkbox,\n} from '@grafana/ui';\nimport allLabels from './labelsV2';\nimport { CHConfig, CHSecureConfig, Protocol } from 'types/config';\nimport { CONFIG_SECTION_HEADERS, CONTAINER_MIN_WIDTH } from './constants';\nimport { css } from '@emotion/css';\nimport {\n  trackClickhouseConfigV2HostInput,\n  trackClickhouseConfigV2NativeHttpToggleClicked,\n  trackClickhouseConfigV2PortInput,\n  trackClickhouseConfigV2SecureConnectionChecked,\n} from './tracking';\nimport { HttpProtocolSettingsSection } from './HttpProtocolSettingsSection';\n\nexport interface Props extends DataSourcePluginOptionsEditorProps<CHConfig, CHSecureConfig> {\n  validation?: ValidationAPI;\n}\n\nexport const ServerAndEncryptionSection = (props: Props) => {\n  const { options, onOptionsChange, validation } = props;\n  const { jsonData } = options;\n  const labels = allLabels.components.Config.ConfigEditor;\n  const defaultPort = jsonData.secure\n    ? jsonData.protocol === Protocol.Native\n      ? labels.serverPort.secureNativePort\n      : labels.serverPort.secureHttpPort\n    : jsonData.protocol === Protocol.Native\n      ? labels.serverPort.insecureNativePort\n      : labels.serverPort.insecureHttpPort;\n  const protocolLabel =\n    jsonData.protocol === Protocol.Http\n      ? jsonData.secure\n        ? 'HTTPS'\n        : 'HTTP'\n      : jsonData.secure\n        ? 'Native (secure)'\n        : 'Native';\n\n  const portDescription = `${labels.serverPort.tooltip} (default for ${protocolLabel}: ${defaultPort})`;\n\n  const styles = useStyles2(getStyles);\n\n  const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});\n\n  useEffect(() => {\n    // Always clear errors eagerly when the user fills in a field, regardless of\n    // whether the ValidationAPI is available\n    if (jsonData.host) {\n      setFieldErrors((prev) => { const next = { ...prev }; delete next.host; return next; });\n      validation?.clearError('host');\n    }\n    if (jsonData.port) {\n      setFieldErrors((prev) => { const next = { ...prev }; delete next.port; return next; });\n      validation?.clearError('port');\n    }\n    if (!validation) {\n      return;\n    }\n    return validation.registerValidation(() => {\n      const errors: Record<string, string> = {};\n      if (!jsonData.host) {\n        errors.host = labels.serverAddress.error;\n      }\n      if (!jsonData.port) {\n        errors.port = labels.serverPort.error;\n      }\n      setFieldErrors(errors);\n      Object.entries(errors).forEach(([field, msg]) => validation.setError(field, msg));\n      return Object.keys(errors).length === 0;\n    });\n  }, [jsonData.host, jsonData.port, validation, labels.serverAddress.error, labels.serverPort.error]);\n\n  const PROTOCOL_OPTIONS = [\n    { label: 'Native', value: Protocol.Native },\n    { label: options.jsonData.secure ? 'HTTPS' : 'HTTP', value: Protocol.Http },\n  ];\n\n  const onProtocolToggle = (protocol: Protocol) => {\n    onOptionsChange({\n      ...options,\n      jsonData: {\n        ...options.jsonData,\n        protocol: protocol,\n      },\n    });\n  };\n\n  const onPortChange = (port: string) => {\n    onOptionsChange({\n      ...options,\n      jsonData: {\n        ...options.jsonData,\n        port: +port,\n      },\n    });\n  };\n\n  return (\n    <Box\n      borderStyle=\"solid\"\n      borderColor=\"weak\"\n      padding={2}\n      marginBottom={4}\n      id={`${CONFIG_SECTION_HEADERS[0].id}`}\n      minWidth={CONTAINER_MIN_WIDTH}\n    >\n      <CollapsableSection\n        label={<Text variant=\"h3\">{CONFIG_SECTION_HEADERS[0].label}</Text>}\n        isOpen={!!CONFIG_SECTION_HEADERS[0].isOpen}\n      >\n        <Text variant=\"body\" color=\"secondary\">\n          Enter the server address of your ClickHouse instance. Then select your protocol, port and security options. If\n          you need further guidance, visit the{' '}\n          <TextLink\n            href=\"https://grafana.com/grafana/plugins/grafana-clickhouse-datasource/\"\n            icon=\"external-link-alt\"\n            external\n          >\n            Grafana docs\n          </TextLink>\n        </Text>\n        <Field\n          required\n          label={labels.serverAddress.label}\n          style={{ marginTop: '30px' }}\n          invalid={!!fieldErrors.host}\n          error={fieldErrors.host}\n        >\n          <Input\n            name=\"host\"\n            value={jsonData.host || ''}\n            onChange={(e) => onUpdateDatasourceJsonDataOption(props, 'host')(e)}\n            label={labels.serverAddress.label}\n            aria-label={labels.serverAddress.label}\n            data-testid=\"clickhouse-v2-config-host-input\"\n            placeholder={labels.serverAddress.placeholder}\n            onBlur={(e) => {\n              trackClickhouseConfigV2HostInput();\n              if (!e.currentTarget.value) {\n                setFieldErrors((prev) => ({ ...prev, host: labels.serverAddress.error }));\n                validation?.setError('host', labels.serverAddress.error);\n              }\n            }}\n          />\n        </Field>\n        <div className={styles.protocolPortRow}>\n          <div className={styles.protocolSection}>\n            <div className={styles.protocolLabel}>\n              <Text variant=\"bodySmall\" weight=\"bold\">\n                {labels.protocol.label}\n              </Text>\n              <Toggletip\n                theme=\"info\"\n                placement=\"top\"\n                content={\n                  <div className={styles.toggleTipText}>\n                    <Text>\n                      ClickHouse supports two server protocols: Native TCP and HTTP. Both protocols can be secured with\n                      TLS.\n                      <br />\n                      <br />\n                      <TextLink href=\"https://clickhouse.com/docs/interfaces/tcp\" variant=\"bodySmall\" external>\n                        Native TCP\n                      </TextLink>{' '}\n                      is the default and recommended option.\n                      <br />\n                      <TextLink href=\"https://clickhouse.com/docs/interfaces/http\" variant=\"bodySmall\" external>\n                        HTTP\n                      </TextLink>{' '}\n                      is for servers configured to accept HTTP connections.\n                    </Text>\n                  </div>\n                }\n              >\n                <IconButton\n                  name=\"question-circle\"\n                  aria-label=\"More info about Protocol\"\n                  size=\"xs\"\n                  className={styles.toggleTipIcon}\n                />\n              </Toggletip>\n            </div>\n            <Field label=\"\" description={<div className={styles.toggleTipText}>{labels.protocol.tooltip}</div>}>\n              <RadioButtonGroup<Protocol>\n                options={PROTOCOL_OPTIONS}\n                value={jsonData.protocol || Protocol.Native}\n                onChange={(e) => {\n                  trackClickhouseConfigV2NativeHttpToggleClicked({ nativeHttpToggle: e });\n                  onProtocolToggle(e!);\n                }}\n              />\n            </Field>\n          </div>\n          <div className={styles.portSection}>\n            <Field\n              required\n              label={labels.serverPort.label}\n              description={portDescription}\n              invalid={!!fieldErrors.port}\n              error={fieldErrors.port}\n            >\n              <Input\n                name=\"port\"\n                type=\"number\"\n                value={jsonData.port || ''}\n                onChange={(e) => onPortChange(e.currentTarget.value)}\n                label={labels.serverPort.label}\n                aria-label={labels.serverPort.label}\n                data-testid=\"clickhouse-v2-config-port-input\"\n                placeholder={labels.serverPort.placeholder}\n                onBlur={(e) => {\n                  trackClickhouseConfigV2PortInput({ port: e.currentTarget.value });\n                  if (!e.currentTarget.value) {\n                    setFieldErrors((prev) => ({ ...prev, port: labels.serverPort.error }));\n                    validation?.setError('port', labels.serverPort.error);\n                  }\n                }}\n              />\n            </Field>\n          </div>\n        </div>\n        <div className={styles.secureToggle}>\n          <Checkbox\n            label={labels.secure.label}\n            description={labels.secure.tooltip}\n            checked={jsonData.secure || false}\n            onChange={(e) => {\n              trackClickhouseConfigV2SecureConnectionChecked({ secureConnection: e.currentTarget.checked });\n              onUpdateDatasourceJsonDataOptionChecked(props, 'secure')(e);\n            }}\n          />\n        </div>\n        <HttpProtocolSettingsSection {...props} />\n      </CollapsableSection>\n    </Box>\n  );\n};\n\nconst getStyles = (theme: GrafanaTheme2) => ({\n  protocolPortRow: css({\n    display: 'flex',\n    alignItems: 'center',\n  }),\n  protocolLabel: css({\n    display: 'flex',\n    height: 15,\n    width: '205px',\n  }),\n  protocolSection: css({\n    display: 'flex',\n    flexDirection: 'column',\n    justifyContent: 'center',\n    gap: theme.spacing(0),\n    marginRight: theme.spacing(5),\n  }),\n  toggleTipIcon: css({\n    marginLeft: theme.spacing(0.5),\n    padding: 0,\n  }),\n  portSection: css({\n    width: '100%',\n  }),\n  toggleTipText: css({\n    whiteSpace: 'pre-line',\n  }),\n  secureToggle: css({\n    display: 'flex',\n    alignItems: 'center',\n    marginTop: theme.spacing(1),\n  }),\n});\n"
  },
  {
    "path": "src/views/config-v2/TLSSSLSettingsSection.test.tsx",
    "content": "import React from 'react';\nimport { render, screen, fireEvent } from '@testing-library/react';\nimport { TLSSSLSettingsSection } from './TLSSSLSettingsSection';\nimport { createTestProps } from './helpers';\n\ndescribe('TLSSSLSettingsSection', () => {\n  const onOptionsChangeMock = jest.fn();\n  let consoleSpy: jest.SpyInstance;\n\n  const defaultProps = createTestProps({\n    options: {\n      jsonData: {\n        tlsSkipVerify: false,\n        tlsAuth: false,\n        tlsAuthWithCACert: false,\n      },\n      secureJsonData: {},\n      secureJsonFields: {},\n    },\n    mocks: {\n      onOptionsChange: onOptionsChangeMock,\n    },\n  });\n\n  beforeEach(() => {\n    // Mock console.error to suppress React act() warnings\n    consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});\n    jest.clearAllMocks();\n  });\n\n  afterEach(() => {\n    consoleSpy.mockRestore();\n  });\n\n  it('renders the three TLS checkboxes', () => {\n    render(<TLSSSLSettingsSection {...defaultProps} />);\n    const openTLSSection = screen.getByRole('button', { name: /tls\\/ssl settings/i });\n    fireEvent.click(openTLSSection);\n\n    expect(screen.getByLabelText(/skip tls verify/i)).toBeInTheDocument();\n    expect(screen.getByLabelText(/tls client auth/i)).toBeInTheDocument();\n    expect(screen.getByLabelText(/with ca cert/i)).toBeInTheDocument();\n  });\n\n  it('updates jsonData.tlsSkipVerify on click', () => {\n    render(<TLSSSLSettingsSection {...defaultProps} />);\n    const openTLSSection = screen.getByRole('button', { name: /tls\\/ssl settings/i });\n    fireEvent.click(openTLSSection);\n\n    const cb = screen.getByLabelText(/skip tls verify/i);\n    fireEvent.click(cb);\n\n    expect(onOptionsChangeMock).toHaveBeenCalled();\n    const last = onOptionsChangeMock.mock.lastCall?.[0];\n    expect(last.jsonData).toHaveProperty('tlsSkipVerify');\n  });\n\n  it('shows Client Cert + Client Key inputs when tlsAuth is true', async () => {\n    const props = createTestProps({\n      options: {\n        jsonData: { tlsSkipVerify: false, tlsAuth: true, tlsAuthWithCACert: false },\n        secureJsonData: {},\n        secureJsonFields: {},\n      },\n      mocks: { onOptionsChange: onOptionsChangeMock },\n    });\n\n    render(<TLSSSLSettingsSection {...props} />);\n\n    expect(screen.getByText(/client cert/i)).toBeInTheDocument();\n    expect(screen.getByText(/client key/i)).toBeInTheDocument();\n  });\n\n  it('updates secureJsonData.tlsClientCert when typing into Client certificate', () => {\n    const props = createTestProps({\n      options: {\n        jsonData: { tlsSkipVerify: false, tlsAuth: true, tlsAuthWithCACert: false },\n        secureJsonData: {},\n        secureJsonFields: {},\n      },\n      mocks: { onOptionsChange: onOptionsChangeMock },\n    });\n\n    render(<TLSSSLSettingsSection {...props} />);\n\n    const input = screen.getByPlaceholderText(/client cert\\. begins with/i);\n    fireEvent.change(input, { target: { value: '---CERT---' } });\n\n    const last = onOptionsChangeMock.mock.lastCall?.[0];\n    expect(last.secureJsonData?.tlsClientCert).toBe('---CERT---');\n  });\n\n  it('updates secureJsonData.tlsClientKey when typing into Client key', () => {\n    const props = createTestProps({\n      options: {\n        jsonData: { tlsSkipVerify: false, tlsAuth: true, tlsAuthWithCACert: false },\n        secureJsonData: {},\n        secureJsonFields: {},\n      },\n      mocks: { onOptionsChange: onOptionsChangeMock },\n    });\n\n    render(<TLSSSLSettingsSection {...props} />);\n\n    const input = screen.getByPlaceholderText(/client key\\. begins with/i);\n    fireEvent.change(input, { target: { value: '---CERT---' } });\n\n    const last = onOptionsChangeMock.mock.lastCall?.[0];\n    expect(last.secureJsonData?.tlsClientKey).toBe('---CERT---');\n  });\n\n  it('updates secureJsonData.tlsCACert when typing into CA cert', () => {\n    const props = createTestProps({\n      options: {\n        jsonData: { tlsSkipVerify: false, tlsAuth: false, tlsAuthWithCACert: true },\n        secureJsonData: {},\n        secureJsonFields: {},\n      },\n      mocks: { onOptionsChange: onOptionsChangeMock },\n    });\n\n    render(<TLSSSLSettingsSection {...props} />);\n\n    const input = screen.getByPlaceholderText(/ca cert\\. begins with/i);\n    fireEvent.change(input, { target: { value: '---CERT---' } });\n\n    const last = onOptionsChangeMock.mock.lastCall?.[0];\n    expect(last.secureJsonData?.tlsCACert).toBe('---CERT---');\n  });\n\n  it('shows CA certificate input when tlsAuthWithCACert is true', () => {\n    const props = createTestProps({\n      options: {\n        jsonData: { tlsSkipVerify: false, tlsAuth: false, tlsAuthWithCACert: true },\n        secureJsonData: {},\n        secureJsonFields: { tlsCACert: false },\n      },\n      mocks: { onOptionsChange: onOptionsChangeMock },\n    });\n\n    render(<TLSSSLSettingsSection {...props} />);\n\n    expect(screen.getByLabelText(/ca cert/i)).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "src/views/config-v2/TLSSSLSettingsSection.tsx",
    "content": "import { DataSourcePluginOptionsEditorProps, onUpdateDatasourceJsonDataOptionChecked } from '@grafana/data';\nimport { Box, CollapsableSection, CertificationKey, Text, useStyles2, Checkbox, Stack, Badge } from '@grafana/ui';\nimport React from 'react';\nimport { CHConfig, CHSecureConfig } from 'types/config';\nimport { CONFIG_SECTION_HEADERS, CONTAINER_MIN_WIDTH } from './constants';\nimport allLabels from './labelsV2';\nimport { css } from '@emotion/css';\nimport {\n  trackClickhouseConfigV2SkipTLSVerifyToggleClicked,\n  trackClickhouseConfigV2TLSClientAuthToggleClicked,\n  trackClickhouseConfigV2WithCACertToggleClicked,\n} from './tracking';\n\nexport interface Props extends DataSourcePluginOptionsEditorProps<CHConfig, CHSecureConfig> {}\n\nexport const TLSSSLSettingsSection = (props: Props) => {\n  const { options, onOptionsChange } = props;\n  const { jsonData, secureJsonFields } = options;\n  const secureJsonData = (options.secureJsonData || {}) as CHSecureConfig;\n  const labels = allLabels.components.Config.ConfigEditor;\n  const hasTLSCACert = secureJsonFields && secureJsonFields.tlsCACert;\n  const hasTLSClientCert = secureJsonFields && secureJsonFields.tlsClientCert;\n  const hasTLSClientKey = secureJsonFields && secureJsonFields.tlsClientKey;\n  const styles = useStyles2(getStyles);\n\n  const onCertificateChangeFactory = (key: keyof Omit<CHSecureConfig, 'password'>, value: string) => {\n    onOptionsChange({\n      ...options,\n      secureJsonData: {\n        ...secureJsonData,\n        [key]: value,\n      },\n    });\n  };\n  const onResetClickFactory = (key: keyof Omit<CHSecureConfig, 'password'>) => {\n    onOptionsChange({\n      ...options,\n      secureJsonFields: {\n        ...secureJsonFields,\n        [key]: false,\n      },\n      secureJsonData: {\n        ...secureJsonData,\n        [key]: '',\n      },\n    });\n  };\n\n  return (\n    <Box\n      borderStyle=\"solid\"\n      borderColor=\"weak\"\n      padding={2}\n      marginBottom={4}\n      id={`${CONFIG_SECTION_HEADERS[2].id}`}\n      minWidth={CONTAINER_MIN_WIDTH}\n    >\n      <CollapsableSection\n        label={\n          <>\n            <Text variant=\"h3\">{CONFIG_SECTION_HEADERS[2].label}</Text>\n            <Badge text=\"optional\" color=\"darkgrey\" className={styles.badge} />\n          </>\n        }\n        isOpen={!!(jsonData.tlsSkipVerify || jsonData.tlsAuth || jsonData.tlsAuthWithCACert)}\n      >\n        <Text variant=\"body\" color=\"secondary\">\n          TLS/SSL certificates are used to prove identity and encrypt traffic between Grafana and ClickHouse.\n        </Text>\n        <div className={styles.contentSection}>\n          <Stack\n            direction={jsonData.tlsAuth || jsonData.tlsAuthWithCACert ? 'column' : 'row'}\n            gap={3}\n            alignItems=\"flex-start\"\n          >\n            <Checkbox\n              className={css({ margin: 0 })}\n              label={labels.tlsSkipVerify.label}\n              value={jsonData.tlsSkipVerify || false}\n              onChange={(e) => {\n                trackClickhouseConfigV2SkipTLSVerifyToggleClicked({ skipTlsVerifyToggle: e.currentTarget.checked });\n                onUpdateDatasourceJsonDataOptionChecked(props, 'tlsSkipVerify')(e);\n              }}\n            />\n            <Checkbox\n              className={css({ margin: 0 })}\n              label={labels.tlsClientAuth.label}\n              value={jsonData.tlsAuth || false}\n              onChange={(e) => {\n                trackClickhouseConfigV2TLSClientAuthToggleClicked({ clientAuthToggle: e.currentTarget.checked });\n                onUpdateDatasourceJsonDataOptionChecked(props, 'tlsAuth')(e);\n              }}\n            />\n            {jsonData.tlsAuth && (\n              <div className={styles.certsSection}>\n                <CertificationKey\n                  hasCert={!!hasTLSClientCert}\n                  onChange={(e) => onCertificateChangeFactory('tlsClientCert', e.currentTarget.value)}\n                  placeholder={labels.tlsClientCert.placeholder}\n                  label={labels.tlsClientCert.label}\n                  onClick={() => onResetClickFactory('tlsClientCert')}\n                  data-testid=\"tls-client-cert\"\n                />\n                <CertificationKey\n                  hasCert={!!hasTLSClientKey}\n                  placeholder={labels.tlsClientKey.placeholder}\n                  label={labels.tlsClientKey.label}\n                  onChange={(e) => onCertificateChangeFactory('tlsClientKey', e.currentTarget.value)}\n                  onClick={() => onResetClickFactory('tlsClientKey')}\n                  data-testid=\"tls-client-key\"\n                />\n              </div>\n            )}\n            <Checkbox\n              label={labels.tlsAuthWithCACert.label}\n              value={jsonData.tlsAuthWithCACert || false}\n              onChange={(e) => {\n                trackClickhouseConfigV2WithCACertToggleClicked({ caCertToggle: e.currentTarget.checked });\n                onUpdateDatasourceJsonDataOptionChecked(props, 'tlsAuthWithCACert')(e);\n              }}\n            />\n            <div className={styles.certsSection}>\n              {jsonData.tlsAuthWithCACert && (\n                <CertificationKey\n                  hasCert={!!hasTLSCACert}\n                  onChange={(e) => onCertificateChangeFactory('tlsCACert', e.currentTarget.value)}\n                  placeholder={labels.tlsCACert.placeholder}\n                  label={labels.tlsCACert.label}\n                  onClick={() => onResetClickFactory('tlsCACert')}\n                  data-testid=\"tls-ca-cert\"\n                />\n              )}\n            </div>\n          </Stack>\n        </div>\n      </CollapsableSection>\n    </Box>\n  );\n};\n\nconst getStyles = () => ({\n  contentSection: css({\n    marginTop: '30px',\n  }),\n  optionsRow: css({\n    display: 'flex',\n    gap: '50px',\n  }),\n  certsSection: css({\n    marginTop: '10px',\n  }),\n  badge: css({\n    marginLeft: 'auto',\n  }),\n});\n"
  },
  {
    "path": "src/views/config-v2/constants.ts",
    "content": "import { selectors } from '@grafana/e2e-selectors';\n\nexport const CONTAINER_MIN_WIDTH = '450px';\n\nexport const CONFIG_SECTION_HEADERS = [\n  { label: 'Server and encryption', id: 'server', isOpen: true, isOptional: false },\n  { label: 'Database credentials', id: 'credentials', isOpen: true, isOptional: false },\n  { label: 'TLS/SSL settings', id: 'tls', isOpen: false, isOptional: true },\n  { label: 'Additional settings', id: 'additional', isOpen: false, isOptional: true },\n  { label: 'Save & test', id: `${selectors.pages.DataSource.saveAndTest}`, isOpen: undefined, isOptional: null },\n];\n\nexport const CONFIG_SECTION_HEADERS_WITH_PDC = [\n  { label: 'Server and encryption', id: 'server', isOpen: true, isOptional: false },\n  { label: 'Database credentials', id: 'credentials', isOpen: true, isOptional: false },\n  { label: 'TLS/SSL settings', id: 'tls', isOpen: false, isOptional: true },\n  { label: 'Additional settings', id: 'additional', isOpen: false, isOptional: true },\n  { label: 'Private data source connect', id: 'pdc', isOpen: false, isOptional: true },\n  { label: 'Save & test', id: `${selectors.pages.DataSource.saveAndTest}`, isOpen: undefined, isOptional: null },\n];\n"
  },
  {
    "path": "src/views/config-v2/helpers.ts",
    "content": "import { Protocol } from 'types/config';\nimport { ValidationAPI } from '../CHConfigEditorHooks';\n\n/**\n * Creates a mock ValidationAPI for use in tests. Captures the registered\n * validator so tests can invoke it directly and assert on inline error display.\n */\nexport const createMockValidation = (): ValidationAPI & { runValidator: () => boolean | Promise<boolean> } => {\n  let registeredValidator: (() => boolean | Promise<boolean>) | null = null;\n  return {\n    registerValidation: jest.fn((fn) => {\n      registeredValidator = fn;\n      return () => {};\n    }),\n    validate: jest.fn(async () => true),\n    isValid: jest.fn(() => true),\n    getErrors: jest.fn(() => ({})),\n    setError: jest.fn(),\n    clearError: jest.fn(),\n    runValidator: () => registeredValidator?.() ?? true,\n  };\n};\n\n/**\n * Creates a set of test props for the Clickhouse V2 config page for use in tests.\n * This function allows you to override default properties for specific test cases.\n */\nexport const createTestProps = (overrides: { options?: object; mocks?: object }) => ({\n  options: {\n    access: 'proxy',\n    basicAuth: false,\n    basicAuthUser: '',\n    database: '',\n    id: 1,\n    isDefault: false,\n    jsonData: {\n      version: '',\n      host: '',\n      port: 9000,\n      protocol: Protocol.Native,\n      username: '',\n    },\n    name: 'ClickHouse',\n    orgId: 1,\n    readOnly: false,\n    secureJsonFields: {},\n    type: 'clickhouse',\n    typeLogoUrl: '',\n    typeName: 'ClickHouse',\n    uid: 'z',\n    url: '',\n    user: '',\n    withCredentials: false,\n    ...overrides.options,\n  },\n  onOptionsChange: jest.fn(),\n  ...overrides.mocks,\n});\n"
  },
  {
    "path": "src/views/config-v2/labelsV2.ts",
    "content": "import { ColumnHint } from 'types/queryBuilder';\n\nexport default {\n  components: {\n    Config: {\n      ConfigEditor: {\n        serverAddress: {\n          label: 'Server address',\n          placeholder: 'Enter server address',\n          tooltip: 'ClickHouse host address',\n          error: 'Server address required',\n        },\n        serverPort: {\n          label: 'Server port',\n          placeholder: 'Enter server port',\n          insecureNativePort: '9000',\n          insecureHttpPort: '8123',\n          secureNativePort: '9440',\n          secureHttpPort: '8443',\n          tooltip: 'ClickHouse server port',\n          error: 'Port is required',\n        },\n        path: {\n          label: 'HTTP URL Path',\n          tooltip: 'Additional URL path for HTTP requests. Default: /',\n          placeholder: 'additional-path',\n        },\n        protocol: {\n          label: 'Protocol',\n          tooltip: 'Native is the default protocol',\n        },\n        username: {\n          label: 'Username',\n          placeholder: 'Enter username',\n          tooltip: 'We recommend configuring',\n          error: 'Username is required',\n        },\n        password: {\n          label: 'Password',\n          placeholder: 'Enter password',\n          tooltip: 'ClickHouse password',\n        },\n        tlsSkipVerify: {\n          label: 'Skip TLS Verify',\n          tooltip: 'Skip TLS Verify',\n        },\n        tlsClientAuth: {\n          label: 'TLS Client Auth',\n          tooltip: 'TLS Client Auth',\n        },\n        tlsAuthWithCACert: {\n          label: 'With CA Cert',\n          tooltip: 'Needed for verifying self-signed TLS Certs',\n        },\n        tlsCACert: {\n          label: 'CA Cert',\n          placeholder: 'CA Cert. Begins with -----BEGIN CERTIFICATE-----',\n        },\n        tlsClientCert: {\n          label: 'Client Cert',\n          placeholder: 'Client Cert. Begins with -----BEGIN CERTIFICATE-----',\n        },\n        tlsClientKey: {\n          label: 'Client Key',\n          placeholder: 'Client Key. Begins with -----BEGIN RSA PRIVATE KEY-----',\n        },\n        secure: {\n          label: 'Secure Connection',\n          tooltip: 'Check to connect securely with TLS',\n        },\n        secureSocksProxy: {\n          label: 'Enable Secure Socks Proxy',\n          tooltip: 'Enable proxying the datasource connection through the secure socks proxy to a different network.',\n        },\n        enableRowLimit: {\n          label: 'Enable row limit',\n          testid: 'data-testid enable-row-limit-switch',\n          tooltip:\n            'Enable using the Grafana row limit setting to limit the number of rows returned from ClickHouse. Ensure the appropriate permissions are set for your user. Only supported for Grafana >= 11.0.0. Defaults to false.',\n        },\n      },\n      HttpHeadersConfig: {\n        title: 'HTTP Headers',\n        label: 'Custom HTTP Headers',\n        description: 'Add Custom HTTP headers when querying the database',\n        headerNameLabel: 'Header Name',\n        headerNamePlaceholder: 'X-Custom-Header',\n        insecureHeaderValueLabel: 'Header Value',\n        secureHeaderValueLabel: 'Secure Header Value',\n        secureLabel: 'Secure',\n        addHeaderLabel: 'Add Header',\n        forwardGrafanaHeaders: {\n          label: 'Forward Grafana HTTP Headers to data source',\n          tooltip: 'Forward Grafana HTTP Headers to data source.',\n        },\n      },\n      AliasTableConfig: {\n        title: 'Column Alias Tables',\n        descriptionParts: [\n          'Provide alias tables with a',\n          '(`alias` String, `select` String, `type` String)',\n          'schema to use as a source for column selection.',\n        ],\n        addTableLabel: 'Add Table',\n        targetDatabaseLabel: 'Target Database',\n        targetDatabasePlaceholder: '(optional)',\n        targetTableLabel: 'Target Table',\n        aliasDatabaseLabel: 'Alias Database',\n        aliasDatabasePlaceholder: '(optional)',\n        aliasTableLabel: 'Alias Table',\n      },\n\n      DefaultDatabaseTableConfig: {\n        title: 'Default DB and table',\n        database: {\n          label: 'Default database',\n          description: 'The default database used by the query builder',\n          name: 'defaultDatabase',\n          placeholder: 'default',\n        },\n        table: {\n          label: 'Default table',\n          description: 'The default table used by the query builder',\n          name: 'defaultTable',\n          placeholder: 'table',\n        },\n      },\n      QuerySettingsConfig: {\n        title: 'Query settings',\n        connMaxLifetime: {\n          label: 'Connection Max Lifetime (minutes)',\n          name: 'connMaxLifetime',\n          placeholder: '5',\n          tooltip: 'Maximum lifetime of a connection in minutes',\n        },\n        dialTimeout: {\n          label: 'Dial Timeout (seconds)',\n          name: 'dialTimeout',\n          placeholder: '10',\n          tooltip: 'Timeout in seconds for connection',\n        },\n        maxIdleConns: {\n          label: 'Max Idle Connections',\n          name: 'maxIdleConns',\n          placeholder: '25',\n          tooltip: 'Maximum number of idle connections',\n        },\n        maxOpenConns: {\n          label: 'Max Open Connections',\n          name: 'maxOpenConns',\n          placeholder: '50',\n          tooltip: 'Maximum number of open connections',\n        },\n        queryTimeout: {\n          label: 'Query Timeout (seconds)',\n          name: 'queryTimeout',\n          placeholder: '60',\n          tooltip: 'Timeout in seconds for read queries',\n        },\n        validateSql: {\n          label: 'Validate SQL',\n          tooltip: 'Validate SQL in the editor.',\n        },\n      },\n      TracesConfig: {\n        title: 'Traces configuration',\n        description: '(Optional) Default settings for trace queries',\n        defaultDatabase: {\n          label: 'Default trace database',\n          description: 'the default database used by the trace query builder',\n          name: 'defaultDatabase',\n          placeholder: 'default',\n        },\n        defaultTable: {\n          label: 'Default trace table',\n          description: 'the default table used by the trace query builder',\n          name: 'defaultTable',\n        },\n        columns: {\n          title: 'Default columns',\n          description: 'Default columns for trace queries. Leave empty to disable.',\n\n          traceId: {\n            label: 'Trace ID column',\n            tooltip: 'Column for the trace ID',\n          },\n          spanId: {\n            label: 'Span ID column',\n            tooltip: 'Column for the span ID',\n          },\n          parentSpanId: {\n            label: 'Parent Span ID column',\n            tooltip: 'Column for the parent span ID',\n          },\n          serviceName: {\n            label: 'Service Name column',\n            tooltip: 'Column for the service name',\n          },\n          operationName: {\n            label: 'Operation Name column',\n            tooltip: 'Column for the operation name',\n          },\n          startTime: {\n            label: 'Start Time column',\n            tooltip: 'Column for the start time',\n          },\n          durationTime: {\n            label: 'Duration Time column',\n            tooltip: 'Column for the duration time',\n          },\n          tags: {\n            label: 'Tags column',\n            tooltip: 'Column for the trace tags',\n          },\n          serviceTags: {\n            label: 'Service Tags column',\n            tooltip: 'Column for the service tags',\n          },\n          flattenNested: {\n            label: 'Use Flatten Nested',\n            tooltip: 'Enable if your traces table was created with flatten_nested=1',\n          },\n          eventsPrefix: {\n            label: 'Events prefix',\n            tooltip: 'Prefix for the events column (Events.Timestamp, Events.Name, etc.)',\n          },\n          linksPrefix: {\n            label: 'Links prefix',\n            tooltip: 'Prefix for the trace references column (Links.TraceId, Links.TraceState, etc.)',\n          },\n          kind: {\n            label: 'Kind column',\n            tooltip: 'Column for the trace kind',\n          },\n          statusCode: {\n            label: 'Status Code column',\n            tooltip: 'Column for the trace status code',\n          },\n          statusMessage: {\n            label: 'Status Message column',\n            tooltip: 'Column for the trace status message',\n          },\n          instrumentationLibraryName: {\n            label: 'Library Name column',\n            tooltip: 'Column for the instrumentation library name',\n          },\n          instrumentationLibraryVersion: {\n            label: 'Library Version column',\n            tooltip: 'Column for the instrumentation library version',\n          },\n          state: {\n            label: 'State column',\n            tooltip: 'Column for the trace state',\n          },\n        },\n      },\n      traceIdCorrelation: {\n        title: 'Trace ID correlation',\n        description: 'Options for showing links to correlated data.',\n\n        showTraceLinks: {\n          label: 'Show \"View trace\" links',\n          tooltip: 'Show \"View trace\" links on trace_id/traceid fields.',\n        },\n      },\n      LogsConfig: {\n        title: 'Logs configuration',\n        description: '(Optional) default settings for log queries',\n        defaultDatabase: {\n          label: 'Default log database',\n          description: 'the default database used by the logs query builder',\n          name: 'defaultDatabase',\n          placeholder: 'default',\n        },\n        defaultTable: {\n          label: 'Default log table',\n          description: 'the default table used by the logs query builder',\n          name: 'defaultTable',\n        },\n        columns: {\n          title: 'Default columns',\n          description: 'Default columns for log queries. Leave empty to disable.',\n\n          time: {\n            label: 'Time column',\n            tooltip: 'Column for the log timestamp',\n          },\n          level: {\n            label: 'Log Level column',\n            tooltip: 'Column for the log level',\n          },\n          message: {\n            label: 'Log Message column',\n            tooltip: 'Column for log message',\n          },\n        },\n        traceIdCorrelation: {\n          title: 'Trace ID correlation',\n          description: 'Options for showing links to correlated data.',\n\n          showLogLinks: {\n            label: 'Show \"View logs\" links',\n            tooltip: 'Show \"View logs\" links on trace_id/traceid fields.',\n          },\n        },\n        contextColumns: {\n          title: 'Context columns',\n          description:\n            'These columns are used to narrow down a single log row to its original service/container/pod source. At least one is required for the log context feature to work.',\n\n          selectContextColumns: {\n            label: 'Auto-Select Columns',\n            tooltip: 'When enabled, will always include context columns in log queries',\n          },\n          columns: {\n            label: 'Context Columns',\n            tooltip: \"Comma separated list of column names to use for identifying a log's source\",\n            placeholder: 'Column name (enter key to add)',\n          },\n        },\n      },\n    },\n    EditorTypeSwitcher: {\n      label: 'Editor Type',\n      tooltip: 'Switches between the raw SQL Editor and the Query Builder.',\n      switcher: {\n        title: 'Are you sure?',\n        body: 'Queries that are too complex for the Query Builder will be altered.',\n        confirmText: 'Continue',\n        dismissText: 'Cancel',\n      },\n      cannotConvert: {\n        title: 'Cannot convert',\n        message: 'Do you want to delete your current query and use the query builder?',\n        confirmText: 'Yes',\n      },\n    },\n    expandBuilderButton: {\n      label: 'Show full query',\n      tooltip: 'Shows the full query builder',\n    },\n    QueryTypeSwitcher: {\n      label: 'Query Type',\n      tooltip: 'Sets the layout for the query builder',\n      sqlTooltip: 'Sets the panel type for explore view',\n    },\n    DatabaseSelect: {\n      label: 'Database',\n      tooltip: 'ClickHouse database to query from',\n      empty: '<select database>',\n    },\n    TableSelect: {\n      label: 'Table',\n      tooltip: 'ClickHouse table to query from',\n      empty: '<select table>',\n    },\n    ColumnsEditor: {\n      label: 'Columns',\n      tooltip: 'A list of columns to include in the query',\n    },\n    OtelVersionSelect: {\n      label: 'Use OTel',\n      tooltip: 'Enables Open Telemetry schema versioning',\n    },\n    LimitEditor: {\n      label: 'Limit',\n      tooltip: 'Limits the number of rows returned by the query',\n    },\n    SqlPreview: {\n      label: 'SQL Preview',\n      tooltip: 'Preview of the generated SQL. You can safely switch to SQL Editor to customize the generated query',\n    },\n    AggregatesEditor: {\n      label: 'Aggregates',\n      tooltip: 'Aggregate functions to use',\n      aliasLabel: 'as',\n      aliasTooltip: 'alias for this aggregate function',\n      addLabel: 'Aggregate',\n    },\n    OrderByEditor: {\n      label: 'Order By',\n      tooltip: 'Order by column',\n      addLabel: 'Order By',\n    },\n    FilterEditor: {\n      label: 'Filters',\n      tooltip: `List of filters`,\n      addLabel: 'Filter',\n      mapKeyPlaceholder: 'map key',\n    },\n    GroupByEditor: {\n      label: 'Group By',\n      tooltip: 'Group the results by specific column',\n    },\n    LogsQueryBuilder: {\n      logTimeColumn: {\n        label: 'Time',\n        tooltip: 'Column that contains the log timestamp',\n      },\n      logLevelColumn: {\n        label: 'Log Level',\n        tooltip: 'Column that contains the log level',\n      },\n      logMessageColumn: {\n        label: 'Message',\n        tooltip: 'Column that contains the log message',\n      },\n      logLabelsColumn: {\n        label: 'Labels',\n        tooltip: 'A column with a key/value structure for log labels',\n      },\n      liveView: {\n        label: 'Live View',\n        tooltip: 'Enable to update logs in real time',\n      },\n      logMessageFilter: {\n        label: 'Message Filter',\n        tooltip: 'Applies a LIKE filter to the log message body',\n        clearButton: 'Clear',\n      },\n      logLevelFilter: {\n        label: 'Level Filter',\n        tooltip: 'Applies a filter to the log level',\n      },\n    },\n    TimeSeriesQueryBuilder: {\n      simpleQueryModeLabel: 'Simple',\n      aggregateQueryModeLabel: 'Aggregate',\n      builderModeLabel: 'Builder Mode',\n      builderModeTooltip: 'Switches the query builder between the simple and aggregate modes',\n      timeColumn: {\n        label: 'Time',\n        tooltip: 'Column to use for the time series',\n      },\n    },\n    TableQueryBuilder: {\n      simpleQueryModeLabel: 'Simple',\n      aggregateQueryModeLabel: 'Aggregate',\n      builderModeLabel: 'Builder Mode',\n      builderModeTooltip: 'Switches the query builder between the simple and aggregate modes',\n    },\n    TraceQueryBuilder: {\n      traceIdModeLabel: 'Trace ID',\n      traceSearchModeLabel: 'Trace Search',\n      traceModeLabel: 'Trace Mode',\n      traceModeTooltip: 'Switches between trace ID and trace search mode',\n      columnsSection: 'Columns',\n      filtersSection: 'Filters',\n\n      columns: {\n        traceId: {\n          label: 'Trace ID Column',\n          tooltip: 'Column that contains the trace ID',\n        },\n        spanId: {\n          label: 'Span ID Column',\n          tooltip: 'Column that contains the span ID',\n        },\n        parentSpanId: {\n          label: 'Parent Span ID Column',\n          tooltip: 'Column that contains the parent span ID',\n        },\n        serviceName: {\n          label: 'Service Name Column',\n          tooltip: 'Column that contains the service name',\n        },\n        operationName: {\n          label: 'Operation Name Column',\n          tooltip: 'Column that contains the operation name',\n        },\n        startTime: {\n          label: 'Start Time Column',\n          tooltip: 'Column that contains the start time',\n        },\n        durationTime: {\n          label: 'Duration Time Column',\n          tooltip: 'Column that contains the duration time',\n        },\n        durationUnit: {\n          label: 'Duration Unit',\n          tooltip: 'The unit of time used for the duration time',\n        },\n        tags: {\n          label: 'Tags Column',\n          tooltip: 'Column that contains the trace tags',\n        },\n        serviceTags: {\n          label: 'Service Tags Column',\n          tooltip: 'Column that contains the service tags',\n        },\n        flattenNested: {\n          label: 'Use Flatten Nested',\n          tooltip: 'Enable if your traces table was created with flatten_nested=1',\n        },\n        eventsPrefix: {\n          label: 'Events Prefix',\n          tooltip: 'Prefix for the events column',\n        },\n        linksPrefix: {\n          label: 'Links Prefix',\n          tooltip: 'Prefix for the trace references column',\n        },\n        kind: {\n          label: 'Kind Column',\n          tooltip: 'Column that contains the trace kind',\n        },\n        statusCode: {\n          label: 'Status Code Column',\n          tooltip: 'Column that contains the trace status code',\n        },\n        statusMessage: {\n          label: 'Status Message Column',\n          tooltip: 'Column that contains the trace status message',\n        },\n        instrumentationLibraryName: {\n          label: 'Library Name Column',\n          tooltip: 'Column that contains the instrumentation library name (Optional)',\n        },\n        instrumentationLibraryVersion: {\n          label: 'Library Version Column',\n          tooltip: 'Column that contains the instrumentation library version (Optional)',\n        },\n        state: {\n          label: 'State Column',\n          tooltip: 'Column that contains the trace state',\n        },\n        traceIdFilter: {\n          label: 'Trace ID',\n          tooltip: 'filter by a specific trace ID',\n        },\n      },\n    },\n  },\n  types: {\n    EditorType: {\n      sql: 'SQL Editor',\n      builder: 'Query Builder',\n    },\n    QueryType: {\n      table: 'Table',\n      logs: 'Logs',\n      timeseries: 'Time Series',\n      traces: 'Traces',\n    },\n    ColumnHint: {\n      [ColumnHint.Time]: 'Time',\n\n      [ColumnHint.LogLevel]: 'Level',\n      [ColumnHint.LogMessage]: 'Message',\n\n      [ColumnHint.TraceId]: 'Trace ID',\n      [ColumnHint.TraceSpanId]: 'Span ID',\n      [ColumnHint.TraceParentSpanId]: 'Parent Span ID',\n      [ColumnHint.TraceServiceName]: 'Service Name',\n      [ColumnHint.TraceOperationName]: 'Operation Name',\n      [ColumnHint.TraceDurationTime]: 'Duration Time',\n      [ColumnHint.TraceTags]: 'Tags',\n      [ColumnHint.TraceServiceTags]: 'Service Tags',\n      [ColumnHint.TraceStatusCode]: 'Status Code',\n      [ColumnHint.TraceKind]: 'Kind',\n      [ColumnHint.TraceStatusMessage]: 'Status Message',\n      [ColumnHint.TraceInstrumentationLibraryName]: 'Instrumentation Library Name',\n      [ColumnHint.TraceInstrumentationLibraryVersion]: 'Instrumentation Library Version',\n      [ColumnHint.TraceState]: 'State',\n    },\n  },\n};\n"
  },
  {
    "path": "src/views/config-v2/tracking.ts",
    "content": "import { reportInteraction } from '@grafana/runtime';\nimport { TimeUnit } from 'types/queryBuilder';\n\n// Feedback form\nexport const trackClickhouseConfigV2FeedbackButtonClicked = () => {\n  reportInteraction('clickhouse_config_v2_feedback_button_clicked');\n};\n\n// Server and encryption section\nexport const trackClickhouseConfigV2HostInput = () => {\n  reportInteraction('clickhouse_config_v2_host_input');\n};\n\nexport const trackClickhouseConfigV2PortInput = (props: { port: string }) => {\n  reportInteraction('clickhouse_config_v2_port_input', props);\n};\n\nexport const trackClickhouseConfigV2NativeHttpToggleClicked = (props: { nativeHttpToggle: string }) => {\n  reportInteraction('clickhouse_config_v2_native_http_toggle_clicked', props);\n};\n\nexport const trackClickhouseConfigV2SecureConnectionChecked = (props: { secureConnection: boolean }) => {\n  reportInteraction('clickhouse_config_v2_secure_connection_checked', props);\n};\n\n// Database credentials section\nexport const trackClickhouseConfigV2DatabaseCredentialsUserInput = () => {\n  reportInteraction('clickhouse_config_v2_database_credentials_user_input');\n};\n\nexport const trackClickhouseConfigV2DatabaseCredentialsPasswordInput = () => {\n  reportInteraction('clickhouse_config_v2_database_credentials_password_input');\n};\n\n// TLS/SSL Settings section\nexport const trackClickhouseConfigV2SkipTLSVerifyToggleClicked = (props: { skipTlsVerifyToggle: boolean }) => {\n  reportInteraction('clickhouse_config_v2_skip_tls_verify_toggle_clicked', props);\n};\n\nexport const trackClickhouseConfigV2TLSClientAuthToggleClicked = (props: { clientAuthToggle: boolean }) => {\n  reportInteraction('clickhouse_config_v2_tls_client_auth_toggle_clicked', props);\n};\n\nexport const trackClickhouseConfigV2WithCACertToggleClicked = (props: { caCertToggle: boolean }) => {\n  reportInteraction('clickhouse_config_v2_with_ca_cert_toggle_clicked', props);\n};\n\n// Additional settings\n// Default DB and Table section\nexport const trackClickhouseConfigV2DefaultDbInput = () => {\n  reportInteraction('clickhouse_config_v2_default_db_input');\n};\n\nexport const trackClickhouseConfigV2DefaultTableInput = () => {\n  reportInteraction('clickhouse_config_v2_default_table_input');\n};\n\n// Query settings section\nexport const trackClickhouseConfigV2QuerySettings = (props: {\n  queryTimeout?: number;\n  dialTimeout?: number;\n  maxIdleConns?: number;\n  maxOpenConns?: number;\n  connMaxLifetime?: number;\n  validateSql?: boolean;\n}) => {\n  reportInteraction('clickhouse_config_v2_query_settings', props);\n};\n\n// Logs config section\nexport const trackClickhouseConfigV2LogsConfig = (props: {\n  defaultDatabase?: string;\n  defaultTable?: string;\n  otelEnabled?: boolean;\n  version?: string;\n  timeColumn?: string;\n  levelColumn?: string;\n  messageColumn?: string;\n  selectContextColumns?: boolean;\n  contextColumns?: string[];\n}) => {\n  reportInteraction('clickhouse_config_v2_logs_config', props);\n};\n\n// Traces config section\nexport const trackClickhouseConfigV2TracesConfig = (props: {\n  defaultDatabase?: string;\n  defaultTable?: string;\n  otelEnabled?: boolean;\n  version?: string;\n  traceIdColumn?: string;\n  spanIdColumn?: string;\n  operationNameColumn?: string;\n  parentSpanIdColumn?: string;\n  serviceNameColumn?: string;\n  durationColumn?: string;\n  durationUnit?: TimeUnit;\n  startTimeColumn?: string;\n  tagsColumn?: string;\n  serviceTagsColumn?: string;\n  kindColumn?: string;\n  statusCodeColumn?: string;\n  statusMessageColumn?: string;\n  stateColumn?: string;\n  instrumentationLibraryNameColumn?: string;\n  instrumentationLibraryVersionColumn?: string;\n  flattenNested?: boolean;\n  traceEventsColumnPrefix?: string;\n  traceLinksColumnPrefix?: string;\n}) => {\n  reportInteraction('clickhouse_config_v2_traces_config', props);\n};\n\n// Column Alias Tables section\nexport const trackClickhouseConfigV2ColumnAliasTableAdded = () => {\n  reportInteraction('clickhouse_config_v2_column_alias_table_added');\n};\n\n// Row limit section\nexport const trackClickhouseConfigV2EnableRowLimitToggle = (props: { rowLimitEnabled: boolean }) => {\n  reportInteraction('clickhouse_config_v2_enable_row_limit_toggle', props);\n};\n\n// Custom Settings section\nexport const trackClickhouseConfigV2CustomSettingClicked = () => {\n  reportInteraction('clickhouse_config_v2_custom_setting_clicked');\n};\n"
  },
  {
    "path": "src/views/trackingV1.ts",
    "content": "import { reportInteraction } from '@grafana/runtime';\nimport { TimeUnit } from 'types/queryBuilder';\n\n// Server section\nexport const trackClickhouseConfigV1HostInput = () => {\n  reportInteraction('clickhouse_config_v1_host_input');\n};\n\nexport const trackClickhouseConfigV1PortInput = (props: { port: string }) => {\n  reportInteraction('clickhouse_config_v1_port_input', props);\n};\n\nexport const trackClickhouseConfigV1NativeHttpToggleClicked = (props: { nativeHttpToggle: string }) => {\n  reportInteraction('clickhouse_config_v1_native_http_toggle_clicked', props);\n};\n\nexport const trackClickhouseConfigV1SecureConnectionToggleClicked = (props: { secureConnection: boolean }) => {\n  reportInteraction('clickhouse_config_v1_secure_connection_toggle_clicked', props);\n};\n\n// TLS/SSL Settings section\nexport const trackClickhouseConfigV1SkipTLSVerifyToggleClicked = (props: { skipTlsVerifyToggle: boolean }) => {\n  reportInteraction('clickhouse_config_v1_skip_tls_verify_toggle_clicked', props);\n};\n\nexport const trackClickhouseConfigV1TLSClientAuthToggleClicked = (props: { clientAuthToggle: boolean }) => {\n  reportInteraction('clickhouse_config_v1_tls_client_auth_toggle_clicked', props);\n};\n\nexport const trackClickhouseConfigV1WithCACertToggleClicked = (props: { caCertToggle: boolean }) => {\n  reportInteraction('clickhouse_config_v1_with_ca_cert_toggle_clicked', props);\n};\n\n// Default DB and Table section\nexport const trackClickhouseConfigV1DefaultDbInput = () => {\n  reportInteraction('clickhouse_config_v1_default_db_input');\n};\n\nexport const trackClickhouseConfigV1DefaultTableInput = () => {\n  reportInteraction('clickhouse_config_v1_default_table_input');\n};\n\n// Query settings section\nexport const trackClickhouseConfigV1QuerySettings = (props: {\n  queryTimeout?: number;\n  dialTimeout?: number;\n  maxIdleConns?: number;\n  maxOpenConns?: number;\n  connMaxLifetime?: number;\n  validateSql?: boolean;\n}) => {\n  reportInteraction('clickhouse_config_v1_query_settings', props);\n};\n\n// Logs config section\nexport const trackClickhouseConfigV1LogsConfig = (props: {\n  defaultDatabase?: string;\n  defaultTable?: string;\n  otelEnabled?: boolean;\n  version?: string;\n  filterTimeColumn?: string;\n  timeColumn?: string;\n  levelColumn?: string;\n  messageColumn?: string;\n  selectContextColumns?: boolean;\n  contextColumns?: string[];\n}) => {\n  reportInteraction('clickhouse_config_v1_logs_config', props);\n};\n\n// Traces config section\nexport const trackClickhouseConfigV1TracesConfig = (props: {\n  defaultDatabase?: string;\n  defaultTable?: string;\n  otelEnabled?: boolean;\n  version?: string;\n  traceIdColumn?: string;\n  spanIdColumn?: string;\n  operationNameColumn?: string;\n  parentSpanIdColumn?: string;\n  serviceNameColumn?: string;\n  durationColumn?: string;\n  durationUnit?: TimeUnit;\n  startTimeColumn?: string;\n  tagsColumn?: string;\n  serviceTagsColumn?: string;\n  kindColumn?: string;\n  statusCodeColumn?: string;\n  statusMessageColumn?: string;\n  stateColumn?: string;\n  instrumentationLibraryNameColumn?: string;\n  instrumentationLibraryVersionColumn?: string;\n  flattenNested?: boolean;\n  traceEventsColumnPrefix?: string;\n  traceLinksColumnPrefix?: string;\n  traceTimestampTableSuffix?: string;\n}) => {\n  reportInteraction('clickhouse_config_v1_traces_config', props);\n};\n\n// Column Alias Tables section\nexport const trackClickhouseConfigV1ColumnAliasTableAdded = () => {\n  reportInteraction('clickhouse_config_v1_column_alias_table_added');\n};\n\n// Row limit section\nexport const trackClickhouseConfigV1EnableRowLimitToggle = (props: { rowLimitEnabled: boolean }) => {\n  reportInteraction('clickhouse_config_v1_enable_row_limit_toggle', props);\n};\n\n// Custom Settings section\nexport const trackClickhouseConfigV1CustomSettingAdded = () => {\n  reportInteraction('clickhouse_config_v1_custom_setting_added');\n};\n"
  },
  {
    "path": "tests/benchmarks/README.md",
    "content": "# Benchmarks\n\nScripts here are **not part of CI**. They run against a live ClickHouse and\nexist as reproducible evidence for performance claims made in PRs, and as\nregression guards for anyone touching the affected code paths.\n\n## trace-id-query.sh\n\nProves that the two-step trace ID lookup in\n[`src/data/sqlGenerator.ts`](../../src/data/sqlGenerator.ts) (`generateTraceIdQuery`)\nactually avoids a full table scan when a companion `_trace_id_ts` index table\nexists.\n\n### Why this exists\n\nTrace ID lookup is the hot path for \"View trace\" deep-links from logs and\ndashboards. On a traces table ordered by `(ServiceName, Timestamp)` — the\nrealistic OTel layout — a pure `WHERE TraceId = '…'` filter prunes nothing\nand forces ClickHouse to read every granule. The companion index table\n(keyed by `TraceId`) lets us resolve a tight `(Start, End)` time window\nfirst, then use that window to let the primary key prune granules in the\nmain query.\n\nUnit tests in [`sqlGenerator.test.ts`](../../src/data/sqlGenerator.test.ts)\nassert the generated SQL is right; this benchmark asserts the SQL is\nactually faster on real data.\n\n### What it measures\n\n`read_rows` from `system.query_log`, not wall-clock time. Two reasons:\n\n- CI wall-clock is noisy; row counts are deterministic.\n- `read_rows` is the direct cause of the speedup — fewer granules scanned\n  because the time range narrows primary-key pruning.\n\nWall-clock (`query_duration_ms`) and `read_bytes` are logged for reference.\n\n### Running it\n\n```bash\ndocker compose up -d clickhouse-server\n./tests/benchmarks/trace-id-query.sh\n```\n\nThe seed inserts ~5M rows and takes around 30s on a laptop. Environment\nknobs:\n\n| Var | Default | Purpose |\n| --- | --- | --- |\n| `CH_HOST` | `localhost` | ClickHouse host |\n| `CH_PORT` | `9000` | Native protocol port |\n| `CH_DB` | `bench` | Benchmark database name |\n| `ROW_COUNT` | `5000000` | Noise rows to insert |\n| `MIN_READ_ROWS_RATIO` | `40` | Pass/fail floor for `slow_rows / fast_rows` |\n\nObserved output (5M seed, local Docker):\n\n```text\n                          read_rows      read_bytes   duration_ms\n  unoptimized (slow):       5000020      125001640            50\n  optimized   (fast):         90112        1001918            43\n\n  read_rows ratio: 55x fewer rows in the optimized path\n[bench] OK: optimization reads 55x fewer rows (floor 40x).\n```\n\nThe ratio grows with `ROW_COUNT` (84× at 20M rows) but sub-linearly — the\nfast path's cost is dominated by the two index-table subqueries, which\nthemselves scale with the index size. The floor is set to catch a\nregression where the optimization stops firing entirely (ratio → 1x), not\nto assert a specific multiplier.\n\nExits non-zero if the ratio drops below the floor — useful when bisecting\na suspected regression in `generateTraceIdQuery`.\n\n### When to re-run\n\n- Before merging any change to `generateTraceIdQuery`.\n- When changing the shape of the `_trace_id_ts` companion table or its\n  default suffix.\n- When someone claims the optimization \"isn't helping in production\" — rerun\n  against a seed that matches their schema to reproduce.\n"
  },
  {
    "path": "tests/benchmarks/trace-id-query.sh",
    "content": "#!/usr/bin/env bash\n#\n# Benchmark the trace ID query optimization in generateTraceIdQuery().\n#\n# Seeds a synthetic traces table (~5M rows) plus the companion _trace_id_ts\n# index table, runs the unoptimized and optimized forms against it, and\n# compares read_rows / read_bytes / query_duration_ms from system.query_log.\n#\n# The deterministic signal is read_rows: the optimization exists to make\n# ClickHouse skip granules by narrowing the time range to what the primary\n# key can prune. Wall-clock is logged for reference only.\n#\n# Prereqs:\n#   docker compose up -d clickhouse-server\n#\n# Usage:\n#   ./tests/benchmarks/trace-id-query.sh\n#\n# Exits non-zero if the read_rows ratio (slow/fast) drops below\n# MIN_READ_ROWS_RATIO. The default floor (40x) is set below the observed\n# ratio at 5M rows (~55x) so transient ClickHouse variance doesn't flake\n# the check. The optimization is real — it's just capped by how many rows\n# the two index-table subqueries themselves read, which grows with index\n# size. Bump ROW_COUNT to see a larger ratio.\n\nset -euo pipefail\n\nCH_HOST=${CH_HOST:-localhost}\nCH_PORT=${CH_PORT:-9000}\nCH_DB=${CH_DB:-bench}\nROW_COUNT=${ROW_COUNT:-5000000}\nTARGET_TRACE_ID=${TARGET_TRACE_ID:-00000000000000000000000000000042}\nMIN_READ_ROWS_RATIO=${MIN_READ_ROWS_RATIO:-40}\n\n# Prefer `clickhouse-client` on the host; fall back to the dockerised one.\nif command -v clickhouse-client >/dev/null 2>&1; then\n  CH_CMD=\"clickhouse-client --host ${CH_HOST} --port ${CH_PORT}\"\nelse\n  CH_CMD=\"docker exec -i clickhouse-server clickhouse-client\"\nfi\n\nrun_sql() {\n  # shellcheck disable=SC2086\n  ${CH_CMD} --multiquery --query \"$1\"\n}\n\nrun_sql_format() {\n  # shellcheck disable=SC2086\n  ${CH_CMD} --query \"$1\" --format \"$2\"\n}\n\nlog() { printf '[bench] %s\\n' \"$*\" >&2; }\n\nlog \"Seeding ${ROW_COUNT} rows into ${CH_DB}.bench_traces (this can take ~30s)\"\n\nrun_sql \"\nCREATE DATABASE IF NOT EXISTS ${CH_DB};\n\nDROP TABLE IF EXISTS ${CH_DB}.bench_traces;\nDROP TABLE IF EXISTS ${CH_DB}.bench_traces_trace_id_ts;\n\nCREATE TABLE ${CH_DB}.bench_traces\n(\n    TraceId     String,\n    SpanId      String,\n    ParentSpanId String,\n    ServiceName LowCardinality(String),\n    SpanName    LowCardinality(String),\n    Timestamp   DateTime64(9) CODEC(Delta, ZSTD),\n    Duration    Int64\n)\nENGINE = MergeTree\nPARTITION BY toDate(Timestamp)\nORDER BY (ServiceName, Timestamp);\n\nCREATE TABLE ${CH_DB}.bench_traces_trace_id_ts\n(\n    TraceId String,\n    Start   DateTime64(9),\n    End     DateTime64(9)\n)\nENGINE = MergeTree\nORDER BY (TraceId, Start);\n\n-- Noise rows: uniformly distributed across 24h, several services.\nINSERT INTO ${CH_DB}.bench_traces\nSELECT\n    lower(hex(reinterpretAsFixedString(rand64()))) AS TraceId,\n    lower(hex(reinterpretAsFixedString(rand64()))) AS SpanId,\n    lower(hex(reinterpretAsFixedString(rand64()))) AS ParentSpanId,\n    ['api', 'worker', 'cache', 'db', 'auth'][(number % 5) + 1] AS ServiceName,\n    ['GET /users', 'POST /pay', 'SELECT', 'auth.verify', 'cache.get'][(number % 5) + 1] AS SpanName,\n    toDateTime64('2026-03-01 00:00:00', 9) + INTERVAL (number % 86400) SECOND AS Timestamp,\n    (rand() % 1000000)::Int64 AS Duration\nFROM numbers(${ROW_COUNT});\n\n-- Target trace: 20 spans clustered in a 1-second window 6 hours into the range.\nINSERT INTO ${CH_DB}.bench_traces\nSELECT\n    '${TARGET_TRACE_ID}' AS TraceId,\n    lower(hex(reinterpretAsFixedString(rand64()))) AS SpanId,\n    lower(hex(reinterpretAsFixedString(rand64()))) AS ParentSpanId,\n    'api' AS ServiceName,\n    'GET /target' AS SpanName,\n    toDateTime64('2026-03-01 06:00:00', 9) + INTERVAL (number * 50) MILLISECOND AS Timestamp,\n    (rand() % 10000)::Int64 AS Duration\nFROM numbers(20);\n\n-- Populate the index: one row per TraceId with its span time range.\nINSERT INTO ${CH_DB}.bench_traces_trace_id_ts\nSELECT TraceId, min(Timestamp) AS Start, max(Timestamp) AS End\nFROM ${CH_DB}.bench_traces\nGROUP BY TraceId;\n\nOPTIMIZE TABLE ${CH_DB}.bench_traces FINAL;\nOPTIMIZE TABLE ${CH_DB}.bench_traces_trace_id_ts FINAL;\n\"\n\nlog \"Seed complete. Running queries.\"\n\n# Disable result cache and give each query a unique tag we can look up in\n# system.query_log.\nSLOW_TAG=\"bench_trace_slow_$(date +%s)_$RANDOM\"\nFAST_TAG=\"bench_trace_fast_$(date +%s)_$RANDOM\"\n\n# Unoptimized: the pre-fix path. Filters the whole table by TraceId.\nSLOW_SQL=\"SELECT /* ${SLOW_TAG} */ * FROM ${CH_DB}.bench_traces WHERE TraceId = '${TARGET_TRACE_ID}' FORMAT Null\"\n\n# Optimized: the exact shape generateTraceIdQuery() emits today when\n# applyTraceIdOptimization fires.\nFAST_SQL=\"WITH /* ${FAST_TAG} */\n  '${TARGET_TRACE_ID}' AS trace_id,\n  (SELECT min(Start) FROM ${CH_DB}.bench_traces_trace_id_ts WHERE TraceId = trace_id) AS trace_start,\n  (SELECT max(End) + 1 FROM ${CH_DB}.bench_traces_trace_id_ts WHERE TraceId = trace_id) AS trace_end\nSELECT * FROM ${CH_DB}.bench_traces\nWHERE TraceId = trace_id\n  AND Timestamp >= trace_start\n  AND Timestamp <= trace_end\nFORMAT Null\"\n\nrun_sql \"SET use_query_cache = 0; ${SLOW_SQL};\"\nrun_sql \"SET use_query_cache = 0; ${FAST_SQL};\"\n\nlog \"Flushing query_log\"\nrun_sql \"SYSTEM FLUSH LOGS;\"\n\nmetrics_for() {\n  local tag=\"$1\"\n  run_sql_format \"\n    SELECT read_rows, read_bytes, query_duration_ms\n    FROM system.query_log\n    WHERE type = 'QueryFinish'\n      AND query LIKE '%${tag}%'\n      AND query NOT LIKE '%system.query_log%'\n    ORDER BY event_time DESC\n    LIMIT 1\n  \" TSV\n}\n\nread -r SLOW_ROWS SLOW_BYTES SLOW_MS < <(metrics_for \"${SLOW_TAG}\")\nread -r FAST_ROWS FAST_BYTES FAST_MS < <(metrics_for \"${FAST_TAG}\")\n\nif [[ -z \"${SLOW_ROWS:-}\" || -z \"${FAST_ROWS:-}\" ]]; then\n  log \"ERROR: could not read query_log metrics. Slow='${SLOW_ROWS:-}' Fast='${FAST_ROWS:-}'\"\n  exit 2\nfi\n\nprintf '\\n'\nprintf '                          read_rows      read_bytes   duration_ms\\n'\nprintf '  unoptimized (slow):  %12s   %12s   %11s\\n' \"${SLOW_ROWS}\" \"${SLOW_BYTES}\" \"${SLOW_MS}\"\nprintf '  optimized   (fast):  %12s   %12s   %11s\\n' \"${FAST_ROWS}\" \"${FAST_BYTES}\" \"${FAST_MS}\"\n\nif [[ \"${FAST_ROWS}\" -eq 0 ]]; then\n  log \"ERROR: optimized query read 0 rows — index table or target trace is likely missing.\"\n  exit 3\nfi\n\nRATIO=$(( SLOW_ROWS / FAST_ROWS ))\nprintf '\\n  read_rows ratio: %sx fewer rows in the optimized path\\n' \"${RATIO}\"\n\nif (( RATIO < MIN_READ_ROWS_RATIO )); then\n  log \"FAIL: read_rows ratio ${RATIO}x is below the floor of ${MIN_READ_ROWS_RATIO}x.\"\n  log \"      (Either the optimization regressed or the seed is too small — tune ROW_COUNT.)\"\n  exit 1\nfi\n\nlog \"OK: optimization reads ${RATIO}x fewer rows (floor ${MIN_READ_ROWS_RATIO}x).\"\n"
  },
  {
    "path": "tests/e2e/adhocRegexFilter.spec.ts",
    "content": "import { expect, test, ExplorePage } from '@grafana/plugin-e2e';\nimport { Page } from '@playwright/test';\n\nconst PLUGIN_TYPE = 'grafana-clickhouse-datasource';\n\nconst isCloudRun = !!process.env.GRAFANA_URL;\n\nconst CLOUD_DEFAULT_UID = 'clickhouse-native-ds-m';\nconst LOCAL_DEFAULT_UID = 'clickhouse-e2e';\nconst DATASOURCE_UID = process.env.DS_E2E_UID || (isCloudRun ? CLOUD_DEFAULT_UID : LOCAL_DEFAULT_UID);\n\n// Time range that fully covers the seed fixture data in tests/e2e/fixtures/seed.sql\nconst FIXTURE_FROM_ISO = '2024-03-15T09:45:00.000Z';\nconst FIXTURE_TO_ISO = '2024-03-15T10:15:00.000Z';\n\nfunction exploreUrl(from = FIXTURE_FROM_ISO, to = FIXTURE_TO_ISO): string {\n  const query = {\n    refId: 'A',\n    datasource: { type: PLUGIN_TYPE, uid: DATASOURCE_UID },\n    editorType: 'sql',\n    pluginVersion: '',\n    rawSql: '',\n  };\n  const panes = JSON.stringify({\n    explore: {\n      datasource: DATASOURCE_UID,\n      queries: [query],\n      range: { from, to },\n    },\n  });\n  return `/explore?orgId=1&schemaVersion=1&panes=${encodeURIComponent(panes)}`;\n}\n\nasync function enterSql(page: Page, sql: string) {\n  const editor = page.getByRole('code');\n  await editor.click();\n  await page.keyboard.press('ControlOrMeta+a');\n  await page.keyboard.type(sql);\n  // Dismiss any Monaco autocomplete popup that may have captured a keyword\n  // mid-stream (e.g. `NOT ` can trigger a suggestion list that, if left open,\n  // swallows the Enter/click on the Run Query button or rewrites the last\n  // token when focus shifts).\n  await page.keyboard.press('Escape');\n}\n\nasync function waitForQueryDataResponseWithBody(explorePage: ExplorePage) {\n  let body: Record<string, unknown> | null = null;\n  const responsePromise = explorePage.waitForQueryDataResponse(async (r) => {\n    if (!r.ok()) {\n      return false;\n    }\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const b: any = await r.json().catch(() => null);\n    if (!Array.isArray(b?.results?.A?.frames)) {\n      return false;\n    }\n    body = b as Record<string, unknown>;\n    return true;\n  });\n  return { responsePromise, getBody: () => body };\n}\n\n// ---------------------------------------------------------------------------\n// Ad-hoc regex-operator filter regression guards (#1443)\n//\n// Unit tests in src/data/adHocFilter.test.ts cover the JS-level mapping of\n// Grafana's `=~` / `!~` operators to ClickHouse `REGEXP` / `NOT REGEXP`\n// and the exact `additional_table_filters={...}` shape AdHocFilter.apply()\n// produces. Those tests can't confirm that ClickHouse actually accepts\n// `REGEXP` and `NOT REGEXP` as a filter operator — only E2E can.\n//\n// Each test below runs a plain `WHERE ... REGEXP ...` query against the\n// fixture to verify ClickHouse accepts the operator. If a future refactor\n// silently reintroduces ILIKE (which is a LIKE pattern, not a regex), the\n// third test — which uses a regex-only pattern that matches nothing as a\n// LIKE — will fail.\n//\n// We deliberately avoid typing the full `SETTINGS additional_table_filters={...}`\n// shape through the Monaco editor because Monaco auto-closes `{`, which\n// mangles that syntax on keystroke entry. The unit tests cover the exact\n// shape; the E2E tests cover the operator semantics.\n// ---------------------------------------------------------------------------\n\ntest.describe('Ad-hoc regex operator filters', () => {\n  test.beforeEach(() => {\n    test.skip(\n      isCloudRun,\n      'Fixture-data tests depend on e2e_test.events seeded by tests/e2e/fixtures/seed.sql via the local e2e-data-loader Docker service, which is not available on Cloud.'\n    );\n  });\n\n  test.describe.configure({ mode: 'serial' });\n\n  test('REGEXP returns matching rows', async ({ page, explorePage }) => {\n    await page.goto(exploreUrl());\n    // The fixture has exactly two messages that start with \"Request\":\n    // \"Request received\" and \"Request processed\".\n    await enterSql(\n      page,\n      \"SELECT timestamp, message FROM e2e_test.events WHERE message REGEXP '^Request' ORDER BY timestamp\"\n    );\n\n    const { responsePromise, getBody } = await waitForQueryDataResponseWithBody(explorePage);\n    await page.locator('.query-editor-row').getByRole('button', { name: 'Run Query' }).click();\n    await responsePromise;\n\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const frames = (getBody() as any)?.results?.A?.frames;\n    expect(frames?.length).toBeGreaterThan(0);\n    const messages = frames[0]?.data?.values?.[1];\n    expect(messages).toEqual(['Request received', 'Request processed']);\n  });\n\n  test('NOT REGEXP returns the complement', async ({ page, explorePage }) => {\n    await page.goto(exploreUrl());\n    // 10 rows in the fixture, 2 start with \"Request\", so 8 remain.\n    // Parenthesize the predicate so the `NOT` keyword is not immediately\n    // followed by `REGEXP` — Monaco's SQL autocomplete tends to offer\n    // keyword suggestions after `NOT ` and can swallow/mangle the next\n    // token when typed via page.keyboard.type().\n    await enterSql(\n      page,\n      \"SELECT timestamp, message FROM e2e_test.events WHERE NOT (message REGEXP '^Request') ORDER BY timestamp\"\n    );\n\n    const { responsePromise, getBody } = await waitForQueryDataResponseWithBody(explorePage);\n    await page.locator('.query-editor-row').getByRole('button', { name: 'Run Query' }).click();\n    await responsePromise;\n\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const frames = (getBody() as any)?.results?.A?.frames;\n    expect(frames?.length).toBeGreaterThan(0);\n    const messages = frames[0]?.data?.values?.[1];\n    expect(messages?.length).toBe(8);\n    // None of the remaining rows should start with \"Request\".\n    expect(messages?.every((m: string) => !m.startsWith('Request'))).toBe(true);\n  });\n\n  test('REGEXP treats the pattern as a regex, not a LIKE pattern', async ({ page, explorePage }) => {\n    await page.goto(exploreUrl());\n    // This test exists specifically to guard against a regression back to\n    // ILIKE. The pattern `^(info|warn)$` is a regex that matches \"info\" or\n    // \"warn\" exactly. As an ILIKE pattern it would match nothing (the\n    // characters `^`, `(`, `|`, `)`, `$` are not wildcards in LIKE, and\n    // there is no level literally equal to `^(info|warn)$`). If this test\n    // ever sees zero rows, ILIKE is back.\n    await enterSql(\n      page,\n      \"SELECT level FROM e2e_test.events WHERE level REGEXP '^(info|warn)$' ORDER BY timestamp\"\n    );\n\n    const { responsePromise, getBody } = await waitForQueryDataResponseWithBody(explorePage);\n    await page.locator('.query-editor-row').getByRole('button', { name: 'Run Query' }).click();\n    await responsePromise;\n\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const frames = (getBody() as any)?.results?.A?.frames;\n    expect(frames?.length).toBeGreaterThan(0);\n    const levels: string[] = frames[0]?.data?.values?.[0] ?? [];\n    // Fixture has 5 \"info\" rows and 1 \"warn\" row.\n    expect(levels.length).toBe(6);\n    expect(levels.every((l) => l === 'info' || l === 'warn')).toBe(true);\n  });\n});\n"
  },
  {
    "path": "tests/e2e/columnRoles.spec.ts",
    "content": "import { expect, test } from '@grafana/plugin-e2e';\nimport { Locator, Page } from '@playwright/test';\nimport { QueryType } from '../../src/types/queryBuilder';\nimport { Components as Selectors } from '../../src/selectors';\n\nconst PLUGIN_TYPE = 'grafana-clickhouse-datasource';\n\nconst isCloudRun = !!process.env.GRAFANA_URL;\n\nconst CLOUD_DEFAULT_UID = 'clickhouse-native-ds-m';\nconst LOCAL_DEFAULT_UID = 'clickhouse-e2e';\nconst DATASOURCE_UID = process.env.DS_E2E_UID || (isCloudRun ? CLOUD_DEFAULT_UID : LOCAL_DEFAULT_UID);\n\n// Doc anchor the help link points to; kept in sync with labels.ts.\nconst COLUMN_ROLES_DOCS_PATH = 'https://grafana.com/docs/plugins/grafana-clickhouse-datasource/latest/query-editor/#column-roles';\n\ninterface ExploreUrlOpts {\n  queryType?: QueryType;\n}\n\n/**\n * Build an Explore URL that preselects a query type. The editor still opens\n * in SQL Editor mode on load (Grafana does not restore editorType from the\n * URL pane state for this plugin); tests that need the Query Builder must\n * call switchToBuilderMode after navigation.\n */\nfunction exploreUrl(opts: ExploreUrlOpts = {}): string {\n  const { queryType = QueryType.Table } = opts;\n\n  const query: Record<string, unknown> = {\n    refId: 'A',\n    datasource: { type: PLUGIN_TYPE, uid: DATASOURCE_UID },\n    editorType: 'sql',\n    pluginVersion: '',\n    rawSql: '',\n    queryType,\n  };\n\n  const panes = JSON.stringify({\n    explore: {\n      datasource: DATASOURCE_UID,\n      queries: [query],\n      range: { from: 'now-1h', to: 'now' },\n    },\n  });\n\n  return `/explore?orgId=1&schemaVersion=1&panes=${encodeURIComponent(panes)}`;\n}\n\n/**\n * Switch from the default SQL Editor mode into Query Builder. Dismisses the\n * \"Cannot convert\" confirmation that appears when the SQL body is empty or\n * not a plain SELECT. Grafana does not restore `queryType` from Explore's pane\n * state, and switching editor types resets the query type to \"Table\"; callers\n * that need Logs / Traces / Time Series must pass `queryType` so we re-select\n * it after the mode switch.\n */\nasync function switchToBuilderMode(page: Page, queryType?: QueryType) {\n  await page.getByRole('radio', { name: 'Query Builder' }).click();\n  const continueButton = page.getByRole('button', { name: 'Continue' });\n  if (await continueButton.isVisible({ timeout: 3000 }).catch(() => false)) {\n    await continueButton.click();\n  }\n  await expect(page.getByRole('radio', { name: 'Query Builder' })).toBeChecked();\n\n  if (queryType && queryType !== QueryType.Table) {\n    const label = queryTypeRadioLabel(queryType);\n    await page.getByRole('radio', { name: label, exact: true }).click();\n    await expect(page.getByRole('radio', { name: label, exact: true })).toBeChecked();\n  }\n}\n\n/**\n * Map a QueryType to the human-readable label used by the Query Type radio\n * group. Kept near switchToBuilderMode so both the selection and assertion use\n * the same string.\n */\nfunction queryTypeRadioLabel(queryType: QueryType): string {\n  switch (queryType) {\n    case QueryType.Logs:\n      return 'Logs';\n    case QueryType.TimeSeries:\n      return 'Time Series';\n    case QueryType.Traces:\n      return 'Traces';\n    default:\n      return 'Table';\n  }\n}\n\n/**\n * Returns the InlineFormLabel element that renders a given column selector\n * label. Grafana renders these as `<label class=\"query-keyword\">` with the\n * label text as a child text node.\n */\nfunction labelLocator(page: Page, labelText: string): Locator {\n  return page.locator('label.query-keyword', { hasText: labelText });\n}\n\n/**\n * Hover the info-icon tooltip inside a label and return its popover text.\n * Grafana's InlineFormLabel renders the tooltip as a Popper child with\n * role=\"tooltip\"; Playwright can target it once the hover has fired.\n */\nasync function readTooltip(page: Page, label: Locator): Promise<string> {\n  // The icon is rendered inside the label; hovering the label surfaces the tooltip\n  // in all current @grafana/ui versions we support.\n  await label.locator('svg').first().hover();\n  const tooltip = page.getByRole('tooltip');\n  await expect(tooltip).toBeVisible();\n  return (await tooltip.textContent()) ?? '';\n}\n\ntest.describe('Column roles guidance', () => {\n  test.describe('Logs query builder', () => {\n    test.beforeEach(async ({ page }) => {\n      await page.goto(exploreUrl({ queryType: QueryType.Logs }));\n      await switchToBuilderMode(page, QueryType.Logs);\n    });\n\n    test('renders the column-roles help note with a docs link', async ({ page }) => {\n      const help = page.getByTestId(Selectors.QueryBuilder.LogsQueryBuilder.columnRolesHelp);\n      await expect(help).toBeVisible();\n      await expect(help).toContainText('column roles', { ignoreCase: true });\n\n      const link = page.getByTestId(Selectors.QueryBuilder.LogsQueryBuilder.columnRolesHelpLink);\n      await expect(link).toBeVisible();\n      await expect(link).toHaveAttribute('href', COLUMN_ROLES_DOCS_PATH);\n      await expect(link).toHaveAttribute('target', '_blank');\n      await expect(link).toHaveAttribute('rel', /noreferrer/);\n    });\n\n    test('Time column tooltip explains the `timestamp` SQL alias', async ({ page }) => {\n      const text = await readTooltip(page, labelLocator(page, 'Time'));\n      expect(text.toLowerCase()).toContain('timestamp');\n      expect(text.toLowerCase()).toContain('aliased to');\n    });\n\n    test('Message column tooltip explains the `body` SQL alias', async ({ page }) => {\n      const text = await readTooltip(page, labelLocator(page, 'Message'));\n      expect(text.toLowerCase()).toContain('body');\n      expect(text.toLowerCase()).toContain('log message');\n    });\n\n    test('Log Level column tooltip explains the `level` SQL alias', async ({ page }) => {\n      const text = await readTooltip(page, labelLocator(page, 'Log Level'));\n      expect(text.toLowerCase()).toContain('level');\n      expect(text.toLowerCase()).toContain('severity');\n    });\n  });\n\n  test.describe('Traces query builder', () => {\n    test.beforeEach(async ({ page }) => {\n      await page.goto(exploreUrl({ queryType: QueryType.Traces }));\n      await switchToBuilderMode(page, QueryType.Traces);\n      // The Columns section is collapsible; make sure it's open before we assert.\n      // If it's already open the header click is a no-op (toggles closed, then we re-open).\n      const columnsHeader = page.getByRole('button', { name: 'Columns' });\n      if (await columnsHeader.isVisible().catch(() => false)) {\n        const helpVisible = await page\n          .getByTestId(Selectors.QueryBuilder.TraceQueryBuilder.columnRolesHelp)\n          .isVisible()\n          .catch(() => false);\n        if (!helpVisible) {\n          await columnsHeader.click();\n        }\n      }\n    });\n\n    test('renders the column-roles help note with a docs link', async ({ page }) => {\n      const help = page.getByTestId(Selectors.QueryBuilder.TraceQueryBuilder.columnRolesHelp);\n      await expect(help).toBeVisible();\n      await expect(help).toContainText('column roles', { ignoreCase: true });\n\n      const link = page.getByTestId(Selectors.QueryBuilder.TraceQueryBuilder.columnRolesHelpLink);\n      await expect(link).toBeVisible();\n      await expect(link).toHaveAttribute('href', COLUMN_ROLES_DOCS_PATH);\n    });\n\n    test('Trace ID column tooltip explains the `traceID` SQL alias', async ({ page }) => {\n      const text = await readTooltip(page, labelLocator(page, 'Trace ID Column'));\n      expect(text).toContain('traceID');\n      expect(text.toLowerCase()).toContain('aliased to');\n    });\n\n    test('Span ID column tooltip explains the `spanID` SQL alias', async ({ page }) => {\n      const text = await readTooltip(page, labelLocator(page, 'Span ID Column'));\n      expect(text).toContain('spanID');\n    });\n\n    test('Duration Time column tooltip mentions the unit setting', async ({ page }) => {\n      const text = await readTooltip(page, labelLocator(page, 'Duration Time Column'));\n      expect(text.toLowerCase()).toContain('duration');\n      expect(text.toLowerCase()).toContain('unit');\n    });\n  });\n\n  test.describe('Time series query builder', () => {\n    test.beforeEach(async ({ page }) => {\n      await page.goto(exploreUrl({ queryType: QueryType.TimeSeries }));\n      await switchToBuilderMode(page, QueryType.TimeSeries);\n    });\n\n    test('renders the column-roles help note with a docs link', async ({ page }) => {\n      const help = page.getByTestId(Selectors.QueryBuilder.TimeSeriesQueryBuilder.columnRolesHelp);\n      await expect(help).toBeVisible();\n      await expect(help).toContainText('Time column is required');\n\n      const link = page.getByTestId(Selectors.QueryBuilder.TimeSeriesQueryBuilder.columnRolesHelpLink);\n      await expect(link).toBeVisible();\n      await expect(link).toHaveAttribute('href', COLUMN_ROLES_DOCS_PATH);\n    });\n\n    test('Time column tooltip describes DateTime/DateTime64 requirement', async ({ page }) => {\n      const text = await readTooltip(page, labelLocator(page, 'Time'));\n      expect(text).toContain('DateTime');\n      expect(text.toLowerCase()).toContain('order and bucket');\n    });\n  });\n\n  test.describe('Table query builder', () => {\n    // The Table query type has no column roles; verify the help note is NOT rendered there\n    // so the guidance doesn't leak into unrelated UI.\n    test('does not render any column-roles help note', async ({ page }) => {\n      await page.goto(exploreUrl({ queryType: QueryType.Table }));\n      await switchToBuilderMode(page);\n      await expect(page.getByTestId(Selectors.QueryBuilder.LogsQueryBuilder.columnRolesHelp)).toHaveCount(0);\n      await expect(page.getByTestId(Selectors.QueryBuilder.TraceQueryBuilder.columnRolesHelp)).toHaveCount(0);\n      await expect(page.getByTestId(Selectors.QueryBuilder.TimeSeriesQueryBuilder.columnRolesHelp)).toHaveCount(0);\n    });\n  });\n});\n"
  },
  {
    "path": "tests/e2e/configEditor.spec.ts",
    "content": "import { expect, test } from '@grafana/plugin-e2e';\nimport { Page } from '@playwright/test';\nimport { CHConfig } from '../../src/types/config';\n\nconst PLUGIN_UID = 'grafana-clickhouse-datasource';\nconst PROVISIONING_FILE = 'clickhouse.yml';\n\n// GRAFANA_URL is set only by the Cloud cron workflow (playwright-cloud). Local and PR CI\n// don't set it, so its presence is a reliable signal that we're running against a shared\n// Cloud instance where the local provisioning/datasources/clickhouse.yml is not applied.\nconst isCloudRun = !!process.env.GRAFANA_URL;\n\nfunction resolveClickhouseUrl(env = process.env) {\n  const { CI, DS_INSTANCE_HOST } = env;\n  return CI ? DS_INSTANCE_HOST || 'clickhouse-server' : 'localhost';\n}\n\nasync function configurePDC(page: Page, networkName: string) {\n  await page.getByRole('combobox', { name: 'Private data source connect' }).click();\n  await page.getByText(networkName).click();\n}\n\n/**\n * Waits for the config editor to fully render, then returns true if V2\n * (newClickhouseConfigPageDesign) is active. Uses waitForSelector so it\n * handles both slow plugin initialization and CI environments reliably —\n * unlike isVisible(), which returns immediately without waiting.\n */\nasync function isV2Editor(page: Page): Promise<boolean> {\n  await page.waitForSelector('[placeholder=\"Enter server address\"], [placeholder=\"Server address\"]', {\n    timeout: 10000,\n  });\n  return page.locator('[placeholder=\"Enter server address\"]').isVisible();\n}\n\ntest.describe('Config editor', () => {\n  test.describe('rendering', () => {\n    test('smoke: should render config editor', { tag: ['@plugins'] }, async ({ createDataSourceConfigPage, page }) => {\n      await createDataSourceConfigPage({ type: PLUGIN_UID });\n      const isV2 = await isV2Editor(page);\n      // V2 renders section titles inside CollapsableSection: the toggle button gets\n      // aria-label pointing to the label div, so getByRole('button') is the right selector.\n      await expect(\n        isV2\n          ? page.getByRole('button', { name: 'Server and encryption' })\n          : page.getByRole('heading', { name: 'Server' })\n      ).toBeVisible();\n    });\n\n    test('should render Server section', async ({ createDataSourceConfigPage, page }) => {\n      await createDataSourceConfigPage({ type: PLUGIN_UID });\n      const isV2 = await isV2Editor(page);\n      await expect(\n        isV2\n          ? page.getByRole('button', { name: 'Server and encryption' })\n          : page.getByRole('heading', { name: 'Server' })\n      ).toBeVisible();\n      await expect(page.getByPlaceholder(isV2 ? 'Enter server address' : 'Server address')).toBeVisible();\n      await expect(page.getByPlaceholder(isV2 ? 'Enter server port' : '9000')).toBeVisible();\n      await expect(page.getByRole('radio', { name: 'Native' })).toBeVisible();\n      await expect(page.getByRole('radio', { name: 'HTTP' })).toBeVisible();\n    });\n\n    test('should render TLS / SSL Settings section', async ({ createDataSourceConfigPage, page }) => {\n      await createDataSourceConfigPage({ type: PLUGIN_UID });\n      const isV2 = await isV2Editor(page);\n      if (isV2) {\n        await expect(page.getByRole('button', { name: 'TLS/SSL settings' })).toBeVisible();\n        // TLS/SSL section is collapsed by default in V2 — expand it before checking inner fields.\n        await page.getByRole('button', { name: 'TLS/SSL settings' }).click();\n      } else {\n        await expect(page.getByRole('heading', { name: 'TLS / SSL Settings' })).toBeVisible();\n      }\n      // The label and description for these fields share identical text — use .first() to\n      // target the visible label div, not the description span that follows it.\n      await expect(page.getByText('Skip TLS Verify').first()).toBeVisible();\n      await expect(page.getByText('TLS Client Auth').first()).toBeVisible();\n    });\n\n    test('should render Credentials section', async ({ createDataSourceConfigPage, page }) => {\n      await createDataSourceConfigPage({ type: PLUGIN_UID });\n      const isV2 = await isV2Editor(page);\n      await expect(\n        isV2\n          ? page.getByRole('button', { name: 'Database credentials' })\n          : page.getByRole('heading', { name: 'Credentials' })\n      ).toBeVisible();\n      await expect(page.getByPlaceholder(isV2 ? 'Enter username' : 'default')).toBeVisible();\n      await expect(page.getByPlaceholder(isV2 ? 'Enter password' : 'password')).toBeVisible();\n    });\n  });\n\n  test.describe('provisioned datasource', () => {\n    test.beforeEach(() => {\n      test.skip(\n        isCloudRun,\n        'Provisioned-datasource tests assert values from the local provisioning/datasources/clickhouse.yml file, which is not applied on the shared Cloud instance.'\n      );\n    });\n\n    test('should load provisioned server address', async ({\n      readProvisionedDataSource,\n      gotoDataSourceConfigPage,\n      page,\n    }) => {\n      const ds = await readProvisionedDataSource<CHConfig>({ fileName: PROVISIONING_FILE });\n      await gotoDataSourceConfigPage(ds.uid);\n      const isV2 = await isV2Editor(page);\n      await expect(page.getByPlaceholder(isV2 ? 'Enter server address' : 'Server address')).toHaveValue(\n        'clickhouse-server'\n      );\n    });\n\n    test('should load provisioned port and protocol', async ({\n      readProvisionedDataSource,\n      gotoDataSourceConfigPage,\n      page,\n    }) => {\n      const ds = await readProvisionedDataSource<CHConfig>({ fileName: PROVISIONING_FILE });\n      await gotoDataSourceConfigPage(ds.uid);\n      const isV2 = await isV2Editor(page);\n      await expect(page.getByPlaceholder(isV2 ? 'Enter server port' : '9000')).toHaveValue('9000');\n      await expect(page.getByRole('radio', { name: 'Native' })).toBeChecked();\n    });\n  });\n\n  test.describe('save & test', () => {\n    test('should pass health check for provisioned datasource', async ({\n      readProvisionedDataSource,\n      gotoDataSourceConfigPage,\n      page,\n    }) => {\n      test.skip(\n        isCloudRun,\n        'Provisioned-datasource tests assert values from the local provisioning/datasources/clickhouse.yml file, which is not applied on the shared Cloud instance.'\n      );\n      // Provisioned datasources show a read-only \"Test\" button (not \"Save & test\"),\n      // since the UI cannot modify provisioned configuration.\n      const ds = await readProvisionedDataSource<CHConfig>({ fileName: PROVISIONING_FILE });\n      await gotoDataSourceConfigPage(ds.uid);\n      await page.getByRole('button', { name: 'Test' }).click();\n      await expect(page.getByText('Data source is working')).toBeVisible();\n    });\n\n    test('invalid credentials should return an error', async ({ createDataSourceConfigPage, page }) => {\n      const configPage = await createDataSourceConfigPage({ type: PLUGIN_UID });\n      const isV2 = await isV2Editor(page);\n      await page.getByPlaceholder(isV2 ? 'Enter server address' : 'Server address').fill(resolveClickhouseUrl());\n      if (isV2) {\n        await page.getByPlaceholder('Enter server port').fill('9000');\n        await page.getByPlaceholder('Enter username').fill('invalid_user');\n      }\n      await expect(configPage.saveAndTest()).not.toBeOK();\n    });\n\n    test('valid credentials should display a success alert on the page', async ({\n      createDataSourceConfigPage,\n      page,\n    }) => {\n      // Requires ClickHouse to be reachable FROM INSIDE the Grafana container.\n      // In Docker Compose, set DS_INSTANCE_HOST=clickhouse-server. Skipped otherwise.\n      test.skip(\n        !process.env.CI && !process.env.DS_INSTANCE_HOST,\n        'ClickHouse must be reachable from inside Grafana; set DS_INSTANCE_HOST or run in CI'\n      );\n\n      const configPage = await createDataSourceConfigPage({ type: PLUGIN_UID });\n      const isV2 = await isV2Editor(page);\n      await page.getByPlaceholder(isV2 ? 'Enter server address' : 'Server address').fill(resolveClickhouseUrl());\n      await page.getByPlaceholder(isV2 ? 'Enter server port' : '9000').fill(process.env.DS_INSTANCE_PORT ?? '9000');\n      await page\n        .getByPlaceholder(isV2 ? 'Enter username' : 'default')\n        .fill(process.env.DS_INSTANCE_USERNAME ?? 'default');\n      await page.getByPlaceholder(isV2 ? 'Enter password' : 'password').fill(process.env.DS_INSTANCE_PASSWORD ?? '');\n\n      if (process.env.DS_PDC_NETWORK_NAME) {\n        await configurePDC(page, process.env.DS_PDC_NETWORK_NAME);\n      }\n\n      await configPage.saveAndTest();\n      await expect(configPage).toHaveAlert('success', { hasNotText: 'Datasource updated' });\n    });\n\n    test('mandatory fields should show error if left empty', async ({ createDataSourceConfigPage, page }) => {\n      const configPage = await createDataSourceConfigPage({ type: PLUGIN_UID });\n\n      // This test requires the V2 config editor (newClickhouseConfigPageDesign feature toggle).\n      // The V2 editor shows inline validation errors on blur; V1 only shows them after save.\n      const isV2 = await isV2Editor(page);\n      test.skip(!isV2, 'Requires newClickhouseConfigPageDesign feature toggle to be enabled');\n\n      const hostInput = page.getByPlaceholder('Enter server address');\n      await hostInput.focus();\n      await hostInput.press('Tab');\n      await expect(page.getByText('Server address required', { exact: true })).toBeVisible();\n\n      const portInput = page.getByPlaceholder('Enter server port');\n      await portInput.focus();\n      await portInput.press('Tab');\n      await expect(page.getByText('Port is required', { exact: true })).toBeVisible();\n\n      // In V2, validation blocks the save when required fields are empty — no network\n      // request is made. Grafana surfaces the errors in the testing-status banner instead.\n      await page.getByRole('button', { name: 'Save & test' }).click();\n      await expect(page.getByText('Server address required', { exact: true })).toBeVisible();\n      await expect(page.getByText('Port is required', { exact: true })).toBeVisible();\n    });\n  });\n});\n"
  },
  {
    "path": "tests/e2e/fixtures/seed.sql",
    "content": "-- E2E test fixture data for grafana-clickhouse-datasource.\n-- Fully self-contained: no external URLs, no network dependencies.\n-- Time range covered: 2024-03-15 10:00:00 – 2024-03-15 10:09:00 UTC.\n-- Tests that query this data should use a window of at least\n--   from: '2024-03-15T09:45:00.000Z'\n--   to:   '2024-03-15T10:15:00.000Z'\n\nCREATE DATABASE IF NOT EXISTS e2e_test;\n\nCREATE TABLE IF NOT EXISTS e2e_test.events\n(\n    timestamp DateTime,\n    level     LowCardinality(String),\n    message   String,\n    value     Float64,\n    service   LowCardinality(String)\n)\nENGINE = MergeTree\nORDER BY timestamp;\n\nINSERT INTO e2e_test.events (timestamp, level, message, value, service) VALUES\n    ('2024-03-15 10:00:00', 'info',  'Service started',           1.0,  'api'),\n    ('2024-03-15 10:01:00', 'debug', 'Request received',          2.5,  'api'),\n    ('2024-03-15 10:02:00', 'info',  'Request processed',         1.2,  'api'),\n    ('2024-03-15 10:03:00', 'warn',  'High memory usage',        85.3,  'worker'),\n    ('2024-03-15 10:04:00', 'error', 'Connection timeout',        0.0,  'worker'),\n    ('2024-03-15 10:05:00', 'info',  'Recovery complete',         1.0,  'worker'),\n    ('2024-03-15 10:06:00', 'debug', 'Cache hit',                 0.5,  'cache'),\n    ('2024-03-15 10:07:00', 'info',  'Scheduled task started',    1.0,  'scheduler'),\n    ('2024-03-15 10:08:00', 'info',  'Scheduled task completed',  1.0,  'scheduler'),\n    ('2024-03-15 10:09:00', 'error', 'Database connection failed', 0.0, 'db');\n"
  },
  {
    "path": "tests/e2e/fixtures/trace_spans.sql",
    "content": "-- Trace-spans fixture for the #1541 trace-viewer LIMIT regression guard.\n--\n-- Self-contained so ordering with seed.sql does not matter. The\n-- docker-compose e2e-data-loader service iterates every *.sql file under\n-- /data in lexicographic order.\n--\n-- The bug: when the user searches traces with a LIMIT (e.g. 3), the plugin\n-- reuses the same LIMIT for the single-trace span query, truncating the\n-- waterfall. Fix drops LIMIT from `generateTraceIdQuery`. The fixture seeds\n-- 5 spans for trace 'e2e-trace-a' so an E2E test can assert the \"no LIMIT\"\n-- SQL pattern returns all 5 spans.\n\nCREATE DATABASE IF NOT EXISTS e2e_test;\n\nCREATE TABLE IF NOT EXISTS e2e_test.trace_spans\n(\n    Timestamp    DateTime64(9),\n    TraceId      String,\n    SpanId       String,\n    ParentSpanId String,\n    ServiceName  LowCardinality(String),\n    SpanName     LowCardinality(String),\n    Duration     Int64\n)\nENGINE = MergeTree\nORDER BY (TraceId, Timestamp);\n\n-- Five spans for 'e2e-trace-a' cover the regression case for #1541 (must\n-- not be truncated at LIMIT 3). The 'e2e-trace-b' row is unrelated noise\n-- so the WHERE TraceId = '…' filter is exercised.\nINSERT INTO e2e_test.trace_spans\n    (Timestamp, TraceId, SpanId, ParentSpanId, ServiceName, SpanName, Duration) VALUES\n    ('2024-03-15 10:00:00', 'e2e-trace-a', 'span-1', '',        'api',    'HTTP GET /',        10000000),\n    ('2024-03-15 10:00:01', 'e2e-trace-a', 'span-2', 'span-1',  'api',    'db.query users',     5000000),\n    ('2024-03-15 10:00:02', 'e2e-trace-a', 'span-3', 'span-1',  'api',    'cache.get profile',  2000000),\n    ('2024-03-15 10:00:03', 'e2e-trace-a', 'span-4', 'span-2',  'db',     'SELECT users',       4000000),\n    ('2024-03-15 10:00:04', 'e2e-trace-a', 'span-5', 'span-3',  'cache',  'GET profile:42',     1000000),\n    ('2024-03-15 10:00:10', 'e2e-trace-b', 'span-9', '',        'worker', 'job.run',            8000000);\n"
  },
  {
    "path": "tests/e2e/queryEditor.spec.ts",
    "content": "import { expect, test, ExplorePage } from '@grafana/plugin-e2e';\nimport { Page } from '@playwright/test';\nimport { QueryType } from '../../src/types/queryBuilder';\nimport { EditorType } from '../../src/types/sql';\n\nconst PLUGIN_TYPE = 'grafana-clickhouse-datasource';\n\n// GRAFANA_URL is set only by the Cloud cron workflow (see .github/workflows/cron.yml).\n// Its presence indicates the local provisioning file and seed fixtures do not apply.\nconst isCloudRun = !!process.env.GRAFANA_URL;\n\n// CLOUD_DEFAULT_UID points at `[managed_data_source] - ClickHouse Native (PDC)` on the\n// shared Cloud dev instance. The infra team uses a stable `clickhouse-{protocol}-ds-m`\n// naming convention, but if the datasource is ever re-provisioned and Cloud E2E starts\n// failing with datasource-not-found errors, log into the instance, copy the current uid\n// from the /connections/datasources/edit/<uid> URL, and update this constant (or set\n// DS_E2E_UID in the workflow as a quick override).\nconst CLOUD_DEFAULT_UID = 'clickhouse-native-ds-m';\nconst LOCAL_DEFAULT_UID = 'clickhouse-e2e';\nconst DATASOURCE_UID = process.env.DS_E2E_UID || (isCloudRun ? CLOUD_DEFAULT_UID : LOCAL_DEFAULT_UID);\n\n// Time range that fully covers the seed fixture data in tests/e2e/fixtures/seed.sql\nconst FIXTURE_FROM_ISO = '2024-03-15T09:45:00.000Z';\nconst FIXTURE_TO_ISO = '2024-03-15T10:15:00.000Z';\n\ninterface ExploreUrlOpts {\n  queryType?: QueryType;\n  editorType?: EditorType;\n  from?: string;\n  to?: string;\n}\n\n/**\n * Build an Explore URL encoding the full pane state.\n * Note: Grafana does not restore editorType or builderOptions from the URL panes\n * state for this plugin — the editor always opens in SQL Editor mode.\n * queryType IS restored via the query's top-level queryType field (used by SQL mode).\n */\nfunction exploreUrl(opts: ExploreUrlOpts = {}): string {\n  const { from = 'now-1h', to = 'now' } = opts;\n\n  const query: Record<string, unknown> = {\n    refId: 'A',\n    datasource: { type: PLUGIN_TYPE, uid: DATASOURCE_UID },\n    editorType: 'sql',\n    pluginVersion: '',\n    rawSql: '',\n  };\n\n  const panes = JSON.stringify({\n    explore: {\n      datasource: DATASOURCE_UID,\n      queries: [query],\n      range: { from, to },\n    },\n  });\n  \n  return `/explore?orgId=1&schemaVersion=1&panes=${encodeURIComponent(panes)}`;\n}\n\n/**\n * Switch the query editor from SQL Editor mode (the default on page load) to\n * Query Builder mode. If the editor contains a non-SELECT statement, Grafana\n * shows a \"Cannot convert\" confirmation dialog — dismiss it by clicking Continue.\n */\nasync function switchToBuilderMode(page: Page) {\n  await page.getByRole('radio', { name: 'Query Builder' }).click();\n  // When the SQL editor contains a non-SELECT statement (e.g. empty), Grafana\n  // shows a \"Cannot convert\" confirmation dialog before switching modes.\n  const continueButton = page.getByRole('button', { name: 'Continue' });\n  if (await continueButton.isVisible({ timeout: 3000 }).catch(() => false)) {\n    await continueButton.click();\n  }\n  // Wait until the Query Builder radio is checked, confirming the builder has rendered.\n  await expect(page.getByRole('radio', { name: 'Query Builder' })).toBeChecked();\n}\n\n/**\n * Type SQL into the CodeMirror editor. Clicks to focus, selects all existing\n * content, then types the replacement query.\n */\nasync function enterSql(page: Page, sql: string) {\n  const editor = page.getByRole('code');\n  await editor.click();\n  await page.keyboard.press('ControlOrMeta+a');\n  await page.keyboard.type(sql);\n}\n\n/**\n * Wraps explorePage.waitForQueryDataResponse, reading the response body\n * inside the predicate while the CDP buffer is still live.\n *\n * TODO: patch @grafana/plugin-e2e so waitForQueryDataResponse exposes the\n * body directly, removing the need for this workaround.\n */\nasync function waitForQueryDataResponseWithBody(explorePage: ExplorePage) {\n  let body: Record<string, unknown> | null = null;\n  const responsePromise = explorePage.waitForQueryDataResponse(async (r) => {\n    if (!r.ok()) {\n      return false;\n    }\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const b: any = await r.json().catch(() => null);\n    if (!Array.isArray(b?.results?.A?.frames)) {\n      return false;\n    }\n    body = b as Record<string, unknown>;\n    return true;\n  });\n  return { responsePromise, getBody: () => body };\n}\n\n// ---------------------------------------------------------------------------\n// Rendering tests — verify the query editor UI structure without requiring\n// real query results.\n// ---------------------------------------------------------------------------\n\ntest.describe('Query editor', () => {\n  test.describe('rendering', () => {\n    test('smoke: renders all query type options', { tag: ['@plugins'] }, async ({ page }) => {\n      await page.goto(exploreUrl());\n      // Query type radios are always visible regardless of editor mode\n      await expect(page.getByRole('radio', { name: 'Table' })).toBeVisible();\n      await expect(page.getByRole('radio', { name: 'Logs' })).toBeVisible();\n      await expect(page.getByRole('radio', { name: 'Time Series' })).toBeVisible();\n      await expect(page.getByRole('radio', { name: 'Traces' })).toBeVisible();\n    });\n\n    test('renders editor type switcher', async ({ page }) => {\n      await page.goto(exploreUrl());\n      // Grafana opens the editor in SQL Editor mode by default\n      await expect(page.getByRole('radio', { name: 'SQL Editor' })).toBeChecked();\n      await expect(page.getByRole('radio', { name: 'Query Builder' })).toBeVisible();\n    });\n\n    test('renders Run Query button', async ({ page }) => {\n      await page.goto(exploreUrl());\n      // The toolbar also has a \"Run query\" button — scope to the query editor row to\n      // avoid a strict-mode violation from matching both.\n      await expect(\n        page.locator('.query-editor-row').getByRole('button', { name: 'Run Query' })\n      ).toBeVisible();\n    });\n\n    test('renders SQL editor code area', async ({ page }) => {\n      await page.goto(exploreUrl());\n      await expect(page.getByRole('code')).toBeVisible();\n    });\n  });\n\n  test.describe('Query Builder mode', () => {\n    test('renders database and table selectors after switching to Builder mode', async ({ page }) => {\n      await page.goto(exploreUrl());\n      await switchToBuilderMode(page);\n      // Use a scoped locator — `label.query-keyword` is the Grafana inline form label\n      // class used by the builder for all its field labels (Database, Table, etc.).\n      await expect(\n        page.locator('.query-editor-row').locator('label.query-keyword', { hasText: 'Database' })\n      ).toBeVisible();\n    });\n\n    test('renders builder mode toggle with Simple and Aggregate options', async ({ page }) => {\n      await page.goto(exploreUrl());\n      await switchToBuilderMode(page);\n      await expect(page.getByText('Builder Mode')).toBeVisible();\n      await expect(page.getByRole('radio', { name: 'Simple' })).toBeVisible();\n      await expect(page.getByRole('radio', { name: 'Aggregate' })).toBeVisible();\n    });\n  });\n\n  test.describe('Query type selection', () => {\n    test('can select Logs query type', async ({ page }) => {\n      await page.goto(exploreUrl());\n      await page.getByRole('radio', { name: 'Logs' }).click();\n      await expect(page.getByRole('radio', { name: 'Logs' })).toBeChecked();\n    });\n\n    test('can select Time Series query type', async ({ page }) => {\n      await page.goto(exploreUrl());\n      await page.getByRole('radio', { name: 'Time Series' }).click();\n      await expect(page.getByRole('radio', { name: 'Time Series' })).toBeChecked();\n    });\n\n    test('can select Traces query type', async ({ page }) => {\n      await page.goto(exploreUrl());\n      await page.getByRole('radio', { name: 'Traces' }).click();\n      await expect(page.getByRole('radio', { name: 'Traces' })).toBeChecked();\n    });\n  });\n});\n\n// ---------------------------------------------------------------------------\n// Fixture-data tests — require e2e_test.events seeded by seed.sql via the\n// e2e-data-loader Docker Compose service. Run serially to avoid competing for\n// the same ClickHouse instance under parallel workers.\n// ---------------------------------------------------------------------------\n\ntest.describe('Query editor with fixture data', () => {\n  test.describe.configure({ mode: 'serial' });\n\n  test.beforeEach(() => {\n    test.skip(\n      isCloudRun,\n      'Fixture-data tests depend on e2e_test.events seeded by tests/e2e/fixtures/seed.sql via the local e2e-data-loader Docker service, which is not available on Cloud.'\n    );\n  });\n\n  test('SQL query returns rows from fixture data', async ({ page, explorePage }) => {\n    await page.goto(exploreUrl({ from: FIXTURE_FROM_ISO, to: FIXTURE_TO_ISO }));\n    await enterSql(page, 'SELECT timestamp, level, message FROM e2e_test.events ORDER BY timestamp LIMIT 10');\n\n    const { responsePromise, getBody } = await waitForQueryDataResponseWithBody(explorePage);\n    await page.locator('.query-editor-row').getByRole('button', { name: 'Run Query' }).click();\n\n    await responsePromise;\n    expect((getBody() as any)?.results?.A?.frames?.length).toBeGreaterThan(0);\n  });\n\n  test('Aggregate SQL query returns a count from fixture data', async ({ page, explorePage }) => {\n    await page.goto(exploreUrl({ from: FIXTURE_FROM_ISO, to: FIXTURE_TO_ISO }));\n    await enterSql(page, 'SELECT count(*) AS total FROM e2e_test.events');\n\n    const { responsePromise, getBody } = await waitForQueryDataResponseWithBody(explorePage);\n    await page.locator('.query-editor-row').getByRole('button', { name: 'Run Query' }).click();\n\n    await responsePromise;\n    expect((getBody() as any)?.results?.A?.frames?.length).toBeGreaterThan(0);\n  });\n\n  test('SQL query with WHERE filter returns matching rows', async ({ page, explorePage }) => {\n    await page.goto(exploreUrl({ from: FIXTURE_FROM_ISO, to: FIXTURE_TO_ISO }));\n    await enterSql(page, \"SELECT timestamp, message FROM e2e_test.events WHERE level = 'error' ORDER BY timestamp\");\n\n    const { responsePromise, getBody } = await waitForQueryDataResponseWithBody(explorePage);\n    await page.locator('.query-editor-row').getByRole('button', { name: 'Run Query' }).click();\n\n    await responsePromise;\n    expect((getBody() as any)?.results?.A?.frames?.length).toBeGreaterThan(0);\n  });\n});\n"
  },
  {
    "path": "tests/e2e/sqlAutocomplete.spec.ts",
    "content": "import { expect, test } from '@grafana/plugin-e2e';\nimport { Page } from '@playwright/test';\n\nconst PLUGIN_TYPE = 'grafana-clickhouse-datasource';\n\n// GRAFANA_URL is set only by the Cloud cron workflow (see .github/workflows/cron.yml).\nconst isCloudRun = !!process.env.GRAFANA_URL;\n\nconst CLOUD_DEFAULT_UID = 'clickhouse-native-ds-m';\nconst LOCAL_DEFAULT_UID = 'clickhouse-e2e';\nconst DATASOURCE_UID = process.env.DS_E2E_UID || (isCloudRun ? CLOUD_DEFAULT_UID : LOCAL_DEFAULT_UID);\n\nfunction exploreUrl(): string {\n  const query: Record<string, unknown> = {\n    refId: 'A',\n    datasource: { type: PLUGIN_TYPE, uid: DATASOURCE_UID },\n    editorType: 'sql',\n    pluginVersion: '',\n    rawSql: '',\n  };\n\n  const panes = JSON.stringify({\n    explore: {\n      datasource: DATASOURCE_UID,\n      queries: [query],\n      range: { from: 'now-1h', to: 'now' },\n    },\n  });\n\n  return `/explore?orgId=1&schemaVersion=1&panes=${encodeURIComponent(panes)}`;\n}\n\nasync function focusEditorAndType(page: Page, text: string) {\n  const editor = page.getByRole('code');\n  await editor.click();\n  await page.keyboard.press('ControlOrMeta+a');\n  await page.keyboard.press('Delete');\n  await page.keyboard.type(text);\n}\n\n// Plugin macros are static (defined in src/ch-parser/pluginMacros.ts) so they're\n// a deterministic target across local fixture and Cloud cron runs.\nasync function captureMacroLabels(page: Page): Promise<string[]> {\n  const widget = page.locator('.monaco-editor .suggest-widget.visible');\n  await widget.waitFor({ timeout: 5000 });\n  const labels = await page\n    .locator('.monaco-editor .suggest-widget .monaco-list-row .label-name')\n    .allTextContents();\n  return labels.map((l) => l.trim()).filter((l) => l.startsWith('$__'));\n}\n\nfunction findDuplicates(labels: string[]): string[] {\n  const seen = new Set<string>();\n  const dupes = new Set<string>();\n  labels.forEach((l) => (seen.has(l) ? dupes.add(l) : seen.add(l)));\n  return [...dupes];\n}\n\ntest.describe('SQL editor autocomplete', () => {\n  test('does not duplicate suggestions after editor remount', async ({ page }) => {\n    await page.goto(exploreUrl());\n    // `$` is a registered trigger character; `$__` narrows the popup to plugin macros.\n    await focusEditorAndType(page, 'SELECT * FROM t WHERE $__');\n\n    const firstMountLabels = await captureMacroLabels(page);\n    expect(firstMountLabels.length, 'first mount surfaces plugin macros').toBeGreaterThan(0);\n    expect(findDuplicates(firstMountLabels), 'first mount has no duplicate macros').toEqual([]);\n\n    // Navigate away and back to force SqlEditor to unmount and remount.\n    await page.goto('/');\n    await page.goto(exploreUrl());\n    await focusEditorAndType(page, 'SELECT * FROM t WHERE $__');\n\n    const secondMountLabels = await captureMacroLabels(page);\n    expect(findDuplicates(secondMountLabels), 'second mount has no duplicate macros').toEqual([]);\n    expect(new Set(secondMountLabels)).toEqual(new Set(firstMountLabels));\n  });\n});\n"
  },
  {
    "path": "tests/e2e/sqlValidation.spec.ts",
    "content": "import { expect, test } from '@grafana/plugin-e2e';\nimport { Page } from '@playwright/test';\n\nconst PLUGIN_TYPE = 'grafana-clickhouse-datasource';\n\n// GRAFANA_URL is set only by the Cloud cron workflow (see .github/workflows/cron.yml).\nconst isCloudRun = !!process.env.GRAFANA_URL;\n\n// CLOUD_DEFAULT_UID points at `[managed_data_source] - ClickHouse Native (PDC)` on the\n// shared Cloud dev instance. The infra team uses a stable `clickhouse-{protocol}-ds-m`\n// naming convention, but if the datasource is ever re-provisioned and Cloud E2E starts\n// failing with datasource-not-found errors, log into the instance, copy the current uid\n// from the /connections/datasources/edit/<uid> URL, and update this constant (or set\n// DS_E2E_UID in the workflow as a quick override).\nconst CLOUD_DEFAULT_UID = 'clickhouse-native-ds-m';\nconst LOCAL_DEFAULT_UID = 'clickhouse-e2e';\nconst DATASOURCE_UID = process.env.DS_E2E_UID || (isCloudRun ? CLOUD_DEFAULT_UID : LOCAL_DEFAULT_UID);\n\n/**\n * Build an Explore URL with an empty SQL query so the editor opens in\n * SQL Editor mode with no pre-existing content.\n */\nfunction exploreUrl(): string {\n  const query: Record<string, unknown> = {\n    refId: 'A',\n    datasource: { type: PLUGIN_TYPE, uid: DATASOURCE_UID },\n    editorType: 'sql',\n    pluginVersion: '',\n    rawSql: '',\n  };\n\n  const panes = JSON.stringify({\n    explore: {\n      datasource: DATASOURCE_UID,\n      queries: [query],\n      range: { from: 'now-1h', to: 'now' },\n    },\n  });\n\n  return `/explore?orgId=1&schemaVersion=1&panes=${encodeURIComponent(panes)}`;\n}\n\n/**\n * Type SQL into the Monaco editor. Clicks to focus, selects all existing\n * content, then types the replacement query. Each keystroke triggers the\n * editor's onKeyUp handler, which runs validate() and writes Monaco markers.\n */\nasync function enterSql(page: Page, sql: string) {\n  const editor = page.getByRole('code');\n  await editor.click();\n  await page.keyboard.press('ControlOrMeta+a');\n  await page.keyboard.type(sql);\n}\n\n/**\n * Monaco renders validation errors by adding the `squiggly-error` CSS class\n * to inline decoration spans under the offending tokens. Counting instances\n * of that class gives a DOM-level read of what the user sees.\n *\n * The typed SQL is committed synchronously but Monaco schedules decoration\n * rendering on the next animation frame, so we give it a brief moment before\n * asserting.\n */\nasync function expectNoErrorMarkers(page: Page) {\n  await expect(async () => {\n    await expect(page.locator('.squiggly-error')).toHaveCount(0);\n  }).toPass({ timeout: 2000 });\n}\n\nasync function expectHasErrorMarker(page: Page) {\n  await expect(page.locator('.squiggly-error').first()).toBeVisible({ timeout: 2000 });\n}\n\n/**\n * Regression guard for the js-sql-parser false-positive bug: that parser flagged\n * valid ClickHouse-specific syntax (FINAL, PREWHERE, ARRAY JOIN, SETTINGS,\n * ASOF JOIN, :: cast, etc.) as errors, producing red squiggles in the SQL editor\n * whenever `validateSql` was enabled. This suite exercises the full wiring —\n * user types SQL → onKeyUp → validate() → setModelMarkers() → Monaco renders — so\n * if anyone re-introduces a parser that misidentifies these constructs, the\n * regression shows up as a failing test here rather than as a user complaint.\n */\ntest.describe('SQL editor validation', () => {\n  const validClickhouseQueries: Array<{ name: string; sql: string }> = [\n    { name: 'FINAL keyword', sql: 'SELECT * FROM test.events FINAL' },\n    { name: 'PREWHERE clause', sql: 'SELECT * FROM t PREWHERE x > 1 WHERE y > 2' },\n    { name: 'ARRAY JOIN', sql: 'SELECT * FROM t ARRAY JOIN arr' },\n    { name: 'SETTINGS', sql: 'SELECT * FROM t SETTINGS max_rows_to_read = 1000' },\n    { name: 'GLOBAL IN', sql: 'SELECT * FROM t WHERE id GLOBAL IN (SELECT id FROM t2)' },\n    { name: 'ASOF JOIN', sql: 'SELECT * FROM t1 ASOF JOIN t2 ON t1.id = t2.id' },\n    { name: ':: cast operator', sql: \"SELECT '2024-01-01'::DateTime FROM t\" },\n    { name: 'Grafana $__timeFilter macro', sql: 'SELECT * FROM t WHERE $__timeFilter(timestamp)' },\n    { name: 'Grafana ${variable} template', sql: 'SELECT * FROM t WHERE service = ${service}' },\n  ];\n\n  for (const { name, sql } of validClickhouseQueries) {\n    test(`does not flag ${name} as invalid`, async ({ page }) => {\n      await page.goto(exploreUrl());\n      await enterSql(page, sql);\n      await expectNoErrorMarkers(page);\n    });\n  }\n\n  // Control test: without this, all the positive assertions above would still\n  // pass if validation were silently disabled (no validator → no markers). This\n  // confirms the editor → validator → Monaco marker pipeline is actually wired.\n  //\n  // We use an unclosed `/*` block comment rather than an unclosed string, because\n  // Monaco's auto-close-bracket feature inserts a matching `'` as you type, which\n  // defeats the unclosed-string case.\n  test('flags a genuine error (unclosed block comment)', async ({ page }) => {\n    await page.goto(exploreUrl());\n    await enterSql(page, 'SELECT * FROM t /* unclosed comment');\n    await expectHasErrorMarker(page);\n  });\n});\n"
  },
  {
    "path": "tests/e2e/traceLimit.spec.ts",
    "content": "import { expect, test, ExplorePage } from '@grafana/plugin-e2e';\nimport { Page } from '@playwright/test';\n\n// Regression guard for #1541 — Trace viewer LIMIT applies to both list view\n// and trace view.\n//\n// Before the fix, when a user searched traces with `limit = 3`, clicking\n// through to a single trace reused the same LIMIT clause for the span\n// query, so the waterfall was missing spans. The fix drops LIMIT from\n// `generateTraceIdQuery` (single-trace mode). Unit tests in\n// `src/data/sqlGenerator.test.ts` cover the generator directly. This E2E\n// test runs the SQL the generator now produces against a seeded trace and\n// verifies every span is returned (not truncated at 3) — the end-to-end\n// guarantee the issue was really about.\n//\n// We exercise this via the SQL editor rather than clicking through the\n// Traces query-builder UI because the plugin's Traces builder needs OTel\n// column provisioning that isn't currently wired into the e2e setup.\n// Unit tests cover the builder side.\n\nconst PLUGIN_TYPE = 'grafana-clickhouse-datasource';\n\nconst isCloudRun = !!process.env.GRAFANA_URL;\n\nconst CLOUD_DEFAULT_UID = 'clickhouse-native-ds-m';\nconst LOCAL_DEFAULT_UID = 'clickhouse-e2e';\nconst DATASOURCE_UID = process.env.DS_E2E_UID || (isCloudRun ? CLOUD_DEFAULT_UID : LOCAL_DEFAULT_UID);\n\n// The trace_spans fixture in tests/e2e/fixtures/trace_spans.sql seeds five\n// spans for this trace at 2024-03-15 10:00:00–10:00:04 UTC.\nconst FIXTURE_FROM_ISO = '2024-03-15T09:45:00.000Z';\nconst FIXTURE_TO_ISO = '2024-03-15T10:15:00.000Z';\nconst TRACE_ID = 'e2e-trace-a';\nconst EXPECTED_SPAN_COUNT = 5;\n\nfunction exploreUrl(from: string, to: string): string {\n  const query = {\n    refId: 'A',\n    datasource: { type: PLUGIN_TYPE, uid: DATASOURCE_UID },\n    editorType: 'sql',\n    pluginVersion: '',\n    rawSql: '',\n  };\n  const panes = JSON.stringify({\n    explore: { datasource: DATASOURCE_UID, queries: [query], range: { from, to } },\n  });\n  return `/explore?orgId=1&schemaVersion=1&panes=${encodeURIComponent(panes)}`;\n}\n\nasync function enterSql(page: Page, sql: string) {\n  const editor = page.getByRole('code');\n  await editor.click();\n  await page.keyboard.press('ControlOrMeta+a');\n  await page.keyboard.type(sql);\n}\n\nasync function waitForQueryDataResponseWithBody(explorePage: ExplorePage) {\n  let body: Record<string, unknown> | null = null;\n  const responsePromise = explorePage.waitForQueryDataResponse(async (r) => {\n    if (!r.ok()) {\n      return false;\n    }\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const b: any = await r.json().catch(() => null);\n    if (!Array.isArray(b?.results?.A?.frames)) {\n      return false;\n    }\n    body = b as Record<string, unknown>;\n    return true;\n  });\n  return { responsePromise, getBody: () => body };\n}\n\ntest.describe('Trace ID query (#1541)', () => {\n  test.beforeEach(() => {\n    test.skip(\n      isCloudRun,\n      'Fixture-data tests depend on the local trace_spans seed (tests/e2e/fixtures/trace_spans.sql) loaded via the e2e-data-loader Docker service, which is not available on Cloud.'\n    );\n  });\n\n  test.describe.configure({ mode: 'serial' });\n\n  test('single-trace span query returns all spans (no LIMIT truncation)', async ({ page, explorePage }) => {\n    // Mirror the SQL shape the fixed `generateTraceIdQuery` now produces\n    // for a non-OTel trace lookup: SELECT ... WHERE TraceId = '…' with NO\n    // LIMIT clause. Before the fix this SQL had LIMIT 3 appended, cutting\n    // the waterfall.\n    const sql = `SELECT TraceId AS traceID, SpanId AS spanID, ParentSpanId AS parentSpanID, ServiceName AS serviceName, SpanName AS operationName FROM e2e_test.trace_spans WHERE TraceId = '${TRACE_ID}'`;\n\n    await page.goto(exploreUrl(FIXTURE_FROM_ISO, FIXTURE_TO_ISO));\n    await enterSql(page, sql);\n\n    const { responsePromise, getBody } = await waitForQueryDataResponseWithBody(explorePage);\n    await page.locator('.query-editor-row').getByRole('button', { name: 'Run Query' }).click();\n    await responsePromise;\n\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const frames = (getBody() as any)?.results?.A?.frames;\n    expect(Array.isArray(frames) && frames.length).toBeGreaterThan(0);\n\n    // Sum the row count across the returned data frames. clickhouse-datasource\n    // returns one frame with a values array per column; the number of spans\n    // equals the length of any column's value list.\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const firstFrame = frames[0] as any;\n    const anyColumn = firstFrame?.data?.values?.[0];\n    expect(Array.isArray(anyColumn)).toBe(true);\n    expect(anyColumn.length).toBe(EXPECTED_SPAN_COUNT);\n  });\n\n  test('applying a 3-row LIMIT truncates — confirms the fixture has >3 spans', async ({ page, explorePage }) => {\n    // Complementary assertion: with the old buggy behavior (LIMIT 3 inherited\n    // from the list query), only 3 of the 5 spans would be returned. This\n    // test guards against the fixture accidentally seeding <=3 spans, which\n    // would make the companion \"no LIMIT\" assertion above trivially pass.\n    const sql = `SELECT TraceId FROM e2e_test.trace_spans WHERE TraceId = '${TRACE_ID}' LIMIT 3`;\n\n    await page.goto(exploreUrl(FIXTURE_FROM_ISO, FIXTURE_TO_ISO));\n    await enterSql(page, sql);\n\n    const { responsePromise, getBody } = await waitForQueryDataResponseWithBody(explorePage);\n    await page.locator('.query-editor-row').getByRole('button', { name: 'Run Query' }).click();\n    await responsePromise;\n\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const frames = (getBody() as any)?.results?.A?.frames;\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const firstFrame = frames?.[0] as any;\n    const anyColumn = firstFrame?.data?.values?.[0];\n    expect(anyColumn.length).toBe(3);\n  });\n});\n"
  },
  {
    "path": "tests/fixtures/property-prices.sql",
    "content": "-- This is a dataset of UK property prices, for use as a test fixture. Based on the \n-- example dataset from the ClickHouse documentation:\n-- https://clickhouse.com/docs/en/getting-started/example-datasets/uk-price-paid\n\nCREATE TABLE uk_price_paid\n(\n    price UInt32,\n    date Date,\n    postcode1 LowCardinality(String),\n    postcode2 LowCardinality(String),\n    type Enum8('terraced' = 1, 'semi-detached' = 2, 'detached' = 3, 'flat' = 4, 'other' = 0),\n    is_new UInt8,\n    duration Enum8('freehold' = 1, 'leasehold' = 2, 'unknown' = 0),\n    addr1 String,\n    addr2 String,\n    street LowCardinality(String),\n    locality LowCardinality(String),\n    town LowCardinality(String),\n    district LowCardinality(String),\n    county LowCardinality(String)\n)\nENGINE = MergeTree\nORDER BY (postcode1, postcode2, addr1, addr2);\n\n\nINSERT INTO uk_price_paid\nWITH\nsplitByChar(' ', postcode) AS p\nSELECT\n    toUInt32(price_string) AS price,\n    parseDateTimeBestEffortUS(time) AS date,\n    p[1] AS postcode1,\n    p[2] AS postcode2,\n    transform(a, ['T', 'S', 'D', 'F', 'O'], ['terraced', 'semi-detached', 'detached', 'flat', 'other']) AS type,\n    b = 'Y' AS is_new,\n    transform(c, ['F', 'L', 'U'], ['freehold', 'leasehold', 'unknown']) AS duration,\n    addr1,\n    addr2,\n    street,\n    locality,\n    town,\n    district,\n    county\nFROM url(\n    'http://prod.publicdata.landregistry.gov.uk.s3-website-eu-west-1.amazonaws.com/pp-complete.csv',\n    'CSV',\n    'uuid_string String,\n    price_string String,\n    time String,\n    postcode String,\n    a String,\n    b String,\n    c String,\n    addr1 String,\n    addr2 String,\n    street String,\n    locality String,\n    town String,\n    district String,\n    county String,\n    d String,\n    e String'\n) SETTINGS max_http_get_redirects=10;\n\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"extends\": \"./.config/tsconfig.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \".\"\n  }\n}\n"
  }
]