Compare commits

...

3 Commits

Author SHA1 Message Date
Nicolo-Maria
8e5279cddb refactor: remove clojure changes 2025-08-25 11:39:51 +02:00
Nicolo-Maria
a838898361 feat: add support for Python transforms including editor and modal components 2025-08-20 18:54:39 +02:00
Nicolo-Maria
40347e5408 refactor: simplify driver CI workflows and move legacy versions to scheduled runs
- Move legacy database versions to separate drivers-legacy.yml workflow
  - PostgreSQL 12, MySQL 8.0, MariaDB 10.2, Oracle 18.4
  - Runs daily at 2 AM UTC or on manual trigger
  - Reduces load on regular CI runs

- Simplify main drivers.yml workflow
  - Remove complex matrix strategies for database versions
  - Keep only latest versions in main workflow
  - Each database now has a dedicated, simple job

- Remove redundant inputs.skip pattern
  - Eliminate skip parameter from workflow_call triggers
  - Move skip logic to job-level if conditions in run-tests.yml
  - Makes workflow control more explicit and cleaner

- Organize drivers into performance tiers
  - Tier 1: Fast drivers (H2, SQLite, PostgreSQL Latest, MySQL Latest)
  - Tier 2: Cloud drivers (BigQuery, Snowflake, Redshift, ClickHouse)
  - Tier 3: Complex setup drivers (Athena, Databricks, Oracle, MongoDB variants)

Benefits:
- Cleaner, more maintainable workflow structure
- Reduced CI load by moving legacy versions to scheduled runs
- Better performance through tiered execution
- Explicit workflow control at caller level
2025-08-20 10:25:31 +02:00
30 changed files with 1549 additions and 201 deletions

View File

@@ -2,10 +2,6 @@ name: App DB Tests
on:
workflow_call:
inputs:
skip:
type: boolean
default: false
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}-app-db
@@ -13,7 +9,6 @@ concurrency:
jobs:
be-tests-mariadb:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 60
strategy:
@@ -125,7 +120,6 @@ jobs:
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-mysql:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 60
strategy:
@@ -237,7 +231,6 @@ jobs:
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-postgres:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 60
strategy:

216
.github/workflows/drivers-legacy.yml vendored Normal file
View File

@@ -0,0 +1,216 @@
name: Driver Tests - Legacy Versions
on:
schedule:
# Run daily at 2 AM UTC
- cron: '0 2 * * *'
workflow_dispatch:
workflow_call:
concurrency:
group: ${{ github.workflow }}-legacy
cancel-in-progress: true
jobs:
# Legacy versions: older database versions tested on schedule
be-tests-postgres-12:
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
CI: 'true'
DRIVERS: postgres
MB_DB_TYPE: postgres
MB_DB_PORT: 5432
MB_DB_HOST: localhost
MB_DB_DBNAME: mb_test
MB_DB_USER: mb_test
MB_POSTGRESQL_TEST_USER: mb_test
services:
postgres:
image: postgres:12-alpine
ports:
- "5432:5432"
env:
POSTGRES_USER: mb_test
POSTGRES_DB: mb_test
POSTGRES_HOST_AUTH_METHOD: trust
name: PostgreSQL 12.x
steps:
- uses: actions/checkout@v4
- name: Test PostgreSQL 12.x
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-postgres-12-ee'
test-args: >-
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/old-migrations-test]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-mysql-8:
runs-on: ubuntu-22.04
timeout-minutes: 60
services:
mysql:
image: mysql:8.0
ports:
- "3306:3306"
env:
MYSQL_ALLOW_EMPTY_PASSWORD: true
MYSQL_DATABASE: circle_test
env:
CI: 'true'
DRIVERS: mysql
MB_DB_TYPE: mysql
MB_DB_HOST: localhost
MB_DB_PORT: 3306
MB_DB_DBNAME: circle_test
MB_DB_USER: root
MB_MYSQL_TEST_USER: root
name: MySQL 8.0
steps:
- uses: actions/checkout@v4
- name: Test MySQL 8.0
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mysql-8-0-ee'
test-args: >-
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/old-migrations-test]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-mariadb-10-2:
runs-on: ubuntu-22.04
timeout-minutes: 60
services:
mariadb:
image: circleci/mariadb:10.2.23
ports:
- "3306:3306"
env:
MYSQL_ALLOW_EMPTY_PASSWORD: true
MYSQL_DATABASE: circle_test
env:
CI: 'true'
DRIVERS: mysql
MB_DB_TYPE: mysql
MB_DB_HOST: localhost
MB_DB_PORT: 3306
MB_DB_DBNAME: circle_test
MB_DB_USER: root
MB_MYSQL_TEST_USER: root
name: MariaDB 10.2
steps:
- uses: actions/checkout@v4
- name: Test MariaDB 10.2
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mariadb-10-2-ee'
test-args: >-
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/old-migrations-test]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-oracle-18-4:
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
CI: 'true'
DRIVERS: oracle
MB_ORACLE_TEST_HOST: localhost
MB_ORACLE_TEST_USER: system
MB_ORACLE_TEST_PASSWORD: password
MB_ORACLE_TEST_SERVICE_NAME: XEPDB1
services:
oracle:
image: gvenzl/oracle-xe:18.4.0-slim
env:
ORACLE_PASSWORD: password
ports:
- "1521:1521"
name: Oracle 18.4
steps:
- uses: actions/checkout@v4
- name: Test Oracle 18.4
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-oracle-18-4-ee'
test-args: >-
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/old-migrations-test]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
legacy-results:
needs:
- be-tests-postgres-12
- be-tests-mysql-8
- be-tests-mariadb-10-2
- be-tests-oracle-18-4
runs-on: ubuntu-latest
timeout-minutes: 5
name: legacy-results
if: always() && !cancelled()
steps:
- name: Check legacy driver status
uses: actions/github-script@v7
env:
needs: ${{ toJson(needs) }}
with:
script: | #js
const needs = JSON.parse(process.env.needs);
const jobs = Object.entries(needs).map(([jobName, jobValues]) => ({
name: jobName,
result: jobValues.result
}));
if (jobs.every(job => (job.result === 'skipped' || job.result === 'success'))) {
console.log("✅ Legacy drivers passed!");
process.exit(0);
} else {
console.log("❌ Legacy drivers failed:");
jobs.forEach((job) => {
if (job.result !== 'success' && job.result !== 'skipped') {
console.log(`${job.name} - ${job.result}`);
}
});
process.exit(1);
}

191
.github/workflows/drivers-tier1.yml vendored Normal file
View File

@@ -0,0 +1,191 @@
name: Driver Tests - Tier 1 (Fast)
on:
workflow_call:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}-tier1
cancel-in-progress: true
jobs:
# Tier 1: Fast drivers (5-15 minutes, no complex setup)
be-tests-h2:
runs-on: ubuntu-22.04
timeout-minutes: 20
env:
CI: 'true'
name: H2
steps:
- uses: actions/checkout@v4
- name: Test H2 driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-h2'
test-args: >-
:only-tags [:mb/driver-tests]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-sqlite:
runs-on: ubuntu-22.04
timeout-minutes: 20
env:
CI: 'true'
DRIVERS: sqlite
name: SQLite
steps:
- uses: actions/checkout@v4
- name: Test SQLite driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-sqlite-ee'
test-args: >-
:only-tags [:mb/driver-tests]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-postgres-latest:
runs-on: ubuntu-22.04
timeout-minutes: 20
env:
CI: 'true'
DRIVERS: postgres
MB_DB_TYPE: postgres
MB_DB_PORT: 5432
MB_DB_HOST: localhost
MB_DB_DBNAME: mb_test
MB_DB_USER: mb_test
MB_POSTGRESQL_TEST_USER: mb_test
MB_POSTGRES_SSL_TEST_SSL: 'true'
MB_POSTGRES_SSL_TEST_SSL_MODE: verify-full
MB_POSTGRES_SSL_TEST_SSL_ROOT_CERT_PATH: 'test-resources/certificates/us-east-2-bundle.pem'
services:
postgres:
image: postgres:latest
ports:
- "5432:5432"
env:
POSTGRES_USER: mb_test
POSTGRES_DB: mb_test
POSTGRES_HOST_AUTH_METHOD: trust
name: PostgreSQL Latest
steps:
- uses: actions/checkout@v4
- name: Test PostgreSQL Latest
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-postgres-latest-ee'
test-args: >-
:only-tags [:mb/driver-tests]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-mysql-latest:
runs-on: ubuntu-22.04
timeout-minutes: 20
services:
mysql:
image: mysql:latest
ports:
- "3306:3306"
env:
MYSQL_ALLOW_EMPTY_PASSWORD: true
MYSQL_DATABASE: circle_test
env:
CI: 'true'
DRIVERS: mysql
MB_DB_TYPE: mysql
MB_DB_HOST: localhost
MB_DB_PORT: 3306
MB_DB_DBNAME: circle_test
MB_DB_USER: root
MB_MYSQL_TEST_USER: root
MB_MYSQL_SSL_TEST_SSL: 'true'
MB_MYSQL_SSL_TEST_HOST: ${{ secrets.MYSQL_RDS_SSL_INSTANCE_HOST }}
MB_MYSQL_SSL_TEST_ADDITIONAL_OPTIONS: 'verifyServerCertificate=true'
MB_MYSQL_SSL_TEST_SSL_CERT: ${{ secrets.MB_MYSQL_SSL_TEST_SSL_CERT }}
MB_MYSQL_SSL_TEST_USER: metabase
MB_MYSQL_SSL_TEST_PASSWORD: ${{ secrets.MYSQL_RDS_SSL_INSTANCE_PASSWORD }}
name: MySQL Latest
steps:
- uses: actions/checkout@v4
- name: Test MySQL Latest
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mysql-latest-ee'
test-args: >-
:only-tags [:mb/driver-tests]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
tier1-results:
needs:
- be-tests-h2
- be-tests-sqlite
- be-tests-postgres-latest
- be-tests-mysql-latest
runs-on: ubuntu-latest
timeout-minutes: 5
name: tier1-results
if: always() && !cancelled()
steps:
- name: Check tier 1 status
uses: actions/github-script@v7
env:
needs: ${{ toJson(needs) }}
with:
script: | #js
const needs = JSON.parse(process.env.needs);
const jobs = Object.entries(needs).map(([jobName, jobValues]) => ({
name: jobName,
result: jobValues.result
}));
if (jobs.every(job => (job.result === 'skipped' || job.result === 'success'))) {
console.log("✅ Tier 1 (Fast) drivers passed!");
process.exit(0);
} else {
console.log("❌ Tier 1 (Fast) drivers failed:");
jobs.forEach((job) => {
if (job.result !== 'success') {
console.log(`${job.name} - ${job.result}`);
}
});
process.exit(1);
}

200
.github/workflows/drivers-tier2.yml vendored Normal file
View File

@@ -0,0 +1,200 @@
name: Driver Tests - Tier 2 (Medium)
on:
workflow_call:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}-tier2
cancel-in-progress: true
jobs:
# Tier 2: Medium drivers (15-35 minutes, cloud services/docker setup)
be-tests-bigquery-cloud-sdk:
runs-on: ubuntu-22.04
timeout-minutes: 35
env:
CI: 'true'
DRIVERS: bigquery-cloud-sdk
MB_BIGQUERY_TEST_PROJECT_ID: ${{ secrets.BIGQUERY_TEST_PROJECT_ID }}
MB_BIGQUERY_TEST_CLIENT_ID: ${{ secrets.MB_BIGQUERY_TEST_CLIENT_ID }}
MB_BIGQUERY_TEST_CLIENT_SECRET: ${{ secrets.MB_BIGQUERY_TEST_CLIENT_SECRET }}
MB_BIGQUERY_TEST_ACCESS_TOKEN: ${{ secrets.MB_BIGQUERY_TEST_ACCESS_TOKEN }}
MB_BIGQUERY_TEST_REFRESH_TOKEN: ${{ secrets.MB_BIGQUERY_TEST_REFRESH_TOKEN }}
MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON: ${{ secrets.MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON }}
MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON_ROUTING: ${{ secrets.MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON_ROUTING }}
name: BigQuery
steps:
- uses: actions/checkout@v4
- name: Test BigQuery Cloud SDK driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-bigquery-cloud-sdk-ee'
test-args: >-
:only-tags [:mb/driver-tests]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-snowflake:
runs-on: ubuntu-22.04
timeout-minutes: 35
env:
CI: 'true'
DRIVERS: snowflake
MB_SNOWFLAKE_TEST_USER: METABASE CI
MB_SNOWFLAKE_TEST_ACCOUNT: ${{ secrets.MB_SNOWFLAKE_TEST_ACCOUNT }}
MB_SNOWFLAKE_TEST_PASSWORD: ${{ secrets.MB_SNOWFLAKE_TEST_PASSWORD }}
MB_SNOWFLAKE_TEST_WAREHOUSE: ${{ secrets.MB_SNOWFLAKE_TEST_WAREHOUSE }}
MB_SNOWFLAKE_TEST_PK_USER: METABASE PK
MB_SNOWFLAKE_TEST_PK_PRIVATE_KEY: ${{ secrets.MB_SNOWFLAKE_TEST_PK_PRIVATE_KEY }}
MB_SNOWFLAKE_TEST_PK_PRIVATE_KEY_2: ${{ secrets.MB_SNOWFLAKE_TEST_PK_PRIVATE_KEY_2 }}
MB_SNOWFLAKE_TEST_PK_PUBLIC_KEY: ${{ secrets.MB_SNOWFLAKE_TEST_PK_PUBLIC_KEY }}
MB_SNOWFLAKE_TEST_PK_PUBLIC_KEY_2: ${{ secrets.MB_SNOWFLAKE_TEST_PK_PUBLIC_KEY_2 }}
MB_SNOWFLAKE_TEST_RSA_ROLE_TEST_DEFAULT_USER: RSA_ROLE_TEST_DEFAULT_USER
MB_SNOWFLAKE_TEST_RSA_ROLE_TEST_CUSTOM_USER: RSA_ROLE_TEST_CUSTOM_USER
MB_SNOWFLAKE_TEST_RSA_ROLE_TEST_ROLE: RSA_ROLE_TEST_ROLE
MB_SNOWFLAKE_TEST_RSA_ROLE_TEST_DB: RSA_ROLE_TEST_DB
name: Snowflake
steps:
- uses: actions/checkout@v4
- name: Test Snowflake driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-snowflake-ee'
test-args: >-
:only-tags [:mb/driver-tests]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-redshift-standard:
runs-on: ubuntu-22.04
timeout-minutes: 35
env:
CI: 'true'
DRIVERS: redshift
MB_REDSHIFT_TEST_USER: metabase_ci
MB_REDSHIFT_TEST_DB: testdb
MB_REDSHIFT_TEST_DB_ROUTING: dev
MB_REDSHIFT_TEST_HOST: ${{ secrets.MB_REDSHIFT_TEST_HOST }}
MB_REDSHIFT_TEST_PASSWORD: ${{ secrets.MB_REDSHIFT_TEST_PASSWORD }}
name: Redshift (Standard)
steps:
- uses: actions/checkout@v4
- name: Test Redshift driver (excluding upload tests)
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-redshift-ee'
test-args: >-
:only-tags [:mb/driver-tests]
:exclude-tags [:mb/upload-tests]
:fail-fast? true
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-clickhouse:
runs-on: ubuntu-22.04
timeout-minutes: 35
env:
CI: 'true'
DRIVERS: clickhouse
MB_CLICKHOUSE_TEST_HOST: localhost
MB_CLICKHOUSE_TEST_PORT: 8123
MB_CLICKHOUSE_TEST_OLD_PORT: 8124
MB_CLICKHOUSE_TEST_NGINX_PORT: 8127
name: ClickHouse
steps:
- uses: actions/checkout@v4
- name: Add ClickHouse TLS instance to /etc/hosts
run: |
sudo echo "127.0.0.1 server.clickhouseconnect.test" | sudo tee -a /etc/hosts
- name: Start ClickHouse in Docker
uses: hoverkraft-tech/compose-action@v2.0.0
with:
compose-file: "modules/drivers/clickhouse/docker-compose.yml"
down-flags: "--volumes"
services: |
clickhouse
clickhouse_tls
clickhouse_older_version
clickhouse_cluster_node1
clickhouse_cluster_node2
nginx
- name: Test ClickHouse driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-clickhouse-ee'
test-args: >-
:only-tags [:mb/driver-tests]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
tier2-results:
needs:
- be-tests-bigquery-cloud-sdk
- be-tests-snowflake
- be-tests-redshift-standard
- be-tests-clickhouse
runs-on: ubuntu-latest
timeout-minutes: 5
name: tier2-results
if: always() && !cancelled()
steps:
- name: Check tier 2 status
uses: actions/github-script@v7
env:
needs: ${{ toJson(needs) }}
with:
script: | #js
const needs = JSON.parse(process.env.needs);
const jobs = Object.entries(needs).map(([jobName, jobValues]) => ({
name: jobName,
result: jobValues.result
}));
if (jobs.every(job => (job.result === 'skipped' || job.result === 'success'))) {
console.log("✅ Tier 2 (Medium) drivers passed!");
process.exit(0);
} else {
console.log("❌ Tier 2 (Medium) drivers failed:");
jobs.forEach((job) => {
if (job.result !== 'success') {
console.log(`${job.name} - ${job.result}`);
}
});
process.exit(1);
}

256
.github/workflows/drivers-tier3.yml vendored Normal file
View File

@@ -0,0 +1,256 @@
name: Driver Tests - Tier 3 (Slow/Complex)
on:
workflow_call:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}-tier3
cancel-in-progress: true
jobs:
# Determine if tier 3 drivers should run (same logic as current determine-driver-skips)
determine-tier3-skips:
runs-on: ubuntu-latest
outputs:
skip: ${{ steps.check_team_and_branch.outputs.skip }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Fetch base branch
if: github.event_name == 'pull_request' && github.event.pull_request.base.ref != ''
run: |
git fetch origin ${{ github.event.pull_request.base.ref }}:${{ github.event.pull_request.base.ref }}
- name: Check if tier3 drivers should run
id: check_team_and_branch
run: |
IS_MASTER_OR_RELEASE="${{ github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/release-') }}"
HAS_TEAM_LABEL="${{ contains(github.event.pull_request.labels.*.name, '.Team/Drivers') || contains(github.event.pull_request.labels.*.name, '.Team/Querying') || contains(github.event.pull_request.labels.*.name, '.Team/SemanticLayer') || contains(github.event.pull_request.labels.*.name, '.Team/DevEx') }}"
echo "Is master or release: ${IS_MASTER_OR_RELEASE}"
echo "Has relevant team label: ${HAS_TEAM_LABEL}"
if [[ "${IS_MASTER_OR_RELEASE}" == "true" ]]; then
echo "skip=false" >> $GITHUB_OUTPUT
echo "Merging into master or a release branch. Tier 3 driver tests will run."
elif [[ "${HAS_TEAM_LABEL}" == "true" ]]; then
echo "skip=false" >> $GITHUB_OUTPUT
echo "A relevant team label is present. Tier 3 driver tests will run."
else
echo "Checking for changes in relevant folders..."
CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }})
CHANGED_FOLDERS=()
for folder in "src/metabase/lib" "src/metabase/query_processor" "modules/drivers" "src/metabase/driver"; do
if echo "$CHANGED_FILES" | grep -q "$folder"; then
CHANGED_FOLDERS+=("$folder")
fi
done
if [[ ${#CHANGED_FOLDERS[@]} -gt 0 ]]; then
echo "skip=false" >> $GITHUB_OUTPUT
echo "Changes detected in: ${CHANGED_FOLDERS[*]}. Tier 3 driver tests will run."
else
echo "skip=true" >> $GITHUB_OUTPUT
echo "No relevant changes found. Tier 3 driver tests will be skipped."
fi
fi
shell: bash
# Tier 3: Complex drivers (30-60 minutes, complex setup, external dependencies)
be-tests-athena:
needs: determine-tier3-skips
if: ${{ !(needs.determine-tier3-skips.outputs.skip == 'true') }}
runs-on: ubuntu-22.04
timeout-minutes: 65
env:
CI: 'true'
DRIVERS: athena
MB_ATHENA_TEST_REGION: us-east-1
MB_ATHENA_TEST_REGION_ROUTING: us-east-2
MB_ATHENA_TEST_ACCESS_KEY: ${{ secrets.MB_ATHENA_TEST_ACCESS_KEY }}
MB_ATHENA_TEST_SECRET_KEY: ${{ secrets.MB_ATHENA_TEST_SECRET_KEY }}
MB_ATHENA_TEST_S3_STAGING_DIR: ${{ secrets.MB_ATHENA_TEST_S3_STAGING_DIR }}
MB_ATHENA_TEST_S3_STAGING_DIR_ROUTING: ${{ secrets.MB_ATHENA_TEST_S3_STAGING_DIR_ROUTING }}
MB_ATHENA_TEST_WITHOUT_GET_TABLE_METADATA_ACCESS_KEY: ${{ secrets.MB_ATHENA_TEST_WITHOUT_GET_TABLE_METADATA_ACCESS_KEY }}
MB_ATHENA_TEST_WITHOUT_GET_TABLE_METADATA_SECRET_KEY: ${{ secrets.MB_ATHENA_TEST_WITHOUT_GET_TABLE_METADATA_SECRET_KEY }}
name: Athena
steps:
- uses: actions/checkout@v4
- name: Test Athena driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-athena-ee'
test-args: >-
:only-tags [:mb/driver-tests]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-databricks:
needs: determine-tier3-skips
if: ${{ !(needs.determine-tier3-skips.outputs.skip == 'true') }}
runs-on: ubuntu-22.04
timeout-minutes: 65
env:
CI: 'true'
DRIVERS: databricks
MB_DATABRICKS_TEST_HOST: ${{ secrets.MB_DATABRICKS_JDBC_TEST_HOST }}
MB_DATABRICKS_TEST_HTTP_PATH: ${{ secrets.MB_DATABRICKS_JDBC_TEST_HTTP_PATH }}
MB_DATABRICKS_TEST_TOKEN: ${{ secrets.MB_DATABRICKS_JDBC_TEST_TOKEN }}
MB_DATABRICKS_TEST_CATALOG: 'metabase_ci'
MB_DATABRICKS_TEST_CATALOG_ROUTING: 'metabase_routing_catalog'
MB_DATABRICKS_TEST_CLIENT_ID: ${{ secrets.MB_DATABRICKS_JDBC_TEST_CLIENT_ID }}
MB_DATABRICKS_TEST_OAUTH_SECRET: ${{ secrets.MB_DATABRICKS_JDBC_TEST_OAUTH_SECRET }}
name: Databricks
steps:
- uses: actions/checkout@v4
- name: Test Databricks driver
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-databricks-ee'
test-args: >-
:only-tags [:mb/driver-tests]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-oracle-latest:
needs: determine-tier3-skips
if: ${{ !(needs.determine-tier3-skips.outputs.skip == 'true') }}
runs-on: ubuntu-22.04
timeout-minutes: 65
env:
CI: 'true'
DRIVERS: oracle
MB_ORACLE_TEST_HOST: localhost
MB_ORACLE_TEST_USER: ''
MB_ORACLE_TEST_SERVICE_NAME: XEPDB1
MB_ORACLE_TEST_PASSWORD: ''
MB_ORACLE_TEST_SSL: true
MB_ORACLE_SSL_TEST_SSL: true
MB_ORACLE_TEST_SSL_USE_TRUSTSTORE: true
MB_ORACLE_TEST_SSL_TRUSTSTORE_PATH: './test_resources/ssl/oracle/truststore.p12'
MB_ORACLE_TEST_SSL_TRUSTSTORE_OPTIONS: local
MB_ORACLE_TEST_SSL_TRUSTSTORE_PASSWORD_VALUE: 'PassworD_#1234'
MB_ORACLE_TEST_SSL_USE_KEYSTORE: true
MB_ORACLE_TEST_SSL_KEYSTORE_PATH: './test_resources/ssl/oracle/keystore.p12'
MB_ORACLE_TEST_SSL_KEYSTORE_OPTIONS: local
MB_ORACLE_TEST_SSL_KEYSTORE_PASSWORD_VALUE: 'PassworD_#1234'
services:
oracle:
image: metabase/qa-databases:oracle-xe-21.3
env:
ORACLE_PASSWORD: password
ports:
- "1521:2484"
name: Oracle 21.3
steps:
- uses: actions/checkout@v4
- name: Test Oracle 21.3
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-oracle-21-3-ee'
test-args: >-
:only-tags [:mb/driver-tests]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-mongo-latest:
needs: determine-tier3-skips
if: ${{ !(needs.determine-tier3-skips.outputs.skip == 'true') }}
runs-on: ubuntu-22.04
timeout-minutes: 65
env:
CI: 'true'
DRIVERS: mongo
MB_MONGO_TEST_USER: metabase
MB_MONGO_TEST_PASSWORD: metasample123
services:
mongodb:
image: mongo:latest
ports:
- "27017:27017"
env:
MONGO_INITDB_ROOT_USERNAME: metabase
MONGO_INITDB_ROOT_PASSWORD: metasample123
name: MongoDB Latest
steps:
- uses: actions/checkout@v4
- name: Test MongoDB Latest
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mongo-latest-ee'
test-args: >-
:exclude-tags [:mongo-sharded-cluster-tests]
:only-tags [:mb/driver-tests]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
tier3-results:
needs:
- be-tests-athena
- be-tests-databricks
- be-tests-oracle-latest
- be-tests-mongo-latest
runs-on: ubuntu-latest
timeout-minutes: 5
name: tier3-results
if: always() && !cancelled()
steps:
- name: Check tier 3 status
uses: actions/github-script@v7
env:
needs: ${{ toJson(needs) }}
with:
script: | #js
const needs = JSON.parse(process.env.needs);
const jobs = Object.entries(needs).map(([jobName, jobValues]) => ({
name: jobName,
result: jobValues.result
}));
if (jobs.every(job => (job.result === 'skipped' || job.result === 'success'))) {
console.log("✅ Tier 3 (Slow/Complex) drivers passed!");
process.exit(0);
} else {
console.log("❌ Tier 3 (Slow/Complex) drivers failed:");
jobs.forEach((job) => {
if (job.result !== 'success') {
console.log(`${job.name} - ${job.result}`);
}
});
process.exit(1);
}

View File

@@ -2,10 +2,6 @@ name: Driver Tests
on:
workflow_call:
inputs:
skip:
type: boolean
default: false
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}-drivers
@@ -66,7 +62,7 @@ jobs:
shell: bash
be-tests-athena-ee:
needs: determine-driver-skips
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
if: ${{ !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
@@ -103,7 +99,6 @@ jobs:
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-bigquery-cloud-sdk-ee:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
@@ -138,7 +133,6 @@ jobs:
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-clickhouse-ee:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
@@ -186,7 +180,7 @@ jobs:
be-tests-druid-ee:
needs: determine-driver-skips
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
if: ${{ !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
@@ -222,7 +216,7 @@ jobs:
be-tests-druid-jdbc-ee:
needs: determine-driver-skips
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
if: ${{ !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
@@ -258,7 +252,7 @@ jobs:
be-tests-databricks-ee:
needs: determine-driver-skips
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
if: ${{ !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
@@ -294,7 +288,7 @@ jobs:
be-tests-databricks-multi-catalog-ee:
needs: determine-driver-skips
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
if: ${{ !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
@@ -330,7 +324,6 @@ jobs:
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-h2:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
@@ -356,64 +349,18 @@ jobs:
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-mysql-mariadb:
if: ${{ !inputs.skip }}
be-tests-mysql-latest:
runs-on: ubuntu-22.04
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
version:
- name: MariaDB 10.2
junit-name: be-tests-mariadb-10-2-ee
image: circleci/mariadb:10.2.23
env:
enable-ssl-tests: 'false'
- name: MariaDB Latest
junit-name: be-tests-mariadb-latest-ee
image: circleci/mariadb:latest
env:
enable-ssl-tests: 'false'
- name: MySQL 8.0
junit-name: be-tests-mysql-8-0-ee
image: mysql:8.0
env:
enable-ssl-tests: 'false'
- name: MySQL Latest
junit-name: be-tests-mysql-latest-ee
image: mysql:latest
env:
enable-ssl-tests: 'true'
job:
- name: Driver Tests
test-args: >-
:only-tags [:mb/driver-tests]
services:
mysql:
image: ${{ matrix.version.image }}
image: mysql:latest
ports:
- "3306:3306"
env:
MYSQL_ALLOW_EMPTY_PASSWORD: true
MYSQL_DATABASE: circle_test
env:
# Only run old migrations tests on pushes to master or release branches. All other branches should skip tests
# with the tag `mb/old-migrations-test`. `__ADDITIONAL_EXCLUDED_TAG__` is not used anywhere outside of splicing
# it in to the command below.
__ADDITIONAL_EXCLUDED_TAG__: >-
${{
(
github.event_name == 'push' &&
(
github.ref == 'master' ||
startsWith(github.ref, 'release-')
) &&
''
) ||
':mb/old-migrations-test'
}}
# actual serious env vars below
CI: 'true'
DRIVERS: mysql
MB_DB_TYPE: mysql
@@ -422,33 +369,63 @@ jobs:
MB_DB_DBNAME: circle_test
MB_DB_USER: root
MB_MYSQL_TEST_USER: root
#
# SSL config: only for mysql latest tests.
#
# set up env vars for something named "MYSQL_SSL" to run MySQL SSL tests verifying connectivity with PEM cert
# they are deliberately given a different name to prevent them from affecting the regular test run against
# the configured MySQL instance, but there is one particular test (mysql-connect-with-ssl-and-pem-cert-test)
# that overrides the MB_MYSQL_TEST_* values with them
# the MYSQL_RDS_SSL_INSTANCE vars are defined as secrets and can be altered
MB_MYSQL_SSL_TEST_SSL: ${{ matrix.version.env.enable-ssl-tests }}
# SSL configuration for MySQL latest
MB_MYSQL_SSL_TEST_SSL: 'true'
MB_MYSQL_SSL_TEST_HOST: ${{ secrets.MYSQL_RDS_SSL_INSTANCE_HOST }}
MB_MYSQL_SSL_TEST_ADDITIONAL_OPTIONS: 'verifyServerCertificate=true'
# the contents of the ./resources/certificates/rds-combined-ca-bundle.pem file
MB_MYSQL_SSL_TEST_SSL_CERT: ${{ secrets.MB_MYSQL_SSL_TEST_SSL_CERT }}
MB_MYSQL_SSL_TEST_USER: metabase
MB_MYSQL_SSL_TEST_PASSWORD: ${{ secrets.MYSQL_RDS_SSL_INSTANCE_PASSWORD }}
# for historic reasons (I don't want to mess around with required jobs) the job name should be something like
# "be-tests-mariadb-10-2-ee (0)"
name: "${{ matrix.version.name }} ${{ matrix.job.name }}"
name: MySQL Latest
steps:
- uses: actions/checkout@v4
- name: Test ${{ matrix.version.name }}
- name: Test MySQL Latest
uses: ./.github/actions/test-driver
with:
junit-name: ${{ matrix.version.junit-name }}
junit-name: 'be-tests-mysql-latest-ee'
test-args: >-
${{ matrix.job.test-args }}
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
:only-tags [:mb/driver-tests]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
with:
input-path: ./target/junit/
output-name: ${{ github.job }}
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-mariadb-latest:
runs-on: ubuntu-22.04
timeout-minutes: 60
services:
mariadb:
image: circleci/mariadb:latest
ports:
- "3306:3306"
env:
MYSQL_ALLOW_EMPTY_PASSWORD: true
MYSQL_DATABASE: circle_test
env:
CI: 'true'
DRIVERS: mysql
MB_DB_TYPE: mysql
MB_DB_HOST: localhost
MB_DB_PORT: 3306
MB_DB_DBNAME: circle_test
MB_DB_USER: root
MB_MYSQL_TEST_USER: root
name: MariaDB Latest
steps:
- uses: actions/checkout@v4
- name: Test MariaDB Latest
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mariadb-latest-ee'
test-args: >-
:only-tags [:mb/driver-tests]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -463,7 +440,7 @@ jobs:
be-tests-mongo:
needs: determine-driver-skips
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
if: ${{ !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-22.04
timeout-minutes: 60
strategy:
@@ -513,7 +490,7 @@ jobs:
be-tests-mongo-ssl:
needs: determine-driver-skips
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
if: ${{ !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-22.04
timeout-minutes: 60
strategy:
@@ -578,7 +555,7 @@ jobs:
be-tests-mongo-sharded-cluster-ee:
needs: determine-driver-skips
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
if: ${{ !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
@@ -610,64 +587,41 @@ jobs:
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-oracle:
be-tests-oracle-21:
needs: determine-driver-skips
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
if: ${{ !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-22.04
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
version:
- name: Oracle 18.4
junit-name: be-tests-oracle-18-4-ee
image: gvenzl/oracle-xe:18.4.0-slim
port: 1521
env:
user: system
password: 'password'
enable-ssl-tests: false
- name: Oracle 21.3
junit-name: be-tests-oracle-21-3-ee
image: metabase/qa-databases:oracle-xe-21.3
port: 2484
env:
user: ''
password: ''
enable-ssl-tests: true
env:
CI: 'true'
DRIVERS: oracle
MB_ORACLE_TEST_HOST: localhost
MB_ORACLE_TEST_USER: '${{ matrix.version.env.user }}'
MB_ORACLE_TEST_PORT: 2484
MB_ORACLE_TEST_SERVICE_NAME: XEPDB1
# Only the non-SSL 18.4 tests specify password as an env var; the SSL 21.3 tests get it from the keystore I guess
MB_ORACLE_TEST_PASSWORD: '${{ matrix.version.env.password }}'
# These are ignored for the 18.4 tests which do not test SSL
MB_ORACLE_TEST_SSL: ${{ matrix.version.env.enable-ssl-tests }}
MB_ORACLE_SSL_TEST_SSL: ${{ matrix.version.env.enable-ssl-tests }}
MB_ORACLE_TEST_SSL_USE_TRUSTSTORE: ${{ matrix.version.env.enable-ssl-tests }}
MB_ORACLE_TEST_SSL: 'true'
MB_ORACLE_SSL_TEST_SSL: 'true'
MB_ORACLE_TEST_SSL_USE_TRUSTSTORE: 'true'
MB_ORACLE_TEST_SSL_TRUSTSTORE_PATH: './test_resources/ssl/oracle/truststore.p12'
MB_ORACLE_TEST_SSL_TRUSTSTORE_OPTIONS: local
MB_ORACLE_TEST_SSL_TRUSTSTORE_PASSWORD_VALUE: 'PassworD_#1234'
MB_ORACLE_TEST_SSL_USE_KEYSTORE: ${{ matrix.version.env.enable-ssl-tests }}
MB_ORACLE_TEST_SSL_USE_KEYSTORE: 'true'
MB_ORACLE_TEST_SSL_KEYSTORE_PATH: './test_resources/ssl/oracle/keystore.p12'
MB_ORACLE_TEST_SSL_KEYSTORE_OPTIONS: local
MB_ORACLE_TEST_SSL_KEYSTORE_PASSWORD_VALUE: 'PassworD_#1234'
services:
oracle:
image: ${{ matrix.version.image }}
image: metabase/qa-databases:oracle-xe-21.3
env:
ORACLE_PASSWORD: password
ports:
- "1521:${{ matrix.version.port }}"
name: ${{ matrix.version.name }}
- "2484:2484"
name: Oracle 21.3
steps:
- uses: actions/checkout@v4
- name: Test ${{ matrix.version.name }}
- name: Test Oracle 21.3
uses: ./.github/actions/test-driver
with:
junit-name: ${{ matrix.version.junit-name }}
junit-name: 'be-tests-oracle-21-3-ee'
test-args: >-
:only-tags [:mb/driver-tests]
- name: Upload Test Results
@@ -682,46 +636,10 @@ jobs:
aws-region: ${{ vars.AWS_REGION }}
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-postgres:
if: ${{ !inputs.skip }}
be-tests-postgres-latest:
runs-on: ubuntu-22.04
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
version:
- name: Postgres 12.x
junit-name: postgres-ee
docker-image: postgres:12-alpine
env:
enable-ssl-tests: 'false'
- name: Postgres Latest
junit-name: postgres-latest-ee
docker-image: postgres:latest
env:
enable-ssl-tests: 'true'
job:
- name: Driver Tests
test-args: >-
:only-tags [:mb/driver-tests]
name: "${{ matrix.version.name }} ${{ matrix.job.name }}"
env:
# Only run old migrations tests on pushes to master or release branches. All other branches should skip tests
# with the tag `mb/old-migrations-test`. `__ADDITIONAL_EXCLUDED_TAG__` is not used anywhere outside of splicing
# it in to the command below.
__ADDITIONAL_EXCLUDED_TAG__: >-
${{
(
github.event_name == 'push' &&
(
github.ref == 'master' ||
startsWith(github.ref, 'release-')
) &&
''
) ||
':mb/old-migrations-test'
}}
# actual serious env vars below
CI: 'true'
DRIVERS: postgres
MB_DB_TYPE: postgres
@@ -730,28 +648,28 @@ jobs:
MB_DB_DBNAME: mb_test
MB_DB_USER: mb_test
MB_POSTGRESQL_TEST_USER: mb_test
# SSL tests are only enabled for the postgres-latest job.
MB_POSTGRES_SSL_TEST_SSL: ${{ matrix.version.env.enable-ssl-tests }}
# SSL tests enabled for postgres latest
MB_POSTGRES_SSL_TEST_SSL: 'true'
MB_POSTGRES_SSL_TEST_SSL_MODE: verify-full
MB_POSTGRES_SSL_TEST_SSL_ROOT_CERT_PATH: 'test-resources/certificates/us-east-2-bundle.pem'
services:
postgres:
image: ${{ matrix.version.docker-image }}
image: postgres:latest
ports:
- "5432:5432"
env:
POSTGRES_USER: mb_test
POSTGRES_DB: mb_test
POSTGRES_HOST_AUTH_METHOD: trust
name: PostgreSQL Latest
steps:
- uses: actions/checkout@v4
- name: Test ${{ matrix.version.name }} (${{ matrix.job.name }})
- name: Test PostgreSQL Latest
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-${{ matrix.version.junit-name }}'
junit-name: 'be-tests-postgres-latest-ee'
test-args: >-
${{ matrix.job.test-args }}
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
:only-tags [:mb/driver-tests]
- name: Upload Test Results
uses: ./.github/actions/upload-test-results
if: always()
@@ -766,7 +684,7 @@ jobs:
be-tests-presto-jdbc-ee:
needs: determine-driver-skips
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
if: ${{ !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
@@ -834,7 +752,6 @@ jobs:
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-redshift-ee:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 60
strategy:
@@ -884,7 +801,6 @@ jobs:
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
be-tests-snowflake-ee:
if: ${{ !inputs.skip }}
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
@@ -928,7 +844,7 @@ jobs:
be-tests-sparksql-ee:
needs: determine-driver-skips
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
if: ${{ !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
@@ -962,7 +878,7 @@ jobs:
be-tests-sqlite-ee:
needs: determine-driver-skips
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
if: ${{ !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
@@ -991,7 +907,7 @@ jobs:
be-tests-sqlserver:
needs: determine-driver-skips
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
if: ${{ !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-22.04
timeout-minutes: 30
strategy:
@@ -1042,7 +958,7 @@ jobs:
be-tests-starburst-ee:
needs: determine-driver-skips
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
if: ${{ !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
@@ -1080,7 +996,7 @@ jobs:
be-tests-vertica-ee:
needs: determine-driver-skips
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
if: ${{ !(needs.determine-driver-skips.outputs.skip == 'true') }}
runs-on: ubuntu-22.04
timeout-minutes: 60
env:
@@ -1122,13 +1038,15 @@ jobs:
- be-tests-bigquery-cloud-sdk-ee
- be-tests-druid-ee
- be-tests-databricks-ee
- be-tests-databricks-multi-catalog-ee
- be-tests-druid-jdbc-ee
- be-tests-mysql-mariadb
- be-tests-mysql-latest
- be-tests-mariadb-latest
- be-tests-mongo
- be-tests-mongo-ssl
- be-tests-mongo-sharded-cluster-ee
- be-tests-oracle
- be-tests-postgres
- be-tests-oracle-21
- be-tests-postgres-latest
- be-tests-presto-jdbc-ee
- be-tests-redshift-ee
- be-tests-snowflake-ee

View File

@@ -90,17 +90,27 @@ jobs:
app-db-tests:
needs: files-changed
if: ${{ needs.files-changed.outputs.backend_all == 'true' }}
uses: ./.github/workflows/app-db.yml
secrets: inherit
with:
skip: ${{ needs.files-changed.outputs.backend_all != 'true' }}
driver-tests:
driver-tests-tier1:
needs: files-changed
uses: ./.github/workflows/drivers.yml
if: ${{ needs.files-changed.outputs.backend_all == 'true' }}
uses: ./.github/workflows/drivers-tier1.yml
secrets: inherit
driver-tests-tier2:
needs: files-changed
if: ${{ needs.files-changed.outputs.backend_all == 'true' }}
uses: ./.github/workflows/drivers-tier2.yml
secrets: inherit
driver-tests-tier3:
needs: files-changed
if: ${{ needs.files-changed.outputs.backend_all == 'true' }}
uses: ./.github/workflows/drivers-tier3.yml
secrets: inherit
with:
skip: ${{ needs.files-changed.outputs.backend_all != 'true' }}
frontend-tests:
needs: files-changed

View File

@@ -34,7 +34,7 @@ export function trackTransformCreate({
creationType,
}: {
triggeredFrom: "transform-page-create-menu";
creationType: "query" | "native" | "saved-question";
creationType: "query" | "native" | "python" | "saved-question";
}) {
trackSimpleEvent({
event: "transform_create",

View File

@@ -0,0 +1,69 @@
import { useState } from "react";
import { t } from "ttag";
import { useListDatabasesQuery } from "metabase/api";
import { LoadingAndErrorWrapper } from "metabase/common/components/LoadingAndErrorWrapper";
import { Box, Select, Stack, Text } from "metabase/ui";
import type { Database } from "metabase-types/api";
type PythonDataPickerProps = {
database?: number;
onChange: (database: number, table?: number) => void;
};
export function PythonDataPicker({
database,
onChange,
}: PythonDataPickerProps) {
const [selectedDatabaseId, setSelectedDatabaseId] = useState<
number | undefined
>(database);
const {
data: databasesResponse,
isLoading: isLoadingDatabases,
error: databaseError,
} = useListDatabasesQuery();
if (databaseError) {
return <LoadingAndErrorWrapper error={databaseError} />;
}
if (isLoadingDatabases) {
return <LoadingAndErrorWrapper loading />;
}
const databases = databasesResponse?.data || [];
const databaseOptions = databases.map((db: Database) => ({
value: db.id.toString(),
label: db.name,
}));
const handleDatabaseChange = (value: string | null) => {
const dbId = value ? parseInt(value) : undefined;
setSelectedDatabaseId(dbId);
if (dbId) {
onChange(dbId, undefined); // Clear table selection when database changes
}
};
return (
<Stack gap="md">
<Box>
<Text size="sm" fw="bold" mb="xs">
{t`Source Database`}
</Text>
<Text size="xs" c="dimmed" mb="sm">
{t`Select the database that contains your source data`}
</Text>
<Select
data={databaseOptions}
value={selectedDatabaseId?.toString() || null}
onChange={handleDatabaseChange}
placeholder={t`Select a database`}
clearable
/>
</Box>
</Stack>
);
}

View File

@@ -0,0 +1 @@
export * from "./PythonDataPicker";

View File

@@ -0,0 +1,112 @@
import { useHotkeys } from "@mantine/hooks";
import { useState } from "react";
import { Flex } from "metabase/ui";
import type { TransformSource } from "metabase-types/api";
import { PythonDataPicker } from "../PythonDataPicker";
import { EditorHeader } from "../QueryEditor/EditorHeader";
import { PythonEditor } from "../QueryEditor/PythonEditor";
type PythonTransformEditorProps = {
initialSource: TransformSource & { type: "python" };
isNew?: boolean;
isSaving?: boolean;
onSave: (newSource: TransformSource & { type: "python" }) => void;
onCancel: () => void;
};
export function PythonTransformEditor({
initialSource,
isNew = true,
isSaving = false,
onSave,
onCancel,
}: PythonTransformEditorProps) {
const [source, setSource] = useState(initialSource);
const [isSourceDirty, setIsSourceDirty] = useState(false);
const [isRunning, setIsRunning] = useState(false);
const handleScriptChange = (script: string) => {
const newSource = {
...source,
script,
};
setSource(newSource);
setIsSourceDirty(true);
};
const handleDataChange = (database: number, table?: number) => {
const newSource = {
...source,
database,
table,
};
setSource(newSource);
setIsSourceDirty(true);
};
const handleSave = () => {
onSave(source);
};
const handleRunScript = async () => {
setIsRunning(true);
// TODO: Implement Python script execution
// For now, just simulate running
await new Promise((resolve) => setTimeout(resolve, 1000));
setIsRunning(false);
};
const handleCancelScript = () => {
setIsRunning(false);
};
const handleCmdEnter = () => {
if (isRunning) {
handleCancelScript();
} else {
handleRunScript();
}
};
useHotkeys([["mod+Enter", handleCmdEnter]], []);
const canSave = Boolean(source.script.trim() && source.database);
return (
<Flex
w="100%"
h="100%"
direction="column"
bg="bg-white"
data-testid="python-transform-editor"
>
<EditorHeader
isNew={isNew}
isSaving={isSaving}
canSave={canSave && (isNew || isSourceDirty)}
onSave={handleSave}
onCancel={onCancel}
/>
<Flex direction="column" style={{ flex: 1 }} p="md">
<PythonDataPicker
database={source.database}
onChange={handleDataChange}
/>
<Flex direction="column" style={{ flex: 1, marginTop: "1rem" }}>
<PythonEditor
script={source.script}
isRunnable={true}
isRunning={isRunning}
isResultDirty={false}
onChange={handleScriptChange}
onRunScript={handleRunScript}
onCancelScript={handleCancelScript}
/>
</Flex>
</Flex>
</Flex>
);
}

View File

@@ -0,0 +1 @@
export { PythonTransformEditor } from "./PythonTransformEditor";

View File

@@ -7,6 +7,9 @@ import { Notebook } from "metabase/querying/notebook/components/Notebook";
import { Box } from "metabase/ui";
import type Question from "metabase-lib/v1/Question";
import type NativeQuery from "metabase-lib/v1/queries/NativeQuery";
import type { TransformSource } from "metabase-types/api";
import { PythonEditor } from "../PythonEditor";
import S from "./EditorBody.module.css";
import { ResizableBoxHandle } from "./ResizableBoxHandle";
@@ -16,10 +19,13 @@ const EDITOR_HEIGHT = 400;
type EditorBodyProps = {
question: Question;
isNative: boolean;
isPython?: boolean;
pythonSource?: TransformSource & { type: "python" };
isRunnable: boolean;
isRunning: boolean;
isResultDirty: boolean;
onChange: (newQuestion: Question) => void;
onPythonChange?: (script: string) => void;
onRunQuery: () => Promise<void>;
onCancelQuery: () => void;
};
@@ -27,10 +33,13 @@ type EditorBodyProps = {
export function EditorBody({
question,
isNative,
isPython = false,
pythonSource,
isRunnable,
isRunning,
isResultDirty,
onChange,
onPythonChange,
onRunQuery,
onCancelQuery,
}: EditorBodyProps) {
@@ -61,6 +70,24 @@ export function EditorBody({
onChange(newNativeQuery.question());
};
const handlePythonChange = (script: string) => {
onPythonChange?.(script);
};
if (isPython) {
return (
<PythonEditor
script={pythonSource?.script || ""}
isRunnable={isRunnable}
isRunning={isRunning}
isResultDirty={isResultDirty}
onChange={handlePythonChange}
onRunScript={onRunQuery}
onCancelScript={onCancelQuery}
/>
);
}
return isNative ? (
<NativeQueryEditor
question={question}

View File

@@ -0,0 +1,61 @@
import { t } from "ttag";
import { CodeEditor } from "metabase/common/components/CodeEditor";
import { Box, Button, Group, Text } from "metabase/ui";
type PythonEditorProps = {
script: string;
isRunnable: boolean;
isRunning: boolean;
isResultDirty: boolean;
onChange: (script: string) => void;
onRunScript: () => Promise<void>;
onCancelScript: () => void;
};
export function PythonEditor({
script,
isRunnable,
isRunning,
isResultDirty,
onChange,
onRunScript,
onCancelScript,
}: PythonEditorProps) {
const handleScriptChange = (newScript: string) => {
onChange(newScript);
};
return (
<Box h="100%" style={{ display: "flex", flexDirection: "column" }}>
{/* Python Script Editor */}
<Box style={{ flex: 1 }}>
<CodeEditor
value={script}
onChange={handleScriptChange}
language="python"
/>
</Box>
{/* Action Buttons */}
<Box mt="md">
<Group gap="sm">
<Button
variant="filled"
leftSection={isRunning ? undefined : <span>▶</span>}
onClick={isRunning ? onCancelScript : onRunScript}
disabled={!isRunnable && !isRunning}
loading={isRunning}
>
{isRunning ? t`Cancel` : t`Run Python Script`}
</Button>
{isResultDirty && (
<Text size="sm" c="dimmed">
{t`Results may be out of date`}
</Text>
)}
</Group>
</Box>
</Box>
);
}

View File

@@ -0,0 +1 @@
export { PythonEditor } from "./PythonEditor";

View File

@@ -9,6 +9,7 @@ import { hasPremiumFeature } from "metabase-enterprise/settings";
import { JobListPage } from "./pages/JobListPage";
import { JobPage } from "./pages/JobPage";
import { NewJobPage } from "./pages/NewJobPage";
import { NewPythonTransformPage } from "./pages/NewPythonTransformPage";
import { NewTransformQueryPage } from "./pages/NewTransformQueryPage";
import { RunListPage } from "./pages/RunListPage";
import { TransformListPage } from "./pages/TransformListPage";
@@ -32,7 +33,16 @@ if (hasPremiumFeature("transforms")) {
<Route path="runs" component={RunListPage} />
<Route path=":transformId" component={TransformPage} />
</Route>
<Route path="new/:type" component={NewTransformQueryPage} />
<Route
path="new/:type"
component={(props: any) =>
props.params.type === "python" ? (
<NewPythonTransformPage />
) : (
<NewTransformQueryPage params={props.params} />
)
}
/>
<Route path="new/card/:cardId" component={NewTransformQueryPage} />
<Route path=":transformId/query" component={TransformQueryPage} />
</Route>

View File

@@ -0,0 +1,139 @@
import { Field, Form, Formik } from "formik";
import { t } from "ttag";
import * as Yup from "yup";
import { Button, Group, Modal, Stack, TextInput } from "metabase/ui";
import { useCreateTransformMutation } from "metabase-enterprise/api";
import type { Transform, TransformSource } from "metabase-types/api";
type CreatePythonTransformModalProps = {
source: TransformSource & { type: "python" };
onCreate: (transform: Transform) => void;
onClose: () => void;
};
type FormValues = {
name: string;
description: string | null;
targetSchema: string | null;
targetName: string;
};
const getValidationSchema = () =>
Yup.object({
name: Yup.string().required(t`Name is required`),
description: Yup.string().nullable(),
targetSchema: Yup.string().nullable(),
targetName: Yup.string().required(t`Table name is required`),
});
export function CreatePythonTransformModal({
source,
onCreate,
onClose,
}: CreatePythonTransformModalProps) {
const [createTransform] = useCreateTransformMutation();
const initialValues: FormValues = {
name: "",
description: null,
targetSchema: "python_transforms",
targetName: "",
};
const handleSubmit = async (values: FormValues) => {
try {
const transform = await createTransform({
name: values.name,
description: values.description,
source,
target: {
type: "table",
schema: values.targetSchema,
name: values.targetName,
},
}).unwrap();
onCreate(transform);
} catch (error) {
console.error("Error creating Python transform:", error);
// TODO: Show error toast
}
};
return (
<Modal
opened
onClose={onClose}
title={t`Create Python Transform`}
size="md"
>
<Formik
initialValues={initialValues}
validationSchema={getValidationSchema()}
onSubmit={handleSubmit}
>
{({ isSubmitting, isValid }) => (
<Form>
<Stack gap="md">
<Field name="name">
{({ field, meta }: any) => (
<TextInput
{...field}
label={t`Transform name`}
placeholder={t`My Python Transform`}
error={meta.touched && meta.error}
/>
)}
</Field>
<Field name="description">
{({ field }: any) => (
<TextInput
{...field}
label={t`Description (optional)`}
placeholder={t`What does this transform do?`}
/>
)}
</Field>
<Field name="targetName">
{({ field, meta }: any) => (
<TextInput
{...field}
label={t`Output table name`}
placeholder={t`my_python_transform`}
error={meta.touched && meta.error}
/>
)}
</Field>
<Field name="targetSchema">
{({ field }: any) => (
<TextInput
{...field}
label={t`Schema (optional)`}
placeholder={t`python_transforms`}
/>
)}
</Field>
<Group justify="flex-end" mt="md">
<Button variant="subtle" onClick={onClose}>
{t`Cancel`}
</Button>
<Button
type="submit"
loading={isSubmitting}
disabled={!isValid}
>
{t`Create transform`}
</Button>
</Group>
</Stack>
</Form>
)}
</Formik>
</Modal>
);
}

View File

@@ -0,0 +1 @@
export { CreatePythonTransformModal } from "./CreatePythonTransformModal";

View File

@@ -0,0 +1,75 @@
import { useDisclosure } from "@mantine/hooks";
import { useState } from "react";
import { push } from "react-router-redux";
import { useDispatch } from "metabase/lib/redux";
import type { Transform, TransformSource } from "metabase-types/api";
import { PythonTransformEditor } from "../../components/PythonTransformEditor";
import { getTransformListUrl, getTransformUrl } from "../../urls";
import { CreatePythonTransformModal } from "./CreatePythonTransformModal";
const DEFAULT_PYTHON_SOURCE: TransformSource & { type: "python" } = {
type: "python",
database: 1, // Default to first database, will be updated by user
table: undefined,
script: `# Write your Python transformation script here
import pandas as pd
def transform(data):
"""
Your transformation function.
Args:
data: Input data (to be defined based on implementation)
Returns:
Transformed data
"""
# Your transformation logic here
return data
# Example usage:
# result = transform(input_data)`,
};
export function NewPythonTransformPage() {
const [source, setSource] = useState<TransformSource & { type: "python" }>(
DEFAULT_PYTHON_SOURCE,
);
const [isModalOpened, { open: openModal, close: closeModal }] =
useDisclosure();
const dispatch = useDispatch();
const handleCreate = (transform: Transform) => {
dispatch(push(getTransformUrl(transform.id)));
};
const handleSaveClick = (newSource: TransformSource & { type: "python" }) => {
setSource(newSource);
openModal();
};
const handleCancelClick = () => {
dispatch(push(getTransformListUrl()));
};
return (
<>
<PythonTransformEditor
initialSource={DEFAULT_PYTHON_SOURCE}
isNew
onSave={handleSaveClick}
onCancel={handleCancelClick}
/>
{isModalOpened && (
<CreatePythonTransformModal
source={source}
onCreate={handleCreate}
onClose={closeModal}
/>
)}
</>
);
}

View File

@@ -0,0 +1 @@
export { NewPythonTransformPage } from "./NewPythonTransformPage";

View File

@@ -90,7 +90,7 @@ function getParsedParams({
cardId,
}: NewTransformQueryPageParams): NewTransformQueryPageParsedParams {
return {
type: type === "native" ? "native" : "query",
type: type === "native" ? "native" : type === "python" ? "query" : "query",
cardId: cardId != null ? Urls.extractEntityId(cardId) : undefined,
};
}

View File

@@ -72,6 +72,19 @@ export function CreateTransformMenu() {
>
{t`SQL query`}
</Menu.Item>
<Menu.Item
component={ForwardRefLink}
to={getNewTransformFromTypeUrl("python")}
leftSection={<Icon name="code_block" />}
onClick={() => {
trackTransformCreate({
triggeredFrom: "transform-page-create-menu",
creationType: "python",
});
}}
>
{t`Python script`}
</Menu.Item>
<Menu.Item
leftSection={<Icon name="folder" />}
onClick={handleSavedQuestionClick}

View File

@@ -19,9 +19,12 @@ type ManageSectionProps = {
};
export function ManageSection({ transform }: ManageSectionProps) {
const sectionLabel =
transform.source.type === "python" ? t`Python Script` : t`Query`;
return (
<TitleSection
label={t`Query`}
label={sectionLabel}
rightSection={
<Group>
<EditQueryButton transform={transform} />
@@ -30,7 +33,22 @@ export function ManageSection({ transform }: ManageSectionProps) {
}
>
<Card p={0} shadow="none" withBorder>
<QueryView query={transform.source.query} />
{transform.source.type === "query" && (
<QueryView query={transform.source.query} />
)}
{transform.source.type === "python" && (
<div style={{ padding: "1rem" }}>
<p>
<strong>{t`Database ID`}:</strong> {transform.source.database}
</p>
{transform.source.table && (
<p>
<strong>{t`Table ID`}:</strong> {transform.source.table}
</p>
)}
<pre>{transform.source.script}</pre>
</div>
)}
</Card>
</TitleSection>
);
@@ -41,13 +59,16 @@ type EditQueryButtonProps = {
};
function EditQueryButton({ transform }: EditQueryButtonProps) {
const buttonText =
transform.source.type === "python" ? t`Edit script` : t`Edit query`;
return (
<Button
component={Link}
to={getTransformQueryUrl(transform.id)}
leftSection={<Icon name="pencil_lines" aria-hidden />}
>
{t`Edit query`}
{buttonText}
</Button>
);
}

View File

@@ -54,7 +54,12 @@ type TargetInfoProps = {
function TargetInfo({ transform }: TargetInfoProps) {
const { source, target, table } = transform;
const { database: databaseId } = source.query;
const databaseId =
source.type === "query"
? source.query.database
: source.type === "python"
? source.database
: undefined;
const { data, isLoading } = useGetDatabaseQuery(
table == null && databaseId != null ? { id: databaseId } : skipToken,
);

View File

@@ -78,7 +78,12 @@ function UpdateTargetForm({
onClose,
}: UpdateTargetFormProps) {
const { source, target, table } = transform;
const { database: databaseId } = source.query;
const databaseId =
source.type === "query"
? source.query.database
: source.type === "python"
? source.database
: undefined;
const [updateTransform] = useUpdateTransformMutation();
const [deleteTransformTarget] = useDeleteTransformTargetMutation();
const initialValues = useMemo(() => getInitialValues(transform), [transform]);

View File

@@ -53,6 +53,13 @@ export function TransformQueryPageBody({
const dispatch = useDispatch();
const { sendErrorToast, sendSuccessToast } = useMetadataToasts();
if (transform.source.type === "python") {
// For Python transforms, redirect to the main transform page
// where they can edit the script
dispatch(push(getTransformUrl(transform.id)));
return null;
}
const handleSave = async (query: DatasetQuery) => {
const { error } = await updateTransform({
id: transform.id,

View File

@@ -15,7 +15,9 @@ export function getTransformListUrl() {
return ROOT_URL;
}
export function getNewTransformFromTypeUrl(type: DatasetQuery["type"]) {
export function getNewTransformFromTypeUrl(
type: DatasetQuery["type"] | "python",
) {
return `${ROOT_URL}/new/${type}`;
}

View File

@@ -271,7 +271,7 @@ export type TransformJobTriggerManualRunEvent = ValidateEvent<{
export type TransformCreateEvent = ValidateEvent<{
event: "transform_create";
triggered_from: "transform-page-create-menu";
event_detail: "query" | "native" | "saved-question";
event_detail: "query" | "native" | "python" | "saved-question";
}>;
export type TransformCreatedEvent = ValidateEvent<{

View File

@@ -9,13 +9,19 @@ import type {
import { createMockStructuredDatasetQuery } from "./query";
export function createMockTransformSource(
opts?: Partial<TransformSource>,
): TransformSource {
export function createMockTransformSource(): TransformSource {
return {
type: "query",
query: createMockStructuredDatasetQuery(),
...opts,
};
}
export function createMockPythonTransformSource(): TransformSource {
return {
type: "python",
script: "# Python script\nprint('Hello, world!')",
database: 1,
table: 1,
};
}

View File

@@ -22,10 +22,17 @@ export type Transform = {
last_run?: TransformRun | null;
};
export type TransformSource = {
type: "query";
query: DatasetQuery;
};
export type TransformSource =
| {
type: "query";
query: DatasetQuery;
}
| {
type: "python";
script: string;
database: number;
table?: number;
};
export type TransformTargetType = "table";