From e3903a3ed986d712d3a12b5465adc4a2af42a1ab Mon Sep 17 00:00:00 2001 From: eric-wang-1990 Date: Tue, 2 Jun 2026 01:20:52 +0000 Subject: [PATCH 01/17] ci: use UC REST API for schema create/drop in Reyden nightly Reyden warehouse rejects CREATE/DROP SCHEMA via the SQL statement API ("Unsupported CREATE type: SCHEMA"). Switch to the Unity Catalog REST API (/api/2.1/unity-catalog/schemas) for both provision and cleanup. --- .github/workflows/reyden-rest-nightly.yml | 36 +++++++++-------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/.github/workflows/reyden-rest-nightly.yml b/.github/workflows/reyden-rest-nightly.yml index 94f0ef4b0..8365ee5c2 100644 --- a/.github/workflows/reyden-rest-nightly.yml +++ b/.github/workflows/reyden-rest-nightly.yml @@ -79,22 +79,22 @@ jobs: env: OAUTH_TOKEN: ${{ steps.oauth.outputs.OAUTH_TOKEN }} PER_RUN_SCHEMA: ${{ steps.schema.outputs.PER_RUN_SCHEMA }} - WAREHOUSE_ID: ${{ steps.schema.outputs.WAREHOUSE_ID }} run: | set -e - STATEMENT="CREATE SCHEMA IF NOT EXISTS main.${PER_RUN_SCHEMA}" - PAYLOAD=$(jq -n --arg w "$WAREHOUSE_ID" --arg s "$STATEMENT" '{warehouse_id: $w, statement: $s, wait_timeout: "30s"}') - RESPONSE=$(curl -sS -X POST "https://${{ env.DATABRICKS_SERVER_HOSTNAME }}/api/2.0/sql/statements" \ + # Reyden warehouse rejects CREATE SCHEMA via the SQL statement API; + # use the Unity Catalog REST API instead. + PAYLOAD=$(jq -n --arg name "$PER_RUN_SCHEMA" '{catalog_name: "main", name: $name}') + RESPONSE=$(curl -sS -X POST "https://${{ env.DATABRICKS_SERVER_HOSTNAME }}/api/2.1/unity-catalog/schemas" \ -H "Authorization: Bearer $OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d "$PAYLOAD") - STATUS=$(echo "$RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin).get('status', {}).get('state', ''))") - if [ "$STATUS" != "SUCCEEDED" ]; then - echo "ERROR: CREATE SCHEMA failed (status=$STATUS)" + FULL_NAME=$(echo "$RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin).get('full_name', ''))") + if [ -z "$FULL_NAME" ]; then + echo "ERROR: CREATE SCHEMA failed" echo "Response: $RESPONSE" exit 1 fi - echo "Created schema: main.${PER_RUN_SCHEMA}" + echo "Created schema: $FULL_NAME" - name: Seed test data env: @@ -191,18 +191,10 @@ jobs: env: OAUTH_TOKEN: ${{ steps.oauth.outputs.OAUTH_TOKEN }} PER_RUN_SCHEMA: ${{ steps.schema.outputs.PER_RUN_SCHEMA }} - WAREHOUSE_ID: ${{ steps.schema.outputs.WAREHOUSE_ID }} run: | - STATEMENT="DROP SCHEMA IF EXISTS main.${PER_RUN_SCHEMA} CASCADE" - PAYLOAD=$(jq -n --arg w "$WAREHOUSE_ID" --arg s "$STATEMENT" '{warehouse_id: $w, statement: $s, wait_timeout: "30s"}') - RESPONSE=$(curl -sS -X POST "https://${{ env.DATABRICKS_SERVER_HOSTNAME }}/api/2.0/sql/statements" \ - -H "Authorization: Bearer $OAUTH_TOKEN" \ - -H "Content-Type: application/json" \ - -d "$PAYLOAD") - STATUS=$(echo "$RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin).get('status', {}).get('state', ''))") - if [ "$STATUS" != "SUCCEEDED" ]; then - echo "WARNING: DROP SCHEMA failed (status=$STATUS)" - echo "Response: $RESPONSE" - else - echo "Dropped schema: main.${PER_RUN_SCHEMA}" - fi + # Use the Unity Catalog REST API to match how the schema was created. + RESPONSE=$(curl -sS -X DELETE \ + "https://${{ env.DATABRICKS_SERVER_HOSTNAME }}/api/2.1/unity-catalog/schemas/main.${PER_RUN_SCHEMA}?force=true" \ + -H "Authorization: Bearer $OAUTH_TOKEN") + echo "Drop response: $RESPONSE" + echo "Dropped schema: main.${PER_RUN_SCHEMA}" From 3f2b4858d69919ba7b6e9c48cdbd38f1ca16a370 Mon Sep 17 00:00:00 2001 From: eric-wang-1990 Date: Tue, 2 Jun 2026 01:29:19 +0000 Subject: [PATCH 02/17] ci: skip schema provisioning for Reyden nightly Reyden does not support DDL. Remove the per-run schema create/seed/drop steps and point the nightly at the pre-existing main.adbc_testing schema instead. --- .github/workflows/reyden-rest-nightly.yml | 100 +--------------------- 1 file changed, 3 insertions(+), 97 deletions(-) diff --git a/.github/workflows/reyden-rest-nightly.yml b/.github/workflows/reyden-rest-nightly.yml index 8365ee5c2..5a9fd1414 100644 --- a/.github/workflows/reyden-rest-nightly.yml +++ b/.github/workflows/reyden-rest-nightly.yml @@ -62,88 +62,7 @@ jobs: echo "::add-mask::$OAUTH_TOKEN" echo "OAUTH_TOKEN=$OAUTH_TOKEN" >> $GITHUB_OUTPUT - - name: Compute per-run schema name - id: schema - run: | - PER_RUN_SCHEMA="adbc_testing_run_${{ github.run_id }}_${{ github.run_attempt }}_rest" - echo "PER_RUN_SCHEMA=$PER_RUN_SCHEMA" >> $GITHUB_OUTPUT - echo "Per-run schema: main.$PER_RUN_SCHEMA" - WAREHOUSE_ID=$(echo "${{ env.DATABRICKS_HTTP_PATH }}" | awk -F/ '{print $NF}') - if [ -z "$WAREHOUSE_ID" ]; then - echo "ERROR: Could not extract warehouse id from DATABRICKS_HTTP_PATH" - exit 1 - fi - echo "WAREHOUSE_ID=$WAREHOUSE_ID" >> $GITHUB_OUTPUT - - - name: Provision per-run schema - env: - OAUTH_TOKEN: ${{ steps.oauth.outputs.OAUTH_TOKEN }} - PER_RUN_SCHEMA: ${{ steps.schema.outputs.PER_RUN_SCHEMA }} - run: | - set -e - # Reyden warehouse rejects CREATE SCHEMA via the SQL statement API; - # use the Unity Catalog REST API instead. - PAYLOAD=$(jq -n --arg name "$PER_RUN_SCHEMA" '{catalog_name: "main", name: $name}') - RESPONSE=$(curl -sS -X POST "https://${{ env.DATABRICKS_SERVER_HOSTNAME }}/api/2.1/unity-catalog/schemas" \ - -H "Authorization: Bearer $OAUTH_TOKEN" \ - -H "Content-Type: application/json" \ - -d "$PAYLOAD") - FULL_NAME=$(echo "$RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin).get('full_name', ''))") - if [ -z "$FULL_NAME" ]; then - echo "ERROR: CREATE SCHEMA failed" - echo "Response: $RESPONSE" - exit 1 - fi - echo "Created schema: $FULL_NAME" - - - name: Seed test data - env: - OAUTH_TOKEN: ${{ steps.oauth.outputs.OAUTH_TOKEN }} - PER_RUN_SCHEMA: ${{ steps.schema.outputs.PER_RUN_SCHEMA }} - WAREHOUSE_ID: ${{ steps.schema.outputs.WAREHOUSE_ID }} - DATABRICKS_SERVER_HOSTNAME: ${{ env.DATABRICKS_SERVER_HOSTNAME }} - run: | - python3 - <<'PYEOF' - import json, os, re, sys, urllib.request - - with open("csharp/test/Resources/Databricks.sql") as f: - content = f.read() - content = re.sub(r"^\s*--.*$", "", content, flags=re.MULTILINE) - full_table = f"main.{os.environ['PER_RUN_SCHEMA']}.adbc_testing_table" - content = content.replace("{ADBC_CATALOG}.{ADBC_DATASET}.{ADBC_TABLE}", full_table) - statements = [s.strip() for s in content.split(";") if s.strip()] - - host = os.environ["DATABRICKS_SERVER_HOSTNAME"] - token = os.environ["OAUTH_TOKEN"] - warehouse = os.environ["WAREHOUSE_ID"] - url = f"https://{host}/api/2.0/sql/statements" - - for i, stmt in enumerate(statements, 1): - payload = json.dumps({ - "warehouse_id": warehouse, - "statement": stmt, - "wait_timeout": "30s", - }).encode() - req = urllib.request.Request( - url, data=payload, - headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, - method="POST", - ) - with urllib.request.urlopen(req) as resp: - body = json.loads(resp.read()) - state = body.get("status", {}).get("state", "") - if state != "SUCCEEDED": - print(f"ERROR: statement {i} failed (state={state})") - print(f"Statement: {stmt[:200]}") - print(f"Response: {json.dumps(body, indent=2)}") - sys.exit(1) - print(f"OK [{i}/{len(statements)}]: {stmt[:80].replace(chr(10), ' ')}") - print(f"Seeded {len(statements)} statements into main.{os.environ['PER_RUN_SCHEMA']}") - PYEOF - - name: Create Databricks config file - env: - PER_RUN_SCHEMA: ${{ steps.schema.outputs.PER_RUN_SCHEMA }} run: | mkdir -p ~/.databricks cat > ~/.databricks/connection.json << EOF @@ -158,8 +77,8 @@ jobs: "type": "databricks", "protocol": "rest", "catalog": "main", - "db_schema": "${PER_RUN_SCHEMA}", - "query": "SELECT * FROM main.${PER_RUN_SCHEMA}.adbc_testing_table", + "db_schema": "adbc_testing", + "query": "SELECT * FROM main.adbc_testing.adbc_testing_table", "expectedResults": 12, "isCITesting": true, "tracePropagationEnabled": "true", @@ -167,7 +86,7 @@ jobs: "traceStateEnabled": "false", "metadata": { "catalog": "main", - "schema": "${PER_RUN_SCHEMA}", + "schema": "adbc_testing", "table": "adbc_testing_table", "expectedColumnCount": 19 } @@ -185,16 +104,3 @@ jobs: run: | export DATABRICKS_TEST_CONFIG_FILE="$HOME/.databricks/connection.json" ./ci/scripts/csharp_test_databricks_e2e.sh "${{ github.workspace }}" - - - name: Drop per-run schema - if: always() && steps.schema.outputs.PER_RUN_SCHEMA != '' - env: - OAUTH_TOKEN: ${{ steps.oauth.outputs.OAUTH_TOKEN }} - PER_RUN_SCHEMA: ${{ steps.schema.outputs.PER_RUN_SCHEMA }} - run: | - # Use the Unity Catalog REST API to match how the schema was created. - RESPONSE=$(curl -sS -X DELETE \ - "https://${{ env.DATABRICKS_SERVER_HOSTNAME }}/api/2.1/unity-catalog/schemas/main.${PER_RUN_SCHEMA}?force=true" \ - -H "Authorization: Bearer $OAUTH_TOKEN") - echo "Drop response: $RESPONSE" - echo "Dropped schema: main.${PER_RUN_SCHEMA}" From 6a9501fb495530e6c98050f19e51edd971d57fc4 Mon Sep 17 00:00:00 2001 From: eric-wang-1990 Date: Tue, 2 Jun 2026 01:36:05 +0000 Subject: [PATCH 03/17] ci: use regular warehouse for setup, Reyden only for test execution Reyden does not support any DDL (CREATE/DROP schema/table). Split the two concerns: SETUP_HTTP_PATH (TEST_PECO_WAREHOUSE_HTTP_PATH) handles schema provision, seeding, and cleanup; DATABRICKS_HTTP_PATH (TEST_PECO_REYDEN_HTTP_PATH) is used only in the test connection config. --- .github/workflows/reyden-rest-nightly.yml | 112 +++++++++++++++++++++- 1 file changed, 109 insertions(+), 3 deletions(-) diff --git a/.github/workflows/reyden-rest-nightly.yml b/.github/workflows/reyden-rest-nightly.yml index 5a9fd1414..c236c4824 100644 --- a/.github/workflows/reyden-rest-nightly.yml +++ b/.github/workflows/reyden-rest-nightly.yml @@ -30,7 +30,10 @@ jobs: runs-on: ubuntu-latest env: DATABRICKS_SERVER_HOSTNAME: ${{ secrets.DATABRICKS_HOST }} + # Reyden warehouse — used only for test execution (no DDL support) DATABRICKS_HTTP_PATH: ${{ secrets.TEST_PECO_REYDEN_HTTP_PATH }} + # Regular DBSQL warehouse — used for schema/table setup and teardown + SETUP_HTTP_PATH: ${{ secrets.TEST_PECO_WAREHOUSE_HTTP_PATH }} DATABRICKS_TEST_CLIENT_ID: ${{ secrets.DATABRICKS_TEST_CLIENT_ID }} DATABRICKS_TEST_CLIENT_SECRET: ${{ secrets.DATABRICKS_TEST_CLIENT_SECRET }} steps: @@ -62,7 +65,89 @@ jobs: echo "::add-mask::$OAUTH_TOKEN" echo "OAUTH_TOKEN=$OAUTH_TOKEN" >> $GITHUB_OUTPUT + - name: Compute per-run schema name + id: schema + run: | + PER_RUN_SCHEMA="adbc_testing_run_${{ github.run_id }}_${{ github.run_attempt }}_rest" + echo "PER_RUN_SCHEMA=$PER_RUN_SCHEMA" >> $GITHUB_OUTPUT + echo "Per-run schema: main.$PER_RUN_SCHEMA" + # Use the regular warehouse ID for setup operations + WAREHOUSE_ID=$(echo "${{ env.SETUP_HTTP_PATH }}" | awk -F/ '{print $NF}') + if [ -z "$WAREHOUSE_ID" ]; then + echo "ERROR: Could not extract warehouse id from SETUP_HTTP_PATH" + exit 1 + fi + echo "WAREHOUSE_ID=$WAREHOUSE_ID" >> $GITHUB_OUTPUT + + - name: Provision per-run schema + env: + OAUTH_TOKEN: ${{ steps.oauth.outputs.OAUTH_TOKEN }} + PER_RUN_SCHEMA: ${{ steps.schema.outputs.PER_RUN_SCHEMA }} + WAREHOUSE_ID: ${{ steps.schema.outputs.WAREHOUSE_ID }} + run: | + set -e + STATEMENT="CREATE SCHEMA IF NOT EXISTS main.${PER_RUN_SCHEMA}" + PAYLOAD=$(jq -n --arg w "$WAREHOUSE_ID" --arg s "$STATEMENT" '{warehouse_id: $w, statement: $s, wait_timeout: "30s"}') + RESPONSE=$(curl -sS -X POST "https://${{ env.DATABRICKS_SERVER_HOSTNAME }}/api/2.0/sql/statements" \ + -H "Authorization: Bearer $OAUTH_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD") + STATUS=$(echo "$RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin).get('status', {}).get('state', ''))") + if [ "$STATUS" != "SUCCEEDED" ]; then + echo "ERROR: CREATE SCHEMA failed (status=$STATUS)" + echo "Response: $RESPONSE" + exit 1 + fi + echo "Created schema: main.${PER_RUN_SCHEMA}" + + - name: Seed test data + env: + OAUTH_TOKEN: ${{ steps.oauth.outputs.OAUTH_TOKEN }} + PER_RUN_SCHEMA: ${{ steps.schema.outputs.PER_RUN_SCHEMA }} + WAREHOUSE_ID: ${{ steps.schema.outputs.WAREHOUSE_ID }} + DATABRICKS_SERVER_HOSTNAME: ${{ env.DATABRICKS_SERVER_HOSTNAME }} + run: | + python3 - <<'PYEOF' + import json, os, re, sys, urllib.request + + with open("csharp/test/Resources/Databricks.sql") as f: + content = f.read() + content = re.sub(r"^\s*--.*$", "", content, flags=re.MULTILINE) + full_table = f"main.{os.environ['PER_RUN_SCHEMA']}.adbc_testing_table" + content = content.replace("{ADBC_CATALOG}.{ADBC_DATASET}.{ADBC_TABLE}", full_table) + statements = [s.strip() for s in content.split(";") if s.strip()] + + host = os.environ["DATABRICKS_SERVER_HOSTNAME"] + token = os.environ["OAUTH_TOKEN"] + warehouse = os.environ["WAREHOUSE_ID"] + url = f"https://{host}/api/2.0/sql/statements" + + for i, stmt in enumerate(statements, 1): + payload = json.dumps({ + "warehouse_id": warehouse, + "statement": stmt, + "wait_timeout": "30s", + }).encode() + req = urllib.request.Request( + url, data=payload, + headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, + method="POST", + ) + with urllib.request.urlopen(req) as resp: + body = json.loads(resp.read()) + state = body.get("status", {}).get("state", "") + if state != "SUCCEEDED": + print(f"ERROR: statement {i} failed (state={state})") + print(f"Statement: {stmt[:200]}") + print(f"Response: {json.dumps(body, indent=2)}") + sys.exit(1) + print(f"OK [{i}/{len(statements)}]: {stmt[:80].replace(chr(10), ' ')}") + print(f"Seeded {len(statements)} statements into main.{os.environ['PER_RUN_SCHEMA']}") + PYEOF + - name: Create Databricks config file + env: + PER_RUN_SCHEMA: ${{ steps.schema.outputs.PER_RUN_SCHEMA }} run: | mkdir -p ~/.databricks cat > ~/.databricks/connection.json << EOF @@ -77,8 +162,8 @@ jobs: "type": "databricks", "protocol": "rest", "catalog": "main", - "db_schema": "adbc_testing", - "query": "SELECT * FROM main.adbc_testing.adbc_testing_table", + "db_schema": "${PER_RUN_SCHEMA}", + "query": "SELECT * FROM main.${PER_RUN_SCHEMA}.adbc_testing_table", "expectedResults": 12, "isCITesting": true, "tracePropagationEnabled": "true", @@ -86,7 +171,7 @@ jobs: "traceStateEnabled": "false", "metadata": { "catalog": "main", - "schema": "adbc_testing", + "schema": "${PER_RUN_SCHEMA}", "table": "adbc_testing_table", "expectedColumnCount": 19 } @@ -104,3 +189,24 @@ jobs: run: | export DATABRICKS_TEST_CONFIG_FILE="$HOME/.databricks/connection.json" ./ci/scripts/csharp_test_databricks_e2e.sh "${{ github.workspace }}" + + - name: Drop per-run schema + if: always() && steps.schema.outputs.PER_RUN_SCHEMA != '' + env: + OAUTH_TOKEN: ${{ steps.oauth.outputs.OAUTH_TOKEN }} + PER_RUN_SCHEMA: ${{ steps.schema.outputs.PER_RUN_SCHEMA }} + WAREHOUSE_ID: ${{ steps.schema.outputs.WAREHOUSE_ID }} + run: | + STATEMENT="DROP SCHEMA IF EXISTS main.${PER_RUN_SCHEMA} CASCADE" + PAYLOAD=$(jq -n --arg w "$WAREHOUSE_ID" --arg s "$STATEMENT" '{warehouse_id: $w, statement: $s, wait_timeout: "30s"}') + RESPONSE=$(curl -sS -X POST "https://${{ env.DATABRICKS_SERVER_HOSTNAME }}/api/2.0/sql/statements" \ + -H "Authorization: Bearer $OAUTH_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD") + STATUS=$(echo "$RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin).get('status', {}).get('state', ''))") + if [ "$STATUS" != "SUCCEEDED" ]; then + echo "WARNING: DROP SCHEMA failed (status=$STATUS)" + echo "Response: $RESPONSE" + else + echo "Dropped schema: main.${PER_RUN_SCHEMA}" + fi From ce48179bad25764600338021364101c2eb737eb7 Mon Sep 17 00:00:00 2001 From: eric-wang-1990 Date: Tue, 2 Jun 2026 02:26:49 +0000 Subject: [PATCH 04/17] ci: make e2e-tests callable, reyden nightly as thin wrapper Add workflow_call to e2e-tests.yml with an optional execution_http_path secret. Setup/teardown always use TEST_PECO_WAREHOUSE_HTTP_PATH (supports DDL); test execution uses execution_http_path when provided, falling back to the same regular warehouse. reyden-rest-nightly.yml is now a 10-line wrapper that calls e2e-tests.yml with protocol=rest and TEST_PECO_REYDEN_HTTP_PATH as the execution path. --- .github/workflows/e2e-tests.yml | 17 +- .github/workflows/reyden-rest-nightly.yml | 193 +--------------------- 2 files changed, 21 insertions(+), 189 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 22609b029..cbb2fd376 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -15,6 +15,17 @@ name: Tests Workflow on: + workflow_call: + inputs: + protocol: + description: 'Protocol to test (thrift, rest, or both)' + required: false + default: 'rest' + type: string + secrets: + execution_http_path: + description: 'HTTP path for test execution (overrides TEST_PECO_WAREHOUSE_HTTP_PATH). Setup/teardown always use TEST_PECO_WAREHOUSE_HTTP_PATH.' + required: false workflow_dispatch: inputs: protocol: @@ -51,7 +62,11 @@ jobs: protocol: ${{ inputs.protocol == 'thrift' && fromJson('["thrift"]') || inputs.protocol == 'rest' && fromJson('["rest"]') || fromJson('["thrift","rest"]') }} env: DATABRICKS_SERVER_HOSTNAME: ${{ secrets.DATABRICKS_HOST }} + # Setup/teardown always use the regular warehouse (supports DDL). + # Test execution uses execution_http_path when provided (e.g. Reyden), + # otherwise falls back to the same regular warehouse. DATABRICKS_HTTP_PATH: ${{ secrets.TEST_PECO_WAREHOUSE_HTTP_PATH }} + EXECUTION_HTTP_PATH: ${{ secrets.execution_http_path || secrets.TEST_PECO_WAREHOUSE_HTTP_PATH }} DATABRICKS_TEST_CLIENT_ID: ${{ secrets.DATABRICKS_TEST_CLIENT_ID }} DATABRICKS_TEST_CLIENT_SECRET: ${{ secrets.DATABRICKS_TEST_CLIENT_SECRET }} steps: @@ -216,7 +231,7 @@ jobs: mkdir -p ~/.databricks cat > ~/.databricks/connection.json << EOF { - "uri": "https://${{ env.DATABRICKS_SERVER_HOSTNAME }}${{ env.DATABRICKS_HTTP_PATH }}", + "uri": "https://${{ env.DATABRICKS_SERVER_HOSTNAME }}${{ env.EXECUTION_HTTP_PATH }}", "auth_type": "oauth", "grant_type": "client_credentials", "client_id": "${{ env.DATABRICKS_TEST_CLIENT_ID }}", diff --git a/.github/workflows/reyden-rest-nightly.yml b/.github/workflows/reyden-rest-nightly.yml index c236c4824..61fdf9766 100644 --- a/.github/workflows/reyden-rest-nightly.yml +++ b/.github/workflows/reyden-rest-nightly.yml @@ -20,193 +20,10 @@ on: - cron: '0 2 * * *' workflow_dispatch: -concurrency: - group: ${{ github.repository }}-reyden-rest-nightly - cancel-in-progress: false - jobs: reyden-rest-nightly: - name: "Reyden REST Nightly E2E" - runs-on: ubuntu-latest - env: - DATABRICKS_SERVER_HOSTNAME: ${{ secrets.DATABRICKS_HOST }} - # Reyden warehouse — used only for test execution (no DDL support) - DATABRICKS_HTTP_PATH: ${{ secrets.TEST_PECO_REYDEN_HTTP_PATH }} - # Regular DBSQL warehouse — used for schema/table setup and teardown - SETUP_HTTP_PATH: ${{ secrets.TEST_PECO_WAREHOUSE_HTTP_PATH }} - DATABRICKS_TEST_CLIENT_ID: ${{ secrets.DATABRICKS_TEST_CLIENT_ID }} - DATABRICKS_TEST_CLIENT_SECRET: ${{ secrets.DATABRICKS_TEST_CLIENT_SECRET }} - steps: - - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - submodules: recursive - fetch-depth: 0 - - - name: Set up .NET - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 - with: - dotnet-version: '8.0.x' - - - name: Generate OAuth access token - id: oauth - run: | - OAUTH_RESPONSE=$(curl -s -X POST "https://${{ env.DATABRICKS_SERVER_HOSTNAME }}/oidc/v1/token" \ - -H "Content-Type: application/x-www-form-urlencoded" \ - -d "grant_type=client_credentials" \ - -d "client_id=${{ env.DATABRICKS_TEST_CLIENT_ID }}" \ - -d "client_secret=${{ env.DATABRICKS_TEST_CLIENT_SECRET }}" \ - -d "scope=sql") - OAUTH_TOKEN=$(echo "$OAUTH_RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin)['access_token'])") - if [ -z "$OAUTH_TOKEN" ]; then - echo "ERROR: Failed to generate OAuth token" - exit 1 - fi - echo "::add-mask::$OAUTH_TOKEN" - echo "OAUTH_TOKEN=$OAUTH_TOKEN" >> $GITHUB_OUTPUT - - - name: Compute per-run schema name - id: schema - run: | - PER_RUN_SCHEMA="adbc_testing_run_${{ github.run_id }}_${{ github.run_attempt }}_rest" - echo "PER_RUN_SCHEMA=$PER_RUN_SCHEMA" >> $GITHUB_OUTPUT - echo "Per-run schema: main.$PER_RUN_SCHEMA" - # Use the regular warehouse ID for setup operations - WAREHOUSE_ID=$(echo "${{ env.SETUP_HTTP_PATH }}" | awk -F/ '{print $NF}') - if [ -z "$WAREHOUSE_ID" ]; then - echo "ERROR: Could not extract warehouse id from SETUP_HTTP_PATH" - exit 1 - fi - echo "WAREHOUSE_ID=$WAREHOUSE_ID" >> $GITHUB_OUTPUT - - - name: Provision per-run schema - env: - OAUTH_TOKEN: ${{ steps.oauth.outputs.OAUTH_TOKEN }} - PER_RUN_SCHEMA: ${{ steps.schema.outputs.PER_RUN_SCHEMA }} - WAREHOUSE_ID: ${{ steps.schema.outputs.WAREHOUSE_ID }} - run: | - set -e - STATEMENT="CREATE SCHEMA IF NOT EXISTS main.${PER_RUN_SCHEMA}" - PAYLOAD=$(jq -n --arg w "$WAREHOUSE_ID" --arg s "$STATEMENT" '{warehouse_id: $w, statement: $s, wait_timeout: "30s"}') - RESPONSE=$(curl -sS -X POST "https://${{ env.DATABRICKS_SERVER_HOSTNAME }}/api/2.0/sql/statements" \ - -H "Authorization: Bearer $OAUTH_TOKEN" \ - -H "Content-Type: application/json" \ - -d "$PAYLOAD") - STATUS=$(echo "$RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin).get('status', {}).get('state', ''))") - if [ "$STATUS" != "SUCCEEDED" ]; then - echo "ERROR: CREATE SCHEMA failed (status=$STATUS)" - echo "Response: $RESPONSE" - exit 1 - fi - echo "Created schema: main.${PER_RUN_SCHEMA}" - - - name: Seed test data - env: - OAUTH_TOKEN: ${{ steps.oauth.outputs.OAUTH_TOKEN }} - PER_RUN_SCHEMA: ${{ steps.schema.outputs.PER_RUN_SCHEMA }} - WAREHOUSE_ID: ${{ steps.schema.outputs.WAREHOUSE_ID }} - DATABRICKS_SERVER_HOSTNAME: ${{ env.DATABRICKS_SERVER_HOSTNAME }} - run: | - python3 - <<'PYEOF' - import json, os, re, sys, urllib.request - - with open("csharp/test/Resources/Databricks.sql") as f: - content = f.read() - content = re.sub(r"^\s*--.*$", "", content, flags=re.MULTILINE) - full_table = f"main.{os.environ['PER_RUN_SCHEMA']}.adbc_testing_table" - content = content.replace("{ADBC_CATALOG}.{ADBC_DATASET}.{ADBC_TABLE}", full_table) - statements = [s.strip() for s in content.split(";") if s.strip()] - - host = os.environ["DATABRICKS_SERVER_HOSTNAME"] - token = os.environ["OAUTH_TOKEN"] - warehouse = os.environ["WAREHOUSE_ID"] - url = f"https://{host}/api/2.0/sql/statements" - - for i, stmt in enumerate(statements, 1): - payload = json.dumps({ - "warehouse_id": warehouse, - "statement": stmt, - "wait_timeout": "30s", - }).encode() - req = urllib.request.Request( - url, data=payload, - headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, - method="POST", - ) - with urllib.request.urlopen(req) as resp: - body = json.loads(resp.read()) - state = body.get("status", {}).get("state", "") - if state != "SUCCEEDED": - print(f"ERROR: statement {i} failed (state={state})") - print(f"Statement: {stmt[:200]}") - print(f"Response: {json.dumps(body, indent=2)}") - sys.exit(1) - print(f"OK [{i}/{len(statements)}]: {stmt[:80].replace(chr(10), ' ')}") - print(f"Seeded {len(statements)} statements into main.{os.environ['PER_RUN_SCHEMA']}") - PYEOF - - - name: Create Databricks config file - env: - PER_RUN_SCHEMA: ${{ steps.schema.outputs.PER_RUN_SCHEMA }} - run: | - mkdir -p ~/.databricks - cat > ~/.databricks/connection.json << EOF - { - "uri": "https://${{ env.DATABRICKS_SERVER_HOSTNAME }}${{ env.DATABRICKS_HTTP_PATH }}", - "auth_type": "oauth", - "grant_type": "client_credentials", - "client_id": "${{ env.DATABRICKS_TEST_CLIENT_ID }}", - "client_secret": "${{ env.DATABRICKS_TEST_CLIENT_SECRET }}", - "scope": "sql", - "access_token": "${{ steps.oauth.outputs.OAUTH_TOKEN }}", - "type": "databricks", - "protocol": "rest", - "catalog": "main", - "db_schema": "${PER_RUN_SCHEMA}", - "query": "SELECT * FROM main.${PER_RUN_SCHEMA}.adbc_testing_table", - "expectedResults": 12, - "isCITesting": true, - "tracePropagationEnabled": "true", - "traceParentHeaderName": "traceparent", - "traceStateEnabled": "false", - "metadata": { - "catalog": "main", - "schema": "${PER_RUN_SCHEMA}", - "table": "adbc_testing_table", - "expectedColumnCount": 19 - } - } - EOF - echo "DATABRICKS_TEST_CONFIG_FILE=$HOME/.databricks/connection.json" >> $GITHUB_ENV - - - name: Build - shell: bash - run: | - ./ci/scripts/csharp_build.sh "${{ github.workspace }}" - - - name: Run All Tests - shell: bash - run: | - export DATABRICKS_TEST_CONFIG_FILE="$HOME/.databricks/connection.json" - ./ci/scripts/csharp_test_databricks_e2e.sh "${{ github.workspace }}" - - - name: Drop per-run schema - if: always() && steps.schema.outputs.PER_RUN_SCHEMA != '' - env: - OAUTH_TOKEN: ${{ steps.oauth.outputs.OAUTH_TOKEN }} - PER_RUN_SCHEMA: ${{ steps.schema.outputs.PER_RUN_SCHEMA }} - WAREHOUSE_ID: ${{ steps.schema.outputs.WAREHOUSE_ID }} - run: | - STATEMENT="DROP SCHEMA IF EXISTS main.${PER_RUN_SCHEMA} CASCADE" - PAYLOAD=$(jq -n --arg w "$WAREHOUSE_ID" --arg s "$STATEMENT" '{warehouse_id: $w, statement: $s, wait_timeout: "30s"}') - RESPONSE=$(curl -sS -X POST "https://${{ env.DATABRICKS_SERVER_HOSTNAME }}/api/2.0/sql/statements" \ - -H "Authorization: Bearer $OAUTH_TOKEN" \ - -H "Content-Type: application/json" \ - -d "$PAYLOAD") - STATUS=$(echo "$RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin).get('status', {}).get('state', ''))") - if [ "$STATUS" != "SUCCEEDED" ]; then - echo "WARNING: DROP SCHEMA failed (status=$STATUS)" - echo "Response: $RESPONSE" - else - echo "Dropped schema: main.${PER_RUN_SCHEMA}" - fi + uses: ./.github/workflows/e2e-tests.yml + with: + protocol: rest + secrets: + execution_http_path: ${{ secrets.TEST_PECO_REYDEN_HTTP_PATH }} From df65d355071a913190ac83b2f8b5bb417fbe525c Mon Sep 17 00:00:00 2001 From: eric-wang-1990 Date: Tue, 2 Jun 2026 02:40:34 +0000 Subject: [PATCH 05/17] ci: fix secret inheritance in Reyden nightly workflow_call Replace explicit secret passing (which dropped DATABRICKS_HOST and credentials) with secrets: inherit. Control Reyden routing via a boolean input use_reyden_for_execution instead of a passed secret. --- .github/workflows/e2e-tests.yml | 12 ++++++------ .github/workflows/reyden-rest-nightly.yml | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index cbb2fd376..3867d2f15 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -22,10 +22,11 @@ on: required: false default: 'rest' type: string - secrets: - execution_http_path: - description: 'HTTP path for test execution (overrides TEST_PECO_WAREHOUSE_HTTP_PATH). Setup/teardown always use TEST_PECO_WAREHOUSE_HTTP_PATH.' + use_reyden_for_execution: + description: 'Route test execution through the Reyden warehouse (TEST_PECO_REYDEN_HTTP_PATH). Setup/teardown always use TEST_PECO_WAREHOUSE_HTTP_PATH.' required: false + default: false + type: boolean workflow_dispatch: inputs: protocol: @@ -63,10 +64,9 @@ jobs: env: DATABRICKS_SERVER_HOSTNAME: ${{ secrets.DATABRICKS_HOST }} # Setup/teardown always use the regular warehouse (supports DDL). - # Test execution uses execution_http_path when provided (e.g. Reyden), - # otherwise falls back to the same regular warehouse. + # Test execution uses Reyden when use_reyden_for_execution=true. DATABRICKS_HTTP_PATH: ${{ secrets.TEST_PECO_WAREHOUSE_HTTP_PATH }} - EXECUTION_HTTP_PATH: ${{ secrets.execution_http_path || secrets.TEST_PECO_WAREHOUSE_HTTP_PATH }} + EXECUTION_HTTP_PATH: ${{ inputs.use_reyden_for_execution && secrets.TEST_PECO_REYDEN_HTTP_PATH || secrets.TEST_PECO_WAREHOUSE_HTTP_PATH }} DATABRICKS_TEST_CLIENT_ID: ${{ secrets.DATABRICKS_TEST_CLIENT_ID }} DATABRICKS_TEST_CLIENT_SECRET: ${{ secrets.DATABRICKS_TEST_CLIENT_SECRET }} steps: diff --git a/.github/workflows/reyden-rest-nightly.yml b/.github/workflows/reyden-rest-nightly.yml index 61fdf9766..875560671 100644 --- a/.github/workflows/reyden-rest-nightly.yml +++ b/.github/workflows/reyden-rest-nightly.yml @@ -25,5 +25,5 @@ jobs: uses: ./.github/workflows/e2e-tests.yml with: protocol: rest - secrets: - execution_http_path: ${{ secrets.TEST_PECO_REYDEN_HTTP_PATH }} + use_reyden_for_execution: true + secrets: inherit From 98c670352bb096ba9765e873f8354d47ceeded3e Mon Sep 17 00:00:00 2001 From: eric-wang-1990 Date: Tue, 2 Jun 2026 03:34:14 +0000 Subject: [PATCH 06/17] ci: add e2e_only flag to skip unit tests in nightly run Add e2e_only boolean input to e2e-tests.yml workflow_call. When true, passes --filter FullyQualifiedName!~Tests.Unit to dotnet test, skipping the bulk of unit tests. The test script now forwards extra args to dotnet test via ${@:2}. Set e2e_only: true in reyden-rest-nightly.yml. --- .github/workflows/e2e-tests.yml | 11 ++++++++++- .github/workflows/reyden-rest-nightly.yml | 1 + ci/scripts/csharp_test_databricks_e2e.sh | 4 +--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 3867d2f15..71a6419f3 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -27,6 +27,11 @@ on: required: false default: false type: boolean + e2e_only: + description: 'Skip unit tests — run only E2E tests (excludes Tests.Unit namespace).' + required: false + default: false + type: boolean workflow_dispatch: inputs: protocol: @@ -269,7 +274,11 @@ jobs: shell: bash run: | export DATABRICKS_TEST_CONFIG_FILE="$HOME/.databricks/connection.json" - ./ci/scripts/csharp_test_databricks_e2e.sh "${{ github.workspace }}" + FILTER_ARG="" + if [[ "${{ inputs.e2e_only }}" == "true" ]]; then + FILTER_ARG="--filter FullyQualifiedName!~Tests.Unit" + fi + ./ci/scripts/csharp_test_databricks_e2e.sh "${{ github.workspace }}" $FILTER_ARG # Always run, even on test failure or job cancellation, so we don't # leak schemas in the workspace. diff --git a/.github/workflows/reyden-rest-nightly.yml b/.github/workflows/reyden-rest-nightly.yml index 875560671..2ec439150 100644 --- a/.github/workflows/reyden-rest-nightly.yml +++ b/.github/workflows/reyden-rest-nightly.yml @@ -26,4 +26,5 @@ jobs: with: protocol: rest use_reyden_for_execution: true + e2e_only: true secrets: inherit diff --git a/ci/scripts/csharp_test_databricks_e2e.sh b/ci/scripts/csharp_test_databricks_e2e.sh index de3a67054..a021a7014 100755 --- a/ci/scripts/csharp_test_databricks_e2e.sh +++ b/ci/scripts/csharp_test_databricks_e2e.sh @@ -16,10 +16,8 @@ set -ex -# Run all tests (both E2E and Unit tests) source_dir=${1}/csharp/test pushd ${source_dir} -# Run all tests in the Databricks test project -dotnet test --verbosity normal +dotnet test --verbosity normal ${@:2} popd From 167e356001020a0dc1b2e9c048a7017a7602039b Mon Sep 17 00:00:00 2001 From: eric-wang-1990 Date: Tue, 2 Jun 2026 07:00:46 +0000 Subject: [PATCH 07/17] test(csharp): skip/adapt E2E tests for Reyden read-only warehouse Reyden supports SELECT and INSERT but not CREATE TABLE, DROP TABLE, UPDATE, DELETE, SHOW COLUMNS, KEY type (SHOW PRIMARY KEYS/CROSS REFERENCES), or bare SET statements. Add isReadOnly and mutableTable config flags to gate tests appropriately: - Seed step provisions adbc_testing_mutable via the regular warehouse so INSERT tests have a pre-existing table to write into on Reyden - isReadOnly=true skips DDL-based and unsupported-metadata tests - INSERT tests route to the pre-provisioned table when isReadOnly; unique IDs + WHERE clause isolate rows across parallel runs - UPDATE and DELETE tests skip entirely under isReadOnly - TestServerSidePropertyOnSeaPath skips (bare SET not supported) Closes #505 --- .github/workflows/e2e-tests.yml | 16 +++++ .../test/E2E/DatabricksTestConfiguration.cs | 6 ++ csharp/test/E2E/ServerSidePropertyE2ETest.cs | 1 + .../StatementExecutionDriverE2ETests.cs | 64 +++++++++++++------ csharp/test/E2E/StatementTests.cs | 21 ++++++ 5 files changed, 87 insertions(+), 21 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 71a6419f3..111f15bc9 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -226,6 +226,20 @@ jobs: # Keep log noise low — first 80 chars per statement. print(f"OK [{i}/{len(statements)}]: {stmt[:80].replace(chr(10), ' ')}") print(f"Seeded {len(statements)} statements into main.{os.environ['PER_RUN_SCHEMA']}") + + # Create an empty mutable table for INSERT tests (used when execution routes through Reyden, + # which supports INSERT but not CREATE TABLE). + schema = os.environ["PER_RUN_SCHEMA"] + mutable_stmt = f"CREATE TABLE IF NOT EXISTS main.{schema}.adbc_testing_mutable (id INT, name STRING) USING DELTA" + payload = json.dumps({"warehouse_id": warehouse, "statement": mutable_stmt, "wait_timeout": "30s"}).encode() + req = urllib.request.Request(url, data=payload, + headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, method="POST") + with urllib.request.urlopen(req) as resp: + body = json.loads(resp.read()) + if body.get("status", {}).get("state", "") != "SUCCEEDED": + print(f"ERROR: CREATE mutable table failed: {json.dumps(body, indent=2)}") + sys.exit(1) + print(f"Created mutable table: main.{schema}.adbc_testing_mutable") PYEOF - name: Create Databricks config file @@ -250,6 +264,8 @@ jobs: "query": "SELECT * FROM main.${PER_RUN_SCHEMA}.adbc_testing_table", "expectedResults": 12, "isCITesting": true, + "isReadOnly": ${{ inputs.use_reyden_for_execution == true }}, + "mutableTable": "main.${PER_RUN_SCHEMA}.adbc_testing_mutable", "tracePropagationEnabled": "true", "traceParentHeaderName": "traceparent", "traceStateEnabled": "false", diff --git a/csharp/test/E2E/DatabricksTestConfiguration.cs b/csharp/test/E2E/DatabricksTestConfiguration.cs index dd1352a3b..9b085b932 100644 --- a/csharp/test/E2E/DatabricksTestConfiguration.cs +++ b/csharp/test/E2E/DatabricksTestConfiguration.cs @@ -58,6 +58,12 @@ public class DatabricksTestConfiguration : SparkTestConfiguration [JsonPropertyName("isCITesting"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public bool IsCITesting { get; set; } = false; + [JsonPropertyName("isReadOnly"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool IsReadOnly { get; set; } = false; + + [JsonPropertyName("mutableTable"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string MutableTable { get; set; } = string.Empty; + [JsonPropertyName("enableRunAsyncInThriftOp"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string EnableRunAsyncInThriftOp { get; set; } = string.Empty; diff --git a/csharp/test/E2E/ServerSidePropertyE2ETest.cs b/csharp/test/E2E/ServerSidePropertyE2ETest.cs index 62142d76f..3719461e9 100644 --- a/csharp/test/E2E/ServerSidePropertyE2ETest.cs +++ b/csharp/test/E2E/ServerSidePropertyE2ETest.cs @@ -106,6 +106,7 @@ public async Task TestServerSideProperty(bool applyWithQueries) [InlineData(false)] public async Task TestServerSidePropertyOnSeaPath(bool applyWithQueries) { + Skip.If(TestConfiguration.IsReadOnly, "Reyden does not support bare SET statement"); var additionalConnectionParams = new Dictionary() { // Force the SEA path regardless of the test config's default protocol. diff --git a/csharp/test/E2E/StatementExecution/StatementExecutionDriverE2ETests.cs b/csharp/test/E2E/StatementExecution/StatementExecutionDriverE2ETests.cs index f62972185..8358736b4 100644 --- a/csharp/test/E2E/StatementExecution/StatementExecutionDriverE2ETests.cs +++ b/csharp/test/E2E/StatementExecution/StatementExecutionDriverE2ETests.cs @@ -223,15 +223,20 @@ public void ExecuteUpdate_InsertData_ReturnsAffectedRows() { SkipIfNotConfigured(); + bool usePredefinedTable = TestConfiguration.IsReadOnly; + Skip.If(usePredefinedTable && string.IsNullOrEmpty(TestConfiguration.MutableTable), "IsReadOnly mode requires mutableTable in test config"); + using var connection = CreateRestConnection(); - var tableName = $"test_insert_{Guid.NewGuid():N}".Substring(0, 40); + string tableName = usePredefinedTable ? TestConfiguration.MutableTable : $"test_insert_{Guid.NewGuid():N}".Substring(0, 40); try { - // Create table - using var createStatement = connection.CreateStatement(); - createStatement.SqlQuery = $"CREATE TABLE {tableName} (id INT, name STRING) USING DELTA"; - createStatement.ExecuteUpdate(); + if (!usePredefinedTable) + { + using var createStatement = connection.CreateStatement(); + createStatement.SqlQuery = $"CREATE TABLE {tableName} (id INT, name STRING) USING DELTA"; + createStatement.ExecuteUpdate(); + } // Insert data using var insertStatement = connection.CreateStatement(); @@ -243,10 +248,13 @@ public void ExecuteUpdate_InsertData_ReturnsAffectedRows() } finally { - // Cleanup - using var dropStatement = connection.CreateStatement(); - dropStatement.SqlQuery = $"DROP TABLE IF EXISTS {tableName}"; - dropStatement.ExecuteUpdate(); + if (!usePredefinedTable) + { + // Cleanup + using var dropStatement = connection.CreateStatement(); + dropStatement.SqlQuery = $"DROP TABLE IF EXISTS {tableName}"; + dropStatement.ExecuteUpdate(); + } } } @@ -254,6 +262,7 @@ public void ExecuteUpdate_InsertData_ReturnsAffectedRows() public void ExecuteUpdate_UpdateData_ReturnsAffectedRows() { SkipIfNotConfigured(); + Skip.If(TestConfiguration.IsReadOnly, "Reyden does not support UPDATE"); using var connection = CreateRestConnection(); var tableName = $"test_update_{Guid.NewGuid():N}".Substring(0, 40); @@ -290,6 +299,7 @@ public void ExecuteUpdate_UpdateData_ReturnsAffectedRows() public void ExecuteUpdate_DeleteData_ReturnsAffectedRows() { SkipIfNotConfigured(); + Skip.If(TestConfiguration.IsReadOnly, "Reyden does not support DELETE"); using var connection = CreateRestConnection(); var tableName = $"test_delete_{Guid.NewGuid():N}".Substring(0, 40); @@ -349,23 +359,32 @@ public void ExecuteQuery_AfterInsert_ReturnsInsertedData() { SkipIfNotConfigured(); + bool usePredefinedTable = TestConfiguration.IsReadOnly; + Skip.If(usePredefinedTable && string.IsNullOrEmpty(TestConfiguration.MutableTable), "IsReadOnly mode requires mutableTable in test config"); + using var connection = CreateRestConnection(); - var tableName = $"test_query_after_insert_{Guid.NewGuid():N}".Substring(0, 40); + string tableName = usePredefinedTable ? TestConfiguration.MutableTable : $"test_query_after_insert_{Guid.NewGuid():N}".Substring(0, 40); + // Use unique IDs to isolate rows across concurrent runs when sharing the mutable table. + int id1 = usePredefinedTable ? Math.Abs(Guid.NewGuid().GetHashCode()) : 1; + int id2 = id1 + 1; try { - // Create and populate table - using var createStatement = connection.CreateStatement(); - createStatement.SqlQuery = $"CREATE TABLE {tableName} (id INT, name STRING) USING DELTA"; - createStatement.ExecuteUpdate(); + if (!usePredefinedTable) + { + using var createStatement = connection.CreateStatement(); + createStatement.SqlQuery = $"CREATE TABLE {tableName} (id INT, name STRING) USING DELTA"; + createStatement.ExecuteUpdate(); + } using var insertStatement = connection.CreateStatement(); - insertStatement.SqlQuery = $"INSERT INTO {tableName} VALUES (1, 'Alice'), (2, 'Bob')"; + insertStatement.SqlQuery = $"INSERT INTO {tableName} VALUES ({id1}, 'Alice'), ({id2}, 'Bob')"; insertStatement.ExecuteUpdate(); - // Query the data + // Query the data — filter by unique IDs when using the shared mutable table. + string whereClause = usePredefinedTable ? $" WHERE id IN ({id1}, {id2})" : ""; using var selectStatement = connection.CreateStatement(); - selectStatement.SqlQuery = $"SELECT * FROM {tableName} ORDER BY id"; + selectStatement.SqlQuery = $"SELECT * FROM {tableName}{whereClause} ORDER BY id"; var queryResult = selectStatement.ExecuteQuery(); Assert.NotNull(queryResult); @@ -384,10 +403,13 @@ public void ExecuteQuery_AfterInsert_ReturnsInsertedData() } finally { - // Cleanup - using var dropStatement = connection.CreateStatement(); - dropStatement.SqlQuery = $"DROP TABLE IF EXISTS {tableName}"; - dropStatement.ExecuteUpdate(); + if (!usePredefinedTable) + { + // Cleanup + using var dropStatement = connection.CreateStatement(); + dropStatement.SqlQuery = $"DROP TABLE IF EXISTS {tableName}"; + dropStatement.ExecuteUpdate(); + } } } diff --git a/csharp/test/E2E/StatementTests.cs b/csharp/test/E2E/StatementTests.cs index a2b56cb46..f966a322d 100644 --- a/csharp/test/E2E/StatementTests.cs +++ b/csharp/test/E2E/StatementTests.cs @@ -45,6 +45,22 @@ public StatementTests(ITestOutputHelper? outputHelper) { } + // Reyden does not support DDL (CREATE TABLE) — hide the inherited test and skip it. + [SkippableFact] + public new async Task CanInteractUsingSetOptions() + { + Skip.If(TestConfiguration.IsReadOnly, "Reyden does not support DDL (CREATE TABLE via TemporaryTable)"); + await base.CanInteractUsingSetOptions(); + } + + // Reyden does not support SHOW COLUMNS — hide the inherited test and skip it. + [SkippableFact] + public new async Task CanGetColumns() + { + Skip.If(TestConfiguration.IsReadOnly, "Reyden does not support SHOW COLUMNS"); + await base.CanGetColumns(); + } + // TODO: PECO-3011 - SEA StatementExecutionStatement does not validate poll time option protected override void ValidateCanSetOptionPollTime(string value, bool throws = false) { @@ -171,6 +187,7 @@ protected override void CreateNewTableName(out string tableName, out string full [SkippableFact] public async Task CanGetPrimaryKeysDatabricks() { + Skip.If(TestConfiguration.IsReadOnly, "Reyden does not support KEY primitive type (SHOW PRIMARY KEYS)"); await base.CanGetPrimaryKeys(TestConfiguration.Metadata.Catalog, TestConfiguration.Metadata.Schema); } @@ -185,6 +202,7 @@ public async Task CanGetCrossReferenceFromParentTableDatabricks() [SkippableFact] public async Task CanGetCrossReferenceFromChildTableDatabricks() { + Skip.If(TestConfiguration.IsReadOnly, "Reyden does not support KEY primitive type (SHOW CROSS REFERENCES)"); await base.CanGetCrossReferenceFromChildTable(TestConfiguration.Metadata.Catalog, TestConfiguration.Metadata.Schema); } @@ -270,6 +288,7 @@ public async Task AllStatementTypesDisposeWithoutErrors(string statementType, st [SkippableFact] public async Task CanGetColumnsWithBaseTypeName() { + Skip.If(TestConfiguration.IsReadOnly, "Reyden does not support SHOW COLUMNS"); var statement = Connection.CreateStatement(); statement.SetOption(ApacheParameters.IsMetadataCommand, "true"); statement.SetOption(ApacheParameters.CatalogName, TestConfiguration.Metadata.Catalog); @@ -648,6 +667,7 @@ public async Task CanGetColumnsExtended(string tableName, string createTableSqlL [SkippableFact] public async Task CanGetColumnsOnNoColumnTable() { + Skip.If(TestConfiguration.IsReadOnly, "Reyden does not support DDL (CREATE TABLE) or SHOW COLUMNS"); string? catalogName = TestConfiguration.Metadata.Catalog; string? schemaName = TestConfiguration.Metadata.Schema; string tableName = Guid.NewGuid().ToString("N"); @@ -1015,6 +1035,7 @@ public async Task StatusPollerKeepsQueryAlive(bool useCloudFetch, string configN [InlineData("false", false)] // Should only use default catalog public async Task EnableMultipleCatalogSupportAffectsMetadataQueries(string enableMultipleCatalogSupport, bool shouldAllowMultipleCatalogs) { + Skip.If(TestConfiguration.IsReadOnly, "Reyden does not support SHOW COLUMNS or SHOW CATALOGS"); // Create a connection with the specified EnableMultipleCatalogSupport setting var testConfig = (DatabricksTestConfiguration)TestConfiguration.Clone(); testConfig.EnableMultipleCatalogSupport = enableMultipleCatalogSupport; From 82a9085b5ef62e80c3293c32a803e926a486e798 Mon Sep 17 00:00:00 2001 From: eric-wang-1990 Date: Tue, 2 Jun 2026 07:52:48 +0000 Subject: [PATCH 08/17] fix(csharp): suppress xUnit1024 on new-hiding test overrides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit xUnit1024 errors on CanInteractUsingSetOptions and CanGetColumns because xUnit does not allow same-named methods in an inheritance chain. The new keyword is intentional here — we need to intercept the inherited tests to add Skip.If guards for Reyden. Suppress the analyzer error with pragma around both methods. --- csharp/test/E2E/StatementTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/csharp/test/E2E/StatementTests.cs b/csharp/test/E2E/StatementTests.cs index f966a322d..3055e069f 100644 --- a/csharp/test/E2E/StatementTests.cs +++ b/csharp/test/E2E/StatementTests.cs @@ -46,6 +46,7 @@ public StatementTests(ITestOutputHelper? outputHelper) } // Reyden does not support DDL (CREATE TABLE) — hide the inherited test and skip it. +#pragma warning disable xUnit1024 [SkippableFact] public new async Task CanInteractUsingSetOptions() { @@ -60,6 +61,7 @@ public StatementTests(ITestOutputHelper? outputHelper) Skip.If(TestConfiguration.IsReadOnly, "Reyden does not support SHOW COLUMNS"); await base.CanGetColumns(); } +#pragma warning restore xUnit1024 // TODO: PECO-3011 - SEA StatementExecutionStatement does not validate poll time option protected override void ValidateCanSetOptionPollTime(string value, bool throws = false) From ef90009637892ae90e27e89bc0a2ed2f6d98aef1 Mon Sep 17 00:00:00 2001 From: eric-wang-1990 Date: Tue, 2 Jun 2026 19:10:07 +0000 Subject: [PATCH 09/17] ci: publish E2E nightly results to a GitHub Pages dashboard Adds an append-only history dashboard for the Reyden REST nightly E2E run, served from the existing gh-pages branch at https://adbc-drivers.github.io/databricks/e2e-nightly/. - parse-trx-to-json.py: TRX -> per-run JSON with totals, pass rate, and a coarse failure "signature" per failure (404 warehouse, read-only write rejected, assertion mismatch, ...) so 100+ failures group into a few causes. - update-e2e-dashboard.py: merges each run into e2e-nightly/data/runs.json (keyed by run+protocol, capped history) and writes full per-run detail. - e2e-dashboard/index.html: static page with summary cards, a Chart.js pass/fail/skip trend + pass-rate line, latest-run failure analysis, and a clickable run-history table that lazy-loads each run's failures. - e2e-tests.yml: emit + upload a TRX; new opt-in publish_dashboard input and a publish-dashboard job that runs always() so failed runs are still recorded. - reyden-rest-nightly.yml: opt in (publish_dashboard) and grant contents:write. Co-authored-by: Isaac --- .github/e2e-dashboard/index.html | 219 ++++++++++++++++++++++ .github/scripts/parse-trx-to-json.py | 197 +++++++++++++++++++ .github/scripts/update-e2e-dashboard.py | 95 ++++++++++ .github/workflows/e2e-tests.yml | 84 ++++++++- .github/workflows/reyden-rest-nightly.yml | 6 + 5 files changed, 600 insertions(+), 1 deletion(-) create mode 100644 .github/e2e-dashboard/index.html create mode 100644 .github/scripts/parse-trx-to-json.py create mode 100644 .github/scripts/update-e2e-dashboard.py diff --git a/.github/e2e-dashboard/index.html b/.github/e2e-dashboard/index.html new file mode 100644 index 000000000..14807b336 --- /dev/null +++ b/.github/e2e-dashboard/index.html @@ -0,0 +1,219 @@ + + + + + + +E2E Nightly Dashboard — Databricks ADBC + + + + +
+
+

E2E Nightly Test Dashboard

+
Loading…
+
+ +
+ +
+

Trend (per nightly run)

+ +
+ +
+

Latest run — failure analysis

+
+
+ +
+

Run history (click a row to expand failures)

+ + + + + + + +
Date (UTC)ProtocolPass ratePassedFailedSkippedResult
+
+ +
+ Data stored in e2e-nightly/data/ on the gh-pages branch · append-only history. +
+
+ + + + diff --git a/.github/scripts/parse-trx-to-json.py b/.github/scripts/parse-trx-to-json.py new file mode 100644 index 000000000..4bbdcbb1d --- /dev/null +++ b/.github/scripts/parse-trx-to-json.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parse one or more VSTest .trx files into a single nightly-run JSON record. + +Usage: + parse-trx-to-json.py OUTPUT.json TRX [TRX ...] + +The output record captures the totals, per-test outcomes for everything that +did not pass, and a coarse "failure signature" for each failure so the +dashboard can group 100+ failures into a handful of root causes. Run metadata +(commit, branch, run id, timestamp, …) is read from the environment so the +script stays decoupled from the workflow. +""" +import json +import os +import re +import sys +import xml.etree.ElementTree as ET + +# TRX uses a default namespace; strip it so we can query with plain tag names. +_NS = {"t": "http://microsoft.com/schemas/VisualStudio/TeamTest/2010"} + + +def _localname(tag): + return tag.split("}", 1)[-1] if "}" in tag else tag + + +def signature_for(message): + """Collapse a failure message into a stable, human-readable bucket. + + The goal is triage, not precision: hundreds of parameterized failures that + share one root cause (a 404 warehouse, a read-only rejection, …) should + land in the same bucket regardless of the per-case query/value text. + """ + if not message: + return "Unknown / no message" + m = message.replace("\r", " ").replace("\n", " ") + + patterns = [ + (r"ENDPOINT_NOT_FOUND", "Warehouse not found (ENDPOINT_NOT_FOUND / HTTP 404)"), + (r"read[- ]?only|READ_ONLY|cannot be modified|not.*allowed.*read", "Read-only warehouse rejected write/DDL"), + (r"INSERT|UPDATE|DELETE|MERGE|CREATE TABLE|DROP TABLE|ALTER TABLE", "DML/DDL rejected"), + (r"PERMISSION_DENIED|not authorized|Forbidden|HTTP 403", "Permission denied (403)"), + (r"timeout|timed out|TimeoutException", "Timeout"), + (r"Couldn't connect|connection refused|HttpRequestException", "Connection / transport error"), + (r"TABLE_OR_VIEW_NOT_FOUND|cannot be found|does not exist", "Object not found"), + (r"PARSE_SYNTAX_ERROR|SYNTAX_ERROR", "SQL syntax error"), + (r"Assert\.|Equal\(|Xunit|expected", "Assertion failed (value mismatch)"), + ] + for pat, label in patterns: + if re.search(pat, m, re.IGNORECASE): + return label + # Fall back to the exception type at the head of the message, if present. + exc = re.match(r"\s*([A-Za-z0-9_.]+Exception)", m) + if exc: + return exc.group(1) + return "Other" + + +def class_of(test_name): + """Best-effort owning class: strip the parameter list and the method.""" + base = test_name.split("(", 1)[0] + return base.rsplit(".", 1)[0] if "." in base else base + + +def parse_trx(path): + tree = ET.parse(path) + root = tree.getroot() + + # Map testId -> testName from the section. + names = {} + for ut in root.iter(): + if _localname(ut.tag) == "UnitTest": + tm = None + for child in ut: + if _localname(child.tag) == "TestMethod": + tm = child + break + tid = ut.get("id") + if tid is not None and tm is not None: + cls = tm.get("className", "") + name = tm.get("name", "") + names[tid] = (f"{cls}.{name}" if cls else name) + + results = [] + for r in root.iter(): + if _localname(r.tag) != "UnitTestResult": + continue + outcome = r.get("outcome", "") + tid = r.get("testId") + test_name = names.get(tid, r.get("testName", "unknown")) + message = "" + stack = "" + for out in r: + if _localname(out.tag) != "Output": + continue + for err in out: + if _localname(err.tag) == "ErrorInfo": + for e in err: + ln = _localname(e.tag) + if ln == "Message": + message = (e.text or "").strip() + elif ln == "StackTrace": + stack = (e.text or "").strip() + results.append({ + "name": test_name, + "class": class_of(test_name), + "outcome": outcome, + "message": message, + "stack": stack, + }) + return results + + +def main(): + if len(sys.argv) < 3: + print("usage: parse-trx-to-json.py OUTPUT.json TRX [TRX ...]", file=sys.stderr) + sys.exit(2) + + out_path = sys.argv[1] + trx_paths = sys.argv[2:] + + all_results = [] + for p in trx_paths: + if os.path.isdir(p): + for name in sorted(os.listdir(p)): + if name.endswith(".trx"): + all_results.extend(parse_trx(os.path.join(p, name))) + elif os.path.exists(p): + all_results.extend(parse_trx(p)) + else: + print(f"WARNING: {p} not found, skipping", file=sys.stderr) + + passed = [r for r in all_results if r["outcome"] == "Passed"] + failed = [r for r in all_results if r["outcome"] == "Failed"] + skipped = [r for r in all_results if r["outcome"] in ("NotExecuted", "Inconclusive")] + + # Trim payload: keep full detail for failures only. + for r in failed: + r["signature"] = signature_for(r["message"]) + if len(r["message"]) > 2000: + r["message"] = r["message"][:2000] + " …(truncated)" + if len(r["stack"]) > 2000: + r["stack"] = r["stack"][:2000] + " …(truncated)" + + by_signature = {} + by_class = {} + for r in failed: + by_signature[r["signature"]] = by_signature.get(r["signature"], 0) + 1 + by_class[r["class"]] = by_class.get(r["class"], 0) + 1 + + total = len(all_results) + record = { + "run_id": os.environ.get("GITHUB_RUN_ID", ""), + "run_attempt": os.environ.get("GITHUB_RUN_ATTEMPT", ""), + "run_number": os.environ.get("GITHUB_RUN_NUMBER", ""), + "timestamp": os.environ.get("RUN_TIMESTAMP", ""), + "commit": os.environ.get("GITHUB_SHA", ""), + "branch": os.environ.get("GITHUB_REF_NAME", ""), + "protocol": os.environ.get("RUN_PROTOCOL", ""), + "read_only": os.environ.get("RUN_READ_ONLY", "") == "true", + "workflow": os.environ.get("GITHUB_WORKFLOW", ""), + "html_url": ( + f"{os.environ.get('GITHUB_SERVER_URL', 'https://github.com')}/" + f"{os.environ.get('GITHUB_REPOSITORY', '')}/actions/runs/" + f"{os.environ.get('GITHUB_RUN_ID', '')}" + ), + "total": total, + "passed": len(passed), + "failed": len(failed), + "skipped": len(skipped), + "pass_rate": round(100.0 * len(passed) / total, 1) if total else 0.0, + "by_signature": dict(sorted(by_signature.items(), key=lambda kv: -kv[1])), + "by_class": dict(sorted(by_class.items(), key=lambda kv: -kv[1])), + "failures": sorted(failed, key=lambda r: (r["signature"], r["name"])), + } + + with open(out_path, "w") as f: + json.dump(record, f, indent=2) + print(f"Parsed {total} results ({record['passed']} passed, " + f"{record['failed']} failed, {record['skipped']} skipped) -> {out_path}") + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/update-e2e-dashboard.py b/.github/scripts/update-e2e-dashboard.py new file mode 100644 index 000000000..6b03500ce --- /dev/null +++ b/.github/scripts/update-e2e-dashboard.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Merge a parsed nightly-run record into the gh-pages E2E dashboard. + +Usage: + update-e2e-dashboard.py RUN_RECORD.json DASHBOARD_DIR TEMPLATE_INDEX.html + + RUN_RECORD.json output of parse-trx-to-json.py + DASHBOARD_DIR e.g. /e2e-nightly + TEMPLATE_INDEX static dashboard page to (re)publish alongside the data + +Layout produced under DASHBOARD_DIR: + index.html the dashboard (copied from template) + data/runs.json lightweight history index (one row/run) + data/run--.json full per-run detail (failure list) + +History is append-only and keyed by (run_id, protocol) so a re-run replaces +its prior row rather than duplicating it. +""" +import json +import os +import shutil +import sys + +# Keep history bounded so the index stays small and the page stays fast. +MAX_RUNS = 365 + + +def main(): + if len(sys.argv) != 4: + print("usage: update-e2e-dashboard.py RUN_RECORD.json DASHBOARD_DIR TEMPLATE_INDEX.html", + file=sys.stderr) + sys.exit(2) + + record_path, dashboard_dir, template = sys.argv[1], sys.argv[2], sys.argv[3] + data_dir = os.path.join(dashboard_dir, "data") + os.makedirs(data_dir, exist_ok=True) + + with open(record_path) as f: + record = json.load(f) + + run_id = record.get("run_id", "0") + protocol = record.get("protocol", "") or "unknown" + detail_name = f"run-{run_id}-{protocol}.json" + + # Full detail (incl. failure messages) lives in its own file, fetched + # lazily by the dashboard only when a run is expanded. + with open(os.path.join(data_dir, detail_name), "w") as f: + json.dump(record, f, indent=2) + + # Lightweight summary row for the history index — drop the heavy bits. + summary = {k: record[k] for k in ( + "run_id", "run_attempt", "run_number", "timestamp", "commit", "branch", + "protocol", "read_only", "html_url", "total", "passed", "failed", + "skipped", "pass_rate", "by_signature", "by_class", + ) if k in record} + summary["detail"] = detail_name + + runs_path = os.path.join(data_dir, "runs.json") + runs = [] + if os.path.exists(runs_path): + try: + with open(runs_path) as f: + runs = json.load(f) + except (ValueError, OSError): + runs = [] + + key = (summary.get("run_id"), summary.get("protocol")) + runs = [r for r in runs if (r.get("run_id"), r.get("protocol")) != key] + runs.append(summary) + runs.sort(key=lambda r: (r.get("timestamp", ""), r.get("run_id", ""))) + if len(runs) > MAX_RUNS: + runs = runs[-MAX_RUNS:] + + with open(runs_path, "w") as f: + json.dump(runs, f, indent=2) + + shutil.copyfile(template, os.path.join(dashboard_dir, "index.html")) + print(f"Dashboard updated: {len(runs)} runs in index, detail -> {detail_name}") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 111f15bc9..b8b546f28 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -32,6 +32,11 @@ on: required: false default: false type: boolean + publish_dashboard: + description: 'Publish parsed test results to the GitHub Pages E2E nightly dashboard (gh-pages branch). Intended for nightly runs.' + required: false + default: false + type: boolean workflow_dispatch: inputs: protocol: @@ -294,7 +299,21 @@ jobs: if [[ "${{ inputs.e2e_only }}" == "true" ]]; then FILTER_ARG="--filter FullyQualifiedName!~Tests.Unit" fi - ./ci/scripts/csharp_test_databricks_e2e.sh "${{ github.workspace }}" $FILTER_ARG + # Always emit a TRX so the dashboard can parse pass/fail/skip + per-test + # failure detail, even when the run fails (set -e in the script still + # writes the TRX before the non-zero exit; the upload step is always()). + ./ci/scripts/csharp_test_databricks_e2e.sh "${{ github.workspace }}" $FILTER_ARG \ + --logger "trx;LogFileName=results-${{ matrix.protocol }}.trx" \ + --results-directory "${{ github.workspace }}/TestResults" + + - name: Upload test results (TRX) + if: always() && steps.gate.outputs.run == 'true' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: e2e-trx-${{ matrix.protocol }} + path: ${{ github.workspace }}/TestResults/*.trx + if-no-files-found: warn + retention-days: 7 # Always run, even on test failure or job cancellation, so we don't # leak schemas in the workspace. @@ -320,3 +339,66 @@ jobs: else echo "Dropped schema: main.${PER_RUN_SCHEMA}" fi + + # Parse the TRX results and publish an append-only history dashboard to the + # gh-pages branch (served at https://adbc-drivers.github.io/databricks/e2e-nightly/). + # Runs only when the caller opts in (nightly), and always() so a failed test + # run is still recorded — surfacing failures is the whole point of the page. + publish-dashboard: + name: Publish E2E Dashboard + needs: run-e2e-tests + if: always() && inputs.publish_dashboard + runs-on: ubuntu-latest + permissions: + contents: write # push parsed results to the gh-pages branch + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: Download test results + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: e2e-trx-* + path: trx + + - name: Build dashboard and push to gh-pages + env: + RUN_READ_ONLY: ${{ inputs.use_reyden_for_execution }} + run: | + set -e + shopt -s nullglob + RUN_TIMESTAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + export RUN_TIMESTAMP + + dirs=(trx/e2e-trx-*) + if [ ${#dirs[@]} -eq 0 ]; then + echo "No TRX artifacts found — nothing to publish." + exit 0 + fi + + git fetch origin gh-pages + git worktree add /tmp/ghp gh-pages + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + for d in "${dirs[@]}"; do + proto="${d#trx/e2e-trx-}" + export RUN_PROTOCOL="$proto" + echo "::group::Parsing $proto" + python3 .github/scripts/parse-trx-to-json.py "/tmp/record-${proto}.json" "$d" + python3 .github/scripts/update-e2e-dashboard.py \ + "/tmp/record-${proto}.json" /tmp/ghp/e2e-nightly .github/e2e-dashboard/index.html + echo "::endgroup::" + done + + cd /tmp/ghp + git add e2e-nightly + if git diff --cached --quiet; then + echo "No dashboard changes to commit." + else + git commit -m "e2e-nightly: record run ${GITHUB_RUN_ID} (${GITHUB_REF_NAME})" + git push origin gh-pages + echo "Published: https://adbc-drivers.github.io/databricks/e2e-nightly/" + fi diff --git a/.github/workflows/reyden-rest-nightly.yml b/.github/workflows/reyden-rest-nightly.yml index 2ec439150..ed57215c0 100644 --- a/.github/workflows/reyden-rest-nightly.yml +++ b/.github/workflows/reyden-rest-nightly.yml @@ -20,6 +20,11 @@ on: - cron: '0 2 * * *' workflow_dispatch: +# Grant the reusable workflow permission to push the parsed results to the +# gh-pages branch (the dashboard's append-only history store). +permissions: + contents: write + jobs: reyden-rest-nightly: uses: ./.github/workflows/e2e-tests.yml @@ -27,4 +32,5 @@ jobs: protocol: rest use_reyden_for_execution: true e2e_only: true + publish_dashboard: true secrets: inherit From a039bae3059ff2e4182de23c163443b05f6841fc Mon Sep 17 00:00:00 2001 From: eric-wang-1990 <115501094+eric-wang-1990@users.noreply.github.com> Date: Thu, 4 Jun 2026 23:54:04 -0700 Subject: [PATCH 10/17] ci: classify Reyden nightly E2E failures by root cause (#512) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What The raw pass-rate on the SEA/Reyden nightly (~61% on the latest run) is dominated by **expected** failures — unsupported DDL/types, no Thrift endpoint on a SEA-only warehouse, no CloudFetch — which buries the genuine driver bugs. This adds a coarse **root-cause category** on top of the existing per-failure signature so the dashboard separates expected noise from the real backlog. On the latest run (127 failures) the split is **51 expected Reyden gaps / 76 real issues**. ## How it classifies Classification follows the failing step, which the message already encodes: - A test with a `CREATE TABLE/SCHEMA` step Reyden can't run fails **at that step** with an `Unsupported …` message → **Reyden capability gap (expected)**. - A value/cast mismatch means setup succeeded and the `INSERT→SELECT→DELETE` round-trip returned wrong data → **Real issue** (e.g. a SEA-path serialization difference). - Missing warehouse / read-only / auth / timeout / transport → **Environment / infra**. ## Changes - **`parse-trx-to-json.py`**: refined `signature_for()` (Thrift-on-SEA, CloudFetch, unsupported-feature buckets; `PARSE_SYNTAX_ERROR` ordered before the broad assertion bucket), added `category_for()` + per-failure `category` + a `by_category` rollup. - **`update-e2e-dashboard.py`**: propagate `by_category` into the `runs.json` summary row. - **`index.html`**: "By root-cause category" rollup with a color-coded legend, and failure detail grouped by category → signature. Degrades gracefully for older runs without `by_category`. Validated against the latest run's data and syntax-checked (`py_compile` + `node --check`). This pull request and its description were written by Isaac. --- .github/e2e-dashboard/index.html | 37 ++++++++++++- .github/scripts/parse-trx-to-json.py | 74 ++++++++++++++++++++++++- .github/scripts/update-e2e-dashboard.py | 2 +- 3 files changed, 108 insertions(+), 5 deletions(-) diff --git a/.github/e2e-dashboard/index.html b/.github/e2e-dashboard/index.html index 14807b336..600b8de88 100644 --- a/.github/e2e-dashboard/index.html +++ b/.github/e2e-dashboard/index.html @@ -55,6 +55,12 @@ .muted { color:var(--muted); } .small { font-size:12px; } .group-h { margin:12px 0 4px; font-size:13px; font-weight:700; } canvas { max-height:340px; } + .catbadge { display:inline-block; font-size:12px; font-weight:600; padding:2px 8px; border-radius:6px; } + .cat-gap { background:#e8eefc; color:#2949b8; } + .cat-env { background:#fdf3dc; color:#9a7400; } + .cat-real { background:#fbe7e7; color:#d83b3b; } + .legend { list-style:none; margin:10px 0 4px; padding:0; font-size:12px; color:var(--muted); } + .legend li { margin:5px 0; line-height:1.5; } @@ -98,6 +104,21 @@

Run history (click a row to expand failures) { if (!ts) return "—"; const d = new Date(ts); return isNaN(d) ? ts : d.toISOString().slice(0,16).replace("T"," "); }; const esc = s => (s||"").replace(/[&<>"]/g, c => ({"&":"&","<":"<",">":">",'"':"""}[c])); +// Root-cause categories emitted by parse-trx-to-json.py. A low raw pass-rate is +// usually dominated by expected Reyden gaps; the "Real issue" bucket is the +// actual backlog. A test with a CREATE step that Reyden can't run fails AT that +// step (an "Unsupported …" gap); a value mismatch means setup succeeded and the +// round-trip returned wrong data — a genuine bug. +const CATEGORIES = { + "Reyden capability gap (expected)": { cls:"cat-gap", + blurb:"The backend doesn't support the operation or protocol — unsupported DDL/statement/type (e.g. CREATE TABLE/SCHEMA), no Thrift endpoint on a SEA/REST-only warehouse, or CloudFetch. Expected: gate/skip these; they shouldn't count against the driver." }, + "Environment / infra": { cls:"cat-env", + blurb:"Missing/misconfigured warehouse, read-only rejection, auth, timeout, or transport error. Not a driver bug — fix the environment or retry." }, + "Real issue / to investigate": { cls:"cat-real", + blurb:"A genuine driver bug. A value/cast mismatch on an INSERT→SELECT→DELETE round-trip means setup succeeded but the data came back wrong (e.g. a SEA-path serialization difference); also covers SQL/syntax errors and anything unclassified." }, +}; +const catBadge = name => `${esc(name)}`; + function bar(p, f, s) { const t = p+f+s || 1; return `
@@ -142,6 +163,18 @@

Run history (click a row to expand failures) + `${catBadge(k)}${v}`).join(""); + const legend = catEntries.length + ? `
    ` + catEntries.map(([k]) => + `
  • ${catBadge(k)} ${esc((CATEGORIES[k]||{}).blurb||"")}
  • `).join("") + `
` + : ""; + const catSection = catEntries.length ? ` +
By root-cause category
+ ${catRows}
CategoryCount
+ ${legend}` : ""; + const sigRows = Object.entries(latest.by_signature||{}).map(([k,v]) => `${esc(k)}${v}`).join(""); const clsRows = Object.entries(latest.by_class||{}).map(([k,v]) => @@ -149,6 +182,7 @@

Run history (click a row to expand failures)Latest run #${esc(latest.run_number||latest.run_id)} (${esc(latest.protocol)}${latest.read_only?", read-only":""}) — ${latest.failed} failures grouped below.

+ ${catSection}
By failure signature (root cause)
${sigRows}
SignatureCount
By test class
@@ -167,8 +201,9 @@

Run history (click a row to expand failures)${catBadge(f.category)}

`; } if (f.signature !== curSig) { curSig = f.signature; html += `
${esc(curSig)}
`; } html += `
  • ${esc(f.name)}
    ` + (f.message ? `
    ${esc(f.message)}
    ` : "") + `
`; diff --git a/.github/scripts/parse-trx-to-json.py b/.github/scripts/parse-trx-to-json.py index 4bbdcbb1d..f33d46e14 100644 --- a/.github/scripts/parse-trx-to-json.py +++ b/.github/scripts/parse-trx-to-json.py @@ -48,16 +48,43 @@ def signature_for(message): return "Unknown / no message" m = message.replace("\r", " ").replace("\n", " ") + # Order matters: the most specific patterns win. In particular the + # Thrift-on-SEA and CloudFetch buckets must be matched before the generic + # "Couldn't connect / HttpRequestException" transport bucket, because their + # messages also contain that text. patterns = [ + # --- Reyden capability gaps (expected; see SIG_CATEGORY) ------------- + # A Thrift session against a SEA/REST-only warehouse (e.g. Reyden) has + # no Thrift endpoint, so the server returns ENDPOINT_NOT_FOUND. This is + # NOT a missing/misconfigured warehouse — the warehouse works over REST. + (r"Thrift server error.*ENDPOINT_NOT_FOUND|ENDPOINT_NOT_FOUND.*Thrift server error", + "Thrift endpoint unavailable on SEA/Reyden warehouse"), + (r"Error in download process|CloudFetch.*download", + "CloudFetch not supported on Reyden (download failed)"), + (r"PARSER_UNSUPPORTED_FEATURE|UNSUPPORTED_FEATURE|Unsupported statement|" + r"Unsupported CREATE type|Unsupported Delta table type|Unsupported .*type", + "Reyden unsupported feature (DDL / statement / type)"), + # --- Environment / infra -------------------------------------------- (r"ENDPOINT_NOT_FOUND", "Warehouse not found (ENDPOINT_NOT_FOUND / HTTP 404)"), (r"read[- ]?only|READ_ONLY|cannot be modified|not.*allowed.*read", "Read-only warehouse rejected write/DDL"), - (r"INSERT|UPDATE|DELETE|MERGE|CREATE TABLE|DROP TABLE|ALTER TABLE", "DML/DDL rejected"), (r"PERMISSION_DENIED|not authorized|Forbidden|HTTP 403", "Permission denied (403)"), (r"timeout|timed out|TimeoutException", "Timeout"), (r"Couldn't connect|connection refused|HttpRequestException", "Connection / transport error"), - (r"TABLE_OR_VIEW_NOT_FOUND|cannot be found|does not exist", "Object not found"), + # --- Genuine SQL errors with specific codes ------------------------ + # Checked before the broad assertion bucket below, whose "expected" + # token also appears in "Expected identifier ..." syntax-error text. (r"PARSE_SYNTAX_ERROR|SYNTAX_ERROR", "SQL syntax error"), + # --- Genuine driver bugs -------------------------------------------- + # A value/cast mismatch means the test got PAST any setup (a rejected + # CREATE TABLE would have failed earlier with an "Unsupported …" message + # in the Reyden-gap bucket above). So a wrong value on an + # INSERT→SELECT→DELETE round-trip is a real driver bug, e.g. a SEA-path + # result-serialization difference — not expected Reyden behaviour. + (r"CAST_INVALID_INPUT", "Type cast mismatch on round-trip"), + (r"TABLE_OR_VIEW_NOT_FOUND|cannot be found|does not exist", "Object not found"), (r"Assert\.|Equal\(|Xunit|expected", "Assertion failed (value mismatch)"), + # --- Generic DML/DDL rejection (catch-all, lowest priority) -------- + (r"INSERT|UPDATE|DELETE|MERGE|CREATE TABLE|DROP TABLE|ALTER TABLE", "DML/DDL rejected"), ] for pat, label in patterns: if re.search(pat, m, re.IGNORECASE): @@ -69,6 +96,43 @@ def signature_for(message): return "Other" +# Root-cause category layered on top of the fine-grained signature. The +# dashboard rolls failures up to these three buckets so a low raw pass-rate +# (dominated by expected Reyden gaps) doesn't mask the genuine driver bugs. +# +# Classification hinges on WHICH step failed, which the message already encodes: +# - A test with a CREATE TABLE/SCHEMA step that Reyden can't run fails AT that +# step with an "Unsupported …" message -> CAT_REYDEN_GAP (expected). +# - A value/cast mismatch means setup succeeded and the INSERT→SELECT→DELETE +# round-trip returned wrong data -> CAT_REAL (a genuine driver bug). +CAT_REYDEN_GAP = "Reyden capability gap (expected)" +CAT_ENVIRONMENT = "Environment / infra" +CAT_REAL = "Real issue / to investigate" + +# Explicit signature -> category map. Any signature not listed here (including +# the dynamic "" fallbacks and the value/cast/DML/syntax buckets) +# is treated as CAT_REAL so genuine, unclassified failures surface rather than +# hide. +_SIGNATURE_CATEGORY = { + "Thrift endpoint unavailable on SEA/Reyden warehouse": CAT_REYDEN_GAP, + "CloudFetch not supported on Reyden (download failed)": CAT_REYDEN_GAP, + "Reyden unsupported feature (DDL / statement / type)": CAT_REYDEN_GAP, + "Warehouse not found (ENDPOINT_NOT_FOUND / HTTP 404)": CAT_ENVIRONMENT, + "Read-only warehouse rejected write/DDL": CAT_ENVIRONMENT, + "Permission denied (403)": CAT_ENVIRONMENT, + "Timeout": CAT_ENVIRONMENT, + "Connection / transport error": CAT_ENVIRONMENT, + # "Assertion failed (value mismatch)", "Type cast mismatch on round-trip", + # "Object not found", "DML/DDL rejected", "SQL syntax error", "Other", + # "Unknown / no message" and any "" fall through to CAT_REAL. +} + + +def category_for(signature): + """Map a fine-grained signature to one of the four root-cause buckets.""" + return _SIGNATURE_CATEGORY.get(signature, CAT_REAL) + + def class_of(test_name): """Best-effort owning class: strip the parameter list and the method.""" base = test_name.split("(", 1)[0] @@ -150,6 +214,7 @@ def main(): # Trim payload: keep full detail for failures only. for r in failed: r["signature"] = signature_for(r["message"]) + r["category"] = category_for(r["signature"]) if len(r["message"]) > 2000: r["message"] = r["message"][:2000] + " …(truncated)" if len(r["stack"]) > 2000: @@ -157,9 +222,11 @@ def main(): by_signature = {} by_class = {} + by_category = {} for r in failed: by_signature[r["signature"]] = by_signature.get(r["signature"], 0) + 1 by_class[r["class"]] = by_class.get(r["class"], 0) + 1 + by_category[r["category"]] = by_category.get(r["category"], 0) + 1 total = len(all_results) record = { @@ -182,9 +249,10 @@ def main(): "failed": len(failed), "skipped": len(skipped), "pass_rate": round(100.0 * len(passed) / total, 1) if total else 0.0, + "by_category": dict(sorted(by_category.items(), key=lambda kv: -kv[1])), "by_signature": dict(sorted(by_signature.items(), key=lambda kv: -kv[1])), "by_class": dict(sorted(by_class.items(), key=lambda kv: -kv[1])), - "failures": sorted(failed, key=lambda r: (r["signature"], r["name"])), + "failures": sorted(failed, key=lambda r: (r["category"], r["signature"], r["name"])), } with open(out_path, "w") as f: diff --git a/.github/scripts/update-e2e-dashboard.py b/.github/scripts/update-e2e-dashboard.py index 6b03500ce..d9b7b7e8b 100644 --- a/.github/scripts/update-e2e-dashboard.py +++ b/.github/scripts/update-e2e-dashboard.py @@ -64,7 +64,7 @@ def main(): summary = {k: record[k] for k in ( "run_id", "run_attempt", "run_number", "timestamp", "commit", "branch", "protocol", "read_only", "html_url", "total", "passed", "failed", - "skipped", "pass_rate", "by_signature", "by_class", + "skipped", "pass_rate", "by_category", "by_signature", "by_class", ) if k in record} summary["detail"] = detail_name From aee6441ebaca7dd4e45d4fa7d828e4cc84abca5a Mon Sep 17 00:00:00 2001 From: eric-wang-1990 <115501094+eric-wang-1990@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:15:19 -0700 Subject: [PATCH 11/17] feat(ci): split Reyden gap signatures + per-error dashboard detail (#513) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Adds the per-error granularity requested for the E2E nightly dashboard. - **Splits** the single `Reyden unsupported feature` signature into the specific errors so each gap shows its own count: `Unsupported statement: SHOW COLUMNS` (13), `Unsupported CREATE type: SCHEMA` (4), `Unsupported feature: CREATE OR REPLACE TABLE` (2), `Unsupported type: INTERVAL` (2), plus a generic fallback. All map to the **Reyden capability gap** category. - **Dashboard**: replaces the flat signature/class tables with a **"By error (root cause)"** table — `Category | Error | Count | What it is | Tests` — where "What it is" is a plain-language description (`SIGNATURE_DESCRIPTIONS`) and "Tests" lists the affected `Class.Method`s (from the run detail file). ## Changes - `.github/scripts/parse-trx-to-json.py` — `signature_for` splits the unsupported bucket; `_SIGNATURE_CATEGORY` maps the new signatures to the gap category (old combined signature kept for back-compat). - `.github/e2e-dashboard/index.html` — `SIGNATURE_DESCRIPTIONS` + `sigDetailTable()`; `renderAnalysis` now fetches the run detail and renders the per-error table. ## Verification Generated the dashboard locally from the latest run's data (re-classified) and rendered it in a browser — the split, descriptions, and test lists display correctly. Scripts `py_compile` clean and the dashboard JS passes `node --check`. This pull request and its description were written by Isaac. --- .github/e2e-dashboard/index.html | 80 ++++++++++++++++++++++++---- .github/scripts/parse-trx-to-json.py | 17 +++++- 2 files changed, 85 insertions(+), 12 deletions(-) diff --git a/.github/e2e-dashboard/index.html b/.github/e2e-dashboard/index.html index 600b8de88..131cc10a1 100644 --- a/.github/e2e-dashboard/index.html +++ b/.github/e2e-dashboard/index.html @@ -119,6 +119,63 @@

Run history (click a row to expand failures) `${esc(name)}`; +// Plain-language "what it is" per failure signature (keys must match +// parse-trx-to-json.py's signature_for output). +const SIGNATURE_DESCRIPTIONS = { + "Thrift endpoint unavailable on SEA/Reyden warehouse": "REST-only warehouse has no Thrift endpoint", + "CloudFetch not supported on Reyden (download failed)": "cloud result download not supported", + "Unsupported statement: SHOW COLUMNS": "metadata path not implemented", + "Unsupported CREATE type: SCHEMA": "cannot create schema", + "Unsupported feature: CREATE OR REPLACE TABLE": "cannot create / replace table", + "Unsupported type: INTERVAL": "cannot store / read interval", + "Reyden unsupported feature (other)": "other unsupported DDL / statement / type", + "Reyden unsupported feature (DDL / statement / type)": "unsupported DDL / statement / type", + "Warehouse not found (ENDPOINT_NOT_FOUND / HTTP 404)": "warehouse missing or misconfigured", + "Read-only warehouse rejected write/DDL": "warehouse is read-only", + "Permission denied (403)": "auth / permission issue", + "Timeout": "operation timed out", + "Connection / transport error": "could not connect to the server", + "SQL syntax error": "malformed statement emitted by the driver", + "Type cast mismatch on round-trip": "value came back wrong-typed — driver bug", + "Object not found": "metadata resolved a missing object", + "Assertion failed (value mismatch)": "round-trip returned wrong data — driver bug", + "DML/DDL rejected": "write / DDL rejected", +}; +const sigDesc = s => SIGNATURE_DESCRIPTIONS[s] || ""; + +// Class.Method, dropping the namespace prefix and the parameter list. +const shortTest = name => { + const base = (name || "").split("(")[0]; + return base.split(".").slice(-2).join("."); +}; + +// Per-error table for the latest run: Category | Error | Count | What it is | Tests. +function sigDetailTable(failures) { + if (!failures || !failures.length) return 'No failure detail recorded.'; + const groups = {}; + for (const f of failures) { + const cat = f.category || "Uncategorized", sig = f.signature || "Unknown"; + const key = cat + "" + sig; + (groups[key] ??= { cat, sig, items: [] }).items.push(f); + } + const rows = Object.values(groups) + .sort((a, b) => a.cat < b.cat ? -1 : a.cat > b.cat ? 1 : b.items.length - a.items.length) + .map(({ cat, sig, items }) => { + const tests = [...new Set(items.map(f => shortTest(f.name)))].sort(); + const shown = tests.slice(0, 8).map(esc).join("
") + + (tests.length > 8 ? `
+${tests.length - 8} more` : ""); + return ` + ${catBadge(cat)} + ${esc(sig)} + ${items.length} + ${esc(sigDesc(sig))} + ${shown}`; + }).join(""); + return ` + + ${rows}
CategoryErrorCountWhat it isTests
`; +} + function bar(p, f, s) { const t = p+f+s || 1; return `
@@ -160,7 +217,7 @@

Run history (click a row to expand failures)Run history (click a row to expand failures)CategoryCount${catRows} ${legend}` : ""; - const sigRows = Object.entries(latest.by_signature||{}).map(([k,v]) => - `${esc(k)}${v}`).join(""); - const clsRows = Object.entries(latest.by_class||{}).map(([k,v]) => - `${esc(k)}${v}`).join(""); el.innerHTML = `

Latest run #${esc(latest.run_number||latest.run_id)} - (${esc(latest.protocol)}${latest.read_only?", read-only":""}) — ${latest.failed} failures grouped below.

+ (${esc(latest.protocol)}${latest.read_only?", read-only":""}) — ${latest.failed} failures.

${catSection} -
By failure signature (root cause)
- ${sigRows}
SignatureCount
-
By test class
- ${clsRows}
ClassCount
`; +
By error (root cause)
+
Loading per-error detail…
`; + + // The per-error table needs the failure list (test names) from the run detail file. + try { + const full = await (await fetch("data/" + latest.detail + "?_=" + Date.now())).json(); + document.getElementById("sigTable").innerHTML = sigDetailTable(full.failures || []); + } catch (e) { + document.getElementById("sigTable").innerHTML = + `Could not load per-error detail: ${esc(String(e))}`; + } } async function toggleDetail(tr, run) { diff --git a/.github/scripts/parse-trx-to-json.py b/.github/scripts/parse-trx-to-json.py index f33d46e14..24410be87 100644 --- a/.github/scripts/parse-trx-to-json.py +++ b/.github/scripts/parse-trx-to-json.py @@ -61,9 +61,16 @@ def signature_for(message): "Thrift endpoint unavailable on SEA/Reyden warehouse"), (r"Error in download process|CloudFetch.*download", "CloudFetch not supported on Reyden (download failed)"), - (r"PARSER_UNSUPPORTED_FEATURE|UNSUPPORTED_FEATURE|Unsupported statement|" + # Specific unsupported features, split out so the dashboard shows each + # distinct gap (count + what it means) rather than one opaque bucket. + (r"Unsupported statement:\s*SHOW COLUMNS", "Unsupported statement: SHOW COLUMNS"), + (r"Unsupported CREATE type:\s*SCHEMA", "Unsupported CREATE type: SCHEMA"), + (r"CREATE OR REPLACE TABLE", "Unsupported feature: CREATE OR REPLACE TABLE"), + (r"interval year to month|interval day to second", "Unsupported type: INTERVAL"), + # Any other unsupported DDL/statement/type Reyden rejects. + (r"PARSER_UNSUPPORTED_FEATURE|UNSUPPORTED_FEATURE|DELTA_UNSUPPORTED|Unsupported statement|" r"Unsupported CREATE type|Unsupported Delta table type|Unsupported .*type", - "Reyden unsupported feature (DDL / statement / type)"), + "Reyden unsupported feature (other)"), # --- Environment / infra -------------------------------------------- (r"ENDPOINT_NOT_FOUND", "Warehouse not found (ENDPOINT_NOT_FOUND / HTTP 404)"), (r"read[- ]?only|READ_ONLY|cannot be modified|not.*allowed.*read", "Read-only warehouse rejected write/DDL"), @@ -116,6 +123,12 @@ def signature_for(message): _SIGNATURE_CATEGORY = { "Thrift endpoint unavailable on SEA/Reyden warehouse": CAT_REYDEN_GAP, "CloudFetch not supported on Reyden (download failed)": CAT_REYDEN_GAP, + "Unsupported statement: SHOW COLUMNS": CAT_REYDEN_GAP, + "Unsupported CREATE type: SCHEMA": CAT_REYDEN_GAP, + "Unsupported feature: CREATE OR REPLACE TABLE": CAT_REYDEN_GAP, + "Unsupported type: INTERVAL": CAT_REYDEN_GAP, + "Reyden unsupported feature (other)": CAT_REYDEN_GAP, + # Back-compat: older runs used one combined signature. "Reyden unsupported feature (DDL / statement / type)": CAT_REYDEN_GAP, "Warehouse not found (ENDPOINT_NOT_FOUND / HTTP 404)": CAT_ENVIRONMENT, "Read-only warehouse rejected write/DDL": CAT_ENVIRONMENT, From 37243c529374f4297d6141f18347329ec4d9934f Mon Sep 17 00:00:00 2001 From: eric-wang-1990 Date: Fri, 5 Jun 2026 15:07:50 -0700 Subject: [PATCH 12/17] Revert E2E test changes on this branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops the csharp/test/E2E/* edits introduced by the Reyden read-only adaptation (commit 167e356) and restores them to origin/main. Per PR #505 review the E2E test edits should not ship as part of the Reyden nightly infrastructure PR — they belong in a separate change that proposes a read-only-warehouse-aware test model independently. Files restored from origin/main: csharp/test/E2E/DatabricksTestConfiguration.cs csharp/test/E2E/ServerSidePropertyE2ETest.cs csharp/test/E2E/StatementExecution/StatementExecutionDriverE2ETests.cs csharp/test/E2E/StatementTests.cs Non-E2E commits (workflow plumbing, classifier, dashboard, etc.) are unaffected. Co-authored-by: Isaac --- .../test/E2E/DatabricksTestConfiguration.cs | 6 -- csharp/test/E2E/ServerSidePropertyE2ETest.cs | 1 - .../StatementExecutionDriverE2ETests.cs | 64 ++++++------------- csharp/test/E2E/StatementTests.cs | 23 ------- 4 files changed, 21 insertions(+), 73 deletions(-) diff --git a/csharp/test/E2E/DatabricksTestConfiguration.cs b/csharp/test/E2E/DatabricksTestConfiguration.cs index 9b085b932..dd1352a3b 100644 --- a/csharp/test/E2E/DatabricksTestConfiguration.cs +++ b/csharp/test/E2E/DatabricksTestConfiguration.cs @@ -58,12 +58,6 @@ public class DatabricksTestConfiguration : SparkTestConfiguration [JsonPropertyName("isCITesting"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public bool IsCITesting { get; set; } = false; - [JsonPropertyName("isReadOnly"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public bool IsReadOnly { get; set; } = false; - - [JsonPropertyName("mutableTable"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public string MutableTable { get; set; } = string.Empty; - [JsonPropertyName("enableRunAsyncInThriftOp"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string EnableRunAsyncInThriftOp { get; set; } = string.Empty; diff --git a/csharp/test/E2E/ServerSidePropertyE2ETest.cs b/csharp/test/E2E/ServerSidePropertyE2ETest.cs index 3719461e9..62142d76f 100644 --- a/csharp/test/E2E/ServerSidePropertyE2ETest.cs +++ b/csharp/test/E2E/ServerSidePropertyE2ETest.cs @@ -106,7 +106,6 @@ public async Task TestServerSideProperty(bool applyWithQueries) [InlineData(false)] public async Task TestServerSidePropertyOnSeaPath(bool applyWithQueries) { - Skip.If(TestConfiguration.IsReadOnly, "Reyden does not support bare SET statement"); var additionalConnectionParams = new Dictionary() { // Force the SEA path regardless of the test config's default protocol. diff --git a/csharp/test/E2E/StatementExecution/StatementExecutionDriverE2ETests.cs b/csharp/test/E2E/StatementExecution/StatementExecutionDriverE2ETests.cs index 8358736b4..f62972185 100644 --- a/csharp/test/E2E/StatementExecution/StatementExecutionDriverE2ETests.cs +++ b/csharp/test/E2E/StatementExecution/StatementExecutionDriverE2ETests.cs @@ -223,20 +223,15 @@ public void ExecuteUpdate_InsertData_ReturnsAffectedRows() { SkipIfNotConfigured(); - bool usePredefinedTable = TestConfiguration.IsReadOnly; - Skip.If(usePredefinedTable && string.IsNullOrEmpty(TestConfiguration.MutableTable), "IsReadOnly mode requires mutableTable in test config"); - using var connection = CreateRestConnection(); - string tableName = usePredefinedTable ? TestConfiguration.MutableTable : $"test_insert_{Guid.NewGuid():N}".Substring(0, 40); + var tableName = $"test_insert_{Guid.NewGuid():N}".Substring(0, 40); try { - if (!usePredefinedTable) - { - using var createStatement = connection.CreateStatement(); - createStatement.SqlQuery = $"CREATE TABLE {tableName} (id INT, name STRING) USING DELTA"; - createStatement.ExecuteUpdate(); - } + // Create table + using var createStatement = connection.CreateStatement(); + createStatement.SqlQuery = $"CREATE TABLE {tableName} (id INT, name STRING) USING DELTA"; + createStatement.ExecuteUpdate(); // Insert data using var insertStatement = connection.CreateStatement(); @@ -248,13 +243,10 @@ public void ExecuteUpdate_InsertData_ReturnsAffectedRows() } finally { - if (!usePredefinedTable) - { - // Cleanup - using var dropStatement = connection.CreateStatement(); - dropStatement.SqlQuery = $"DROP TABLE IF EXISTS {tableName}"; - dropStatement.ExecuteUpdate(); - } + // Cleanup + using var dropStatement = connection.CreateStatement(); + dropStatement.SqlQuery = $"DROP TABLE IF EXISTS {tableName}"; + dropStatement.ExecuteUpdate(); } } @@ -262,7 +254,6 @@ public void ExecuteUpdate_InsertData_ReturnsAffectedRows() public void ExecuteUpdate_UpdateData_ReturnsAffectedRows() { SkipIfNotConfigured(); - Skip.If(TestConfiguration.IsReadOnly, "Reyden does not support UPDATE"); using var connection = CreateRestConnection(); var tableName = $"test_update_{Guid.NewGuid():N}".Substring(0, 40); @@ -299,7 +290,6 @@ public void ExecuteUpdate_UpdateData_ReturnsAffectedRows() public void ExecuteUpdate_DeleteData_ReturnsAffectedRows() { SkipIfNotConfigured(); - Skip.If(TestConfiguration.IsReadOnly, "Reyden does not support DELETE"); using var connection = CreateRestConnection(); var tableName = $"test_delete_{Guid.NewGuid():N}".Substring(0, 40); @@ -359,32 +349,23 @@ public void ExecuteQuery_AfterInsert_ReturnsInsertedData() { SkipIfNotConfigured(); - bool usePredefinedTable = TestConfiguration.IsReadOnly; - Skip.If(usePredefinedTable && string.IsNullOrEmpty(TestConfiguration.MutableTable), "IsReadOnly mode requires mutableTable in test config"); - using var connection = CreateRestConnection(); - string tableName = usePredefinedTable ? TestConfiguration.MutableTable : $"test_query_after_insert_{Guid.NewGuid():N}".Substring(0, 40); - // Use unique IDs to isolate rows across concurrent runs when sharing the mutable table. - int id1 = usePredefinedTable ? Math.Abs(Guid.NewGuid().GetHashCode()) : 1; - int id2 = id1 + 1; + var tableName = $"test_query_after_insert_{Guid.NewGuid():N}".Substring(0, 40); try { - if (!usePredefinedTable) - { - using var createStatement = connection.CreateStatement(); - createStatement.SqlQuery = $"CREATE TABLE {tableName} (id INT, name STRING) USING DELTA"; - createStatement.ExecuteUpdate(); - } + // Create and populate table + using var createStatement = connection.CreateStatement(); + createStatement.SqlQuery = $"CREATE TABLE {tableName} (id INT, name STRING) USING DELTA"; + createStatement.ExecuteUpdate(); using var insertStatement = connection.CreateStatement(); - insertStatement.SqlQuery = $"INSERT INTO {tableName} VALUES ({id1}, 'Alice'), ({id2}, 'Bob')"; + insertStatement.SqlQuery = $"INSERT INTO {tableName} VALUES (1, 'Alice'), (2, 'Bob')"; insertStatement.ExecuteUpdate(); - // Query the data — filter by unique IDs when using the shared mutable table. - string whereClause = usePredefinedTable ? $" WHERE id IN ({id1}, {id2})" : ""; + // Query the data using var selectStatement = connection.CreateStatement(); - selectStatement.SqlQuery = $"SELECT * FROM {tableName}{whereClause} ORDER BY id"; + selectStatement.SqlQuery = $"SELECT * FROM {tableName} ORDER BY id"; var queryResult = selectStatement.ExecuteQuery(); Assert.NotNull(queryResult); @@ -403,13 +384,10 @@ public void ExecuteQuery_AfterInsert_ReturnsInsertedData() } finally { - if (!usePredefinedTable) - { - // Cleanup - using var dropStatement = connection.CreateStatement(); - dropStatement.SqlQuery = $"DROP TABLE IF EXISTS {tableName}"; - dropStatement.ExecuteUpdate(); - } + // Cleanup + using var dropStatement = connection.CreateStatement(); + dropStatement.SqlQuery = $"DROP TABLE IF EXISTS {tableName}"; + dropStatement.ExecuteUpdate(); } } diff --git a/csharp/test/E2E/StatementTests.cs b/csharp/test/E2E/StatementTests.cs index a96252021..6e5e0bf85 100644 --- a/csharp/test/E2E/StatementTests.cs +++ b/csharp/test/E2E/StatementTests.cs @@ -45,24 +45,6 @@ public StatementTests(ITestOutputHelper? outputHelper) { } - // Reyden does not support DDL (CREATE TABLE) — hide the inherited test and skip it. -#pragma warning disable xUnit1024 - [SkippableFact] - public new async Task CanInteractUsingSetOptions() - { - Skip.If(TestConfiguration.IsReadOnly, "Reyden does not support DDL (CREATE TABLE via TemporaryTable)"); - await base.CanInteractUsingSetOptions(); - } - - // Reyden does not support SHOW COLUMNS — hide the inherited test and skip it. - [SkippableFact] - public new async Task CanGetColumns() - { - Skip.If(TestConfiguration.IsReadOnly, "Reyden does not support SHOW COLUMNS"); - await base.CanGetColumns(); - } -#pragma warning restore xUnit1024 - // TODO: PECO-3011 - SEA StatementExecutionStatement does not validate poll time option protected override void ValidateCanSetOptionPollTime(string value, bool throws = false) { @@ -189,7 +171,6 @@ protected override void CreateNewTableName(out string tableName, out string full [SkippableFact] public async Task CanGetPrimaryKeysDatabricks() { - Skip.If(TestConfiguration.IsReadOnly, "Reyden does not support KEY primitive type (SHOW PRIMARY KEYS)"); await base.CanGetPrimaryKeys(TestConfiguration.Metadata.Catalog, TestConfiguration.Metadata.Schema); } @@ -204,7 +185,6 @@ public async Task CanGetCrossReferenceFromParentTableDatabricks() [SkippableFact] public async Task CanGetCrossReferenceFromChildTableDatabricks() { - Skip.If(TestConfiguration.IsReadOnly, "Reyden does not support KEY primitive type (SHOW CROSS REFERENCES)"); await base.CanGetCrossReferenceFromChildTable(TestConfiguration.Metadata.Catalog, TestConfiguration.Metadata.Schema); } @@ -290,7 +270,6 @@ public async Task AllStatementTypesDisposeWithoutErrors(string statementType, st [SkippableFact] public async Task CanGetColumnsWithBaseTypeName() { - Skip.If(TestConfiguration.IsReadOnly, "Reyden does not support SHOW COLUMNS"); var statement = Connection.CreateStatement(); statement.SetOption(ApacheParameters.IsMetadataCommand, "true"); statement.SetOption(ApacheParameters.CatalogName, TestConfiguration.Metadata.Catalog); @@ -654,7 +633,6 @@ public async Task CanGetColumnsExtended(string tableName, string createTableSqlL [SkippableFact] public async Task CanGetColumnsOnNoColumnTable() { - Skip.If(TestConfiguration.IsReadOnly, "Reyden does not support DDL (CREATE TABLE) or SHOW COLUMNS"); string? catalogName = TestConfiguration.Metadata.Catalog; string? schemaName = TestConfiguration.Metadata.Schema; string tableName = Guid.NewGuid().ToString("N"); @@ -1022,7 +1000,6 @@ public async Task StatusPollerKeepsQueryAlive(bool useCloudFetch, string configN [InlineData("false", false)] // Should only use default catalog public async Task EnableMultipleCatalogSupportAffectsMetadataQueries(string enableMultipleCatalogSupport, bool shouldAllowMultipleCatalogs) { - Skip.If(TestConfiguration.IsReadOnly, "Reyden does not support SHOW COLUMNS or SHOW CATALOGS"); // Create a connection with the specified EnableMultipleCatalogSupport setting var testConfig = (DatabricksTestConfiguration)TestConfiguration.Clone(); testConfig.EnableMultipleCatalogSupport = enableMultipleCatalogSupport; From 2ec64ffd3401365a80d299bdb1f858a281dab1ad Mon Sep 17 00:00:00 2001 From: eric-wang-1990 Date: Fri, 5 Jun 2026 15:20:18 -0700 Subject: [PATCH 13/17] Un-wrap Reyden nightly; restore generic e2e-tests workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "make e2e-tests callable + Reyden nightly as thin wrapper" refactor (ce48179) leaked Reyden-specific routing (use_reyden_for_execution, EXECUTION_HTTP_PATH, CREATE TABLE adbc_testing_mutable, isReadOnly / mutableTable connection.json fields, the publish-dashboard job, …) into the generic e2e-tests workflow. With the C# read-only test adaptation reverted in 37243c5 those knobs no longer have a consumer, leaving e2e-tests.yml carrying dead Reyden code on every PR run. This commit moves all Reyden-specific scaffolding out of e2e-tests.yml and back into reyden-rest-nightly.yml as a self-contained workflow: - e2e-tests.yml restored byte-for-byte to origin/main (no workflow_call, no Reyden routing, no dashboard, no TRX upload, no e2e_only filter). - reyden-rest-nightly.yml rebuilt from the pre-refactor 6a9501f version with the additions that lived briefly in e2e-tests.yml: - CREATE TABLE adbc_testing_mutable in the seed step (Reyden supports INSERT but not CREATE TABLE). - isReadOnly / mutableTable fields in the generated connection.json. - --filter FullyQualifiedName!~Tests.Unit on the test invocation (nightly only runs E2E). - TRX logger + upload-artifact step. - publish-dashboard job that parses TRX and pushes to gh-pages (RUN_READ_ONLY is hard-coded 'true' since this workflow always exercises the read-only Reyden warehouse). - permissions: contents: write at workflow scope for the gh-pages push. Helper scripts (.github/scripts/parse-trx-to-json.py, .github/scripts/update-e2e-dashboard.py) and .github/e2e-dashboard/ index.html are unchanged; the consolidated publish-dashboard job calls them in place. Trade-off accepted: ~180 lines of duplication between the two workflow files. Justified because Reyden's needs (split setup/execution warehouses, pre-created mutable table, read-only-tagged dashboard) don't fit a generic e2e-tests parameterization cleanly, and the called-workflow extensibility model GitHub Actions provides is too limited for the kind of hooks Reyden would need. Co-authored-by: Isaac --- .github/workflows/e2e-tests.yml | 126 +-------- .github/workflows/reyden-rest-nightly.yml | 307 +++++++++++++++++++++- 2 files changed, 300 insertions(+), 133 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index b8b546f28..22609b029 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -15,28 +15,6 @@ name: Tests Workflow on: - workflow_call: - inputs: - protocol: - description: 'Protocol to test (thrift, rest, or both)' - required: false - default: 'rest' - type: string - use_reyden_for_execution: - description: 'Route test execution through the Reyden warehouse (TEST_PECO_REYDEN_HTTP_PATH). Setup/teardown always use TEST_PECO_WAREHOUSE_HTTP_PATH.' - required: false - default: false - type: boolean - e2e_only: - description: 'Skip unit tests — run only E2E tests (excludes Tests.Unit namespace).' - required: false - default: false - type: boolean - publish_dashboard: - description: 'Publish parsed test results to the GitHub Pages E2E nightly dashboard (gh-pages branch). Intended for nightly runs.' - required: false - default: false - type: boolean workflow_dispatch: inputs: protocol: @@ -73,10 +51,7 @@ jobs: protocol: ${{ inputs.protocol == 'thrift' && fromJson('["thrift"]') || inputs.protocol == 'rest' && fromJson('["rest"]') || fromJson('["thrift","rest"]') }} env: DATABRICKS_SERVER_HOSTNAME: ${{ secrets.DATABRICKS_HOST }} - # Setup/teardown always use the regular warehouse (supports DDL). - # Test execution uses Reyden when use_reyden_for_execution=true. DATABRICKS_HTTP_PATH: ${{ secrets.TEST_PECO_WAREHOUSE_HTTP_PATH }} - EXECUTION_HTTP_PATH: ${{ inputs.use_reyden_for_execution && secrets.TEST_PECO_REYDEN_HTTP_PATH || secrets.TEST_PECO_WAREHOUSE_HTTP_PATH }} DATABRICKS_TEST_CLIENT_ID: ${{ secrets.DATABRICKS_TEST_CLIENT_ID }} DATABRICKS_TEST_CLIENT_SECRET: ${{ secrets.DATABRICKS_TEST_CLIENT_SECRET }} steps: @@ -231,20 +206,6 @@ jobs: # Keep log noise low — first 80 chars per statement. print(f"OK [{i}/{len(statements)}]: {stmt[:80].replace(chr(10), ' ')}") print(f"Seeded {len(statements)} statements into main.{os.environ['PER_RUN_SCHEMA']}") - - # Create an empty mutable table for INSERT tests (used when execution routes through Reyden, - # which supports INSERT but not CREATE TABLE). - schema = os.environ["PER_RUN_SCHEMA"] - mutable_stmt = f"CREATE TABLE IF NOT EXISTS main.{schema}.adbc_testing_mutable (id INT, name STRING) USING DELTA" - payload = json.dumps({"warehouse_id": warehouse, "statement": mutable_stmt, "wait_timeout": "30s"}).encode() - req = urllib.request.Request(url, data=payload, - headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, method="POST") - with urllib.request.urlopen(req) as resp: - body = json.loads(resp.read()) - if body.get("status", {}).get("state", "") != "SUCCEEDED": - print(f"ERROR: CREATE mutable table failed: {json.dumps(body, indent=2)}") - sys.exit(1) - print(f"Created mutable table: main.{schema}.adbc_testing_mutable") PYEOF - name: Create Databricks config file @@ -255,7 +216,7 @@ jobs: mkdir -p ~/.databricks cat > ~/.databricks/connection.json << EOF { - "uri": "https://${{ env.DATABRICKS_SERVER_HOSTNAME }}${{ env.EXECUTION_HTTP_PATH }}", + "uri": "https://${{ env.DATABRICKS_SERVER_HOSTNAME }}${{ env.DATABRICKS_HTTP_PATH }}", "auth_type": "oauth", "grant_type": "client_credentials", "client_id": "${{ env.DATABRICKS_TEST_CLIENT_ID }}", @@ -269,8 +230,6 @@ jobs: "query": "SELECT * FROM main.${PER_RUN_SCHEMA}.adbc_testing_table", "expectedResults": 12, "isCITesting": true, - "isReadOnly": ${{ inputs.use_reyden_for_execution == true }}, - "mutableTable": "main.${PER_RUN_SCHEMA}.adbc_testing_mutable", "tracePropagationEnabled": "true", "traceParentHeaderName": "traceparent", "traceStateEnabled": "false", @@ -295,25 +254,7 @@ jobs: shell: bash run: | export DATABRICKS_TEST_CONFIG_FILE="$HOME/.databricks/connection.json" - FILTER_ARG="" - if [[ "${{ inputs.e2e_only }}" == "true" ]]; then - FILTER_ARG="--filter FullyQualifiedName!~Tests.Unit" - fi - # Always emit a TRX so the dashboard can parse pass/fail/skip + per-test - # failure detail, even when the run fails (set -e in the script still - # writes the TRX before the non-zero exit; the upload step is always()). - ./ci/scripts/csharp_test_databricks_e2e.sh "${{ github.workspace }}" $FILTER_ARG \ - --logger "trx;LogFileName=results-${{ matrix.protocol }}.trx" \ - --results-directory "${{ github.workspace }}/TestResults" - - - name: Upload test results (TRX) - if: always() && steps.gate.outputs.run == 'true' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: e2e-trx-${{ matrix.protocol }} - path: ${{ github.workspace }}/TestResults/*.trx - if-no-files-found: warn - retention-days: 7 + ./ci/scripts/csharp_test_databricks_e2e.sh "${{ github.workspace }}" # Always run, even on test failure or job cancellation, so we don't # leak schemas in the workspace. @@ -339,66 +280,3 @@ jobs: else echo "Dropped schema: main.${PER_RUN_SCHEMA}" fi - - # Parse the TRX results and publish an append-only history dashboard to the - # gh-pages branch (served at https://adbc-drivers.github.io/databricks/e2e-nightly/). - # Runs only when the caller opts in (nightly), and always() so a failed test - # run is still recorded — surfacing failures is the whole point of the page. - publish-dashboard: - name: Publish E2E Dashboard - needs: run-e2e-tests - if: always() && inputs.publish_dashboard - runs-on: ubuntu-latest - permissions: - contents: write # push parsed results to the gh-pages branch - steps: - - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 0 - - - name: Download test results - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - with: - pattern: e2e-trx-* - path: trx - - - name: Build dashboard and push to gh-pages - env: - RUN_READ_ONLY: ${{ inputs.use_reyden_for_execution }} - run: | - set -e - shopt -s nullglob - RUN_TIMESTAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)" - export RUN_TIMESTAMP - - dirs=(trx/e2e-trx-*) - if [ ${#dirs[@]} -eq 0 ]; then - echo "No TRX artifacts found — nothing to publish." - exit 0 - fi - - git fetch origin gh-pages - git worktree add /tmp/ghp gh-pages - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - for d in "${dirs[@]}"; do - proto="${d#trx/e2e-trx-}" - export RUN_PROTOCOL="$proto" - echo "::group::Parsing $proto" - python3 .github/scripts/parse-trx-to-json.py "/tmp/record-${proto}.json" "$d" - python3 .github/scripts/update-e2e-dashboard.py \ - "/tmp/record-${proto}.json" /tmp/ghp/e2e-nightly .github/e2e-dashboard/index.html - echo "::endgroup::" - done - - cd /tmp/ghp - git add e2e-nightly - if git diff --cached --quiet; then - echo "No dashboard changes to commit." - else - git commit -m "e2e-nightly: record run ${GITHUB_RUN_ID} (${GITHUB_REF_NAME})" - git push origin gh-pages - echo "Published: https://adbc-drivers.github.io/databricks/e2e-nightly/" - fi diff --git a/.github/workflows/reyden-rest-nightly.yml b/.github/workflows/reyden-rest-nightly.yml index ed57215c0..5487866a3 100644 --- a/.github/workflows/reyden-rest-nightly.yml +++ b/.github/workflows/reyden-rest-nightly.yml @@ -12,6 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +# Nightly E2E run against the Reyden REST warehouse. +# +# Self-contained workflow: the generic e2e-tests.yml does not carry any +# Reyden-specific routing (Reyden rejects DDL on the SQL statement API), so +# all Reyden-aware setup, test-config emission, test execution, and result +# publishing lives in this file. Setup/teardown talks to a regular DBSQL +# warehouse (TEST_PECO_WAREHOUSE_HTTP_PATH) for the DDL the Reyden warehouse +# would refuse; only the actual test run targets the Reyden warehouse +# (TEST_PECO_REYDEN_HTTP_PATH). name: Reyden REST Nightly E2E on: @@ -20,17 +29,297 @@ on: - cron: '0 2 * * *' workflow_dispatch: -# Grant the reusable workflow permission to push the parsed results to the -# gh-pages branch (the dashboard's append-only history store). +# publish-dashboard pushes the parsed test results to the gh-pages branch. permissions: contents: write +concurrency: + group: ${{ github.repository }}-reyden-rest-nightly + cancel-in-progress: false + jobs: reyden-rest-nightly: - uses: ./.github/workflows/e2e-tests.yml - with: - protocol: rest - use_reyden_for_execution: true - e2e_only: true - publish_dashboard: true - secrets: inherit + name: "Reyden REST Nightly E2E" + runs-on: ubuntu-latest + env: + DATABRICKS_SERVER_HOSTNAME: ${{ secrets.DATABRICKS_HOST }} + # Reyden warehouse — used only for test execution (no DDL support) + DATABRICKS_HTTP_PATH: ${{ secrets.TEST_PECO_REYDEN_HTTP_PATH }} + # Regular DBSQL warehouse — used for schema/table setup and teardown + SETUP_HTTP_PATH: ${{ secrets.TEST_PECO_WAREHOUSE_HTTP_PATH }} + DATABRICKS_TEST_CLIENT_ID: ${{ secrets.DATABRICKS_TEST_CLIENT_ID }} + DATABRICKS_TEST_CLIENT_SECRET: ${{ secrets.DATABRICKS_TEST_CLIENT_SECRET }} + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + submodules: recursive + fetch-depth: 0 + + - name: Set up .NET + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + with: + dotnet-version: '8.0.x' + + - name: Generate OAuth access token + id: oauth + run: | + OAUTH_RESPONSE=$(curl -s -X POST "https://${{ env.DATABRICKS_SERVER_HOSTNAME }}/oidc/v1/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" \ + -d "client_id=${{ env.DATABRICKS_TEST_CLIENT_ID }}" \ + -d "client_secret=${{ env.DATABRICKS_TEST_CLIENT_SECRET }}" \ + -d "scope=sql") + OAUTH_TOKEN=$(echo "$OAUTH_RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin)['access_token'])") + if [ -z "$OAUTH_TOKEN" ]; then + echo "ERROR: Failed to generate OAuth token" + exit 1 + fi + echo "::add-mask::$OAUTH_TOKEN" + echo "OAUTH_TOKEN=$OAUTH_TOKEN" >> $GITHUB_OUTPUT + + - name: Compute per-run schema name + id: schema + run: | + PER_RUN_SCHEMA="adbc_testing_run_${{ github.run_id }}_${{ github.run_attempt }}_rest" + echo "PER_RUN_SCHEMA=$PER_RUN_SCHEMA" >> $GITHUB_OUTPUT + echo "Per-run schema: main.$PER_RUN_SCHEMA" + # Use the regular warehouse ID for setup operations + WAREHOUSE_ID=$(echo "${{ env.SETUP_HTTP_PATH }}" | awk -F/ '{print $NF}') + if [ -z "$WAREHOUSE_ID" ]; then + echo "ERROR: Could not extract warehouse id from SETUP_HTTP_PATH" + exit 1 + fi + echo "WAREHOUSE_ID=$WAREHOUSE_ID" >> $GITHUB_OUTPUT + + - name: Provision per-run schema + env: + OAUTH_TOKEN: ${{ steps.oauth.outputs.OAUTH_TOKEN }} + PER_RUN_SCHEMA: ${{ steps.schema.outputs.PER_RUN_SCHEMA }} + WAREHOUSE_ID: ${{ steps.schema.outputs.WAREHOUSE_ID }} + run: | + set -e + STATEMENT="CREATE SCHEMA IF NOT EXISTS main.${PER_RUN_SCHEMA}" + PAYLOAD=$(jq -n --arg w "$WAREHOUSE_ID" --arg s "$STATEMENT" '{warehouse_id: $w, statement: $s, wait_timeout: "30s"}') + RESPONSE=$(curl -sS -X POST "https://${{ env.DATABRICKS_SERVER_HOSTNAME }}/api/2.0/sql/statements" \ + -H "Authorization: Bearer $OAUTH_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD") + STATUS=$(echo "$RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin).get('status', {}).get('state', ''))") + if [ "$STATUS" != "SUCCEEDED" ]; then + echo "ERROR: CREATE SCHEMA failed (status=$STATUS)" + echo "Response: $RESPONSE" + exit 1 + fi + echo "Created schema: main.${PER_RUN_SCHEMA}" + + - name: Seed test data + env: + OAUTH_TOKEN: ${{ steps.oauth.outputs.OAUTH_TOKEN }} + PER_RUN_SCHEMA: ${{ steps.schema.outputs.PER_RUN_SCHEMA }} + WAREHOUSE_ID: ${{ steps.schema.outputs.WAREHOUSE_ID }} + DATABRICKS_SERVER_HOSTNAME: ${{ env.DATABRICKS_SERVER_HOSTNAME }} + run: | + python3 - <<'PYEOF' + import json, os, re, sys, urllib.request + + with open("csharp/test/Resources/Databricks.sql") as f: + content = f.read() + content = re.sub(r"^\s*--.*$", "", content, flags=re.MULTILINE) + full_table = f"main.{os.environ['PER_RUN_SCHEMA']}.adbc_testing_table" + content = content.replace("{ADBC_CATALOG}.{ADBC_DATASET}.{ADBC_TABLE}", full_table) + statements = [s.strip() for s in content.split(";") if s.strip()] + + host = os.environ["DATABRICKS_SERVER_HOSTNAME"] + token = os.environ["OAUTH_TOKEN"] + warehouse = os.environ["WAREHOUSE_ID"] + url = f"https://{host}/api/2.0/sql/statements" + + for i, stmt in enumerate(statements, 1): + payload = json.dumps({ + "warehouse_id": warehouse, + "statement": stmt, + "wait_timeout": "30s", + }).encode() + req = urllib.request.Request( + url, data=payload, + headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, + method="POST", + ) + with urllib.request.urlopen(req) as resp: + body = json.loads(resp.read()) + state = body.get("status", {}).get("state", "") + if state != "SUCCEEDED": + print(f"ERROR: statement {i} failed (state={state})") + print(f"Statement: {stmt[:200]}") + print(f"Response: {json.dumps(body, indent=2)}") + sys.exit(1) + print(f"OK [{i}/{len(statements)}]: {stmt[:80].replace(chr(10), ' ')}") + print(f"Seeded {len(statements)} statements into main.{os.environ['PER_RUN_SCHEMA']}") + + # Create an empty mutable table for INSERT tests. Reyden supports INSERT but + # not CREATE TABLE, so the table has to be pre-created here using the regular + # warehouse before tests start writing to it. + schema = os.environ["PER_RUN_SCHEMA"] + mutable_stmt = f"CREATE TABLE IF NOT EXISTS main.{schema}.adbc_testing_mutable (id INT, name STRING) USING DELTA" + payload = json.dumps({"warehouse_id": warehouse, "statement": mutable_stmt, "wait_timeout": "30s"}).encode() + req = urllib.request.Request(url, data=payload, + headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, method="POST") + with urllib.request.urlopen(req) as resp: + body = json.loads(resp.read()) + if body.get("status", {}).get("state", "") != "SUCCEEDED": + print(f"ERROR: CREATE mutable table failed: {json.dumps(body, indent=2)}") + sys.exit(1) + print(f"Created mutable table: main.{schema}.adbc_testing_mutable") + PYEOF + + - name: Create Databricks config file + env: + PER_RUN_SCHEMA: ${{ steps.schema.outputs.PER_RUN_SCHEMA }} + run: | + mkdir -p ~/.databricks + cat > ~/.databricks/connection.json << EOF + { + "uri": "https://${{ env.DATABRICKS_SERVER_HOSTNAME }}${{ env.DATABRICKS_HTTP_PATH }}", + "auth_type": "oauth", + "grant_type": "client_credentials", + "client_id": "${{ env.DATABRICKS_TEST_CLIENT_ID }}", + "client_secret": "${{ env.DATABRICKS_TEST_CLIENT_SECRET }}", + "scope": "sql", + "access_token": "${{ steps.oauth.outputs.OAUTH_TOKEN }}", + "type": "databricks", + "protocol": "rest", + "catalog": "main", + "db_schema": "${PER_RUN_SCHEMA}", + "query": "SELECT * FROM main.${PER_RUN_SCHEMA}.adbc_testing_table", + "expectedResults": 12, + "isCITesting": true, + "isReadOnly": true, + "mutableTable": "main.${PER_RUN_SCHEMA}.adbc_testing_mutable", + "tracePropagationEnabled": "true", + "traceParentHeaderName": "traceparent", + "traceStateEnabled": "false", + "metadata": { + "catalog": "main", + "schema": "${PER_RUN_SCHEMA}", + "table": "adbc_testing_table", + "expectedColumnCount": 19 + } + } + EOF + echo "DATABRICKS_TEST_CONFIG_FILE=$HOME/.databricks/connection.json" >> $GITHUB_ENV + + - name: Build + shell: bash + run: | + ./ci/scripts/csharp_build.sh "${{ github.workspace }}" + + - name: Run E2E tests + shell: bash + run: | + export DATABRICKS_TEST_CONFIG_FILE="$HOME/.databricks/connection.json" + # Nightly only — skip unit tests (Tests.Unit namespace). Always emit a + # TRX so the dashboard can parse pass/fail/skip + per-test failure + # detail; set -e in the script still writes the TRX before the + # non-zero exit and the upload step is always(). + ./ci/scripts/csharp_test_databricks_e2e.sh "${{ github.workspace }}" \ + --filter FullyQualifiedName!~Tests.Unit \ + --logger "trx;LogFileName=results-rest.trx" \ + --results-directory "${{ github.workspace }}/TestResults" + + - name: Upload test results (TRX) + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: e2e-trx-rest + path: ${{ github.workspace }}/TestResults/*.trx + if-no-files-found: warn + retention-days: 7 + + # Always run, even on test failure or job cancellation, so we don't leak + # schemas in the workspace. + - name: Drop per-run schema + if: always() && steps.schema.outputs.PER_RUN_SCHEMA != '' + env: + OAUTH_TOKEN: ${{ steps.oauth.outputs.OAUTH_TOKEN }} + PER_RUN_SCHEMA: ${{ steps.schema.outputs.PER_RUN_SCHEMA }} + WAREHOUSE_ID: ${{ steps.schema.outputs.WAREHOUSE_ID }} + run: | + STATEMENT="DROP SCHEMA IF EXISTS main.${PER_RUN_SCHEMA} CASCADE" + PAYLOAD=$(jq -n --arg w "$WAREHOUSE_ID" --arg s "$STATEMENT" '{warehouse_id: $w, statement: $s, wait_timeout: "30s"}') + RESPONSE=$(curl -sS -X POST "https://${{ env.DATABRICKS_SERVER_HOSTNAME }}/api/2.0/sql/statements" \ + -H "Authorization: Bearer $OAUTH_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD") + STATUS=$(echo "$RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin).get('status', {}).get('state', ''))") + if [ "$STATUS" != "SUCCEEDED" ]; then + # Don't fail the job on cleanup error — leaked schemas can be + # garbage-collected later. But surface the failure. + echo "WARNING: DROP SCHEMA failed (status=$STATUS)" + echo "Response: $RESPONSE" + else + echo "Dropped schema: main.${PER_RUN_SCHEMA}" + fi + + # Parse the TRX results and publish an append-only history to the gh-pages + # branch (served at https://adbc-drivers.github.io/databricks/e2e-nightly/). + # Runs always() so a failed test run is still recorded — surfacing failures + # is the whole point of the page. + publish-dashboard: + name: Publish E2E Dashboard + needs: reyden-rest-nightly + if: always() + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: Download test results + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: e2e-trx-* + path: trx + + - name: Build dashboard and push to gh-pages + env: + # The Reyden nightly always exercises the read-only Reyden warehouse, + # so the parsed run record is tagged read-only for the dashboard. + RUN_READ_ONLY: 'true' + run: | + set -e + shopt -s nullglob + RUN_TIMESTAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + export RUN_TIMESTAMP + + dirs=(trx/e2e-trx-*) + if [ ${#dirs[@]} -eq 0 ]; then + echo "No TRX artifacts found — nothing to publish." + exit 0 + fi + + git fetch origin gh-pages + git worktree add /tmp/ghp gh-pages + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + for d in "${dirs[@]}"; do + proto="${d#trx/e2e-trx-}" + export RUN_PROTOCOL="$proto" + echo "::group::Parsing $proto" + python3 .github/scripts/parse-trx-to-json.py "/tmp/record-${proto}.json" "$d" + python3 .github/scripts/update-e2e-dashboard.py \ + "/tmp/record-${proto}.json" /tmp/ghp/e2e-nightly .github/e2e-dashboard/index.html + echo "::endgroup::" + done + + cd /tmp/ghp + git add e2e-nightly + if git diff --cached --quiet; then + echo "No dashboard changes to commit." + else + git commit -m "e2e-nightly: record run ${GITHUB_RUN_ID} (${GITHUB_REF_NAME})" + git push origin gh-pages + echo "Published: https://adbc-drivers.github.io/databricks/e2e-nightly/" + fi From 6437f0f3d2e9c176e0b0e6a447d4cca65cdea698 Mon Sep 17 00:00:00 2001 From: eric-wang-1990 Date: Sat, 6 Jun 2026 03:43:03 +0000 Subject: [PATCH 14/17] ci(dashboard): make truncated per-error test list expandable The "By error (root cause)" table capped the Tests column at 8 names and showed a static "+N more". Turn that into a clickable toggle that expands the full deduped test list and collapses back to "show fewer". Co-authored-by: Isaac --- .github/e2e-dashboard/index.html | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/e2e-dashboard/index.html b/.github/e2e-dashboard/index.html index 131cc10a1..e8c5d9db9 100644 --- a/.github/e2e-dashboard/index.html +++ b/.github/e2e-dashboard/index.html @@ -53,6 +53,8 @@ .fail-list .tname { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12.5px; word-break:break-word; } .fail-list .msg { color:var(--muted); font-size:12px; margin-top:2px; white-space:pre-wrap; word-break:break-word; } .muted { color:var(--muted); } .small { font-size:12px; } + .toggle-tests { color:#2563eb; text-decoration:none; cursor:pointer; } + .toggle-tests:hover { text-decoration:underline; } .group-h { margin:12px 0 4px; font-size:13px; font-weight:700; } canvas { max-height:340px; } .catbadge { display:inline-block; font-size:12px; font-weight:600; padding:2px 8px; border-radius:6px; } @@ -143,6 +145,16 @@

Run history (click a row to expand failures) SIGNATURE_DESCRIPTIONS[s] || ""; +// Expand/collapse the truncated test list in the per-error table. +function toggleTests(a) { + const more = a.parentNode.querySelector(".more-tests"); + if (!more) return false; + const collapsed = more.hasAttribute("hidden"); + more.toggleAttribute("hidden"); + a.textContent = collapsed ? "show fewer" : `+${a.dataset.n} more`; + return false; +} + // Class.Method, dropping the namespace prefix and the parameter list. const shortTest = name => { const base = (name || "").split("(")[0]; @@ -162,8 +174,12 @@

Run history (click a row to expand failures) a.cat < b.cat ? -1 : a.cat > b.cat ? 1 : b.items.length - a.items.length) .map(({ cat, sig, items }) => { const tests = [...new Set(items.map(f => shortTest(f.name)))].sort(); - const shown = tests.slice(0, 8).map(esc).join("
") + - (tests.length > 8 ? `
+${tests.length - 8} more` : ""); + const head = tests.slice(0, 8).map(esc).join("
"); + const rest = tests.slice(8); + const shown = head + (rest.length + ? `` + + `
+${rest.length} more` + : ""); return ` ${catBadge(cat)} ${esc(sig)} From 8622bf26ab815c87047f716575a8c2bc9bb44525 Mon Sep 17 00:00:00 2001 From: eric-wang-1990 Date: Sat, 6 Jun 2026 07:49:12 +0000 Subject: [PATCH 15/17] ci(dashboard): bucket known Reyden backend gaps out of "Real issue" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three failure modes are confirmed Reyden backend limitations, not driver bugs, but the categorizer was filing two of them under "Real issue / to investigate" via the generic value-mismatch bucket: - rows_affected = -1 on INSERT/UPDATE (known Reyden bug) — surfaced as "Expected: 1 / Actual: -1" - no hive_metastore catalog — surfaced as Expected "hive_metastore" / Actual "main" Add two specific signatures ahead of the generic assertion bucket and map them to CAT_REYDEN_GAP. (SHOW COLUMNS and PK/FK metadata were already classified as gaps.) On the latest run this moves 71 failures out of "Real issue" (80 -> 9), leaving only genuine driver-side items (syntax error, col_0 alias, AfterInsert 2/0, cast mismatch). Co-authored-by: Isaac --- .github/e2e-dashboard/index.html | 2 ++ .github/scripts/parse-trx-to-json.py | 24 +++++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/.github/e2e-dashboard/index.html b/.github/e2e-dashboard/index.html index e8c5d9db9..a70debbe5 100644 --- a/.github/e2e-dashboard/index.html +++ b/.github/e2e-dashboard/index.html @@ -130,6 +130,8 @@

Run history (click a row to expand failures) CAT_REYDEN_GAP (expected). # - A value/cast mismatch means setup succeeded and the INSERT→SELECT→DELETE -# round-trip returned wrong data -> CAT_REAL (a genuine driver bug). +# round-trip returned wrong data -> CAT_REAL (a genuine driver bug), EXCEPT +# the two known Reyden backend mismatches (rows_affected=-1, missing +# hive_metastore) which signature_for buckets as CAT_REYDEN_GAP. CAT_REYDEN_GAP = "Reyden capability gap (expected)" CAT_ENVIRONMENT = "Environment / infra" CAT_REAL = "Real issue / to investigate" @@ -127,6 +147,8 @@ def signature_for(message): "Unsupported CREATE type: SCHEMA": CAT_REYDEN_GAP, "Unsupported feature: CREATE OR REPLACE TABLE": CAT_REYDEN_GAP, "Unsupported type: INTERVAL": CAT_REYDEN_GAP, + "Reyden rows_affected not returned (known backend bug)": CAT_REYDEN_GAP, + "Reyden does not support hive_metastore catalog": CAT_REYDEN_GAP, "Reyden unsupported feature (other)": CAT_REYDEN_GAP, # Back-compat: older runs used one combined signature. "Reyden unsupported feature (DDL / statement / type)": CAT_REYDEN_GAP, From bfc0d284a6603fe1f49d80f6f6ebf1955768ec26 Mon Sep 17 00:00:00 2001 From: eric-wang-1990 Date: Sat, 6 Jun 2026 19:22:41 +0000 Subject: [PATCH 16/17] ci(dashboard): scope "Real issue" to the 4 tracked Reyden SEA bugs The "Real issue / to investigate" bucket now contains only the confirmed Reyden SEA backend bugs filed under Epic SC-222102, each given its own signature: - SHOW SCHEMAS/TABLES IN ALL CATALOGS rejected (SC-233357) - bare SET rejected (SC-233354) - result schema omits column aliases / col_0 (SC-233356) - ANSI strict-cast on heterogeneous ARRAY/MAP (SC-233355) Generic value-mismatch and DML/DDL-rejected are remapped to the expected Reyden capability-gap bucket (e.g. the non-reproducible write-not-persisted case), since on this Reyden-only nightly everything outside the four tracked bugs is a known limitation. Latest run: Real 9 -> 8 (4 tracked defects), gaps 107 -> 108. Co-authored-by: Isaac --- .github/e2e-dashboard/index.html | 22 +++++++------- .github/scripts/parse-trx-to-json.py | 45 ++++++++++++++++++---------- 2 files changed, 41 insertions(+), 26 deletions(-) diff --git a/.github/e2e-dashboard/index.html b/.github/e2e-dashboard/index.html index a70debbe5..edc6b13a8 100644 --- a/.github/e2e-dashboard/index.html +++ b/.github/e2e-dashboard/index.html @@ -107,17 +107,16 @@

Run history (click a row to expand failures) (s||"").replace(/[&<>"]/g, c => ({"&":"&","<":"<",">":">",'"':"""}[c])); // Root-cause categories emitted by parse-trx-to-json.py. A low raw pass-rate is -// usually dominated by expected Reyden gaps; the "Real issue" bucket is the -// actual backlog. A test with a CREATE step that Reyden can't run fails AT that -// step (an "Unsupported …" gap); a value mismatch means setup succeeded and the -// round-trip returned wrong data — a genuine bug. +// dominated by expected Reyden capability gaps; the "Real issue" bucket is the +// small set of confirmed, tracked Reyden SEA bugs. Everything else on this +// Reyden-only nightly is a known limitation. const CATEGORIES = { "Reyden capability gap (expected)": { cls:"cat-gap", - blurb:"The backend doesn't support the operation or protocol — unsupported DDL/statement/type (e.g. CREATE TABLE/SCHEMA), no Thrift endpoint on a SEA/REST-only warehouse, or CloudFetch. Expected: gate/skip these; they shouldn't count against the driver." }, + blurb:"A known Reyden limitation — unsupported DDL/statement/type (CREATE TABLE/SCHEMA, SHOW COLUMNS, INTERVAL, …), no Thrift endpoint, no CloudFetch, no hive_metastore, rows_affected=-1, write-not-persisted value mismatches. Expected: gate/skip; not a driver bug." }, "Environment / infra": { cls:"cat-env", blurb:"Missing/misconfigured warehouse, read-only rejection, auth, timeout, or transport error. Not a driver bug — fix the environment or retry." }, "Real issue / to investigate": { cls:"cat-real", - blurb:"A genuine driver bug. A value/cast mismatch on an INSERT→SELECT→DELETE round-trip means setup succeeded but the data came back wrong (e.g. a SEA-path serialization difference); also covers SQL/syntax errors and anything unclassified." }, + blurb:"Confirmed Reyden SEA backend bugs, each reproduced (Reyden vs DBSQL / raw SEA API) and filed under Epic SC-222102: SHOW SCHEMAS/TABLES IN ALL CATALOGS (SC-233357), bare SET (SC-233354), col_0 aliases (SC-233356), ANSI heterogeneous cast (SC-233355). Any unclassified failure also surfaces here." }, }; const catBadge = name => `${esc(name)}`; @@ -134,16 +133,19 @@

Run history (click a row to expand failures) SIGNATURE_DESCRIPTIONS[s] || ""; diff --git a/.github/scripts/parse-trx-to-json.py b/.github/scripts/parse-trx-to-json.py index f941b55c4..09b7cc117 100644 --- a/.github/scripts/parse-trx-to-json.py +++ b/.github/scripts/parse-trx-to-json.py @@ -92,21 +92,27 @@ def signature_for(message): (r"PERMISSION_DENIED|not authorized|Forbidden|HTTP 403", "Permission denied (403)"), (r"timeout|timed out|TimeoutException", "Timeout"), (r"Couldn't connect|connection refused|HttpRequestException", "Connection / transport error"), + # --- Confirmed Reyden SEA backend bugs (filed under Epic SC-222102) - + # Reproduced against Reyden vs DBSQL / raw SEA API and filed as [M4] + # tickets. Matched ahead of the generic syntax/assertion buckets so each + # gets its own row and lands in CAT_REAL (the real, tracked-bug bucket) + # rather than the catch-alls. Everything NOT matched here is a known + # Reyden limitation (CAT_REYDEN_GAP) — see the category map below. + (r"at or near 'CATALOGS'", "Reyden SEA rejects SHOW SCHEMAS/TABLES IN ALL CATALOGS"), + (r"Expected identifier but got end of input", "Reyden SEA rejects bare SET statement"), + (r"Actual:\s*\"?col_0\"?", "Reyden SEA omits column aliases (returns col_0)"), # --- Genuine SQL errors with specific codes ------------------------ # Checked before the broad assertion bucket below, whose "expected" # token also appears in "Expected identifier ..." syntax-error text. (r"PARSE_SYNTAX_ERROR|SYNTAX_ERROR", "SQL syntax error"), - # --- Genuine driver bugs -------------------------------------------- - # A value/cast mismatch means the test got PAST any setup (a rejected - # CREATE TABLE would have failed earlier with an "Unsupported …" message - # in the Reyden-gap bucket above). So a wrong value on an - # INSERT→SELECT→DELETE round-trip is a real driver bug, e.g. a SEA-path - # result-serialization difference — not expected Reyden behaviour. - # EXCEPTION: the two best-known Reyden value mismatches (rows_affected=-1 - # and the missing hive_metastore catalog) are matched above as expected - # gaps; only the remaining mismatches reach this bucket. + # --- Confirmed Reyden bug: ANSI strict-cast (filed SC-233355) ------- + # Heterogeneous ARRAY/MAP literals fail to coerce on Reyden under ANSI. (r"CAST_INVALID_INPUT", "Type cast mismatch on round-trip"), (r"TABLE_OR_VIEW_NOT_FOUND|cannot be found|does not exist", "Object not found"), + # Generic value mismatch — after the specific tracked-bug signatures + # above are extracted, the remaining value mismatches on Reyden are + # known limitations (e.g. write-not-persisted), so this is mapped to + # CAT_REYDEN_GAP, not the real-bug bucket. (r"Assert\.|Equal\(|Xunit|expected", "Assertion failed (value mismatch)"), # --- Generic DML/DDL rejection (catch-all, lowest priority) -------- (r"INSERT|UPDATE|DELETE|MERGE|CREATE TABLE|DROP TABLE|ALTER TABLE", "DML/DDL rejected"), @@ -128,10 +134,12 @@ def signature_for(message): # Classification hinges on WHICH step failed, which the message already encodes: # - A test with a CREATE TABLE/SCHEMA step that Reyden can't run fails AT that # step with an "Unsupported …" message -> CAT_REYDEN_GAP (expected). -# - A value/cast mismatch means setup succeeded and the INSERT→SELECT→DELETE -# round-trip returned wrong data -> CAT_REAL (a genuine driver bug), EXCEPT -# the two known Reyden backend mismatches (rows_affected=-1, missing -# hive_metastore) which signature_for buckets as CAT_REYDEN_GAP. +# - CAT_REAL is now the set of CONFIRMED + tracked Reyden SEA bugs: each was +# reproduced (Reyden vs DBSQL / raw SEA API) and filed under Epic SC-222102. +# Only the four specific signatures above (IN ALL CATALOGS, bare SET, col_0 +# aliases, ANSI cast) land here. Every other failure on this Reyden-only +# nightly is a known limitation -> CAT_REYDEN_GAP, including the generic +# value-mismatch / DML-rejected buckets. CAT_REYDEN_GAP = "Reyden capability gap (expected)" CAT_ENVIRONMENT = "Environment / infra" CAT_REAL = "Real issue / to investigate" @@ -157,9 +165,14 @@ def signature_for(message): "Permission denied (403)": CAT_ENVIRONMENT, "Timeout": CAT_ENVIRONMENT, "Connection / transport error": CAT_ENVIRONMENT, - # "Assertion failed (value mismatch)", "Type cast mismatch on round-trip", - # "Object not found", "DML/DDL rejected", "SQL syntax error", "Other", - # "Unknown / no message" and any "" fall through to CAT_REAL. + # Generic value-mismatch and DML/DDL-rejected on this Reyden-only nightly are + # known limitations (the specific real bugs have their own signatures above). + "Assertion failed (value mismatch)": CAT_REYDEN_GAP, + "DML/DDL rejected": CAT_REYDEN_GAP, + # The four tracked Reyden SEA bugs ("Reyden SEA rejects …", "Reyden SEA omits + # column aliases (returns col_0)", "Type cast mismatch on round-trip") and any + # unclassified "" / "SQL syntax error" / "Object not found" / + # "Other" fall through to CAT_REAL so genuine new issues still surface. } From a1d47b6084687f063f30c28fc0d617aefae01992 Mon Sep 17 00:00:00 2001 From: eric-wang-1990 Date: Mon, 8 Jun 2026 16:26:08 +0000 Subject: [PATCH 17/17] ci(dashboard): reference SC-233423 for the rows_affected=-1 limitation The known Reyden rows_affected=-1 backend bug now has a tracking ticket; surface it in the signature description. Stays in the expected-gap bucket. Co-authored-by: Isaac --- .github/e2e-dashboard/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/e2e-dashboard/index.html b/.github/e2e-dashboard/index.html index edc6b13a8..99200c0bc 100644 --- a/.github/e2e-dashboard/index.html +++ b/.github/e2e-dashboard/index.html @@ -129,7 +129,7 @@

Run history (click a row to expand failures)