Skip to content

Certify the wide MSSQL/dbt → ClickHouse release route locally

This operator how-to proves one exact clean commit in disposable Docker services. It is required when a release changes MSSQL type mapping, BCP Native, the optional native accelerator, the Python reference transcoder, dbt SQL Server materialization, or Parquet/S3 pull. Its receipts always say production_certification: UNVERIFIED; local success never certifies a real deployment.

1. Prepare the host

Run from a clean repository checkout. Install all route extras and verify the two system tools used by SQL Server:

uv sync \
  --extra mssql --extra clickhouse --extra accel \
  --extra dbt-mssql --extra columnar --extra s3
command -v bcp
odbcinst -q -d | grep -F "ODBC Driver 18 for SQL Server"

Supply secrets through your shell or secret manager. The following exports map the host process to the published ports in docker/docker-compose.integration.yml; they do not print or persist secret values:

: "${DPONE_IT_MSSQL_PASSWORD:?set DPONE_IT_MSSQL_PASSWORD}"
: "${DPONE_IT_CH_PASSWORD:?set DPONE_IT_CH_PASSWORD}"
: "${DPONE_IT_MINIO_ACCESS_KEY:?set DPONE_IT_MINIO_ACCESS_KEY}"
: "${DPONE_IT_MINIO_SECRET_KEY:?set DPONE_IT_MINIO_SECRET_KEY}"

export DPONE_IT_MSSQL_HOST=127.0.0.1
export DPONE_IT_MSSQL_PORT="${DPONE_IT_MSSQL_PORT_FORWARD:-51433}"
export DPONE_IT_MSSQL_DATABASE="${DPONE_IT_MSSQL_DATABASE:-dpone_it}"
export DPONE_IT_MSSQL_USER="${DPONE_IT_MSSQL_USER:-sa}"
export DPONE_IT_CH_HOST=127.0.0.1
export DPONE_IT_CH_PORT="${DPONE_IT_CH_PORT_FORWARD:-59000}"
export DPONE_IT_CH_HTTP_PORT="${DPONE_IT_CH_HTTP_PORT_FORWARD:-58123}"
export DPONE_IT_CH_DATABASE="${DPONE_IT_CH_DATABASE:-dpone_it}"
export DPONE_IT_CH_USER="${DPONE_IT_CH_USER:-default}"
export DPONE_IT_S3_ENDPOINT="http://127.0.0.1:${DPONE_IT_MINIO_PORT_FORWARD:-59090}"
export DPONE_IT_S3_REGION="${DPONE_IT_S3_REGION:-us-east-1}"
export DPONE_IT_S3_ACCESS_KEY="${DPONE_IT_MINIO_ACCESS_KEY}"
export DPONE_IT_S3_SECRET_KEY="${DPONE_IT_MINIO_SECRET_KEY}"

Start every dependency before the first live command. minio-init creates dpone-stage; ClickHouse mounts the matching dpone_stage named collection.

docker compose -f docker/docker-compose.integration.yml up -d --wait mssql clickhouse minio
docker compose -f docker/docker-compose.integration.yml run --rm mssql-init
docker compose -f docker/docker-compose.integration.yml run --rm minio-init
docker compose -f docker/docker-compose.integration.yml ps

Use a new directory outside the checkout. Every campaign and authority plan is create-once; do not reuse or delete a directory to hide a failed attempt.

export DPONE_RELEASE_ID=0.74.0  # replace with the synchronized candidate
export DPONE_WIDE_CERT_ROOT="$(mktemp -d "/tmp/dpone-wide-v${DPONE_RELEASE_ID}.XXXXXX")"

2. Create 201 columns and materialize 202 through dbt

The first command creates wide_release_source.orders with the canonical 201 source columns and also exercises the required native backend once. Its output is kept outside Git so source-snapshot verification stays clean.

uv run python tools/mssql_clickhouse_bcp_native_type_certification.py \
  --release-id "${DPONE_RELEASE_ID}" \
  --source-schema wide_release_source --source-table orders \
  --rows 10000 --column-count 201 --typed-hash-rows 10000 \
  --binary-format native --acceleration-mode required \
  --target-table wide_source_native_required \
  --target-rows-per-partition 2500 --export-workers 1 --load-workers 1 \
  --output-dir "${DPONE_WIDE_CERT_ROOT}/source"

The checked-in dbt project must preserve all 201 fields and add nullable decimal(38,8) dbt_calculated_amount as field 202. The producer verifies the canonical source inventory, every passthrough type/nullability, all-row hashes, the calculation, manifest, run results, connection identity, and exact commit.

uv run python tools/mssql_dbt_wide_materialization.py \
  --release-id "${DPONE_RELEASE_ID}" \
  --source-schema wide_release_source --source-table orders \
  --target-schema wide_release_dbt --target-table wide_dbt_result \
  --rows 10000 --source-column-count 201 \
  --target-path "${DPONE_WIDE_CERT_ROOT}/dbt-target" \
  --output-dir "${DPONE_WIDE_CERT_ROOT}/dbt-evidence"

Freeze an independent, secret-free authority plan before the downstream loads. It observes the 202-column MSSQL schema and binds MSSQL, ClickHouse, and S3 coordinates plus policy-derived target schema digests. An acknowledgement-loss retry must reuse this exact file; the producer refuses to overwrite it.

uv run python tools/mssql_clickhouse_wide_release_authority.py \
  --release-id "${DPONE_RELEASE_ID}" \
  --source-schema wide_release_dbt --source-table wide_dbt_result \
  --output "${DPONE_WIDE_CERT_ROOT}/authority.json"

3. Prove native, Python, and Parquet/S3

Both BCP commands reuse the exact dbt relation and hash all 10,000 rows. The required run must observe native_accelerated; the off run must observe python_reference as the deterministic reference oracle. Both compare the actual ClickHouse type/nullability schema with the independently planned schema before reporting PASS.

for MODE in required off; do
  uv run python tools/mssql_clickhouse_bcp_native_type_certification.py \
    --release-id "${DPONE_RELEASE_ID}" --skip-source-prepare \
    --rows 10000 --column-count 202 --typed-hash-rows 10000 \
    --source-schema wide_release_dbt --source-table wide_dbt_result \
    --target-table "wide_dbt_${MODE}" \
    --binary-format native --acceleration-mode "${MODE}" \
    --upstream-evidence "${DPONE_WIDE_CERT_ROOT}/dbt-evidence/mssql_dbt_wide_materialization.json" \
    --output-dir "${DPONE_WIDE_CERT_ROOT}/${MODE}"
done

Run the separate temporal target matrix to prove real auto fallback when the accelerator is unavailable and compare the decoded ClickHouse rows value-for-value with the required native backend. It covers date -> Date|Date32, datetime2 -> DateTime|DateTime64, legacy datetime, smalldatetime, datetimeoffset, time, target-range boundaries, and nulls. The strict JUnit gate rejects a skipped live test.

DPONE_RUN_INTEGRATION=1 DPONE_RUN_INTEGRATION_LIVE=1 uv run pytest \
  tests/integration/mssql/test_mssql_clickhouse_native_temporal_matrix_live.py \
  -m integration_live -q \
  --junitxml "${DPONE_WIDE_CERT_ROOT}/native-temporal-matrix.xml"
uv run python tools/ci/assert_junit_executed.py \
  --junit "${DPONE_WIDE_CERT_ROOT}/native-temporal-matrix.xml" \
  --min-passed 1 --max-skipped 0 \
  --profile mssql_clickhouse_native_temporal_matrix \
  --commit-sha "$(git rev-parse HEAD)" \
  --evidence-json "${DPONE_WIDE_CERT_ROOT}/native-temporal-matrix.json"

The Parquet command uses the same upstream evidence, writes chunks to the run-specific MinIO prefix, pulls them through ClickHouse s3(), verifies the same all-row typed hash and exact sink schema, and proves prefix cleanup.

uv run python tools/mssql_clickhouse_parquet_s3_type_certification.py \
  --release-id "${DPONE_RELEASE_ID}" \
  --rows 10000 --column-count 202 --typed-hash-rows 10000 \
  --source-schema wide_release_dbt --source-table wide_dbt_result \
  --target-table wide_dbt_parquet --run-id "wide-${DPONE_RELEASE_ID}" \
  --upstream-evidence "${DPONE_WIDE_CERT_ROOT}/dbt-evidence/mssql_dbt_wide_materialization.json" \
  --output-dir "${DPONE_WIDE_CERT_ROOT}/parquet"

Each producer exits 0 only for a self-verified PASS receipt. Exit 1 means the data may have transferred but exact evidence is not acceptable. Failure JSON contains a stable phase code, not raw driver text or credentials. Fix the cause and start a new mktemp campaign; never overwrite the old evidence.

4. Build and verify the immutable campaign

The campaign accepts exactly one required-native receipt, one Python-reference receipt, and one Parquet receipt. It deep-verifies copied dbt/result bytes, source generation, 10,000/201/202 shape, endpoint authorities, target schemas, cleanup, and the fixed slot order before returning 0.

uv run python tools/mssql_clickhouse_wide_release_campaign.py \
  --release-id "${DPONE_RELEASE_ID}" \
  --source-relation wide_release_dbt.wide_dbt_result \
  --dbt-evidence "${DPONE_WIDE_CERT_ROOT}/dbt-evidence/mssql_dbt_wide_materialization.json" \
  --authority "${DPONE_WIDE_CERT_ROOT}/authority.json" \
  --native-required-receipt "${DPONE_WIDE_CERT_ROOT}/required/local_route_certification_receipt.json" \
  --native-required-target dpone_it.wide_dbt_required \
  --native-off-receipt "${DPONE_WIDE_CERT_ROOT}/off/local_route_certification_receipt.json" \
  --native-off-target dpone_it.wide_dbt_off \
  --parquet-receipt "${DPONE_WIDE_CERT_ROOT}/parquet/local_route_certification_receipt.json" \
  --parquet-target dpone_it.wide_dbt_parquet \
  --output-dir "${DPONE_WIDE_CERT_ROOT}/campaign"

Verify an existing campaign after an output/acknowledgement loss without rewriting any bytes:

uv run python tools/mssql_clickhouse_wide_release_campaign.py \
  --release-id "${DPONE_RELEASE_ID}" \
  --source-relation wide_release_dbt.wide_dbt_result \
  --dbt-evidence "${DPONE_WIDE_CERT_ROOT}/dbt-evidence/mssql_dbt_wide_materialization.json" \
  --authority "${DPONE_WIDE_CERT_ROOT}/authority.json" \
  --native-required-target dpone_it.wide_dbt_required \
  --native-off-target dpone_it.wide_dbt_off \
  --parquet-target dpone_it.wide_dbt_parquet \
  --verify-existing "${DPONE_WIDE_CERT_ROOT}/campaign/mssql_clickhouse_wide_release_campaign.json"

Machine contracts:

Attach the campaign as supplemental local evidence, not as a required production route slot. Add this argument to the complete R1–R9 command in Release evidence:

--artifact local_mssql_clickhouse_wide_campaign="${DPONE_WIDE_CERT_ROOT}/campaign/mssql_clickhouse_wide_release_campaign.json"
--artifact local_mssql_clickhouse_temporal_matrix="${DPONE_WIDE_CERT_ROOT}/native-temporal-matrix.json"

The normal R1–R9 artifacts remain required separately. Do not tag or publish only because this local campaign passed.