@databricks/appkit 0.61.1 → 0.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -0
- package/dist/agents/databricks.d.ts.map +1 -1
- package/dist/agents/databricks.js.map +1 -1
- package/dist/agents/supervisor-api.d.ts.map +1 -1
- package/dist/agents/supervisor-api.js.map +1 -1
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js.map +1 -1
- package/dist/appkit/package.js +1 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/storage/memory.js.map +1 -1
- package/dist/cache/storage/persistent.js.map +1 -1
- package/dist/cli/commands/codemod/index.js.map +1 -1
- package/dist/cli/commands/codemod/on-plugins-ready.js.map +1 -1
- package/dist/cli/commands/docs.js.map +1 -1
- package/dist/cli/commands/doctor/bundle.js.map +1 -1
- package/dist/cli/commands/doctor/index.js.map +1 -1
- package/dist/cli/commands/doctor/report.js.map +1 -1
- package/dist/cli/commands/doctor/resolve-targets.js.map +1 -1
- package/dist/cli/commands/doctor/run.js.map +1 -1
- package/dist/cli/commands/generate-types.js.map +1 -1
- package/dist/cli/commands/lint.js.map +1 -1
- package/dist/cli/commands/plugin/add-resource/add-resource.js.map +1 -1
- package/dist/cli/commands/plugin/create/create.js.map +1 -1
- package/dist/cli/commands/plugin/create/prompt-resource.js.map +1 -1
- package/dist/cli/commands/plugin/create/scaffold.js.map +1 -1
- package/dist/cli/commands/plugin/index.js.map +1 -1
- package/dist/cli/commands/plugin/list/list.js.map +1 -1
- package/dist/cli/commands/plugin/promote/promote.js.map +1 -1
- package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
- package/dist/cli/commands/plugin/validate/validate-manifest.js.map +1 -1
- package/dist/cli/commands/plugin/validate/validate.js.map +1 -1
- package/dist/cli/commands/registry/add.js.map +1 -1
- package/dist/cli/commands/registry/client.js.map +1 -1
- package/dist/cli/commands/registry/config-writer.js.map +1 -1
- package/dist/cli/commands/registry/env-reconcile.js.map +1 -1
- package/dist/cli/commands/registry/env-writer.js.map +1 -1
- package/dist/cli/commands/registry/index.js.map +1 -1
- package/dist/cli/commands/registry/info.js.map +1 -1
- package/dist/cli/commands/registry/list.js.map +1 -1
- package/dist/cli/commands/registry/requirements.js.map +1 -1
- package/dist/cli/commands/registry/server-register.js.map +1 -1
- package/dist/cli/commands/registry/workspace-picker.js.map +1 -1
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/connectors/files/client.js.map +1 -1
- package/dist/connectors/lakebase/index.d.ts.map +1 -1
- package/dist/connectors/lakebase/index.js.map +1 -1
- package/dist/connectors/lakebase/pool-manager.d.ts.map +1 -1
- package/dist/connectors/lakebase/pool-manager.js.map +1 -1
- package/dist/connectors/lakebase/routing-pool.d.ts.map +1 -1
- package/dist/connectors/lakebase/routing-pool.js.map +1 -1
- package/dist/connectors/mcp/client.d.ts.map +1 -1
- package/dist/connectors/mcp/client.js.map +1 -1
- package/dist/connectors/sql-warehouse/client.js.map +1 -1
- package/dist/context/client-options.js.map +1 -1
- package/dist/context/execution-context.d.ts.map +1 -1
- package/dist/context/execution-context.js.map +1 -1
- package/dist/context/index.d.ts +1 -1
- package/dist/context/service-context.d.ts +55 -3
- package/dist/context/service-context.d.ts.map +1 -1
- package/dist/context/service-context.js.map +1 -1
- package/dist/core/agent/build-toolkit.js.map +1 -1
- package/dist/core/agent/load-agents.d.ts.map +1 -1
- package/dist/core/agent/load-agents.js.map +1 -1
- package/dist/core/agent/run-agent.d.ts.map +1 -1
- package/dist/core/agent/run-agent.js.map +1 -1
- package/dist/core/agent/toolkit-resolver.js.map +1 -1
- package/dist/core/agent/tools/define-tool.d.ts.map +1 -1
- package/dist/core/agent/tools/define-tool.js.map +1 -1
- package/dist/core/agent/tools/tool.d.ts.map +1 -1
- package/dist/core/agent/tools/tool.js.map +1 -1
- package/dist/core/agent/types.d.ts.map +1 -1
- package/dist/core/agent/types.js.map +1 -1
- package/dist/core/appkit.d.ts.map +1 -1
- package/dist/core/appkit.js.map +1 -1
- package/dist/core/lifecycle-manager.js.map +1 -1
- package/dist/core/plugin-context.d.ts +13 -0
- package/dist/core/plugin-context.d.ts.map +1 -1
- package/dist/core/plugin-context.js +12 -1
- package/dist/core/plugin-context.js.map +1 -1
- package/dist/errors/configuration.d.ts.map +1 -1
- package/dist/errors/configuration.js.map +1 -1
- package/dist/logging/logger.js.map +1 -1
- package/dist/logging/wide-event-emitter.js.map +1 -1
- package/dist/plugin/dev-reader.d.ts.map +1 -1
- package/dist/plugin/dev-reader.js.map +1 -1
- package/dist/plugin/interceptors/cache.js.map +1 -1
- package/dist/plugin/interceptors/retry.js.map +1 -1
- package/dist/plugin/interceptors/telemetry.js.map +1 -1
- package/dist/plugin/plugin.d.ts +2 -2
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/plugins/agents/agents.d.ts.map +1 -1
- package/dist/plugins/agents/agents.js.map +1 -1
- package/dist/plugins/agents/event-translator.js.map +1 -1
- package/dist/plugins/agents/thread-store.js.map +1 -1
- package/dist/plugins/ai-search/ai-search.d.ts.map +1 -1
- package/dist/plugins/ai-search/ai-search.js.map +1 -1
- package/dist/plugins/analytics/analytics.d.ts.map +1 -1
- package/dist/plugins/analytics/analytics.js.map +1 -1
- package/dist/plugins/analytics/mv/cache.js.map +1 -1
- package/dist/plugins/analytics/mv/constants.js.map +1 -1
- package/dist/plugins/analytics/mv/formatters.js.map +1 -1
- package/dist/plugins/analytics/mv/metadata.js.map +1 -1
- package/dist/plugins/analytics/mv/registry.js.map +1 -1
- package/dist/plugins/analytics/mv/schemas.js.map +1 -1
- package/dist/plugins/analytics/query.js.map +1 -1
- package/dist/plugins/analytics/result-delivery.js.map +1 -1
- package/dist/plugins/files/helpers.js.map +1 -1
- package/dist/plugins/files/plugin.d.ts.map +1 -1
- package/dist/plugins/files/plugin.js.map +1 -1
- package/dist/plugins/files/types.d.ts.map +1 -1
- package/dist/plugins/genie/genie.d.ts.map +1 -1
- package/dist/plugins/genie/genie.js.map +1 -1
- package/dist/plugins/jobs/plugin.d.ts.map +1 -1
- package/dist/plugins/jobs/plugin.js.map +1 -1
- package/dist/plugins/jobs/types.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.js.map +1 -1
- package/dist/plugins/lakebase/types.d.ts.map +1 -1
- package/dist/plugins/server/base-server.js.map +1 -1
- package/dist/plugins/server/client-config-sanitizer.js.map +1 -1
- package/dist/plugins/server/index.d.ts.map +1 -1
- package/dist/plugins/server/index.js.map +1 -1
- package/dist/plugins/server/react-source-loc-vite-plugin.js.map +1 -1
- package/dist/plugins/server/remote-tunnel/remote-tunnel-controller.js.map +1 -1
- package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
- package/dist/plugins/server/static-server.js.map +1 -1
- package/dist/plugins/server/utils.js.map +1 -1
- package/dist/plugins/server/vite-dev-server.js.map +1 -1
- package/dist/plugins/serving/schema-filter.js.map +1 -1
- package/dist/plugins/serving/serving.d.ts.map +1 -1
- package/dist/plugins/serving/serving.js.map +1 -1
- package/dist/plugins/ui-variants/index.js.map +1 -1
- package/dist/registry/manifest-loader.d.ts.map +1 -1
- package/dist/registry/manifest-loader.js.map +1 -1
- package/dist/registry/resource-registry.d.ts.map +1 -1
- package/dist/registry/resource-registry.js.map +1 -1
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry/types.js.map +1 -1
- package/dist/schemas/manifest.d.ts.map +1 -1
- package/dist/schemas/manifest.js.map +1 -1
- package/dist/schemas/metric-fqn.js.map +1 -1
- package/dist/shared/src/plugin.d.ts.map +1 -1
- package/dist/shared/src/schemas/manifest.d.ts.map +1 -1
- package/dist/shared/src/schemas/metric-fqn.js.map +1 -1
- package/dist/shared/src/schemas/metric-metadata-bundle.js.map +1 -1
- package/dist/shared/src/schemas/metric-source.js.map +1 -1
- package/dist/shared/src/sse/analytics.js.map +1 -1
- package/dist/shared/src/workspace-client/legacy.js.map +1 -1
- package/dist/shared/src/workspace-client/types.d.ts.map +1 -1
- package/dist/stream/sse-writer.js.map +1 -1
- package/dist/stream/stream-manager.d.ts.map +1 -1
- package/dist/stream/stream-manager.js.map +1 -1
- package/dist/stream/types.js.map +1 -1
- package/dist/telemetry/instrumentations.js.map +1 -1
- package/dist/telemetry/telemetry-manager.js.map +1 -1
- package/dist/telemetry/telemetry-provider.js.map +1 -1
- package/dist/telemetry/trace-sampler.js.map +1 -1
- package/dist/testing/expect-stream.d.ts +111 -0
- package/dist/testing/expect-stream.d.ts.map +1 -0
- package/dist/testing/expect-stream.js +154 -0
- package/dist/testing/expect-stream.js.map +1 -0
- package/dist/testing/fixtures.d.ts +257 -0
- package/dist/testing/fixtures.d.ts.map +1 -0
- package/dist/testing/fixtures.js +385 -0
- package/dist/testing/fixtures.js.map +1 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/test-plugin-context.d.ts +158 -0
- package/dist/testing/test-plugin-context.d.ts.map +1 -0
- package/dist/testing/test-plugin-context.js +139 -0
- package/dist/testing/test-plugin-context.js.map +1 -0
- package/dist/type-generator/cache.js.map +1 -1
- package/dist/type-generator/index.js.map +1 -1
- package/dist/type-generator/migration.js.map +1 -1
- package/dist/type-generator/mv-registry/config.js.map +1 -1
- package/dist/type-generator/query-registry.js.map +1 -1
- package/dist/type-generator/serving/cache.js.map +1 -1
- package/dist/type-generator/serving/generator.js.map +1 -1
- package/dist/type-generator/serving/server-file-extractor.d.ts.map +1 -1
- package/dist/type-generator/serving/server-file-extractor.js.map +1 -1
- package/dist/type-generator/serving/vite-plugin.d.ts.map +1 -1
- package/dist/type-generator/serving/vite-plugin.js.map +1 -1
- package/dist/type-generator/vite-plugin.d.ts.map +1 -1
- package/dist/type-generator/vite-plugin.js.map +1 -1
- package/dist/workspace-client/legacy.js.map +1 -1
- package/dist/workspace-client/types.d.ts.map +1 -1
- package/docs/plugins/testing.md +245 -0
- package/llms.txt +1 -0
- package/package.json +15 -2
- package/sbom.cdx.json +1 -1
- package/skills/appkit-ui-variants/SKILL.md +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.js","names":[],"sources":["../../../../src/plugins/analytics/mv/registry.ts"],"sourcesContent":["import path from \"node:path\";\n// Canonical metric-source schema — the single source of truth for\n// `config/metric-views/definitions.json`. Imported from the shared source\n// directly (matching the type-generator's runtime, which pulls the zod-free\n// `metric-fqn.ts` from the same tree) so the runtime and the generated JSON\n// schema validate identically.\nimport { metricSourceSchema } from \"../../../../../shared/src/schemas/metric-source\";\nimport type { AppManager, DevFileReader, RequestLike } from \"../../../app\";\nimport { createLogger } from \"../../../logging/logger\";\nimport type { MetricRegistration } from \"../types\";\nimport { laneFromExecutor, METRIC_CONFIG_FILE } from \"./constants\";\n\nconst logger = createLogger(\"analytics:metric-views\");\n\n/**\n * Read and validate `config/metric-views/definitions.json` into a metric registry.\n *\n * Async and stateless — registration is a pure config parse with no warehouse\n * round-trip, no `DESCRIBE`, and no build-time metadata bundle.\n *\n * The file is read **through {@link AppManager.readMetricViewsConfig}** rather\n * than `node:fs` directly, so this path is dev-tunnel-aware (a `?dev` request\n * reads the developer's local file over the WebSocket tunnel) and inherits the\n * traversal guard. In production that's a plain `fs.readFile` under the hood,\n * so the semantics below are unchanged.\n *\n * Absent file -> empty registry (`null` from `readMetricViewsConfig`).\n * Malformed file -> 503 (throws).\n *\n * @param app - The {@link AppManager} that resolves + reads the config file.\n * @param req - Optional request object, used to detect dev mode.\n * @param devFileReader - Optional dev tunnel reader.\n */\nexport async function loadMetricRegistry(\n app: AppManager,\n req?: RequestLike,\n devFileReader?: DevFileReader,\n): Promise<Record<string, MetricRegistration>> {\n const metricPath = path.join(app.metricViewsDir, METRIC_CONFIG_FILE);\n\n const raw = await app.readMetricViewsConfig(\n METRIC_CONFIG_FILE,\n req,\n devFileReader,\n );\n if (raw === null) {\n // Absent file (ENOENT in prod / dev-tunnel not-found) or a rejected\n // traversal path → dormant. Same as the old ENOENT branch.\n return Object.create(null);\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new Error(\n `Failed to parse definitions.json at ${metricPath}: ${(err as Error).message}`,\n );\n }\n\n const result = metricSourceSchema.safeParse(parsed);\n if (!result.success) {\n const issues = result.error.issues\n .map((i) => `${i.path.join(\".\")}: ${i.message}`)\n .join(\"; \");\n throw new Error(`Invalid definitions.json at ${metricPath}: ${issues}`);\n }\n\n // Null-prototype map so a metric key that collides with an inherited\n // `Object.prototype` member (`__proto__`, `constructor`, `toString`, …)\n // cannot resolve to a truthy non-registration at the `registry[key]` read\n // site and slip past the unknown-key 404.\n // Keys are still grammar-gated by `metricKeySchema` (identifier shape),\n // but the null prototype removes the whole class of inherited-property lookups as a boundary.\n const registry: Record<string, MetricRegistration> = Object.create(null);\n for (const [key, entry] of Object.entries(result.data.metricViews ?? {})) {\n registry[key] = {\n key,\n source: entry.source,\n lane: laneFromExecutor(entry.executor),\n };\n }\n\n logger.debug(\n \"Loaded metric registry: %d entry(ies)\",\n Object.keys(registry).length,\n );\n return registry;\n}\n"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"registry.js","names":[],"sources":["../../../../src/plugins/analytics/mv/registry.ts"],"sourcesContent":["import path from \"node:path\";\n\n// Canonical metric-source schema — the single source of truth for\n// `config/metric-views/definitions.json`. Imported from the shared source\n// directly (matching the type-generator's runtime, which pulls the zod-free\n// `metric-fqn.ts` from the same tree) so the runtime and the generated JSON\n// schema validate identically.\nimport { metricSourceSchema } from \"../../../../../shared/src/schemas/metric-source\";\nimport type { AppManager, DevFileReader, RequestLike } from \"../../../app\";\nimport { createLogger } from \"../../../logging/logger\";\nimport type { MetricRegistration } from \"../types\";\nimport { laneFromExecutor, METRIC_CONFIG_FILE } from \"./constants\";\n\nconst logger = createLogger(\"analytics:metric-views\");\n\n/**\n * Read and validate `config/metric-views/definitions.json` into a metric registry.\n *\n * Async and stateless — registration is a pure config parse with no warehouse\n * round-trip, no `DESCRIBE`, and no build-time metadata bundle.\n *\n * The file is read **through {@link AppManager.readMetricViewsConfig}** rather\n * than `node:fs` directly, so this path is dev-tunnel-aware (a `?dev` request\n * reads the developer's local file over the WebSocket tunnel) and inherits the\n * traversal guard. In production that's a plain `fs.readFile` under the hood,\n * so the semantics below are unchanged.\n *\n * Absent file -> empty registry (`null` from `readMetricViewsConfig`).\n * Malformed file -> 503 (throws).\n *\n * @param app - The {@link AppManager} that resolves + reads the config file.\n * @param req - Optional request object, used to detect dev mode.\n * @param devFileReader - Optional dev tunnel reader.\n */\nexport async function loadMetricRegistry(\n app: AppManager,\n req?: RequestLike,\n devFileReader?: DevFileReader,\n): Promise<Record<string, MetricRegistration>> {\n const metricPath = path.join(app.metricViewsDir, METRIC_CONFIG_FILE);\n\n const raw = await app.readMetricViewsConfig(\n METRIC_CONFIG_FILE,\n req,\n devFileReader,\n );\n if (raw === null) {\n // Absent file (ENOENT in prod / dev-tunnel not-found) or a rejected\n // traversal path → dormant. Same as the old ENOENT branch.\n return Object.create(null);\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new Error(\n `Failed to parse definitions.json at ${metricPath}: ${(err as Error).message}`,\n );\n }\n\n const result = metricSourceSchema.safeParse(parsed);\n if (!result.success) {\n const issues = result.error.issues\n .map((i) => `${i.path.join(\".\")}: ${i.message}`)\n .join(\"; \");\n throw new Error(`Invalid definitions.json at ${metricPath}: ${issues}`);\n }\n\n // Null-prototype map so a metric key that collides with an inherited\n // `Object.prototype` member (`__proto__`, `constructor`, `toString`, …)\n // cannot resolve to a truthy non-registration at the `registry[key]` read\n // site and slip past the unknown-key 404.\n // Keys are still grammar-gated by `metricKeySchema` (identifier shape),\n // but the null prototype removes the whole class of inherited-property lookups as a boundary.\n const registry: Record<string, MetricRegistration> = Object.create(null);\n for (const [key, entry] of Object.entries(result.data.metricViews ?? {})) {\n registry[key] = {\n key,\n source: entry.source,\n lane: laneFromExecutor(entry.executor),\n };\n }\n\n logger.debug(\n \"Loaded metric registry: %d entry(ies)\",\n Object.keys(registry).length,\n );\n return registry;\n}\n"],"mappings":";;;;;;;AAaA,MAAM,SAAS,aAAa,yBAAyB;;;;;;;;;;;;;;;;;;;;AAqBrD,eAAsB,mBACpB,KACA,KACA,eAC6C;CAC7C,MAAM,aAAa,KAAK,KAAK,IAAI,gBAAgB,mBAAmB;CAEpE,MAAM,MAAM,MAAM,IAAI,sBACpB,oBACA,KACA,cACD;AACD,KAAI,QAAQ,KAGV,QAAO,OAAO,OAAO,KAAK;CAG5B,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,IAAI;UACjB,KAAK;AACZ,QAAM,IAAI,MACR,uCAAuC,WAAW,IAAK,IAAc,UACtE;;CAGH,MAAM,SAAS,mBAAmB,UAAU,OAAO;AACnD,KAAI,CAAC,OAAO,SAAS;EACnB,MAAM,SAAS,OAAO,MAAM,OACzB,KAAK,MAAM,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC,IAAI,EAAE,UAAU,CAC/C,KAAK,KAAK;AACb,QAAM,IAAI,MAAM,+BAA+B,WAAW,IAAI,SAAS;;CASzE,MAAM,WAA+C,OAAO,OAAO,KAAK;AACxE,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,KAAK,eAAe,EAAE,CAAC,CACtE,UAAS,OAAO;EACd;EACA,QAAQ,MAAM;EACd,MAAM,iBAAiB,MAAM,SAAS;EACvC;AAGH,QAAO,MACL,yCACA,OAAO,KAAK,SAAS,CAAC,OACvB;AACD,QAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schemas.js","names":[],"sources":["../../../../src/plugins/analytics/mv/schemas.ts"],"sourcesContent":["import { z } from \"zod\";\nimport { isValidColumnName } from \"../../../../../shared/src/schemas/metric-fqn\";\nimport { ValidationError } from \"../../../errors\";\nimport type {\n IAnalyticsMetricRequest,\n MetricFilter,\n MetricFilterOperatorName,\n MetricPredicate,\n} from \"../types\";\nimport { normalizeAnalyticsFormat } from \"../types\";\nimport {\n LIST_VALUE_OPERATORS,\n METRIC_DIMENSIONS_MAX,\n METRIC_FILTER_GROUP_MAX,\n METRIC_FILTER_MAX_DEPTH,\n METRIC_FILTER_OPERATORS,\n METRIC_FILTER_VALUES_MAX,\n METRIC_LIMIT_MAX,\n METRIC_MEASURES_MAX,\n METRIC_ORDER_BY_MAX,\n METRIC_ORDER_DIRECTIONS,\n NULL_OPERATORS,\n SINGLE_VALUE_OPERATORS,\n STRING_OPERATORS,\n TIME_GRAIN_PATTERN,\n} from \"./constants\";\n\n/** A leaf predicate: `{ member, operator, values? }`, no extra keys. */\nconst filterPredicateSchema: z.ZodType<MetricPredicate> = z\n .object({\n member: z\n .string()\n .min(1, { message: \"filter predicate 'member' cannot be empty\" })\n .refine(isValidColumnName, {\n message:\n \"filter predicate 'member' contains a character that cannot be used in a SQL identifier (control character or newline)\",\n }),\n operator: z.string().min(1, {\n message: \"filter predicate 'operator' cannot be empty\",\n }) as z.ZodType<MetricFilterOperatorName>,\n values: z\n .array(z.union([z.string(), z.number()]))\n .max(METRIC_FILTER_VALUES_MAX, {\n message: `filter predicate 'values' length exceeds the maximum of ${METRIC_FILTER_VALUES_MAX}`,\n })\n .optional(),\n })\n .strict();\n\n/** Recursive filter: a predicate leaf or an `{ and }` / `{ or }` group. */\nconst filterSchema: z.ZodType<MetricFilter> = z.lazy(() =>\n z.union([\n filterPredicateSchema,\n z\n .object({\n and: z.array(filterSchema).max(METRIC_FILTER_GROUP_MAX, {\n message: `filter 'and' group exceeds the maximum of ${METRIC_FILTER_GROUP_MAX} children`,\n }),\n })\n .strict(),\n z\n .object({\n or: z.array(filterSchema).max(METRIC_FILTER_GROUP_MAX, {\n message: `filter 'or' group exceeds the maximum of ${METRIC_FILTER_GROUP_MAX} children`,\n }),\n })\n .strict(),\n ]),\n);\n\nconst metricRequestSchema = z\n .object({\n measures: z\n .array(\n z\n .string()\n .min(1, \"measure name cannot be empty\")\n .refine(isValidColumnName, {\n message:\n \"measure name contains a character that cannot be used in a SQL identifier (control character or newline)\",\n }),\n )\n .min(1, \"at least one measure is required\")\n .max(METRIC_MEASURES_MAX, {\n message: `measures length exceeds the maximum of ${METRIC_MEASURES_MAX}`,\n }),\n dimensions: z\n .array(\n z\n .string()\n .min(1, \"dimension name cannot be empty\")\n .refine(isValidColumnName, {\n message:\n \"dimension name contains a character that cannot be used in a SQL identifier (control character or newline)\",\n }),\n )\n .max(METRIC_DIMENSIONS_MAX, {\n message: `dimensions length exceeds the maximum of ${METRIC_DIMENSIONS_MAX}`,\n })\n .optional(),\n filter: filterSchema.optional(),\n // Grammar-shaped bucketing grain, applied to `timeDimension` via\n // `date_trunc`. The token is validated for safety here; the grain literal\n // is interpolated (single-quoted, never a bind param) in\n // `renderDimensionClause`, so this pattern gate is the security boundary.\n timeGrain: z\n .string()\n .min(1, { message: \"timeGrain cannot be empty\" })\n .regex(TIME_GRAIN_PATTERN, {\n message: \"timeGrain must match /^[a-z][a-z_]*$/\",\n })\n .optional(),\n // The single dimension `timeGrain` applies to via `date_trunc`. A column\n // identifier (backtick-quoted at interpolation), so it accepts the full\n // delimited-identifier grammar. Cross-field rules in `superRefine`:\n // required when `timeGrain` is set, and must be one of `dimensions`.\n timeDimension: z\n .string()\n .min(1, { message: \"timeDimension cannot be empty\" })\n .refine(isValidColumnName, {\n message:\n \"timeDimension contains a character that cannot be used in a SQL identifier (control character or newline)\",\n })\n .optional(),\n orderBy: z\n .array(\n z\n .object({\n field: z\n .string()\n .min(1, \"orderBy field cannot be empty\")\n .refine(isValidColumnName, {\n message:\n \"orderBy field contains a character that cannot be used in a SQL identifier (control character or newline)\",\n }),\n direction: z.enum(METRIC_ORDER_DIRECTIONS).optional(),\n })\n .strict(),\n )\n .min(1, \"orderBy cannot be an empty array\")\n .max(METRIC_ORDER_BY_MAX, {\n message: `orderBy length exceeds the maximum of ${METRIC_ORDER_BY_MAX}`,\n })\n .optional(),\n limit: z\n .number()\n .int({ message: \"limit must be an integer\" })\n .positive({ message: \"limit must be positive\" })\n .max(METRIC_LIMIT_MAX, {\n message: `limit exceeds the maximum of ${METRIC_LIMIT_MAX}`,\n })\n .optional(),\n format: z.enum([\"JSON_ARRAY\", \"ARROW_STREAM\", \"JSON\", \"ARROW\"]).optional(),\n })\n .strict()\n .superRefine((value, ctx) => {\n if (value.filter != null) {\n validateFilterTree(value.filter, ctx, [\"filter\"], 0);\n }\n\n const seen = new Set<string>();\n const collided = new Set<string>();\n for (const name of [...value.measures, ...(value.dimensions ?? [])]) {\n if (seen.has(name)) {\n collided.add(name);\n }\n seen.add(name);\n }\n if (collided.size > 0) {\n ctx.addIssue({\n code: \"custom\",\n message:\n \"measures and dimensions must be unique across both lists (a name cannot repeat, nor appear as both a measure and a dimension)\",\n path: [\"measures\"],\n });\n }\n\n if (\n value.format != null &&\n normalizeAnalyticsFormat(value.format) !== \"JSON_ARRAY\"\n ) {\n ctx.addIssue({\n code: \"custom\",\n message:\n \"format: only JSON_ARRAY is supported on the metric route at v1 (ARROW_STREAM is not yet implemented)\",\n path: [\"format\"],\n });\n }\n\n if (value.timeGrain != null && value.timeDimension == null) {\n ctx.addIssue({\n code: \"custom\",\n message: \"timeDimension is required when timeGrain is set\",\n path: [\"timeDimension\"],\n });\n }\n if (\n value.timeDimension != null &&\n !(value.dimensions ?? []).includes(value.timeDimension)\n ) {\n ctx.addIssue({\n code: \"custom\",\n message: \"timeDimension must be one of dimensions\",\n path: [\"timeDimension\"],\n });\n }\n\n if (value.orderBy != null) {\n const selectedNames = new Set([\n ...value.measures,\n ...(value.dimensions ?? []),\n ]);\n\n for (let i = 0; i < value.orderBy.length; i++) {\n if (!selectedNames.has(value.orderBy[i].field)) {\n ctx.addIssue({\n code: \"custom\",\n message: \"orderBy field must be one of measures or dimensions\",\n path: [\"orderBy\", i, \"field\"],\n });\n }\n }\n\n const seenFields = new Set<string>();\n let hasDuplicate = false;\n for (const entry of value.orderBy) {\n if (seenFields.has(entry.field)) {\n hasDuplicate = true;\n break;\n }\n seenFields.add(entry.field);\n }\n if (hasDuplicate) {\n ctx.addIssue({\n code: \"custom\",\n message: \"orderBy fields must be unique\",\n path: [\"orderBy\"],\n });\n }\n }\n }) as z.ZodType<IAnalyticsMetricRequest>;\n\nfunction validateFilterTree(\n node: MetricFilter,\n ctx: z.RefinementCtx,\n path: Array<string | number>,\n depth: number,\n): void {\n if (node === null || typeof node !== \"object\") {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path,\n message: \"filter node must be a Predicate or { and } / { or } group\",\n });\n return;\n }\n\n if (\"and\" in node || \"or\" in node) {\n if (depth + 1 > METRIC_FILTER_MAX_DEPTH) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path,\n message: `filter AND/OR nesting exceeds the maximum depth of ${METRIC_FILTER_MAX_DEPTH}`,\n });\n return;\n }\n\n const groupKey = \"and\" in node ? \"and\" : \"or\";\n const children = (\n node as { and?: ReadonlyArray<MetricFilter> } & {\n or?: ReadonlyArray<MetricFilter>;\n }\n )[groupKey];\n\n if (!Array.isArray(children)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [...path, groupKey],\n message: `filter ${groupKey} group must be an array of predicates or nested groups`,\n });\n return;\n }\n\n if (children.length === 0) {\n // Reject empty groups of either kind. An empty `or` is vacuously false;\n // an empty `and` contributes no constraint and renders to no WHERE\n // clause — identical SQL to omitting `filter` entirely, but it would\n // canonicalize to a distinct cache key (`and()` vs `_`), needlessly\n // splitting the cache across semantically identical requests. Requiring\n // at least one child keeps request shape ↔ cache key one-to-one.\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [...path, groupKey],\n message: `filter '${groupKey}' group must contain at least one predicate`,\n });\n return;\n }\n\n children.forEach((child, idx) => {\n validateFilterTree(child, ctx, [...path, groupKey, idx], depth + 1);\n });\n return;\n }\n\n const predicate = node as MetricPredicate;\n\n if (\n !METRIC_FILTER_OPERATORS.includes(\n predicate.operator as MetricFilterOperatorName,\n )\n ) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [...path, \"operator\"],\n message: `filter operator \"${predicate.operator}\" is not one of: ${METRIC_FILTER_OPERATORS.join(\", \")}`,\n });\n return;\n }\n\n const op = predicate.operator;\n const values = predicate.values;\n const valuesLen = values?.length ?? 0;\n\n if (NULL_OPERATORS.has(op)) {\n if (values != null && valuesLen > 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [...path, \"values\"],\n message: `filter operator \"${op}\" must not carry values`,\n });\n }\n } else if (SINGLE_VALUE_OPERATORS.has(op)) {\n if (valuesLen !== 1) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [...path, \"values\"],\n message: `filter operator \"${op}\" requires exactly one value (got ${valuesLen})`,\n });\n }\n } else if (LIST_VALUE_OPERATORS.has(op)) {\n if (valuesLen < 1) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [...path, \"values\"],\n message: `filter operator \"${op}\" requires at least one value`,\n });\n }\n }\n\n if (STRING_OPERATORS.has(op) && valuesLen > 0) {\n const v = predicate.values?.[0];\n if (typeof v !== \"string\") {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [...path, \"values\"],\n message: `filter operator \"${op}\" requires a string value (got ${typeof v})`,\n });\n }\n }\n}\n\nfunction preCheckFilterDepth(filter: unknown): void {\n if (filter == null || typeof filter !== \"object\") return;\n const stack: Array<[unknown, number]> = [[filter, 0]];\n while (stack.length > 0) {\n const popped = stack.pop();\n if (popped === undefined) continue;\n const [node, depth] = popped;\n if (node == null || typeof node !== \"object\") continue;\n const obj = node as Record<string, unknown>;\n for (const groupKey of [\"and\", \"or\"] as const) {\n const children = obj[groupKey];\n if (!Array.isArray(children)) continue;\n if (children.length > METRIC_FILTER_GROUP_MAX) {\n throw new ValidationError(\n \"Invalid metric request body (fields: filter)\",\n {\n context: {\n reason: `filter ${groupKey} group has ${children.length} children; the maximum is ${METRIC_FILTER_GROUP_MAX}`,\n },\n },\n );\n }\n if (depth + 1 > METRIC_FILTER_MAX_DEPTH) {\n throw new ValidationError(\n \"Invalid metric request body (fields: filter)\",\n {\n context: {\n reason: `filter AND/OR nesting exceeds the maximum depth of ${METRIC_FILTER_MAX_DEPTH}`,\n },\n },\n );\n }\n for (const child of children) {\n stack.push([child, depth + 1]);\n }\n }\n }\n}\n\nexport function validateMetricRequest(body: unknown): IAnalyticsMetricRequest {\n if (body != null && typeof body === \"object\") {\n preCheckFilterDepth((body as { filter?: unknown }).filter);\n }\n const result = metricRequestSchema.safeParse(body);\n if (!result.success) {\n const fieldPaths = result.error.issues\n .map((i) => i.path.join(\".\") || \"(root)\")\n .join(\", \");\n throw new ValidationError(\n fieldPaths.length > 0\n ? `Invalid metric request body (fields: ${fieldPaths})`\n : \"Invalid metric request body\",\n { context: { issues: result.error.issues } },\n );\n }\n return result.data;\n}\n"],"mappings":";;;;;;;;;AA4BA,MAAM,wBAAoD,EACvD,OAAO;CACN,QAAQ,EACL,QAAQ,CACR,IAAI,GAAG,EAAE,SAAS,6CAA6C,CAAC,CAChE,OAAO,mBAAmB,EACzB,SACE,yHACH,CAAC;CACJ,UAAU,EAAE,QAAQ,CAAC,IAAI,GAAG,EAC1B,SAAS,+CACV,CAAC;CACF,QAAQ,EACL,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,QAAQ,CAAC,CAAC,CAAC,CACxC,IAAI,0BAA0B,EAC7B,SAAS,2DAA2D,4BACrE,CAAC,CACD,UAAU;CACd,CAAC,CACD,QAAQ;;AAGX,MAAM,eAAwC,EAAE,WAC9C,EAAE,MAAM;CACN;CACA,EACG,OAAO,EACN,KAAK,EAAE,MAAM,aAAa,CAAC,IAAI,yBAAyB,EACtD,SAAS,6CAA6C,wBAAwB,YAC/E,CAAC,EACH,CAAC,CACD,QAAQ;CACX,EACG,OAAO,EACN,IAAI,EAAE,MAAM,aAAa,CAAC,IAAI,yBAAyB,EACrD,SAAS,4CAA4C,wBAAwB,YAC9E,CAAC,EACH,CAAC,CACD,QAAQ;CACZ,CAAC,CACH;AAED,MAAM,sBAAsB,EACzB,OAAO;CACN,UAAU,EACP,MACC,EACG,QAAQ,CACR,IAAI,GAAG,+BAA+B,CACtC,OAAO,mBAAmB,EACzB,SACE,4GACH,CAAC,CACL,CACA,IAAI,GAAG,mCAAmC,CAC1C,IAAI,qBAAqB,EACxB,SAAS,0CAA0C,uBACpD,CAAC;CACJ,YAAY,EACT,MACC,EACG,QAAQ,CACR,IAAI,GAAG,iCAAiC,CACxC,OAAO,mBAAmB,EACzB,SACE,8GACH,CAAC,CACL,CACA,IAAI,uBAAuB,EAC1B,SAAS,4CAA4C,yBACtD,CAAC,CACD,UAAU;CACb,QAAQ,aAAa,UAAU;CAK/B,WAAW,EACR,QAAQ,CACR,IAAI,GAAG,EAAE,SAAS,6BAA6B,CAAC,CAChD,MAAM,oBAAoB,EACzB,SAAS,yCACV,CAAC,CACD,UAAU;CAKb,eAAe,EACZ,QAAQ,CACR,IAAI,GAAG,EAAE,SAAS,iCAAiC,CAAC,CACpD,OAAO,mBAAmB,EACzB,SACE,6GACH,CAAC,CACD,UAAU;CACb,SAAS,EACN,MACC,EACG,OAAO;EACN,OAAO,EACJ,QAAQ,CACR,IAAI,GAAG,gCAAgC,CACvC,OAAO,mBAAmB,EACzB,SACE,6GACH,CAAC;EACJ,WAAW,EAAE,KAAK,wBAAwB,CAAC,UAAU;EACtD,CAAC,CACD,QAAQ,CACZ,CACA,IAAI,GAAG,mCAAmC,CAC1C,IAAI,qBAAqB,EACxB,SAAS,yCAAyC,uBACnD,CAAC,CACD,UAAU;CACb,OAAO,EACJ,QAAQ,CACR,IAAI,EAAE,SAAS,4BAA4B,CAAC,CAC5C,SAAS,EAAE,SAAS,0BAA0B,CAAC,CAC/C,IAAI,kBAAkB,EACrB,SAAS,gCAAgC,oBAC1C,CAAC,CACD,UAAU;CACb,QAAQ,EAAE,KAAK;EAAC;EAAc;EAAgB;EAAQ;EAAQ,CAAC,CAAC,UAAU;CAC3E,CAAC,CACD,QAAQ,CACR,aAAa,OAAO,QAAQ;AAC3B,KAAI,MAAM,UAAU,KAClB,oBAAmB,MAAM,QAAQ,KAAK,CAAC,SAAS,EAAE,EAAE;CAGtD,MAAM,uBAAO,IAAI,KAAa;CAC9B,MAAM,2BAAW,IAAI,KAAa;AAClC,MAAK,MAAM,QAAQ,CAAC,GAAG,MAAM,UAAU,GAAI,MAAM,cAAc,EAAE,CAAE,EAAE;AACnE,MAAI,KAAK,IAAI,KAAK,CAChB,UAAS,IAAI,KAAK;AAEpB,OAAK,IAAI,KAAK;;AAEhB,KAAI,SAAS,OAAO,EAClB,KAAI,SAAS;EACX,MAAM;EACN,SACE;EACF,MAAM,CAAC,WAAW;EACnB,CAAC;AAGJ,KACE,MAAM,UAAU,QAChB,yBAAyB,MAAM,OAAO,KAAK,aAE3C,KAAI,SAAS;EACX,MAAM;EACN,SACE;EACF,MAAM,CAAC,SAAS;EACjB,CAAC;AAGJ,KAAI,MAAM,aAAa,QAAQ,MAAM,iBAAiB,KACpD,KAAI,SAAS;EACX,MAAM;EACN,SAAS;EACT,MAAM,CAAC,gBAAgB;EACxB,CAAC;AAEJ,KACE,MAAM,iBAAiB,QACvB,EAAE,MAAM,cAAc,EAAE,EAAE,SAAS,MAAM,cAAc,CAEvD,KAAI,SAAS;EACX,MAAM;EACN,SAAS;EACT,MAAM,CAAC,gBAAgB;EACxB,CAAC;AAGJ,KAAI,MAAM,WAAW,MAAM;EACzB,MAAM,gBAAgB,IAAI,IAAI,CAC5B,GAAG,MAAM,UACT,GAAI,MAAM,cAAc,EAAE,CAC3B,CAAC;AAEF,OAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,QAAQ,IACxC,KAAI,CAAC,cAAc,IAAI,MAAM,QAAQ,GAAG,MAAM,CAC5C,KAAI,SAAS;GACX,MAAM;GACN,SAAS;GACT,MAAM;IAAC;IAAW;IAAG;IAAQ;GAC9B,CAAC;EAIN,MAAM,6BAAa,IAAI,KAAa;EACpC,IAAI,eAAe;AACnB,OAAK,MAAM,SAAS,MAAM,SAAS;AACjC,OAAI,WAAW,IAAI,MAAM,MAAM,EAAE;AAC/B,mBAAe;AACf;;AAEF,cAAW,IAAI,MAAM,MAAM;;AAE7B,MAAI,aACF,KAAI,SAAS;GACX,MAAM;GACN,SAAS;GACT,MAAM,CAAC,UAAU;GAClB,CAAC;;EAGN;AAEJ,SAAS,mBACP,MACA,KACA,MACA,OACM;AACN,KAAI,SAAS,QAAQ,OAAO,SAAS,UAAU;AAC7C,MAAI,SAAS;GACX,MAAM,EAAE,aAAa;GACrB;GACA,SAAS;GACV,CAAC;AACF;;AAGF,KAAI,SAAS,QAAQ,QAAQ,MAAM;AACjC,MAAI,QAAQ,IAAI,yBAAyB;AACvC,OAAI,SAAS;IACX,MAAM,EAAE,aAAa;IACrB;IACA,SAAS,sDAAsD;IAChE,CAAC;AACF;;EAGF,MAAM,WAAW,SAAS,OAAO,QAAQ;EACzC,MAAM,WACJ,KAGA;AAEF,MAAI,CAAC,MAAM,QAAQ,SAAS,EAAE;AAC5B,OAAI,SAAS;IACX,MAAM,EAAE,aAAa;IACrB,MAAM,CAAC,GAAG,MAAM,SAAS;IACzB,SAAS,UAAU,SAAS;IAC7B,CAAC;AACF;;AAGF,MAAI,SAAS,WAAW,GAAG;AAOzB,OAAI,SAAS;IACX,MAAM,EAAE,aAAa;IACrB,MAAM,CAAC,GAAG,MAAM,SAAS;IACzB,SAAS,WAAW,SAAS;IAC9B,CAAC;AACF;;AAGF,WAAS,SAAS,OAAO,QAAQ;AAC/B,sBAAmB,OAAO,KAAK;IAAC,GAAG;IAAM;IAAU;IAAI,EAAE,QAAQ,EAAE;IACnE;AACF;;CAGF,MAAM,YAAY;AAElB,KACE,CAAC,wBAAwB,SACvB,UAAU,SACX,EACD;AACA,MAAI,SAAS;GACX,MAAM,EAAE,aAAa;GACrB,MAAM,CAAC,GAAG,MAAM,WAAW;GAC3B,SAAS,oBAAoB,UAAU,SAAS,mBAAmB,wBAAwB,KAAK,KAAK;GACtG,CAAC;AACF;;CAGF,MAAM,KAAK,UAAU;CACrB,MAAM,SAAS,UAAU;CACzB,MAAM,YAAY,QAAQ,UAAU;AAEpC,KAAI,eAAe,IAAI,GAAG,EACxB;MAAI,UAAU,QAAQ,YAAY,EAChC,KAAI,SAAS;GACX,MAAM,EAAE,aAAa;GACrB,MAAM,CAAC,GAAG,MAAM,SAAS;GACzB,SAAS,oBAAoB,GAAG;GACjC,CAAC;YAEK,uBAAuB,IAAI,GAAG,EACvC;MAAI,cAAc,EAChB,KAAI,SAAS;GACX,MAAM,EAAE,aAAa;GACrB,MAAM,CAAC,GAAG,MAAM,SAAS;GACzB,SAAS,oBAAoB,GAAG,oCAAoC,UAAU;GAC/E,CAAC;YAEK,qBAAqB,IAAI,GAAG,EACrC;MAAI,YAAY,EACd,KAAI,SAAS;GACX,MAAM,EAAE,aAAa;GACrB,MAAM,CAAC,GAAG,MAAM,SAAS;GACzB,SAAS,oBAAoB,GAAG;GACjC,CAAC;;AAIN,KAAI,iBAAiB,IAAI,GAAG,IAAI,YAAY,GAAG;EAC7C,MAAM,IAAI,UAAU,SAAS;AAC7B,MAAI,OAAO,MAAM,SACf,KAAI,SAAS;GACX,MAAM,EAAE,aAAa;GACrB,MAAM,CAAC,GAAG,MAAM,SAAS;GACzB,SAAS,oBAAoB,GAAG,iCAAiC,OAAO,EAAE;GAC3E,CAAC;;;AAKR,SAAS,oBAAoB,QAAuB;AAClD,KAAI,UAAU,QAAQ,OAAO,WAAW,SAAU;CAClD,MAAM,QAAkC,CAAC,CAAC,QAAQ,EAAE,CAAC;AACrD,QAAO,MAAM,SAAS,GAAG;EACvB,MAAM,SAAS,MAAM,KAAK;AAC1B,MAAI,WAAW,OAAW;EAC1B,MAAM,CAAC,MAAM,SAAS;AACtB,MAAI,QAAQ,QAAQ,OAAO,SAAS,SAAU;EAC9C,MAAM,MAAM;AACZ,OAAK,MAAM,YAAY,CAAC,OAAO,KAAK,EAAW;GAC7C,MAAM,WAAW,IAAI;AACrB,OAAI,CAAC,MAAM,QAAQ,SAAS,CAAE;AAC9B,OAAI,SAAS,SAAS,wBACpB,OAAM,IAAI,gBACR,gDACA,EACE,SAAS,EACP,QAAQ,UAAU,SAAS,aAAa,SAAS,OAAO,4BAA4B,2BACrF,EACF,CACF;AAEH,OAAI,QAAQ,IAAI,wBACd,OAAM,IAAI,gBACR,gDACA,EACE,SAAS,EACP,QAAQ,sDAAsD,2BAC/D,EACF,CACF;AAEH,QAAK,MAAM,SAAS,SAClB,OAAM,KAAK,CAAC,OAAO,QAAQ,EAAE,CAAC;;;;AAMtC,SAAgB,sBAAsB,MAAwC;AAC5E,KAAI,QAAQ,QAAQ,OAAO,SAAS,SAClC,qBAAqB,KAA8B,OAAO;CAE5D,MAAM,SAAS,oBAAoB,UAAU,KAAK;AAClD,KAAI,CAAC,OAAO,SAAS;EACnB,MAAM,aAAa,OAAO,MAAM,OAC7B,KAAK,MAAM,EAAE,KAAK,KAAK,IAAI,IAAI,SAAS,CACxC,KAAK,KAAK;AACb,QAAM,IAAI,gBACR,WAAW,SAAS,IAChB,wCAAwC,WAAW,KACnD,+BACJ,EAAE,SAAS,EAAE,QAAQ,OAAO,MAAM,QAAQ,EAAE,CAC7C;;AAEH,QAAO,OAAO"}
|
|
1
|
+
{"version":3,"file":"schemas.js","names":[],"sources":["../../../../src/plugins/analytics/mv/schemas.ts"],"sourcesContent":["import { z } from \"zod\";\n\nimport { isValidColumnName } from \"../../../../../shared/src/schemas/metric-fqn\";\nimport { ValidationError } from \"../../../errors\";\nimport type {\n IAnalyticsMetricRequest,\n MetricFilter,\n MetricFilterOperatorName,\n MetricPredicate,\n} from \"../types\";\nimport { normalizeAnalyticsFormat } from \"../types\";\nimport {\n LIST_VALUE_OPERATORS,\n METRIC_DIMENSIONS_MAX,\n METRIC_FILTER_GROUP_MAX,\n METRIC_FILTER_MAX_DEPTH,\n METRIC_FILTER_OPERATORS,\n METRIC_FILTER_VALUES_MAX,\n METRIC_LIMIT_MAX,\n METRIC_MEASURES_MAX,\n METRIC_ORDER_BY_MAX,\n METRIC_ORDER_DIRECTIONS,\n NULL_OPERATORS,\n SINGLE_VALUE_OPERATORS,\n STRING_OPERATORS,\n TIME_GRAIN_PATTERN,\n} from \"./constants\";\n\n/** A leaf predicate: `{ member, operator, values? }`, no extra keys. */\nconst filterPredicateSchema: z.ZodType<MetricPredicate> = z\n .object({\n member: z\n .string()\n .min(1, { message: \"filter predicate 'member' cannot be empty\" })\n .refine(isValidColumnName, {\n message:\n \"filter predicate 'member' contains a character that cannot be used in a SQL identifier (control character or newline)\",\n }),\n operator: z.string().min(1, {\n message: \"filter predicate 'operator' cannot be empty\",\n }) as z.ZodType<MetricFilterOperatorName>,\n values: z\n .array(z.union([z.string(), z.number()]))\n .max(METRIC_FILTER_VALUES_MAX, {\n message: `filter predicate 'values' length exceeds the maximum of ${METRIC_FILTER_VALUES_MAX}`,\n })\n .optional(),\n })\n .strict();\n\n/** Recursive filter: a predicate leaf or an `{ and }` / `{ or }` group. */\nconst filterSchema: z.ZodType<MetricFilter> = z.lazy(() =>\n z.union([\n filterPredicateSchema,\n z\n .object({\n and: z.array(filterSchema).max(METRIC_FILTER_GROUP_MAX, {\n message: `filter 'and' group exceeds the maximum of ${METRIC_FILTER_GROUP_MAX} children`,\n }),\n })\n .strict(),\n z\n .object({\n or: z.array(filterSchema).max(METRIC_FILTER_GROUP_MAX, {\n message: `filter 'or' group exceeds the maximum of ${METRIC_FILTER_GROUP_MAX} children`,\n }),\n })\n .strict(),\n ]),\n);\n\nconst metricRequestSchema = z\n .object({\n measures: z\n .array(\n z\n .string()\n .min(1, \"measure name cannot be empty\")\n .refine(isValidColumnName, {\n message:\n \"measure name contains a character that cannot be used in a SQL identifier (control character or newline)\",\n }),\n )\n .min(1, \"at least one measure is required\")\n .max(METRIC_MEASURES_MAX, {\n message: `measures length exceeds the maximum of ${METRIC_MEASURES_MAX}`,\n }),\n dimensions: z\n .array(\n z\n .string()\n .min(1, \"dimension name cannot be empty\")\n .refine(isValidColumnName, {\n message:\n \"dimension name contains a character that cannot be used in a SQL identifier (control character or newline)\",\n }),\n )\n .max(METRIC_DIMENSIONS_MAX, {\n message: `dimensions length exceeds the maximum of ${METRIC_DIMENSIONS_MAX}`,\n })\n .optional(),\n filter: filterSchema.optional(),\n // Grammar-shaped bucketing grain, applied to `timeDimension` via\n // `date_trunc`. The token is validated for safety here; the grain literal\n // is interpolated (single-quoted, never a bind param) in\n // `renderDimensionClause`, so this pattern gate is the security boundary.\n timeGrain: z\n .string()\n .min(1, { message: \"timeGrain cannot be empty\" })\n .regex(TIME_GRAIN_PATTERN, {\n message: \"timeGrain must match /^[a-z][a-z_]*$/\",\n })\n .optional(),\n // The single dimension `timeGrain` applies to via `date_trunc`. A column\n // identifier (backtick-quoted at interpolation), so it accepts the full\n // delimited-identifier grammar. Cross-field rules in `superRefine`:\n // required when `timeGrain` is set, and must be one of `dimensions`.\n timeDimension: z\n .string()\n .min(1, { message: \"timeDimension cannot be empty\" })\n .refine(isValidColumnName, {\n message:\n \"timeDimension contains a character that cannot be used in a SQL identifier (control character or newline)\",\n })\n .optional(),\n orderBy: z\n .array(\n z\n .object({\n field: z\n .string()\n .min(1, \"orderBy field cannot be empty\")\n .refine(isValidColumnName, {\n message:\n \"orderBy field contains a character that cannot be used in a SQL identifier (control character or newline)\",\n }),\n direction: z.enum(METRIC_ORDER_DIRECTIONS).optional(),\n })\n .strict(),\n )\n .min(1, \"orderBy cannot be an empty array\")\n .max(METRIC_ORDER_BY_MAX, {\n message: `orderBy length exceeds the maximum of ${METRIC_ORDER_BY_MAX}`,\n })\n .optional(),\n limit: z\n .number()\n .int({ message: \"limit must be an integer\" })\n .positive({ message: \"limit must be positive\" })\n .max(METRIC_LIMIT_MAX, {\n message: `limit exceeds the maximum of ${METRIC_LIMIT_MAX}`,\n })\n .optional(),\n format: z.enum([\"JSON_ARRAY\", \"ARROW_STREAM\", \"JSON\", \"ARROW\"]).optional(),\n })\n .strict()\n .superRefine((value, ctx) => {\n if (value.filter != null) {\n validateFilterTree(value.filter, ctx, [\"filter\"], 0);\n }\n\n const seen = new Set<string>();\n const collided = new Set<string>();\n for (const name of [...value.measures, ...(value.dimensions ?? [])]) {\n if (seen.has(name)) {\n collided.add(name);\n }\n seen.add(name);\n }\n if (collided.size > 0) {\n ctx.addIssue({\n code: \"custom\",\n message:\n \"measures and dimensions must be unique across both lists (a name cannot repeat, nor appear as both a measure and a dimension)\",\n path: [\"measures\"],\n });\n }\n\n if (\n value.format != null &&\n normalizeAnalyticsFormat(value.format) !== \"JSON_ARRAY\"\n ) {\n ctx.addIssue({\n code: \"custom\",\n message:\n \"format: only JSON_ARRAY is supported on the metric route at v1 (ARROW_STREAM is not yet implemented)\",\n path: [\"format\"],\n });\n }\n\n if (value.timeGrain != null && value.timeDimension == null) {\n ctx.addIssue({\n code: \"custom\",\n message: \"timeDimension is required when timeGrain is set\",\n path: [\"timeDimension\"],\n });\n }\n if (\n value.timeDimension != null &&\n !(value.dimensions ?? []).includes(value.timeDimension)\n ) {\n ctx.addIssue({\n code: \"custom\",\n message: \"timeDimension must be one of dimensions\",\n path: [\"timeDimension\"],\n });\n }\n\n if (value.orderBy != null) {\n const selectedNames = new Set([\n ...value.measures,\n ...(value.dimensions ?? []),\n ]);\n\n for (let i = 0; i < value.orderBy.length; i++) {\n if (!selectedNames.has(value.orderBy[i].field)) {\n ctx.addIssue({\n code: \"custom\",\n message: \"orderBy field must be one of measures or dimensions\",\n path: [\"orderBy\", i, \"field\"],\n });\n }\n }\n\n const seenFields = new Set<string>();\n let hasDuplicate = false;\n for (const entry of value.orderBy) {\n if (seenFields.has(entry.field)) {\n hasDuplicate = true;\n break;\n }\n seenFields.add(entry.field);\n }\n if (hasDuplicate) {\n ctx.addIssue({\n code: \"custom\",\n message: \"orderBy fields must be unique\",\n path: [\"orderBy\"],\n });\n }\n }\n }) as z.ZodType<IAnalyticsMetricRequest>;\n\nfunction validateFilterTree(\n node: MetricFilter,\n ctx: z.RefinementCtx,\n path: Array<string | number>,\n depth: number,\n): void {\n if (node === null || typeof node !== \"object\") {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path,\n message: \"filter node must be a Predicate or { and } / { or } group\",\n });\n return;\n }\n\n if (\"and\" in node || \"or\" in node) {\n if (depth + 1 > METRIC_FILTER_MAX_DEPTH) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path,\n message: `filter AND/OR nesting exceeds the maximum depth of ${METRIC_FILTER_MAX_DEPTH}`,\n });\n return;\n }\n\n const groupKey = \"and\" in node ? \"and\" : \"or\";\n const children = (\n node as { and?: ReadonlyArray<MetricFilter> } & {\n or?: ReadonlyArray<MetricFilter>;\n }\n )[groupKey];\n\n if (!Array.isArray(children)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [...path, groupKey],\n message: `filter ${groupKey} group must be an array of predicates or nested groups`,\n });\n return;\n }\n\n if (children.length === 0) {\n // Reject empty groups of either kind. An empty `or` is vacuously false;\n // an empty `and` contributes no constraint and renders to no WHERE\n // clause — identical SQL to omitting `filter` entirely, but it would\n // canonicalize to a distinct cache key (`and()` vs `_`), needlessly\n // splitting the cache across semantically identical requests. Requiring\n // at least one child keeps request shape ↔ cache key one-to-one.\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [...path, groupKey],\n message: `filter '${groupKey}' group must contain at least one predicate`,\n });\n return;\n }\n\n children.forEach((child, idx) => {\n validateFilterTree(child, ctx, [...path, groupKey, idx], depth + 1);\n });\n return;\n }\n\n const predicate = node as MetricPredicate;\n\n if (\n !METRIC_FILTER_OPERATORS.includes(\n predicate.operator as MetricFilterOperatorName,\n )\n ) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [...path, \"operator\"],\n message: `filter operator \"${predicate.operator}\" is not one of: ${METRIC_FILTER_OPERATORS.join(\", \")}`,\n });\n return;\n }\n\n const op = predicate.operator;\n const values = predicate.values;\n const valuesLen = values?.length ?? 0;\n\n if (NULL_OPERATORS.has(op)) {\n if (values != null && valuesLen > 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [...path, \"values\"],\n message: `filter operator \"${op}\" must not carry values`,\n });\n }\n } else if (SINGLE_VALUE_OPERATORS.has(op)) {\n if (valuesLen !== 1) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [...path, \"values\"],\n message: `filter operator \"${op}\" requires exactly one value (got ${valuesLen})`,\n });\n }\n } else if (LIST_VALUE_OPERATORS.has(op)) {\n if (valuesLen < 1) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [...path, \"values\"],\n message: `filter operator \"${op}\" requires at least one value`,\n });\n }\n }\n\n if (STRING_OPERATORS.has(op) && valuesLen > 0) {\n const v = predicate.values?.[0];\n if (typeof v !== \"string\") {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: [...path, \"values\"],\n message: `filter operator \"${op}\" requires a string value (got ${typeof v})`,\n });\n }\n }\n}\n\nfunction preCheckFilterDepth(filter: unknown): void {\n if (filter == null || typeof filter !== \"object\") return;\n const stack: Array<[unknown, number]> = [[filter, 0]];\n while (stack.length > 0) {\n const popped = stack.pop();\n if (popped === undefined) continue;\n const [node, depth] = popped;\n if (node == null || typeof node !== \"object\") continue;\n const obj = node as Record<string, unknown>;\n for (const groupKey of [\"and\", \"or\"] as const) {\n const children = obj[groupKey];\n if (!Array.isArray(children)) continue;\n if (children.length > METRIC_FILTER_GROUP_MAX) {\n throw new ValidationError(\n \"Invalid metric request body (fields: filter)\",\n {\n context: {\n reason: `filter ${groupKey} group has ${children.length} children; the maximum is ${METRIC_FILTER_GROUP_MAX}`,\n },\n },\n );\n }\n if (depth + 1 > METRIC_FILTER_MAX_DEPTH) {\n throw new ValidationError(\n \"Invalid metric request body (fields: filter)\",\n {\n context: {\n reason: `filter AND/OR nesting exceeds the maximum depth of ${METRIC_FILTER_MAX_DEPTH}`,\n },\n },\n );\n }\n for (const child of children) {\n stack.push([child, depth + 1]);\n }\n }\n }\n}\n\nexport function validateMetricRequest(body: unknown): IAnalyticsMetricRequest {\n if (body != null && typeof body === \"object\") {\n preCheckFilterDepth((body as { filter?: unknown }).filter);\n }\n const result = metricRequestSchema.safeParse(body);\n if (!result.success) {\n const fieldPaths = result.error.issues\n .map((i) => i.path.join(\".\") || \"(root)\")\n .join(\", \");\n throw new ValidationError(\n fieldPaths.length > 0\n ? `Invalid metric request body (fields: ${fieldPaths})`\n : \"Invalid metric request body\",\n { context: { issues: result.error.issues } },\n );\n }\n return result.data;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query.js","names":["sqlHelpers"],"sources":["../../../src/plugins/analytics/query.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { isSQLTypeMarker, type SQLTypeMarker, sql as sqlHelpers } from \"shared\";\nimport { getWorkspaceId } from \"../../context\";\nimport { ValidationError } from \"../../errors\";\nimport type { sql } from \"../../workspace-client\";\n\ntype SQLParameterValue = SQLTypeMarker | null | undefined;\n\nexport class QueryProcessor {\n async processQueryParams(\n query: string,\n parameters?: Record<string, SQLParameterValue>,\n ): Promise<Record<string, SQLParameterValue>> {\n const processed = { ...parameters };\n\n // extract all params from the query\n const paramMatches = query.matchAll(/:([a-zA-Z_]\\w*)/g);\n const queryParams = new Set(Array.from(paramMatches, (m) => m[1]));\n\n // auto-inject workspaceId if needed and not provided\n if (queryParams.has(\"workspaceId\") && !processed.workspaceId) {\n const workspaceId = await getWorkspaceId();\n if (workspaceId) {\n processed.workspaceId = sqlHelpers.string(workspaceId);\n }\n }\n\n return processed;\n }\n\n hashQuery(query: string): string {\n return createHash(\"md5\").update(query).digest(\"hex\");\n }\n\n convertToSQLParameters(\n query: string,\n parameters?: Record<string, SQLParameterValue>,\n ): { statement: string; parameters: sql.StatementParameterListItem[] } {\n const sqlParameters: sql.StatementParameterListItem[] = [];\n\n if (parameters) {\n // extract all params from the query\n const queryParamMatches = query.matchAll(/:([a-zA-Z_]\\w*)/g);\n const queryParams = new Set(Array.from(queryParamMatches, (m) => m[1]));\n\n // only allow parameters that exist in the query\n for (const key of Object.keys(parameters)) {\n if (!queryParams.has(key)) {\n const validParams = Array.from(queryParams).join(\", \") || \"none\";\n throw ValidationError.invalidValue(\n key,\n parameters[key],\n `a parameter defined in the query (valid: ${validParams})`,\n );\n }\n }\n\n // convert parameters to SQL parameters\n for (const [key, value] of Object.entries(parameters)) {\n const parameter = this._createParameter(key, value);\n if (parameter) {\n sqlParameters.push(parameter);\n }\n }\n }\n\n return { statement: query, parameters: sqlParameters };\n }\n\n private _createParameter(\n key: string,\n value: SQLParameterValue,\n ): sql.StatementParameterListItem | null {\n if (value === null || value === undefined) {\n return null;\n }\n\n if (!isSQLTypeMarker(value)) {\n throw ValidationError.invalidValue(\n key,\n value,\n \"SQL type (use sql.string(), sql.number(), sql.date(), sql.timestamp(), or sql.boolean())\",\n );\n }\n\n return {\n name: key,\n value: value.value,\n type: value.__sql_type,\n };\n }\n}\n"],"mappings":";;;;;;;;
|
|
1
|
+
{"version":3,"file":"query.js","names":["sqlHelpers"],"sources":["../../../src/plugins/analytics/query.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\n\nimport { isSQLTypeMarker, type SQLTypeMarker, sql as sqlHelpers } from \"shared\";\n\nimport { getWorkspaceId } from \"../../context\";\nimport { ValidationError } from \"../../errors\";\nimport type { sql } from \"../../workspace-client\";\n\ntype SQLParameterValue = SQLTypeMarker | null | undefined;\n\nexport class QueryProcessor {\n async processQueryParams(\n query: string,\n parameters?: Record<string, SQLParameterValue>,\n ): Promise<Record<string, SQLParameterValue>> {\n const processed = { ...parameters };\n\n // extract all params from the query\n const paramMatches = query.matchAll(/:([a-zA-Z_]\\w*)/g);\n const queryParams = new Set(Array.from(paramMatches, (m) => m[1]));\n\n // auto-inject workspaceId if needed and not provided\n if (queryParams.has(\"workspaceId\") && !processed.workspaceId) {\n const workspaceId = await getWorkspaceId();\n if (workspaceId) {\n processed.workspaceId = sqlHelpers.string(workspaceId);\n }\n }\n\n return processed;\n }\n\n hashQuery(query: string): string {\n return createHash(\"md5\").update(query).digest(\"hex\");\n }\n\n convertToSQLParameters(\n query: string,\n parameters?: Record<string, SQLParameterValue>,\n ): { statement: string; parameters: sql.StatementParameterListItem[] } {\n const sqlParameters: sql.StatementParameterListItem[] = [];\n\n if (parameters) {\n // extract all params from the query\n const queryParamMatches = query.matchAll(/:([a-zA-Z_]\\w*)/g);\n const queryParams = new Set(Array.from(queryParamMatches, (m) => m[1]));\n\n // only allow parameters that exist in the query\n for (const key of Object.keys(parameters)) {\n if (!queryParams.has(key)) {\n const validParams = Array.from(queryParams).join(\", \") || \"none\";\n throw ValidationError.invalidValue(\n key,\n parameters[key],\n `a parameter defined in the query (valid: ${validParams})`,\n );\n }\n }\n\n // convert parameters to SQL parameters\n for (const [key, value] of Object.entries(parameters)) {\n const parameter = this._createParameter(key, value);\n if (parameter) {\n sqlParameters.push(parameter);\n }\n }\n }\n\n return { statement: query, parameters: sqlParameters };\n }\n\n private _createParameter(\n key: string,\n value: SQLParameterValue,\n ): sql.StatementParameterListItem | null {\n if (value === null || value === undefined) {\n return null;\n }\n\n if (!isSQLTypeMarker(value)) {\n throw ValidationError.invalidValue(\n key,\n value,\n \"SQL type (use sql.string(), sql.number(), sql.date(), sql.timestamp(), or sql.boolean())\",\n );\n }\n\n return {\n name: key,\n value: value.value,\n type: value.__sql_type,\n };\n }\n}\n"],"mappings":";;;;;;;;AAUA,IAAa,iBAAb,MAA4B;CAC1B,MAAM,mBACJ,OACA,YAC4C;EAC5C,MAAM,YAAY,EAAE,GAAG,YAAY;EAGnC,MAAM,eAAe,MAAM,SAAS,mBAAmB;AAIvD,MAHoB,IAAI,IAAI,MAAM,KAAK,eAAe,MAAM,EAAE,GAAG,CAAC,CAGlD,IAAI,cAAc,IAAI,CAAC,UAAU,aAAa;GAC5D,MAAM,cAAc,MAAM,gBAAgB;AAC1C,OAAI,YACF,WAAU,cAAcA,IAAW,OAAO,YAAY;;AAI1D,SAAO;;CAGT,UAAU,OAAuB;AAC/B,SAAO,WAAW,MAAM,CAAC,OAAO,MAAM,CAAC,OAAO,MAAM;;CAGtD,uBACE,OACA,YACqE;EACrE,MAAM,gBAAkD,EAAE;AAE1D,MAAI,YAAY;GAEd,MAAM,oBAAoB,MAAM,SAAS,mBAAmB;GAC5D,MAAM,cAAc,IAAI,IAAI,MAAM,KAAK,oBAAoB,MAAM,EAAE,GAAG,CAAC;AAGvE,QAAK,MAAM,OAAO,OAAO,KAAK,WAAW,CACvC,KAAI,CAAC,YAAY,IAAI,IAAI,EAAE;IACzB,MAAM,cAAc,MAAM,KAAK,YAAY,CAAC,KAAK,KAAK,IAAI;AAC1D,UAAM,gBAAgB,aACpB,KACA,WAAW,MACX,4CAA4C,YAAY,GACzD;;AAKL,QAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,WAAW,EAAE;IACrD,MAAM,YAAY,KAAK,iBAAiB,KAAK,MAAM;AACnD,QAAI,UACF,eAAc,KAAK,UAAU;;;AAKnC,SAAO;GAAE,WAAW;GAAO,YAAY;GAAe;;CAGxD,AAAQ,iBACN,KACA,OACuC;AACvC,MAAI,UAAU,QAAQ,UAAU,OAC9B,QAAO;AAGT,MAAI,CAAC,gBAAgB,MAAM,CACzB,OAAM,gBAAgB,aACpB,KACA,OACA,2FACD;AAGH,SAAO;GACL,MAAM;GACN,OAAO,MAAM;GACb,MAAM,MAAM;GACb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"result-delivery.js","names":[],"sources":["../../../src/plugins/analytics/result-delivery.ts"],"sourcesContent":["import { type DataType, type Field, Type, tableFromIPC } from \"apache-arrow\";\nimport type { SQLTypeMarker } from \"shared\";\nimport { ExecutionError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport type { RefreshChunkLink } from \"../../stream/arrow-stream-processor\";\nimport type { sql } from \"../../workspace-client\";\n\n/**\n * Centralized disposition/format fallback for analytics result delivery.\n *\n * Two warehouse capability profiles must both work, and their supported\n * combinations are mutually exclusive per warehouse:\n *\n * - **Reyden**: `ARROW_STREAM + INLINE` (and `JSON_ARRAY + INLINE`); does NOT\n * support `EXTERNAL_LINKS`.\n * - **Normal SQL warehouse**: `JSON_ARRAY + INLINE` and\n * `ARROW_STREAM + EXTERNAL_LINKS`; rejects `ARROW_STREAM + INLINE`.\n *\n * The fallback logic lives here (rather than inline in the plugin route) so it\n * can be unit-tested against a fake executor without HTTP, and so the brittle\n * \"which rejection is this\" decision is expressed once.\n */\n\nconst logger = createLogger(\"analytics:delivery\");\n\n/**\n * Minimal query surface the fallback needs. The analytics plugin (and its\n * `asUser(req)` proxy) satisfy it, so the same delivery logic runs unchanged\n * under service-principal and on-behalf-of identities.\n */\nexport interface QueryExecutor {\n query(\n query: string,\n parameters: Record<string, SQLTypeMarker | null | undefined> | undefined,\n formatParameters: { disposition: string; format: string },\n signal?: AbortSignal,\n ): Promise<\n | {\n attachment?: string;\n data?: Record<string, unknown>[];\n external_links?: sql.ExternalLink[];\n columnNames?: string[];\n statement_id?: string;\n status?: unknown;\n refreshChunkLink?: RefreshChunkLink;\n }\n | undefined\n >;\n}\n\n/** Streams already-resolved EXTERNAL_LINKS chunks; the connector provides it. */\nexport interface ArrowChunkStreamer {\n streamExternalLinks(\n chunks: sql.ExternalLink[],\n signal?: AbortSignal,\n refresh?: RefreshChunkLink,\n ): AsyncGenerator<Uint8Array, void, unknown>;\n}\n\n/** The arrow delivery mode a warehouse actually supports. */\nexport type ArrowCapability = \"inline\" | \"external\";\n\n/** Optional per-warehouse capability memo, avoiding a doomed probe per query. */\ninterface ArrowDeliveryOptions {\n /**\n * When `\"external\"`, skip the `INLINE+ARROW_STREAM` attempt and go straight\n * to `EXTERNAL_LINKS` — a standard warehouse rejects INLINE arrow on every\n * query, so once we've learned that, the probe is pure waste. `\"inline\"` or\n * undefined attempts INLINE first (the common/Reyden path).\n */\n capabilityHint?: ArrowCapability;\n /**\n * Called once the working delivery mode is known, so the caller can memoize\n * it per warehouse for subsequent queries.\n */\n onCapabilityResolved?: (capability: ArrowCapability) => void;\n}\n\n/**\n * How a warehouse rejected a disposition/format combination.\n *\n * Only the two structured error codes Databricks emits for capability\n * mismatches (`INVALID_PARAMETER_VALUE`, `NOT_IMPLEMENTED`) gate a fallback —\n * auth, permission, and SQL errors never match, so they propagate untouched.\n * Message matching only *disambiguates* which capability is missing once the\n * code has confirmed it's a capability error.\n */\ntype DispositionRejection =\n | \"needs-arrow-inline\" // INLINE requires ARROW_STREAM\n | \"needs-json-inline\" // INLINE requires JSON_ARRAY\n | \"external-links-unsupported\" // EXTERNAL_LINKS not implemented\n | null;\n\n/**\n * Whether an error is a disposition/format *capability* rejection — the only\n * class of error a fallback should react to. Gated on the two structured codes\n * Databricks emits for capability mismatches (`INVALID_PARAMETER_VALUE`,\n * `NOT_IMPLEMENTED`); auth, permission, and SQL errors carry other codes and\n * never match. This is deliberately message-independent: a Databricks wording\n * change must not turn a capability rejection into a hard 500.\n */\nfunction isCapabilityRejection(err: unknown): boolean {\n const structuredCode =\n err instanceof ExecutionError ? err.errorCode : undefined;\n if (\n structuredCode === \"INVALID_PARAMETER_VALUE\" ||\n structuredCode === \"NOT_IMPLEMENTED\"\n ) {\n return true;\n }\n const lower = (\n err instanceof Error ? err.message : String(err)\n ).toLowerCase();\n return (\n lower.includes(\"invalid_parameter_value\") ||\n lower.includes(\"not_implemented\")\n );\n}\n\nexport function classifyDispositionRejection(\n err: unknown,\n): DispositionRejection {\n // Auth / permission / SQL errors carry other codes → never fall back.\n if (!isCapabilityRejection(err)) return null;\n\n const msg = err instanceof Error ? err.message : String(err);\n const lower = msg.toLowerCase();\n\n // EXTERNAL_LINKS disposition not implemented (Reyden).\n if (\n /external[_\\s]?links/.test(lower) &&\n (lower.includes(\"not yet implemented\") ||\n lower.includes(\"not implemented\") ||\n lower.includes(\"not supported\"))\n ) {\n return \"external-links-unsupported\";\n }\n\n // Remaining cases are INLINE disposition/format mismatches.\n if (!lower.includes(\"inline\")) return null;\n\n if (\n /only supports\\s+arrow_stream/i.test(msg) ||\n /must be\\s+arrow_stream/i.test(msg)\n ) {\n return \"needs-arrow-inline\";\n }\n if (\n /only supports\\s+json_array/i.test(msg) ||\n /must be\\s+json_array/i.test(msg) ||\n /arrow_stream\\s+(is\\s+|was\\s+)?not\\s+supported/i.test(msg)\n ) {\n return \"needs-json-inline\";\n }\n\n return null;\n}\n\n/** Structured error: the warehouse supports no Arrow delivery mode at all. */\nexport function arrowDeliveryUnsupported(): ExecutionError {\n return ExecutionError.statementFailed(\n \"Warehouse supports neither ARROW_STREAM+INLINE nor ARROW_STREAM+EXTERNAL_LINKS\",\n \"ARROW_DELIVERY_UNSUPPORTED\",\n 'This warehouse cannot return Arrow results. Re-run the query with format=\"JSON_ARRAY\".',\n );\n}\n\nconst errMessage = (err: unknown): string =>\n err instanceof Error ? err.message : String(err);\n\n/**\n * Produce the raw Arrow IPC bytes for an ARROW_STREAM query, applying the\n * capability fallback:\n *\n * 1. `INLINE + ARROW_STREAM` — on success the base64 attachment is decoded\n * once and yielded as a `Uint8Array` view (no Arrow parsing, no stash).\n * 2. On a `needs-json-inline` rejection, `EXTERNAL_LINKS + ARROW_STREAM` —\n * the pre-signed chunks resolved in the executor's own context are\n * streamed one at a time.\n * 3. If EXTERNAL_LINKS is also unsupported, a structured\n * `ARROW_DELIVERY_UNSUPPORTED` error is thrown.\n *\n * `out` is populated with the real column names and statement id before the\n * first chunk is yielded, so the caller can emit them (e.g. as a header)\n * ahead of the body.\n */\nexport async function* deliverArrowBytes(\n executor: QueryExecutor,\n streamer: ArrowChunkStreamer,\n query: string,\n processedParams: Record<string, SQLTypeMarker | null | undefined> | undefined,\n out: { columnNames?: string[]; statementId?: string },\n signal?: AbortSignal,\n opts?: ArrowDeliveryOptions,\n): AsyncGenerator<Uint8Array, void, unknown> {\n // Skip the INLINE probe when a prior query already learned this warehouse\n // only serves arrow via EXTERNAL_LINKS (standard warehouses reject INLINE\n // arrow on every query, so probing each time is pure waste).\n if (opts?.capabilityHint !== \"external\") {\n try {\n const result = await executor.query(\n query,\n processedParams,\n { disposition: \"INLINE\", format: \"ARROW_STREAM\" },\n signal,\n );\n if (result?.attachment) {\n opts?.onCapabilityResolved?.(\"inline\");\n out.columnNames = result.columnNames;\n out.statementId = result.statement_id;\n // Decode base64 once; yield a view over the Buffer (no copy, no parse).\n const decoded = Buffer.from(result.attachment, \"base64\");\n yield new Uint8Array(\n decoded.buffer,\n decoded.byteOffset,\n decoded.byteLength,\n );\n return;\n }\n // INLINE succeeded but returned no attachment (rare: inline data_array\n // under ARROW_STREAM). Fall through to EXTERNAL_LINKS.\n logger.warn(\n \"ARROW_STREAM INLINE returned no attachment; falling back to EXTERNAL_LINKS\",\n );\n } catch (err: unknown) {\n if (signal?.aborted) throw err;\n // Any capability-coded rejection of INLINE+ARROW_STREAM means this\n // warehouse won't serve arrow inline — try EXTERNAL_LINKS. We\n // intentionally do NOT require the message to match a specific phrase\n // (\"must be JSON_ARRAY\", …): the errorCode gate already excludes auth/SQL\n // errors, and relying on exact wording would turn a Databricks message\n // reword into a hard 500 on every standard warehouse. Worst case for a\n // genuinely bad parameter that happens to carry a capability code: one\n // wasted re-execution that fails identically on EXTERNAL_LINKS and\n // propagates.\n if (!isCapabilityRejection(err)) throw err;\n logger.warn(\n \"ARROW_STREAM INLINE rejected by warehouse, falling back to EXTERNAL_LINKS: %s\",\n errMessage(err),\n );\n }\n }\n\n let ext: Awaited<ReturnType<QueryExecutor[\"query\"]>>;\n try {\n ext = await executor.query(\n query,\n processedParams,\n { disposition: \"EXTERNAL_LINKS\", format: \"ARROW_STREAM\" },\n signal,\n );\n } catch (err: unknown) {\n if (signal?.aborted) throw err;\n // Neither INLINE nor EXTERNAL_LINKS Arrow is supported — surface a clear,\n // actionable error instead of a raw warehouse rejection.\n if (classifyDispositionRejection(err) === \"external-links-unsupported\") {\n throw arrowDeliveryUnsupported();\n }\n throw err;\n }\n\n if (!ext?.external_links) {\n throw ExecutionError.missingData(\"external_links\");\n }\n opts?.onCapabilityResolved?.(\"external\");\n out.columnNames = ext.columnNames;\n out.statementId = ext.statement_id;\n // Stream the pre-signed links resolved in THIS request's execution context\n // (user creds for `.obo.sql`, service principal otherwise). Pre-signed URLs\n // need no auth to download, so there is no second `getStatement` under a\n // mismatched identity. `refreshChunkLink` (also bound to this context) lets\n // the streamer re-mint a link that expires mid-download.\n yield* streamer.streamExternalLinks(\n ext.external_links,\n signal,\n ext.refreshChunkLink,\n );\n}\n\n/** JSON row result plus the metadata the SSE `result` message forwards. */\ninterface JsonResult {\n data: Record<string, unknown>[] | undefined;\n status: unknown;\n statement_id?: string;\n}\n\n/**\n * Produce JSON rows for a JSON_ARRAY query, applying the capability fallback:\n *\n * 1. `INLINE + JSON_ARRAY` — the native, common path.\n * 2. On a `needs-arrow-inline` rejection (warehouse only accepts ARROW_STREAM\n * for INLINE, e.g. Reyden), retry `INLINE + ARROW_STREAM` and decode the\n * attachment to plain rows server-side so the caller's JSON contract holds.\n *\n * EXTERNAL_LINKS is never used for the JSON fallback.\n */\nexport async function deliverJsonResult(\n executor: QueryExecutor,\n query: string,\n processedParams: Record<string, SQLTypeMarker | null | undefined> | undefined,\n signal?: AbortSignal,\n): Promise<JsonResult> {\n try {\n const result = await executor.query(\n query,\n processedParams,\n { disposition: \"INLINE\", format: \"JSON_ARRAY\" },\n signal,\n );\n return {\n data: result?.data,\n status: result?.status,\n statement_id: result?.statement_id,\n };\n } catch (err: unknown) {\n if (signal?.aborted) throw err;\n if (classifyDispositionRejection(err) !== \"needs-arrow-inline\") throw err;\n logger.warn(\n \"JSON_ARRAY INLINE rejected by warehouse, retrying as ARROW_STREAM INLINE and decoding server-side: %s\",\n errMessage(err),\n );\n }\n\n const arrowResult = await executor.query(\n query,\n processedParams,\n { disposition: \"INLINE\", format: \"ARROW_STREAM\" },\n signal,\n );\n if (!arrowResult?.attachment) {\n throw ExecutionError.missingData(\"ARROW_STREAM attachment\");\n }\n const rows = decodeArrowAttachmentToRows(\n arrowResult.attachment,\n arrowResult.columnNames,\n );\n return {\n data: rows,\n status: arrowResult.status,\n statement_id: arrowResult.statement_id,\n };\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// Server-side Arrow → JSON row materializer (JSON fallback only)\n// ────────────────────────────────────────────────────────────────────────────\n\n/**\n * Hard caps on the server-side JSON_ARRAY fallback. The materializer builds\n * every row as a plain JS object on the Node main thread (O(rows × cols)\n * allocations), so a runaway result blocks the event loop and pressures GC.\n * Cap on rows AND decoded bytes — either dimension can blow up independently.\n */\nconst JSON_ARRAY_FALLBACK_MAX_ROWS = 100_000;\nconst JSON_ARRAY_FALLBACK_MAX_BYTES = 64 * 1024 * 1024;\n\n/** Render an apache-arrow Decimal cell to a fixed-point string. */\nfunction formatDecimalCell(\n value: { toString(): string },\n scale: number,\n): string {\n const unscaled = value.toString();\n if (scale <= 0) return unscaled;\n const negative = unscaled.startsWith(\"-\");\n let digits = negative ? unscaled.slice(1) : unscaled;\n if (digits.length <= scale) digits = digits.padStart(scale + 1, \"0\");\n const point = digits.length - scale;\n const out = `${digits.slice(0, point)}.${digits.slice(point)}`;\n return negative ? `-${out}` : out;\n}\n\n/**\n * Render an apache-arrow Timestamp cell to ISO-8601 ms precision — `Z` for\n * zoned columns, no `Z` for TIMESTAMP_NTZ — matching native JSON_ARRAY.\n */\nfunction formatTimestampCell(epochMs: number, hasTimezone: boolean): string {\n const iso = new Date(epochMs).toISOString();\n return hasTimezone ? iso : iso.slice(0, -1);\n}\n\n/** Render an apache-arrow Date cell to `yyyy-MM-dd` (UTC). */\nfunction formatDateCell(epochMs: number): string {\n return new Date(epochMs).toISOString().slice(0, 10);\n}\n\n/**\n * Render a single scalar Arrow value to the string form the warehouse emits\n * under native JSON_ARRAY. Used for both top-level cells and (recursively, via\n * {@link renderNestedValue}) scalar leaves inside List/Struct/Map columns,\n * keyed on the Arrow field type so a nested `DECIMAL` keeps its scale and a\n * nested `TIMESTAMP` becomes ISO-8601 instead of raw epoch-ms.\n */\nfunction formatScalarCell(value: unknown, type: DataType): string {\n switch (type.typeId) {\n case Type.Decimal:\n return formatDecimalCell(\n value as { toString(): string },\n (type as DataType & { scale: number }).scale,\n );\n case Type.Timestamp:\n return formatTimestampCell(\n Number(value),\n (type as DataType & { timezone?: string | null }).timezone != null,\n );\n case Type.Date:\n return formatDateCell(Number(value));\n default:\n if (value instanceof Uint8Array) {\n return Buffer.from(value).toString(\"base64\");\n }\n if (value instanceof Date) return value.toISOString();\n return String(value);\n }\n}\n\n/**\n * Recursively render a nested Arrow value (List / Struct / Map, or a scalar\n * leaf) to the plain JS shape the warehouse nests inside a JSON_ARRAY cell:\n * structs and maps become objects, lists become arrays, and every scalar leaf\n * is stringified per {@link formatScalarCell}. The caller `JSON.stringify`s the\n * result so nested columns match the warehouse's native `data_array` exactly.\n */\nfunction renderNestedValue(value: unknown, type: DataType): unknown {\n if (value == null) return null;\n switch (type.typeId) {\n case Type.List:\n case Type.FixedSizeList: {\n const itemType = (type as DataType & { children: Field[] }).children[0]\n .type;\n const out: unknown[] = [];\n for (const item of value as Iterable<unknown>) {\n out.push(renderNestedValue(item, itemType));\n }\n return out;\n }\n case Type.Struct: {\n const fields = (type as DataType & { children: Field[] }).children;\n const obj: Record<string, unknown> = {};\n for (const field of fields) {\n obj[field.name] = renderNestedValue(\n (value as Record<string, unknown>)[field.name],\n field.type,\n );\n }\n return obj;\n }\n case Type.Map: {\n // A Map is a List<Struct<key, value>>; the value child carries the type.\n const entriesType = (type as DataType & { children: Field[] }).children[0]\n .type;\n const valueType = (entriesType as DataType & { children: Field[] })\n .children[1].type;\n const obj: Record<string, unknown> = {};\n for (const [k, v] of value as Iterable<[unknown, unknown]>) {\n obj[String(k)] = renderNestedValue(v, valueType);\n }\n return obj;\n }\n default:\n return formatScalarCell(value, type);\n }\n}\n\n/** Parse a STRING cell that looks like JSON into an object/array. */\nfunction maybeParseJsonString(value: string): unknown {\n if (value && (value[0] === \"{\" || value[0] === \"[\")) {\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n }\n return value;\n}\n\n/**\n * Decode a base64 Arrow IPC attachment to plain row objects, matching what the\n * warehouse emits natively under JSON_ARRAY so callers cannot tell which path\n * served the query.\n *\n * The output key uses the manifest name (`columnNames[i]`) when available —\n * Databricks encodes the Arrow schema positionally (col_0, …) — while the\n * vector is still fetched by the Arrow field's own name.\n */\nexport function decodeArrowAttachmentToRows(\n attachment: string,\n columnNames?: string[],\n): Record<string, unknown>[] {\n const decoded = Buffer.from(attachment, \"base64\");\n if (decoded.byteLength > JSON_ARRAY_FALLBACK_MAX_BYTES) {\n throw ExecutionError.statementFailed(\n `Result attachment is ${decoded.byteLength} bytes; JSON_ARRAY fallback materializer caps at ${JSON_ARRAY_FALLBACK_MAX_BYTES} bytes. Re-issue the query with format=\"ARROW_STREAM\" to stream the full result.`,\n \"RESULT_TOO_LARGE_FOR_JSON_FALLBACK\",\n \"Result too large for JSON format. Re-run with ARROW_STREAM format.\",\n );\n }\n const table = tableFromIPC(\n new Uint8Array(decoded.buffer, decoded.byteOffset, decoded.byteLength),\n );\n if (table.numRows > JSON_ARRAY_FALLBACK_MAX_ROWS) {\n throw ExecutionError.statementFailed(\n `Result has ${table.numRows} rows; JSON_ARRAY fallback materializer caps at ${JSON_ARRAY_FALLBACK_MAX_ROWS}. Re-issue the query with format=\"ARROW_STREAM\" to stream the full result.`,\n \"RESULT_TOO_LARGE_FOR_JSON_FALLBACK\",\n `Result too large for JSON format (over ${JSON_ARRAY_FALLBACK_MAX_ROWS} rows). Re-run with ARROW_STREAM format.`,\n );\n }\n // Resolve child vectors once (getChild walks fields on every call). The\n // output key uses the manifest name; the vector is fetched by field name.\n const columns = table.schema.fields.map((f, i) => {\n const outName =\n columnNames?.[i] && columnNames[i].length > 0 ? columnNames[i] : f.name;\n return [outName, table.getChild(f.name), f.type] as const;\n });\n const rows: Record<string, unknown>[] = [];\n for (let i = 0; i < table.numRows; i++) {\n const row: Record<string, unknown> = {};\n for (const [name, col, type] of columns) {\n const v = col?.get(i);\n if (v == null) {\n row[name] = null;\n continue;\n }\n switch (type.typeId) {\n case Type.Decimal:\n case Type.Timestamp:\n case Type.Date:\n row[name] = formatScalarCell(v, type);\n continue;\n // Nested columns (STRUCT / ARRAY / MAP) arrive on the native\n // JSON_ARRAY wire as a JSON string with every scalar leaf stringified\n // by type; render the same shape and stringify so callers cannot tell\n // the fallback path from the native one.\n case Type.List:\n case Type.FixedSizeList:\n case Type.Struct:\n case Type.Map:\n row[name] = JSON.stringify(renderNestedValue(v, type));\n continue;\n }\n if (\n typeof v === \"number\" ||\n typeof v === \"bigint\" ||\n typeof v === \"boolean\"\n ) {\n row[name] = String(v);\n } else if (typeof v === \"string\") {\n row[name] = maybeParseJsonString(v);\n } else if (v instanceof Uint8Array) {\n row[name] = Buffer.from(v).toString(\"base64\");\n } else {\n row[name] = formatScalarCell(v, type);\n }\n }\n rows.push(row);\n }\n return rows;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"result-delivery.js","names":[],"sources":["../../../src/plugins/analytics/result-delivery.ts"],"sourcesContent":["import { type DataType, type Field, Type, tableFromIPC } from \"apache-arrow\";\nimport type { SQLTypeMarker } from \"shared\";\n\nimport { ExecutionError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport type { RefreshChunkLink } from \"../../stream/arrow-stream-processor\";\nimport type { sql } from \"../../workspace-client\";\n\n/**\n * Centralized disposition/format fallback for analytics result delivery.\n *\n * Two warehouse capability profiles must both work, and their supported\n * combinations are mutually exclusive per warehouse:\n *\n * - **Reyden**: `ARROW_STREAM + INLINE` (and `JSON_ARRAY + INLINE`); does NOT\n * support `EXTERNAL_LINKS`.\n * - **Normal SQL warehouse**: `JSON_ARRAY + INLINE` and\n * `ARROW_STREAM + EXTERNAL_LINKS`; rejects `ARROW_STREAM + INLINE`.\n *\n * The fallback logic lives here (rather than inline in the plugin route) so it\n * can be unit-tested against a fake executor without HTTP, and so the brittle\n * \"which rejection is this\" decision is expressed once.\n */\n\nconst logger = createLogger(\"analytics:delivery\");\n\n/**\n * Minimal query surface the fallback needs. The analytics plugin (and its\n * `asUser(req)` proxy) satisfy it, so the same delivery logic runs unchanged\n * under service-principal and on-behalf-of identities.\n */\nexport interface QueryExecutor {\n query(\n query: string,\n parameters: Record<string, SQLTypeMarker | null | undefined> | undefined,\n formatParameters: { disposition: string; format: string },\n signal?: AbortSignal,\n ): Promise<\n | {\n attachment?: string;\n data?: Record<string, unknown>[];\n external_links?: sql.ExternalLink[];\n columnNames?: string[];\n statement_id?: string;\n status?: unknown;\n refreshChunkLink?: RefreshChunkLink;\n }\n | undefined\n >;\n}\n\n/** Streams already-resolved EXTERNAL_LINKS chunks; the connector provides it. */\nexport interface ArrowChunkStreamer {\n streamExternalLinks(\n chunks: sql.ExternalLink[],\n signal?: AbortSignal,\n refresh?: RefreshChunkLink,\n ): AsyncGenerator<Uint8Array, void, unknown>;\n}\n\n/** The arrow delivery mode a warehouse actually supports. */\nexport type ArrowCapability = \"inline\" | \"external\";\n\n/** Optional per-warehouse capability memo, avoiding a doomed probe per query. */\ninterface ArrowDeliveryOptions {\n /**\n * When `\"external\"`, skip the `INLINE+ARROW_STREAM` attempt and go straight\n * to `EXTERNAL_LINKS` — a standard warehouse rejects INLINE arrow on every\n * query, so once we've learned that, the probe is pure waste. `\"inline\"` or\n * undefined attempts INLINE first (the common/Reyden path).\n */\n capabilityHint?: ArrowCapability;\n /**\n * Called once the working delivery mode is known, so the caller can memoize\n * it per warehouse for subsequent queries.\n */\n onCapabilityResolved?: (capability: ArrowCapability) => void;\n}\n\n/**\n * How a warehouse rejected a disposition/format combination.\n *\n * Only the two structured error codes Databricks emits for capability\n * mismatches (`INVALID_PARAMETER_VALUE`, `NOT_IMPLEMENTED`) gate a fallback —\n * auth, permission, and SQL errors never match, so they propagate untouched.\n * Message matching only *disambiguates* which capability is missing once the\n * code has confirmed it's a capability error.\n */\ntype DispositionRejection =\n | \"needs-arrow-inline\" // INLINE requires ARROW_STREAM\n | \"needs-json-inline\" // INLINE requires JSON_ARRAY\n | \"external-links-unsupported\" // EXTERNAL_LINKS not implemented\n | null;\n\n/**\n * Whether an error is a disposition/format *capability* rejection — the only\n * class of error a fallback should react to. Gated on the two structured codes\n * Databricks emits for capability mismatches (`INVALID_PARAMETER_VALUE`,\n * `NOT_IMPLEMENTED`); auth, permission, and SQL errors carry other codes and\n * never match. This is deliberately message-independent: a Databricks wording\n * change must not turn a capability rejection into a hard 500.\n */\nfunction isCapabilityRejection(err: unknown): boolean {\n const structuredCode =\n err instanceof ExecutionError ? err.errorCode : undefined;\n if (\n structuredCode === \"INVALID_PARAMETER_VALUE\" ||\n structuredCode === \"NOT_IMPLEMENTED\"\n ) {\n return true;\n }\n const lower = (\n err instanceof Error ? err.message : String(err)\n ).toLowerCase();\n return (\n lower.includes(\"invalid_parameter_value\") ||\n lower.includes(\"not_implemented\")\n );\n}\n\nexport function classifyDispositionRejection(\n err: unknown,\n): DispositionRejection {\n // Auth / permission / SQL errors carry other codes → never fall back.\n if (!isCapabilityRejection(err)) return null;\n\n const msg = err instanceof Error ? err.message : String(err);\n const lower = msg.toLowerCase();\n\n // EXTERNAL_LINKS disposition not implemented (Reyden).\n if (\n /external[_\\s]?links/.test(lower) &&\n (lower.includes(\"not yet implemented\") ||\n lower.includes(\"not implemented\") ||\n lower.includes(\"not supported\"))\n ) {\n return \"external-links-unsupported\";\n }\n\n // Remaining cases are INLINE disposition/format mismatches.\n if (!lower.includes(\"inline\")) return null;\n\n if (\n /only supports\\s+arrow_stream/i.test(msg) ||\n /must be\\s+arrow_stream/i.test(msg)\n ) {\n return \"needs-arrow-inline\";\n }\n if (\n /only supports\\s+json_array/i.test(msg) ||\n /must be\\s+json_array/i.test(msg) ||\n /arrow_stream\\s+(is\\s+|was\\s+)?not\\s+supported/i.test(msg)\n ) {\n return \"needs-json-inline\";\n }\n\n return null;\n}\n\n/** Structured error: the warehouse supports no Arrow delivery mode at all. */\nexport function arrowDeliveryUnsupported(): ExecutionError {\n return ExecutionError.statementFailed(\n \"Warehouse supports neither ARROW_STREAM+INLINE nor ARROW_STREAM+EXTERNAL_LINKS\",\n \"ARROW_DELIVERY_UNSUPPORTED\",\n 'This warehouse cannot return Arrow results. Re-run the query with format=\"JSON_ARRAY\".',\n );\n}\n\nconst errMessage = (err: unknown): string =>\n err instanceof Error ? err.message : String(err);\n\n/**\n * Produce the raw Arrow IPC bytes for an ARROW_STREAM query, applying the\n * capability fallback:\n *\n * 1. `INLINE + ARROW_STREAM` — on success the base64 attachment is decoded\n * once and yielded as a `Uint8Array` view (no Arrow parsing, no stash).\n * 2. On a `needs-json-inline` rejection, `EXTERNAL_LINKS + ARROW_STREAM` —\n * the pre-signed chunks resolved in the executor's own context are\n * streamed one at a time.\n * 3. If EXTERNAL_LINKS is also unsupported, a structured\n * `ARROW_DELIVERY_UNSUPPORTED` error is thrown.\n *\n * `out` is populated with the real column names and statement id before the\n * first chunk is yielded, so the caller can emit them (e.g. as a header)\n * ahead of the body.\n */\nexport async function* deliverArrowBytes(\n executor: QueryExecutor,\n streamer: ArrowChunkStreamer,\n query: string,\n processedParams: Record<string, SQLTypeMarker | null | undefined> | undefined,\n out: { columnNames?: string[]; statementId?: string },\n signal?: AbortSignal,\n opts?: ArrowDeliveryOptions,\n): AsyncGenerator<Uint8Array, void, unknown> {\n // Skip the INLINE probe when a prior query already learned this warehouse\n // only serves arrow via EXTERNAL_LINKS (standard warehouses reject INLINE\n // arrow on every query, so probing each time is pure waste).\n if (opts?.capabilityHint !== \"external\") {\n try {\n const result = await executor.query(\n query,\n processedParams,\n { disposition: \"INLINE\", format: \"ARROW_STREAM\" },\n signal,\n );\n if (result?.attachment) {\n opts?.onCapabilityResolved?.(\"inline\");\n out.columnNames = result.columnNames;\n out.statementId = result.statement_id;\n // Decode base64 once; yield a view over the Buffer (no copy, no parse).\n const decoded = Buffer.from(result.attachment, \"base64\");\n yield new Uint8Array(\n decoded.buffer,\n decoded.byteOffset,\n decoded.byteLength,\n );\n return;\n }\n // INLINE succeeded but returned no attachment (rare: inline data_array\n // under ARROW_STREAM). Fall through to EXTERNAL_LINKS.\n logger.warn(\n \"ARROW_STREAM INLINE returned no attachment; falling back to EXTERNAL_LINKS\",\n );\n } catch (err: unknown) {\n if (signal?.aborted) throw err;\n // Any capability-coded rejection of INLINE+ARROW_STREAM means this\n // warehouse won't serve arrow inline — try EXTERNAL_LINKS. We\n // intentionally do NOT require the message to match a specific phrase\n // (\"must be JSON_ARRAY\", …): the errorCode gate already excludes auth/SQL\n // errors, and relying on exact wording would turn a Databricks message\n // reword into a hard 500 on every standard warehouse. Worst case for a\n // genuinely bad parameter that happens to carry a capability code: one\n // wasted re-execution that fails identically on EXTERNAL_LINKS and\n // propagates.\n if (!isCapabilityRejection(err)) throw err;\n logger.warn(\n \"ARROW_STREAM INLINE rejected by warehouse, falling back to EXTERNAL_LINKS: %s\",\n errMessage(err),\n );\n }\n }\n\n let ext: Awaited<ReturnType<QueryExecutor[\"query\"]>>;\n try {\n ext = await executor.query(\n query,\n processedParams,\n { disposition: \"EXTERNAL_LINKS\", format: \"ARROW_STREAM\" },\n signal,\n );\n } catch (err: unknown) {\n if (signal?.aborted) throw err;\n // Neither INLINE nor EXTERNAL_LINKS Arrow is supported — surface a clear,\n // actionable error instead of a raw warehouse rejection.\n if (classifyDispositionRejection(err) === \"external-links-unsupported\") {\n throw arrowDeliveryUnsupported();\n }\n throw err;\n }\n\n if (!ext?.external_links) {\n throw ExecutionError.missingData(\"external_links\");\n }\n opts?.onCapabilityResolved?.(\"external\");\n out.columnNames = ext.columnNames;\n out.statementId = ext.statement_id;\n // Stream the pre-signed links resolved in THIS request's execution context\n // (user creds for `.obo.sql`, service principal otherwise). Pre-signed URLs\n // need no auth to download, so there is no second `getStatement` under a\n // mismatched identity. `refreshChunkLink` (also bound to this context) lets\n // the streamer re-mint a link that expires mid-download.\n yield* streamer.streamExternalLinks(\n ext.external_links,\n signal,\n ext.refreshChunkLink,\n );\n}\n\n/** JSON row result plus the metadata the SSE `result` message forwards. */\ninterface JsonResult {\n data: Record<string, unknown>[] | undefined;\n status: unknown;\n statement_id?: string;\n}\n\n/**\n * Produce JSON rows for a JSON_ARRAY query, applying the capability fallback:\n *\n * 1. `INLINE + JSON_ARRAY` — the native, common path.\n * 2. On a `needs-arrow-inline` rejection (warehouse only accepts ARROW_STREAM\n * for INLINE, e.g. Reyden), retry `INLINE + ARROW_STREAM` and decode the\n * attachment to plain rows server-side so the caller's JSON contract holds.\n *\n * EXTERNAL_LINKS is never used for the JSON fallback.\n */\nexport async function deliverJsonResult(\n executor: QueryExecutor,\n query: string,\n processedParams: Record<string, SQLTypeMarker | null | undefined> | undefined,\n signal?: AbortSignal,\n): Promise<JsonResult> {\n try {\n const result = await executor.query(\n query,\n processedParams,\n { disposition: \"INLINE\", format: \"JSON_ARRAY\" },\n signal,\n );\n return {\n data: result?.data,\n status: result?.status,\n statement_id: result?.statement_id,\n };\n } catch (err: unknown) {\n if (signal?.aborted) throw err;\n if (classifyDispositionRejection(err) !== \"needs-arrow-inline\") throw err;\n logger.warn(\n \"JSON_ARRAY INLINE rejected by warehouse, retrying as ARROW_STREAM INLINE and decoding server-side: %s\",\n errMessage(err),\n );\n }\n\n const arrowResult = await executor.query(\n query,\n processedParams,\n { disposition: \"INLINE\", format: \"ARROW_STREAM\" },\n signal,\n );\n if (!arrowResult?.attachment) {\n throw ExecutionError.missingData(\"ARROW_STREAM attachment\");\n }\n const rows = decodeArrowAttachmentToRows(\n arrowResult.attachment,\n arrowResult.columnNames,\n );\n return {\n data: rows,\n status: arrowResult.status,\n statement_id: arrowResult.statement_id,\n };\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// Server-side Arrow → JSON row materializer (JSON fallback only)\n// ────────────────────────────────────────────────────────────────────────────\n\n/**\n * Hard caps on the server-side JSON_ARRAY fallback. The materializer builds\n * every row as a plain JS object on the Node main thread (O(rows × cols)\n * allocations), so a runaway result blocks the event loop and pressures GC.\n * Cap on rows AND decoded bytes — either dimension can blow up independently.\n */\nconst JSON_ARRAY_FALLBACK_MAX_ROWS = 100_000;\nconst JSON_ARRAY_FALLBACK_MAX_BYTES = 64 * 1024 * 1024;\n\n/** Render an apache-arrow Decimal cell to a fixed-point string. */\nfunction formatDecimalCell(\n value: { toString(): string },\n scale: number,\n): string {\n const unscaled = value.toString();\n if (scale <= 0) return unscaled;\n const negative = unscaled.startsWith(\"-\");\n let digits = negative ? unscaled.slice(1) : unscaled;\n if (digits.length <= scale) digits = digits.padStart(scale + 1, \"0\");\n const point = digits.length - scale;\n const out = `${digits.slice(0, point)}.${digits.slice(point)}`;\n return negative ? `-${out}` : out;\n}\n\n/**\n * Render an apache-arrow Timestamp cell to ISO-8601 ms precision — `Z` for\n * zoned columns, no `Z` for TIMESTAMP_NTZ — matching native JSON_ARRAY.\n */\nfunction formatTimestampCell(epochMs: number, hasTimezone: boolean): string {\n const iso = new Date(epochMs).toISOString();\n return hasTimezone ? iso : iso.slice(0, -1);\n}\n\n/** Render an apache-arrow Date cell to `yyyy-MM-dd` (UTC). */\nfunction formatDateCell(epochMs: number): string {\n return new Date(epochMs).toISOString().slice(0, 10);\n}\n\n/**\n * Render a single scalar Arrow value to the string form the warehouse emits\n * under native JSON_ARRAY. Used for both top-level cells and (recursively, via\n * {@link renderNestedValue}) scalar leaves inside List/Struct/Map columns,\n * keyed on the Arrow field type so a nested `DECIMAL` keeps its scale and a\n * nested `TIMESTAMP` becomes ISO-8601 instead of raw epoch-ms.\n */\nfunction formatScalarCell(value: unknown, type: DataType): string {\n switch (type.typeId) {\n case Type.Decimal:\n return formatDecimalCell(\n value as { toString(): string },\n (type as DataType & { scale: number }).scale,\n );\n case Type.Timestamp:\n return formatTimestampCell(\n Number(value),\n (type as DataType & { timezone?: string | null }).timezone != null,\n );\n case Type.Date:\n return formatDateCell(Number(value));\n default:\n if (value instanceof Uint8Array) {\n return Buffer.from(value).toString(\"base64\");\n }\n if (value instanceof Date) return value.toISOString();\n return String(value);\n }\n}\n\n/**\n * Recursively render a nested Arrow value (List / Struct / Map, or a scalar\n * leaf) to the plain JS shape the warehouse nests inside a JSON_ARRAY cell:\n * structs and maps become objects, lists become arrays, and every scalar leaf\n * is stringified per {@link formatScalarCell}. The caller `JSON.stringify`s the\n * result so nested columns match the warehouse's native `data_array` exactly.\n */\nfunction renderNestedValue(value: unknown, type: DataType): unknown {\n if (value == null) return null;\n switch (type.typeId) {\n case Type.List:\n case Type.FixedSizeList: {\n const itemType = (type as DataType & { children: Field[] }).children[0]\n .type;\n const out: unknown[] = [];\n for (const item of value as Iterable<unknown>) {\n out.push(renderNestedValue(item, itemType));\n }\n return out;\n }\n case Type.Struct: {\n const fields = (type as DataType & { children: Field[] }).children;\n const obj: Record<string, unknown> = {};\n for (const field of fields) {\n obj[field.name] = renderNestedValue(\n (value as Record<string, unknown>)[field.name],\n field.type,\n );\n }\n return obj;\n }\n case Type.Map: {\n // A Map is a List<Struct<key, value>>; the value child carries the type.\n const entriesType = (type as DataType & { children: Field[] }).children[0]\n .type;\n const valueType = (entriesType as DataType & { children: Field[] })\n .children[1].type;\n const obj: Record<string, unknown> = {};\n for (const [k, v] of value as Iterable<[unknown, unknown]>) {\n obj[String(k)] = renderNestedValue(v, valueType);\n }\n return obj;\n }\n default:\n return formatScalarCell(value, type);\n }\n}\n\n/** Parse a STRING cell that looks like JSON into an object/array. */\nfunction maybeParseJsonString(value: string): unknown {\n if (value && (value[0] === \"{\" || value[0] === \"[\")) {\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n }\n return value;\n}\n\n/**\n * Decode a base64 Arrow IPC attachment to plain row objects, matching what the\n * warehouse emits natively under JSON_ARRAY so callers cannot tell which path\n * served the query.\n *\n * The output key uses the manifest name (`columnNames[i]`) when available —\n * Databricks encodes the Arrow schema positionally (col_0, …) — while the\n * vector is still fetched by the Arrow field's own name.\n */\nexport function decodeArrowAttachmentToRows(\n attachment: string,\n columnNames?: string[],\n): Record<string, unknown>[] {\n const decoded = Buffer.from(attachment, \"base64\");\n if (decoded.byteLength > JSON_ARRAY_FALLBACK_MAX_BYTES) {\n throw ExecutionError.statementFailed(\n `Result attachment is ${decoded.byteLength} bytes; JSON_ARRAY fallback materializer caps at ${JSON_ARRAY_FALLBACK_MAX_BYTES} bytes. Re-issue the query with format=\"ARROW_STREAM\" to stream the full result.`,\n \"RESULT_TOO_LARGE_FOR_JSON_FALLBACK\",\n \"Result too large for JSON format. Re-run with ARROW_STREAM format.\",\n );\n }\n const table = tableFromIPC(\n new Uint8Array(decoded.buffer, decoded.byteOffset, decoded.byteLength),\n );\n if (table.numRows > JSON_ARRAY_FALLBACK_MAX_ROWS) {\n throw ExecutionError.statementFailed(\n `Result has ${table.numRows} rows; JSON_ARRAY fallback materializer caps at ${JSON_ARRAY_FALLBACK_MAX_ROWS}. Re-issue the query with format=\"ARROW_STREAM\" to stream the full result.`,\n \"RESULT_TOO_LARGE_FOR_JSON_FALLBACK\",\n `Result too large for JSON format (over ${JSON_ARRAY_FALLBACK_MAX_ROWS} rows). Re-run with ARROW_STREAM format.`,\n );\n }\n // Resolve child vectors once (getChild walks fields on every call). The\n // output key uses the manifest name; the vector is fetched by field name.\n const columns = table.schema.fields.map((f, i) => {\n const outName =\n columnNames?.[i] && columnNames[i].length > 0 ? columnNames[i] : f.name;\n return [outName, table.getChild(f.name), f.type] as const;\n });\n const rows: Record<string, unknown>[] = [];\n for (let i = 0; i < table.numRows; i++) {\n const row: Record<string, unknown> = {};\n for (const [name, col, type] of columns) {\n const v = col?.get(i);\n if (v == null) {\n row[name] = null;\n continue;\n }\n switch (type.typeId) {\n case Type.Decimal:\n case Type.Timestamp:\n case Type.Date:\n row[name] = formatScalarCell(v, type);\n continue;\n // Nested columns (STRUCT / ARRAY / MAP) arrive on the native\n // JSON_ARRAY wire as a JSON string with every scalar leaf stringified\n // by type; render the same shape and stringify so callers cannot tell\n // the fallback path from the native one.\n case Type.List:\n case Type.FixedSizeList:\n case Type.Struct:\n case Type.Map:\n row[name] = JSON.stringify(renderNestedValue(v, type));\n continue;\n }\n if (\n typeof v === \"number\" ||\n typeof v === \"bigint\" ||\n typeof v === \"boolean\"\n ) {\n row[name] = String(v);\n } else if (typeof v === \"string\") {\n row[name] = maybeParseJsonString(v);\n } else if (v instanceof Uint8Array) {\n row[name] = Buffer.from(v).toString(\"base64\");\n } else {\n row[name] = formatScalarCell(v, type);\n }\n }\n rows.push(row);\n }\n return rows;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.js","names":[],"sources":["../../../src/plugins/files/helpers.ts"],"sourcesContent":["export {\n contentTypeFromPath,\n isTextContentType,\n} from \"../../connectors/files/defaults\";\n\n/**\n * Extract the parent directory from a file or directory path.\n *\n * Handles edge cases such as root-level files (`\"/file.txt\"` → `\"/\"`),\n * paths without slashes (`\"file.txt\"` → `\"\"`), and trailing slashes.\n */\nexport function parentDirectory(path: string): string {\n const normalized =\n path.length > 1 && path.endsWith(\"/\") ? path.slice(0, -1) : path;\n const lastSlash = normalized.lastIndexOf(\"/\");\n\n if (lastSlash > 0) return normalized.substring(0, lastSlash);\n if (normalized.startsWith(\"/\")) return \"/\";\n return \"\";\n}\n\n/**\n * Sanitize a filename for use in a `Content-Disposition` HTTP header.\n *\n * Redundancy check – Unity Catalog is unlikely to allow filenames with\n * quotes or control characters, but we sanitize defensively to prevent\n * HTTP header injection if upstream constraints ever change.\n */\nexport function sanitizeFilename(raw: string): string {\n //
|
|
1
|
+
{"version":3,"file":"helpers.js","names":[],"sources":["../../../src/plugins/files/helpers.ts"],"sourcesContent":["export {\n contentTypeFromPath,\n isTextContentType,\n} from \"../../connectors/files/defaults\";\n\n/**\n * Extract the parent directory from a file or directory path.\n *\n * Handles edge cases such as root-level files (`\"/file.txt\"` → `\"/\"`),\n * paths without slashes (`\"file.txt\"` → `\"\"`), and trailing slashes.\n */\nexport function parentDirectory(path: string): string {\n const normalized =\n path.length > 1 && path.endsWith(\"/\") ? path.slice(0, -1) : path;\n const lastSlash = normalized.lastIndexOf(\"/\");\n\n if (lastSlash > 0) return normalized.substring(0, lastSlash);\n if (normalized.startsWith(\"/\")) return \"/\";\n return \"\";\n}\n\n/**\n * Sanitize a filename for use in a `Content-Disposition` HTTP header.\n *\n * Redundancy check – Unity Catalog is unlikely to allow filenames with\n * quotes or control characters, but we sanitize defensively to prevent\n * HTTP header injection if upstream constraints ever change.\n */\nexport function sanitizeFilename(raw: string): string {\n // oxlint-disable-next-line no-control-regex -- filenames must not contain ASCII control characters\n return raw.replace(/[\"\\\\]/g, \"\\\\$&\").replace(/[\\x00-\\x1f]/g, \"\");\n}\n"],"mappings":";;;;;;;;;AAWA,SAAgB,gBAAgB,MAAsB;CACpD,MAAM,aACJ,KAAK,SAAS,KAAK,KAAK,SAAS,IAAI,GAAG,KAAK,MAAM,GAAG,GAAG,GAAG;CAC9D,MAAM,YAAY,WAAW,YAAY,IAAI;AAE7C,KAAI,YAAY,EAAG,QAAO,WAAW,UAAU,GAAG,UAAU;AAC5D,KAAI,WAAW,WAAW,IAAI,CAAE,QAAO;AACvC,QAAO;;;;;;;;;AAUT,SAAgB,iBAAiB,KAAqB;AAEpD,QAAO,IAAI,QAAQ,UAAU,OAAO,CAAC,QAAQ,gBAAgB,GAAG"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","names":[],"sources":["../../../src/plugins/files/plugin.ts"],"mappings":";;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","names":[],"sources":["../../../src/plugins/files/plugin.ts"],"mappings":";;;;;;;;;;;;;cAmEa,WAAA,SAAoB,MAAA,YAAkB,YAAA;EACjD,IAAA;;SAGO,QAAA,EAAuB,cAAA;EAAA,iBACb,WAAA;EAAA,UACC,MAAA,EAAQ,YAAA;EAAA,QAElB,gBAAA;EAAA,QACA,aAAA;EAAA,QACA,UAAA;EAAA,QACA,KAAA;;;;;;SAOD,eAAA,CAAgB,MAAA,EAAQ,YAAA,GAAe,MAAA,SAAe,YAAA;EA4CtB;;;;;;;;;;;;;;;;;;;;;;;;;EAAA,OAAhC,uBAAA,CAAwB,MAAA,EAAQ,YAAA,GAAe,mBAAA;EAxD5B;;;;;;;;;;;;;;;;;;EAAA,QA+FlB,YAAA;EA+JY;;;;;;;;;;EAAA,QA/HZ,eAAA;EAkcM;;;;EAAA,QA1aA,YAAA;EAmqBA;;;;;;;;;;;;;;;;;;;EAAA,QApnBA,cAAA;cAwDF,MAAA,EAAQ,YAAA;EAiDpB,YAAA,CAAa,MAAA,EAAQ,UAAA;EA8uCb;;;;EAAA,QAhnCA,cAAA;EAwuCR;;;;;;;;;;;;;EAAA,QAxsCQ,cAAA;EA+uCG;;;;EAAA,QA5tCH,YAAA;EAAA,QAQA,aAAA;EAAA,QAyBA,cAAA;EAAA,QAaA,iBAAA;EAuuCQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAAA,QAprCF,oBAAA;EAAA,QAiFN,eAAA;EAAA,QAuCA,gBAAA;EAAA,QAOM,WAAA;EAAA,QAsCA,WAAA;EAAA,QAoFA,eAAA;EAAA,QAWA,UAAA;;;;;;UAgBA,UAAA;EAAA,QAqFA,aAAA;EAAA,QA0CA,eAAA;EAAA,QA0CA,cAAA;EAAA,QA0CA,aAAA;EAAA,QAkJA,YAAA;EAAA,QA8CA,aAAA;EAAA,QAgDN,YAAA;;;;;;;;;;UAmBA,uBAAA;;;;;;;;;;;;;UAmBA,mBAAA;;;;;;;;;;;;UAiBA,sBAAA;;;;;;;;;;;;;;;UA8BM,YAAA;;;;;;;;;;;;;;;;UAyBN,uBAAA;;;;;;;;;;;UAkBA,wBAAA;;;;;;;;;;;;;UAoCA,2BAAA;;;;;;YAgCE,eAAA,CACR,SAAA,UACA,IAAA,EAAM,cAAA,GACL,SAAA;;;;;;;;;;;UA8DK,kBAAA;EAAA,QA4FA,cAAA;EAAA,QAEA,UAAA;EAOF,QAAA,CAAA,GAAY,OAAA;EAmBlB,aAAA,CAAA,GAAiB,mBAAA;EAIX,gBAAA,CACJ,IAAA,UACA,IAAA,WACA,MAAA,GAAS,WAAA,GACR,OAAA;EAIH,OAAA,CAAQ,IAAA,GAJE,cAAA,GAIoD,MAAA,SAAA,YAAA;;;;;;;;;;;;;;;;;;;;;;;;EA2B9D,OAAA,CAAA,GAAW,WAAA;EAiDX,YAAA,CAAA,GAAgB,MAAA;AAAA;;;;cAQL,KAAA,EAAK,QAAA,QAAA,WAAA,EAAA,YAAA;;gCAAA,UAAA"}
|