From a0629130150ce4e6935fd18deccb6fd7304fb44d Mon Sep 17 00:00:00 2001 From: Per Fuchs Date: Wed, 15 Jul 2026 14:42:15 +0200 Subject: [PATCH] hyper: accept kebab-case plan JSON keys (backward compatible) Hyper PR #13364 (hyper-db) renamed plan JSON property keys from camelCase to kebab-case (e.g. operatorId -> operator-id, debugName -> debug-name, valueForComparison -> value-for-comparison). Only keys changed; enum/tag values (operator tags, join types, ...) keep their camelCase spelling. The Hyper loader is largely key-agnostic, but it reads three renamed keys by literal name. Accept both spellings so plans produced before and after the cutover both render: - operatorId / operator-id (operator id map + crosslink resolution) - debugName / debug-name (display name + always-shown property) - valueForComparison / value-for-comparison (fixed child ordering) Other referenced keys (analyze, sqlpos, cpu-cycles, tuple-count, and the crosslink source keys magic/builder/input/source) were not renamed by the PR and are unchanged. Adds a kebab-case tablescan example plan. --- query-graphs/src/hyper.ts | 19 +++++++++++------ .../hyper/tablescan-kebab-case.plan.json | 21 +++++++++++++++++++ 2 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 standalone-app/examples/hyper/tablescan-kebab-case.plan.json diff --git a/query-graphs/src/hyper.ts b/query-graphs/src/hyper.ts index b9c8c6f..f431088 100644 --- a/query-graphs/src/hyper.ts +++ b/query-graphs/src/hyper.ts @@ -140,8 +140,10 @@ function convertHyperNode(rawNode: Json, parentKey, conversionState: ConversionS } } - // Display these properties always as properties, even if they are more complex - const propertyKeys = ["debugName", "analyze", "sqlpos"]; + // Display these properties always as properties, even if they are more complex. + // `debugName` is the pre-kebab-case spelling of `debug-name`; we accept both for + // backwards compatibility with plans produced before the Hyper kebab-case cutover. + const propertyKeys = ["debug-name", "debugName", "analyze", "sqlpos"]; for (const key of propertyKeys) { if (!rawNode.hasOwnProperty(key)) { continue; @@ -152,7 +154,9 @@ function convertHyperNode(rawNode: Json, parentKey, conversionState: ConversionS // Determine the order in which other keys are displayed. // For some keys, we enforce a specific order here (e.g., "left" comes before "right"). // For all other keys, we use alphabetic order. - const fixedChildOrder = ["inputs", "input", "left", "right", "value", "valueForComparison"]; + // `value-for-comparison` / `valueForComparison`: both spellings are listed so the + // fixed child ordering works for plans from before and after the kebab-case cutover. + const fixedChildOrder = ["inputs", "input", "left", "right", "value", "value-for-comparison", "valueForComparison"]; const orderedKeys = Object.getOwnPropertyNames(rawNode) .filter((k) => { // `propertyKeys` and `operator`/`expression` were already handled @@ -210,7 +214,9 @@ function convertHyperNode(rawNode: Json, parentKey, conversionState: ConversionS // Figure out the display name const specificDisplayName = renderingConfig.displayNameKey ? properties.get(renderingConfig.displayNameKey) : undefined; - const debugNameNode = tryGetPropertyPath(rawNode, ["debugName", "value"]); + // Accept both `debug-name` (post kebab-case cutover) and the legacy `debugName`. + const debugNameNode = + tryGetPropertyPath(rawNode, ["debug-name", "value"]) ?? tryGetPropertyPath(rawNode, ["debugName", "value"]); const debugName = typeof debugNameNode === "string" ? debugNameNode : undefined; const displayName = debugName ?? specificDisplayName ?? properties?.get("name") ?? nodeTag ?? ""; @@ -257,9 +263,10 @@ function convertHyperNode(rawNode: Json, parentKey, conversionState: ConversionS } } - // Add to `operatorId` map if applicable + // Add to `operator-id` map if applicable. + // `operatorId` is the legacy spelling; accept both for backwards compatibility. if (nodeType == "operator") { - const operatorId = properties?.get("operatorId"); + const operatorId = properties?.get("operator-id") ?? properties?.get("operatorId"); if (operatorId !== undefined) { conversionState.operatorsById.set(operatorId, convertedNode); } diff --git a/standalone-app/examples/hyper/tablescan-kebab-case.plan.json b/standalone-app/examples/hyper/tablescan-kebab-case.plan.json new file mode 100644 index 0000000..c8223cc --- /dev/null +++ b/standalone-app/examples/hyper/tablescan-kebab-case.plan.json @@ -0,0 +1,21 @@ +{ + "operator": "executiontarget", + "operator-id": 1, + "cardinality": 5, + "produces-rows": true, + "output": [{"expression": "iuref", "iu": ["scan_r_name", ["Char", 25]]}], + "output-names": ["r_name"], + "inputs": [{ + "operator": "tablescan", + "operator-id": 2, + "sqlpos": [[41, 47]], + "cardinality": 5, + "volatility": "stable", + "relation-id": 9, + "schema": {"type":"sessionschema"}, + "attribute-count": 3, + "attributes": [{"col-id": 1, "name": "r_name", "type": ["Char", 25], "iu": ["scan_r_name", ["Char", 25]]}], + "debug-name": {"classification": "nonsensitive", "value": "region"}, + "selectivity": 1 + }] +}