This is an automated email from the ASF dual-hosted git repository.
morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new f40c80dbf83 [improvement](tools) Run SSB, TPCH and TPCDS benchmarks
with one command (#68278)
f40c80dbf83 is described below
commit f40c80dbf831f30a2da3f4b0fa077eca9b03e511
Author: HappenLee <[email protected]>
AuthorDate: Sat Oct 10 18:03:42 2026 +0800
[improvement](tools) Run SSB, TPCH and TPCDS benchmarks with one command
(#68278)
### What problem does this PR solve?
Issue Number: None
The existing SSB, TPCH and TPCDS tools require generating local data
files and
running separate creation, import, statistics and benchmark commands.
Add one
entry point per suite, backed by a shared runner:
```bash
tools/ssb-tools/bin/run-ssb.sh -s 1 -d ssb_sf1
tools/tpch-tools/bin/run-tpch.sh -s 1 -d tpch_sf1
tools/tpcds-tools/bin/run-tpcds.sh -s 1 -d tpcds_sf1
```
Rows are generated on demand through Doris's Trino connector and
imported with
`INSERT INTO ... SELECT ...`, without intermediate data files or a
separate
Trino server. SSB uses the included native-dbgen plugin. TPCH/TPCDS use
complete
upstream Trino 435 plugin distributions, pinned by SHA-256 and
downloaded during
operator installation. Plugins must be installed on every FE and BE
before use.
The runner executes the original scale-specific table DDL and query SQL:
- SSB prepares its five base tables and supports the original
`lineorder_flat`
workflow through `--mode ssb|flat|both`. Both suites have 13 query
files.
- TPCH prepares its eight base tables and `revenue0` view and runs 22
query files.
- TPCDS prepares its 24 base tables and runs 103 scale-specific query
files,
including the second statements of templates 14, 23, 24 and 39.
Full statistics are collected synchronously before query timing. Every
query
runs three times with SQL and BE query result caches disabled.
`--queries-only`
repeats measurements against already prepared data. Each run preserves
SQL
results, errors, preparation logs, settings and CSV timings in a fresh
directory.
Parallel TPCH/TPCDS tables default to `max(10, SCALE)` generator splits:
10/100/1000/10000 for SF1/100/1000/10000. `--splits COUNT` overrides
that count;
invalid values fail before connecting. TPCH region/nation and TPCDS
tables below
the pinned generator's one-million-record parallelization threshold use
one
split, including with an override. Small tables share a single-split
catalog;
`--splits 1` reuses the main catalog. Per-table effective counts are
logged.
Preparation stderr is saved in `prepare.log`, while failures still print
the
log directory to the terminal. SSB preserves the old generator's ten
lineorder partitions, enables its existing 64-bit path and fixes SSB
random
stream usage-counter bounds. Each SSB scan owns and closes its generator
process.
Imports explicitly project target column names, including TPCH's
reordered
lineitem columns and Trino 435's `p_response_targe` spelling for TPCDS
promotion.
TPCH uses standard prefixed column names and DECIMAL values. A fresh
database
prevents duplicate appends on retries. INSERTs use strict mode with zero
filtered
rows and must return a metadata `status` of `VISIBLE`. SQL text or
labels
containing `COMMITTED` cannot cause a false failure; missing or
non-visible
status stops preparation. Query errors stop the suite and are not
recorded as
successful timings.
---
tools/benchmark/.gitignore | 2 +
tools/benchmark/README.md | 164 +++++++++
tools/benchmark/download-tpc-plugins.sh | 41 +++
tools/benchmark/run-benchmark.sh | 356 +++++++++++++++++++
tools/benchmark/tests/test_run_benchmark.py | 380 +++++++++++++++++++++
tools/ssb-tools/.gitignore | 1 +
tools/ssb-tools/README.md | 92 ++++-
tools/ssb-tools/bin/run-ssb.sh | 21 ++
tools/ssb-tools/ddl/insert-ssb-flat.sql | 30 ++
tools/ssb-tools/trino-ssb/.gitignore | 1 +
tools/ssb-tools/trino-ssb/build.sh | 45 +++
tools/ssb-tools/trino-ssb/dbgen-stream.patch | 62 ++++
tools/ssb-tools/trino-ssb/pom.xml | 104 ++++++
.../org/apache/doris/tools/ssb/SsbConnector.java | 226 ++++++++++++
.../java/org/apache/doris/tools/ssb/SsbPlugin.java | 56 +++
.../org/apache/doris/tools/ssb/SsbRecordSet.java | 198 +++++++++++
.../java/org/apache/doris/tools/ssb/SsbTable.java | 60 ++++
.../META-INF/services/io.trino.spi.Plugin | 18 +
.../apache/doris/tools/ssb/SsbConnectorTest.java | 186 ++++++++++
tools/tpcds-tools/.gitignore | 1 +
tools/tpcds-tools/README.md | 21 ++
tools/tpcds-tools/bin/run-tpcds.sh | 21 ++
tools/tpch-tools/.gitignore | 1 +
tools/tpch-tools/README.md | 21 ++
tools/tpch-tools/bin/run-tpch.sh | 21 ++
25 files changed, 2128 insertions(+), 1 deletion(-)
diff --git a/tools/benchmark/.gitignore b/tools/benchmark/.gitignore
new file mode 100644
index 00000000000..bf5351bd9c2
--- /dev/null
+++ b/tools/benchmark/.gitignore
@@ -0,0 +1,2 @@
+target/
+tests/__pycache__/
diff --git a/tools/benchmark/README.md b/tools/benchmark/README.md
new file mode 100644
index 00000000000..81f2d817083
--- /dev/null
+++ b/tools/benchmark/README.md
@@ -0,0 +1,164 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+
+# Streaming benchmark workflows
+
+The SSB, TPCH, and TPCDS entry points create a new Doris database, generate
rows
+while Doris scans a Trino catalog, import with `INSERT INTO ... SELECT ...`,
+collect statistics synchronously, and execute each existing query three times.
+No intermediate data files or separate Trino server are needed.
+
+| Entry point | Scale factors | Tables | Query files |
+| --- | --- | --- | --- |
+| `../ssb-tools/bin/run-ssb.sh` | 1, 100, 1000 | Five SSB tables and
`lineorder_flat` | 13 SSB + 13 SSB Flat |
+| `../tpch-tools/bin/run-tpch.sh` | 1, 100, 1000, 10000 | Eight TPCH tables
and the `revenue0` view | 22 |
+| `../tpcds-tools/bin/run-tpcds.sh` | 1, 100, 1000, 10000 | 24 TPCDS tables |
103, including the second statements of 14, 23, 24, and 39 |
+
+TPCH and TPCDS use the existing multi-table DDL for the requested scale factor
+and run their original query SQL. SSB supports its original and flat-table
query
+suites, using the existing `lineorder_flat` DDL and joins. All suites collect
+full statistics synchronously before query timing.
+
+## Install the generators once
+
+For SSB, follow [the SSB plugin build instructions](../ssb-tools/README.md).
+For TPCH/TPCDS, download the complete upstream plugin distributions with
`curl`,
+`unzip`, and `sha256sum` installed:
+
+```bash
+./download-tpc-plugins.sh
+cp -r target/trino-tpch-435 target/trino-tpcds-435
/path/to/fe/plugins/trino_plugins/
+cp -r target/trino-tpch-435 target/trino-tpcds-435
/path/to/be/plugins/trino_plugins/
+```
+
+Install both directories on **every FE and BE**, then restart those nodes. The
+script pins Trino 435 to match Doris's Trino SPI and verifies each archive's
+SHA-256 before extracting it. Keep all bundled runtime JARs in each directory;
+copying only the connector JAR is insufficient. Downloads happen only during
+this operator installation step, never from catalog properties.
+
+These plugins use Trino's Java TPCH/TPCDS generators. They are distinct from
the
+native generator versions used by the older file-based scripts; their outputs
+are not asserted to be byte-for-byte identical to those tools. TPCH explicitly
+uses standard prefixed column names and DECIMAL monetary values. Each import
+selects target column names explicitly, so different source and destination
+column orders do not reorder the data. The TPCDS import also maps Trino 435's
`p_response_targe` spelling to the existing Doris
+`promotion.p_response_target` column.
+
+## Run
+
+Set each suite's `conf/doris-cluster.conf`, or pass a separate connection file:
+
+```bash
+../tpch-tools/bin/run-tpch.sh -s 1 -d tpch_sf1 -c /path/to/cluster.conf
+../tpcds-tools/bin/run-tpcds.sh -s 1 -d tpcds_sf1 -c /path/to/cluster.conf
+../ssb-tools/bin/run-ssb.sh -s 1 -d ssb_sf1 -c /path/to/cluster.conf
+```
+
+Parallel TPCH/TPCDS tables default to `max(10, SCALE)` generator splits:
+
+| Scale factor | Default splits for parallel tables |
+| --- | --- |
+| 1 | 10 |
+| 100 | 100 |
+| 1000 | 1000 |
+| 10000 | 10000 |
+
+Use `--splits COUNT` to override this count during preparation, for example:
+
+```bash
+../tpch-tools/bin/run-tpch.sh -s 1000 -d tpch_sf1000 --splits 256
+```
+
+`COUNT` must be an integer from 1 to 2147483647. Small tables always use one
split,
+even when `--splits` is supplied:
+
+- TPCH: `region` and `nation`.
+- TPCDS at every supported scale: `call_center`, `catalog_page`, `date_dim`,
+ `household_demographics`, `income_band`, `item`, `promotion`, `reason`,
+ `ship_mode`, `store`, `time_dim`, `warehouse`, `web_page`, and `web_site`.
+- TPCDS at SF1 also uses one split for `customer`, `customer_address`, and all
+ six sales/returns tables.
+
+The TPCDS choices follow the pinned generator's
+[`Parallel.splitWork`](https://github.com/trinodb/tpcds/blob/1.4/src/main/java/io/trino/tpcds/Parallel.java)
+rule: tables with fewer than one million generator records produce rows only in
+the first chunk. Sales generator records expand into multiple output rows.
+The runner creates a `${suite}_gen_${database}` catalog for parallel tables
and,
+when the selected count exceeds one, a `${suite}_one_${database}` catalog for
+small tables. Both use the same plugin, scale, and column options. Each table's
+effective split count is recorded in `prepare.log`; actual concurrent execution
+depends on cluster resources. `--queries-only` does not recreate or change
these
+catalogs. SSB does not accept `--splits`: it retains the original generator's
ten
+lineorder partitions because changing the partition count changes its random
+data streams. SSB dimension tables use one split each.
+
+Preparation requires a **new database**. An existing database is rejected
before
+any tables are changed, preventing duplicate appends after a partial load.
+Imports use strict mode and zero filtered rows. Each INSERT must return a
+`status` field equal to `VISIBLE`; errors, missing status, or any other status
+(including `COMMITTED`) abort preparation. Failed runs preserve tables and logs
+for inspection. Do not rerun preparation into that database.
+
+To repeat measurements against completed data, use `--queries-only`. For TPCDS,
+pass the same scale factor as the imported data, because query constants differ
+by scale:
+
+```bash
+../tpcds-tools/bin/run-tpcds.sh -s 100 -d tpcds_sf100 --queries-only
+```
+
+SSB also accepts `--mode ssb|flat|both` (default `both`). This option is
rejected
+by TPCH and TPCDS. Existing file-based entry points remain available.
+
+## Results and timing
+
+Each run creates a fresh `<suite>-tools/results/<timestamp>-<pid>/` directory.
+Use `--result-dir /path/to/new-directory` to select another location; existing
+result directories are rejected rather than overwritten.
+
+- `result.csv`: suite, query, first-run milliseconds, two repeated-run times,
and
+ the minimum repeated-run time.
+- `<suite>/q*.cold.out`, `q*.hot1.out`, `q*.hot2.out`: query results, with
matching
+ `.err` files for diagnostics. TPCDS variants have distinct names such as
`q14_1`.
+- `prepare.log`: per-table split counts, DDL, import, and statistics
diagnostics,
+ including preparation errors from stderr.
+- `environment.txt`: Doris version, session variables, and table status.
+
+Timing includes mysql client startup, connection, execution, and result
transfer.
+The `cold_ms` column means the **first execution**; no storage or OS caches are
+flushed. SQL result caching and BE query result caching are disabled in every
+measured session. A query failure stops immediately and never produces a
+successful timing row for that query. These are engineering measurements, not
+certified TPC benchmark results.
+
+## Orchestration tests
+
+From the repository root:
+
+```bash
+python3 -m unittest discover -s tools/benchmark/tests -v
+```
+
+The tests use a fake mysql client to exercise complete suite selection, TPCDS
+split queries, explicit column projection, scale-specific DDL and queries,
+generator split defaults and overrides, cache settings, result isolation,
INSERT
+status checks, and failure paths. They verify that TPCH/TPCDS prepare only
their
+original tables and that SSB retains its flat-table workflow. They do not
replace
+a real Doris import/query run.
diff --git a/tools/benchmark/download-tpc-plugins.sh
b/tools/benchmark/download-tpc-plugins.sh
new file mode 100755
index 00000000000..cce40453708
--- /dev/null
+++ b/tools/benchmark/download-tpc-plugins.sh
@@ -0,0 +1,41 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+set -euo pipefail
+BENCHMARK_ROOT=$(cd "$(dirname "$0")" && pwd)
+cd "${BENCHMARK_ROOT}"
+# Match fe/pom.xml's Trino SPI version. These are the complete upstream plugin
+# distributions, including runtime dependencies, not standalone connector JARs.
+version=435
+mkdir -p target
+for connector in tpch tpcds; do
+ # Both values in the fixed connector list are covered.
+ # shellcheck disable=SC2249
+ case "${connector}" in
+ tpch)
checksum=59d99220b5f47a63606f3bbcd101b7a8e3a160438b6f2fdc361b91badf4695e1 ;;
+ tpcds)
checksum=38b9027d8201cb37ff14d051840aeb41eb29ad11bc89a862d294ce9049ae85e8 ;;
+ esac
+ archive="target/trino-${connector}-${version}.zip"
+ curl --fail --location --retry 3 \
+
"https://repo.maven.apache.org/maven2/io/trino/trino-${connector}/${version}/trino-${connector}-${version}.zip"
\
+ --output "${archive}"
+ echo "${checksum} ${archive}" | sha256sum --check
+ unzip -oq "${archive}" -d target
+done
+echo 'Copy the entire target/trino-tpch-435 and target/trino-tpcds-435
directories'
+echo 'into plugins/trino_plugins/ on every FE and BE, then restart those
nodes.'
diff --git a/tools/benchmark/run-benchmark.sh b/tools/benchmark/run-benchmark.sh
new file mode 100755
index 00000000000..89a3a35827c
--- /dev/null
+++ b/tools/benchmark/run-benchmark.sh
@@ -0,0 +1,356 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+set -Eeuo pipefail
+BENCHMARK_ROOT=$(cd "$(dirname "$0")" && pwd)
+benchmark=$1
+shift
+case "${benchmark}" in
+ ssb | tpch | tpcds) ;;
+ *)
+ echo "Unknown benchmark: ${benchmark}" >&2
+ exit 1
+ ;;
+esac
+SUITE_ROOT="${BENCHMARK_ROOT}/../${benchmark}-tools"
+benchmark_name=${benchmark^^}
+
+usage() {
+ cat <<EOF
+Usage: run-${benchmark}.sh [-s SCALE] [-c CONFIG] [-d DATABASE]
[--queries-only]
+ [--result-dir DIRECTORY] [--mode both|ssb|flat]
[--splits COUNT]
+
+Create tables, generate and import data through the Trino ${benchmark}
connector,
+collect statistics, and run every existing query three times. No data files or
+separate Trino server are needed. SSB can also prepare lineorder_flat.
+Existing table DDL and query SQL are preserved.
+
+SCALE: 1, 100, 1000 (also 10000 for TPCH/TPCDS). Default: 1.
+--mode is only supported by SSB; its default is both.
+--splits controls parallel TPCH/TPCDS tables; its default is max(10, SCALE).
+Small tables use one split, including when --splits is supplied.
+COUNT must be an integer from 1 to 2147483647 and applies during preparation.
+Requires mysql and the generator plugin installed on every Doris FE and BE.
+The default connection configuration is conf/doris-cluster.conf.
+Preparation requires a new database. Use --queries-only against completed data.
+EOF
+}
+
+scale=1
+config="${SUITE_ROOT}/conf/doris-cluster.conf"
+database=''
+queries_only=0
+mode=''
+generator_splits=''
+result_dir="${SUITE_ROOT}/results/$(date +%Y%m%d-%H%M%S)-$$"
+options=$(getopt -o hs:c:d: -l help,queries-only,mode:,result-dir:,splits: --
"$@")
+eval set -- "${options}"
+while true; do
+ case "$1" in
+ -h | --help)
+ usage
+ exit 0
+ ;;
+ -s)
+ scale=$2
+ shift 2
+ ;;
+ -c)
+ config=$2
+ shift 2
+ ;;
+ -d)
+ database=$2
+ shift 2
+ ;;
+ --queries-only)
+ queries_only=1
+ shift
+ ;;
+ --mode)
+ mode=$2
+ shift 2
+ ;;
+ --result-dir)
+ result_dir=$2
+ shift 2
+ ;;
+ --splits)
+ # Both Trino connectors parse the split count as a Java int.
+ if [[ ! $2 =~ ^[1-9][0-9]{0,9}$ ]] || (($2 > 2147483647)); then
+ echo '--splits must be an integer from 1 to 2147483647.' >&2
+ exit 1
+ fi
+ generator_splits=$2
+ shift 2
+ ;;
+ --)
+ shift
+ break
+ ;;
+ *)
+ usage >&2
+ exit 1
+ ;;
+ esac
+done
+if [[ $# -ne 0 || ! ${scale} =~ ^(1|100|1000|10000)$ ]] ||
+ [[ ${benchmark} == ssb && ${scale} == 10000 ]] ||
+ [[ ${benchmark} == ssb && -n ${generator_splits} ]] ||
+ [[ ${benchmark} != ssb && -n ${mode} ]] ||
+ [[ -n ${mode} && ! ${mode} =~ ^(both|ssb|flat)$ ]]; then
+ usage >&2
+ exit 1
+fi
+mode=${mode:-both}
+generator_splits=${generator_splits:-$((scale > 10 ? scale : 10))}
+ddl="${SUITE_ROOT}/ddl/create-${benchmark}-tables-sf${scale}.sql"
+single_split_tables=()
+# Each list is the base tables in the existing DDL; TPCH's revenue0 is a view.
+# The benchmark name is validated above.
+# shellcheck disable=SC2249
+case "${benchmark}" in
+ ssb)
+ tables=(customer part supplier dates lineorder)
+ suites=(ssb ssb-flat)
+ query_ids=(1.1 1.2 1.3 2.1 2.2 2.3 3.1 3.2 3.3 3.4 4.1 4.2 4.3)
+ generator_properties="'trino.connector.name'='ssb'"
+ ;;
+ tpch)
+ tables=(region nation supplier customer part partsupp orders lineitem)
+ single_split_tables=(region nation)
+ suites=(tpch)
+ mapfile -t query_ids < <(seq 1 22)
+ # Match the existing Doris DDL, including exact two-decimal monetary
values.
+ generator_properties="'trino.connector.name'='tpch',
+ 'trino.tpch.column-naming'='STANDARD',
+ 'trino.tpch.double-type-mapping'='DECIMAL'"
+ split_property='trino.tpch.splits-per-node'
+ ;;
+ tpcds)
+ tables=(call_center catalog_page catalog_returns catalog_sales
customer_address
+ customer_demographics customer date_dim household_demographics
income_band
+ inventory item promotion reason ship_mode store_returns
store_sales store
+ time_dim warehouse web_page web_returns web_sales web_site)
+ # Trino 435 uses tpcds 1.4: Parallel.splitWork emits rows only in
chunk 1
+ # when Scaling.getRowCount is below one million. These tables stay
below
+ # that threshold at every supported scale; SF1 has additional such
tables.
+ single_split_tables=(call_center catalog_page date_dim
household_demographics
+ income_band item promotion reason ship_mode store time_dim
warehouse web_page web_site)
+ if [[ ${scale} -eq 1 ]]; then
+ single_split_tables+=(catalog_returns catalog_sales
customer_address customer
+ store_returns store_sales web_returns web_sales)
+ fi
+ suites=(tpcds)
+ query_ids=()
+ for ((query = 1; query <= 99; query++)); do
+ query_ids+=("${query}")
+ # The existing suite splits these four templates into two
statements.
+ case "${query}" in
+ 14 | 23 | 24 | 39) query_ids+=("${query}_1") ;;
+ *) ;;
+ esac
+ done
+ generator_properties="'trino.connector.name'='tpcds'"
+ split_property='trino.tpcds.split-count'
+ ;;
+esac
+
+query_path() {
+ # The benchmark name is validated above.
+ # shellcheck disable=SC2249
+ case "${benchmark}" in
+ ssb) printf '%s/%s-queries/q%s.sql' "${SUITE_ROOT}" "$1" "$2" ;;
+ tpch) printf '%s/queries/q%s.sql' "${SUITE_ROOT}" "$2" ;;
+ tpcds) printf '%s/queries/sf%s/query%s.sql' "${SUITE_ROOT}" "${scale}"
"$2" ;;
+ esac
+}
+
+# Fail before creating a database if a requested suite is incomplete.
+for suite in "${suites[@]}"; do
+ for query in "${query_ids[@]}"; do
+ test -r "$(query_path "${suite}" "${query}")"
+ done
+done
+test -r "${ddl}"
+# shellcheck disable=SC1090
+source "${config}"
+database=${database:-${DB}}
+if [[ ! ${database} =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then
+ echo 'The database name must contain only letters, digits and
underscores.' >&2
+ exit 1
+fi
+export MYSQL_PWD=${PASSWORD:-}
+mysql_args=(--protocol=tcp --host="${FE_HOST}" --port="${FE_QUERY_PORT}"
--user="${USER}"
+ --batch --raw --skip-column-names --comments --skip-force)
+command -v mysql >/dev/null
+mkdir -p "$(dirname "${result_dir}")"
+# Refuse to overwrite an earlier run's measurements or query results.
+mkdir "${result_dir}"
+result_dir=$(cd "${result_dir}" && pwd)
+# Keep the log location visible even when the failing command redirects stderr.
+exec {error_fd}>&2
+trap 'echo "${benchmark_name} failed at line ${LINENO}. Logs: ${result_dir}.
Prepared tables are preserved." >&"${error_fd}"' ERR
+
+run_sql() {
+ local sql=$1
+ shift
+ mysql "${mysql_args[@]}" "$@" --execute "${sql}"
+}
+
+create_generator_catalog() {
+ local properties=${generator_properties}
+ if [[ ${benchmark} != ssb ]]; then
+ properties+=", '${split_property}'='$2'"
+ fi
+ run_sql "CREATE CATALOG \`$1\` PROPERTIES (
+ 'type'='trino-connector', ${properties});"
>>"${result_dir}/prepare.log" 2>&1
+}
+
+insert_sql() {
+ # A successful INSERT can otherwise return COMMITTED before its rows are
visible.
+ # Preserve the diagnostic and abort rather than benchmark incomplete data
or retry a write.
+ local output insert_status status=0
+ output=$(
+ set -e
+ run_sql "SET enable_insert_strict=true; SET insert_max_filter_ratio=0;
+ SET insert_timeout=14400; SET insert_visible_timeout_ms=600000; $1"
-vvv 2>&1
+ ) || status=$?
+ printf '%s\n' "${output}" >>"${result_dir}/prepare.log"
+ if [[ ${status} -ne 0 ]]; then
+ printf '%s\n' "${output}" >&2
+ return "${status}"
+ fi
+ # mysql -vvv also echoes the SQL. Read the status field only from returned
+ # metadata records, so SQL identifiers and label values cannot match it.
+ insert_status=$(awk -F"'" '
+ /^[[:space:]]*\{/ {
+ for (i = 2; i + 2 <= NF; i += 2) {
+ if ($i == "status" && $(i + 1) ~
/^[[:space:]]*:[[:space:]]*$/) {
+ print $(i + 2)
+ break
+ }
+ }
+ }' <<<"${output}")
+ if [[ ${insert_status} != VISIBLE ]]; then
+ echo "INSERT did not report VISIBLE (status:
${insert_status:-missing}); inspect prepare.log before continuing." >&2
+ return 1
+ fi
+}
+
+echo "${benchmark_name} SF${scale}: ${FE_HOST}:${FE_QUERY_PORT}/${database};
results: ${result_dir}"
+if [[ ${queries_only} -eq 0 ]]; then
+ if [[ ${benchmark} != ssb ]]; then
+ echo "Generator splits for parallel tables: ${generator_splits}; small
tables: 1" |
+ tee -a "${result_dir}/prepare.log"
+ fi
+ echo "Creating database and Trino ${benchmark_name} catalog"
+ # CREATE DATABASE is deliberately not IF NOT EXISTS: a retry must never
append
+ # the same generated data to these DUPLICATE KEY tables, even after a
partial run.
+ run_sql "CREATE DATABASE \`${database}\`;" >>"${result_dir}/prepare.log"
2>&1
+ generator_catalog="${benchmark}_gen_${database}"
+ create_generator_catalog "${generator_catalog}" "${generator_splits}"
+ single_split_catalog=${generator_catalog}
+ if [[ ${benchmark} != ssb && ${generator_splits} -gt 1 ]]; then
+ # Connector split properties belong to a catalog, not an individual
scan.
+ # Reuse one single-split catalog for all small tables.
+ single_split_catalog="${benchmark}_one_${database}"
+ create_generator_catalog "${single_split_catalog}" 1
+ fi
+ mysql_args+=(--database="${database}")
+ mysql "${mysql_args[@]}" <"${ddl}" \
+ >>"${result_dir}/prepare.log" 2>&1
+
+ for table in "${tables[@]}"; do
+ source_catalog=${generator_catalog}
+ if [[ ${benchmark} != ssb ]]; then
+ table_splits=${generator_splits}
+ if [[ " ${single_split_tables[*]} " == *" ${table} "* ]]; then
+ source_catalog=${single_split_catalog}
+ table_splits=1
+ fi
+ echo "Generating and importing ${table} (${table_splits} generator
splits)" |
+ tee -a "${result_dir}/prepare.log"
+ else
+ echo "Generating and importing ${table}"
+ fi
+ # Use target column names explicitly: source column order must not
affect the import.
+ # The backticks in sed quote SQL identifiers, not shell commands.
+ # shellcheck disable=SC2016
+ columns=$(
+ set -e
+ run_sql "DESC \`${table}\`;" 2>>"${result_dir}/prepare.log" |
+ cut -f1 | sed 's/.*/`&`/' | paste -sd,
+ )
+ source_columns=${columns}
+ if [[ ${benchmark} == tpcds && ${table} == promotion ]]; then
+ # Trino 435's generator misspells this column. Keep Doris's
existing
+ # schema and map the source name without relying on source column
order.
+ source_columns=${columns//p_response_target/p_response_targe}
+ fi
+ insert_sql "INSERT INTO \`${table}\` (${columns})
+ SELECT ${source_columns} FROM
\`${source_catalog}\`.\`sf${scale}\`.\`${table}\`;"
+ done
+
+ if [[ ${benchmark} == ssb && ${mode} != ssb ]]; then
+ echo 'Building lineorder_flat'
+ mysql "${mysql_args[@]}"
<"${SUITE_ROOT}/ddl/create-ssb-flat-tables-sf${scale}.sql" \
+ >>"${result_dir}/prepare.log" 2>&1
+ # Keep the existing loader's year-sized joins to bound each INSERT's
memory use.
+ for year in 1992 1993 1994 1995 1996 1997 1998; do
+ flat_sql=$(sed "s/@YEAR@/${year}/g; s/@NEXT_YEAR@/$((year + 1))/g"
\
+ "${SUITE_ROOT}/ddl/insert-ssb-flat.sql")
+ insert_sql "${flat_sql}"
+ done
+ fi
+ echo 'Collecting statistics'
+ run_sql "ANALYZE DATABASE \`${database}\` WITH FULL WITH SYNC;"
>>"${result_dir}/prepare.log" 2>&1
+else
+ mysql_args+=(--database="${database}")
+fi
+
+# Repeated runs should execute the query while still benefiting from warm data
caches.
+mysql_args+=(--init-command='SET enable_sql_cache=false,
enable_query_cache=false')
+run_sql 'SELECT VERSION(); SHOW VARIABLES; SHOW TABLE STATUS;'
>"${result_dir}/environment.txt"
+printf 'suite,query,cold_ms,hot1_ms,hot2_ms,best_hot_ms\n'
>"${result_dir}/result.csv"
+for suite in "${suites[@]}"; do
+ if [[ (${mode} == ssb && ${suite} == ssb-flat) || (${mode} == flat &&
${suite} == ssb) ]]; then
+ continue
+ fi
+ mkdir "${result_dir}/${suite}"
+ cold_total=0
+ hot_total=0
+ for query in "${query_ids[@]}"; do
+ times=()
+ for run in cold hot1 hot2; do
+ start=$(date +%s%3N)
+ mysql "${mysql_args[@]}" <"$(query_path "${suite}" "${query}")" \
+ >"${result_dir}/${suite}/q${query}.${run}.out" \
+ 2>"${result_dir}/${suite}/q${query}.${run}.err"
+ end=$(date +%s%3N)
+ times+=("$((end - start))")
+ done
+ best=$((times[1] < times[2] ? times[1] : times[2]))
+ printf '%s,q%s,%s,%s,%s,%s\n' "${suite}" "${query}" "${times[@]}"
"${best}" |
+ tee -a "${result_dir}/result.csv"
+ cold_total=$((cold_total + times[0]))
+ hot_total=$((hot_total + best))
+ done
+ echo "${suite}: total cold ${cold_total} ms, total best hot ${hot_total}
ms"
+done
+echo "${benchmark_name} completed. Results: ${result_dir}/result.csv"
diff --git a/tools/benchmark/tests/test_run_benchmark.py
b/tools/benchmark/tests/test_run_benchmark.py
new file mode 100644
index 00000000000..a6a90a2178f
--- /dev/null
+++ b/tools/benchmark/tests/test_run_benchmark.py
@@ -0,0 +1,380 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+"""Exercise orchestration failures without requiring a running Doris
cluster."""
+
+import csv
+import json
+import os
+from pathlib import Path
+import re
+import subprocess
+import tempfile
+import unittest
+
+
+TOOLS_ROOT = Path(__file__).resolve().parents[2]
+MYSQL = r'''#!/usr/bin/env python3
+import json, os, sys
+args = sys.argv[1:]
+sql = args[args.index('--execute') + 1] if '--execute' in args else
sys.stdin.read()
+sql = '\n'.join(line for line in sql.splitlines() if not
line.lstrip().startswith('--')).strip()
+with open(os.environ['BENCHMARK_TEST_CALLS'], 'a') as log:
+ log.write(json.dumps({'sql': sql, 'args': args}) + '\n')
+scenario = os.environ.get('BENCHMARK_TEST_SCENARIO', '')
+if sql.startswith('CREATE DATABASE') and scenario == 'existing_database':
+ sys.exit('Database already exists')
+if sql.startswith('CREATE CATALOG'):
+ if scenario == 'catalog_error' or (scenario == 'single_catalog_error' and
'_one_' in sql):
+ sys.exit('Generator plugin is unavailable')
+if 'create table' in sql.lower() and scenario == 'ddl_error':
+ sys.exit('Table creation failed')
+if sql.startswith('ANALYZE') and scenario == 'analyze_error':
+ sys.exit('Statistics collection failed')
+if sql.startswith('DESC'):
+ if scenario == 'desc_error':
+ sys.exit('Cannot describe target table')
+ if '`promotion`' in sql:
+ print('p_promo_sk\tINT\np_response_target\tINT')
+ else:
+ print('lo_orderkey\tBIGINT\nlo_linenumber\tINT')
+elif sql.startswith('CREATE TABLE') and 'lineorder_flat' in sql and scenario
== 'flat_ddl_error':
+ sys.exit('Wide table creation failed')
+elif 'INSERT INTO' in sql:
+ if '-vvv' in args:
+ print('--------------\n' + sql + '\n--------------')
+ if scenario == 'insert_error' or (scenario == 'flat_insert_error' and
'lineorder_flat' in sql):
+ sys.exit('Generator exited with 23')
+ print('Query OK, 10 rows affected')
+ if scenario != 'missing_status':
+ status = ('COMMITTED' if scenario == 'unpublished' or
+ (scenario == 'flat_unpublished' and 'lineorder_flat' in sql)
else
+ 'PREPARE' if scenario == 'unexpected_status' else 'VISIBLE')
+ label = 'COMMITTED_label' if scenario == 'committed_label' else
'insert_label'
+ print("{'label':'%s', 'status':'%s', 'txnId':'123'}" % (label, status))
+elif sql.lower().startswith(('select', 'with')) and 'VERSION()' not in sql:
+ if scenario == 'query_error':
+ sys.exit('Query execution failed')
+ print('42')
+'''
+
+
+class RunBenchmarkTest(unittest.TestCase):
+ def setUp(self):
+ self.temporary = tempfile.TemporaryDirectory(prefix="benchmark run ")
+ self.addCleanup(self.temporary.cleanup)
+ self.root = Path(self.temporary.name)
+ (self.root / "mysql").write_text(MYSQL)
+ (self.root / "mysql").chmod(0o755)
+ self.config = self.root / "cluster.conf"
+
self.config.write_text("FE_HOST=127.0.0.1\nFE_QUERY_PORT=9030\nUSER=root\nPASSWORD=''\nDB=benchmark_test\n")
+ self.calls = self.root / "calls.jsonl"
+ self.results = self.root / "results"
+
+ def run_benchmark(self, scenario="", *options, benchmark="ssb"):
+ return subprocess.run(
+ ["bash", str(TOOLS_ROOT /
f"{benchmark}-tools/bin/run-{benchmark}.sh"), "-c", str(self.config),
+ "--result-dir", str(self.results), *options],
+ env=dict(os.environ, PATH=f"{self.root}:{os.environ['PATH']}",
+ BENCHMARK_TEST_CALLS=str(self.calls),
BENCHMARK_TEST_SCENARIO=scenario),
+ cwd=self.root, capture_output=True, text=True, timeout=30,
+ )
+
+ def statements(self):
+ return [json.loads(line)["sql"] for line in
self.calls.read_text().splitlines()]
+
+ def test_complete_workflow_and_distinct_results(self):
+ result = self.run_benchmark()
+ self.assertEqual(0, result.returncode, result.stdout + result.stderr)
+ calls = self.statements()
+ self.assertEqual(12, sum("INSERT INTO" in sql for sql in calls))
+ self.assertEqual(1, sum(sql.startswith("ANALYZE") for sql in calls))
+ with (self.results / "result.csv").open() as file:
+ rows = list(csv.DictReader(file))
+ self.assertEqual(26, len(rows))
+ self.assertEqual({"ssb", "ssb-flat"}, {row["suite"] for row in rows})
+ self.assertEqual(78, len(list(self.results.glob("*/*.out"))))
+
+ def test_existing_database_never_receives_inserts(self):
+ result = self.run_benchmark("existing_database")
+ self.assertNotEqual(0, result.returncode)
+ self.assertEqual(1, len(self.statements()))
+
+ def test_failed_or_unpublished_insert_stops_preparation(self):
+ for scenario in ("insert_error", "unpublished", "missing_status",
"unexpected_status"):
+ with self.subTest(scenario=scenario):
+ self.results = self.root / scenario
+ self.calls = self.root / f"{scenario}.jsonl"
+ result = self.run_benchmark(scenario)
+ self.assertNotEqual(0, result.returncode)
+ self.assertEqual(1, sum("INSERT INTO" in sql for sql in
self.statements()))
+ self.assertFalse((self.results / "result.csv").exists())
+ self.assertTrue((self.results / "prepare.log").read_text())
+
+ def test_visible_status_is_not_confused_with_sql_or_label(self):
+ for benchmark in ("ssb", "tpch", "tpcds"):
+ with self.subTest(benchmark=benchmark):
+ self.results = self.root / benchmark
+ self.calls = self.root / f"{benchmark}.jsonl"
+ result = self.run_benchmark("committed_label", "-d",
f"{benchmark}_COMMITTED",
+ benchmark=benchmark)
+ self.assertEqual(0, result.returncode, result.stdout +
result.stderr)
+ log = (self.results / "prepare.log").read_text()
+ self.assertIn(f"{benchmark}_gen_{benchmark}_COMMITTED", log)
+ self.assertIn("'label':'COMMITTED_label'", log)
+ self.assertTrue((self.results / "result.csv").exists())
+
+ def test_visible_in_sql_does_not_hide_an_unpublished_insert(self):
+ result = self.run_benchmark("unpublished", "-d", "tpch_VISIBLE",
benchmark="tpch")
+ self.assertNotEqual(0, result.returncode)
+ self.assertEqual(1, sum("INSERT INTO" in sql for sql in
self.statements()))
+ self.assertFalse((self.results / "result.csv").exists())
+
+ def test_generator_splits_scale_with_data_and_allow_override(self):
+ tpcds_small = {"call_center", "catalog_page", "date_dim",
"household_demographics",
+ "income_band", "item", "promotion", "reason",
"ship_mode", "store",
+ "time_dim", "warehouse", "web_page", "web_site"}
+ tpcds_sf1_small = {"catalog_returns", "catalog_sales",
"customer_address", "customer",
+ "store_returns", "store_sales", "web_returns",
"web_sales"}
+ for benchmark, property_name in (("tpch", "splits-per-node"),
("tpcds", "split-count")):
+ for scale, splits in ((1, 10), (100, 100), (1000, 1000), (10000,
10000)):
+ for override in (None, 7, 1):
+ with self.subTest(benchmark=benchmark, scale=scale,
override=override):
+ name = f"{benchmark}-{scale}-{override}"
+ self.results = self.root / name
+ self.calls = self.root / f"{name}.jsonl"
+ options = ("-s", str(scale))
+ if override is not None:
+ options += ("--splits", str(override))
+ # Complete every import, then stop before running
benchmark queries.
+ result = self.run_benchmark("analyze_error", *options,
benchmark=benchmark)
+ self.assertNotEqual(0, result.returncode)
+ expected = override if override is not None else splits
+ calls = self.statements()
+ catalogs = {}
+ for sql in calls:
+ if sql.startswith("CREATE CATALOG"):
+ name = re.search(r"CREATE CATALOG `([^`]+)`",
sql).group(1)
+ count =
re.search(rf"'trino\.{benchmark}\.{property_name}'='(\d+)'", sql)
+ catalogs[name] = int(count.group(1))
+ if benchmark == "tpch":
+
self.assertIn("'trino.tpch.column-naming'='STANDARD'", sql)
+
self.assertIn("'trino.tpch.double-type-mapping'='DECIMAL'", sql)
+ self.assertEqual(1 if expected == 1 else 2,
len(catalogs))
+ small = {"region", "nation"} if benchmark == "tpch"
else tpcds_small
+ if benchmark == "tpcds" and scale == 1:
+ small = small | tpcds_sf1_small
+ inserts = [sql for sql in calls if "INSERT INTO" in
sql]
+ self.assertEqual(8 if benchmark == "tpch" else 24,
len(inserts))
+ log = (self.results / "prepare.log").read_text()
+ for sql in inserts:
+ table = re.search(r"INSERT INTO `([^`]+)`",
sql).group(1)
+ source = re.search(r"FROM
`([^`]+)`\.`sf(\d+)`\.`([^`]+)`", sql)
+ self.assertEqual((str(scale), table),
source.group(2, 3))
+ table_splits = 1 if table in small else expected
+ self.assertEqual(table_splits,
catalogs[source.group(1)], table)
+ self.assertIn(f"{table} ({table_splits} generator
splits)", log)
+ self.assertIn("Statistics collection failed", log)
+ self.assertFalse((self.results /
"result.csv").exists())
+ ddl = TOOLS_ROOT /
f"{benchmark}-tools/ddl/create-{benchmark}-tables-sf{scale}.sql"
+ expected_ddl = "\n".join(line for line in
ddl.read_text().splitlines()
+ if not
line.lstrip().startswith("--")).strip()
+ self.assertEqual(expected_ddl, next(sql for sql in
calls if "create table" in sql.lower()))
+
+ def test_preparation_errors_are_saved_in_log(self):
+ scenarios = {"existing_database": "Database already exists",
+ "catalog_error": "Generator plugin is unavailable",
+ "ddl_error": "Table creation failed",
+ "desc_error": "Cannot describe target table",
+ "analyze_error": "Statistics collection failed"}
+ for benchmark in ("ssb", "tpch", "tpcds"):
+ for scenario, error in scenarios.items():
+ with self.subTest(benchmark=benchmark, scenario=scenario):
+ self.results = self.root / f"{benchmark}-{scenario}"
+ self.calls = self.root / f"{benchmark}-{scenario}.jsonl"
+ result = self.run_benchmark(scenario, benchmark=benchmark)
+ self.assertNotEqual(0, result.returncode)
+ self.assertIn(error, (self.results /
"prepare.log").read_text())
+ self.assertIn(str(self.results), result.stderr)
+ self.assertFalse((self.results / "result.csv").exists())
+ self.assertFalse(any("VERSION()" in sql for sql in
self.statements()))
+
+ def test_single_split_catalog_error_prevents_imports(self):
+ for benchmark in ("tpch", "tpcds"):
+ with self.subTest(benchmark=benchmark):
+ self.results = self.root / benchmark
+ self.calls = self.root / f"{benchmark}.jsonl"
+ result = self.run_benchmark("single_catalog_error",
benchmark=benchmark)
+ self.assertNotEqual(0, result.returncode)
+ self.assertIn("Generator plugin is unavailable", (self.results
/ "prepare.log").read_text())
+ self.assertFalse(any("INSERT INTO" in sql for sql in
self.statements()))
+ self.assertFalse((self.results / "result.csv").exists())
+
+ def test_invalid_splits_are_rejected_before_connecting(self):
+ for benchmark in ("tpch", "tpcds"):
+ for splits in ("", "0", "-1", "1.5", "abc", "2147483648",
"9223372036854775808"):
+ with self.subTest(benchmark=benchmark, splits=splits):
+ result = self.run_benchmark("", "--splits", splits,
benchmark=benchmark)
+ self.assertNotEqual(0, result.returncode)
+ self.assertFalse(self.calls.exists())
+ result = self.run_benchmark("", "--splits", "10", benchmark="ssb")
+ self.assertNotEqual(0, result.returncode)
+ self.assertFalse(self.calls.exists())
+
+ def test_query_failure_is_reported_and_stops_the_suite(self):
+ result = self.run_benchmark("query_error", "--queries-only")
+ self.assertNotEqual(0, result.returncode)
+ self.assertEqual(2, len(self.statements()))
+ self.assertIn("Query execution failed", (self.results /
"ssb/q1.1.cold.err").read_text())
+ self.assertNotIn("SSB completed", result.stdout)
+
+ def test_queries_only_does_not_modify_tables(self):
+ result = self.run_benchmark("", "--queries-only", "--mode", "flat")
+ self.assertEqual(0, result.returncode, result.stdout + result.stderr)
+ self.assertTrue(all(sql.startswith("SELECT") for sql in
self.statements()))
+ self.assertFalse((self.results / "ssb").exists())
+ self.assertEqual(39, len(list((self.results /
"ssb-flat").glob("*.out"))))
+
+ def test_invalid_scale_is_rejected_before_connecting(self):
+ result = self.run_benchmark("", "-s", "2")
+ self.assertNotEqual(0, result.returncode)
+ self.assertFalse(self.calls.exists())
+
+ def test_tpch_and_tpcds_complete_workflow(self):
+ for benchmark, table_count, query_count in (("tpch", 8, 22), ("tpcds",
24, 103)):
+ with self.subTest(benchmark=benchmark):
+ self.results = self.root / benchmark
+ self.calls = self.root / f"{benchmark}.jsonl"
+ result = self.run_benchmark(benchmark=benchmark)
+ self.assertEqual(0, result.returncode, result.stdout +
result.stderr)
+ calls = self.statements()
+ inserts = [sql for sql in calls if "INSERT INTO" in sql]
+ self.assertEqual(table_count, len(inserts))
+ self.assertFalse(any("_flat" in sql for sql in calls))
+ self.assertFalse((self.results /
"flat-row-counts.tsv").exists())
+ self.assertTrue(all(f"`{benchmark}_gen_benchmark_test`.`sf1`."
in sql or
+ f"`{benchmark}_one_benchmark_test`.`sf1`."
in sql for sql in inserts))
+ # Target columns and source projections must use the same
explicit names.
+ ordinary_inserts = [sql for sql in inserts if "INSERT INTO
`promotion`" not in sql]
+ self.assertTrue(all("(`lo_orderkey`,`lo_linenumber`)" in sql
for sql in ordinary_inserts))
+ self.assertTrue(all("SELECT `lo_orderkey`,`lo_linenumber`" in
sql for sql in ordinary_inserts))
+ analyses = [sql for sql in calls if sql.startswith("ANALYZE")]
+ self.assertEqual(1, len(analyses))
+ self.assertIn("WITH FULL WITH SYNC", analyses[0])
+ self.assertGreater(max(i for i, sql in enumerate(calls) if
sql.startswith("ANALYZE")),
+ max(i for i, sql in enumerate(calls) if
"INSERT INTO" in sql))
+ with (self.results / "result.csv").open() as file:
+ rows = list(csv.DictReader(file))
+ self.assertEqual(query_count, len(rows))
+ self.assertEqual({benchmark}, {row["suite"] for row in rows})
+ for row in rows:
+ self.assertEqual(min(int(row["hot1_ms"]),
int(row["hot2_ms"])), int(row["best_hot_ms"]))
+ self.assertEqual(query_count * 3,
len(list(self.results.glob("*/*.out"))))
+ entries = [json.loads(line) for line in
self.calls.read_text().splitlines()]
+ query_entries = entries[-query_count * 3:]
+ self.assertTrue(all("--init-command=SET
enable_sql_cache=false, enable_query_cache=false"
+ in entry["args"] for entry in
query_entries))
+ if benchmark == "tpch":
+ self.assertIn("'trino.tpch.column-naming'='STANDARD'",
calls[1])
+
self.assertIn("'trino.tpch.double-type-mapping'='DECIMAL'", calls[1])
+ self.assertTrue(any("create view revenue0" in sql for sql
in calls))
+ else:
+ self.assertIn("'trino.tpcds.split-count'='10'", calls[1])
+ promotion = next(sql for sql in inserts if "INSERT INTO
`promotion`" in sql)
+ self.assertIn("(`p_promo_sk`,`p_response_target`)",
promotion)
+ self.assertIn("SELECT `p_promo_sk`,`p_response_targe`",
promotion)
+ for query in (14, 23, 24, 39):
+ self.assertIn(f"q{query}_1", [row["query"] for row in
rows])
+
+ def test_tpc_queries_only_uses_requested_scale(self):
+ for benchmark in ("tpch", "tpcds"):
+ with self.subTest(benchmark=benchmark):
+ self.results = self.root / benchmark
+ self.calls = self.root / f"{benchmark}.jsonl"
+ result = self.run_benchmark("", "--queries-only", "-s",
"10000", benchmark=benchmark)
+ self.assertEqual(0, result.returncode, result.stdout +
result.stderr)
+ calls = self.statements()
+ self.assertFalse(any("INSERT INTO" in sql or
sql.startswith(("CREATE", "ANALYZE"))
+ for sql in calls))
+ query_path = (TOOLS_ROOT /
"tpcds-tools/queries/sf10000/query1.sql" if benchmark == "tpcds"
+ else TOOLS_ROOT / "tpch-tools/queries/q1.sql")
+ expected = "\n".join(line for line in
query_path.read_text().splitlines()
+ if not
line.lstrip().startswith("--")).strip()
+ self.assertEqual(expected, calls[1])
+
+ def test_tpc_preparation_errors_abort_before_queries(self):
+ for benchmark in ("tpch", "tpcds"):
+ for scenario in ("existing_database", "insert_error",
"unpublished"):
+ with self.subTest(benchmark=benchmark, scenario=scenario):
+ self.results = self.root / f"{benchmark}-{scenario}"
+ self.calls = self.root / f"{benchmark}-{scenario}.jsonl"
+ result = self.run_benchmark(scenario, benchmark=benchmark)
+ self.assertNotEqual(0, result.returncode)
+ self.assertFalse((self.results / "result.csv").exists())
+ self.assertFalse(any(sql.startswith("ANALYZE") for sql in
self.statements()))
+ expected_inserts = 0 if scenario == "existing_database"
else 1
+ self.assertEqual(expected_inserts, sum("INSERT INTO" in
sql for sql in self.statements()))
+
+ def test_ssb_flat_failures_abort_before_measuring_queries(self):
+ for scenario in ("flat_ddl_error", "flat_insert_error",
"flat_unpublished"):
+ with self.subTest(scenario=scenario):
+ self.results = self.root / scenario
+ self.calls = self.root / f"{scenario}.jsonl"
+ result = self.run_benchmark(scenario)
+ self.assertNotEqual(0, result.returncode)
+ self.assertFalse((self.results / "result.csv").exists())
+ self.assertNotIn("completed", result.stdout)
+ base_inserts = [sql for sql in self.statements()
+ if "INSERT INTO" in sql and "lineorder_flat"
not in sql]
+ self.assertEqual(5, len(base_inserts))
+ if scenario == "flat_ddl_error":
+ self.assertIn("Wide table creation failed", (self.results
/ "prepare.log").read_text())
+
+ def test_tpc_query_error_is_not_recorded_as_success(self):
+ for benchmark in ("tpch", "tpcds"):
+ with self.subTest(benchmark=benchmark):
+ self.results = self.root / benchmark
+ self.calls = self.root / f"{benchmark}.jsonl"
+ result = self.run_benchmark("query_error", "--queries-only",
benchmark=benchmark)
+ self.assertNotEqual(0, result.returncode)
+ with (self.results / "result.csv").open() as file:
+ self.assertEqual([], list(csv.DictReader(file)))
+ self.assertIn("Query execution failed", (self.results /
benchmark / "q1.cold.err").read_text())
+ self.assertEqual(2, len(self.statements()))
+
+ def
test_tpc_rejects_ssb_modes_and_invalid_arguments_before_connecting(self):
+ for benchmark in ("tpch", "tpcds"):
+ for options in (("--mode", "flat"), ("--mode", "both"), ("-s",
"2"),
+ ("-d", "invalid-name"), ("unexpected",)):
+ with self.subTest(benchmark=benchmark, options=options):
+ result = self.run_benchmark("", *options,
benchmark=benchmark)
+ self.assertNotEqual(0, result.returncode)
+ self.assertFalse(self.calls.exists())
+
+ def test_existing_result_directory_is_not_overwritten(self):
+ self.results.mkdir()
+ marker = self.results / "result.csv"
+ marker.write_text("previous result")
+ result = self.run_benchmark("", "--queries-only", benchmark="tpch")
+ self.assertNotEqual(0, result.returncode)
+ self.assertEqual("previous result", marker.read_text())
+ self.assertFalse(self.calls.exists())
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tools/ssb-tools/.gitignore b/tools/ssb-tools/.gitignore
new file mode 100644
index 00000000000..fbca2253799
--- /dev/null
+++ b/tools/ssb-tools/.gitignore
@@ -0,0 +1 @@
+results/
diff --git a/tools/ssb-tools/README.md b/tools/ssb-tools/README.md
index 8f857057f99..f6131fe479b 100644
--- a/tools/ssb-tools/README.md
+++ b/tools/ssb-tools/README.md
@@ -17,7 +17,97 @@ specific language governing permissions and limitations
under the License.
-->
-# Usage
+# Run SSB with one command
+
+The Trino SSB connector generates rows while Doris reads them. It uses the same
+`ssb-dbgen` distribution as the existing tools, with stdout connected to the
scan
+through a bounded pipe. There are no intermediate `.tbl` files, Stream Load
+requests, or separate Trino servers.
+
+## Install the generator plugin once
+
+Build on Linux with the same CPU architecture as the Doris nodes. The build
needs
+JDK 17, Maven, GCC, Make, curl, patch, and sha256sum:
+
+```bash
+./trino-ssb/build.sh
+```
+
+Copy the resulting **entire directory** `trino-ssb/target/ssb` into the
configured
+Trino plugin directory on **every FE and BE**. For current Doris installations:
+
+```bash
+cp -r trino-ssb/target/ssb /path/to/fe/plugins/trino_plugins/
+cp -r trino-ssb/target/ssb /path/to/be/plugins/trino_plugins/
+```
+
+Start or restart these nodes after installing the plugin. It consists of a JAR,
+the native `dbgen` executable, and its distribution file. The executable must
+remain executable. No paths or executable commands are accepted as catalog
properties.
+
+## Create, import, and benchmark
+
+Set the Doris connection in `conf/doris-cluster.conf`, then run:
+
+```bash
+./bin/run-ssb.sh -s 1 -d ssb_sf1
+```
+
+This creates a new database and its `ssb_gen_ssb_sf1` catalog, creates the five
+SSB tables, and imports them using `INSERT INTO ... SELECT ...` from the
catalog's
+`sf1` schema. It then builds `lineorder_flat`, collects statistics
synchronously,
+and runs all 13 SSB queries and all 13 SSB Flat queries three times each.
+The supported scale factors are 1, 100, and 1000. Use `--mode ssb` or `--mode
flat`
+to select one benchmark; the default is `both`.
+
+Each run writes a new `results/<timestamp>-<pid>/` directory with:
+
+- `result.csv`: first-run, two repeated-run, and best repeated-run times in
milliseconds.
+- `ssb/` and `ssb-flat/`: each query's output and errors.
+- `prepare.log` and `environment.txt`: import diagnostics and Doris settings.
+
+Timing includes the mysql client connection and result transfer, as in the old
+scripts. The `cold` column means the first execution; the script does not flush
+data caches. SQL result caching and BE query caching are disabled for every
measured
+session. A query or import error stops the run with a nonzero exit status.
+
+Preparation requires a **new database**, so rerunning a failed or completed
load
+cannot append duplicate data. Tables and the catalog are preserved for
inspection.
+To rerun only the queries against completed data:
+
+```bash
+./bin/run-ssb.sh -d ssb_sf1 --queries-only
+```
+
+Use `-c /path/to/cluster.conf` for a separate connection configuration, and
+`--result-dir /path/to/new-directory` for a custom output directory. Existing
+result directories are never overwritten.
+
+## Generator consistency and tests
+
+The connector keeps the old tools' default of ten `lineorder` generator
partitions.
+Each dimension uses one generator. Changing the original dbgen partition count
+changes some random streams, so partition count is intentionally fixed here.
+The date generator runs in UTC on every BE. The native build enables dbgen's
+existing 64-bit code path for large scale factors and avoids an out-of-bounds
+usage-counter update for its SSB-only random streams. Neither change replaces
+the data-generation algorithm.
+
+The existing benchmark SQL is preserved. In particular, SSB Q1.3 filters the
+generator's `d_weeknuminyear`, whereas SSB Flat Q1.3 uses `weekofyear()`;
these week
+definitions differ, so their aggregate results are not expected to match.
+
+The plugin build runs its Java tests, including full SF1 checksums of all five
tables,
+handle serialization, projected columns, large keys, producer failures, and
early
+cursor closure. Run script orchestration tests with:
+
+```bash
+python3 -m unittest discover -s ../benchmark/tests -v
+```
+
+The shared runner also supports [TPCH and TPCDS](../benchmark/README.md).
+
+# Original file-based workflow
These scripts are used to make ssb and ssb flat test.
The ssb flat data comes from ssb tables by way of 'INSERT INTO ... SELECT
...'.
diff --git a/tools/ssb-tools/bin/run-ssb.sh b/tools/ssb-tools/bin/run-ssb.sh
new file mode 100755
index 00000000000..c292279d460
--- /dev/null
+++ b/tools/ssb-tools/bin/run-ssb.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+set -Eeuo pipefail
+SCRIPT_ROOT=$(cd "$(dirname "$0")" && pwd)
+exec bash "${SCRIPT_ROOT}/../../benchmark/run-benchmark.sh" ssb "$@"
diff --git a/tools/ssb-tools/ddl/insert-ssb-flat.sql
b/tools/ssb-tools/ddl/insert-ssb-flat.sql
new file mode 100644
index 00000000000..b8e958deb1b
--- /dev/null
+++ b/tools/ssb-tools/ddl/insert-ssb-flat.sql
@@ -0,0 +1,30 @@
+-- Licensed to the Apache Software Foundation (ASF) under one
+-- or more contributor license agreements. See the NOTICE file
+-- distributed with this work for additional information
+-- regarding copyright ownership. The ASF licenses this file
+-- to you 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.
+
+INSERT INTO lineorder_flat
+SELECT
+ lo_orderdate, lo_orderkey, lo_linenumber, lo_custkey, lo_partkey,
lo_suppkey,
+ lo_orderpriority, lo_shippriority, lo_quantity, lo_extendedprice,
lo_ordtotalprice,
+ lo_discount, lo_revenue, lo_supplycost, lo_tax, lo_commitdate, lo_shipmode,
+ c_name, c_address, c_city, c_nation, c_region, c_phone, c_mktsegment,
+ s_name, s_address, s_city, s_nation, s_region, s_phone,
+ p_name, p_mfgr, p_category, p_brand, p_color, p_type, p_size, p_container
+FROM lineorder l
+INNER JOIN customer c ON c.c_custkey = l.lo_custkey
+INNER JOIN supplier s ON s.s_suppkey = l.lo_suppkey
+INNER JOIN part p ON p.p_partkey = l.lo_partkey
+WHERE lo_orderdate >= @YEAR@0101 AND lo_orderdate < @NEXT_YEAR@0101;
diff --git a/tools/ssb-tools/trino-ssb/.gitignore
b/tools/ssb-tools/trino-ssb/.gitignore
new file mode 100644
index 00000000000..2f7896d1d13
--- /dev/null
+++ b/tools/ssb-tools/trino-ssb/.gitignore
@@ -0,0 +1 @@
+target/
diff --git a/tools/ssb-tools/trino-ssb/build.sh
b/tools/ssb-tools/trino-ssb/build.sh
new file mode 100755
index 00000000000..f6ba55d9ea1
--- /dev/null
+++ b/tools/ssb-tools/trino-ssb/build.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+set -euo pipefail
+SSB_PLUGIN_ROOT=$(cd "$(dirname "$0")" && pwd)
+cd "${SSB_PLUGIN_ROOT}"
+mkdir -p target/native
+
+# The same generator distribution as ../bin/build-ssb-dbgen.sh. Pin its
contents
+# so a benchmark run cannot silently switch to a different data generator.
+archive=target/ssb-dbgen-linux.tar.gz
+if [[ ! -f "${archive}" ]]; then
+ curl --fail --location --retry 3 \
+
https://palo-cloud-repo-bd.bd.bcebos.com/baidu-doris-release/ssb-dbgen-linux.tar.gz
\
+ --output "${archive}"
+fi
+echo "9a28f5a24091fd55d7e2ea12850e0189d98d1e1c6361f84442353ebd7df8e0ef
${archive}" | sha256sum --check
+tar -xzf "${archive}" -C target/native --strip-components=1
+# The legacy source contains trailing whitespace; keep that out of the patch.
+patch -l -d target/native -p1 <dbgen-stream.patch
+parallel=$(($(nproc) / 4))
+parallel=$((parallel > 0 ? parallel : 1))
+make -C target/native -j"${parallel}" CC="${CC:-gcc} -std=gnu89 -fcommon" dbgen
+mvn -B -f pom.xml -T "${parallel}" package
+
+mkdir -p target/ssb
+cp target/trino-ssb.jar target/native/dbgen target/native/dists.dss target/ssb/
+cp target/native/README target/native/TPCH_README target/ssb/
+echo "SSB plugin: ${SSB_PLUGIN_ROOT}/target/ssb"
+echo 'Install this directory under plugins/trino_plugins/ on every FE and BE
before starting Doris.'
diff --git a/tools/ssb-tools/trino-ssb/dbgen-stream.patch
b/tools/ssb-tools/trino-ssb/dbgen-stream.patch
new file mode 100644
index 00000000000..30f5d895862
--- /dev/null
+++ b/tools/ssb-tools/trino-ssb/dbgen-stream.patch
@@ -0,0 +1,62 @@
+--- a/print.c
++++ b/print.c
+@@ -35,41 +35,2 @@
+- char upath[128];
+- FILE *res;
+-
+- if (updates)
+- {
+- if (update > 0) /* updates */
+- if ( insert_segments )
+- {
+- int this_segment;
+- if(strcmp(tdefs[table].name,"orders.tbl"))
+- this_segment=++insert_orders_segment;
+- else
+- this_segment=++insert_lineitem_segment;
+- sprintf(upath, "%s%c%s.u%d.%d",
+- env_config(PATH_TAG, PATH_DFLT),
+- PATH_SEP, tdefs[table].name,
update%10000,this_segment);
+- }
+- else
+- {
+- sprintf(upath, "%s%c%s.u%d",
+- env_config(PATH_TAG, PATH_DFLT),
+- PATH_SEP, tdefs[table].name, update);
+- }
+- else /* deletes */
+- if ( delete_segments )
+- {
+- ++delete_segment;
+- sprintf(upath, "%s%cdelete.u%d.%d",
+- env_config(PATH_TAG, PATH_DFLT),
PATH_SEP, -update%10000,
+- delete_segment);
+- }
+- else
+- {
+- sprintf(upath, "%s%cdelete.%d",
+- env_config(PATH_TAG, PATH_DFLT), PATH_SEP,
-update);
+- }
+- return(fopen(upath, "w"));
+- }
+- res = tbl_open(table, "w");
+- OPEN_CHECK(res, tdefs[table].name);
+- return(res);
++ /* Each connector scan generates exactly one table into its stdout pipe.
*/
++ return stdout;
+--- a/rnd.c
++++ b/rnd.c
+@@ -28 +28,4 @@
+- Seed[stream].usage += 1;
++ /* UnifInt maps SSB-only streams 97 and 98 to stream 0. They have no
++ * per-row usage counter; do not write beyond the Seed array. */
++ if (stream >= 0 && stream <= MAX_STREAM)
++ Seed[stream].usage += 1;
+--- a/config.h
++++ b/config.h
+@@ -100,0 +101,5 @@
++/* Use the existing 64-bit generator path, including large RNG skip counts. */
++#define SUPPORT_64BITS
++#define DSS_HUGE long long
++#define HUGE_COUNT 1
++#define HUGE_FORMAT "%lld"
diff --git a/tools/ssb-tools/trino-ssb/pom.xml
b/tools/ssb-tools/trino-ssb/pom.xml
new file mode 100644
index 00000000000..70003cc2f48
--- /dev/null
+++ b/tools/ssb-tools/trino-ssb/pom.xml
@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.apache.doris</groupId>
+ <artifactId>trino-ssb</artifactId>
+ <version>1.0-SNAPSHOT</version>
+ <properties>
+ <maven.compiler.release>17</maven.compiler.release>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+ <dependencies>
+ <!-- Supplied by Doris's Trino plugin classloader; never bundle a
second SPI. -->
+ <dependency>
+ <groupId>io.trino</groupId>
+ <artifactId>trino-spi</artifactId>
+ <version>435</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ <version>5.10.2</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>io.airlift</groupId>
+ <artifactId>json</artifactId>
+ <version>239</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <version>2.15.3</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <finalName>trino-ssb</finalName>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-checkstyle-plugin</artifactId>
+ <version>3.1.2</version>
+ <dependencies>
+ <dependency>
+ <groupId>com.puppycrawl.tools</groupId>
+ <artifactId>checkstyle</artifactId>
+ <version>9.3</version>
+ </dependency>
+ </dependencies>
+ <configuration>
+
<configLocation>${project.basedir}/../../../fe/check/checkstyle/checkstyle.xml</configLocation>
+
<propertyExpansion>config_loc=${project.basedir}/../../../fe/check/checkstyle</propertyExpansion>
+
<includeTestSourceDirectory>true</includeTestSourceDirectory>
+ <consoleOutput>true</consoleOutput>
+ </configuration>
+ <executions>
+ <execution>
+ <phase>validate</phase>
+ <goals>
+ <goal>check</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>3.13.0</version>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <version>3.2.5</version>
+ <configuration>
+ <systemPropertyVariables>
+
<ssb.test.dbgen>${project.build.directory}/native/dbgen</ssb.test.dbgen>
+ </systemPropertyVariables>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git
a/tools/ssb-tools/trino-ssb/src/main/java/org/apache/doris/tools/ssb/SsbConnector.java
b/tools/ssb-tools/trino-ssb/src/main/java/org/apache/doris/tools/ssb/SsbConnector.java
new file mode 100644
index 00000000000..1866c58672e
--- /dev/null
+++
b/tools/ssb-tools/trino-ssb/src/main/java/org/apache/doris/tools/ssb/SsbConnector.java
@@ -0,0 +1,226 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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.
+
+package org.apache.doris.tools.ssb;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.trino.spi.HostAddress;
+import io.trino.spi.SplitWeight;
+import io.trino.spi.connector.ColumnHandle;
+import io.trino.spi.connector.ColumnMetadata;
+import io.trino.spi.connector.Connector;
+import io.trino.spi.connector.ConnectorMetadata;
+import io.trino.spi.connector.ConnectorRecordSetProvider;
+import io.trino.spi.connector.ConnectorSession;
+import io.trino.spi.connector.ConnectorSplit;
+import io.trino.spi.connector.ConnectorSplitManager;
+import io.trino.spi.connector.ConnectorTableHandle;
+import io.trino.spi.connector.ConnectorTableMetadata;
+import io.trino.spi.connector.ConnectorTransactionHandle;
+import io.trino.spi.connector.FixedSplitSource;
+import io.trino.spi.connector.SchemaTableName;
+import io.trino.spi.connector.SchemaTablePrefix;
+import io.trino.spi.transaction.IsolationLevel;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+public final class SsbConnector implements Connector {
+ // Keep the original tool's ten dbgen partitions: changing -C changes some
random streams.
+ static final int LINEORDER_PARTS = 10;
+ private static final List<String> SCHEMAS = List.of("sf1", "sf100",
"sf1000");
+ private final Path dbgen;
+
+ SsbConnector(Path dbgen) {
+ this.dbgen = dbgen;
+ }
+
+ public enum Transaction implements ConnectorTransactionHandle {
+ INSTANCE
+ }
+
+ public record Table(@JsonProperty("schema") String schema,
@JsonProperty("name") String name)
+ implements ConnectorTableHandle {
+ @JsonCreator
+ public Table {
+ if (!SCHEMAS.contains(schema)) {
+ throw new IllegalArgumentException("Supported SSB schemas: " +
SCHEMAS);
+ }
+ SsbTable.fromName(name);
+ }
+ }
+
+ public record Column(@JsonProperty("index") int index) implements
ColumnHandle {
+ @JsonCreator
+ public Column {
+ }
+ }
+
+ public record Split(@JsonProperty("part") int part,
@JsonProperty("totalParts") int totalParts)
+ implements ConnectorSplit {
+ @JsonCreator
+ public Split {
+ if ((totalParts != 1 && totalParts != LINEORDER_PARTS) || part < 1
|| part > totalParts) {
+ throw new IllegalArgumentException("Invalid SSB partition " +
part + "/" + totalParts);
+ }
+ }
+
+ @Override
+ @JsonIgnore
+ public boolean isRemotelyAccessible() {
+ return true;
+ }
+
+ @Override
+ @JsonIgnore
+ public List<HostAddress> getAddresses() {
+ return List.of();
+ }
+
+ @Override
+ @JsonIgnore
+ public Object getInfo() {
+ return Map.of("part", part, "totalParts", totalParts);
+ }
+
+ @Override
+ @JsonIgnore
+ public long getRetainedSizeInBytes() {
+ return 24; // object header and two int fields
+ }
+
+ @Override
+ @JsonIgnore
+ public SplitWeight getSplitWeight() {
+ return SplitWeight.standard();
+ }
+ }
+
+ @Override
+ public ConnectorTransactionHandle beginTransaction(IsolationLevel
isolation, boolean readOnly, boolean autoCommit) {
+ return Transaction.INSTANCE;
+ }
+
+ @Override
+ public ConnectorMetadata getMetadata(ConnectorSession session,
ConnectorTransactionHandle transaction) {
+ return new Metadata();
+ }
+
+ @Override
+ public ConnectorSplitManager getSplitManager() {
+ return new ConnectorSplitManager() {
+ @Override
+ public io.trino.spi.connector.ConnectorSplitSource
getSplits(ConnectorTransactionHandle transaction,
+ ConnectorSession session, ConnectorTableHandle table,
io.trino.spi.connector.DynamicFilter filter,
+ io.trino.spi.connector.Constraint constraint) {
+ int count = SsbTable.fromName(((Table) table).name()) ==
SsbTable.LINEORDER ? LINEORDER_PARTS : 1;
+ List<Split> splits = new ArrayList<>();
+ for (int part = 1; part <= count; part++) {
+ splits.add(new Split(part, count));
+ }
+ return new FixedSplitSource(splits);
+ }
+ };
+ }
+
+ @Override
+ public ConnectorRecordSetProvider getRecordSetProvider() {
+ return new ConnectorRecordSetProvider() {
+ @Override
+ public io.trino.spi.connector.RecordSet
getRecordSet(ConnectorTransactionHandle transaction,
+ ConnectorSession session, ConnectorSplit split,
ConnectorTableHandle table,
+ List<? extends ColumnHandle> columns) {
+ return new SsbRecordSet(dbgen, (Table) table, (Split) split,
+ columns.stream().map(column -> ((Column)
column).index()).toList());
+ }
+ };
+ }
+
+ private static final class Metadata implements ConnectorMetadata {
+ @Override
+ public List<String> listSchemaNames(ConnectorSession session) {
+ return SCHEMAS;
+ }
+
+ @Override
+ public ConnectorTableHandle getTableHandle(ConnectorSession session,
SchemaTableName table) {
+ if (!SCHEMAS.contains(table.getSchemaName())) {
+ return null;
+ }
+ for (SsbTable ssbTable : SsbTable.values()) {
+ if (ssbTable.tableName().equals(table.getTableName())) {
+ return new Table(table.getSchemaName(),
table.getTableName());
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public List<SchemaTableName> listTables(ConnectorSession session,
Optional<String> schema) {
+ List<SchemaTableName> tables = new ArrayList<>();
+ for (String name : SCHEMAS) {
+ if (schema.isEmpty() || schema.get().equals(name)) {
+ for (SsbTable table : SsbTable.values()) {
+ tables.add(new SchemaTableName(name,
table.tableName()));
+ }
+ }
+ }
+ return tables;
+ }
+
+ @Override
+ public ConnectorTableMetadata getTableMetadata(ConnectorSession
session, ConnectorTableHandle handle) {
+ Table table = (Table) handle;
+ return new ConnectorTableMetadata(new
SchemaTableName(table.schema(), table.name()),
+ SsbTable.fromName(table.name()).columns);
+ }
+
+ @Override
+ public Map<String, ColumnHandle> getColumnHandles(ConnectorSession
session, ConnectorTableHandle handle) {
+ List<ColumnMetadata> columns = getTableMetadata(session,
handle).getColumns();
+ Map<String, ColumnHandle> result = new LinkedHashMap<>();
+ for (int i = 0; i < columns.size(); i++) {
+ result.put(columns.get(i).getName(), new Column(i));
+ }
+ return result;
+ }
+
+ @Override
+ public ColumnMetadata getColumnMetadata(ConnectorSession session,
ConnectorTableHandle table,
+ ColumnHandle column) {
+ return getTableMetadata(session, table).getColumns().get(((Column)
column).index());
+ }
+
+ @Override
+ public Map<SchemaTableName, List<ColumnMetadata>>
listTableColumns(ConnectorSession session,
+ SchemaTablePrefix prefix) {
+ Map<SchemaTableName, List<ColumnMetadata>> result = new
LinkedHashMap<>();
+ for (SchemaTableName table : listTables(session,
prefix.getSchema())) {
+ if (prefix.matches(table)) {
+ result.put(table,
SsbTable.fromName(table.getTableName()).columns);
+ }
+ }
+ return result;
+ }
+ }
+}
diff --git
a/tools/ssb-tools/trino-ssb/src/main/java/org/apache/doris/tools/ssb/SsbPlugin.java
b/tools/ssb-tools/trino-ssb/src/main/java/org/apache/doris/tools/ssb/SsbPlugin.java
new file mode 100644
index 00000000000..4f806e0c685
--- /dev/null
+++
b/tools/ssb-tools/trino-ssb/src/main/java/org/apache/doris/tools/ssb/SsbPlugin.java
@@ -0,0 +1,56 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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.
+
+package org.apache.doris.tools.ssb;
+
+import io.trino.spi.Plugin;
+import io.trino.spi.connector.Connector;
+import io.trino.spi.connector.ConnectorContext;
+import io.trino.spi.connector.ConnectorFactory;
+
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+
+public final class SsbPlugin implements Plugin {
+ @Override
+ public Iterable<ConnectorFactory> getConnectorFactories() {
+ return List.of(new ConnectorFactory() {
+ @Override
+ public String getName() {
+ return "ssb";
+ }
+
+ @Override
+ public Connector create(String catalogName, Map<String, String>
config, ConnectorContext context) {
+ if (!config.isEmpty()) {
+ throw new IllegalArgumentException("SSB has no connector
properties: " + config.keySet());
+ }
+ try {
+ // Only the operator-installed binary next to this JAR is
executable. Catalog
+ // properties cannot select a command, a path, or
arbitrary dbgen arguments.
+ Path pluginJar =
Path.of(SsbPlugin.class.getProtectionDomain()
+ .getCodeSource().getLocation().toURI());
+ return new
SsbConnector(pluginJar.getParent().resolve("dbgen"));
+ } catch (URISyntaxException e) {
+ throw new IllegalStateException("Cannot locate the SSB
plugin directory", e);
+ }
+ }
+ });
+ }
+}
diff --git
a/tools/ssb-tools/trino-ssb/src/main/java/org/apache/doris/tools/ssb/SsbRecordSet.java
b/tools/ssb-tools/trino-ssb/src/main/java/org/apache/doris/tools/ssb/SsbRecordSet.java
new file mode 100644
index 00000000000..803eb29ef75
--- /dev/null
+++
b/tools/ssb-tools/trino-ssb/src/main/java/org/apache/doris/tools/ssb/SsbRecordSet.java
@@ -0,0 +1,198 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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.
+
+package org.apache.doris.tools.ssb;
+
+import io.airlift.slice.Slice;
+import io.airlift.slice.Slices;
+import io.trino.spi.connector.RecordCursor;
+import io.trino.spi.connector.RecordSet;
+import io.trino.spi.type.Type;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+final class SsbRecordSet implements RecordSet {
+ private final Path dbgen;
+ private final SsbConnector.Table table;
+ private final SsbConnector.Split split;
+ private final List<Integer> columns;
+ private final List<Type> types;
+
+ SsbRecordSet(Path dbgen, SsbConnector.Table table, SsbConnector.Split
split, List<Integer> columns) {
+ this.dbgen = dbgen;
+ this.table = table;
+ this.split = split;
+ this.columns = List.copyOf(columns);
+ this.types = columns.stream()
+ .map(index ->
SsbTable.fromName(table.name()).columns.get(index).getType()).toList();
+ }
+
+ @Override
+ public List<Type> getColumnTypes() {
+ return types;
+ }
+
+ @Override
+ public RecordCursor cursor() {
+ return new Cursor();
+ }
+
+ private final class Cursor implements RecordCursor {
+ private final Process process;
+ private final BufferedReader reader;
+ private final int fieldCount =
SsbTable.fromName(table.name()).columns.size();
+ private long completedBytes;
+ private long readTimeNanos;
+ private String[] fields;
+ private boolean closed;
+
+ Cursor() {
+ ProcessBuilder builder = new ProcessBuilder(dbgen.toString(),
"-q", "-s", table.schema().substring(2),
+ "-T", SsbTable.fromName(table.name()).dbgenOption,
+ "-b", dbgen.getParent().resolve("dists.dss").toString(),
+ "-C", Integer.toString(split.totalParts()), "-S",
Integer.toString(split.part()));
+ // mk_date uses localtime. All BEs must generate the same date
dimension.
+ builder.environment().put("TZ", "UTC");
+ // Drain diagnostics without an extra thread or an unbounded
stderr buffer.
+ builder.redirectError(ProcessBuilder.Redirect.INHERIT);
+ try {
+ process = builder.start();
+ reader = new BufferedReader(new
InputStreamReader(process.getInputStream(), StandardCharsets.US_ASCII),
+ 64 * 1024);
+ } catch (IOException e) {
+ throw new UncheckedIOException("Cannot start the
operator-installed SSB generator: " + dbgen, e);
+ }
+ }
+
+ @Override
+ public boolean advanceNextPosition() {
+ if (closed) {
+ return false;
+ }
+ long start = System.nanoTime();
+ try {
+ String line = reader.readLine();
+ if (line == null) {
+ int status = process.waitFor();
+ close();
+ if (status != 0) {
+ throw new IllegalStateException("SSB generator exited
with " + status
+ + " for " + table + ", partition " + split);
+ }
+ return false;
+ }
+ fields = line.split("\\|", -1);
+ if (fields.length != fieldCount + 1 ||
!fields[fieldCount].isEmpty()) {
+ throw new IllegalStateException("Invalid SSB row for " +
table + ", partition " + split);
+ }
+ completedBytes += line.length() + 1;
+ return true;
+ } catch (IOException e) {
+ close();
+ throw new UncheckedIOException("Cannot read SSB data for " +
table + ", partition " + split, e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ close();
+ throw new IllegalStateException("SSB generation interrupted",
e);
+ } catch (RuntimeException e) {
+ close();
+ throw e;
+ } finally {
+ readTimeNanos += System.nanoTime() - start;
+ }
+ }
+
+ @Override
+ public long getCompletedBytes() {
+ return completedBytes;
+ }
+
+ @Override
+ public long getReadTimeNanos() {
+ return readTimeNanos;
+ }
+
+ @Override
+ public Type getType(int field) {
+ return types.get(field);
+ }
+
+ @Override
+ public long getLong(int field) {
+ return Long.parseLong(fields[columns.get(field)]);
+ }
+
+ @Override
+ public Slice getSlice(int field) {
+ return Slices.utf8Slice(fields[columns.get(field)]);
+ }
+
+ @Override
+ public boolean isNull(int field) {
+ columns.get(field);
+ return false;
+ }
+
+ @Override
+ public boolean getBoolean(int field) {
+ throw new UnsupportedOperationException("SSB has no Boolean
columns");
+ }
+
+ @Override
+ public double getDouble(int field) {
+ throw new UnsupportedOperationException("SSB has no floating point
columns");
+ }
+
+ @Override
+ public Object getObject(int field) {
+ throw new UnsupportedOperationException("SSB has no object
columns");
+ }
+
+ @Override
+ public void close() {
+ if (closed) {
+ return;
+ }
+ closed = true;
+ // LIMIT, cancellation and scanner errors must also stop the
producer. Killing before
+ // closing the reader unblocks a producer waiting for space in the
pipe.
+ process.destroy();
+ try {
+ if (!process.waitFor(2, TimeUnit.SECONDS)) {
+ process.destroyForcibly();
+ process.waitFor();
+ }
+ } catch (InterruptedException e) {
+ process.destroyForcibly();
+ Thread.currentThread().interrupt();
+ } finally {
+ try {
+ reader.close();
+ } catch (IOException e) {
+ throw new UncheckedIOException("Cannot close the SSB
generator pipe", e);
+ }
+ }
+ }
+ }
+}
diff --git
a/tools/ssb-tools/trino-ssb/src/main/java/org/apache/doris/tools/ssb/SsbTable.java
b/tools/ssb-tools/trino-ssb/src/main/java/org/apache/doris/tools/ssb/SsbTable.java
new file mode 100644
index 00000000000..85174519631
--- /dev/null
+++
b/tools/ssb-tools/trino-ssb/src/main/java/org/apache/doris/tools/ssb/SsbTable.java
@@ -0,0 +1,60 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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.
+
+package org.apache.doris.tools.ssb;
+
+import io.trino.spi.connector.ColumnMetadata;
+import io.trino.spi.type.BigintType;
+import io.trino.spi.type.VarcharType;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/** Column order is the ssb-dbgen output order, also used by the existing SSB
DDL. */
+enum SsbTable {
+ CUSTOMER("c", "c_custkey", "c_name:s", "c_address:s", "c_city:s",
"c_nation:s", "c_region:s",
+ "c_phone:s", "c_mktsegment:s"),
+ PART("p", "p_partkey", "p_name:s", "p_mfgr:s", "p_category:s",
"p_brand:s", "p_color:s",
+ "p_type:s", "p_size", "p_container:s"),
+ SUPPLIER("s", "s_suppkey", "s_name:s", "s_address:s", "s_city:s",
"s_nation:s", "s_region:s", "s_phone:s"),
+ DATES("d", "d_datekey", "d_date:s", "d_dayofweek:s", "d_month:s",
"d_year", "d_yearmonthnum",
+ "d_yearmonth:s", "d_daynuminweek", "d_daynuminmonth",
"d_daynuminyear", "d_monthnuminyear",
+ "d_weeknuminyear", "d_sellingseason:s", "d_lastdayinweekfl",
"d_lastdayinmonthfl", "d_holidayfl",
+ "d_weekdayfl"),
+ LINEORDER("l", "lo_orderkey", "lo_linenumber", "lo_custkey", "lo_partkey",
"lo_suppkey", "lo_orderdate",
+ "lo_orderpriority:s", "lo_shippriority", "lo_quantity",
"lo_extendedprice", "lo_ordtotalprice",
+ "lo_discount", "lo_revenue", "lo_supplycost", "lo_tax",
"lo_commitdate", "lo_shipmode:s");
+
+ final String dbgenOption;
+ final List<ColumnMetadata> columns;
+
+ SsbTable(String dbgenOption, String... columns) {
+ this.dbgenOption = dbgenOption;
+ this.columns = Arrays.stream(columns).map(column ->
column.endsWith(":s")
+ ? new ColumnMetadata(column.substring(0, column.length() - 2),
VarcharType.VARCHAR)
+ : new ColumnMetadata(column, BigintType.BIGINT)).toList();
+ }
+
+ String tableName() {
+ return name().toLowerCase(Locale.ROOT);
+ }
+
+ static SsbTable fromName(String name) {
+ return valueOf(name.toUpperCase(Locale.ROOT));
+ }
+}
diff --git
a/tools/ssb-tools/trino-ssb/src/main/resources/META-INF/services/io.trino.spi.Plugin
b/tools/ssb-tools/trino-ssb/src/main/resources/META-INF/services/io.trino.spi.Plugin
new file mode 100644
index 00000000000..835218075ab
--- /dev/null
+++
b/tools/ssb-tools/trino-ssb/src/main/resources/META-INF/services/io.trino.spi.Plugin
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+org.apache.doris.tools.ssb.SsbPlugin
diff --git
a/tools/ssb-tools/trino-ssb/src/test/java/org/apache/doris/tools/ssb/SsbConnectorTest.java
b/tools/ssb-tools/trino-ssb/src/test/java/org/apache/doris/tools/ssb/SsbConnectorTest.java
new file mode 100644
index 00000000000..b2d676d11aa
--- /dev/null
+++
b/tools/ssb-tools/trino-ssb/src/test/java/org/apache/doris/tools/ssb/SsbConnectorTest.java
@@ -0,0 +1,186 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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.
+
+package org.apache.doris.tools.ssb;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.airlift.json.ObjectMapperProvider;
+import io.trino.spi.Plugin;
+import io.trino.spi.connector.ConnectorMetadata;
+import io.trino.spi.connector.Constraint;
+import io.trino.spi.connector.DynamicFilter;
+import io.trino.spi.connector.RecordCursor;
+import io.trino.spi.connector.RecordSet;
+import io.trino.spi.connector.SchemaTableName;
+import io.trino.spi.type.BigintType;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.MessageDigest;
+import java.util.HexFormat;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.ServiceLoader;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.IntStream;
+
+class SsbConnectorTest {
+ private final Path dbgen = Path.of(System.getProperty("ssb.test.dbgen"));
+
+ @TempDir
+ Path temporary;
+
+ @Test
+ void metadataAndPluginDiscovery() throws Exception {
+ Plugin plugin = ServiceLoader.load(Plugin.class).stream()
+ .map(ServiceLoader.Provider::get).filter(p -> p instanceof
SsbPlugin).findFirst().orElseThrow();
+ var factory = plugin.getConnectorFactories().iterator().next();
+ Assertions.assertEquals("ssb", factory.getName());
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
factory.create("ssb", Map.of("command", "anything"), null));
+
+ var connector = new SsbConnector(dbgen);
+ ConnectorMetadata metadata = connector.getMetadata(null,
SsbConnector.Transaction.INSTANCE);
+ Assertions.assertEquals(List.of("sf1", "sf100", "sf1000"),
metadata.listSchemaNames(null));
+ Assertions.assertEquals(15, metadata.listTables(null,
Optional.empty()).size());
+ Assertions.assertTrue(metadata.listTables(null,
Optional.of("sf2")).isEmpty());
+ Assertions.assertNull(metadata.getTableHandle(null, new
SchemaTableName("sf2", "customer")));
+ Assertions.assertNull(metadata.getTableHandle(null, new
SchemaTableName("sf1", "unknown")));
+ var handle = metadata.getTableHandle(null, new SchemaTableName("sf1",
"lineorder"));
+ Assertions.assertNotNull(handle);
+ Assertions.assertEquals(17, metadata.getColumnHandles(null,
handle).size());
+ try (var splits =
connector.getSplitManager().getSplits(SsbConnector.Transaction.INSTANCE, null,
handle,
+ DynamicFilter.EMPTY, Constraint.alwaysTrue())) {
+ Assertions.assertEquals(10,
splits.getNextBatch(100).get().getSplits().size());
+ Assertions.assertTrue(splits.isFinished());
+ }
+ }
+
+ @Test
+ void handlesRoundTripBetweenFrontendAndBackend() throws Exception {
+ // Trino disables Jackson's automatic field/getter/constructor
discovery.
+ ObjectMapper mapper = new ObjectMapperProvider().get();
+ for (Object handle : List.of(new SsbConnector.Table("sf1000",
"lineorder"),
+ new SsbConnector.Column(16), new SsbConnector.Split(10, 10),
SsbConnector.Transaction.INSTANCE)) {
+ Assertions.assertEquals(handle,
mapper.readValue(mapper.writeValueAsBytes(handle), handle.getClass()));
+ }
+ Assertions.assertThrows(IllegalArgumentException.class, () -> new
SsbConnector.Table("sf1; anything", "lineorder"));
+ Assertions.assertThrows(IllegalArgumentException.class, () -> new
SsbConnector.Split(0, 10));
+ }
+
+ @Test
+ void projectionPreservesColumnOrderAndTypes() {
+ RecordSet records = new SsbRecordSet(dbgen, new
SsbConnector.Table("sf1", "customer"),
+ new SsbConnector.Split(1, 1), List.of(7, 0, 1));
+ try (RecordCursor cursor = records.cursor()) {
+ Assertions.assertTrue(cursor.advanceNextPosition());
+ Assertions.assertEquals("BUILDING",
cursor.getSlice(0).toStringUtf8());
+ Assertions.assertEquals(1, cursor.getLong(1));
+ Assertions.assertEquals("Customer#000000001",
cursor.getSlice(2).toStringUtf8());
+ Assertions.assertFalse(cursor.isNull(0));
+ Assertions.assertTrue(cursor.getCompletedBytes() > 0);
+ Assertions.assertTrue(cursor.getReadTimeNanos() > 0);
+ }
+ }
+
+ @Test
+ void dimensionsMatchTheExistingGenerator() throws Exception {
+ // SHA-256 of the existing tool's individual-table SF1 output
(TZ=UTC), including
+ // its trailing pipe and newline. Check all rows, not just a generated
sample.
+ verifyTable(SsbTable.CUSTOMER, 30000,
"e84ecfcde0f73bbc48f710ffe1c6d340321e875171013b272cbccee45f1d3cbc");
+ verifyTable(SsbTable.PART, 200000,
"9fb468e8a3e6ca9f5b6f841cf5a81276315fd241fbfdb35c8839328729bb816b");
+ verifyTable(SsbTable.SUPPLIER, 2000,
"90b636621dc449bedce76f347af5802701db6f6334d209f002d73f4044cf9404");
+ verifyTable(SsbTable.DATES, 2556,
"7057acc5ebc0b92225a4937488028767f46e9f8719126a8e29d4bf164e5461d1");
+ }
+
+ private void verifyTable(SsbTable table, long expectedRows, String
expectedDigest) throws Exception {
+ verifyTable(table, 1, expectedRows, expectedDigest);
+ }
+
+ @Test
+ void lineorderMatchesTheExistingTenPartitionLoad() throws Exception {
+ verifyTable(SsbTable.LINEORDER, 10, 6001215,
+
"67243ab009a55fe0a848a15404233789b0b48ff11ae913f85e22957a7751c7e8");
+ }
+
+ private void verifyTable(SsbTable table, int partitions, long
expectedRows, String expectedDigest) throws Exception {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ long rows = 0;
+ for (int part = 1; part <= partitions; part++) {
+ RecordSet records = new SsbRecordSet(dbgen, new
SsbConnector.Table("sf1", table.tableName()),
+ new SsbConnector.Split(part, partitions),
IntStream.range(0, table.columns.size()).boxed().toList());
+ try (RecordCursor cursor = records.cursor()) {
+ while (cursor.advanceNextPosition()) {
+ StringBuilder row = new StringBuilder();
+ for (int i = 0; i < table.columns.size(); i++) {
+ row.append(cursor.getType(i).equals(BigintType.BIGINT)
+ ? Long.toString(cursor.getLong(i)) :
cursor.getSlice(i).toStringUtf8()).append('|');
+ }
+
digest.update(row.append('\n').toString().getBytes(StandardCharsets.US_ASCII));
+ rows++;
+ }
+ }
+ }
+ Assertions.assertEquals(expectedRows, rows, table.tableName());
+ Assertions.assertEquals(expectedDigest,
HexFormat.of().formatHex(digest.digest()), table.tableName());
+ }
+
+ @Test
+ void lineorderPartitionsAndLargeKeys() {
+ // The last partition starts after 90% of the orders. Exercise SF1000
without
+ // materializing billions of rows, including order keys beyond signed
INT.
+ try (RecordCursor cursor = new SsbRecordSet(dbgen, new
SsbConnector.Table("sf1000", "lineorder"),
+ new SsbConnector.Split(10, 10), List.of(0, 1, 2, 3,
4)).cursor()) {
+ Assertions.assertTrue(cursor.advanceNextPosition());
+ Assertions.assertEquals(5400000001L, cursor.getLong(0));
+ Assertions.assertEquals(1, cursor.getLong(1));
+ Assertions.assertTrue(cursor.getLong(2) <= 30000000);
+ Assertions.assertTrue(cursor.getLong(4) <= 2000000);
+ }
+ }
+
+ @Test
+ void nonzeroExitIsNotTreatedAsEndOfData() throws Exception {
+ Path failingGenerator = temporary.resolve("dbgen");
+ Files.writeString(failingGenerator, "#!/bin/sh\nexit 23\n");
+ Assertions.assertTrue(failingGenerator.toFile().setExecutable(true));
+ try (RecordCursor cursor = new SsbRecordSet(failingGenerator, new
SsbConnector.Table("sf1", "customer"),
+ new SsbConnector.Split(1, 1), List.of(0)).cursor()) {
+
Assertions.assertTrue(Assertions.assertThrows(IllegalStateException.class,
cursor::advanceNextPosition)
+ .getMessage().contains("23"));
+ }
+ }
+
+ @Test
+ void closingACursorStopsItsProducer() throws Exception {
+ var childPidsBefore =
ProcessHandle.current().children().map(ProcessHandle::pid).toList();
+ RecordCursor cursor = new SsbRecordSet(dbgen, new
SsbConnector.Table("sf1000", "lineorder"),
+ new SsbConnector.Split(1, 10), List.of(0)).cursor();
+ Assertions.assertTrue(cursor.advanceNextPosition());
+ var producer = ProcessHandle.current().children()
+ .filter(child ->
!childPidsBefore.contains(child.pid())).findFirst().orElseThrow();
+ cursor.close();
+ producer.onExit().get(5, TimeUnit.SECONDS);
+ Assertions.assertFalse(producer.isAlive());
+ cursor.close();
+ Assertions.assertFalse(cursor.advanceNextPosition());
+ }
+}
diff --git a/tools/tpcds-tools/.gitignore b/tools/tpcds-tools/.gitignore
new file mode 100644
index 00000000000..fbca2253799
--- /dev/null
+++ b/tools/tpcds-tools/.gitignore
@@ -0,0 +1 @@
+results/
diff --git a/tools/tpcds-tools/README.md b/tools/tpcds-tools/README.md
index 5c80a395bee..90b0f3b533d 100644
--- a/tools/tpcds-tools/README.md
+++ b/tools/tpcds-tools/README.md
@@ -17,6 +17,27 @@ specific language governing permissions and limitations
under the License.
-->
+# Run TPCDS with one command
+
+Install the Trino TPCDS generator on every FE and BE using the
+[shared installation
instructions](../benchmark/README.md#install-the-generators-once),
+then configure `conf/doris-cluster.conf` and run:
+
+```bash
+./bin/run-tpcds.sh -s 1 -d tpcds_sf1
+```
+
+This streams generated rows directly into Doris, creates the existing benchmark
+tables, collects statistics, and records three executions per query. It
supports
+SF1, SF100, SF1000, and SF10000. Preparation requires a new database.
+Use `--queries-only` to measure existing data again, with the same scale
factor.
+
+See [shared workflow options, result files, and timing
semantics](../benchmark/README.md).
+TPCDS uses the original scale-specific table DDL and multi-table queries.
+Install the plugin before running the command.
+
+# Original file-based workflow
+
## Usage
These scripts are used to make tpc-ds test.
diff --git a/tools/tpcds-tools/bin/run-tpcds.sh
b/tools/tpcds-tools/bin/run-tpcds.sh
new file mode 100755
index 00000000000..38e6ba69101
--- /dev/null
+++ b/tools/tpcds-tools/bin/run-tpcds.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+set -Eeuo pipefail
+SCRIPT_ROOT=$(cd "$(dirname "$0")" && pwd)
+exec bash "${SCRIPT_ROOT}/../../benchmark/run-benchmark.sh" tpcds "$@"
diff --git a/tools/tpch-tools/.gitignore b/tools/tpch-tools/.gitignore
new file mode 100644
index 00000000000..fbca2253799
--- /dev/null
+++ b/tools/tpch-tools/.gitignore
@@ -0,0 +1 @@
+results/
diff --git a/tools/tpch-tools/README.md b/tools/tpch-tools/README.md
index a7deac387ba..32a3ceb89f6 100644
--- a/tools/tpch-tools/README.md
+++ b/tools/tpch-tools/README.md
@@ -17,6 +17,27 @@ specific language governing permissions and limitations
under the License.
-->
+# Run TPCH with one command
+
+Install the Trino TPCH generator on every FE and BE using the
+[shared installation
instructions](../benchmark/README.md#install-the-generators-once),
+then configure `conf/doris-cluster.conf` and run:
+
+```bash
+./bin/run-tpch.sh -s 1 -d tpch_sf1
+```
+
+This streams generated rows directly into Doris, creates the existing benchmark
+tables, collects statistics, and records three executions per query. It
supports
+SF1, SF100, SF1000, and SF10000. Preparation requires a new database.
+Use `--queries-only` to measure existing data again, with the same scale
factor.
+
+See [shared workflow options, result files, and timing
semantics](../benchmark/README.md).
+TPCH uses the original scale-specific table DDL and multi-table queries.
+Install the plugin before running the command.
+
+# Original file-based workflow
+
## Usage
These scripts are used to make tpc-h test.
diff --git a/tools/tpch-tools/bin/run-tpch.sh b/tools/tpch-tools/bin/run-tpch.sh
new file mode 100755
index 00000000000..9f342c553cc
--- /dev/null
+++ b/tools/tpch-tools/bin/run-tpch.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+set -Eeuo pipefail
+SCRIPT_ROOT=$(cd "$(dirname "$0")" && pwd)
+exec bash "${SCRIPT_ROOT}/../../benchmark/run-benchmark.sh" tpch "$@"
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]